@sudobility/testomniac_runner_service 0.1.52 → 0.1.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/analyzer/page-analyzer/generators/content.d.ts +3 -0
  2. package/dist/analyzer/page-analyzer/generators/content.d.ts.map +1 -0
  3. package/dist/analyzer/page-analyzer/generators/content.js +34 -0
  4. package/dist/analyzer/page-analyzer/generators/content.js.map +1 -0
  5. package/dist/analyzer/page-analyzer/generators/dialogs.d.ts +3 -0
  6. package/dist/analyzer/page-analyzer/generators/dialogs.d.ts.map +1 -0
  7. package/dist/analyzer/page-analyzer/generators/dialogs.js +38 -0
  8. package/dist/analyzer/page-analyzer/generators/dialogs.js.map +1 -0
  9. package/dist/analyzer/page-analyzer/generators/e2e.d.ts +3 -0
  10. package/dist/analyzer/page-analyzer/generators/e2e.d.ts.map +1 -0
  11. package/dist/analyzer/page-analyzer/generators/e2e.js +28 -0
  12. package/dist/analyzer/page-analyzer/generators/e2e.js.map +1 -0
  13. package/dist/analyzer/page-analyzer/generators/forms.d.ts +3 -0
  14. package/dist/analyzer/page-analyzer/generators/forms.d.ts.map +1 -0
  15. package/dist/analyzer/page-analyzer/generators/forms.js +69 -0
  16. package/dist/analyzer/page-analyzer/generators/forms.js.map +1 -0
  17. package/dist/analyzer/page-analyzer/generators/hover-follow-up.d.ts +4 -0
  18. package/dist/analyzer/page-analyzer/generators/hover-follow-up.d.ts.map +1 -0
  19. package/dist/analyzer/page-analyzer/generators/hover-follow-up.js +34 -0
  20. package/dist/analyzer/page-analyzer/generators/hover-follow-up.js.map +1 -0
  21. package/dist/analyzer/page-analyzer/generators/keyboard-disclosure.d.ts +3 -0
  22. package/dist/analyzer/page-analyzer/generators/keyboard-disclosure.d.ts.map +1 -0
  23. package/dist/analyzer/page-analyzer/generators/keyboard-disclosure.js +30 -0
  24. package/dist/analyzer/page-analyzer/generators/keyboard-disclosure.js.map +1 -0
  25. package/dist/analyzer/page-analyzer/generators/navigation.d.ts +3 -0
  26. package/dist/analyzer/page-analyzer/generators/navigation.d.ts.map +1 -0
  27. package/dist/analyzer/page-analyzer/generators/navigation.js +22 -0
  28. package/dist/analyzer/page-analyzer/generators/navigation.js.map +1 -0
  29. package/dist/analyzer/page-analyzer/generators/render.d.ts +3 -0
  30. package/dist/analyzer/page-analyzer/generators/render.d.ts.map +1 -0
  31. package/dist/analyzer/page-analyzer/generators/render.js +26 -0
  32. package/dist/analyzer/page-analyzer/generators/render.js.map +1 -0
  33. package/dist/analyzer/page-analyzer/generators/scaffolds.d.ts +3 -0
  34. package/dist/analyzer/page-analyzer/generators/scaffolds.d.ts.map +1 -0
  35. package/dist/analyzer/page-analyzer/generators/scaffolds.js +36 -0
  36. package/dist/analyzer/page-analyzer/generators/scaffolds.js.map +1 -0
  37. package/dist/analyzer/page-analyzer/generators/semantic-journeys.d.ts +3 -0
  38. package/dist/analyzer/page-analyzer/generators/semantic-journeys.d.ts.map +1 -0
  39. package/dist/analyzer/page-analyzer/generators/semantic-journeys.js +30 -0
  40. package/dist/analyzer/page-analyzer/generators/semantic-journeys.js.map +1 -0
  41. package/dist/analyzer/page-analyzer/generators/variants.d.ts +3 -0
  42. package/dist/analyzer/page-analyzer/generators/variants.d.ts.map +1 -0
  43. package/dist/analyzer/page-analyzer/generators/variants.js +30 -0
  44. package/dist/analyzer/page-analyzer/generators/variants.js.map +1 -0
  45. package/dist/analyzer/{page-analyzer.d.ts → page-analyzer/index.d.ts} +9 -39
  46. package/dist/analyzer/page-analyzer/index.d.ts.map +1 -0
  47. package/dist/analyzer/{page-analyzer.js → page-analyzer/index.js} +118 -398
  48. package/dist/analyzer/page-analyzer/index.js.map +1 -0
  49. package/dist/analyzer/page-analyzer/types.d.ts +28 -0
  50. package/dist/analyzer/page-analyzer/types.d.ts.map +1 -0
  51. package/dist/analyzer/page-analyzer/types.js +1 -0
  52. package/dist/analyzer/page-analyzer/types.js.map +1 -0
  53. package/dist/browser/dom-snapshot.d.ts.map +1 -1
  54. package/dist/browser/dom-snapshot.js +84 -0
  55. package/dist/browser/dom-snapshot.js.map +1 -1
  56. package/dist/extractors/helpers.d.ts.map +1 -1
  57. package/dist/extractors/helpers.js +3 -0
  58. package/dist/extractors/helpers.js.map +1 -1
  59. package/dist/orchestrator/runner.d.ts.map +1 -1
  60. package/dist/orchestrator/runner.js +19 -5
  61. package/dist/orchestrator/runner.js.map +1 -1
  62. package/dist/orchestrator/types.d.ts +2 -0
  63. package/dist/orchestrator/types.d.ts.map +1 -1
  64. package/package.json +1 -1
  65. package/dist/analyzer/page-analyzer.d.ts.map +0 -1
  66. package/dist/analyzer/page-analyzer.js.map +0 -1
