@skyramp/mcp 0.1.8 → 0.2.0-rc.2

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 (122) hide show
  1. package/build/index.js +4 -2
  2. package/build/playwright/registerPlaywrightTools.js +12 -0
  3. package/build/playwright/traceRecordingPrompt.js +15 -0
  4. package/build/prompts/code-reuse.js +106 -7
  5. package/build/prompts/pom-aware-code-reuse.js +106 -7
  6. package/build/prompts/startTraceCollectionPrompts.js +37 -15
  7. package/build/prompts/test-maintenance/drift-analysis-prompt.js +26 -31
  8. package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +40 -1
  9. package/build/prompts/test-maintenance/driftAnalysisSections.js +90 -86
  10. package/build/prompts/test-recommendation/analysisOutputPrompt.js +286 -163
  11. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +154 -45
  12. package/build/prompts/test-recommendation/diffExecutionPlan.js +246 -117
  13. package/build/prompts/test-recommendation/promptPlan.js +290 -0
  14. package/build/prompts/test-recommendation/promptPlan.test.js +336 -0
  15. package/build/prompts/test-recommendation/recommendationSections.js +4 -3
  16. package/build/prompts/test-recommendation/recommendationShared.js +23 -1
  17. package/build/prompts/test-recommendation/scopeAssessment.js +65 -14
  18. package/build/prompts/test-recommendation/scopeAssessment.test.js +93 -2
  19. package/build/prompts/test-recommendation/test-recommendation-prompt.js +36 -12
  20. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +316 -1
  21. package/build/prompts/testbot/testbot-prompts.js +73 -13
  22. package/build/prompts/testbot/testbot-prompts.test.js +114 -1
  23. package/build/resources/testbotResource.js +1 -1
  24. package/build/services/ScenarioGenerationService.integration.test.js +158 -0
  25. package/build/services/ScenarioGenerationService.js +47 -4
  26. package/build/services/ScenarioGenerationService.test.js +158 -22
  27. package/build/services/TestExecutionService.js +73 -15
  28. package/build/services/TestExecutionService.test.js +105 -0
  29. package/build/services/TestGenerationService.js +11 -1
  30. package/build/tools/executeSkyrampTestTool.js +1 -10
  31. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +16 -4
  32. package/build/tools/generate-tests/generateIntegrationRestTool.js +2 -0
  33. package/build/tools/generate-tests/generateUIRestTool.js +2 -0
  34. package/build/tools/test-management/actionsTool.js +152 -63
  35. package/build/tools/test-management/analyzeChangesTool.js +178 -64
  36. package/build/tools/test-management/analyzeChangesTool.test.js +103 -16
  37. package/build/tools/test-management/analyzeTestHealthTool.js +30 -81
  38. package/build/tools/test-management/index.js +1 -0
  39. package/build/tools/test-management/uiAnalyzeChangesTool.js +149 -0
  40. package/build/tools/test-management/uiAnalyzeChangesTool.test.js +100 -0
  41. package/build/tools/trace/resolveSaveStoragePath.js +16 -0
  42. package/build/tools/trace/resolveSaveStoragePath.test.js +17 -0
  43. package/build/tools/trace/resolveSessionPaths.js +39 -0
  44. package/build/tools/trace/resolveSessionPaths.test.js +103 -0
  45. package/build/tools/trace/sessionState.js +14 -0
  46. package/build/tools/trace/sessionState.test.js +17 -0
  47. package/build/tools/trace/startTraceCollectionTool.js +84 -14
  48. package/build/tools/trace/stopTraceCollectionTool.js +9 -2
  49. package/build/types/TestAnalysis.js +50 -0
  50. package/build/types/TestRecommendation.js +6 -58
  51. package/build/types/TestTypes.js +1 -1
  52. package/build/utils/AnalysisStateManager.js +22 -11
  53. package/build/utils/branchDiff.js +11 -2
  54. package/build/utils/docker.test.js +1 -1
  55. package/build/utils/gitStaging.js +52 -3
  56. package/build/utils/gitStaging.test.js +19 -1
  57. package/build/utils/repoScanner.js +18 -10
  58. package/build/utils/repoScanner.test.js +92 -0
  59. package/build/utils/routeParsers.js +180 -25
  60. package/build/utils/routeParsers.test.js +180 -1
  61. package/build/utils/scenarioDrafting.js +220 -17
  62. package/build/utils/scenarioDrafting.test.js +182 -9
  63. package/build/utils/sourceRouteExtractor.js +806 -0
  64. package/build/utils/sourceRouteExtractor.test.js +565 -0
  65. package/build/utils/uiPageEnumerator.js +319 -0
  66. package/build/utils/uiPageEnumerator.test.js +422 -0
  67. package/build/utils/utils.js +27 -0
  68. package/build/utils/versions.js +1 -1
  69. package/build/utils/workspaceAuth.js +33 -4
  70. package/node_modules/playwright/ThirdPartyNotices.txt +6 -6
  71. package/node_modules/playwright/lib/dom-analyzer/analyze.js +111 -0
  72. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1210 -0
  73. package/node_modules/playwright/lib/dom-analyzer/blueprint.test.js +396 -0
  74. package/node_modules/playwright/lib/dom-analyzer/blueprintCache.js +57 -0
  75. package/node_modules/playwright/lib/dom-analyzer/blueprintCache.test.js +57 -0
  76. package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.js +254 -0
  77. package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.test.js +304 -0
  78. package/node_modules/playwright/lib/dom-analyzer/crawler.js +384 -0
  79. package/node_modules/playwright/lib/dom-analyzer/curatedWidgets.js +73 -0
  80. package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +43 -0
  81. package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +85 -0
  82. package/node_modules/playwright/lib/dom-analyzer/fingerprint.js +90 -0
  83. package/node_modules/playwright/lib/dom-analyzer/fingerprint.test.js +231 -0
  84. package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.fixtures.js +145 -0
  85. package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.test.js +41 -0
  86. package/node_modules/playwright/lib/dom-analyzer/graph.js +36 -0
  87. package/node_modules/playwright/lib/dom-analyzer/liveFingerprints.js +43 -0
  88. package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.js +72 -0
  89. package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.test.js +182 -0
  90. package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.js +150 -0
  91. package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.test.js +470 -0
  92. package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.js +169 -0
  93. package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.test.js +269 -0
  94. package/node_modules/playwright/lib/dom-analyzer/serialization.js +75 -0
  95. package/node_modules/playwright/lib/dom-analyzer/slug.js +30 -0
  96. package/node_modules/playwright/lib/dom-analyzer/slug.test.js +84 -0
  97. package/node_modules/playwright/lib/dom-analyzer/widgetContract.js +127 -0
  98. package/node_modules/playwright/lib/dom-analyzer/widgetContract.test.js +212 -0
  99. package/node_modules/playwright/lib/mcp/browser/browserContextFactory.js +3 -1
  100. package/node_modules/playwright/lib/mcp/browser/config.js +1 -1
  101. package/node_modules/playwright/lib/mcp/browser/context.js +17 -1
  102. package/node_modules/playwright/lib/mcp/browser/tab.js +38 -0
  103. package/node_modules/playwright/lib/mcp/browser/tools/domAnalyzer.js +261 -0
  104. package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -3
  105. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.js +146 -0
  106. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.test.js +140 -0
  107. package/node_modules/playwright/lib/mcp/browser/tools/sitemap.js +226 -0
  108. package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +2 -2
  109. package/node_modules/playwright/lib/mcp/browser/tools/widgetContract.js +168 -0
  110. package/node_modules/playwright/lib/mcp/browser/tools.js +6 -0
  111. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +52 -12
  112. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +64 -13
  113. package/node_modules/playwright/package.json +1 -1
  114. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.3.tgz +0 -0
  115. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.4.tgz +0 -0
  116. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.5.tgz +0 -0
  117. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.6.tgz +0 -0
  118. package/package.json +3 -3
  119. package/build/services/TestHealthService.js +0 -694
  120. package/build/services/TestHealthService.test.js +0 -241
  121. package/build/types/TestDriftAnalysis.js +0 -1
  122. package/build/types/TestHealth.js +0 -4
