@skyramp/mcp 0.1.7 → 0.2.0-rc.1

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 (61) hide show
  1. package/build/playwright/registerPlaywrightTools.js +12 -0
  2. package/build/playwright/traceRecordingPrompt.js +15 -0
  3. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +1 -1
  4. package/build/prompts/test-recommendation/diffExecutionPlan.js +31 -0
  5. package/build/prompts/test-recommendation/recommendationSections.js +1 -2
  6. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +94 -0
  7. package/build/prompts/testbot/testbot-prompts.js +115 -11
  8. package/build/prompts/testbot/testbot-prompts.test.js +79 -0
  9. package/build/resources/testbotResource.js +1 -1
  10. package/build/services/ScenarioGenerationService.integration.test.js +158 -0
  11. package/build/services/ScenarioGenerationService.js +36 -3
  12. package/build/services/ScenarioGenerationService.test.js +158 -22
  13. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +16 -4
  14. package/build/tools/generate-tests/generateIntegrationRestTool.js +2 -0
  15. package/build/tools/generate-tests/generateUIRestTool.js +2 -0
  16. package/build/tools/test-management/analyzeChangesTool.js +7 -1
  17. package/build/utils/routeParsers.js +12 -0
  18. package/node_modules/playwright/ThirdPartyNotices.txt +6 -6
  19. package/node_modules/playwright/lib/dom-analyzer/analyze.js +111 -0
  20. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1161 -0
  21. package/node_modules/playwright/lib/dom-analyzer/blueprint.test.js +396 -0
  22. package/node_modules/playwright/lib/dom-analyzer/blueprintCache.js +57 -0
  23. package/node_modules/playwright/lib/dom-analyzer/blueprintCache.test.js +57 -0
  24. package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.js +250 -0
  25. package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.test.js +298 -0
  26. package/node_modules/playwright/lib/dom-analyzer/crawler.js +384 -0
  27. package/node_modules/playwright/lib/dom-analyzer/curatedWidgets.js +73 -0
  28. package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +43 -0
  29. package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +85 -0
  30. package/node_modules/playwright/lib/dom-analyzer/fingerprint.js +90 -0
  31. package/node_modules/playwright/lib/dom-analyzer/fingerprint.test.js +231 -0
  32. package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.fixtures.js +145 -0
  33. package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.test.js +41 -0
  34. package/node_modules/playwright/lib/dom-analyzer/graph.js +36 -0
  35. package/node_modules/playwright/lib/dom-analyzer/liveFingerprints.js +43 -0
  36. package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.js +72 -0
  37. package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.test.js +182 -0
  38. package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.js +169 -0
  39. package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.test.js +269 -0
  40. package/node_modules/playwright/lib/dom-analyzer/serialization.js +75 -0
  41. package/node_modules/playwright/lib/dom-analyzer/slug.js +30 -0
  42. package/node_modules/playwright/lib/dom-analyzer/slug.test.js +84 -0
  43. package/node_modules/playwright/lib/dom-analyzer/widgetContract.js +127 -0
  44. package/node_modules/playwright/lib/dom-analyzer/widgetContract.test.js +212 -0
  45. package/node_modules/playwright/lib/mcp/browser/browserContextFactory.js +3 -1
  46. package/node_modules/playwright/lib/mcp/browser/config.js +1 -1
  47. package/node_modules/playwright/lib/mcp/browser/context.js +17 -1
  48. package/node_modules/playwright/lib/mcp/browser/tab.js +38 -0
  49. package/node_modules/playwright/lib/mcp/browser/tools/domAnalyzer.js +261 -0
  50. package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -3
  51. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.js +129 -0
  52. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.test.js +137 -0
  53. package/node_modules/playwright/lib/mcp/browser/tools/sitemap.js +226 -0
  54. package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +2 -2
  55. package/node_modules/playwright/lib/mcp/browser/tools/widgetContract.js +168 -0
  56. package/node_modules/playwright/lib/mcp/browser/tools.js +6 -0
  57. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +52 -12
  58. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +64 -13
  59. package/node_modules/playwright/package.json +1 -1
  60. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.3.tgz +0 -0
  61. package/package.json +2 -2