@@ -1,30 +1,6 @@
1
- import type { TestElement, Expectation, ActionableItem, SizeClass, TestSurfaceResponse, TestSurfaceBundleRunResponse, FormInfo, TestStep } from "@sudobility/testomniac_types";
2
- import type { ApiClient } from "../api/client";
3
- import type { DetectedScaffoldRegion } from "../scanner/component-detector";
4
- import type { ScanEventHandler } from "../orchestrator/types";
5
- export interface AnalyzerContext {
6
- runnerId: number;
7
- sizeClass: SizeClass;
8
- uid?: string;
9
- currentTestElementId: number;
10
- currentTestSurfaceId: number;
11
- currentSurfaceRunId: number | null;
12
- html: string;
13
- currentPageStateId: number;
14
- beginningPageStateId: number;
15
- currentPath: string;
16
- pageId: number;
17
- pageRequiresLogin: boolean;
18
- scaffolds: DetectedScaffoldRegion[];
19
- scaffoldSelectorByItemSelector: Record<string, string>;
20
- actionableItems: ActionableItem[];
21
- forms: FormInfo[];
22
- journeySteps: TestStep[];
23
- navigationSurface: TestSurfaceResponse;
24
- bundleRun: TestSurfaceBundleRunResponse;
25
- api: ApiClient;
26
- events: ScanEventHandler;
27
- }
1
+ import type { TestElement, Expectation } from "@sudobility/testomniac_types";
2
+ import type { AnalyzerContext } from "./types";
3
+ export type { AnalyzerContext } from "./types";
28
4
  /**
29
5
  * PageAnalyzer generates expectations and discovers new test elements
30
6
  * during discovery mode.
@@ -40,17 +16,7 @@ export declare class PageAnalyzer {
40
16
  * Called AFTER expertises evaluate and the target page state is established.
41
17
  */
42
18
  generateTestElements(testElement: TestElement, context: AnalyzerContext): Promise<void>;
43
- private generateHoverFollowUpCases;
44
- private generateNavigationTestElements;
45
- private generateScaffoldTestElements;
46
- private generateRenderTestElements;
47
- private generateFormTestElements;
48
- private generateE2ETestElements;
49
- private generateSemanticJourneyTestElements;
50
- private generateDialogLifecycleTestElements;
51
- private generateKeyboardAndDisclosureTestElements;
52
- private generateVariantTestElements;
53
- private generateContentTestElements;
19
+ private getScaffoldSurfaceItems;
54
20
  private ensureSurfaceRun;
55
21
  private isMouseActionable;
56
22
  private isSurfaceCandidate;
