From aecf85815a6671a8c21b208f099e514a6363bfb5 Mon Sep 17 00:00:00 2001 From: bharath kumar <164022397+bharathkumar39293@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:24:58 +0600 Subject: [PATCH] fix(js-core): use closest() fallback for nested click target matching (#7327) Co-authored-by: Dhruwang --- .../src/lib/common/tests/utils.test.ts | 83 ++++++++++++++++++- packages/js-core/src/lib/common/utils.ts | 26 ++++-- 2 files changed, 99 insertions(+), 10 deletions(-) diff --git a/packages/js-core/src/lib/common/tests/utils.test.ts b/packages/js-core/src/lib/common/tests/utils.test.ts index 3c90c962a083..fc24d0d21302 100644 --- a/packages/js-core/src/lib/common/tests/utils.test.ts +++ b/packages/js-core/src/lib/common/tests/utils.test.ts @@ -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 }).matches = vi.fn(() => false); + (icon as unknown as { closest: ReturnType }).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 }).matches = vi.fn(() => false); + (other as unknown as { closest: ReturnType }).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 }).matches = vi.fn(() => true); + const closestSpy = vi.fn(); + (button as unknown as { closest: ReturnType }).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", diff --git a/packages/js-core/src/lib/common/utils.ts b/packages/js-core/src/lib/common/utils.ts index c2c082c59beb..f6dd081e8391 100644 --- a/packages/js-core/src/lib/common/utils.ts +++ b/packages/js-core/src/lib/common/utils.ts @@ -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. or inside a