@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,254 @@
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 blueprintDiff_exports = {};
20
+ __export(blueprintDiff_exports, {
21
+ diffBlueprints: () => diffBlueprints
22
+ });
23
+ module.exports = __toCommonJS(blueprintDiff_exports);
24
+ var import_fingerprint = require("./fingerprint");
25
+ const TRANSIENT_ROLES = /* @__PURE__ */ new Set(["status", "alert", "log", "marquee"]);
26
+ function truncate(s, max) {
27
+ return s.length <= max ? s : s.slice(0, max - 1) + "\u2026";
28
+ }
29
+ function isTransient(el) {
30
+ return TRANSIENT_ROLES.has(el.role);
31
+ }
32
+ function indexBlueprint(bp) {
33
+ const sections = /* @__PURE__ */ new Map();
34
+ const elementsByLogical = /* @__PURE__ */ new Map();
35
+ const elementsByStableId = /* @__PURE__ */ new Map();
36
+ const elementsByTestId = /* @__PURE__ */ new Map();
37
+ const repeatingByLogical = /* @__PURE__ */ new Map();
38
+ const repeatingByStableId = /* @__PURE__ */ new Map();
39
+ const repeatingByTestId = /* @__PURE__ */ new Map();
40
+ const ambiguousElementStableIds = /* @__PURE__ */ new Set();
41
+ const ambiguousElementTestIds = /* @__PURE__ */ new Set();
42
+ const ambiguousRepeatingStableIds = /* @__PURE__ */ new Set();
43
+ const ambiguousRepeatingTestIds = /* @__PURE__ */ new Set();
44
+ function setOnce(map, key, value, ambiguous) {
45
+ if (map.has(key))
46
+ ambiguous.add(key);
47
+ else
48
+ map.set(key, value);
49
+ }
50
+ for (const section of bp.sections) {
51
+ sections.set(section.name, section);
52
+ for (const el of section.elements) {
53
+ const entry = { section, element: el };
54
+ elementsByLogical.set(`${section.name}::${el.logicalName}`, entry);
55
+ if (el.stableId !== null)
56
+ setOnce(elementsByStableId, `${section.name}::${el.stableId}`, entry, ambiguousElementStableIds);
57
+ if (el.testId !== null)
58
+ setOnce(elementsByTestId, `${section.name}::${el.testId}`, entry, ambiguousElementTestIds);
59
+ }
60
+ for (const rep of section.repeatingElements) {
61
+ const entry = { section, element: rep };
62
+ repeatingByLogical.set(`${section.name}::${rep.logicalName}`, entry);
63
+ if (rep.stableId !== null)
64
+ setOnce(repeatingByStableId, `${section.name}::${rep.stableId}`, entry, ambiguousRepeatingStableIds);
65
+ if (rep.testId !== null)
66
+ setOnce(repeatingByTestId, `${section.name}::${rep.testId}`, entry, ambiguousRepeatingTestIds);
67
+ }
68
+ }
69
+ return {
70
+ sections,
71
+ elementsByLogical,
72
+ elementsByStableId,
73
+ elementsByTestId,
74
+ repeatingByLogical,
75
+ repeatingByStableId,
76
+ repeatingByTestId,
77
+ ambiguousElementStableIds,
78
+ ambiguousElementTestIds,
79
+ ambiguousRepeatingStableIds,
80
+ ambiguousRepeatingTestIds
81
+ };
82
+ }
83
+ function findCounterpart(el, section, index) {
84
+ const byLogical = index.elementsByLogical.get(`${section.name}::${el.logicalName}`);
85
+ if (byLogical) return byLogical;
86
+ if (el.stableId !== null) {
87
+ const key = `${section.name}::${el.stableId}`;
88
+ if (!index.ambiguousElementStableIds.has(key)) {
89
+ const bySid = index.elementsByStableId.get(key);
90
+ if (bySid) return bySid;
91
+ }
92
+ }
93
+ if (el.testId !== null) {
94
+ const key = `${section.name}::${el.testId}`;
95
+ if (!index.ambiguousElementTestIds.has(key)) {
96
+ const byTid = index.elementsByTestId.get(key);
97
+ if (byTid) return byTid;
98
+ }
99
+ }
100
+ return null;
101
+ }
102
+ function findRepeatingCounterpart(rep, section, index) {
103
+ const byLogical = index.repeatingByLogical.get(`${section.name}::${rep.logicalName}`);
104
+ if (byLogical) return byLogical;
105
+ if (rep.stableId !== null) {
106
+ const key = `${section.name}::${rep.stableId}`;
107
+ if (!index.ambiguousRepeatingStableIds.has(key)) {
108
+ const bySid = index.repeatingByStableId.get(key);
109
+ if (bySid) return bySid;
110
+ }
111
+ }
112
+ if (rep.testId !== null) {
113
+ const key = `${section.name}::${rep.testId}`;
114
+ if (!index.ambiguousRepeatingTestIds.has(key)) {
115
+ const byTid = index.repeatingByTestId.get(key);
116
+ if (byTid) return byTid;
117
+ }
118
+ }
119
+ return null;
120
+ }
121
+ function elementRef(section, el) {
122
+ return {
123
+ logicalName: el.logicalName,
124
+ sectionLogicalName: section.name,
125
+ role: el.role,
126
+ accessibleName: el.accessibleName
127
+ };
128
+ }
129
+ function diffBlueprints(before, after) {
130
+ const A = indexBlueprint(before);
131
+ const B = indexBlueprint(after);
132
+ const delta = {
133
+ hasStructuralChange: false,
134
+ sectionsAdded: [],
135
+ sectionsRemoved: [],
136
+ elementsAdded: [],
137
+ elementsRemoved: [],
138
+ repeatingCountChanges: [],
139
+ repeatingItemsChanged: [],
140
+ textChanges: [],
141
+ enrichmentChanges: []
142
+ };
143
+ if (before.url !== after.url)
144
+ delta.urlChange = { before: before.url, after: after.url };
145
+ if (before.pageHash !== after.pageHash)
146
+ delta.pageHashChange = { before: before.pageHash, after: after.pageHash };
147
+ for (const name of B.sections.keys())
148
+ if (!A.sections.has(name)) delta.sectionsAdded.push({ sectionLogicalName: name });
149
+ for (const name of A.sections.keys())
150
+ if (!B.sections.has(name)) delta.sectionsRemoved.push({ sectionLogicalName: name });
151
+ for (const { section, element } of B.elementsByLogical.values()) {
152
+ if (findCounterpart(element, section, A)) continue;
153
+ delta.elementsAdded.push({
154
+ ...elementRef(section, element),
155
+ kind: isTransient(element) ? "transient" : "persistent"
156
+ });
157
+ }
158
+ for (const { section, element } of A.elementsByLogical.values()) {
159
+ if (findCounterpart(element, section, B)) continue;
160
+ delta.elementsRemoved.push(elementRef(section, element));
161
+ }
162
+ for (const { section: sBefore, element: elBefore } of A.elementsByLogical.values()) {
163
+ const matched = findCounterpart(elBefore, sBefore, B);
164
+ if (!matched) continue;
165
+ const elAfter = matched.element;
166
+ const tA = elBefore.accessibleName.trim();
167
+ const tB = elAfter.accessibleName.trim();
168
+ if (tA !== tB) {
169
+ delta.textChanges.push({
170
+ logicalName: elBefore.logicalName,
171
+ sectionLogicalName: sBefore.name,
172
+ role: elAfter.role,
173
+ accessibleName: elAfter.accessibleName,
174
+ before: truncate(tA, 200),
175
+ after: truncate(tB, 200)
176
+ });
177
+ }
178
+ if (elBefore.mutability !== elAfter.mutability) {
179
+ delta.enrichmentChanges.push({
180
+ logicalName: elBefore.logicalName,
181
+ sectionLogicalName: sBefore.name,
182
+ field: "mutability",
183
+ before: elBefore.mutability,
184
+ after: elAfter.mutability
185
+ });
186
+ }
187
+ if (elBefore.widgetType !== elAfter.widgetType) {
188
+ delta.enrichmentChanges.push({
189
+ logicalName: elBefore.logicalName,
190
+ sectionLogicalName: sBefore.name,
191
+ field: "widgetType",
192
+ before: elBefore.widgetType,
193
+ after: elAfter.widgetType
194
+ });
195
+ }
196
+ }
197
+ for (const { section, element: repB } of B.repeatingByLogical.values()) {
198
+ const matched = findRepeatingCounterpart(repB, section, A);
199
+ if (!matched) {
200
+ delta.elementsAdded.push({
201
+ logicalName: repB.logicalName,
202
+ sectionLogicalName: section.name,
203
+ role: repB.role,
204
+ accessibleName: repB.accessibleNameTemplate,
205
+ kind: "persistent"
206
+ });
207
+ continue;
208
+ }
209
+ const repA = matched.element;
210
+ if (repA.items.length !== repB.items.length) {
211
+ delta.repeatingCountChanges.push({
212
+ logicalName: repB.logicalName,
213
+ sectionLogicalName: section.name,
214
+ role: repB.role,
215
+ accessibleNameTemplate: repB.accessibleNameTemplate,
216
+ before: repA.items.length,
217
+ after: repB.items.length,
218
+ delta: repB.items.length - repA.items.length
219
+ });
220
+ } else {
221
+ const aKeys = new Set(repA.items.map((i) => (0, import_fingerprint.canonicalJson)(i.parameters)));
222
+ const bKeys = new Set(repB.items.map((i) => (0, import_fingerprint.canonicalJson)(i.parameters)));
223
+ let changed = aKeys.size !== bKeys.size;
224
+ if (!changed) {
225
+ for (const k of aKeys) if (!bKeys.has(k)) {
226
+ changed = true;
227
+ break;
228
+ }
229
+ }
230
+ if (changed) {
231
+ delta.repeatingItemsChanged.push({
232
+ logicalName: repB.logicalName,
233
+ sectionLogicalName: section.name,
234
+ changed: true
235
+ });
236
+ }
237
+ }
238
+ }
239
+ for (const { section, element: repA } of A.repeatingByLogical.values()) {
240
+ if (findRepeatingCounterpart(repA, section, B)) continue;
241
+ delta.elementsRemoved.push({
242
+ logicalName: repA.logicalName,
243
+ sectionLogicalName: section.name,
244
+ role: repA.role,
245
+ accessibleName: repA.accessibleNameTemplate
246
+ });
247
+ }
248
+ delta.hasStructuralChange = delta.sectionsAdded.length > 0 || delta.sectionsRemoved.length > 0 || delta.elementsAdded.length > 0 || delta.elementsRemoved.length > 0 || delta.repeatingCountChanges.length > 0 || delta.repeatingItemsChanged.length > 0;
249
+ return delta;
250
+ }
251
+ // Annotate the CommonJS export names for ESM import in node:
252
+ 0 && (module.exports = {
253
+ diffBlueprints
254
+ });
@@ -0,0 +1,304 @@
1
+ "use strict";
2
+ var import_blueprintDiff = require("./blueprintDiff");
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
+ function makeElement(overrides = {}) {
14
+ return {
15
+ logicalName: "el",
16
+ role: "button",
17
+ accessibleName: "Click me",
18
+ xpath: "//button",
19
+ mutability: "immutable",
20
+ widgetType: "native",
21
+ framePath: [],
22
+ shadowRoot: false,
23
+ stableId: null,
24
+ testId: null,
25
+ fingerprint: null,
26
+ ...overrides
27
+ };
28
+ }
29
+ function makeRepeating(overrides = {}) {
30
+ return {
31
+ logicalName: "view_details_for_order_btn",
32
+ role: "button",
33
+ accessibleNameTemplate: "View details for order {orderId}",
34
+ parameters: ["orderId"],
35
+ xpathPattern: ".//button[{row}]",
36
+ indexParameter: "orderId",
37
+ items: [],
38
+ mutability: "immutable",
39
+ widgetType: "native",
40
+ framePath: [],
41
+ shadowRoot: false,
42
+ stableId: null,
43
+ testId: null,
44
+ fingerprint: null,
45
+ ...overrides
46
+ };
47
+ }
48
+ function makeSection(name, elements = [], reps = []) {
49
+ return { name, landmark: "main", elements, repeatingElements: reps };
50
+ }
51
+ function makeBlueprint(url, sections, pageHash = "h1") {
52
+ return {
53
+ schemaVersion: 1,
54
+ url,
55
+ capturedAt: "2026-04-30T00:00:00.000Z",
56
+ pageHash,
57
+ sections
58
+ };
59
+ }
60
+ test("diffBlueprints: identical blueprints have no changes", () => {
61
+ const bp = makeBlueprint("http://x/a", [makeSection("header", [makeElement()])]);
62
+ const d = (0, import_blueprintDiff.diffBlueprints)(bp, bp);
63
+ assertEqual(d.hasStructuralChange, false);
64
+ assertEqual(d.sectionsAdded, []);
65
+ assertEqual(d.sectionsRemoved, []);
66
+ assertEqual(d.elementsAdded, []);
67
+ assertEqual(d.elementsRemoved, []);
68
+ assertEqual(d.textChanges, []);
69
+ });
70
+ test("diffBlueprints: urlChange emitted when URL differs", () => {
71
+ const a = makeBlueprint("http://x/a", [makeSection("main")]);
72
+ const b = makeBlueprint("http://x/b", [makeSection("main")]);
73
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
74
+ assertEqual(d.urlChange, { before: "http://x/a", after: "http://x/b" });
75
+ });
76
+ test("diffBlueprints: pageHashChange emitted when hash differs", () => {
77
+ const a = makeBlueprint("http://x/a", [makeSection("main")], "h1");
78
+ const b = makeBlueprint("http://x/a", [makeSection("main")], "h2");
79
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
80
+ assertEqual(d.pageHashChange, { before: "h1", after: "h2" });
81
+ });
82
+ test("diffBlueprints: hasStructuralChange false when only URL and pageHash changed", () => {
83
+ const a = makeBlueprint("http://x/a", [makeSection("main")], "h1");
84
+ const b = makeBlueprint("http://x/b", [makeSection("main")], "h2");
85
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
86
+ assertEqual(d.urlChange, { before: "http://x/a", after: "http://x/b" });
87
+ assertEqual(d.pageHashChange, { before: "h1", after: "h2" });
88
+ assertEqual(d.hasStructuralChange, false);
89
+ });
90
+ test("diffBlueprints: sectionsAdded / sectionsRemoved", () => {
91
+ const a = makeBlueprint("http://x", [makeSection("a")]);
92
+ const b = makeBlueprint("http://x", [makeSection("a"), makeSection("b")]);
93
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
94
+ assertEqual(d.sectionsAdded, [{ sectionLogicalName: "b" }]);
95
+ assertEqual(d.sectionsRemoved, []);
96
+ assertEqual(d.hasStructuralChange, true);
97
+ });
98
+ test("diffBlueprints: elementsAdded in existing section", () => {
99
+ const a = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "a" })])]);
100
+ const b = makeBlueprint("http://x", [makeSection("main", [
101
+ makeElement({ logicalName: "a" }),
102
+ makeElement({ logicalName: "b", accessibleName: "Second" })
103
+ ])]);
104
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
105
+ assertEqual(d.elementsAdded.length, 1);
106
+ assertEqual(d.elementsAdded[0].logicalName, "b");
107
+ assertEqual(d.elementsAdded[0].kind, "persistent");
108
+ });
109
+ test("diffBlueprints: elementsAdded with role=status tagged transient", () => {
110
+ const a = makeBlueprint("http://x", [makeSection("main")]);
111
+ const b = makeBlueprint("http://x", [makeSection("main", [
112
+ makeElement({ logicalName: "toast", role: "status", accessibleName: "Saved" })
113
+ ])]);
114
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
115
+ assertEqual(d.elementsAdded[0].kind, "transient");
116
+ });
117
+ test("diffBlueprints: role=alert is transient", () => {
118
+ const a = makeBlueprint("http://x", [makeSection("main")]);
119
+ const b = makeBlueprint("http://x", [makeSection("main", [
120
+ makeElement({ logicalName: "err", role: "alert", accessibleName: "Error" })
121
+ ])]);
122
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
123
+ assertEqual(d.elementsAdded[0].kind, "transient");
124
+ });
125
+ test("diffBlueprints: elementsRemoved", () => {
126
+ const a = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "a" })])]);
127
+ const b = makeBlueprint("http://x", [makeSection("main")]);
128
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
129
+ assertEqual(d.elementsRemoved.length, 1);
130
+ assertEqual(d.elementsRemoved[0].logicalName, "a");
131
+ });
132
+ test("diffBlueprints: same logicalName in different sections is not accidentally matched", () => {
133
+ const a = makeBlueprint("http://x", [
134
+ makeSection("header", [makeElement({ logicalName: "login_btn", accessibleName: "Log in" })]),
135
+ makeSection("footer", [makeElement({ logicalName: "login_btn", accessibleName: "Sign in" })])
136
+ ]);
137
+ const b = makeBlueprint("http://x", [
138
+ makeSection("header", [makeElement({ logicalName: "login_btn", accessibleName: "Log in" })])
139
+ // footer's login_btn removed
140
+ ]);
141
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
142
+ assertEqual(d.elementsRemoved.length, 1);
143
+ assertEqual(d.elementsRemoved[0].logicalName, "login_btn");
144
+ assertEqual(d.elementsRemoved[0].sectionLogicalName, "footer");
145
+ assertEqual(d.textChanges, []);
146
+ assertEqual(d.elementsAdded, []);
147
+ });
148
+ test("diffBlueprints: repeatingCountChanges 12 -> 13", () => {
149
+ const items12 = Array.from({ length: 12 }, (_, i) => ({ parameters: { orderId: String(i + 1) } }));
150
+ const items13 = [...items12, { parameters: { orderId: "13" } }];
151
+ const a = makeBlueprint("http://x", [makeSection("list", [], [makeRepeating({ items: items12 })])]);
152
+ const b = makeBlueprint("http://x", [makeSection("list", [], [makeRepeating({ items: items13 })])]);
153
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
154
+ assertEqual(d.repeatingCountChanges.length, 1);
155
+ assertEqual(d.repeatingCountChanges[0].before, 12);
156
+ assertEqual(d.repeatingCountChanges[0].after, 13);
157
+ assertEqual(d.repeatingCountChanges[0].delta, 1);
158
+ assertEqual(d.repeatingCountChanges[0].role, "button");
159
+ assertEqual(d.repeatingCountChanges[0].accessibleNameTemplate, "View details for order {orderId}");
160
+ });
161
+ test("diffBlueprints: repeatingItemsChanged detects same-count composition shift", () => {
162
+ const items1 = [{ parameters: { orderId: "1" } }, { parameters: { orderId: "2" } }];
163
+ const items2 = [{ parameters: { orderId: "1" } }, { parameters: { orderId: "3" } }];
164
+ const a = makeBlueprint("http://x", [makeSection("list", [], [makeRepeating({ items: items1 })])]);
165
+ const b = makeBlueprint("http://x", [makeSection("list", [], [makeRepeating({ items: items2 })])]);
166
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
167
+ assertEqual(d.repeatingCountChanges, []);
168
+ assertEqual(d.repeatingItemsChanged.length, 1);
169
+ assertEqual(d.repeatingItemsChanged[0].changed, true);
170
+ });
171
+ test("diffBlueprints: textChanges on matched element", () => {
172
+ const a = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "status", accessibleName: "Pending" })])]);
173
+ const b = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "status", accessibleName: "Cancelled" })])]);
174
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
175
+ assertEqual(d.textChanges.length, 1);
176
+ assertEqual(d.textChanges[0].before, "Pending");
177
+ assertEqual(d.textChanges[0].after, "Cancelled");
178
+ assertEqual(d.textChanges[0].role, "button");
179
+ assertEqual(d.textChanges[0].accessibleName, "Cancelled");
180
+ });
181
+ test("diffBlueprints: whitespace-only text changes ignored", () => {
182
+ const a = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "s", accessibleName: "Saved" })])]);
183
+ const b = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "s", accessibleName: "Saved " })])]);
184
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
185
+ assertEqual(d.textChanges, []);
186
+ });
187
+ test("diffBlueprints: text truncated to 200 chars", () => {
188
+ const long = "x".repeat(300);
189
+ const a = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "s", accessibleName: "a" })])]);
190
+ const b = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "s", accessibleName: long })])]);
191
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
192
+ if (d.textChanges[0].after.length > 200)
193
+ throw new Error(`text not truncated: ${d.textChanges[0].after.length}`);
194
+ });
195
+ test("diffBlueprints: enrichmentChanges for mutability flip", () => {
196
+ const a = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "x", mutability: "immutable" })])]);
197
+ const b = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "x", mutability: "mutable" })])]);
198
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
199
+ assertEqual(d.enrichmentChanges.length, 1);
200
+ assertEqual(d.enrichmentChanges[0].field, "mutability");
201
+ });
202
+ test("diffBlueprints: enrichmentChanges for widgetType flip", () => {
203
+ const a = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "x", widgetType: "native" })])]);
204
+ const b = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "x", widgetType: "custom", fingerprint: "abcdef0123456789" })])]);
205
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
206
+ assertEqual(d.enrichmentChanges.length, 1);
207
+ assertEqual(d.enrichmentChanges[0].field, "widgetType");
208
+ assertEqual(d.enrichmentChanges[0].before, "native");
209
+ assertEqual(d.enrichmentChanges[0].after, "custom");
210
+ });
211
+ test("diffBlueprints: hasStructuralChange false when only text changed", () => {
212
+ const a = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "s", accessibleName: "A" })])]);
213
+ const b = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "s", accessibleName: "B" })])]);
214
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
215
+ assertEqual(d.hasStructuralChange, false);
216
+ });
217
+ test("diffBlueprints: element matched via stableId when logicalName drifts", () => {
218
+ const a = makeBlueprint("http://x", [makeSection("main", [
219
+ makeElement({ logicalName: "status_region", stableId: "toast-region", accessibleName: "" })
220
+ ])]);
221
+ const b = makeBlueprint("http://x", [makeSection("main", [
222
+ makeElement({ logicalName: "order_placed_status", stableId: "toast-region", accessibleName: "Order placed" })
223
+ ])]);
224
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
225
+ assertEqual(d.elementsAdded, []);
226
+ assertEqual(d.elementsRemoved, []);
227
+ assertEqual(d.textChanges.length, 1);
228
+ assertEqual(d.textChanges[0].before, "");
229
+ assertEqual(d.textChanges[0].after, "Order placed");
230
+ assertEqual(d.textChanges[0].role, "button");
231
+ assertEqual(d.textChanges[0].accessibleName, "Order placed");
232
+ assertEqual(d.hasStructuralChange, false);
233
+ });
234
+ test("diffBlueprints: element matched via testId when logicalName drifts", () => {
235
+ const a = makeBlueprint("http://x", [makeSection("main", [
236
+ makeElement({ logicalName: "save_btn", testId: "save-action", accessibleName: "Save" })
237
+ ])]);
238
+ const b = makeBlueprint("http://x", [makeSection("main", [
239
+ makeElement({ logicalName: "saving_btn", testId: "save-action", accessibleName: "Saving\u2026" })
240
+ ])]);
241
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
242
+ assertEqual(d.elementsAdded, []);
243
+ assertEqual(d.elementsRemoved, []);
244
+ assertEqual(d.textChanges.length, 1);
245
+ });
246
+ test("diffBlueprints: elements with no stable identity fall through to add+remove when logicalName drifts", () => {
247
+ const a = makeBlueprint("http://x", [makeSection("main", [
248
+ makeElement({ logicalName: "alpha", stableId: null, testId: null, accessibleName: "Alpha" })
249
+ ])]);
250
+ const b = makeBlueprint("http://x", [makeSection("main", [
251
+ makeElement({ logicalName: "beta", stableId: null, testId: null, accessibleName: "Beta" })
252
+ ])]);
253
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
254
+ assertEqual(d.elementsAdded.length, 1);
255
+ assertEqual(d.elementsRemoved.length, 1);
256
+ });
257
+ test("diffBlueprints: duplicate testId in same section disables testId fallback", () => {
258
+ const a = makeBlueprint("http://x", [makeSection("main", [
259
+ makeElement({ logicalName: "alpha_btn", testId: "shared-tid", accessibleName: "Alpha" }),
260
+ makeElement({ logicalName: "beta_btn", testId: "shared-tid", accessibleName: "Beta" })
261
+ ])]);
262
+ const b = makeBlueprint("http://x", [makeSection("main", [
263
+ makeElement({ logicalName: "alpha_renamed", testId: "shared-tid", accessibleName: "Alpha Renamed" }),
264
+ makeElement({ logicalName: "beta_renamed", testId: "shared-tid", accessibleName: "Beta Renamed" })
265
+ ])]);
266
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
267
+ assertEqual(d.elementsAdded.length, 2);
268
+ assertEqual(d.elementsRemoved.length, 2);
269
+ assertEqual(d.textChanges.length, 0);
270
+ });
271
+ test("diffBlueprints: unique testId still falls back to match correctly", () => {
272
+ const a = makeBlueprint("http://x", [makeSection("main", [
273
+ makeElement({ logicalName: "unique_btn", testId: "unique-tid", accessibleName: "Unique" }),
274
+ makeElement({ logicalName: "dup_a", testId: "dup-tid", accessibleName: "Dup A" }),
275
+ makeElement({ logicalName: "dup_b", testId: "dup-tid", accessibleName: "Dup B" })
276
+ ])]);
277
+ const b = makeBlueprint("http://x", [makeSection("main", [
278
+ makeElement({ logicalName: "unique_renamed", testId: "unique-tid", accessibleName: "Unique Renamed" }),
279
+ makeElement({ logicalName: "dup_a", testId: "dup-tid", accessibleName: "Dup A" }),
280
+ makeElement({ logicalName: "dup_b", testId: "dup-tid", accessibleName: "Dup B" })
281
+ ])]);
282
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
283
+ assertEqual(d.elementsAdded.length, 0);
284
+ assertEqual(d.elementsRemoved.length, 0);
285
+ assertEqual(d.textChanges.length, 1);
286
+ });
287
+ let failed = 0;
288
+ for (const { name, run } of cases) {
289
+ try {
290
+ run();
291
+ console.log(" \u2713", name);
292
+ } catch (e) {
293
+ failed++;
294
+ console.log(" \u2717", name);
295
+ console.log(" ", e.message);
296
+ }
297
+ }
298
+ if (failed > 0) {
299
+ console.log(`
300
+ ${failed}/${cases.length} failed`);
301
+ process.exit(1);
302
+ }
303
+ console.log(`
304
+ ${cases.length} passed`);