mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-21 19:39:28 -05:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ddeef4096f | |||
| 9fe4678c47 | |||
| 49acc1cbb8 | |||
| e29300df2c |
@@ -887,6 +887,7 @@ describe("utils.ts", () => {
|
|||||||
targetElement.className = "other";
|
targetElement.className = "other";
|
||||||
|
|
||||||
targetElement.matches = vi.fn(() => false);
|
targetElement.matches = vi.fn(() => false);
|
||||||
|
targetElement.closest = vi.fn(() => null); // no ancestor matches either
|
||||||
|
|
||||||
const action: TEnvironmentStateActionClass = {
|
const action: TEnvironmentStateActionClass = {
|
||||||
id: "clabc123abc",
|
id: "clabc123abc",
|
||||||
@@ -993,13 +994,93 @@ describe("utils.ts", () => {
|
|||||||
expect(result).toBe(true);
|
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", () => {
|
test("handles multiple cssSelectors correctly", () => {
|
||||||
const targetElement = document.createElement("div");
|
const targetElement = document.createElement("div");
|
||||||
targetElement.className = "test other";
|
targetElement.className = "test other";
|
||||||
|
|
||||||
targetElement.matches = vi.fn((selector) => {
|
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 = {
|
const action: TEnvironmentStateActionClass = {
|
||||||
id: "clabc123abc",
|
id: "clabc123abc",
|
||||||
|
|||||||
@@ -304,20 +304,28 @@ export const evaluateNoCodeConfigClick = (
|
|||||||
|
|
||||||
if (!innerHtml && !cssSelector) return false;
|
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) {
|
if (cssSelector) {
|
||||||
// Split selectors that start with a . or # including the . or #
|
let matchesDirectly = false;
|
||||||
const individualSelectors = cssSelector
|
try {
|
||||||
.split(/(?=[.#])/) // split before each . or #
|
matchesDirectly = targetElement.matches(cssSelector);
|
||||||
.map((sel) => sel.trim()); // remove leftover whitespace
|
} catch {
|
||||||
for (const selector of individualSelectors) {
|
matchesDirectly = false;
|
||||||
if (!targetElement.matches(selector)) {
|
}
|
||||||
return 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 connector = action.noCodeConfig.urlFiltersConnector ?? "or";
|
||||||
const isValidUrl = handleUrlFilters(urlFilters, connector);
|
const isValidUrl = handleUrlFilters(urlFilters, connector);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user