fix(js-core): use closest() fallback for nested click target matching (#7327)

Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
This commit is contained in:
bharath kumar
2026-02-27 12:24:58 +06:00
committed by GitHub
parent c6ebaea989
commit aecf85815a
2 changed files with 99 additions and 10 deletions
@@ -887,6 +887,7 @@ describe("utils.ts", () => {
targetElement.className = "other";
targetElement.matches = vi.fn(() => false);
targetElement.closest = vi.fn(() => null); // no ancestor matches either
const action: TEnvironmentStateActionClass = {
id: "clabc123abc",
@@ -993,13 +994,93 @@ describe("utils.ts", () => {
expect(result).toBe(true);
});
// --- Regression tests for nested child click target (issue #7314) ---
// In this test environment document.createElement() returns a plain mock object,
// so we set .matches and .closest as vi.fn() — the same pattern used by existing tests.
// This exercises the exact code path of the fix: matches() fails → closest() succeeds.
test("returns true when clicking a child element inside a button matched by cssSelector", () => {
const button = document.createElement("button");
const icon = document.createElement("span");
// Simulate: icon does NOT directly match ".my-btn", but its closest ancestor does
(icon as unknown as { matches: ReturnType<typeof vi.fn> }).matches = vi.fn(() => false);
(icon as unknown as { closest: ReturnType<typeof vi.fn> }).closest = vi.fn(() => button);
const action: TEnvironmentStateActionClass = {
id: "clabc123abc",
name: "Test Action",
type: "noCode",
key: null,
noCodeConfig: {
type: "click",
urlFilters: [],
elementSelector: { cssSelector: ".my-btn" },
},
};
// Before fix: matches() → false → returns false (bug)
// After fix: matches() → false → closest() → button → returns true (correct)
const result = evaluateNoCodeConfigClick(icon as unknown as HTMLElement, action);
expect(result).toBe(true);
});
test("returns false when clicking a child element with no matching ancestor", () => {
const other = document.createElement("div");
// Simulate: element doesn't match, and no ancestor matches either
(other as unknown as { matches: ReturnType<typeof vi.fn> }).matches = vi.fn(() => false);
(other as unknown as { closest: ReturnType<typeof vi.fn> }).closest = vi.fn(() => null);
const action: TEnvironmentStateActionClass = {
id: "clabc123abc",
name: "Test Action",
type: "noCode",
key: null,
noCodeConfig: {
type: "click",
urlFilters: [],
elementSelector: { cssSelector: ".my-btn" },
},
};
const result = evaluateNoCodeConfigClick(other as unknown as HTMLElement, action);
expect(result).toBe(false);
});
test("uses direct target (not closest) when target directly matches cssSelector", () => {
const button = document.createElement("button");
// Simulate: click on the button itself — matches() succeeds, closest() should NOT be called
(button as unknown as { matches: ReturnType<typeof vi.fn> }).matches = vi.fn(() => true);
const closestSpy = vi.fn();
(button as unknown as { closest: ReturnType<typeof vi.fn> }).closest = closestSpy;
const action: TEnvironmentStateActionClass = {
id: "clabc123abc",
name: "Test Action",
type: "noCode",
key: null,
noCodeConfig: {
type: "click",
urlFilters: [],
elementSelector: { cssSelector: ".my-btn" },
},
};
const result = evaluateNoCodeConfigClick(button as unknown as HTMLElement, action);
expect(result).toBe(true);
expect(closestSpy).not.toHaveBeenCalled(); // closest() is only a fallback
});
test("handles multiple cssSelectors correctly", () => {
const targetElement = document.createElement("div");
targetElement.className = "test other";
targetElement.matches = vi.fn((selector) => {
return selector === ".test" || selector === ".other";
return selector === ".test" || selector === ".other" || selector === ".test .other";
});
targetElement.closest = vi.fn(() => null); // not needed but consistent with mock environment
const action: TEnvironmentStateActionClass = {
id: "clabc123abc",
+17 -9
View File
@@ -304,20 +304,28 @@ export const evaluateNoCodeConfigClick = (
if (!innerHtml && !cssSelector) return false;
if (innerHtml && targetElement.innerHTML !== innerHtml) return false;
// Resolve the element to test: prefer the direct click target, but walk up to
// the nearest ancestor that matches the CSS selector (event delegation for nested markup,
// e.g. <svg> or <span> inside a <button class="my-btn">).
let matchedElement: HTMLElement = targetElement;
if (cssSelector) {
// Split selectors that start with a . or # including the . or #
const individualSelectors = cssSelector
.split(/(?=[.#])/) // split before each . or #
.map((sel) => sel.trim()); // remove leftover whitespace
for (const selector of individualSelectors) {
if (!targetElement.matches(selector)) {
return false;
}
let matchesDirectly = false;
try {
matchesDirectly = targetElement.matches(cssSelector);
} catch {
matchesDirectly = false;
}
if (!matchesDirectly) {
const ancestor = targetElement.closest(cssSelector);
if (!ancestor) return false;
matchedElement = ancestor as HTMLElement;
}
}
// Check innerHtml against the resolved element, not the raw click target
if (innerHtml && matchedElement.innerHTML !== innerHtml) return false;
const connector = action.noCodeConfig.urlFiltersConnector ?? "or";
const isValidUrl = handleUrlFilters(urlFilters, connector);