From 06825069a2b7f018f11a904a372a018161e2db92 Mon Sep 17 00:00:00 2001 From: Douwe de Vries Date: Sun, 22 Feb 2026 13:44:11 +0100 Subject: [PATCH 1/5] Improve auto-record assertion reliability for dynamic nav flows --- src/app/services/record-service.test.ts | 1 + src/app/services/record-service.ts | 1 + .../assertion-candidates-snapshot.test.ts | 24 ++++++++ .../improve/assertion-candidates-snapshot.ts | 56 +++++++++++-------- src/core/improve/assertion-candidates.ts | 34 +---------- .../improve/navigation-like-interaction.ts | 43 ++++++++++++++ src/core/transform/selector-normalize.test.ts | 55 ++++++++++++++++++ src/core/transform/selector-normalize.ts | 13 +++++ 8 files changed, 171 insertions(+), 56 deletions(-) create mode 100644 src/core/improve/navigation-like-interaction.ts create mode 100644 src/core/transform/selector-normalize.test.ts diff --git a/src/app/services/record-service.test.ts b/src/app/services/record-service.test.ts index 47dde7f..650f02f 100644 --- a/src/app/services/record-service.test.ts +++ b/src/app/services/record-service.test.ts @@ -129,6 +129,7 @@ describe("runRecord auto-improve", () => { applySelectors: true, applyAssertions: true, assertions: "candidates", + assertionPolicy: "reliable", }); expect(ui.info).toHaveBeenCalledWith("Auto-improve: no changes needed"); }); diff --git a/src/app/services/record-service.ts b/src/app/services/record-service.ts index 8e48309..0c7bc39 100644 --- a/src/app/services/record-service.ts +++ b/src/app/services/record-service.ts @@ -137,6 +137,7 @@ export async function runRecord(opts: RecordCliOptions): Promise { applySelectors: true, applyAssertions: true, assertions: "candidates", + assertionPolicy: "reliable", }); const summary = improveResult.report.summary; diff --git a/src/core/improve/assertion-candidates-snapshot.test.ts b/src/core/improve/assertion-candidates-snapshot.test.ts index b0ce7d0..ee1d42f 100644 --- a/src/core/improve/assertion-candidates-snapshot.test.ts +++ b/src/core/improve/assertion-candidates-snapshot.test.ts @@ -117,6 +117,30 @@ describe("snapshot assertion candidates", () => { expect(out).toHaveLength(0); }); + it("suppresses snapshot text assertions for navigation-like dynamic clicks", () => { + const out = buildSnapshotAssertionCandidates([ + { + index: 1, + step: { + action: "click", + target: { + value: + "getByRole('link', { name: 'Nederlaag voor Trump: hooggerechtshof VS oordeelt dat heffingen onwettig zijn', exact: true })", + kind: "locatorExpression", + source: "manual", + }, + }, + preSnapshot: `- generic [ref=e1]:\n - link "Nieuws" [ref=e2]\n`, + postSnapshot: `- generic [ref=e1]:\n - heading "Ajax komt goed weg" [level=1] [ref=e3]\n`, + preUrl: "https://www.nu.nl/", + postUrl: "https://www.nu.nl/algemeen", + }, + ], "snapshot_native"); + + expect(out.some((candidate) => candidate.candidate.action === "assertText")).toBe(false); + expect(out.some((candidate) => candidate.candidate.action === "assertUrl")).toBe(true); + }); + it("generates multiple candidates from a rich delta", () => { const out = buildSnapshotAssertionCandidates([richDeltaStepSnapshot], "snapshot_native"); diff --git a/src/core/improve/assertion-candidates-snapshot.ts b/src/core/improve/assertion-candidates-snapshot.ts index 9274677..b951326 100644 --- a/src/core/improve/assertion-candidates-snapshot.ts +++ b/src/core/improve/assertion-candidates-snapshot.ts @@ -23,6 +23,7 @@ import { normalizeForCompare, } from "./assertion-candidates-snapshot-shared.js"; import { parseSnapshotNodes } from "./assertion-candidates-snapshot-parser.js"; +import { classifyNavigationLikeInteraction } from "./navigation-like-interaction.js"; export interface StepSnapshot { index: number; @@ -47,14 +48,19 @@ export function buildSnapshotAssertionCandidates( const delta = buildDeltaNodes(preNodes, postNodes); const actedTargetHint = extractActedTargetHint(snapshot.step); - const framePath = + const stepTarget = snapshot.step.action !== "navigate" && snapshot.step.action !== "assertUrl" && snapshot.step.action !== "assertTitle" && "target" in snapshot.step && snapshot.step.target - ? snapshot.step.target.framePath + ? snapshot.step.target : undefined; + const suppressTextCandidates = stepTarget + ? Boolean(classifyNavigationLikeInteraction(snapshot.step, stepTarget)) + : false; + const framePath = + stepTarget ? stepTarget.framePath : undefined; if (snapshot.step.action === "click") { const stableNodes = buildStableNodes(preNodes, postNodes); @@ -89,18 +95,20 @@ export function buildSnapshotAssertionCandidates( ) ); - candidates.push( - ...buildTextChangedCandidates( - snapshot.index, - snapshot.step.action, - preNodes, - postNodes, - actedTargetHint, - framePath, - candidateSource, - MAX_TEXT_CANDIDATES_PER_STEP - ) - ); + if (!suppressTextCandidates) { + candidates.push( + ...buildTextChangedCandidates( + snapshot.index, + snapshot.step.action, + preNodes, + postNodes, + actedTargetHint, + framePath, + candidateSource, + MAX_TEXT_CANDIDATES_PER_STEP + ) + ); + } candidates.push( ...buildStateChangeCandidates( @@ -116,15 +124,17 @@ export function buildSnapshotAssertionCandidates( if (delta.length === 0) continue; - const textCandidates = buildTextCandidates( - snapshot.index, - snapshot.step.action, - delta, - actedTargetHint, - framePath, - candidateSource, - MAX_TEXT_CANDIDATES_PER_STEP - ); + const textCandidates = suppressTextCandidates + ? [] + : buildTextCandidates( + snapshot.index, + snapshot.step.action, + delta, + actedTargetHint, + framePath, + candidateSource, + MAX_TEXT_CANDIDATES_PER_STEP + ); candidates.push(...textCandidates); const textTargetValues = new Set( diff --git a/src/core/improve/assertion-candidates.ts b/src/core/improve/assertion-candidates.ts index 11095e3..8039462 100644 --- a/src/core/improve/assertion-candidates.ts +++ b/src/core/improve/assertion-candidates.ts @@ -1,9 +1,6 @@ import type { AssertionCandidate, StepFinding } from "./report-schema.js"; import type { Step, Target } from "../yaml-schema.js"; -import { - assessTargetDynamics, - extractTargetTextFragments, -} from "./dynamic-target.js"; +import { classifyNavigationLikeInteraction } from "./navigation-like-interaction.js"; const COVERAGE_FALLBACK_CONFIDENCE = 0.76; @@ -123,32 +120,3 @@ function clamp01(value: number): number { if (value > 1) return 1; return value; } - -function classifyNavigationLikeInteraction(step: Step, target: Target): string | undefined { - if (step.action !== "click" && step.action !== "press" && step.action !== "hover") { - return undefined; - } - - const targetValue = target.value; - const isRoleLink = /getByRole\(\s*['"]link['"]/.test(targetValue); - const hasExact = /exact\s*:\s*true/.test(targetValue); - const hasContentCardPattern = - /headline|teaser|article|story|content[-_ ]?card|breaking[-_ ]?push|hero[-_ ]?card/i.test( - targetValue - ); - - const { dynamicSignals } = assessTargetDynamics(target); - const queryTexts = extractTargetTextFragments(target); - const hasHeadlineLikeText = - queryTexts.some((text) => text.length >= 48) || - dynamicSignals.includes("contains_headline_like_text") || - dynamicSignals.includes("contains_weather_or_news_fragment") || - dynamicSignals.includes("contains_pipe_separator") || - dynamicSignals.includes("contains_date_or_time_fragment"); - - if ((isRoleLink && hasHeadlineLikeText) || (isRoleLink && hasExact) || hasContentCardPattern) { - return "navigation-like dynamic click target"; - } - - return undefined; -} diff --git a/src/core/improve/navigation-like-interaction.ts b/src/core/improve/navigation-like-interaction.ts new file mode 100644 index 0000000..3956f61 --- /dev/null +++ b/src/core/improve/navigation-like-interaction.ts @@ -0,0 +1,43 @@ +import type { Step, Target } from "../yaml-schema.js"; +import { + assessTargetDynamics, + extractTargetTextFragments, +} from "./dynamic-target.js"; + +function isNavigationLikeAction(action: Step["action"]): boolean { + return action === "click" || action === "press" || action === "hover"; +} + +export function classifyNavigationLikeInteraction( + step: Step, + target: Target +): string | undefined { + if (!isNavigationLikeAction(step.action)) return undefined; + + const targetValue = target.value; + const isRoleLink = /getByRole\(\s*['"]link['"]/.test(targetValue); + const hasExact = /exact\s*:\s*true/.test(targetValue); + const hasContentCardPattern = + /headline|teaser|article|story|content[-_ ]?card|breaking[-_ ]?push|hero[-_ ]?card/i.test( + targetValue + ); + + const { dynamicSignals } = assessTargetDynamics(target); + const queryTexts = extractTargetTextFragments(target); + const hasHeadlineLikeText = + queryTexts.some((text) => text.length >= 48) || + dynamicSignals.includes("contains_headline_like_text") || + dynamicSignals.includes("contains_weather_or_news_fragment") || + dynamicSignals.includes("contains_pipe_separator") || + dynamicSignals.includes("contains_date_or_time_fragment"); + + if ( + (isRoleLink && hasHeadlineLikeText) || + (isRoleLink && hasExact) || + hasContentCardPattern + ) { + return "navigation-like dynamic click target"; + } + + return undefined; +} diff --git a/src/core/transform/selector-normalize.test.ts b/src/core/transform/selector-normalize.test.ts new file mode 100644 index 0000000..6666304 --- /dev/null +++ b/src/core/transform/selector-normalize.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { locatorNodeToExpression } from "./selector-normalize.js"; + +describe("locatorNodeToExpression dynamic exact normalization", () => { + it("drops exact for long headline-like role names when enabled", () => { + const expression = locatorNodeToExpression( + { + kind: "role", + body: "link", + options: { + name: "Nederlaag voor Trump: hooggerechtshof VS oordeelt dat heffingen onwettig zijn", + exact: true, + }, + }, + 0, + { dropDynamicExact: true } + ); + + expect(expression).toBe( + "getByRole('link', { name: 'Nederlaag voor Trump: hooggerechtshof VS oordeelt dat heffingen onwettig zijn' })" + ); + expect(expression).not.toContain("exact: true"); + }); + + it("drops exact for headline-like text with time fragments", () => { + const expression = locatorNodeToExpression( + { + kind: "text", + body: "Winterweer update Schiphol 12:30, alle vluchten vertraagd", + options: { exact: true }, + }, + 0, + { dropDynamicExact: true } + ); + + expect(expression).toBe( + "getByText('Winterweer update Schiphol 12:30, alle vluchten vertraagd')" + ); + expect(expression).not.toContain("exact: true"); + }); + + it("keeps exact for short stable text", () => { + const expression = locatorNodeToExpression( + { + kind: "role", + body: "link", + options: { name: "Algemeen", exact: true }, + }, + 0, + { dropDynamicExact: true } + ); + + expect(expression).toBe("getByRole('link', { name: 'Algemeen', exact: true })"); + }); +}); diff --git a/src/core/transform/selector-normalize.ts b/src/core/transform/selector-normalize.ts index 0cf524e..439c923 100644 --- a/src/core/transform/selector-normalize.ts +++ b/src/core/transform/selector-normalize.ts @@ -256,6 +256,19 @@ function shouldDropExactForDynamicText(text: string, enabled: boolean): boolean ); const hasNumericSignal = dynamicSignals.includes("contains_numeric_fragment"); const hasHeadlineSignal = dynamicSignals.includes("contains_headline_like_text"); + const hasPipeSeparatorSignal = dynamicSignals.includes("contains_pipe_separator"); + + // Long and headline-like text tends to churn frequently on news pages. + if (hasHeadlineSignal && (normalized.length >= 48 || hasPipeSeparatorSignal)) { + return true; + } + + if ( + hasHeadlineSignal && + (hasWeatherOrNewsSignal || hasDateOrTimeSignal || hasNumericSignal) + ) { + return true; + } const hasDynamicNumericSignal = hasNumericSignal && hasHeadlineSignal; const strongSignalCount = [ From 6ba650fbecd41426f7642923afc40d62d182d133 Mon Sep 17 00:00:00 2001 From: Douwe de Vries Date: Sun, 22 Feb 2026 13:47:39 +0100 Subject: [PATCH 2/5] Avoid content-coupled snapshot assertions for dynamic navigation clicks --- .../assertion-candidates-snapshot.test.ts | 3 ++ .../improve/assertion-candidates-snapshot.ts | 50 ++++++++++--------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/core/improve/assertion-candidates-snapshot.test.ts b/src/core/improve/assertion-candidates-snapshot.test.ts index ee1d42f..dd8541f 100644 --- a/src/core/improve/assertion-candidates-snapshot.test.ts +++ b/src/core/improve/assertion-candidates-snapshot.test.ts @@ -138,7 +138,10 @@ describe("snapshot assertion candidates", () => { ], "snapshot_native"); expect(out.some((candidate) => candidate.candidate.action === "assertText")).toBe(false); + expect(out.some((candidate) => candidate.candidate.action === "assertVisible")).toBe(false); + expect(out.some((candidate) => candidate.candidate.action === "assertEnabled")).toBe(false); expect(out.some((candidate) => candidate.candidate.action === "assertUrl")).toBe(true); + expect(out).toHaveLength(1); }); it("generates multiple candidates from a rich delta", () => { diff --git a/src/core/improve/assertion-candidates-snapshot.ts b/src/core/improve/assertion-candidates-snapshot.ts index b951326..c081d61 100644 --- a/src/core/improve/assertion-candidates-snapshot.ts +++ b/src/core/improve/assertion-candidates-snapshot.ts @@ -56,7 +56,7 @@ export function buildSnapshotAssertionCandidates( snapshot.step.target ? snapshot.step.target : undefined; - const suppressTextCandidates = stepTarget + const suppressContentCandidates = stepTarget ? Boolean(classifyNavigationLikeInteraction(snapshot.step, stepTarget)) : false; const framePath = @@ -95,7 +95,7 @@ export function buildSnapshotAssertionCandidates( ) ); - if (!suppressTextCandidates) { + if (!suppressContentCandidates) { candidates.push( ...buildTextChangedCandidates( snapshot.index, @@ -110,21 +110,23 @@ export function buildSnapshotAssertionCandidates( ); } - candidates.push( - ...buildStateChangeCandidates( - snapshot.index, - snapshot.step.action, - preNodes, - postNodes, - actedTargetHint, - framePath, - candidateSource - ) - ); + if (!suppressContentCandidates) { + candidates.push( + ...buildStateChangeCandidates( + snapshot.index, + snapshot.step.action, + preNodes, + postNodes, + actedTargetHint, + framePath, + candidateSource + ) + ); + } if (delta.length === 0) continue; - const textCandidates = suppressTextCandidates + const textCandidates = suppressContentCandidates ? [] : buildTextCandidates( snapshot.index, @@ -147,15 +149,17 @@ export function buildSnapshotAssertionCandidates( ) ); - const visibleCandidates = buildVisibleCandidates( - snapshot.index, - snapshot.step.action, - delta, - actedTargetHint, - framePath, - candidateSource, - MAX_VISIBLE_CANDIDATES_PER_STEP - ); + const visibleCandidates = suppressContentCandidates + ? [] + : buildVisibleCandidates( + snapshot.index, + snapshot.step.action, + delta, + actedTargetHint, + framePath, + candidateSource, + MAX_VISIBLE_CANDIDATES_PER_STEP + ); for (const visibleCandidate of visibleCandidates) { const visibleTarget = From 4c2df0e231c33a599e0a8a58333c2fd90659663f Mon Sep 17 00:00:00 2001 From: Douwe de Vries Date: Sun, 22 Feb 2026 13:57:04 +0100 Subject: [PATCH 3/5] Use neutral fixture values in snapshot suppression test --- .../improve/assertion-candidates-snapshot.test.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/core/improve/assertion-candidates-snapshot.test.ts b/src/core/improve/assertion-candidates-snapshot.test.ts index dd8541f..002f1cd 100644 --- a/src/core/improve/assertion-candidates-snapshot.test.ts +++ b/src/core/improve/assertion-candidates-snapshot.test.ts @@ -118,22 +118,25 @@ describe("snapshot assertion candidates", () => { }); it("suppresses snapshot text assertions for navigation-like dynamic clicks", () => { + const dynamicLinkText = + "Live update 12:30: Market story shifts quickly after morning session"; + const preUrl = "https://example.test/home"; + const postUrl = "https://example.test/category"; const out = buildSnapshotAssertionCandidates([ { index: 1, step: { action: "click", target: { - value: - "getByRole('link', { name: 'Nederlaag voor Trump: hooggerechtshof VS oordeelt dat heffingen onwettig zijn', exact: true })", + value: `getByRole('link', { name: '${dynamicLinkText}', exact: true })`, kind: "locatorExpression", source: "manual", }, }, - preSnapshot: `- generic [ref=e1]:\n - link "Nieuws" [ref=e2]\n`, - postSnapshot: `- generic [ref=e1]:\n - heading "Ajax komt goed weg" [level=1] [ref=e3]\n`, - preUrl: "https://www.nu.nl/", - postUrl: "https://www.nu.nl/algemeen", + preSnapshot: `- generic [ref=e1]:\n - link "Top story" [ref=e2]\n`, + postSnapshot: `- generic [ref=e1]:\n - heading "Category page" [level=1] [ref=e3]\n`, + preUrl, + postUrl, }, ], "snapshot_native"); From a0bab604cea5930d8d675ebe00fa47ff648b6b8f Mon Sep 17 00:00:00 2001 From: Douwe de Vries Date: Sun, 22 Feb 2026 14:00:11 +0100 Subject: [PATCH 4/5] Narrow nav-like suppression to dynamic links and add regressions --- .../assertion-candidates-snapshot.test.ts | 21 +++++++++++++++++++ src/core/improve/assertion-candidates.test.ts | 20 ++++++++++++++++++ .../improve/navigation-like-interaction.ts | 7 +------ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/core/improve/assertion-candidates-snapshot.test.ts b/src/core/improve/assertion-candidates-snapshot.test.ts index 002f1cd..ebd2efe 100644 --- a/src/core/improve/assertion-candidates-snapshot.test.ts +++ b/src/core/improve/assertion-candidates-snapshot.test.ts @@ -147,6 +147,27 @@ describe("snapshot assertion candidates", () => { expect(out).toHaveLength(1); }); + it("keeps snapshot content assertions for stable exact link clicks", () => { + const out = buildSnapshotAssertionCandidates([ + { + index: 0, + step: { + action: "click", + target: { + value: "getByRole('link', { name: 'Settings', exact: true })", + kind: "locatorExpression", + source: "manual", + }, + }, + preSnapshot: "- generic [ref=e1]:\n", + postSnapshot: + "- generic [ref=e1]:\n - heading \"Account settings\" [level=1] [ref=e2]\n", + }, + ], "snapshot_native"); + + expect(out.some((candidate) => candidate.candidate.action === "assertText")).toBe(true); + }); + it("generates multiple candidates from a rich delta", () => { const out = buildSnapshotAssertionCandidates([richDeltaStepSnapshot], "snapshot_native"); diff --git a/src/core/improve/assertion-candidates.test.ts b/src/core/improve/assertion-candidates.test.ts index 55ea1e8..0b24bdf 100644 --- a/src/core/improve/assertion-candidates.test.ts +++ b/src/core/improve/assertion-candidates.test.ts @@ -201,4 +201,24 @@ describe("buildAssertionCandidates", () => { "navigation-like dynamic click target" ); }); + + it("keeps deterministic fallback for stable exact link clicks", () => { + const out = buildAssertionCandidates( + [ + { + action: "click", + target: { + value: "getByRole('link', { name: 'Settings', exact: true })", + kind: "locatorExpression", + source: "manual", + }, + }, + ], + [] + ); + + expect(out.skippedNavigationLikeClicks).toHaveLength(0); + expect(out.candidates).toHaveLength(1); + expect(out.candidates[0]?.candidate.action).toBe("assertVisible"); + }); }); diff --git a/src/core/improve/navigation-like-interaction.ts b/src/core/improve/navigation-like-interaction.ts index 3956f61..d0be956 100644 --- a/src/core/improve/navigation-like-interaction.ts +++ b/src/core/improve/navigation-like-interaction.ts @@ -16,7 +16,6 @@ export function classifyNavigationLikeInteraction( const targetValue = target.value; const isRoleLink = /getByRole\(\s*['"]link['"]/.test(targetValue); - const hasExact = /exact\s*:\s*true/.test(targetValue); const hasContentCardPattern = /headline|teaser|article|story|content[-_ ]?card|breaking[-_ ]?push|hero[-_ ]?card/i.test( targetValue @@ -31,11 +30,7 @@ export function classifyNavigationLikeInteraction( dynamicSignals.includes("contains_pipe_separator") || dynamicSignals.includes("contains_date_or_time_fragment"); - if ( - (isRoleLink && hasHeadlineLikeText) || - (isRoleLink && hasExact) || - hasContentCardPattern - ) { + if ((isRoleLink && hasHeadlineLikeText) || hasContentCardPattern) { return "navigation-like dynamic click target"; } From 9a16e1d9ef6455c6126d21a568baee68e5c01984 Mon Sep 17 00:00:00 2001 From: Douwe de Vries Date: Sun, 22 Feb 2026 14:11:34 +0100 Subject: [PATCH 5/5] Narrow content-card heuristic to extracted text fragments --- .../assertion-candidates-snapshot.test.ts | 21 +++++++++++++++++++ src/core/improve/assertion-candidates.test.ts | 20 ++++++++++++++++++ .../improve/navigation-like-interaction.ts | 13 ++++++------ 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/core/improve/assertion-candidates-snapshot.test.ts b/src/core/improve/assertion-candidates-snapshot.test.ts index ebd2efe..c7f5897 100644 --- a/src/core/improve/assertion-candidates-snapshot.test.ts +++ b/src/core/improve/assertion-candidates-snapshot.test.ts @@ -168,6 +168,27 @@ describe("snapshot assertion candidates", () => { expect(out.some((candidate) => candidate.candidate.action === "assertText")).toBe(true); }); + it("keeps snapshot content assertions for stable selectors with story/article ids", () => { + const out = buildSnapshotAssertionCandidates([ + { + index: 0, + step: { + action: "click", + target: { + value: "locator('#user-story-tab')", + kind: "locatorExpression", + source: "manual", + }, + }, + preSnapshot: "- generic [ref=e1]:\n", + postSnapshot: + "- generic [ref=e1]:\n - heading \"Profile details\" [level=1] [ref=e2]\n", + }, + ], "snapshot_native"); + + expect(out.some((candidate) => candidate.candidate.action === "assertText")).toBe(true); + }); + it("generates multiple candidates from a rich delta", () => { const out = buildSnapshotAssertionCandidates([richDeltaStepSnapshot], "snapshot_native"); diff --git a/src/core/improve/assertion-candidates.test.ts b/src/core/improve/assertion-candidates.test.ts index 0b24bdf..fd0ff64 100644 --- a/src/core/improve/assertion-candidates.test.ts +++ b/src/core/improve/assertion-candidates.test.ts @@ -221,4 +221,24 @@ describe("buildAssertionCandidates", () => { expect(out.candidates).toHaveLength(1); expect(out.candidates[0]?.candidate.action).toBe("assertVisible"); }); + + it("does not skip stable selectors with story/article in id-like values", () => { + const out = buildAssertionCandidates( + [ + { + action: "click", + target: { + value: "locator('#user-story-tab')", + kind: "locatorExpression", + source: "manual", + }, + }, + ], + [] + ); + + expect(out.skippedNavigationLikeClicks).toHaveLength(0); + expect(out.candidates).toHaveLength(1); + expect(out.candidates[0]?.candidate.action).toBe("assertVisible"); + }); }); diff --git a/src/core/improve/navigation-like-interaction.ts b/src/core/improve/navigation-like-interaction.ts index d0be956..7d7930a 100644 --- a/src/core/improve/navigation-like-interaction.ts +++ b/src/core/improve/navigation-like-interaction.ts @@ -8,6 +8,9 @@ function isNavigationLikeAction(action: Step["action"]): boolean { return action === "click" || action === "press" || action === "hover"; } +const CONTENT_CARD_TEXT_PATTERN = + /headline|teaser|article|story|content[-_ ]?card|breaking[-_ ]?push|hero[-_ ]?card/i; + export function classifyNavigationLikeInteraction( step: Step, target: Target @@ -16,13 +19,11 @@ export function classifyNavigationLikeInteraction( const targetValue = target.value; const isRoleLink = /getByRole\(\s*['"]link['"]/.test(targetValue); - const hasContentCardPattern = - /headline|teaser|article|story|content[-_ ]?card|breaking[-_ ]?push|hero[-_ ]?card/i.test( - targetValue - ); - - const { dynamicSignals } = assessTargetDynamics(target); const queryTexts = extractTargetTextFragments(target); + const hasContentCardPattern = queryTexts.some((text) => + CONTENT_CARD_TEXT_PATTERN.test(text) + ); + const { dynamicSignals } = assessTargetDynamics(target); const hasHeadlineLikeText = queryTexts.some((text) => text.length >= 48) || dynamicSignals.includes("contains_headline_like_text") ||