@sudobility/testomniac_runner_service 0.1.44 → 0.1.46

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 (81) hide show
  1. package/dist/analyzer/page-analyzer.d.ts +74 -1
  2. package/dist/analyzer/page-analyzer.d.ts.map +1 -1
  3. package/dist/analyzer/page-analyzer.js +1491 -6
  4. package/dist/analyzer/page-analyzer.js.map +1 -1
  5. package/dist/browser/control-snapshot.d.ts +4 -0
  6. package/dist/browser/control-snapshot.d.ts.map +1 -0
  7. package/dist/browser/control-snapshot.js +181 -0
  8. package/dist/browser/control-snapshot.js.map +1 -0
  9. package/dist/browser/dom-snapshot.d.ts.map +1 -1
  10. package/dist/browser/dom-snapshot.js +8 -0
  11. package/dist/browser/dom-snapshot.js.map +1 -1
  12. package/dist/browser/ui-snapshot.d.ts +9 -0
  13. package/dist/browser/ui-snapshot.d.ts.map +1 -0
  14. package/dist/browser/ui-snapshot.js +53 -0
  15. package/dist/browser/ui-snapshot.js.map +1 -0
  16. package/dist/expertise/content-expertise.d.ts +1 -0
  17. package/dist/expertise/content-expertise.d.ts.map +1 -1
  18. package/dist/expertise/content-expertise.js +38 -0
  19. package/dist/expertise/content-expertise.js.map +1 -1
  20. package/dist/expertise/tester/commerce-checks.d.ts +18 -0
  21. package/dist/expertise/tester/commerce-checks.d.ts.map +1 -0
  22. package/dist/expertise/tester/commerce-checks.js +158 -0
  23. package/dist/expertise/tester/commerce-checks.js.map +1 -0
  24. package/dist/expertise/tester/control-state.d.ts +31 -0
  25. package/dist/expertise/tester/control-state.d.ts.map +1 -0
  26. package/dist/expertise/tester/control-state.js +46 -0
  27. package/dist/expertise/tester/control-state.js.map +1 -0
  28. package/dist/expertise/tester/core-checks.d.ts +5 -0
  29. package/dist/expertise/tester/core-checks.d.ts.map +1 -0
  30. package/dist/expertise/tester/core-checks.js +46 -0
  31. package/dist/expertise/tester/core-checks.js.map +1 -0
  32. package/dist/expertise/tester/dialog-feedback-checks.d.ts +13 -0
  33. package/dist/expertise/tester/dialog-feedback-checks.d.ts.map +1 -0
  34. package/dist/expertise/tester/dialog-feedback-checks.js +82 -0
  35. package/dist/expertise/tester/dialog-feedback-checks.js.map +1 -0
  36. package/dist/expertise/tester/form-checks.d.ts +8 -0
  37. package/dist/expertise/tester/form-checks.d.ts.map +1 -0
  38. package/dist/expertise/tester/form-checks.js +50 -0
  39. package/dist/expertise/tester/form-checks.js.map +1 -0
  40. package/dist/expertise/tester/keyboard-disclosure-checks.d.ts +7 -0
  41. package/dist/expertise/tester/keyboard-disclosure-checks.d.ts.map +1 -0
  42. package/dist/expertise/tester/keyboard-disclosure-checks.js +46 -0
  43. package/dist/expertise/tester/keyboard-disclosure-checks.js.map +1 -0
  44. package/dist/expertise/tester/navigation-checks.d.ts +8 -0
  45. package/dist/expertise/tester/navigation-checks.d.ts.map +1 -0
  46. package/dist/expertise/tester/navigation-checks.js +64 -0
  47. package/dist/expertise/tester/navigation-checks.js.map +1 -0
  48. package/dist/expertise/tester/page-behavior-checks.d.ts +17 -0
  49. package/dist/expertise/tester/page-behavior-checks.d.ts.map +1 -0
  50. package/dist/expertise/tester/page-behavior-checks.js +144 -0
  51. package/dist/expertise/tester/page-behavior-checks.js.map +1 -0
  52. package/dist/expertise/tester/persistence-checks.d.ts +12 -0
  53. package/dist/expertise/tester/persistence-checks.d.ts.map +1 -0
  54. package/dist/expertise/tester/persistence-checks.js +109 -0
  55. package/dist/expertise/tester/persistence-checks.js.map +1 -0
  56. package/dist/expertise/tester/selection-control-checks.d.ts +7 -0
  57. package/dist/expertise/tester/selection-control-checks.d.ts.map +1 -0
  58. package/dist/expertise/tester/selection-control-checks.js +128 -0
  59. package/dist/expertise/tester/selection-control-checks.js.map +1 -0
  60. package/dist/expertise/tester/text-input-checks.d.ts +8 -0
  61. package/dist/expertise/tester/text-input-checks.d.ts.map +1 -0
  62. package/dist/expertise/tester/text-input-checks.js +194 -0
  63. package/dist/expertise/tester/text-input-checks.js.map +1 -0
  64. package/dist/expertise/tester/validation-checks.d.ts +10 -0
  65. package/dist/expertise/tester/validation-checks.d.ts.map +1 -0
  66. package/dist/expertise/tester/validation-checks.js +59 -0
  67. package/dist/expertise/tester/validation-checks.js.map +1 -0
  68. package/dist/expertise/tester-expertise.d.ts +0 -3
  69. package/dist/expertise/tester-expertise.d.ts.map +1 -1
  70. package/dist/expertise/tester-expertise.js +69 -49
  71. package/dist/expertise/tester-expertise.js.map +1 -1
  72. package/dist/expertise/types.d.ts +12 -1
  73. package/dist/expertise/types.d.ts.map +1 -1
  74. package/dist/extractors/form-extractor.d.ts +19 -0
  75. package/dist/extractors/form-extractor.d.ts.map +1 -1
  76. package/dist/extractors/form-extractor.js +271 -42
  77. package/dist/extractors/form-extractor.js.map +1 -1
  78. package/dist/orchestrator/test-element-executor.d.ts.map +1 -1
  79. package/dist/orchestrator/test-element-executor.js +74 -9
  80. package/dist/orchestrator/test-element-executor.js.map +1 -1
  81. package/package.json +3 -3
@@ -1,6 +1,8 @@
1
1
  import { PlaywrightAction, ExpectationType, ExpectationSeverity, } from "@sudobility/testomniac_types";