@@ -113,6 +79,10 @@ export declare class PageAnalyzer {
113
79
  private hasDisabledAppearanceSignal;
114
80
  private describeActionableItem;
115
81
  private semanticText;
82
+ private selectRepresentativeItems;
83
+ private representativeActionStyle;
84
+ private pickRepresentativeItem;
85
+ private representativePriority;
116
86
  private isAddToCartItem;
117
87
  private isCheckoutItem;
118
88
  private isRemoveItemAction;
@@ -148,4 +118,4 @@ export declare class PageAnalyzer {
148
118
  private detectPasswordRequirements;
149
119
  private generatePasswordVariants;
150
120
  }
151
- //# sourceMappingURL=page-analyzer.d.ts.map
121
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/analyzer/page-analyzer/index.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,19 @@
1
1
  import { PlaywrightAction, ExpectationType, ExpectationSeverity, } from "@sudobility/testomniac_types";
2
- import { computeHashes } from "../browser/page-utils";
2
+ import { computeHashes } from "../../browser/page-utils";
3
3
  import { createHash } from "node:crypto";
4
- import { fillValuePlanner } from "../planners/fill-value-planner";
5
- import { AUTH_URL_PATTERNS, SIGNUP_URL_PATTERNS } from "../config/constants";
4
+ import { fillValuePlanner } from "../../planners/fill-value-planner";
5
+ import { AUTH_URL_PATTERNS, SIGNUP_URL_PATTERNS } from "../../config/constants";
6
+ import { generateHoverFollowUpCases } from "./generators/hover-follow-up";
7
+ import { generateNavigationTestElements } from "./generators/navigation";
8
+ import { generateScaffoldTestElements } from "./generators/scaffolds";
9
+ import { generateRenderTestElements } from "./generators/render";
10
+ import { generateFormTestElements } from "./generators/forms";
11
+ import { generateE2ETestElements } from "./generators/e2e";
12
+ import { generateSemanticJourneyTestElements } from "./generators/semantic-journeys";
13
+ import { generateDialogLifecycleTestElements } from "./generators/dialogs";
14
+ import { generateKeyboardAndDisclosureTestElements } from "./generators/keyboard-disclosure";
15
+ import { generateVariantTestElements } from "./generators/variants";
16
+ import { generateContentTestElements } from "./generators/content";
6
17
  /**
7
18
  * PageAnalyzer generates expectations and discovers new test elements
8
19
  * during discovery mode.
@@ -51,401 +62,28 @@ export class PageAnalyzer {
51
62
  currentPageStateId,
52
63
  };
53
64
  if (this.isHoverOnly(testElement)) {
54
- await this.generateHoverFollowUpCases(testElement, resolvedContext);
65
+ await generateHoverFollowUpCases(this, testElement, resolvedContext);
55
66
  return;
56
67
  }
57
- // a. Navigation test elements — for every link on the page
58
- await this.generateNavigationTestElements(resolvedContext);
59
- // b. Render test elements — capture render coverage for the page
60
- await this.generateRenderTestElements(resolvedContext);
61
- // c. Form and password test elements — build form workflows from extracted forms
62
- await this.generateFormTestElements(resolvedContext);
63
- // d. Synthetic journey test elements — build generic business flows from common UI verbs
64
- await this.generateSemanticJourneyTestElements(resolvedContext);
65
- // e. Journey test elements — preserve discovered multi-step flows as standalone e2e tests
66
- await this.generateE2ETestElements(resolvedContext);
67
- // f. Dialog lifecycle cases — only when a dialog is already open in the analyzed state
68
- await this.generateDialogLifecycleTestElements(resolvedContext);
69
- // g. Scaffold test elements — for each scaffold's actionable elements
70
- await this.generateScaffoldTestElements(resolvedContext);
71
- // h. Content test elements — for non-scaffold actionable elements
72
- await this.generateContentTestElements(resolvedContext);
73
- // i. Keyboard/disclosure cases
74
- await this.generateKeyboardAndDisclosureTestElements(resolvedContext);
75
- // j. Variant/option cases
76
- await this.generateVariantTestElements(resolvedContext);
77
- }
78
- async generateHoverFollowUpCases(testElement, context) {
79
- const selector = this.getPrimarySelector(testElement);
80
- if (!selector || !context.currentPageStateId)
81
- return;
82
- const beginningItems = context.beginningPageStateId > 0
83
- ? await context.api.getItemsByPageState(context.beginningPageStateId)
84
- : [];
85
- const beginningKeys = new Set(beginningItems.map(item => this.getItemKey(item)).filter(Boolean));
86
- const revealedItems = context.actionableItems.filter(item => {
87
- if (!this.isMouseActionable(item))
68
+ await generateNavigationTestElements(this, resolvedContext);
69
+ await generateRenderTestElements(this, resolvedContext);
70
+ await generateFormTestElements(this, resolvedContext);
71
+ await generateSemanticJourneyTestElements(this, resolvedContext);
72
+ await generateE2ETestElements(this, resolvedContext);
73
+ await generateDialogLifecycleTestElements(this, resolvedContext);
74
+ await generateScaffoldTestElements(this, resolvedContext);
75
+ await generateContentTestElements(this, resolvedContext);
76
+ await generateKeyboardAndDisclosureTestElements(this, resolvedContext);
77
+ await generateVariantTestElements(this, resolvedContext);
78
+ }
79
+ getScaffoldSurfaceItems(context, scaffold) {
80
+ return context.actionableItems.filter(item => {
81
+ if (!this.isSurfaceCandidate(item) || !item.selector)
88
82
  return false;
89
- const key = this.getItemKey(item);
90
- return Boolean(key) && !beginningKeys.has(key);
91
- });
92
- const hoveredItem = context.actionableItems.find(item => item.selector === selector) ?? null;
93
- if (revealedItems.length === 0 && hoveredItem) {
94
- const clickCase = this.buildClickTestElement(hoveredItem, context.currentPath, context.sizeClass, context.uid, context.currentPageStateId, context.currentTestElementId);
95
- const tc = await context.api.ensureTestElement(context.runnerId, context.currentTestSurfaceId, clickCase);
96
- await context.api.createTestElementRun({
97
- testElementId: tc.id,
98
- testSurfaceRunId: context.currentSurfaceRunId ?? undefined,
99
- });
100
- return;
101
- }
102
- for (const item of revealedItems) {
103
- const nextHover = this.buildHoverTestElement(item, context.currentPath, context.sizeClass, context.uid, context.currentPageStateId, context.currentTestElementId);
104
- const tc = await context.api.ensureTestElement(context.runnerId, context.currentTestSurfaceId, nextHover);
105
- await context.api.createTestElementRun({
106
- testElementId: tc.id,
107
- testSurfaceRunId: context.currentSurfaceRunId ?? undefined,
108
- });
109
- }
110
- }
111
- async generateNavigationTestElements(context) {
112
- if (context.pageRequiresLogin)
113
- return;
114
- const { api, runnerId, sizeClass, uid, navigationSurface, bundleRun } = context;
115
- const links = context.actionableItems.filter(item => item.actionKind === "navigate" && item.href && item.visible);
116
- // Ensure navigation surface is in the bundle
117
- await api.ensureBundleSurfaceLink(bundleRun.testSurfaceBundleId, navigationSurface.id);
118
- // Ensure a surface run exists for the navigation surface under this bundle run
119
- const surfaceRun = await this.ensureSurfaceRun(api, navigationSurface.id, bundleRun.id);
120
- for (const link of links) {
121
- if (!link.href)
122
- continue;
123
- const path = this.extractRelativePath(link.href);
124
- if (!path)
125
- continue;
126
- const testElement = this.buildNavigationTestElement(path, sizeClass, uid, context.currentPageStateId);
127
- const tc = await api.ensureTestElement(runnerId, navigationSurface.id, testElement);
128
- await api.createTestElementRun({
129
- testElementId: tc.id,
130
- testSurfaceRunId: surfaceRun.id,
131
- });
132
- }
133
- }
134
- async generateScaffoldTestElements(context) {
135
- const { api, runnerId, sizeClass, uid, bundleRun } = context;
136
- for (const scaffold of context.scaffolds) {
137
- // Find actionable items belonging to this scaffold
138
- const scaffoldItems = context.actionableItems.filter(item => item.scaffoldId != null && this.isSurfaceCandidate(item));
139
- if (scaffoldItems.length === 0)
140
- continue;
141
- // Ensure a test surface for this scaffold
142
- const surfaceTitle = `Scaffold: ${scaffold.type}`;
143
- const surface = await api.ensureTestSurface(runnerId, {
144
- title: surfaceTitle,
145
- description: `Tests for ${scaffold.type} scaffold`,
146
- startingPageStateId: context.currentPageStateId,
147
- startingPath: context.currentPath,
148
- sizeClass,
149
- priority: 3,
150
- surface_tags: ["scaffold", scaffold.type],
151
- uid,
152
- });
153
- context.events.onTestSurfaceCreated({
154
- surfaceId: surface.id,
155
- title: surface.title,
156
- });
157
- await api.ensureBundleSurfaceLink(bundleRun.testSurfaceBundleId, surface.id);
158
- const surfaceRun = await this.ensureSurfaceRun(api, surface.id, bundleRun.id);
159
- for (const item of scaffoldItems) {
160
- const testElement = this.shouldUseDirectControlInteraction(item)
161
- ? this.buildControlInteractionTestElement(item, context.currentPath, sizeClass, uid, context.currentPageStateId)
162
- : this.buildHoverTestElement(item, context.currentPath, sizeClass, uid, context.currentPageStateId);
163
- const tc = await api.ensureTestElement(runnerId, surface.id, testElement);
164
- await api.createTestElementRun({
165
- testElementId: tc.id,
166
- testSurfaceRunId: surfaceRun.id,
167
- });
168
- }
169
- }
170
- }
171
- async generateRenderTestElements(context) {
172
- const { api, runnerId, sizeClass, uid, bundleRun } = context;
173
- const surface = await api.ensureTestSurface(runnerId, {
174
- title: `Render: ${context.currentPath}`,
175
- description: `Render validation for ${context.currentPath}`,
176
- startingPageStateId: context.currentPageStateId,
177
- startingPath: context.currentPath,
178
- sizeClass,
179
- priority: 2,
180
- surface_tags: ["render"],
181
- uid,
182
- });
183
- context.events.onTestSurfaceCreated({
184
- surfaceId: surface.id,
185
- title: surface.title,
186
- });
187
- await api.ensureBundleSurfaceLink(bundleRun.testSurfaceBundleId, surface.id);
188
- const surfaceRun = await this.ensureSurfaceRun(api, surface.id, bundleRun.id);
189
- const testElement = this.buildRenderTestElement(context.currentPath, sizeClass, uid, context.currentPageStateId, context.pageId);
190
- const tc = await api.ensureTestElement(runnerId, surface.id, testElement);
191
- await api.createTestElementRun({
192
- testElementId: tc.id,
193
- testSurfaceRunId: surfaceRun.id,
83
+ return (context.scaffoldSelectorByItemSelector[item.selector] ===
84
+ scaffold.selector);
194
85
  });
195
86
  }
196
- async generateFormTestElements(context) {
197
- if (context.forms.length === 0)
198
- return;
199
- const { api, runnerId, sizeClass, uid, bundleRun } = context;
200
- const surface = await api.ensureTestSurface(runnerId, {
201
- title: `Forms: ${context.currentPath}`,
202
- description: `Form workflows for ${context.currentPath}`,
203
- startingPageStateId: context.currentPageStateId,
204
- startingPath: context.currentPath,
205
- sizeClass,
206
- priority: 2,
207
- surface_tags: ["form"],
208
- uid,
209
- });
210
- context.events.onTestSurfaceCreated({
211
- surfaceId: surface.id,
212
- title: surface.title,
213
- });
214
- await api.ensureBundleSurfaceLink(bundleRun.testSurfaceBundleId, surface.id);
215
- const surfaceRun = await this.ensureSurfaceRun(api, surface.id, bundleRun.id);
216
- for (let index = 0; index < context.forms.length; index++) {
217
- const form = context.forms[index];
218
- const formType = this.identifyFormType(form, context.currentPath);
219
- const formLabel = this.describeForm(form, index);
220
- const validValues = this.planFormValues(form, context.actionableItems);
221
- if (this.isSearchForm(form)) {
222
- const searchTests = this.buildSearchTestElements(form, formLabel, context.currentPath, sizeClass, uid, context.currentPageStateId, validValues, context.actionableItems);
223
- for (const searchTest of searchTests) {
224
- const searchElement = await api.ensureTestElement(runnerId, surface.id, searchTest);
225
- await api.createTestElementRun({
226
- testElementId: searchElement.id,
227
- testSurfaceRunId: surfaceRun.id,
228
- });
229
- }
230
- continue;
231
- }
232
- const positive = this.buildFormTestElement(form, formLabel, formType, context.currentPath, sizeClass, uid, context.currentPageStateId, validValues);
233
- const positiveElement = await api.ensureTestElement(runnerId, surface.id, positive);
234
- await api.createTestElementRun({
235
- testElementId: positiveElement.id,
236
- testSurfaceRunId: surfaceRun.id,
237
- });
238
- for (const field of form.fields.filter(field => this.isNegativeCandidateField(field))) {
239
- const negative = this.buildNegativeFormTestElement(form, formLabel, formType, field, context.currentPath, sizeClass, uid, context.currentPageStateId, validValues);
240
- const negativeElement = await api.ensureTestElement(runnerId, surface.id, negative);
241
- await api.createTestElementRun({
242
- testElementId: negativeElement.id,
243
- testSurfaceRunId: surfaceRun.id,
244
- });
245
- const correction = this.buildFormCorrectionTestElement(form, formLabel, formType, field, context.currentPath, sizeClass, uid, context.currentPageStateId, validValues);
246
- const correctionElement = await api.ensureTestElement(runnerId, surface.id, correction);
247
- await api.createTestElementRun({
248
- testElementId: correctionElement.id,
249
- testSurfaceRunId: surfaceRun.id,
250
- });
251
- }
252
- if (this.isPasswordScenario(formType, form)) {
253
- const passwordTests = this.buildPasswordTestElements(form, formLabel, formType, context.currentPath, sizeClass, uid, context.currentPageStateId, validValues, this.detectPasswordRequirements(this.extractVisibleText(context.html)));
254
- for (const passwordTest of passwordTests) {
255
- const passwordElement = await api.ensureTestElement(runnerId, surface.id, passwordTest);
256
- await api.createTestElementRun({
257
- testElementId: passwordElement.id,
258
- testSurfaceRunId: surfaceRun.id,
259
- });
260
- }
261
- }
262
- }
263
- }
264
- async generateE2ETestElements(context) {
265
- if (context.journeySteps.length < 2)
266
- return;
267
- const { api, runnerId, sizeClass, uid, bundleRun } = context;
268
- const surface = await api.ensureTestSurface(runnerId, {
269
- title: `Journeys: ${context.currentPath}`,
270
- description: `End-to-end journeys reaching ${context.currentPath}`,
271
- startingPageStateId: context.currentPageStateId,
272
- startingPath: context.currentPath,
273
- sizeClass,
274
- priority: 2,
275
- surface_tags: ["e2e"],
276
- uid,
277
- });
278
- context.events.onTestSurfaceCreated({
279
- surfaceId: surface.id,
280
- title: surface.title,
281
- });
282
- await api.ensureBundleSurfaceLink(bundleRun.testSurfaceBundleId, surface.id);
283
- const surfaceRun = await this.ensureSurfaceRun(api, surface.id, bundleRun.id);
284
- const e2e = this.buildE2ETestElement(context.currentPath, sizeClass, uid, context.currentPageStateId, context.journeySteps);
285
- const tc = await api.ensureTestElement(runnerId, surface.id, e2e);
286
- await api.createTestElementRun({
287
- testElementId: tc.id,
288
- testSurfaceRunId: surfaceRun.id,
289
- });
290
- }
291
- async generateSemanticJourneyTestElements(context) {
292
- const journeys = this.buildSemanticJourneyTestElements(context);
293
- if (journeys.length === 0)
294
- return;
295
- const { api, runnerId, bundleRun } = context;
296
- const surface = await api.ensureTestSurface(runnerId, {
297
- title: `Journeys: ${context.currentPath}`,
298
- description: `Semantic multi-step journeys from ${context.currentPath}`,
299
- startingPageStateId: context.currentPageStateId,
300
- startingPath: context.currentPath,
301
- sizeClass: context.sizeClass,
302
- priority: 2,
303
- surface_tags: ["e2e", "semantic-journey"],
304
- uid: context.uid,
305
- });
306
- context.events.onTestSurfaceCreated({
307
- surfaceId: surface.id,
308
- title: surface.title,
309
- });
310
- await api.ensureBundleSurfaceLink(bundleRun.testSurfaceBundleId, surface.id);
311
- const surfaceRun = await this.ensureSurfaceRun(api, surface.id, bundleRun.id);
312
- for (const journey of journeys) {
313
- const tc = await api.ensureTestElement(runnerId, surface.id, journey);
314
- await api.createTestElementRun({
315
- testElementId: tc.id,
316
- testSurfaceRunId: surfaceRun.id,
317
- });
318
- }
319
- }
320
- async generateDialogLifecycleTestElements(context) {
321
- if (!this.pageHasOpenDialog(context.html))
322
- return;
323
- const closeCandidates = context.actionableItems.filter(item => item.visible &&
324
- !item.disabled &&
325
- item.selector &&
326
- this.isDialogCloseItem(item));
327
- const tests = [];
328
- for (const item of closeCandidates) {
329
- tests.push(this.buildDialogCloseTestElement(item, context.currentPath, context.sizeClass, context.uid, context.currentPageStateId));
330
- }
331
- tests.push(this.buildEscapeDialogTestElement(context.currentPath, context.sizeClass, context.uid, context.currentPageStateId));
332
- const { api, runnerId, bundleRun } = context;
333
- const surface = await api.ensureTestSurface(runnerId, {
334
- title: `Dialogs: ${context.currentPath}`,
335
- description: `Dialog lifecycle checks for ${context.currentPath}`,
336
- startingPageStateId: context.currentPageStateId,
337
- startingPath: context.currentPath,
338
- sizeClass: context.sizeClass,
339
- priority: 2,
340
- surface_tags: ["dialog"],
341
- uid: context.uid,
342
- });
343
- context.events.onTestSurfaceCreated({
344
- surfaceId: surface.id,
345
- title: surface.title,
346
- });
347
- await api.ensureBundleSurfaceLink(bundleRun.testSurfaceBundleId, surface.id);
348
- const surfaceRun = await this.ensureSurfaceRun(api, surface.id, bundleRun.id);
349
- for (const test of tests) {
350
- const tc = await api.ensureTestElement(runnerId, surface.id, test);
351
- await api.createTestElementRun({
352
- testElementId: tc.id,
353
- testSurfaceRunId: surfaceRun.id,
354
- });
355
- }
356
- }
357
- async generateKeyboardAndDisclosureTestElements(context) {
358
- const tests = this.buildKeyboardAndDisclosureTestElements(context);
359
- if (tests.length === 0)
360
- return;
361
- const { api, runnerId, bundleRun } = context;
362
- const surface = await api.ensureTestSurface(runnerId, {
363
- title: `Keyboard: ${context.currentPath}`,
364
- description: `Keyboard parity and disclosure checks for ${context.currentPath}`,
365
- startingPageStateId: context.currentPageStateId,
366
- startingPath: context.currentPath,
367
- sizeClass: context.sizeClass,
368
- priority: 3,
369
- surface_tags: ["keyboard", "disclosure"],
370
- uid: context.uid,
371
- });
372
- context.events.onTestSurfaceCreated({
373
- surfaceId: surface.id,
374
- title: surface.title,
375
- });
376
- await api.ensureBundleSurfaceLink(bundleRun.testSurfaceBundleId, surface.id);
377
- const surfaceRun = await this.ensureSurfaceRun(api, surface.id, bundleRun.id);
378
- for (const test of tests) {
379
- const tc = await api.ensureTestElement(runnerId, surface.id, test);
380
- await api.createTestElementRun({
381
- testElementId: tc.id,
382
- testSurfaceRunId: surfaceRun.id,
383
- });
384
- }
385
- }
386
- async generateVariantTestElements(context) {
387
- const tests = this.buildVariantTestElements(context);
388
- if (tests.length === 0)
389
- return;
390
- const { api, runnerId, bundleRun } = context;
391
- const surface = await api.ensureTestSurface(runnerId, {
392
- title: `Variants: ${context.currentPath}`,
393
- description: `Variant and option state checks for ${context.currentPath}`,
394
- startingPageStateId: context.currentPageStateId,
395
- startingPath: context.currentPath,
396
- sizeClass: context.sizeClass,
397
- priority: 2,
398
- surface_tags: ["variant", "option"],
399
- uid: context.uid,
400
- });
401
- context.events.onTestSurfaceCreated({
402
- surfaceId: surface.id,
403
- title: surface.title,
404
- });
405
- await api.ensureBundleSurfaceLink(bundleRun.testSurfaceBundleId, surface.id);
406
- const surfaceRun = await this.ensureSurfaceRun(api, surface.id, bundleRun.id);
407
- for (const test of tests) {
408
- const tc = await api.ensureTestElement(runnerId, surface.id, test);
409
- await api.createTestElementRun({
410
- testElementId: tc.id,
411
- testSurfaceRunId: surfaceRun.id,
412
- });
413
- }
414
- }
415
- async generateContentTestElements(context) {
416
- const { api, runnerId, sizeClass, uid, bundleRun, pageId: _pageId, } = context;
417
- // Content items are those NOT in a scaffold
418
- const contentItems = context.actionableItems.filter(item => item.scaffoldId == null && this.isSurfaceCandidate(item));
419
- if (contentItems.length === 0)
420
- return;
421
- const surfaceTitle = `Page: ${context.currentPath}`;
422
- const surface = await api.ensureTestSurface(runnerId, {
423
- title: surfaceTitle,
424
- description: `Tests for page content at ${context.currentPath}`,
425
- startingPageStateId: context.currentPageStateId,
426
- startingPath: context.currentPath,
427
- sizeClass,
428
- priority: 3,
429
- surface_tags: ["page-content"],
430
- uid,
431
- });
432
- context.events.onTestSurfaceCreated({
433
- surfaceId: surface.id,
434
- title: surface.title,
435
- });
436
- await api.ensureBundleSurfaceLink(bundleRun.testSurfaceBundleId, surface.id);
437
- const surfaceRun = await this.ensureSurfaceRun(api, surface.id, bundleRun.id);
438
- for (const item of contentItems) {
439
- const testElement = this.shouldUseDirectControlInteraction(item)
440
- ? this.buildControlInteractionTestElement(item, context.currentPath, sizeClass, uid, context.currentPageStateId)
441
- : this.buildHoverTestElement(item, context.currentPath, sizeClass, uid, context.currentPageStateId);
442
- const tc = await api.ensureTestElement(runnerId, surface.id, testElement);
443
- await api.createTestElementRun({
444
- testElementId: tc.id,
445
- testSurfaceRunId: surfaceRun.id,
446
- });
447
- }
448
- }
449
87
  async ensureSurfaceRun(api, testSurfaceId, bundleRunId) {
450
88
  const openSurfaceRuns = await api.getOpenTestSurfaceRuns(bundleRunId);
451
89
  const existing = openSurfaceRuns.find(surfaceRun => surfaceRun.testSurfaceId === testSurfaceId);
@@ -1245,7 +883,7 @@ export class PageAnalyzer {
1245
883
  return `${descriptor} @ ${form.selector}`;
1246
884
  }
1247
885
  buildSemanticJourneyTestElements(context) {
1248
- const items = context.actionableItems.filter(item => item.visible && !item.disabled && Boolean(item.selector));
886
+ const items = this.selectRepresentativeItems(context.actionableItems.filter(item => item.visible && !item.disabled && Boolean(item.selector)));
1249
887
  const journeys = [];
1250
888
  const collectionCount = this.estimateCollectionCount(context.html);
1251
889
  const addToCart = items.find(item => this.isAddToCartItem(item));
@@ -1660,7 +1298,7 @@ export class PageAnalyzer {
1660
1298
  return field.type === "checkbox" || field.type === "radio";
1661
1299
  }
1662
1300
  buildKeyboardAndDisclosureTestElements(context) {
1663
- const items = context.actionableItems.filter(item => item.visible && !item.disabled && Boolean(item.selector));
1301
+ const items = this.selectRepresentativeItems(context.actionableItems.filter(item => item.visible && !item.disabled && Boolean(item.selector)));
1664
1302
  const tests = [];
1665
1303
  for (const item of items) {
1666
1304
  if (this.isDisclosureItem(item)) {
@@ -1693,10 +1331,10 @@ export class PageAnalyzer {
1693
1331
  return tests;
1694
1332
  }
1695
1333
  buildVariantTestElements(context) {
1696
- const items = context.actionableItems.filter(item => item.visible &&
1334
+ const items = this.selectRepresentativeItems(context.actionableItems.filter(item => item.visible &&
1697
1335
  !item.disabled &&
1698
1336
  Boolean(item.selector) &&
1699
- this.isVariantSelector(item));
1337
+ this.isVariantSelector(item)));
1700
1338
  const tests = items
1701
1339
  .map(item => this.buildVariantTestElement(item, context))
1702
1340
  .filter((item) => Boolean(item));
@@ -2178,6 +1816,7 @@ export class PageAnalyzer {
2178
1816
  describeActionableItem(item) {
2179
1817
  return (item.accessibleName ||
2180
1818
  item.textContent ||
1819
+ String(item.attributes?._containerTitle ?? "") ||
2181
1820
  String(item.attributes?.labelText ?? "") ||
2182
1821
  String(item.attributes?.placeholder ?? "") ||
2183
1822
  item.selector);
@@ -2190,12 +1829,93 @@ export class PageAnalyzer {
2190
1829
  String(item.attributes?.placeholder ?? ""),
2191
1830
  String(item.attributes?.name ?? ""),
2192
1831
  String(item.attributes?.id ?? ""),
1832
+ String(item.attributes?._containerTitle ?? ""),
1833
+ String(item.attributes?._containerCtaStyle ?? ""),
2193
1834
  item.href || "",
2194
1835
  item.selector,
2195
1836
  ]
2196
1837
  .join(" ")
2197
1838
  .toLowerCase();
2198
1839
  }
1840
+ selectRepresentativeItems(items) {
1841
+ const groups = new Map();
1842
+ const passthrough = [];
1843
+ for (const item of items) {
1844
+ const containerFingerprint = String(item.attributes?._containerFingerprint ?? "").trim();
1845
+ if (!containerFingerprint) {
1846
+ passthrough.push(item);
1847
+ continue;
1848
+ }
1849
+ const key = `${containerFingerprint}|${this.representativeActionStyle(item)}`;
1850
+ const bucket = groups.get(key) ?? [];
1851
+ bucket.push(item);
1852
+ groups.set(key, bucket);
1853
+ }
1854
+ const representatives = Array.from(groups.values()).map(group => this.pickRepresentativeItem(group));
1855
+ return [...passthrough, ...representatives];
1856
+ }
1857
+ representativeActionStyle(item) {
1858
+ const text = this.semanticText(item);
1859
+ const sourceHints = String(item.attributes?._sourceHints ?? "").toLowerCase();
1860
+ const containerTitle = String(item.attributes?._containerTitle ?? "").toLowerCase();
1861
+ if (sourceHints.includes("promoted-target") ||
1862
+ sourceHints.includes("cursor-pointer")) {
1863
+ return "tile-click";
1864
+ }
1865
+ if (item.actionKind === "navigate" &&
1866
+ containerTitle &&
1867
+ (text.includes(containerTitle) ||
1868
+ (item.href && !this.isCheckoutItem(item)))) {
1869
+ return "tile-click";
1870
+ }
1871
+ if (this.isAddToCartItem(item))
1872
+ return "cta:add-to-cart";
1873
+ if (/\blogin for pricing\b/.test(text))
1874
+ return "cta:login-for-pricing";
1875
+ if (/\bselect options\b/.test(text))
1876
+ return "cta:select-options";
1877
+ if (this.isCheckoutItem(item))
1878
+ return "cta:checkout";
1879
+ if (this.isRemoveItemAction(item))
1880
+ return "cta:remove";
1881
+ if (item.actionKind === "navigate")
1882
+ return "navigate";
1883
+ if (item.actionKind === "select")
1884
+ return "select";
1885
+ if (item.actionKind === "fill")
1886
+ return "fill";
1887
+ return `${item.actionKind}:${text
1888
+ .replace(/\b[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}\b/g, "email")
1889
+ .replace(/\$\s?\d[\d,.]*/g, "price")
1890
+ .replace(/\d+/g, "n")
1891
+ .slice(0, 80)}`;
1892
+ }
1893
+ pickRepresentativeItem(items) {
1894
+ return [...items].sort((left, right) => {
1895
+ const leftScore = this.representativePriority(left);
1896
+ const rightScore = this.representativePriority(right);
1897
+ if (leftScore !== rightScore)
1898
+ return rightScore - leftScore;
1899
+ return (left.selector?.length ?? 0) - (right.selector?.length ?? 0);
1900
+ })[0];
1901
+ }
1902
+ representativePriority(item) {
1903
+ let score = 0;
1904
+ const sourceHints = String(item.attributes?._sourceHints ?? "").toLowerCase();
1905
+ if (sourceHints.includes("promoted-target"))
1906
+ score += 4;
1907
+ if (sourceHints.includes("cursor-pointer"))
1908
+ score += 3;
1909
+ if (item.actionKind === "navigate")
1910
+ score += 2;
1911
+ if (item.actionKind === "click")
1912
+ score += 1;
1913
+ if (String(item.attributes?._testId ?? ""))
1914
+ score += 1;
1915
+ if (item.accessibleName)
1916
+ score += 1;
1917
+ return score;
1918
+ }
2199
1919
  isAddToCartItem(item) {
2200
1920
  return /\b(add to cart|add to bag|buy now|add item)\b/.test(this.semanticText(item));
2201
1921
  }
@@ -2461,4 +2181,4 @@ export class PageAnalyzer {
2461
2181
  return variants;
2462
2182
  }
2463
2183
  }
2464
- //# sourceMappingURL=page-analyzer.js.map
2184
+ //# sourceMappingURL=index.js.map