@skyramp/mcp 0.1.8 → 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.
- package/build/playwright/registerPlaywrightTools.js +12 -0
- package/build/playwright/traceRecordingPrompt.js +15 -0
- package/build/prompts/test-recommendation/diffExecutionPlan.js +31 -0
- package/build/prompts/test-recommendation/recommendationSections.js +1 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +94 -0
- package/build/prompts/testbot/testbot-prompts.js +115 -11
- package/build/prompts/testbot/testbot-prompts.test.js +79 -0
- package/build/resources/testbotResource.js +1 -1
- package/build/services/ScenarioGenerationService.integration.test.js +158 -0
- package/build/services/ScenarioGenerationService.js +36 -3
- package/build/services/ScenarioGenerationService.test.js +158 -22
- 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/analyzeChangesTool.js +7 -1
- package/build/utils/routeParsers.js +12 -0
- 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 +1161 -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 +250 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.test.js +298 -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/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 +129 -0
- package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.test.js +137 -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/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`);
|