2
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
6
  /**
5
7
  * PageAnalyzer generates expectations and discovers new test elements
6
8
  * during discovery mode.
@@ -54,10 +56,22 @@ export class PageAnalyzer {
54
56
  }
55
57
  // a. Navigation test elements — for every link on the page
56
58
  await this.generateNavigationTestElements(resolvedContext);
57
- // b. Scaffold test elements — for each scaffold's actionable elements
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
58
70
  await this.generateScaffoldTestElements(resolvedContext);
59
- // c. Content test elements — for non-scaffold actionable elements
71
+ // h. Content test elements — for non-scaffold actionable elements
60
72
  await this.generateContentTestElements(resolvedContext);
73
+ // i. Keyboard/disclosure cases
74
+ await this.generateKeyboardAndDisclosureTestElements(resolvedContext);
61
75
  }
62
76
  async generateHoverFollowUpCases(testElement, context) {
63
77
  const selector = this.getPrimarySelector(testElement);
@@ -119,7 +133,7 @@ export class PageAnalyzer {
119
133
  const { api, runnerId, sizeClass, uid, bundleRun } = context;
120
134
  for (const scaffold of context.scaffolds) {
121
135
  // Find actionable items belonging to this scaffold
122
- const scaffoldItems = context.actionableItems.filter(item => item.scaffoldId != null && this.isMouseActionable(item));
136
+ const scaffoldItems = context.actionableItems.filter(item => item.scaffoldId != null && this.isSurfaceCandidate(item));
123
137
  if (scaffoldItems.length === 0)
124
138
  continue;
125
139
  // Ensure a test surface for this scaffold
@@ -141,7 +155,9 @@ export class PageAnalyzer {
141
155
  await api.ensureBundleSurfaceLink(bundleRun.testSurfaceBundleId, surface.id);
142
156
  const surfaceRun = await this.ensureSurfaceRun(api, surface.id, bundleRun.id);
143
157
  for (const item of scaffoldItems) {
144
- const testElement = this.buildHoverTestElement(item, context.currentPath, sizeClass, uid, context.currentPageStateId);
158
+ const testElement = this.shouldUseDirectControlInteraction(item)
159
+ ? this.buildControlInteractionTestElement(item, context.currentPath, sizeClass, uid, context.currentPageStateId)
160
+ : this.buildHoverTestElement(item, context.currentPath, sizeClass, uid, context.currentPageStateId);
145
161
  const tc = await api.ensureTestElement(runnerId, surface.id, testElement);
146
162
  await api.createTestElementRun({
147
163
  testElementId: tc.id,
@@ -150,10 +166,214 @@ export class PageAnalyzer {
150
166
  }
151
167
  }
152
168
  }
169
+ async generateRenderTestElements(context) {
170
+ const { api, runnerId, sizeClass, uid, bundleRun } = context;
171
+ const surface = await api.ensureTestSurface(runnerId, {
172
+ title: `Render: ${context.currentPath}`,
173
+ description: `Render validation for ${context.currentPath}`,
174
+ startingPageStateId: context.currentPageStateId,
175
+ startingPath: context.currentPath,
176
+ sizeClass,
177
+ priority: 2,
178
+ surface_tags: ["render"],
179
+ uid,
180
+ });
181
+ context.events.onTestSurfaceCreated({
182
+ surfaceId: surface.id,
183
+ title: surface.title,
184
+ });
185
+ await api.ensureBundleSurfaceLink(bundleRun.testSurfaceBundleId, surface.id);
186
+ const surfaceRun = await this.ensureSurfaceRun(api, surface.id, bundleRun.id);
187
+ const testElement = this.buildRenderTestElement(context.currentPath, sizeClass, uid, context.currentPageStateId, context.pageId);
188
+ const tc = await api.ensureTestElement(runnerId, surface.id, testElement);
189
+ await api.createTestElementRun({
190
+ testElementId: tc.id,
191
+ testSurfaceRunId: surfaceRun.id,
192
+ });
193
+ }
194
+ async generateFormTestElements(context) {
195
+ if (context.forms.length === 0)
196
+ return;
197
+ const { api, runnerId, sizeClass, uid, bundleRun } = context;
198
+ const surface = await api.ensureTestSurface(runnerId, {
199
+ title: `Forms: ${context.currentPath}`,
200
+ description: `Form workflows for ${context.currentPath}`,
201
+ startingPageStateId: context.currentPageStateId,
202
+ startingPath: context.currentPath,
203
+ sizeClass,
204
+ priority: 2,
205
+ surface_tags: ["form"],
206
+ uid,
207
+ });
208
+ context.events.onTestSurfaceCreated({
209
+ surfaceId: surface.id,
210
+ title: surface.title,
211
+ });
212
+ await api.ensureBundleSurfaceLink(bundleRun.testSurfaceBundleId, surface.id);
213
+ const surfaceRun = await this.ensureSurfaceRun(api, surface.id, bundleRun.id);
214
+ for (let index = 0; index < context.forms.length; index++) {
215
+ const form = context.forms[index];
216
+ const formType = this.identifyFormType(form, context.currentPath);
217
+ const formLabel = this.describeForm(form, index);
218
+ const validValues = this.planFormValues(form, context.actionableItems);
219
+ const positive = this.buildFormTestElement(form, formLabel, formType, context.currentPath, sizeClass, uid, context.currentPageStateId, validValues);
220
+ const positiveElement = await api.ensureTestElement(runnerId, surface.id, positive);
221
+ await api.createTestElementRun({
222
+ testElementId: positiveElement.id,
223
+ testSurfaceRunId: surfaceRun.id,
224
+ });
225
+ for (const field of form.fields.filter(field => this.isNegativeCandidateField(field))) {
226
+ const negative = this.buildNegativeFormTestElement(form, formLabel, formType, field, context.currentPath, sizeClass, uid, context.currentPageStateId, validValues);
227
+ const negativeElement = await api.ensureTestElement(runnerId, surface.id, negative);
228
+ await api.createTestElementRun({
229
+ testElementId: negativeElement.id,
230
+ testSurfaceRunId: surfaceRun.id,
231
+ });
232
+ const correction = this.buildFormCorrectionTestElement(form, formLabel, formType, field, context.currentPath, sizeClass, uid, context.currentPageStateId, validValues);
233
+ const correctionElement = await api.ensureTestElement(runnerId, surface.id, correction);
234
+ await api.createTestElementRun({
235
+ testElementId: correctionElement.id,
236
+ testSurfaceRunId: surfaceRun.id,
237
+ });
238
+ }
239
+ if (this.isPasswordScenario(formType, form)) {
240
+ const passwordTests = this.buildPasswordTestElements(form, formLabel, formType, context.currentPath, sizeClass, uid, context.currentPageStateId, validValues, this.detectPasswordRequirements(this.extractVisibleText(context.html)));
241
+ for (const passwordTest of passwordTests) {
242
+ const passwordElement = await api.ensureTestElement(runnerId, surface.id, passwordTest);
243
+ await api.createTestElementRun({
244
+ testElementId: passwordElement.id,
245
+ testSurfaceRunId: surfaceRun.id,
246
+ });
247
+ }
248
+ }
249
+ }
250
+ }
251
+ async generateE2ETestElements(context) {
252
+ if (context.journeySteps.length < 2)
253
+ return;
254
+ const { api, runnerId, sizeClass, uid, bundleRun } = context;
255
+ const surface = await api.ensureTestSurface(runnerId, {
256
+ title: `Journeys: ${context.currentPath}`,
257
+ description: `End-to-end journeys reaching ${context.currentPath}`,
258
+ startingPageStateId: context.currentPageStateId,
259
+ startingPath: context.currentPath,
260
+ sizeClass,
261
+ priority: 2,
262
+ surface_tags: ["e2e"],
263
+ uid,
264
+ });
265
+ context.events.onTestSurfaceCreated({
266
+ surfaceId: surface.id,
267
+ title: surface.title,
268
+ });
269
+ await api.ensureBundleSurfaceLink(bundleRun.testSurfaceBundleId, surface.id);
270
+ const surfaceRun = await this.ensureSurfaceRun(api, surface.id, bundleRun.id);
271
+ const e2e = this.buildE2ETestElement(context.currentPath, sizeClass, uid, context.currentPageStateId, context.journeySteps);
272
+ const tc = await api.ensureTestElement(runnerId, surface.id, e2e);
273
+ await api.createTestElementRun({
274
+ testElementId: tc.id,
275
+ testSurfaceRunId: surfaceRun.id,
276
+ });
277
+ }
278
+ async generateSemanticJourneyTestElements(context) {
279
+ const journeys = this.buildSemanticJourneyTestElements(context);
280
+ if (journeys.length === 0)
281
+ return;
282
+ const { api, runnerId, bundleRun } = context;
283
+ const surface = await api.ensureTestSurface(runnerId, {
284
+ title: `Journeys: ${context.currentPath}`,
285
+ description: `Semantic multi-step journeys from ${context.currentPath}`,
286
+ startingPageStateId: context.currentPageStateId,
287
+ startingPath: context.currentPath,
288
+ sizeClass: context.sizeClass,
289
+ priority: 2,
290
+ surface_tags: ["e2e", "semantic-journey"],
291
+ uid: context.uid,
292
+ });
293
+ context.events.onTestSurfaceCreated({
294
+ surfaceId: surface.id,
295
+ title: surface.title,
296
+ });
297
+ await api.ensureBundleSurfaceLink(bundleRun.testSurfaceBundleId, surface.id);
298
+ const surfaceRun = await this.ensureSurfaceRun(api, surface.id, bundleRun.id);
299
+ for (const journey of journeys) {
300
+ const tc = await api.ensureTestElement(runnerId, surface.id, journey);
301
+ await api.createTestElementRun({
302
+ testElementId: tc.id,
303
+ testSurfaceRunId: surfaceRun.id,
304
+ });
305
+ }
306
+ }
307
+ async generateDialogLifecycleTestElements(context) {
308
+ if (!this.pageHasOpenDialog(context.html))
309
+ return;
310
+ const closeCandidates = context.actionableItems.filter(item => item.visible &&
311
+ !item.disabled &&
312
+ item.selector &&
313
+ this.isDialogCloseItem(item));
314
+ const tests = [];
315
+ for (const item of closeCandidates) {
316
+ tests.push(this.buildDialogCloseTestElement(item, context.currentPath, context.sizeClass, context.uid, context.currentPageStateId));
317
+ }
318
+ tests.push(this.buildEscapeDialogTestElement(context.currentPath, context.sizeClass, context.uid, context.currentPageStateId));
319
+ const { api, runnerId, bundleRun } = context;
320
+ const surface = await api.ensureTestSurface(runnerId, {
321
+ title: `Dialogs: ${context.currentPath}`,
322
+ description: `Dialog lifecycle checks for ${context.currentPath}`,
323
+ startingPageStateId: context.currentPageStateId,
324
+ startingPath: context.currentPath,
325
+ sizeClass: context.sizeClass,
326
+ priority: 2,
327
+ surface_tags: ["dialog"],
328
+ uid: context.uid,
329
+ });
330
+ context.events.onTestSurfaceCreated({
331
+ surfaceId: surface.id,
332
+ title: surface.title,
333
+ });
334
+ await api.ensureBundleSurfaceLink(bundleRun.testSurfaceBundleId, surface.id);
335
+ const surfaceRun = await this.ensureSurfaceRun(api, surface.id, bundleRun.id);
336
+ for (const test of tests) {
337
+ const tc = await api.ensureTestElement(runnerId, surface.id, test);
338
+ await api.createTestElementRun({
339
+ testElementId: tc.id,
340
+ testSurfaceRunId: surfaceRun.id,
341
+ });
342
+ }
343
+ }
344
+ async generateKeyboardAndDisclosureTestElements(context) {
345
+ const tests = this.buildKeyboardAndDisclosureTestElements(context);
346
+ if (tests.length === 0)
347
+ return;
348
+ const { api, runnerId, bundleRun } = context;
349
+ const surface = await api.ensureTestSurface(runnerId, {
350
+ title: `Keyboard: ${context.currentPath}`,
351
+ description: `Keyboard parity and disclosure checks for ${context.currentPath}`,
352
+ startingPageStateId: context.currentPageStateId,
353
+ startingPath: context.currentPath,
354
+ sizeClass: context.sizeClass,
355
+ priority: 3,
356
+ surface_tags: ["keyboard", "disclosure"],
357
+ uid: context.uid,
358
+ });
359
+ context.events.onTestSurfaceCreated({
360
+ surfaceId: surface.id,
361
+ title: surface.title,
362
+ });
363
+ await api.ensureBundleSurfaceLink(bundleRun.testSurfaceBundleId, surface.id);
364
+ const surfaceRun = await this.ensureSurfaceRun(api, surface.id, bundleRun.id);
365
+ for (const test of tests) {
366
+ const tc = await api.ensureTestElement(runnerId, surface.id, test);
367
+ await api.createTestElementRun({
368
+ testElementId: tc.id,
369
+ testSurfaceRunId: surfaceRun.id,
370
+ });
371
+ }
372
+ }
153
373
  async generateContentTestElements(context) {
154
374
  const { api, runnerId, sizeClass, uid, bundleRun, pageId: _pageId, } = context;
155
375
  // Content items are those NOT in a scaffold
156
- const contentItems = context.actionableItems.filter(item => item.scaffoldId == null && this.isMouseActionable(item));
376
+ const contentItems = context.actionableItems.filter(item => item.scaffoldId == null && this.isSurfaceCandidate(item));
157
377
  if (contentItems.length === 0)
158
378
  return;
159
379
  const surfaceTitle = `Page: ${context.currentPath}`;
@@ -174,7 +394,9 @@ export class PageAnalyzer {
174
394
  await api.ensureBundleSurfaceLink(bundleRun.testSurfaceBundleId, surface.id);
175
395
  const surfaceRun = await this.ensureSurfaceRun(api, surface.id, bundleRun.id);
176
396
  for (const item of contentItems) {
177
- const testElement = this.buildHoverTestElement(item, context.currentPath, sizeClass, uid, context.currentPageStateId);
397
+ const testElement = this.shouldUseDirectControlInteraction(item)
398
+ ? this.buildControlInteractionTestElement(item, context.currentPath, sizeClass, uid, context.currentPageStateId)
399
+ : this.buildHoverTestElement(item, context.currentPath, sizeClass, uid, context.currentPageStateId);
178
400
  const tc = await api.ensureTestElement(runnerId, surface.id, testElement);
179
401
  await api.createTestElementRun({
180
402
  testElementId: tc.id,
@@ -198,6 +420,11 @@ export class PageAnalyzer {
198
420
  !item.disabled &&
199
421
  (item.actionKind === "click" || item.actionKind === "navigate"));
200
422
  }
423
+ isSurfaceCandidate(item) {
424
+ if (!item.visible || !item.selector || item.disabled)
425
+ return false;
426
+ return ["click", "navigate", "fill", "select", "radio_select"].includes(item.actionKind);
427
+ }
201
428
  extractRelativePath(href) {
202
429
  try {
203
430
  const url = new URL(href, "http://placeholder");
@@ -323,6 +550,7 @@ export class PageAnalyzer {
323
550
  pageStateId: context.currentPageStateId,
324
551
  pageId: context.pageId,
325
552
  });
553
+ await this.ensureStoredForms(context.currentPageStateId, context);
326
554
  return context.currentPageStateId;
327
555
  }
328
556
  const hashes = await computeHashes(context.html, context.actionableItems);
@@ -335,6 +563,7 @@ export class PageAnalyzer {
335
563
  pageStateId: existing.id,
336
564
  pageId: context.pageId,
337
565
  });
566
+ await this.ensureStoredForms(existing.id, context);
338
567
  return existing.id;
339
568
  }
340
569
  const contentHash = createHash("sha256").update(context.html).digest("hex");
@@ -354,6 +583,7 @@ export class PageAnalyzer {
354
583
  if (scaffoldIdsBySelector.size > 0) {
355
584
  await context.api.linkPageStateScaffolds(pageState.id, Array.from(new Set(scaffoldIdsBySelector.values())));
356
585
  }
586
+ await this.ensureStoredForms(pageState.id, context);
357
587
  return pageState.id;
358
588
  }
359
589
  async ensureScaffolds(context) {
@@ -369,5 +599,1260 @@ export class PageAnalyzer {
369
599
  }
370
600
  return scaffoldIdsBySelector;
371
601
  }
602
+ async ensureStoredForms(pageStateId, context) {
603
+ if (context.forms.length === 0)
604
+ return;
605
+ const existing = await context.api.getFormsByPageState(pageStateId);
606
+ const existingSelectors = new Set(existing.map(form => form.selector));
607
+ for (const form of context.forms) {
608
+ if (existingSelectors.has(form.selector))
609
+ continue;
610
+ await context.api.insertForm(pageStateId, form, this.identifyFormType(form, context.currentPath));
611
+ }
612
+ }
613
+ buildRenderTestElement(currentPath, sizeClass, uid, startingPageStateId, pageId) {
614
+ return {
615
+ title: `Render — ${currentPath}`,
616
+ type: "render",
617
+ sizeClass,
618
+ surface_tags: ["render"],
619
+ priority: 2,
620
+ page_id: pageId,
621
+ startingPageStateId,
622
+ startingPath: currentPath,
623
+ steps: [
624
+ {
625
+ action: {
626
+ actionType: PlaywrightAction.Goto,
627
+ path: currentPath,
628
+ playwrightCode: `await page.goto('${currentPath}')`,
629
+ description: `Navigate to ${currentPath}`,
630
+ },
631
+ expectations: [
632
+ {
633
+ expectationType: ExpectationType.PageLoaded,
634
+ severity: ExpectationSeverity.MustPass,
635
+ description: `Page ${currentPath} should load`,
636
+ playwrightCode: "await page.waitForLoadState('networkidle')",
637
+ },
638
+ ],
639
+ description: `Navigate to ${currentPath}`,
640
+ continueOnFailure: false,
641
+ },
642
+ {
643
+ action: {
644
+ actionType: PlaywrightAction.WaitForLoadState,
645
+ playwrightCode: "await page.waitForLoadState('networkidle')",
646
+ description: "Wait for page to settle",
647
+ },
648
+ expectations: [],
649
+ description: "Wait for page to settle",
650
+ continueOnFailure: true,
651
+ },
652
+ {
653
+ action: {
654
+ actionType: PlaywrightAction.Screenshot,
655
+ value: `render-${this.slugify(currentPath)}`,
656
+ playwrightCode: `await page.screenshot({ fullPage: true })`,
657
+ description: "Capture screenshot",
658
+ },
659
+ expectations: [],
660
+ description: "Capture screenshot",
661
+ continueOnFailure: true,
662
+ },
663
+ ],
664
+ globalExpectations: this.defaultFlowExpectations("Render page without runtime errors"),
665
+ uid,
666
+ };
667
+ }
668
+ buildFormTestElement(form, formLabel, formType, currentPath, sizeClass, uid, startingPageStateId, validValues) {
669
+ const steps = this.buildFormSteps(form, validValues, undefined);
670
+ return {
671
+ title: `Form — ${formLabel}`,
672
+ type: "form",
673
+ sizeClass,
674
+ surface_tags: ["form", formType],
675
+ priority: this.formPriority(formType),
676
+ startingPageStateId,
677
+ startingPath: currentPath,
678
+ steps,
679
+ globalExpectations: [
680
+ ...this.defaultFlowExpectations(`Form ${formLabel} should execute cleanly`),
681
+ this.makeExpectation(ExpectationType.FormSubmittedSuccessfully, `Form ${formLabel} should submit without client-side errors`),
682
+ this.makeExpectation("feedback_visible", `Submitting ${formLabel} should provide visible user feedback`, {
683
+ expectedTextTokens: [
684
+ "success",
685
+ "saved",
686
+ "submitted",
687
+ "thank",
688
+ "done",
689
+ ],
690
+ forbiddenTextTokens: ["error", "failed", "try again"],
691
+ }),
692
+ this.makeExpectation("field_error_clears_after_fix", `Validation errors on ${formLabel} should clear once the fields are corrected`),
693
+ ],
694
+ uid,
695
+ };
696
+ }
697
+ buildNegativeFormTestElement(form, formLabel, formType, omittedField, currentPath, sizeClass, uid, startingPageStateId, validValues) {
698
+ const steps = this.buildFormSteps(form, validValues, omittedField.selector);
699
+ return {
700
+ title: `Form Negative — ${formLabel} (missing ${this.fieldLabel(omittedField)})`,
701
+ type: "form_negative",
702
+ sizeClass,
703
+ surface_tags: ["form", "negative", formType],
704
+ priority: this.formPriority(formType) + 1,
705
+ startingPageStateId,
706
+ startingPath: currentPath,
707
+ steps,
708
+ globalExpectations: [
709
+ ...this.defaultFlowExpectations(`Negative form check for ${formLabel}`),
710
+ this.makeExpectation(ExpectationType.ValidationMessageVisible, `Validation feedback should appear when ${this.fieldLabel(omittedField)} is omitted`),
711
+ this.makeExpectation("required_error_shown_for_field", `${this.fieldLabel(omittedField)} should show a required-field error when omitted`, {
712
+ targetPath: omittedField.selector,
713
+ }),
714
+ ],
715
+ uid,
716
+ };
717
+ }
718
+ buildFormCorrectionTestElement(form, formLabel, formType, correctedField, currentPath, sizeClass, uid, startingPageStateId, validValues) {
719
+ const steps = this.buildFormSteps(form, validValues, correctedField.selector);
720
+ const correctionValue = validValues[correctedField.selector];
721
+ if (correctionValue) {
722
+ const correctionStep = this.buildFieldStep(correctedField, correctionValue, this.makeExpectation("field_error_clears_after_fix", `${this.fieldLabel(correctedField)} should clear its validation error after correction`, {
723
+ targetPath: correctedField.selector,
724
+ }));
725
+ if (correctionStep) {
726
+ steps.push(correctionStep);
727
+ }
728
+ }
729
+ steps.push(...this.buildSubmitSteps(form.submitSelector));
730
+ return {
731
+ title: `Form Correction — ${formLabel} (fix ${this.fieldLabel(correctedField)})`,
732
+ type: "form",
733
+ sizeClass,
734
+ surface_tags: ["form", "correction", formType],
735
+ priority: this.formPriority(formType) + 1,
736
+ startingPageStateId,
737
+ startingPath: currentPath,
738
+ steps,
739
+ globalExpectations: [
740
+ ...this.defaultFlowExpectations(`Correction flow for ${formLabel}`),
741
+ this.makeExpectation(ExpectationType.FormSubmittedSuccessfully, `Form ${formLabel} should submit after correcting ${this.fieldLabel(correctedField)}`),
742
+ this.makeExpectation("feedback_visible", `Form ${formLabel} should show success feedback after correction`, {
743
+ expectedTextTokens: [
744
+ "success",
745
+ "saved",
746
+ "submitted",
747
+ "thank",
748
+ "done",
749
+ ],
750
+ forbiddenTextTokens: ["error", "required", "invalid"],
751
+ }),
752
+ ],
753
+ uid,
754
+ };
755
+ }
756
+ buildPasswordTestElements(form, formLabel, formType, currentPath, sizeClass, uid, startingPageStateId, validValues, passwordRequirements) {
757
+ const passwordFields = form.fields.filter(field => field.type === "password");
758
+ if (passwordFields.length === 0)
759
+ return [];
760
+ const variants = this.generatePasswordVariants(passwordRequirements);
761
+ return variants.map(variant => {
762
+ const values = { ...validValues };
763
+ for (const field of passwordFields) {
764
+ values[field.selector] = variant.password;
765
+ }
766
+ return {
767
+ title: `Password — ${formLabel} (${variant.description})`,
768
+ type: "password",
769
+ sizeClass,
770
+ surface_tags: ["form", "password", formType],
771
+ priority: variant.shouldFail ? 2 : 1,
772
+ startingPageStateId,
773
+ startingPath: currentPath,
774
+ steps: this.buildFormSteps(form, values, undefined),
775
+ globalExpectations: [
776
+ ...this.defaultFlowExpectations(`Password flow ${variant.description}`),
777
+ {
778
+ expectationType: variant.shouldFail
779
+ ? ExpectationType.ValidationMessageVisible
780
+ : ExpectationType.FormSubmittedSuccessfully,
781
+ severity: ExpectationSeverity.ShouldPass,
782
+ description: variant.shouldFail
783
+ ? `Password validation should reject ${variant.description}`
784
+ : `Password validation should accept ${variant.description}`,
785
+ playwrightCode: "// checked by password follow-up validation",
786
+ },
787
+ ],
788
+ uid,
789
+ };
790
+ });
791
+ }
792
+ buildE2ETestElement(currentPath, sizeClass, uid, startingPageStateId, journeySteps) {
793
+ return {
794
+ title: `E2E — Journey to ${currentPath}`,
795
+ type: "e2e",
796
+ sizeClass,
797
+ surface_tags: ["e2e"],
798
+ priority: 2,
799
+ startingPageStateId,
800
+ startingPath: currentPath,
801
+ steps: journeySteps.map(step => ({
802
+ ...step,
803
+ expectations: [],
804
+ })),
805
+ globalExpectations: this.defaultFlowExpectations(`Journey to ${currentPath} should complete without runtime errors`),
806
+ uid,
807
+ };
808
+ }
809
+ buildFormSteps(form, valuesBySelector, omittedSelector) {
810
+ const steps = [];
811
+ for (const field of form.fields) {
812
+ if (field.selector === omittedSelector)
813
+ continue;
814
+ const analyzerField = field;
815
+ const fieldIsImmutable = Boolean(analyzerField.disabled ||
816
+ analyzerField.readOnly ||
817
+ this.looksVisuallyDisabledField(analyzerField));
818
+ if (fieldIsImmutable)
819
+ continue;
820
+ const value = valuesBySelector[field.selector];
821
+ if (!value || this.isSkippableFieldType(field))
822
+ continue;
823
+ const step = this.buildFieldStep(field, value);
824
+ if (step)
825
+ steps.push(step);
826
+ }
827
+ steps.push(...this.buildSubmitSteps(form.submitSelector));
828
+ return steps;
829
+ }
830
+ buildFieldStep(field, value, trailingExpectation) {
831
+ const analyzerField = field;
832
+ const fieldIsImmutable = Boolean(analyzerField.disabled ||
833
+ analyzerField.readOnly ||
834
+ this.looksVisuallyDisabledField(analyzerField));
835
+ if (fieldIsImmutable)
836
+ return null;
837
+ if (this.isCheckboxLike(field)) {
838
+ return {
839
+ action: {
840
+ actionType: PlaywrightAction.Check,
841
+ path: field.selector,
842
+ value,
843
+ playwrightCode: `await page.locator('${field.selector}').check()`,
844
+ description: `Check ${this.fieldLabel(field)}`,
845
+ },
846
+ expectations: [
847
+ this.makeExpectation(ExpectationType.ElementChecked, `Checking ${this.fieldLabel(field)} should update the control state`, {
848
+ targetPath: field.selector,
849
+ }),
850
+ ...(trailingExpectation ? [trailingExpectation] : []),
851
+ ],
852
+ description: `Check ${this.fieldLabel(field)}`,
853
+ continueOnFailure: false,
854
+ };
855
+ }
856
+ if (this.isSelectLike(field)) {
857
+ return {
858
+ action: {
859
+ actionType: PlaywrightAction.SelectOption,
860
+ path: field.selector,
861
+ value,
862
+ playwrightCode: `await page.locator('${field.selector}').selectOption('${value}')`,
863
+ description: `Select ${this.fieldLabel(field)}`,
864
+ },
865
+ expectations: [
866
+ this.makeExpectation(ExpectationType.InputValue, `Selecting ${this.fieldLabel(field)} should update the selected option`, {
867
+ targetPath: field.selector,
868
+ expectedValue: value,
869
+ }),
870
+ ...(trailingExpectation ? [trailingExpectation] : []),
871
+ ],
872
+ description: `Select ${this.fieldLabel(field)}`,
873
+ continueOnFailure: false,
874
+ };
875
+ }
876
+ return {
877
+ action: {
878
+ actionType: PlaywrightAction.Type,
879
+ path: field.selector,
880
+ value,
881
+ playwrightCode: `await page.locator('${field.selector}').type('${this.escapeSingleQuotes(value)}')`,
882
+ description: `Fill ${this.fieldLabel(field)}`,
883
+ },
884
+ expectations: [
885
+ this.makeExpectation(ExpectationType.InputValue, `Typing into ${this.fieldLabel(field)} should update the control value`, {
886
+ targetPath: field.selector,
887
+ expectedValue: value,
888
+ }),
889
+ ...(trailingExpectation ? [trailingExpectation] : []),
890
+ ],
891
+ description: `Fill ${this.fieldLabel(field)}`,
892
+ continueOnFailure: false,
893
+ };
894
+ }
895
+ buildSubmitSteps(submitSelector) {
896
+ if (!submitSelector)
897
+ return [];
898
+ return [
899
+ {
900
+ action: {
901
+ actionType: PlaywrightAction.Click,
902
+ path: submitSelector,
903
+ playwrightCode: `await page.locator('${submitSelector}').click()`,
904
+ description: "Submit form",
905
+ },
906
+ expectations: [],
907
+ description: "Submit form",
908
+ continueOnFailure: false,
909
+ },
910
+ {
911
+ action: {
912
+ actionType: PlaywrightAction.WaitForLoadState,
913
+ playwrightCode: "await page.waitForLoadState('networkidle')",
914
+ description: "Wait for post-submit state",
915
+ },
916
+ expectations: [],
917
+ description: "Wait for post-submit state",
918
+ continueOnFailure: true,
919
+ },
920
+ ];
921
+ }
922
+ planFormValues(form, actionableItems) {
923
+ const values = {};
924
+ for (const field of form.fields) {
925
+ const analyzerField = field;
926
+ if (analyzerField.disabled ||
927
+ analyzerField.readOnly ||
928
+ this.looksVisuallyDisabledField(analyzerField)) {
929
+ continue;
930
+ }
931
+ if (this.isCheckboxLike(field)) {
932
+ values[field.selector] = "true";
933
+ continue;
934
+ }
935
+ if (this.isSelectLike(field)) {
936
+ const option = field.options?.find(value => value && value.trim().length > 0);
937
+ if (option)
938
+ values[field.selector] = option;
939
+ continue;
940
+ }
941
+ const item = actionableItems.find(candidate => candidate.selector === field.selector);
942
+ const fallbackItem = item ?? {
943
+ stableKey: field.selector,
944
+ selector: field.selector,
945
+ tagName: field.type === "textarea" ? "TEXTAREA" : "INPUT",
946
+ inputType: field.type,
947
+ actionKind: "fill",
948
+ accessibleName: field.label,
949
+ disabled: false,
950
+ visible: true,
951
+ attributes: {
952
+ name: field.name,
953
+ placeholder: field.placeholder,
954
+ labelText: field.label,
955
+ },
956
+ };
957
+ values[field.selector] = fillValuePlanner.planValue(fallbackItem);
958
+ }
959
+ return values;
960
+ }
961
+ identifyFormType(form, currentPath) {
962
+ const url = currentPath.toLowerCase();
963
+ const fields = form.fields;
964
+ const isLoginUrl = AUTH_URL_PATTERNS.some(pattern => url.includes(pattern));
965
+ const isSignupUrl = SIGNUP_URL_PATTERNS.some(pattern => url.includes(pattern));
966
+ const hasPassword = fields.some(field => field.type === "password");
967
+ const hasEmail = fields.some(field => {
968
+ const label = field.label.toLowerCase();
969
+ return (field.type === "email" ||
970
+ field.name.toLowerCase() === "email" ||
971
+ label.includes("email"));
972
+ });
973
+ const hasUsername = fields.some(field => {
974
+ const signal = `${field.name} ${field.label}`.toLowerCase();
975
+ return signal.includes("username") || signal.includes("user name");
976
+ });
977
+ const hasName = fields.some(field => {
978
+ const signal = `${field.name} ${field.label}`.toLowerCase();
979
+ return (signal.includes("name") &&
980
+ !signal.includes("username") &&
981
+ !signal.includes("email"));
982
+ });
983
+ const hasMessage = fields.some(field => {
984
+ const signal = `${field.name} ${field.label}`.toLowerCase();
985
+ return signal.includes("message") || signal.includes("subject");
986
+ });
987
+ const passwordCount = fields.filter(field => field.type === "password").length;
988
+ if (hasMessage)
989
+ return "other";
990
+ if (!hasPassword)
991
+ return "other";
992
+ if (hasEmail || hasUsername) {
993
+ if (passwordCount >= 2 || hasName || isSignupUrl)
994
+ return "signup";
995
+ if (isLoginUrl)
996
+ return "login";
997
+ if (fields.length <= 2)
998
+ return "login";
999
+ return hasName ? "signup" : "login";
1000
+ }
1001
+ if (isSignupUrl)
1002
+ return "signup";
1003
+ if (isLoginUrl)
1004
+ return "login";
1005
+ return "other";
1006
+ }
1007
+ describeForm(form, index) {
1008
+ const namedField = form.fields.find(field => field.label || field.name);
1009
+ const descriptor = namedField?.label || namedField?.name || `form ${index + 1}`;
1010
+ return `${descriptor} @ ${form.selector}`;
1011
+ }
1012
+ buildSemanticJourneyTestElements(context) {
1013
+ const items = context.actionableItems.filter(item => item.visible && !item.disabled && Boolean(item.selector));
1014
+ const journeys = [];
1015
+ const addToCart = items.find(item => this.isAddToCartItem(item));
1016
+ const checkout = items.find(item => this.isCheckoutItem(item));
1017
+ if (addToCart && checkout) {
1018
+ journeys.push(this.buildJourneyTestElement("Commerce journey", ["commerce", "cart", "checkout"], context, [
1019
+ this.buildJourneyAction(addToCart, "Add item to cart", [
1020
+ this.makeExpectation("navigation_or_state_changed", "Adding an item to cart should update the page state"),
1021
+ this.makeExpectation("count_changed", "Adding an item should update a visible count", {
1022
+ expectedCountDelta: 1,
1023
+ }),
1024
+ this.makeExpectation("cart_summary_changed", "Adding an item should update the cart summary"),
1025
+ this.makeExpectation("feedback_visible", "Adding an item should provide visible feedback", {
1026
+ expectedTextTokens: ["added", "success", "cart", "bag"],
1027
+ forbiddenTextTokens: ["error", "failed"],
1028
+ }),
1029
+ this.makeExpectation("loading_completes", "Cart update should complete loading"),
1030
+ this.makeExpectation("page_responsive", "Page should remain responsive after cart update"),
1031
+ ]),
1032
+ this.waitStep(700, "Wait for cart state"),
1033
+ this.buildJourneyAction(checkout, "Proceed to checkout", [
1034
+ this.makeExpectation("navigation_or_state_changed", "Proceeding to checkout should change the page state"),
1035
+ this.makeExpectation("loading_completes", "Checkout transition should complete loading"),
1036
+ this.makeExpectation("page_responsive", "Page should remain responsive during checkout transition"),
1037
+ ]),
1038
+ ]));
1039
+ }
1040
+ const removeItem = items.find(item => this.isRemoveItemAction(item));
1041
+ if (removeItem) {
1042
+ journeys.push(this.buildJourneyTestElement("Remove item from collection", ["commerce", "remove"], context, [
1043
+ this.buildJourneyAction(removeItem, "Remove item", [
1044
+ this.makeExpectation("navigation_or_state_changed", "Removing an item should change the page state"),
1045
+ this.makeExpectation("count_changed", "Removing an item should update a visible count", {
1046
+ expectedCountDelta: -1,
1047
+ }),
1048
+ this.makeExpectation("cart_summary_changed", "Removing an item should update the cart summary"),
1049
+ this.makeExpectation("feedback_visible", "Removing an item should provide visible feedback", {
1050
+ expectedTextTokens: ["removed", "updated", "cart", "bag"],
1051
+ forbiddenTextTokens: ["error", "failed"],
1052
+ }),
1053
+ this.makeExpectation("loading_completes", "Removal flow should complete loading"),
1054
+ this.makeExpectation("page_responsive", "Page should remain responsive after removal"),
1055
+ ]),
1056
+ ]));
1057
+ }
1058
+ const authEntry = items.find(item => this.isAuthEntryItem(item));
1059
+ if (authEntry) {
1060
+ journeys.push(this.buildJourneyTestElement("Authentication entry journey", ["auth"], context, [
1061
+ this.buildJourneyAction(authEntry, "Open authentication entry point", [
1062
+ this.makeExpectation("navigation_or_state_changed", "Authentication entry should open the next auth state"),
1063
+ this.makeExpectation("loading_completes", "Authentication entry flow should settle"),
1064
+ this.makeExpectation("page_responsive", "Page should remain responsive when opening auth flow"),
1065
+ ]),
1066
+ ]));
1067
+ }
1068
+ const mediaCandidate = items.find(item => this.isMediaOpenItem(item));
1069
+ if (mediaCandidate) {
1070
+ const mediaExpectations = [
1071
+ this.makeExpectation("modal_opened", "Opening media should reveal a modal, overlay, or new state"),
1072
+ this.makeExpectation("media_loaded", "Opened media should load successfully"),
1073
+ ];
1074
+ if (this.isVideoLikeItem(mediaCandidate)) {
1075
+ mediaExpectations.push(this.makeExpectation("video_playable", "Opened video should be playable"));
1076
+ }
1077
+ journeys.push(this.buildJourneyTestElement("Media open journey", ["media"], context, [
1078
+ this.buildJourneyAction(mediaCandidate, "Open media", mediaExpectations),
1079
+ ]));
1080
+ }
1081
+ const quantityAction = items.find(item => this.isQuantityAction(item));
1082
+ if (quantityAction) {
1083
+ journeys.push(this.buildJourneyTestElement("Quantity adjustment journey", ["commerce", "quantity"], context, [
1084
+ this.buildJourneyAction(quantityAction, "Adjust quantity", [
1085
+ this.makeExpectation("count_changed", "Adjusting quantity should update a visible count or quantity indicator"),
1086
+ this.makeExpectation("cart_summary_changed", "Adjusting quantity should update the summary values"),
1087
+ this.makeExpectation("loading_completes", "Quantity adjustment should complete loading"),
1088
+ ]),
1089
+ ]));
1090
+ }
1091
+ const filterAction = items.find(item => this.isFilterAction(item));
1092
+ if (filterAction) {
1093
+ journeys.push(this.buildJourneyTestElement("Filter results journey", ["filter"], context, [
1094
+ this.buildJourneyAction(filterAction, "Apply filter", [
1095
+ this.makeExpectation("navigation_or_state_changed", "Applying a filter should change the result state"),
1096
+ this.makeExpectation("results_changed", "Applying a filter should change the visible result summary"),
1097
+ this.makeExpectation("loading_completes", "Filter update should complete loading"),
1098
+ ]),
1099
+ ]));
1100
+ journeys.push(this.buildJourneyTestElement("Filter persistence journey", ["filter", "reload"], context, [
1101
+ this.buildJourneyAction(filterAction, "Apply filter", [
1102
+ this.makeExpectation("navigation_or_state_changed", "Applying a filter should change the result state"),
1103
+ ]),
1104
+ this.waitStep(500, "Wait for filtered state"),
1105
+ this.buildReloadStep([
1106
+ this.makeExpectation("state_persists_after_reload", "Filter state should persist after reload", {
1107
+ expectedTextTokens: ["filter", "results", "showing", "items"],
1108
+ }),
1109
+ ]),
1110
+ ]));
1111
+ }
1112
+ const sortAction = items.find(item => this.isSortAction(item));
1113
+ if (sortAction) {
1114
+ journeys.push(this.buildJourneyTestElement("Sort results journey", ["sort"], context, [
1115
+ this.buildJourneyAction(sortAction, "Change sort order", [
1116
+ this.makeExpectation("navigation_or_state_changed", "Changing sort should update the result state"),
1117
+ this.makeExpectation("collection_order_changed", "Changing sort should change the visible collection ordering"),
1118
+ this.makeExpectation("loading_completes", "Sort update should complete loading"),
1119
+ ]),
1120
+ ]));
1121
+ }
1122
+ if (addToCart) {
1123
+ journeys.push(this.buildJourneyTestElement("Cart persistence journey", ["commerce", "reload"], context, [
1124
+ this.buildJourneyAction(addToCart, "Add item to cart", [
1125
+ this.makeExpectation("navigation_or_state_changed", "Adding an item should change page state before reload"),
1126
+ ]),
1127
+ this.waitStep(500, "Wait for updated cart state"),
1128
+ this.buildReloadStep([
1129
+ this.makeExpectation("state_persists_after_reload", "Cart state should persist after reload", {
1130
+ expectedTextTokens: [
1131
+ "cart",
1132
+ "bag",
1133
+ "basket",
1134
+ "qty",
1135
+ "quantity",
1136
+ ],
1137
+ }),
1138
+ ]),
1139
+ ]));
1140
+ }
1141
+ const backCandidate = items.find(item => item.actionKind === "navigate" ||
1142
+ this.isMediaOpenItem(item) ||
1143
+ this.isAuthEntryItem(item));
1144
+ if (backCandidate) {
1145
+ journeys.push(this.buildJourneyTestElement("Back and forward navigation journey", ["navigation", "history"], context, [
1146
+ this.buildJourneyAction(backCandidate, "Navigate to next state", [
1147
+ this.makeExpectation("navigation_or_state_changed", "Navigation should move to a different state"),
1148
+ ]),
1149
+ this.waitStep(500, "Wait for next state"),
1150
+ this.buildBackStep([
1151
+ this.makeExpectation("back_navigation_restores_state", "Back navigation should restore the previous state"),
1152
+ ]),
1153
+ this.waitStep(300, "Wait after back navigation"),
1154
+ this.buildForwardStep([
1155
+ this.makeExpectation("forward_navigation_reapplies_state", "Forward navigation should reapply the later state"),
1156
+ ]),
1157
+ ]));
1158
+ }
1159
+ return journeys;
1160
+ }
1161
+ buildJourneyTestElement(title, surfaceTags, context, steps) {
1162
+ return {
1163
+ title: `${title} — ${context.currentPath}`,
1164
+ type: "e2e",
1165
+ sizeClass: context.sizeClass,
1166
+ surface_tags: ["e2e", ...surfaceTags],
1167
+ priority: 2,
1168
+ startingPageStateId: context.currentPageStateId,
1169
+ startingPath: context.currentPath,
1170
+ steps,
1171
+ globalExpectations: this.defaultFlowExpectations(`${title} should complete without runtime errors`),
1172
+ uid: context.uid,
1173
+ };
1174
+ }
1175
+ buildJourneyAction(item, description, expectations) {
1176
+ const signal = this.describeActionableItem(item);
1177
+ const action = this.buildSemanticAction(item, description, signal);
1178
+ return {
1179
+ action,
1180
+ expectations,
1181
+ description: `${description}: ${signal}`,
1182
+ continueOnFailure: false,
1183
+ };
1184
+ }
1185
+ waitStep(ms, description) {
1186
+ return {
1187
+ action: {
1188
+ actionType: PlaywrightAction.WaitForTimeout,
1189
+ value: String(ms),
1190
+ playwrightCode: `await page.waitForTimeout(${ms})`,
1191
+ description,
1192
+ },
1193
+ expectations: [],
1194
+ description,
1195
+ continueOnFailure: true,
1196
+ };
1197
+ }
1198
+ buildReloadStep(expectations) {
1199
+ return {
1200
+ action: {
1201
+ actionType: PlaywrightAction.Reload,
1202
+ playwrightCode: "await page.reload({ waitUntil: 'networkidle' })",
1203
+ description: "Reload page",
1204
+ },
1205
+ expectations,
1206
+ description: "Reload page",
1207
+ continueOnFailure: false,
1208
+ };
1209
+ }
1210
+ buildBackStep(expectations) {
1211
+ return {
1212
+ action: {
1213
+ actionType: PlaywrightAction.GoBack,
1214
+ playwrightCode: "await page.goBack()",
1215
+ description: "Go back",
1216
+ },
1217
+ expectations,
1218
+ description: "Go back",
1219
+ continueOnFailure: false,
1220
+ };
1221
+ }
1222
+ buildForwardStep(expectations) {
1223
+ return {
1224
+ action: {
1225
+ actionType: PlaywrightAction.GoForward,
1226
+ playwrightCode: "await page.goForward()",
1227
+ description: "Go forward",
1228
+ },
1229
+ expectations,
1230
+ description: "Go forward",
1231
+ continueOnFailure: false,
1232
+ };
1233
+ }
1234
+ defaultFlowExpectations(description) {
1235
+ return [
1236
+ {
1237
+ expectationType: ExpectationType.PageLoaded,
1238
+ severity: ExpectationSeverity.MustPass,
1239
+ description,
1240
+ playwrightCode: "await page.waitForLoadState('networkidle')",
1241
+ },
1242
+ {
1243
+ expectationType: ExpectationType.NoNetworkErrors,
1244
+ severity: ExpectationSeverity.MustPass,
1245
+ description: "No network errors during flow",
1246
+ playwrightCode: "// checked by TesterExpertise",
1247
+ },
1248
+ {
1249
+ expectationType: ExpectationType.NoConsoleErrors,
1250
+ severity: ExpectationSeverity.ShouldPass,
1251
+ description: "No console errors during flow",
1252
+ playwrightCode: "// checked by TesterExpertise",
1253
+ },
1254
+ ];
1255
+ }
1256
+ formPriority(formType) {
1257
+ if (formType === "login" || formType === "signup")
1258
+ return 1;
1259
+ return 2;
1260
+ }
1261
+ isNegativeCandidateField(field) {
1262
+ const analyzerField = field;
1263
+ return (field.required &&
1264
+ !analyzerField.disabled &&
1265
+ !analyzerField.readOnly &&
1266
+ !this.looksVisuallyDisabledField(analyzerField) &&
1267
+ !this.isCheckboxLike(field) &&
1268
+ !this.isSkippableFieldType(field));
1269
+ }
1270
+ isPasswordScenario(formType, form) {
1271
+ return (formType !== "other" &&
1272
+ form.fields.some(field => field.type === "password"));
1273
+ }
1274
+ isCheckboxLike(field) {
1275
+ return field.type === "checkbox" || field.type === "radio";
1276
+ }
1277
+ buildKeyboardAndDisclosureTestElements(context) {
1278
+ const items = context.actionableItems.filter(item => item.visible && !item.disabled && Boolean(item.selector));
1279
+ const tests = [];
1280
+ for (const item of items) {
1281
+ if (this.isDisclosureItem(item)) {
1282
+ tests.push(this.buildDisclosureToggleTestElement(item, context.currentPath, context.sizeClass, context.uid, context.currentPageStateId));
1283
+ tests.push(this.buildKeyboardActivateTestElement(item, "Enter", "Activate disclosure with Enter", [
1284
+ this.makeExpectation("expanded_state_changed", "Enter should toggle the disclosure state", {
1285
+ targetPath: item.selector,
1286
+ }),
1287
+ ], context.currentPath, context.sizeClass, context.uid, context.currentPageStateId));
1288
+ tests.push(this.buildKeyboardActivateTestElement(item, " ", "Activate disclosure with Space", [
1289
+ this.makeExpectation("expanded_state_changed", "Space should toggle the disclosure state", {
1290
+ targetPath: item.selector,
1291
+ }),
1292
+ ], context.currentPath, context.sizeClass, context.uid, context.currentPageStateId));
1293
+ continue;
1294
+ }
1295
+ if (this.isKeyboardPrimaryAction(item)) {
1296
+ tests.push(this.buildKeyboardActivateTestElement(item, "Enter", "Activate with Enter", [
1297
+ this.makeExpectation("navigation_or_state_changed", "Enter key activation should change the page or control state"),
1298
+ ], context.currentPath, context.sizeClass, context.uid, context.currentPageStateId));
1299
+ }
1300
+ if (this.isKeyboardToggleAction(item)) {
1301
+ tests.push(this.buildKeyboardActivateTestElement(item, " ", "Toggle with Space", [
1302
+ this.makeExpectation(ExpectationType.ElementChecked, "Space key activation should toggle the control state", {
1303
+ targetPath: item.selector,
1304
+ }),
1305
+ ], context.currentPath, context.sizeClass, context.uid, context.currentPageStateId));
1306
+ }
1307
+ }
1308
+ return tests;
1309
+ }
1310
+ buildDisclosureToggleTestElement(item, startingPath, sizeClass, uid, startingPageStateId) {
1311
+ const label = this.describeActionableItem(item);
1312
+ return {
1313
+ title: `Toggle disclosure ${label}`,
1314
+ type: "interaction",
1315
+ sizeClass,
1316
+ surface_tags: ["disclosure", "click"],
1317
+ priority: 3,
1318
+ startingPageStateId,
1319
+ startingPath,
1320
+ steps: [
1321
+ {
1322
+ action: {
1323
+ actionType: PlaywrightAction.Click,
1324
+ path: item.selector,
1325
+ playwrightCode: `await page.click('${item.selector}')`,
1326
+ description: `Toggle disclosure ${label}`,
1327
+ },
1328
+ expectations: [
1329
+ this.makeExpectation("expanded_state_changed", "Clicking the disclosure should toggle its expanded state", {
1330
+ targetPath: item.selector,
1331
+ }),
1332
+ ],
1333
+ description: `Toggle disclosure ${label}`,
1334
+ continueOnFailure: false,
1335
+ },
1336
+ ],
1337
+ globalExpectations: this.defaultFlowExpectations("Disclosure interaction should complete without runtime errors"),
1338
+ uid,
1339
+ };
1340
+ }
1341
+ buildKeyboardActivateTestElement(item, key, titlePrefix, expectations, startingPath, sizeClass, uid, startingPageStateId) {
1342
+ const label = this.describeActionableItem(item);
1343
+ return {
1344
+ title: `${titlePrefix} ${label}`,
1345
+ type: "interaction",
1346
+ sizeClass,
1347
+ surface_tags: [
1348
+ "keyboard",
1349
+ key.trim() === "" ? "space" : key.toLowerCase(),
1350
+ ],
1351
+ priority: 3,
1352
+ startingPageStateId,
1353
+ startingPath,
1354
+ steps: [
1355
+ {
1356
+ action: {
1357
+ actionType: PlaywrightAction.Focus,
1358
+ path: item.selector,
1359
+ playwrightCode: `await page.locator('${item.selector}').focus()`,
1360
+ description: `Focus ${label}`,
1361
+ },
1362
+ expectations: [],
1363
+ description: `Focus ${label}`,
1364
+ continueOnFailure: false,
1365
+ },
1366
+ {
1367
+ action: {
1368
+ actionType: PlaywrightAction.Press,
1369
+ value: key,
1370
+ playwrightCode: `await page.keyboard.press('${key === " " ? "Space" : key}')`,
1371
+ description: `${titlePrefix} ${label}`,
1372
+ },
1373
+ expectations,
1374
+ description: `${titlePrefix} ${label}`,
1375
+ continueOnFailure: false,
1376
+ },
1377
+ ],
1378
+ globalExpectations: this.defaultFlowExpectations("Keyboard activation should complete without runtime errors"),
1379
+ uid,
1380
+ };
1381
+ }
1382
+ buildDialogCloseTestElement(item, startingPath, sizeClass, uid, startingPageStateId) {
1383
+ const label = this.describeActionableItem(item);
1384
+ return {
1385
+ title: `Close dialog via ${label}`,
1386
+ type: "interaction",
1387
+ sizeClass,
1388
+ surface_tags: ["dialog", "close"],
1389
+ priority: 2,
1390
+ startingPageStateId,
1391
+ startingPath,
1392
+ steps: [
1393
+ {
1394
+ action: {
1395
+ actionType: PlaywrightAction.Click,
1396
+ path: item.selector,
1397
+ playwrightCode: `await page.click('${item.selector}')`,
1398
+ description: `Close dialog via ${label}`,
1399
+ },
1400
+ expectations: [
1401
+ this.makeExpectation("dialog_closed", "Close action should dismiss the open dialog"),
1402
+ this.makeExpectation("focus_returned", "Focus should return after the dialog closes"),
1403
+ ],
1404
+ description: `Close dialog via ${label}`,
1405
+ continueOnFailure: false,
1406
+ },
1407
+ ],
1408
+ globalExpectations: this.defaultFlowExpectations("Dialog should close cleanly"),
1409
+ uid,
1410
+ };
1411
+ }
1412
+ buildEscapeDialogTestElement(startingPath, sizeClass, uid, startingPageStateId) {
1413
+ return {
1414
+ title: "Close dialog with Escape",
1415
+ type: "interaction",
1416
+ sizeClass,
1417
+ surface_tags: ["dialog", "escape"],
1418
+ priority: 2,
1419
+ startingPageStateId,
1420
+ startingPath,
1421
+ steps: [
1422
+ {
1423
+ action: {
1424
+ actionType: PlaywrightAction.Press,
1425
+ value: "Escape",
1426
+ playwrightCode: "await page.keyboard.press('Escape')",
1427
+ description: "Press Escape",
1428
+ },
1429
+ expectations: [
1430
+ this.makeExpectation("dialog_closed", "Escape should dismiss the open dialog"),
1431
+ this.makeExpectation("focus_returned", "Focus should return after Escape closes the dialog"),
1432
+ ],
1433
+ description: "Press Escape",
1434
+ continueOnFailure: false,
1435
+ },
1436
+ ],
1437
+ globalExpectations: this.defaultFlowExpectations("Dialog should close on Escape without runtime errors"),
1438
+ uid,
1439
+ };
1440
+ }
1441
+ shouldUseDirectControlInteraction(item) {
1442
+ const role = (item.role ?? "").toLowerCase();
1443
+ const inputType = (item.inputType ?? "").toLowerCase();
1444
+ return (this.looksVisuallyDisabledButEnabled(item) ||
1445
+ item.actionKind === "fill" ||
1446
+ item.actionKind === "select" ||
1447
+ item.actionKind === "radio_select" ||
1448
+ role === "tab" ||
1449
+ role === "radio" ||
1450
+ role === "checkbox" ||
1451
+ role === "switch" ||
1452
+ inputType === "radio" ||
1453
+ inputType === "checkbox");
1454
+ }
1455
+ buildControlInteractionTestElement(item, startingPath, sizeClass, uid, startingPageStateId) {
1456
+ const label = item.accessibleName || item.textContent || item.selector;
1457
+ const role = (item.role ?? "").toLowerCase();
1458
+ const inputType = (item.inputType ?? "").toLowerCase();
1459
+ const isTab = role === "tab";
1460
+ const isFillControl = item.actionKind === "fill";
1461
+ const isSelectControl = item.actionKind === "select";
1462
+ const isRadioControl = item.actionKind === "radio_select" ||
1463
+ role === "radio" ||
1464
+ inputType === "radio";
1465
+ const isCheckboxControl = role === "checkbox" || role === "switch" || inputType === "checkbox";
1466
+ const expectsChecked = isRadioControl || isCheckboxControl || isTab;
1467
+ const expectsInputValue = isFillControl || isSelectControl;
1468
+ const plannedValue = isSelectControl
1469
+ ? this.extractSelectableValue(item)
1470
+ : isFillControl
1471
+ ? fillValuePlanner.planValue(item)
1472
+ : undefined;
1473
+ const isImmutable = this.looksVisuallyDisabledButEnabled(item);
1474
+ let actionType = PlaywrightAction.Click;
1475
+ let actionValue;
1476
+ let playwrightCode = `await page.click('${item.selector}')`;
1477
+ let description = `${isTab ? "Select" : "Activate"} ${label}`;
1478
+ if (isFillControl) {
1479
+ actionType = PlaywrightAction.Type;
1480
+ actionValue = plannedValue;
1481
+ playwrightCode = `await page.locator('${item.selector}').type('${this.escapeSingleQuotes(plannedValue ?? "")}')`;
1482
+ description = `Type into ${label}`;
1483
+ }
1484
+ else if (isSelectControl) {
1485
+ actionType = PlaywrightAction.SelectOption;
1486
+ actionValue = plannedValue;
1487
+ playwrightCode = `await page.locator('${item.selector}').selectOption('${this.escapeSingleQuotes(plannedValue ?? "")}')`;
1488
+ description = `Select ${label}`;
1489
+ }
1490
+ else if (isRadioControl) {
1491
+ actionType = PlaywrightAction.Click;
1492
+ playwrightCode = `await page.click('${item.selector}')`;
1493
+ description = `Select ${label}`;
1494
+ }
1495
+ const expectations = isImmutable
1496
+ ? this.buildImmutableControlExpectations(item, label)
1497
+ : expectsInputValue
1498
+ ? [
1499
+ this.makeExpectation(ExpectationType.InputValue, `${description} should update the control value`, {
1500
+ targetPath: item.selector,
1501
+ expectedValue: plannedValue,
1502
+ }),
1503
+ ]
1504
+ : expectsChecked
1505
+ ? [
1506
+ this.makeExpectation(ExpectationType.ElementChecked, `${label} should react to user input`, {
1507
+ targetPath: item.selector,
1508
+ }),
1509
+ ]
1510
+ : [];
1511
+ return {
1512
+ title: isImmutable ? `Visually Disabled ${description}` : description,
1513
+ type: "interaction",
1514
+ sizeClass,
1515
+ surface_tags: [
1516
+ "interaction",
1517
+ isImmutable ? "visually-disabled" : "enabled",
1518
+ role || inputType || item.actionKind || "control",
1519
+ ],
1520
+ priority: 3,
1521
+ startingPageStateId,
1522
+ startingPath,
1523
+ steps: [
1524
+ {
1525
+ action: {
1526
+ actionType,
1527
+ path: item.selector ?? undefined,
1528
+ value: actionValue,
1529
+ playwrightCode,
1530
+ description,
1531
+ },
1532
+ expectations,
1533
+ description,
1534
+ continueOnFailure: isImmutable,
1535
+ },
1536
+ ],
1537
+ globalExpectations: this.defaultFlowExpectations(isImmutable
1538
+ ? `${label} should remain non-interactive despite appearing disabled`
1539
+ : `${label} should react without runtime errors`),
1540
+ uid,
1541
+ };
1542
+ }
1543
+ buildImmutableControlExpectations(item, label) {
1544
+ const role = (item.role ?? "").toLowerCase();
1545
+ const inputType = (item.inputType ?? "").toLowerCase();
1546
+ if (item.actionKind === "fill" || item.actionKind === "select") {
1547
+ return [
1548
+ this.makeExpectation(ExpectationType.InputValue, `${label} should not respond to user input while it appears disabled`, {
1549
+ targetPath: item.selector,
1550
+ expectNoChange: true,
1551
+ }),
1552
+ ];
1553
+ }
1554
+ if (role === "tab" ||
1555
+ role === "radio" ||
1556
+ role === "checkbox" ||
1557
+ role === "switch" ||
1558
+ inputType === "radio" ||
1559
+ inputType === "checkbox" ||
1560
+ item.actionKind === "radio_select") {
1561
+ return [
1562
+ this.makeExpectation(ExpectationType.ElementChecked, `${label} should not change state while it appears disabled`, {
1563
+ targetPath: item.selector,
1564
+ expectNoChange: true,
1565
+ }),
1566
+ ];
1567
+ }
1568
+ return [
1569
+ this.makeExpectation(ExpectationType.UrlUnchanged, `${label} should not trigger navigation while it appears disabled`, {
1570
+ expectNoChange: true,
1571
+ }),
1572
+ ];
1573
+ }
1574
+ extractSelectableValue(item) {
1575
+ const rawOptions = item.attributes?.options;
1576
+ const parsedOptions = Array.isArray(rawOptions)
1577
+ ? rawOptions
1578
+ : typeof rawOptions === "string"
1579
+ ? this.parseSelectOptions(rawOptions)
1580
+ : [];
1581
+ return parsedOptions.find((value) => typeof value === "string" && value.trim().length > 0);
1582
+ }
1583
+ parseSelectOptions(rawOptions) {
1584
+ try {
1585
+ const parsed = JSON.parse(rawOptions);
1586
+ return Array.isArray(parsed) ? parsed.filter(Boolean) : [];
1587
+ }
1588
+ catch {
1589
+ return rawOptions
1590
+ .split(",")
1591
+ .map(value => value.trim())
1592
+ .filter(Boolean);
1593
+ }
1594
+ }
1595
+ looksVisuallyDisabledButEnabled(item) {
1596
+ if (item.disabled)
1597
+ return false;
1598
+ const attrs = Object.entries(item.attributes ?? {})
1599
+ .map(([key, value]) => `${key}=${String(value)}`)
1600
+ .join(" ")
1601
+ .toLowerCase();
1602
+ return this.hasDisabledAppearanceSignal(attrs);
1603
+ }
1604
+ looksVisuallyDisabledField(field) {
1605
+ if (field.disabled)
1606
+ return false;
1607
+ return this.hasDisabledAppearanceSignal((field.appearanceHint ?? "").toLowerCase());
1608
+ }
1609
+ hasDisabledAppearanceSignal(value) {
1610
+ if (!value)
1611
+ return false;
1612
+ return (value.includes("aria-disabled=true") ||
1613
+ value.includes("cursor-not-allowed") ||
1614
+ value.includes("pointer-events:none") ||
1615
+ value.includes("pointer-events: none") ||
1616
+ value.includes("opacity-50") ||
1617
+ value.includes("opacity:0.5") ||
1618
+ value.includes("opacity: 0.5") ||
1619
+ value.includes("opacity-40") ||
1620
+ value.includes("disabled"));
1621
+ }
1622
+ describeActionableItem(item) {
1623
+ return (item.accessibleName ||
1624
+ item.textContent ||
1625
+ String(item.attributes?.labelText ?? "") ||
1626
+ String(item.attributes?.placeholder ?? "") ||
1627
+ item.selector);
1628
+ }
1629
+ semanticText(item) {
1630
+ return [
1631
+ item.accessibleName || "",
1632
+ item.textContent || "",
1633
+ String(item.attributes?.labelText ?? ""),
1634
+ String(item.attributes?.placeholder ?? ""),
1635
+ String(item.attributes?.name ?? ""),
1636
+ String(item.attributes?.id ?? ""),
1637
+ item.href || "",
1638
+ item.selector,
1639
+ ]
1640
+ .join(" ")
1641
+ .toLowerCase();
1642
+ }
1643
+ isAddToCartItem(item) {
1644
+ return /\b(add to cart|add to bag|buy now|add item)\b/.test(this.semanticText(item));
1645
+ }
1646
+ isCheckoutItem(item) {
1647
+ return /\b(checkout|proceed to checkout|submit order|place order)\b/.test(this.semanticText(item));
1648
+ }
1649
+ isRemoveItemAction(item) {
1650
+ return /\b(remove|delete|trash|clear item|remove item)\b/.test(this.semanticText(item));
1651
+ }
1652
+ isAuthEntryItem(item) {
1653
+ return /\b(sign up|register|create account|sign in|log in|login)\b/.test(this.semanticText(item));
1654
+ }
1655
+ isMediaOpenItem(item) {
1656
+ const text = this.semanticText(item);
1657
+ return (item.tagName === "VIDEO" ||
1658
+ item.tagName === "AUDIO" ||
1659
+ /\b(image|photo|gallery|video|play|watch|zoom|preview)\b/.test(text));
1660
+ }
1661
+ isVideoLikeItem(item) {
1662
+ return (item.tagName === "VIDEO" ||
1663
+ /\b(video|play|watch)\b/.test(this.semanticText(item)));
1664
+ }
1665
+ isQuantityAction(item) {
1666
+ return /\b(qty|quantity|increase|decrease|increment|decrement|plus|minus)\b/.test(this.semanticText(item));
1667
+ }
1668
+ isFilterAction(item) {
1669
+ return /\b(filter|refine|apply filter|show results|category|brand|size|color|price)\b/.test(this.semanticText(item));
1670
+ }
1671
+ isSortAction(item) {
1672
+ return /\b(sort|order by|best selling|price low|price high|newest|featured)\b/.test(this.semanticText(item));
1673
+ }
1674
+ isDialogCloseItem(item) {
1675
+ return /\b(close|dismiss|cancel|done|x)\b/.test(this.semanticText(item));
1676
+ }
1677
+ pageHasOpenDialog(html) {
1678
+ return (/role=["']dialog["']/i.test(html) ||
1679
+ /role=["']alertdialog["']/i.test(html) ||
1680
+ /aria-modal=["']true["']/i.test(html) ||
1681
+ /\bmodal\b/i.test(html) ||
1682
+ /\boverlay\b/i.test(html));
1683
+ }
1684
+ isDisclosureItem(item) {
1685
+ const expanded = String(item.attributes?.["aria-expanded"] ?? "").toLowerCase();
1686
+ return expanded === "true" || expanded === "false";
1687
+ }
1688
+ isKeyboardPrimaryAction(item) {
1689
+ const role = (item.role ?? "").toLowerCase();
1690
+ return (item.actionKind === "navigate" ||
1691
+ role === "button" ||
1692
+ role === "link" ||
1693
+ role === "menuitem" ||
1694
+ item.tagName === "BUTTON" ||
1695
+ item.tagName === "A");
1696
+ }
1697
+ isKeyboardToggleAction(item) {
1698
+ const role = (item.role ?? "").toLowerCase();
1699
+ const inputType = (item.inputType ?? "").toLowerCase();
1700
+ return (role === "checkbox" ||
1701
+ role === "switch" ||
1702
+ role === "radio" ||
1703
+ inputType === "checkbox" ||
1704
+ inputType === "radio");
1705
+ }
1706
+ buildSemanticAction(item, description, signal) {
1707
+ if (item.actionKind === "select") {
1708
+ const value = this.extractSelectableValue(item);
1709
+ return {
1710
+ actionType: PlaywrightAction.SelectOption,
1711
+ path: item.selector,
1712
+ value,
1713
+ playwrightCode: `await page.locator('${item.selector}').selectOption('${this.escapeSingleQuotes(value ?? "")}')`,
1714
+ description: `${description}: ${signal}`,
1715
+ };
1716
+ }
1717
+ if (item.actionKind === "fill") {
1718
+ const value = this.isQuantityAction(item)
1719
+ ? "2"
1720
+ : fillValuePlanner.planValue(item);
1721
+ return {
1722
+ actionType: PlaywrightAction.Type,
1723
+ path: item.selector,
1724
+ value,
1725
+ playwrightCode: `await page.locator('${item.selector}').type('${this.escapeSingleQuotes(value)}')`,
1726
+ description: `${description}: ${signal}`,
1727
+ };
1728
+ }
1729
+ return {
1730
+ actionType: PlaywrightAction.Click,
1731
+ path: item.selector,
1732
+ playwrightCode: `await page.click('${item.selector}')`,
1733
+ description: `${description}: ${signal}`,
1734
+ };
1735
+ }
1736
+ makeExpectation(expectationType, description, extras) {
1737
+ return {
1738
+ expectationType,
1739
+ severity: ExpectationSeverity.ShouldPass,
1740
+ description,
1741
+ playwrightCode: "// evaluated by TesterExpertise",
1742
+ ...(extras ?? {}),
1743
+ };
1744
+ }
1745
+ isSelectLike(field) {
1746
+ return field.type === "select" || field.type === "select-one";
1747
+ }
1748
+ isSkippableFieldType(field) {
1749
+ return ["hidden", "submit", "button", "image", "file"].includes(field.type);
1750
+ }
1751
+ fieldLabel(field) {
1752
+ return field.label || field.name || field.selector;
1753
+ }
1754
+ slugify(value) {
1755
+ return (value
1756
+ .toLowerCase()
1757
+ .replace(/[^a-z0-9]+/g, "-")
1758
+ .replace(/^-+|-+$/g, "") || "page");
1759
+ }
1760
+ escapeSingleQuotes(value) {
1761
+ return value.replace(/'/g, "\\'");
1762
+ }
1763
+ extractVisibleText(html) {
1764
+ return html
1765
+ .replace(/<script[\s\S]*?<\/script>/gi, " ")
1766
+ .replace(/<style[\s\S]*?<\/style>/gi, " ")
1767
+ .replace(/<[^>]+>/g, " ")
1768
+ .replace(/\s+/g, " ")
1769
+ .trim();
1770
+ }
1771
+ detectPasswordRequirements(visibleText) {
1772
+ const lower = visibleText.toLowerCase();
1773
+ const requirements = {
1774
+ requiresUppercase: false,
1775
+ requiresLowercase: false,
1776
+ requiresNumber: false,
1777
+ requiresSpecial: false,
1778
+ noSpaces: false,
1779
+ };
1780
+ const lengthMatch = lower.match(/(?:at least|minimum|min\.?)\s*(\d+)\s*character/i) ||
1781
+ lower.match(/(\d+)\+?\s*character/i);
1782
+ if (lengthMatch) {
1783
+ requirements.minLength = Number.parseInt(lengthMatch[1], 10);
1784
+ }
1785
+ if (/uppercase|capital letter/i.test(lower))
1786
+ requirements.requiresUppercase = true;
1787
+ if (/lowercase/i.test(lower))
1788
+ requirements.requiresLowercase = true;
1789
+ if (/number|digit|\d/i.test(lower) &&
1790
+ /must|require|contain|include/i.test(lower)) {
1791
+ requirements.requiresNumber = true;
1792
+ }
1793
+ if (/special character|symbol|[!@#$%^&*]/i.test(lower) &&
1794
+ /must|require|contain|include/i.test(lower)) {
1795
+ requirements.requiresSpecial = true;
1796
+ }
1797
+ if (/no\s*spaces/i.test(lower))
1798
+ requirements.noSpaces = true;
1799
+ return requirements;
1800
+ }
1801
+ generatePasswordVariants(requirements) {
1802
+ const variants = [];
1803
+ const minimumLength = Math.max(requirements.minLength ?? 8, 8);
1804
+ let validPassword = "Aa1!";
1805
+ while (validPassword.length < minimumLength) {
1806
+ validPassword += "xY2@".charAt(validPassword.length % 4);
1807
+ }
1808
+ if (requirements.minLength) {
1809
+ variants.push({
1810
+ password: validPassword.slice(0, Math.max(requirements.minLength - 1, 1)),
1811
+ description: "too short password",
1812
+ shouldFail: true,
1813
+ });
1814
+ }
1815
+ if (requirements.requiresUppercase) {
1816
+ variants.push({
1817
+ password: validPassword.toLowerCase(),
1818
+ description: "missing uppercase password",
1819
+ shouldFail: true,
1820
+ });
1821
+ }
1822
+ if (requirements.requiresLowercase) {
1823
+ variants.push({
1824
+ password: validPassword.toUpperCase(),
1825
+ description: "missing lowercase password",
1826
+ shouldFail: true,
1827
+ });
1828
+ }
1829
+ if (requirements.requiresNumber) {
1830
+ variants.push({
1831
+ password: validPassword.replace(/\d/g, "a"),
1832
+ description: "missing number password",
1833
+ shouldFail: true,
1834
+ });
1835
+ }
1836
+ if (requirements.requiresSpecial) {
1837
+ variants.push({
1838
+ password: validPassword.replace(/[^a-zA-Z0-9]/g, "a"),
1839
+ description: "missing special character password",
1840
+ shouldFail: true,
1841
+ });
1842
+ }
1843
+ if (requirements.noSpaces) {
1844
+ variants.push({
1845
+ password: `${validPassword.slice(0, 4)} ${validPassword.slice(4)}`,
1846
+ description: "password with spaces",
1847
+ shouldFail: true,
1848
+ });
1849
+ }
1850
+ variants.push({
1851
+ password: validPassword,
1852
+ description: "valid password",
1853
+ shouldFail: false,
1854
+ });
1855
+ return variants;
1856
+ }
372
1857
  }
373
1858
  //# sourceMappingURL=page-analyzer.js.map