@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.
- package/build/index.js +4 -2
- package/build/playwright/registerPlaywrightTools.js +12 -0
- package/build/playwright/traceRecordingPrompt.js +15 -0
- package/build/prompts/code-reuse.js +106 -7
- package/build/prompts/pom-aware-code-reuse.js +106 -7
- package/build/prompts/startTraceCollectionPrompts.js +37 -15
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +26 -31
- package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +40 -1
- package/build/prompts/test-maintenance/driftAnalysisSections.js +90 -86
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +286 -163
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +154 -45
- package/build/prompts/test-recommendation/diffExecutionPlan.js +246 -117
- package/build/prompts/test-recommendation/promptPlan.js +290 -0
- package/build/prompts/test-recommendation/promptPlan.test.js +336 -0
- package/build/prompts/test-recommendation/recommendationSections.js +4 -3
- package/build/prompts/test-recommendation/recommendationShared.js +23 -1
- package/build/prompts/test-recommendation/scopeAssessment.js +65 -14
- package/build/prompts/test-recommendation/scopeAssessment.test.js +93 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +36 -12
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +316 -1
- package/build/prompts/testbot/testbot-prompts.js +73 -13
- package/build/prompts/testbot/testbot-prompts.test.js +114 -1
- package/build/resources/testbotResource.js +1 -1
- package/build/services/ScenarioGenerationService.integration.test.js +158 -0
- package/build/services/ScenarioGenerationService.js +47 -4
- package/build/services/ScenarioGenerationService.test.js +158 -22
- package/build/services/TestExecutionService.js +73 -15
- package/build/services/TestExecutionService.test.js +105 -0
- package/build/services/TestGenerationService.js +11 -1
- package/build/tools/executeSkyrampTestTool.js +1 -10
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +16 -4
- package/build/tools/generate-tests/generateIntegrationRestTool.js +2 -0
- package/build/tools/generate-tests/generateUIRestTool.js +2 -0
- package/build/tools/test-management/actionsTool.js +152 -63
- package/build/tools/test-management/analyzeChangesTool.js +178 -64
- package/build/tools/test-management/analyzeChangesTool.test.js +103 -16
- package/build/tools/test-management/analyzeTestHealthTool.js +30 -81
- package/build/tools/test-management/index.js +1 -0
- package/build/tools/test-management/uiAnalyzeChangesTool.js +149 -0
- package/build/tools/test-management/uiAnalyzeChangesTool.test.js +100 -0
- package/build/tools/trace/resolveSaveStoragePath.js +16 -0
- package/build/tools/trace/resolveSaveStoragePath.test.js +17 -0
- package/build/tools/trace/resolveSessionPaths.js +39 -0
- package/build/tools/trace/resolveSessionPaths.test.js +103 -0
- package/build/tools/trace/sessionState.js +14 -0
- package/build/tools/trace/sessionState.test.js +17 -0
- package/build/tools/trace/startTraceCollectionTool.js +84 -14
- package/build/tools/trace/stopTraceCollectionTool.js +9 -2
- package/build/types/TestAnalysis.js +50 -0
- package/build/types/TestRecommendation.js +6 -58
- package/build/types/TestTypes.js +1 -1
- package/build/utils/AnalysisStateManager.js +22 -11
- package/build/utils/branchDiff.js +11 -2
- package/build/utils/docker.test.js +1 -1
- package/build/utils/gitStaging.js +52 -3
- package/build/utils/gitStaging.test.js +19 -1
- package/build/utils/repoScanner.js +18 -10
- package/build/utils/repoScanner.test.js +92 -0
- package/build/utils/routeParsers.js +180 -25
- package/build/utils/routeParsers.test.js +180 -1
- package/build/utils/scenarioDrafting.js +220 -17
- package/build/utils/scenarioDrafting.test.js +182 -9
- package/build/utils/sourceRouteExtractor.js +806 -0
- package/build/utils/sourceRouteExtractor.test.js +565 -0
- package/build/utils/uiPageEnumerator.js +319 -0
- package/build/utils/uiPageEnumerator.test.js +422 -0
- package/build/utils/utils.js +27 -0
- package/build/utils/versions.js +1 -1
- package/build/utils/workspaceAuth.js +33 -4
- package/node_modules/playwright/ThirdPartyNotices.txt +6 -6
- package/node_modules/playwright/lib/dom-analyzer/analyze.js +111 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1210 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprint.test.js +396 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintCache.js +57 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintCache.test.js +57 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.js +254 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.test.js +304 -0
- package/node_modules/playwright/lib/dom-analyzer/crawler.js +384 -0
- package/node_modules/playwright/lib/dom-analyzer/curatedWidgets.js +73 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +43 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +85 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprint.js +90 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprint.test.js +231 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.fixtures.js +145 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.test.js +41 -0
- package/node_modules/playwright/lib/dom-analyzer/graph.js +36 -0
- package/node_modules/playwright/lib/dom-analyzer/liveFingerprints.js +43 -0
- package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.js +72 -0
- package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.test.js +182 -0
- package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.js +150 -0
- package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.test.js +470 -0
- package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.js +169 -0
- package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.test.js +269 -0
- package/node_modules/playwright/lib/dom-analyzer/serialization.js +75 -0
- package/node_modules/playwright/lib/dom-analyzer/slug.js +30 -0
- package/node_modules/playwright/lib/dom-analyzer/slug.test.js +84 -0
- package/node_modules/playwright/lib/dom-analyzer/widgetContract.js +127 -0
- package/node_modules/playwright/lib/dom-analyzer/widgetContract.test.js +212 -0
- package/node_modules/playwright/lib/mcp/browser/browserContextFactory.js +3 -1
- package/node_modules/playwright/lib/mcp/browser/config.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/context.js +17 -1
- package/node_modules/playwright/lib/mcp/browser/tab.js +38 -0
- package/node_modules/playwright/lib/mcp/browser/tools/domAnalyzer.js +261 -0
- package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -3
- package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.js +146 -0
- package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.test.js +140 -0
- package/node_modules/playwright/lib/mcp/browser/tools/sitemap.js +226 -0
- package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +2 -2
- package/node_modules/playwright/lib/mcp/browser/tools/widgetContract.js +168 -0
- package/node_modules/playwright/lib/mcp/browser/tools.js +6 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +52 -12
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +64 -13
- package/node_modules/playwright/package.json +1 -1
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.3.tgz +0 -0
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.4.tgz +0 -0
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.5.tgz +0 -0
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.6.tgz +0 -0
- package/package.json +3 -3
- package/build/services/TestHealthService.js +0 -694
- package/build/services/TestHealthService.test.js +0 -241
- package/build/types/TestDriftAnalysis.js +0 -1
- 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
|
+
});
|