@@ -0,0 +1,250 @@
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
+ before: truncate(tA, 200),
173
+ after: truncate(tB, 200)
174
+ });
175
+ }
176
+ if (elBefore.mutability !== elAfter.mutability) {
177
+ delta.enrichmentChanges.push({
178
+ logicalName: elBefore.logicalName,
179
+ sectionLogicalName: sBefore.name,
180
+ field: "mutability",
181
+ before: elBefore.mutability,
182
+ after: elAfter.mutability
183
+ });
184
+ }
185
+ if (elBefore.widgetType !== elAfter.widgetType) {
186
+ delta.enrichmentChanges.push({
187
+ logicalName: elBefore.logicalName,
188
+ sectionLogicalName: sBefore.name,
189
+ field: "widgetType",
190
+ before: elBefore.widgetType,
191
+ after: elAfter.widgetType
192
+ });
193
+ }
194
+ }
195
+ for (const { section, element: repB } of B.repeatingByLogical.values()) {
196
+ const matched = findRepeatingCounterpart(repB, section, A);
197
+ if (!matched) {
198
+ delta.elementsAdded.push({
199
+ logicalName: repB.logicalName,
200
+ sectionLogicalName: section.name,
201
+ role: repB.role,
202
+ accessibleName: repB.accessibleNameTemplate,
203
+ kind: "persistent"
204
+ });
205
+ continue;
206
+ }
207
+ const repA = matched.element;
208
+ if (repA.items.length !== repB.items.length) {
209
+ delta.repeatingCountChanges.push({
210
+ logicalName: repB.logicalName,
211
+ sectionLogicalName: section.name,
212
+ before: repA.items.length,
213
+ after: repB.items.length,
214
+ delta: repB.items.length - repA.items.length
215
+ });
216
+ } else {
217
+ const aKeys = new Set(repA.items.map((i) => (0, import_fingerprint.canonicalJson)(i.parameters)));
218
+ const bKeys = new Set(repB.items.map((i) => (0, import_fingerprint.canonicalJson)(i.parameters)));
219
+ let changed = aKeys.size !== bKeys.size;
220
+ if (!changed) {
221
+ for (const k of aKeys) if (!bKeys.has(k)) {
222
+ changed = true;
223
+ break;
224
+ }
225
+ }
226
+ if (changed) {
227
+ delta.repeatingItemsChanged.push({
228
+ logicalName: repB.logicalName,
229
+ sectionLogicalName: section.name,
230
+ changed: true
231
+ });
232
+ }
233
+ }
234
+ }
235
+ for (const { section, element: repA } of A.repeatingByLogical.values()) {
236
+ if (findRepeatingCounterpart(repA, section, B)) continue;
237
+ delta.elementsRemoved.push({
238
+ logicalName: repA.logicalName,
239
+ sectionLogicalName: section.name,
240
+ role: repA.role,
241
+ accessibleName: repA.accessibleNameTemplate
242
+ });
243
+ }
244
+ 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;
245
+ return delta;
246
+ }
247
+ // Annotate the CommonJS export names for ESM import in node:
248
+ 0 && (module.exports = {
249
+ diffBlueprints
250
+ });
@@ -0,0 +1,298 @@
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
+ });
159
+ test("diffBlueprints: repeatingItemsChanged detects same-count composition shift", () => {
160
+ const items1 = [{ parameters: { orderId: "1" } }, { parameters: { orderId: "2" } }];
161
+ const items2 = [{ parameters: { orderId: "1" } }, { parameters: { orderId: "3" } }];
162
+ const a = makeBlueprint("http://x", [makeSection("list", [], [makeRepeating({ items: items1 })])]);
163
+ const b = makeBlueprint("http://x", [makeSection("list", [], [makeRepeating({ items: items2 })])]);
164
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
165
+ assertEqual(d.repeatingCountChanges, []);
166
+ assertEqual(d.repeatingItemsChanged.length, 1);
167
+ assertEqual(d.repeatingItemsChanged[0].changed, true);
168
+ });
169
+ test("diffBlueprints: textChanges on matched element", () => {
170
+ const a = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "status", accessibleName: "Pending" })])]);
171
+ const b = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "status", accessibleName: "Cancelled" })])]);
172
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
173
+ assertEqual(d.textChanges.length, 1);
174
+ assertEqual(d.textChanges[0].before, "Pending");
175
+ assertEqual(d.textChanges[0].after, "Cancelled");
176
+ });
177
+ test("diffBlueprints: whitespace-only text changes ignored", () => {
178
+ const a = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "s", accessibleName: "Saved" })])]);
179
+ const b = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "s", accessibleName: "Saved " })])]);
180
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
181
+ assertEqual(d.textChanges, []);
182
+ });
183
+ test("diffBlueprints: text truncated to 200 chars", () => {
184
+ const long = "x".repeat(300);
185
+ const a = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "s", accessibleName: "a" })])]);
186
+ const b = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "s", accessibleName: long })])]);
187
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
188
+ if (d.textChanges[0].after.length > 200)
189
+ throw new Error(`text not truncated: ${d.textChanges[0].after.length}`);
190
+ });
191
+ test("diffBlueprints: enrichmentChanges for mutability flip", () => {
192
+ const a = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "x", mutability: "immutable" })])]);
193
+ const b = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "x", mutability: "mutable" })])]);
194
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
195
+ assertEqual(d.enrichmentChanges.length, 1);
196
+ assertEqual(d.enrichmentChanges[0].field, "mutability");
197
+ });
198
+ test("diffBlueprints: enrichmentChanges for widgetType flip", () => {
199
+ const a = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "x", widgetType: "native" })])]);
200
+ const b = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "x", widgetType: "custom", fingerprint: "abcdef0123456789" })])]);
201
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
202
+ assertEqual(d.enrichmentChanges.length, 1);
203
+ assertEqual(d.enrichmentChanges[0].field, "widgetType");
204
+ assertEqual(d.enrichmentChanges[0].before, "native");
205
+ assertEqual(d.enrichmentChanges[0].after, "custom");
206
+ });
207
+ test("diffBlueprints: hasStructuralChange false when only text changed", () => {
208
+ const a = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "s", accessibleName: "A" })])]);
209
+ const b = makeBlueprint("http://x", [makeSection("main", [makeElement({ logicalName: "s", accessibleName: "B" })])]);
210
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
211
+ assertEqual(d.hasStructuralChange, false);
212
+ });
213
+ test("diffBlueprints: element matched via stableId when logicalName drifts", () => {
214
+ const a = makeBlueprint("http://x", [makeSection("main", [
215
+ makeElement({ logicalName: "status_region", stableId: "toast-region", accessibleName: "" })
216
+ ])]);
217
+ const b = makeBlueprint("http://x", [makeSection("main", [
218
+ makeElement({ logicalName: "order_placed_status", stableId: "toast-region", accessibleName: "Order placed" })
219
+ ])]);
220
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
221
+ assertEqual(d.elementsAdded, []);
222
+ assertEqual(d.elementsRemoved, []);
223
+ assertEqual(d.textChanges.length, 1);
224
+ assertEqual(d.textChanges[0].before, "");
225
+ assertEqual(d.textChanges[0].after, "Order placed");
226
+ assertEqual(d.hasStructuralChange, false);
227
+ });
228
+ test("diffBlueprints: element matched via testId when logicalName drifts", () => {
229
+ const a = makeBlueprint("http://x", [makeSection("main", [
230
+ makeElement({ logicalName: "save_btn", testId: "save-action", accessibleName: "Save" })
231
+ ])]);
232
+ const b = makeBlueprint("http://x", [makeSection("main", [
233
+ makeElement({ logicalName: "saving_btn", testId: "save-action", accessibleName: "Saving\u2026" })
234
+ ])]);
235
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
236
+ assertEqual(d.elementsAdded, []);
237
+ assertEqual(d.elementsRemoved, []);
238
+ assertEqual(d.textChanges.length, 1);
239
+ });
240
+ test("diffBlueprints: elements with no stable identity fall through to add+remove when logicalName drifts", () => {
241
+ const a = makeBlueprint("http://x", [makeSection("main", [
242
+ makeElement({ logicalName: "alpha", stableId: null, testId: null, accessibleName: "Alpha" })
243
+ ])]);
244
+ const b = makeBlueprint("http://x", [makeSection("main", [
245
+ makeElement({ logicalName: "beta", stableId: null, testId: null, accessibleName: "Beta" })
246
+ ])]);
247
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
248
+ assertEqual(d.elementsAdded.length, 1);
249
+ assertEqual(d.elementsRemoved.length, 1);
250
+ });
251
+ test("diffBlueprints: duplicate testId in same section disables testId fallback", () => {
252
+ const a = makeBlueprint("http://x", [makeSection("main", [
253
+ makeElement({ logicalName: "alpha_btn", testId: "shared-tid", accessibleName: "Alpha" }),
254
+ makeElement({ logicalName: "beta_btn", testId: "shared-tid", accessibleName: "Beta" })
255
+ ])]);
256
+ const b = makeBlueprint("http://x", [makeSection("main", [
257
+ makeElement({ logicalName: "alpha_renamed", testId: "shared-tid", accessibleName: "Alpha Renamed" }),
258
+ makeElement({ logicalName: "beta_renamed", testId: "shared-tid", accessibleName: "Beta Renamed" })
259
+ ])]);
260
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
261
+ assertEqual(d.elementsAdded.length, 2);
262
+ assertEqual(d.elementsRemoved.length, 2);
263
+ assertEqual(d.textChanges.length, 0);
264
+ });
265
+ test("diffBlueprints: unique testId still falls back to match correctly", () => {
266
+ const a = makeBlueprint("http://x", [makeSection("main", [
267
+ makeElement({ logicalName: "unique_btn", testId: "unique-tid", accessibleName: "Unique" }),
268
+ makeElement({ logicalName: "dup_a", testId: "dup-tid", accessibleName: "Dup A" }),
269
+ makeElement({ logicalName: "dup_b", testId: "dup-tid", accessibleName: "Dup B" })
270
+ ])]);
271
+ const b = makeBlueprint("http://x", [makeSection("main", [
272
+ makeElement({ logicalName: "unique_renamed", testId: "unique-tid", accessibleName: "Unique Renamed" }),
273
+ makeElement({ logicalName: "dup_a", testId: "dup-tid", accessibleName: "Dup A" }),
274
+ makeElement({ logicalName: "dup_b", testId: "dup-tid", accessibleName: "Dup B" })
275
+ ])]);
276
+ const d = (0, import_blueprintDiff.diffBlueprints)(a, b);
277
+ assertEqual(d.elementsAdded.length, 0);
278
+ assertEqual(d.elementsRemoved.length, 0);
279
+ assertEqual(d.textChanges.length, 1);
280
+ });
281
+ let failed = 0;
282
+ for (const { name, run } of cases) {
283
+ try {
284
+ run();
285
+ console.log(" \u2713", name);
286
+ } catch (e) {
287
+ failed++;
288
+ console.log(" \u2717", name);
289
+ console.log(" ", e.message);
290
+ }
291
+ }
292
+ if (failed > 0) {
293
+ console.log(`
294
+ ${failed}/${cases.length} failed`);
295
+ process.exit(1);
296
+ }
297
+ console.log(`
298
+ ${cases.length} passed`);