@@ -0,0 +1,470 @@
1
+ "use strict";
2
+ var import_possibleAssertions = require("./possibleAssertions");
3
+ const cases = [];
4
+ function test(name, run) {
5
+ cases.push({ name, run });
6
+ }
7
+ function assertEqual(actual, expected, msg) {
8
+ const a = JSON.stringify(actual);
9
+ const e = JSON.stringify(expected);
10
+ if (a !== e)
11
+ throw new Error(`${msg ?? "assertEqual"} \u2014 expected ${e}, got ${a}`);
12
+ }
13
+ test("Empty delta \u2192 empty array", () => {
14
+ const delta = {
15
+ hasStructuralChange: false,
16
+ sectionsAdded: [],
17
+ sectionsRemoved: [],
18
+ elementsAdded: [],
19
+ elementsRemoved: [],
20
+ repeatingCountChanges: [],
21
+ repeatingItemsChanged: [],
22
+ textChanges: [],
23
+ enrichmentChanges: []
24
+ };
25
+ const assertions = (0, import_possibleAssertions.buildPossibleAssertions)(delta);
26
+ assertEqual(assertions.length, 0);
27
+ });
28
+ test("elementsAdded with a button \u2192 1 assertion, role-based locator, toBeVisible(), MEDIUM tier", () => {
29
+ const delta = {
30
+ hasStructuralChange: true,
31
+ sectionsAdded: [],
32
+ sectionsRemoved: [],
33
+ elementsAdded: [
34
+ {
35
+ logicalName: "save_btn",
36
+ sectionLogicalName: "main",
37
+ role: "button",
38
+ accessibleName: "Save Changes",
39
+ kind: "persistent"
40
+ }
41
+ ],
42
+ elementsRemoved: [],
43
+ repeatingCountChanges: [],
44
+ repeatingItemsChanged: [],
45
+ textChanges: [],
46
+ enrichmentChanges: []
47
+ };
48
+ const assertions = (0, import_possibleAssertions.buildPossibleAssertions)(delta);
49
+ assertEqual(assertions.length, 1);
50
+ assertEqual(assertions[0].code, "await expect(page.getByRole('button', { name: 'Save Changes' })).toBeVisible();");
51
+ assertEqual(assertions[0].tier, "MEDIUM");
52
+ if (!assertions[0].rationale.includes("button"))
53
+ throw new Error("rationale should mention button role");
54
+ });
55
+ test("elementsRemoved with a textbox \u2192 .not.toBeVisible(), MEDIUM", () => {
56
+ const delta = {
57
+ hasStructuralChange: true,
58
+ sectionsAdded: [],
59
+ sectionsRemoved: [],
60
+ elementsAdded: [],
61
+ elementsRemoved: [
62
+ {
63
+ logicalName: "email_input",
64
+ sectionLogicalName: "form",
65
+ role: "textbox",
66
+ accessibleName: "Email"
67
+ }
68
+ ],
69
+ repeatingCountChanges: [],
70
+ repeatingItemsChanged: [],
71
+ textChanges: [],
72
+ enrichmentChanges: []
73
+ };
74
+ const assertions = (0, import_possibleAssertions.buildPossibleAssertions)(delta);
75
+ assertEqual(assertions.length, 1);
76
+ assertEqual(assertions[0].code, "await expect(page.getByRole('textbox', { name: 'Email' })).not.toBeVisible();");
77
+ assertEqual(assertions[0].tier, "MEDIUM");
78
+ if (!assertions[0].rationale.includes("removed"))
79
+ throw new Error("rationale should mention removed");
80
+ });
81
+ test("repeatingCountChanges 12 \u2192 13 \u2192 toHaveCount(13), HIGH", () => {
82
+ const delta = {
83
+ hasStructuralChange: true,
84
+ sectionsAdded: [],
85
+ sectionsRemoved: [],
86
+ elementsAdded: [],
87
+ elementsRemoved: [],
88
+ repeatingCountChanges: [
89
+ {
90
+ logicalName: "order_row_btn",
91
+ sectionLogicalName: "main",
92
+ role: "button",
93
+ accessibleNameTemplate: "View details for order {orderId}",
94
+ before: 12,
95
+ after: 13,
96
+ delta: 1
97
+ }
98
+ ],
99
+ repeatingItemsChanged: [],
100
+ textChanges: [],
101
+ enrichmentChanges: []
102
+ };
103
+ const assertions = (0, import_possibleAssertions.buildPossibleAssertions)(delta);
104
+ assertEqual(assertions.length, 1);
105
+ if (!assertions[0].code.includes("toHaveCount(13)"))
106
+ throw new Error("code should include toHaveCount(13)");
107
+ if (!assertions[0].code.includes("/^View details for order .+$/i"))
108
+ throw new Error("code should include regex pattern from template");
109
+ assertEqual(assertions[0].tier, "HIGH");
110
+ if (!assertions[0].rationale.includes("12") || !assertions[0].rationale.includes("13"))
111
+ throw new Error("rationale should mention before and after counts");
112
+ });
113
+ test('textChanges "Inbox (5)" \u2192 "Inbox (6)" \u2192 toHaveText(...), HIGH', () => {
114
+ const delta = {
115
+ hasStructuralChange: false,
116
+ sectionsAdded: [],
117
+ sectionsRemoved: [],
118
+ elementsAdded: [],
119
+ elementsRemoved: [],
120
+ repeatingCountChanges: [],
121
+ repeatingItemsChanged: [],
122
+ textChanges: [
123
+ {
124
+ logicalName: "inbox_link",
125
+ sectionLogicalName: "sidebar",
126
+ role: "link",
127
+ accessibleName: "Inbox (6)",
128
+ before: "Inbox (5)",
129
+ after: "Inbox (6)"
130
+ }
131
+ ],
132
+ enrichmentChanges: []
133
+ };
134
+ const assertions = (0, import_possibleAssertions.buildPossibleAssertions)(delta);
135
+ assertEqual(assertions.length, 1);
136
+ if (!assertions[0].code.includes("toHaveText('Inbox (6)')"))
137
+ throw new Error("code should include toHaveText with after value");
138
+ if (!assertions[0].code.includes("getByRole('link'"))
139
+ throw new Error("code should use role-based locator");
140
+ assertEqual(assertions[0].tier, "HIGH");
141
+ if (!assertions[0].rationale.includes("Inbox (5)") || !assertions[0].rationale.includes("Inbox (6)"))
142
+ throw new Error("rationale should mention before and after text");
143
+ });
144
+ test("elementsAdded with empty accessibleName + no testId + no stableId \u2192 skipped (zero output)", () => {
145
+ const delta = {
146
+ hasStructuralChange: true,
147
+ sectionsAdded: [],
148
+ sectionsRemoved: [],
149
+ elementsAdded: [
150
+ {
151
+ logicalName: "button_1",
152
+ sectionLogicalName: "main",
153
+ role: "button",
154
+ accessibleName: "",
155
+ kind: "persistent"
156
+ }
157
+ ],
158
+ elementsRemoved: [],
159
+ repeatingCountChanges: [],
160
+ repeatingItemsChanged: [],
161
+ textChanges: [],
162
+ enrichmentChanges: []
163
+ };
164
+ const assertions = (0, import_possibleAssertions.buildPossibleAssertions)(delta);
165
+ assertEqual(assertions.length, 0, "should skip element with empty accessibleName");
166
+ });
167
+ test("Quote-escaping: accessibleName with quotes", () => {
168
+ const delta = {
169
+ hasStructuralChange: true,
170
+ sectionsAdded: [],
171
+ sectionsRemoved: [],
172
+ elementsAdded: [
173
+ {
174
+ logicalName: "greeting_btn",
175
+ sectionLogicalName: "main",
176
+ role: "button",
177
+ accessibleName: "He said 'hi'",
178
+ kind: "persistent"
179
+ }
180
+ ],
181
+ elementsRemoved: [],
182
+ repeatingCountChanges: [],
183
+ repeatingItemsChanged: [],
184
+ textChanges: [],
185
+ enrichmentChanges: []
186
+ };
187
+ const assertions = (0, import_possibleAssertions.buildPossibleAssertions)(delta);
188
+ assertEqual(assertions.length, 1);
189
+ if (!assertions[0].code.includes("\\'"))
190
+ throw new Error("code should escape single quotes");
191
+ });
192
+ test("textChanges with empty accessibleName \u2192 skipped", () => {
193
+ const delta = {
194
+ hasStructuralChange: false,
195
+ sectionsAdded: [],
196
+ sectionsRemoved: [],
197
+ elementsAdded: [],
198
+ elementsRemoved: [],
199
+ repeatingCountChanges: [],
200
+ repeatingItemsChanged: [],
201
+ textChanges: [
202
+ {
203
+ logicalName: "icon_btn",
204
+ sectionLogicalName: "main",
205
+ role: "button",
206
+ accessibleName: "",
207
+ before: "",
208
+ after: "New Text"
209
+ }
210
+ ],
211
+ enrichmentChanges: []
212
+ };
213
+ const assertions = (0, import_possibleAssertions.buildPossibleAssertions)(delta);
214
+ assertEqual(assertions.length, 0, "should skip textChange with empty accessibleName");
215
+ });
216
+ test("repeatingCountChanges with empty accessibleNameTemplate \u2192 skipped", () => {
217
+ const delta = {
218
+ hasStructuralChange: true,
219
+ sectionsAdded: [],
220
+ sectionsRemoved: [],
221
+ elementsAdded: [],
222
+ elementsRemoved: [],
223
+ repeatingCountChanges: [
224
+ {
225
+ logicalName: "item",
226
+ sectionLogicalName: "main",
227
+ role: "listitem",
228
+ accessibleNameTemplate: "",
229
+ before: 5,
230
+ after: 6,
231
+ delta: 1
232
+ }
233
+ ],
234
+ repeatingItemsChanged: [],
235
+ textChanges: [],
236
+ enrichmentChanges: []
237
+ };
238
+ const assertions = (0, import_possibleAssertions.buildPossibleAssertions)(delta);
239
+ assertEqual(assertions.length, 0, "should skip repeatingCountChange with empty accessibleNameTemplate");
240
+ });
241
+ test("Mixed delta with all four kinds \u2192 all four translations appear, ordered HIGH first then MEDIUM then LOW", () => {
242
+ const delta = {
243
+ hasStructuralChange: true,
244
+ sectionsAdded: [],
245
+ sectionsRemoved: [],
246
+ elementsAdded: [
247
+ {
248
+ logicalName: "save_btn",
249
+ sectionLogicalName: "main",
250
+ role: "button",
251
+ accessibleName: "Save",
252
+ kind: "persistent"
253
+ },
254
+ {
255
+ logicalName: "notification_status",
256
+ sectionLogicalName: "main",
257
+ role: "status",
258
+ accessibleName: "Operation complete",
259
+ kind: "transient"
260
+ }
261
+ ],
262
+ elementsRemoved: [
263
+ {
264
+ logicalName: "cancel_btn",
265
+ sectionLogicalName: "main",
266
+ role: "button",
267
+ accessibleName: "Cancel"
268
+ }
269
+ ],
270
+ repeatingCountChanges: [
271
+ {
272
+ logicalName: "order_row_btn",
273
+ sectionLogicalName: "main",
274
+ role: "button",
275
+ accessibleNameTemplate: "View details for order {orderId}",
276
+ before: 12,
277
+ after: 13,
278
+ delta: 1
279
+ }
280
+ ],
281
+ repeatingItemsChanged: [],
282
+ textChanges: [
283
+ {
284
+ logicalName: "inbox_link",
285
+ sectionLogicalName: "sidebar",
286
+ role: "link",
287
+ accessibleName: "Inbox (6)",
288
+ before: "Inbox (5)",
289
+ after: "Inbox (6)"
290
+ }
291
+ ],
292
+ enrichmentChanges: []
293
+ };
294
+ const assertions = (0, import_possibleAssertions.buildPossibleAssertions)(delta);
295
+ assertEqual(assertions.length, 5);
296
+ assertEqual(assertions[0].tier, "HIGH");
297
+ assertEqual(assertions[1].tier, "HIGH");
298
+ assertEqual(assertions[2].tier, "MEDIUM");
299
+ assertEqual(assertions[3].tier, "MEDIUM");
300
+ assertEqual(assertions[4].tier, "LOW");
301
+ });
302
+ function makeBlueprint(url, sections = []) {
303
+ return {
304
+ schemaVersion: 1,
305
+ url,
306
+ capturedAt: "2026-01-01T00:00:00Z",
307
+ pageHash: "0:0",
308
+ sections
309
+ };
310
+ }
311
+ test("Full capture with URL + heading \u2192 2 assertions, HIGH then LOW", () => {
312
+ const blueprint = makeBlueprint("http://localhost:8055/settings/schema", [
313
+ {
314
+ name: "page",
315
+ landmark: "main",
316
+ elements: [
317
+ {
318
+ logicalName: "page_title",
319
+ role: "heading",
320
+ accessibleName: "Data Model",
321
+ xpath: "//h1",
322
+ mutability: "immutable",
323
+ widgetType: "native",
324
+ framePath: [],
325
+ shadowRoot: false
326
+ }
327
+ ],
328
+ repeatingElements: []
329
+ }
330
+ ]);
331
+ const assertions = (0, import_possibleAssertions.buildFullCaptureAssertions)(blueprint);
332
+ assertEqual(assertions.length, 2);
333
+ assertEqual(assertions[0].tier, "HIGH");
334
+ assertEqual(
335
+ assertions[0].code,
336
+ `await expect(page).toHaveURL('http://localhost:8055/settings/schema');`
337
+ );
338
+ assertEqual(assertions[1].tier, "LOW");
339
+ assertEqual(
340
+ assertions[1].code,
341
+ `await expect(page.getByRole('heading', { name: 'Data Model' })).toBeVisible();`
342
+ );
343
+ });
344
+ test("Full capture with URL only (no headings) \u2192 1 assertion", () => {
345
+ const blueprint = makeBlueprint("http://localhost:8055/applications", [
346
+ {
347
+ name: "page",
348
+ landmark: "main",
349
+ elements: [
350
+ {
351
+ logicalName: "create_btn",
352
+ role: "button",
353
+ accessibleName: "Create new",
354
+ xpath: "//button",
355
+ mutability: "mutable",
356
+ widgetType: "native",
357
+ framePath: [],
358
+ shadowRoot: false
359
+ }
360
+ ],
361
+ repeatingElements: []
362
+ }
363
+ ]);
364
+ const assertions = (0, import_possibleAssertions.buildFullCaptureAssertions)(blueprint);
365
+ assertEqual(assertions.length, 1);
366
+ assertEqual(assertions[0].tier, "HIGH");
367
+ });
368
+ test("Full capture with empty URL \u2192 no assertions", () => {
369
+ const blueprint = makeBlueprint("", []);
370
+ const assertions = (0, import_possibleAssertions.buildFullCaptureAssertions)(blueprint);
371
+ assertEqual(assertions.length, 0);
372
+ });
373
+ test("Full capture skips heading with empty accessibleName", () => {
374
+ const blueprint = makeBlueprint("http://example.com/", [
375
+ {
376
+ name: "page",
377
+ landmark: "main",
378
+ elements: [
379
+ {
380
+ logicalName: "unnamed_heading",
381
+ role: "heading",
382
+ accessibleName: " ",
383
+ xpath: "//h1",
384
+ mutability: "immutable",
385
+ widgetType: "native",
386
+ framePath: [],
387
+ shadowRoot: false
388
+ }
389
+ ],
390
+ repeatingElements: []
391
+ }
392
+ ]);
393
+ const assertions = (0, import_possibleAssertions.buildFullCaptureAssertions)(blueprint);
394
+ assertEqual(assertions.length, 1);
395
+ assertEqual(assertions[0].tier, "HIGH");
396
+ });
397
+ test("Full capture finds first heading across multiple sections", () => {
398
+ const blueprint = makeBlueprint("http://example.com/page", [
399
+ {
400
+ name: "header",
401
+ landmark: "banner",
402
+ elements: [],
403
+ // no heading here
404
+ repeatingElements: []
405
+ },
406
+ {
407
+ name: "page",
408
+ landmark: "main",
409
+ elements: [
410
+ {
411
+ logicalName: "main_heading",
412
+ role: "heading",
413
+ accessibleName: "Welcome",
414
+ xpath: "//h1",
415
+ mutability: "immutable",
416
+ widgetType: "native",
417
+ framePath: [],
418
+ shadowRoot: false
419
+ }
420
+ ],
421
+ repeatingElements: []
422
+ }
423
+ ]);
424
+ const assertions = (0, import_possibleAssertions.buildFullCaptureAssertions)(blueprint);
425
+ assertEqual(assertions.length, 2);
426
+ assertEqual(assertions[1].rationale, 'Heading visible on destination page: "Welcome"');
427
+ });
428
+ test("Full capture escapes single-quotes in URL and heading", () => {
429
+ const blueprint = makeBlueprint(`http://example.com/q?name='foo'`, [
430
+ {
431
+ name: "page",
432
+ landmark: "main",
433
+ elements: [
434
+ {
435
+ logicalName: "h1",
436
+ role: "heading",
437
+ accessibleName: `Bob's profile`,
438
+ xpath: "//h1",
439
+ mutability: "immutable",
440
+ widgetType: "native",
441
+ framePath: [],
442
+ shadowRoot: false
443
+ }
444
+ ],
445
+ repeatingElements: []
446
+ }
447
+ ]);
448
+ const assertions = (0, import_possibleAssertions.buildFullCaptureAssertions)(blueprint);
449
+ assertEqual(assertions.length, 2);
450
+ assertEqual(assertions[0].code.includes(`\\'`), true);
451
+ assertEqual(assertions[1].code.includes(`\\'`), true);
452
+ });
453
+ let passed = 0;
454
+ let failed = 0;
455
+ const failures = [];
456
+ for (const c of cases) {
457
+ try {
458
+ c.run();
459
+ passed++;
460
+ } catch (e) {
461
+ failed++;
462
+ failures.push({ name: c.name, error: e.message });
463
+ }
464
+ }
465
+ console.log(`${passed}/${cases.length} passed${failed ? `, ${failed} failed` : ""}`);
466
+ for (const f of failures)
467
+ console.error(` \u2717 ${f.name}: ${f.error}`);
468
+ if (failed > 0) {
469
+ process.exit(1);
470
+ }
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var sectionGrouper_exports = {};
20
+ __export(sectionGrouper_exports, {
21
+ JUNK_SECTION_NAME_PATTERN: () => JUNK_SECTION_NAME_PATTERN,
22
+ resolveSections: () => resolveSections
23
+ });
24
+ module.exports = __toCommonJS(sectionGrouper_exports);
25
+ var import_slug = require("./slug");
26
+ const JUNK_SECTION_NAME_PATTERN = /^(content|section|untitled|main|overview|details)$/i;
27
+ function isJunk(candidateSlug) {
28
+ return !candidateSlug || JUNK_SECTION_NAME_PATTERN.test(candidateSlug);
29
+ }
30
+ function resolveSections(inputs) {
31
+ const assignments = {};
32
+ const candidatesByAnchorKey = /* @__PURE__ */ new Map();
33
+ const elementsByAnchorKey = /* @__PURE__ */ new Map();
34
+ const headingAnchorChainByElement = /* @__PURE__ */ new Map();
35
+ for (const inp of inputs) {
36
+ const { landmarkAncestor, sectioningAncestor, headingAncestry } = inp.ancestry;
37
+ if (landmarkAncestor) {
38
+ const name = landmarkAncestor.nameHint;
39
+ assignments[inp.id] = name;
40
+ candidatesByAnchorKey.set(landmarkAncestor.anchorKey, {
41
+ name,
42
+ landmark: landmarkAncestor.landmark,
43
+ anchorKey: landmarkAncestor.anchorKey
44
+ });
45
+ const arr2 = elementsByAnchorKey.get(landmarkAncestor.anchorKey) ?? [];
46
+ arr2.push(inp.id);
47
+ elementsByAnchorKey.set(landmarkAncestor.anchorKey, arr2);
48
+ continue;
49
+ }
50
+ if (sectioningAncestor) {
51
+ const rawName = sectioningAncestor.accessibleName;
52
+ if (rawName) {
53
+ const candidateSlug = (0, import_slug.slug)(rawName);
54
+ if (!isJunk(candidateSlug)) {
55
+ assignments[inp.id] = candidateSlug;
56
+ candidatesByAnchorKey.set(sectioningAncestor.anchorKey, {
57
+ name: candidateSlug,
58
+ landmark: "other",
59
+ landmarkOther: sectioningAncestor.tagName,
60
+ anchorKey: sectioningAncestor.anchorKey
61
+ });
62
+ const arr2 = elementsByAnchorKey.get(sectioningAncestor.anchorKey) ?? [];
63
+ arr2.push(inp.id);
64
+ elementsByAnchorKey.set(sectioningAncestor.anchorKey, arr2);
65
+ continue;
66
+ }
67
+ }
68
+ }
69
+ let assignedFromHeading = false;
70
+ for (let i = headingAncestry.length - 1; i >= 0; i--) {
71
+ const h = headingAncestry[i];
72
+ const candidateSlug = (0, import_slug.slug)(h.text);
73
+ if (!isJunk(candidateSlug)) {
74
+ let finalName = candidateSlug;
75
+ let suffix = 2;
76
+ while ([...candidatesByAnchorKey.values()].some((c) => c.name === finalName && c.anchorKey !== h.anchorKey)) {
77
+ finalName = `${candidateSlug}_${suffix++}`;
78
+ }
79
+ assignments[inp.id] = finalName;
80
+ candidatesByAnchorKey.set(h.anchorKey, {
81
+ name: finalName,
82
+ landmark: "other",
83
+ landmarkOther: "heading",
84
+ anchorKey: h.anchorKey
85
+ });
86
+ const arr2 = elementsByAnchorKey.get(h.anchorKey) ?? [];
87
+ arr2.push(inp.id);
88
+ elementsByAnchorKey.set(h.anchorKey, arr2);
89
+ headingAnchorChainByElement.set(
90
+ inp.id,
91
+ headingAncestry.map((hh) => hh.anchorKey)
92
+ );
93
+ assignedFromHeading = true;
94
+ break;
95
+ }
96
+ }
97
+ if (assignedFromHeading) continue;
98
+ assignments[inp.id] = "page";
99
+ const pageAnchorKey = "page-fallback";
100
+ candidatesByAnchorKey.set(pageAnchorKey, { name: "page", landmark: "region", anchorKey: pageAnchorKey });
101
+ const arr = elementsByAnchorKey.get(pageAnchorKey) ?? [];
102
+ arr.push(inp.id);
103
+ elementsByAnchorKey.set(pageAnchorKey, arr);
104
+ }
105
+ const candidateAnchorKeysWithElements = new Set(
106
+ [...elementsByAnchorKey.entries()].filter(([, ids]) => ids.length > 0).map(([k]) => k)
107
+ );
108
+ const toDrop = /* @__PURE__ */ new Set();
109
+ for (const [anchorKey, info] of candidatesByAnchorKey.entries()) {
110
+ if (info.landmarkOther !== "heading") continue;
111
+ let isAncestor = false;
112
+ for (const [, chain] of headingAnchorChainByElement.entries()) {
113
+ const idx = chain.indexOf(anchorKey);
114
+ if (idx === -1) continue;
115
+ for (let j = idx + 1; j < chain.length; j++) {
116
+ const deeperKey = chain[j];
117
+ if (candidateAnchorKeysWithElements.has(deeperKey) && deeperKey !== anchorKey) {
118
+ isAncestor = true;
119
+ break;
120
+ }
121
+ }
122
+ if (isAncestor) break;
123
+ }
124
+ if (isAncestor) toDrop.add(anchorKey);
125
+ }
126
+ for (const droppedKey of toDrop) {
127
+ const affectedIds = elementsByAnchorKey.get(droppedKey) ?? [];
128
+ for (const id of affectedIds) {
129
+ const chain = headingAnchorChainByElement.get(id) ?? [];
130
+ let reassigned = null;
131
+ for (let i = chain.length - 1; i >= 0; i--) {
132
+ const k = chain[i];
133
+ if (k === droppedKey) continue;
134
+ if (toDrop.has(k)) continue;
135
+ const cand = candidatesByAnchorKey.get(k);
136
+ if (cand && cand.landmarkOther === "heading") {
137
+ reassigned = cand.name;
138
+ break;
139
+ }
140
+ }
141
+ assignments[id] = reassigned ?? "page";
142
+ }
143
+ candidatesByAnchorKey.delete(droppedKey);
144
+ elementsByAnchorKey.delete(droppedKey);
145
+ }
146
+ const sections = [];
147
+ const seenNames = /* @__PURE__ */ new Set();
148
+ const assignmentOrder = [];
149
+ for (const inp of inputs) {
150
+ const name = assignments[inp.id];
151
+ if (name && !assignmentOrder.includes(name)) assignmentOrder.push(name);
152
+ }
153
+ for (const name of assignmentOrder) {
154
+ if (seenNames.has(name)) continue;
155
+ seenNames.add(name);
156
+ const info = [...candidatesByAnchorKey.values()].find((c) => c.name === name);
157
+ if (info) {
158
+ sections.push({ name: info.name, landmark: info.landmark, landmarkOther: info.landmarkOther });
159
+ } else if (name === "page") {
160
+ sections.push({ name: "page", landmark: "region" });
161
+ }
162
+ }
163
+ return { assignments, sections };
164
+ }
165
+ // Annotate the CommonJS export names for ESM import in node:
166
+ 0 && (module.exports = {
167
+ JUNK_SECTION_NAME_PATTERN,
168
+ resolveSections
169
+ });