@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.
Files changed (60) hide show
  1. package/build/playwright/registerPlaywrightTools.js +12 -0
  2. package/build/playwright/traceRecordingPrompt.js +15 -0
  3. package/build/prompts/test-recommendation/diffExecutionPlan.js +31 -0
  4. package/build/prompts/test-recommendation/recommendationSections.js +1 -2
  5. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +94 -0
  6. package/build/prompts/testbot/testbot-prompts.js +115 -11
  7. package/build/prompts/testbot/testbot-prompts.test.js +79 -0
  8. package/build/resources/testbotResource.js +1 -1
  9. package/build/services/ScenarioGenerationService.integration.test.js +158 -0
  10. package/build/services/ScenarioGenerationService.js +36 -3
  11. package/build/services/ScenarioGenerationService.test.js +158 -22
  12. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +16 -4
  13. package/build/tools/generate-tests/generateIntegrationRestTool.js +2 -0
  14. package/build/tools/generate-tests/generateUIRestTool.js +2 -0
  15. package/build/tools/test-management/analyzeChangesTool.js +7 -1
  16. package/build/utils/routeParsers.js +12 -0
  17. package/node_modules/playwright/ThirdPartyNotices.txt +6 -6
  18. package/node_modules/playwright/lib/dom-analyzer/analyze.js +111 -0
  19. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1161 -0
  20. package/node_modules/playwright/lib/dom-analyzer/blueprint.test.js +396 -0
  21. package/node_modules/playwright/lib/dom-analyzer/blueprintCache.js +57 -0
  22. package/node_modules/playwright/lib/dom-analyzer/blueprintCache.test.js +57 -0
  23. package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.js +250 -0
  24. package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.test.js +298 -0
  25. package/node_modules/playwright/lib/dom-analyzer/crawler.js +384 -0
  26. package/node_modules/playwright/lib/dom-analyzer/curatedWidgets.js +73 -0
  27. package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +43 -0
  28. package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +85 -0
  29. package/node_modules/playwright/lib/dom-analyzer/fingerprint.js +90 -0
  30. package/node_modules/playwright/lib/dom-analyzer/fingerprint.test.js +231 -0
  31. package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.fixtures.js +145 -0
  32. package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.test.js +41 -0
  33. package/node_modules/playwright/lib/dom-analyzer/graph.js +36 -0
  34. package/node_modules/playwright/lib/dom-analyzer/liveFingerprints.js +43 -0
  35. package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.js +72 -0
  36. package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.test.js +182 -0
  37. package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.js +169 -0
  38. package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.test.js +269 -0
  39. package/node_modules/playwright/lib/dom-analyzer/serialization.js +75 -0
  40. package/node_modules/playwright/lib/dom-analyzer/slug.js +30 -0
  41. package/node_modules/playwright/lib/dom-analyzer/slug.test.js +84 -0
  42. package/node_modules/playwright/lib/dom-analyzer/widgetContract.js +127 -0
  43. package/node_modules/playwright/lib/dom-analyzer/widgetContract.test.js +212 -0
  44. package/node_modules/playwright/lib/mcp/browser/browserContextFactory.js +3 -1
  45. package/node_modules/playwright/lib/mcp/browser/config.js +1 -1
  46. package/node_modules/playwright/lib/mcp/browser/context.js +17 -1
  47. package/node_modules/playwright/lib/mcp/browser/tab.js +38 -0
  48. package/node_modules/playwright/lib/mcp/browser/tools/domAnalyzer.js +261 -0
  49. package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -3
  50. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.js +129 -0
  51. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.test.js +137 -0
  52. package/node_modules/playwright/lib/mcp/browser/tools/sitemap.js +226 -0
  53. package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +2 -2
  54. package/node_modules/playwright/lib/mcp/browser/tools/widgetContract.js +168 -0
  55. package/node_modules/playwright/lib/mcp/browser/tools.js +6 -0
  56. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +52 -12
  57. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +64 -13
  58. package/node_modules/playwright/package.json +1 -1
  59. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.3.tgz +0 -0
  60. package/package.json +2 -2
@@ -0,0 +1,396 @@
1
+ "use strict";
2
+ var import_blueprint = require("./blueprint");
3
+ var import_serialization = require("./serialization");
4
+ const cases = [];
5
+ function test(name, run) {
6
+ cases.push({ name, run });
7
+ }
8
+ function assertEqual(actual, expected, msg) {
9
+ const a = JSON.stringify(actual);
10
+ const e = JSON.stringify(expected);
11
+ if (a !== e)
12
+ throw new Error(`${msg ?? "assertEqual"} \u2014 expected ${e}, got ${a}`);
13
+ }
14
+ function assertThrows(fn, substring) {
15
+ let threw = false;
16
+ let message = "";
17
+ try {
18
+ fn();
19
+ } catch (e) {
20
+ threw = true;
21
+ message = e.message;
22
+ }
23
+ if (!threw)
24
+ throw new Error("expected function to throw");
25
+ if (substring && !message.includes(substring))
26
+ throw new Error(`thrown message "${message}" does not include "${substring}"`);
27
+ }
28
+ test("deriveLogicalName: button with simple name", () => {
29
+ assertEqual((0, import_blueprint.deriveLogicalName)("button", "Add Product"), "add_product_btn");
30
+ });
31
+ test("deriveLogicalName: link with preposition", () => {
32
+ assertEqual((0, import_blueprint.deriveLogicalName)("link", "Navigate to Orders page"), "navigate_to_orders_page_link");
33
+ });
34
+ test("deriveLogicalName: textbox maps to _input suffix", () => {
35
+ assertEqual((0, import_blueprint.deriveLogicalName)("textbox", "Search"), "search_input");
36
+ });
37
+ test("deriveLogicalName: strips special chars", () => {
38
+ assertEqual((0, import_blueprint.deriveLogicalName)("button", "Add & Remove!!!"), "add_remove_btn");
39
+ });
40
+ test("deriveLogicalName: trailing role-suffix token stripped", () => {
41
+ assertEqual((0, import_blueprint.deriveLogicalName)("combobox", "Select Yes No"), "yes_no_select");
42
+ });
43
+ test("deriveLogicalName: leading role-suffix token stripped", () => {
44
+ assertEqual((0, import_blueprint.deriveLogicalName)("combobox", "Select Country"), "country_select");
45
+ });
46
+ test("deriveLogicalName: mid-position role-suffix preserved", () => {
47
+ assertEqual((0, import_blueprint.deriveLogicalName)("combobox", "Next: Select Country"), "next_select_country_select");
48
+ });
49
+ test("deriveLogicalName: base-equals-suffix returns empty string", () => {
50
+ assertEqual((0, import_blueprint.deriveLogicalName)("combobox", "Select"), "");
51
+ });
52
+ test("deriveLogicalName: empty accessibleName returns empty string", () => {
53
+ assertEqual((0, import_blueprint.deriveLogicalName)("combobox", ""), "");
54
+ });
55
+ test("deriveLogicalName: button + accessibleName unchanged by dedup", () => {
56
+ assertEqual((0, import_blueprint.deriveLogicalName)("button", "Save Changes"), "save_changes_btn");
57
+ });
58
+ test("deriveLogicalName: CJK accessibleName preserved", () => {
59
+ assertEqual((0, import_blueprint.deriveLogicalName)("link", "\u8CC7\u7523\u7BA1\u7406"), "\u8CC7\u7523\u7BA1\u7406_link");
60
+ assertEqual((0, import_blueprint.deriveLogicalName)("button", "\u4E00\u62EC\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9"), "\u4E00\u62EC\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9_btn");
61
+ });
62
+ test("deriveLogicalName: Cyrillic lowercased and preserved", () => {
63
+ assertEqual((0, import_blueprint.deriveLogicalName)("button", "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C"), "\u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C_btn");
64
+ });
65
+ test("deriveLogicalName: mixed CJK + ASCII digits", () => {
66
+ assertEqual((0, import_blueprint.deriveLogicalName)("button", "\u901A\u77E5 (1063)"), "\u901A\u77E5_1063_btn");
67
+ });
68
+ test("deriveLogicalName: accented Latin preserved", () => {
69
+ assertEqual((0, import_blueprint.deriveLogicalName)("link", "r\xE9sum\xE9 caf\xE9"), "r\xE9sum\xE9_caf\xE9_link");
70
+ });
71
+ test("applyPositionHintToNamelessElements: CJK-named elements no longer fall back to role_N", () => {
72
+ const raws = [
73
+ { role: "link", accessibleName: "\u8CC7\u7523\u60C5\u5831", sectionName: "sidebar" },
74
+ { role: "link", accessibleName: "\u4E00\u62EC\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9", sectionName: "sidebar" }
75
+ ];
76
+ const names = (0, import_blueprint.applyPositionHintToNamelessElements)(raws);
77
+ assertEqual(names, ["\u8CC7\u7523\u60C5\u5831_link", "\u4E00\u62EC\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9_link"]);
78
+ });
79
+ test("applyPositionHintToNamelessElements: single nameless same-role element gets role_1", () => {
80
+ const raws = [
81
+ { role: "combobox", accessibleName: "", sectionName: "main" }
82
+ ];
83
+ const names = (0, import_blueprint.applyPositionHintToNamelessElements)(raws);
84
+ assertEqual(names, ["combobox_1"]);
85
+ });
86
+ test("applyPositionHintToNamelessElements: three nameless same-role same-section \u2192 combobox_1/2/3", () => {
87
+ const raws = [
88
+ { role: "combobox", accessibleName: "", sectionName: "main" },
89
+ { role: "combobox", accessibleName: "", sectionName: "main" },
90
+ { role: "combobox", accessibleName: "", sectionName: "main" }
91
+ ];
92
+ const names = (0, import_blueprint.applyPositionHintToNamelessElements)(raws);
93
+ assertEqual(names, ["combobox_1", "combobox_2", "combobox_3"]);
94
+ });
95
+ test("applyPositionHintToNamelessElements: same role across different sections numbered independently", () => {
96
+ const raws = [
97
+ { role: "combobox", accessibleName: "", sectionName: "header" },
98
+ { role: "combobox", accessibleName: "", sectionName: "main" }
99
+ ];
100
+ const names = (0, import_blueprint.applyPositionHintToNamelessElements)(raws);
101
+ assertEqual(names, ["combobox_1", "combobox_1"]);
102
+ });
103
+ test("applyPositionHintToNamelessElements: mixed named + nameless; nameless numbered ignoring named", () => {
104
+ const raws = [
105
+ { role: "combobox", accessibleName: "Country", sectionName: "main" },
106
+ { role: "combobox", accessibleName: "", sectionName: "main" },
107
+ { role: "combobox", accessibleName: "", sectionName: "main" }
108
+ ];
109
+ const names = (0, import_blueprint.applyPositionHintToNamelessElements)(raws);
110
+ assertEqual(names[0], "country_select");
111
+ assertEqual(names[1], "combobox_1");
112
+ assertEqual(names[2], "combobox_2");
113
+ });
114
+ test("resolveTemplate: single placeholder", () => {
115
+ assertEqual(
116
+ (0, import_blueprint.resolveTemplate)("View details for order {orderId}", { orderId: "12" }),
117
+ "View details for order 12"
118
+ );
119
+ });
120
+ test("resolveTemplate: multiple placeholders", () => {
121
+ assertEqual(
122
+ (0, import_blueprint.resolveTemplate)("{greeting} {name}", { greeting: "Hello", name: "World" }),
123
+ "Hello World"
124
+ );
125
+ });
126
+ test("resolveTemplate: throws on missing parameter", () => {
127
+ assertThrows(() => (0, import_blueprint.resolveTemplate)("{foo} {bar}", { foo: "1" }), "no value for {bar}");
128
+ });
129
+ test("resolveXpath / resolveAccessibleName on a RepeatingBlueprintElement", () => {
130
+ const rep = {
131
+ logicalName: "view_details_for_order_btn",
132
+ role: "button",
133
+ accessibleNameTemplate: "View details for order {orderId}",
134
+ parameters: ["orderId", "row"],
135
+ xpathPattern: '//*[@id="root"]/div/div[3]/div/div/div[{row}]/button',
136
+ indexParameter: "row",
137
+ items: [
138
+ { parameters: { orderId: "12", row: "1" } },
139
+ { parameters: { orderId: "11", row: "2" } }
140
+ ],
141
+ mutability: "immutable",
142
+ widgetType: "native",
143
+ framePath: [],
144
+ shadowRoot: false,
145
+ stableId: null,
146
+ testId: null,
147
+ fingerprint: null
148
+ };
149
+ assertEqual((0, import_blueprint.resolveAccessibleName)(rep, rep.items[0].parameters), "View details for order 12");
150
+ assertEqual(
151
+ (0, import_blueprint.resolveXpath)(rep, rep.items[1].parameters),
152
+ '//*[@id="root"]/div/div[3]/div/div/div[2]/button'
153
+ );
154
+ });
155
+ function makeValidRepeating() {
156
+ return {
157
+ logicalName: "view_details_for_order_btn",
158
+ role: "button",
159
+ accessibleNameTemplate: "View details for order {orderId}",
160
+ parameters: ["orderId", "row"],
161
+ xpathPattern: '//*[@id="root"]/div[{row}]/button',
162
+ indexParameter: "row",
163
+ items: [
164
+ { parameters: { orderId: "12", row: "1" } },
165
+ { parameters: { orderId: "11", row: "2" } }
166
+ ],
167
+ mutability: "immutable",
168
+ widgetType: "native",
169
+ framePath: [],
170
+ shadowRoot: false,
171
+ stableId: null,
172
+ testId: null,
173
+ fingerprint: null
174
+ };
175
+ }
176
+ test("validateRepeatingElement: happy path", () => {
177
+ const rep = makeValidRepeating();
178
+ (0, import_blueprint.validateRepeatingElement)(rep);
179
+ });
180
+ test("validateRepeatingElement: template token typo is rejected", () => {
181
+ const rep = makeValidRepeating();
182
+ rep.accessibleNameTemplate = "View details for order {orderID}";
183
+ assertThrows(() => (0, import_blueprint.validateRepeatingElement)(rep), "template/parameters mismatch");
184
+ });
185
+ test("validateRepeatingElement: indexParameter not in parameters is rejected", () => {
186
+ const rep = makeValidRepeating();
187
+ rep.indexParameter = "notAParameter";
188
+ assertThrows(() => (0, import_blueprint.validateRepeatingElement)(rep), "indexParameter");
189
+ });
190
+ test("validateRepeatingElement: missing token in xpath is rejected", () => {
191
+ const rep = makeValidRepeating();
192
+ rep.xpathPattern = '//*[@id="root"]/div/button';
193
+ assertThrows(() => (0, import_blueprint.validateRepeatingElement)(rep), "template/parameters mismatch");
194
+ });
195
+ test("validateRepeatingElement: item with extra key is rejected", () => {
196
+ const rep = makeValidRepeating();
197
+ rep.items[0].parameters["extra"] = "x";
198
+ assertThrows(() => (0, import_blueprint.validateRepeatingElement)(rep), "parameter-key mismatch");
199
+ });
200
+ test("validateRepeatingElement: item missing a key is rejected", () => {
201
+ const rep = makeValidRepeating();
202
+ delete rep.items[0].parameters["orderId"];
203
+ assertThrows(() => (0, import_blueprint.validateRepeatingElement)(rep), "parameter-key mismatch");
204
+ });
205
+ function makeBlueprint() {
206
+ return {
207
+ schemaVersion: import_serialization.SCHEMA_VERSION,
208
+ url: "http://localhost:5173/orders",
209
+ capturedAt: "2026-04-29T14:22:00.000Z",
210
+ pageHash: "12:123",
211
+ sections: [
212
+ {
213
+ name: "main_navigation",
214
+ landmark: "navigation",
215
+ elements: [
216
+ {
217
+ logicalName: "navigate_to_orders_page_link",
218
+ role: "link",
219
+ accessibleName: "Navigate to Orders page",
220
+ xpath: '//*[@id="root"]/div/header/nav/a[2]',
221
+ mutability: "immutable",
222
+ widgetType: "native",
223
+ framePath: [],
224
+ shadowRoot: false,
225
+ stableId: null,
226
+ testId: null,
227
+ fingerprint: null
228
+ }
229
+ ],
230
+ repeatingElements: []
231
+ },
232
+ {
233
+ name: "page",
234
+ landmark: "region",
235
+ elements: [
236
+ {
237
+ logicalName: "add_order_btn",
238
+ role: "button",
239
+ accessibleName: "Add Order",
240
+ xpath: '//*[@id="root"]/div/div[2]/button',
241
+ mutability: "mutable",
242
+ widgetType: "native",
243
+ framePath: [],
244
+ shadowRoot: false,
245
+ stableId: null,
246
+ testId: null,
247
+ fingerprint: null
248
+ }
249
+ ],
250
+ repeatingElements: [makeValidRepeating()]
251
+ }
252
+ ]
253
+ };
254
+ }
255
+ test("buildMap: flat logicalName \u2192 xpath", () => {
256
+ const bp = makeBlueprint();
257
+ const map = (0, import_blueprint.buildMap)(bp);
258
+ assertEqual(Object.keys(map).sort(), [
259
+ "add_order_btn",
260
+ "navigate_to_orders_page_link",
261
+ "view_details_for_order_btn"
262
+ ]);
263
+ assertEqual(map.add_order_btn, '//*[@id="root"]/div/div[2]/button');
264
+ assertEqual(map.view_details_for_order_btn, '//*[@id="root"]/div[{row}]/button');
265
+ });
266
+ test("buildOutline: includes sections, singular, and repeating", () => {
267
+ const bp = makeBlueprint();
268
+ const outline = (0, import_blueprint.buildOutline)(bp);
269
+ if (!outline.includes("main_navigation (role: navigation)"))
270
+ throw new Error(`outline missing main_navigation section: ${outline}`);
271
+ if (!outline.includes("navigate_to_orders_page_link [immutable]"))
272
+ throw new Error(`outline missing singular element: ${outline}`);
273
+ if (!outline.includes("add_order_btn [mutable]"))
274
+ throw new Error(`outline missing mutable element: ${outline}`);
275
+ if (!outline.includes("view_details_for_order_btn [repeating \xD7 2]"))
276
+ throw new Error(`outline missing repeating element: ${outline}`);
277
+ });
278
+ test("normalizeUrl: strips trailing slash", () => {
279
+ assertEqual((0, import_serialization.normalizeUrl)("http://example.com/path/"), "http://example.com/path");
280
+ });
281
+ test("normalizeUrl: keeps root slash", () => {
282
+ assertEqual((0, import_serialization.normalizeUrl)("http://example.com/"), "http://example.com/");
283
+ });
284
+ test("normalizeUrl: strips fragment", () => {
285
+ assertEqual((0, import_serialization.normalizeUrl)("http://example.com/path#frag"), "http://example.com/path");
286
+ });
287
+ test("normalizeUrl: strips default port", () => {
288
+ assertEqual((0, import_serialization.normalizeUrl)("http://example.com:80/path"), "http://example.com/path");
289
+ assertEqual((0, import_serialization.normalizeUrl)("https://example.com:443/path"), "https://example.com/path");
290
+ });
291
+ test("normalizeUrl: sorts query params", () => {
292
+ assertEqual((0, import_serialization.normalizeUrl)("http://example.com/?b=2&a=1"), "http://example.com/?a=1&b=2");
293
+ });
294
+ test("normalizeUrl: uppercases percent encoding", () => {
295
+ assertEqual((0, import_serialization.normalizeUrl)("http://example.com/path%2fencoded"), "http://example.com/path%2Fencoded");
296
+ });
297
+ test("normalizeUrl: all variants collapse to same key", () => {
298
+ const a = (0, import_serialization.normalizeUrl)("http://example.com:80/path/?b=2&a=1#frag");
299
+ const b = (0, import_serialization.normalizeUrl)("http://example.com/path?a=1&b=2");
300
+ assertEqual(a, b);
301
+ });
302
+ test("validateTimestamp: accepts canonical ISO-8601 UTC", () => {
303
+ const ts = (0, import_serialization.validateTimestamp)("2026-04-29T14:22:00.000Z");
304
+ assertEqual(ts, "2026-04-29T14:22:00.000Z");
305
+ });
306
+ test("validateTimestamp: rejects non-ISO-8601", () => {
307
+ assertThrows(() => (0, import_serialization.validateTimestamp)("2026-04-29 14:22:00"), "canonical ISO-8601");
308
+ });
309
+ test("validateTimestamp: rejects garbage", () => {
310
+ assertThrows(() => (0, import_serialization.validateTimestamp)("not a date"), "Invalid ISO-8601");
311
+ });
312
+ test("nowTimestamp: round-trips through validateTimestamp", () => {
313
+ const ts = (0, import_serialization.nowTimestamp)();
314
+ (0, import_serialization.validateTimestamp)(ts);
315
+ });
316
+ test("assertSchemaVersion: accepts current version", () => {
317
+ (0, import_serialization.assertSchemaVersion)(import_serialization.SCHEMA_VERSION);
318
+ });
319
+ test("assertSchemaVersion: rejects unknown version", () => {
320
+ assertThrows(() => (0, import_serialization.assertSchemaVersion)(import_serialization.SCHEMA_VERSION + 1), "Unsupported schemaVersion");
321
+ });
322
+ test("validateElement: native widget with null fingerprint is valid", () => {
323
+ (0, import_blueprint.validateElement)({ logicalName: "save_btn", widgetType: "native", fingerprint: null });
324
+ });
325
+ test("validateElement: widgetType=custom requires non-null fingerprint", () => {
326
+ assertThrows(
327
+ () => (0, import_blueprint.validateElement)({ logicalName: "date_picker", widgetType: "custom", fingerprint: null }),
328
+ "fingerprint required"
329
+ );
330
+ });
331
+ test("validateElement: widgetType=custom with fingerprint passes", () => {
332
+ (0, import_blueprint.validateElement)({ logicalName: "date_picker", widgetType: "custom", fingerprint: "abcdef0123456789" });
333
+ });
334
+ test("BlueprintInvariantError: non-absolute xpath throws with diagnostic", () => {
335
+ const err = new import_blueprint.BlueprintInvariantError(
336
+ `BlueprintElement 'test_btn' has non-absolute xpath: '/foo/bar'. All xpaths must start with '//' (document-rooted descendant-or-self).`
337
+ );
338
+ assertEqual(err.name, "BlueprintInvariantError");
339
+ if (!err.message.includes("non-absolute xpath"))
340
+ throw new Error("error message missing key phrase");
341
+ if (!err.message.includes("test_btn"))
342
+ throw new Error("error message missing element name");
343
+ });
344
+ test("xpathLiteralNode: simple ASCII value uses double-quote literal", () => {
345
+ assertEqual((0, import_blueprint.xpathLiteralNode)("foo-bar"), '"foo-bar"');
346
+ });
347
+ test("xpathLiteralNode: value with double-quote uses single-quote literal", () => {
348
+ assertEqual((0, import_blueprint.xpathLiteralNode)('has "quotes" inside'), `'has "quotes" inside'`);
349
+ });
350
+ test("xpathLiteralNode: value with single-quote uses double-quote literal", () => {
351
+ assertEqual((0, import_blueprint.xpathLiteralNode)("it's mine"), `"it's mine"`);
352
+ });
353
+ test("xpathLiteralNode: value with both quotes uses concat()", () => {
354
+ const result = (0, import_blueprint.xpathLiteralNode)(`it"s 'wild'`);
355
+ if (!result.startsWith("concat("))
356
+ throw new Error(`expected concat(...) form, got: ${result}`);
357
+ if (!result.includes(`'"'`))
358
+ throw new Error(`expected literal-double-quote segment, got: ${result}`);
359
+ });
360
+ test("xpathLiteralNode: empty string", () => {
361
+ assertEqual((0, import_blueprint.xpathLiteralNode)(""), '""');
362
+ });
363
+ test("xpathLiteralNode: backslash passes through (XPath has no backslash escape)", () => {
364
+ assertEqual((0, import_blueprint.xpathLiteralNode)("path\\with\\slashes"), '"path\\with\\slashes"');
365
+ });
366
+ test("xpathLiteralNode: template placeholders (curly-brace tokens) preserved", () => {
367
+ assertEqual((0, import_blueprint.xpathLiteralNode)("edit-{itemSlug}-btn"), '"edit-{itemSlug}-btn"');
368
+ });
369
+ test("xpathLiteralNode: value containing only double-quotes takes single-quote branch", () => {
370
+ assertEqual((0, import_blueprint.xpathLiteralNode)('"foo"'), `'"foo"'`);
371
+ });
372
+ test("xpathLiteralNode: value with both quotes \u2014 empty segments filtered", () => {
373
+ const result = (0, import_blueprint.xpathLiteralNode)(`"'`);
374
+ if (!result.startsWith("concat("))
375
+ throw new Error(`expected concat() form, got: ${result}`);
376
+ if (result.includes(`''`))
377
+ throw new Error(`concat should not contain empty '' literals, got: ${result}`);
378
+ });
379
+ let passed = 0;
380
+ let failed = 0;
381
+ const failures = [];
382
+ for (const c of cases) {
383
+ try {
384
+ c.run();
385
+ passed++;
386
+ } catch (e) {
387
+ failed++;
388
+ failures.push({ name: c.name, error: e.message });
389
+ }
390
+ }
391
+ console.log(`${passed}/${cases.length} passed${failed ? `, ${failed} failed` : ""}`);
392
+ for (const f of failures)
393
+ console.error(` \u2717 ${f.name}: ${f.error}`);
394
+ if (failed > 0) {
395
+ process.exit(1);
396
+ }
@@ -0,0 +1,57 @@
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 blueprintCache_exports = {};
20
+ __export(blueprintCache_exports, {
21
+ BlueprintCache: () => BlueprintCache
22
+ });
23
+ module.exports = __toCommonJS(blueprintCache_exports);
24
+ class BlueprintCache {
25
+ constructor(max) {
26
+ this.map = /* @__PURE__ */ new Map();
27
+ if (max < 1)
28
+ throw new Error("BlueprintCache: max must be >= 1");
29
+ this.max = max;
30
+ }
31
+ get(url) {
32
+ const found = this.map.get(url);
33
+ if (!found)
34
+ return void 0;
35
+ this.map.delete(url);
36
+ this.map.set(url, found);
37
+ return found;
38
+ }
39
+ put(url, bp) {
40
+ if (this.map.has(url))
41
+ this.map.delete(url);
42
+ this.map.set(url, bp);
43
+ while (this.map.size > this.max) {
44
+ const oldest = this.map.keys().next().value;
45
+ if (oldest === void 0)
46
+ break;
47
+ this.map.delete(oldest);
48
+ }
49
+ }
50
+ clear() {
51
+ this.map.clear();
52
+ }
53
+ }
54
+ // Annotate the CommonJS export names for ESM import in node:
55
+ 0 && (module.exports = {
56
+ BlueprintCache
57
+ });
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var import_vitest = require("vitest");
3
+ var import_blueprintCache = require("./blueprintCache");
4
+ function bp(url, pageHash) {
5
+ return {
6
+ schemaVersion: 1,
7
+ url,
8
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
9
+ pageHash,
10
+ sections: []
11
+ };
12
+ }
13
+ (0, import_vitest.describe)("BlueprintCache", () => {
14
+ (0, import_vitest.it)("returns undefined for unknown url", () => {
15
+ const c = new import_blueprintCache.BlueprintCache(5);
16
+ (0, import_vitest.expect)(c.get("http://a/")).toBeUndefined();
17
+ });
18
+ (0, import_vitest.it)("round-trips a single entry", () => {
19
+ const c = new import_blueprintCache.BlueprintCache(5);
20
+ const b = bp("http://a/", "h1");
21
+ c.put("http://a/", b);
22
+ (0, import_vitest.expect)(c.get("http://a/")).toBe(b);
23
+ });
24
+ (0, import_vitest.it)("overwrites on repeated put for the same url", () => {
25
+ const c = new import_blueprintCache.BlueprintCache(5);
26
+ const b1 = bp("http://a/", "h1");
27
+ const b2 = bp("http://a/", "h2");
28
+ c.put("http://a/", b1);
29
+ c.put("http://a/", b2);
30
+ (0, import_vitest.expect)(c.get("http://a/")).toBe(b2);
31
+ });
32
+ (0, import_vitest.it)("evicts least-recently-used entry once max is exceeded", () => {
33
+ const c = new import_blueprintCache.BlueprintCache(2);
34
+ c.put("http://a/", bp("http://a/", "ha"));
35
+ c.put("http://b/", bp("http://b/", "hb"));
36
+ c.put("http://c/", bp("http://c/", "hc"));
37
+ (0, import_vitest.expect)(c.get("http://a/")).toBeUndefined();
38
+ (0, import_vitest.expect)(c.get("http://b/")).toBeDefined();
39
+ (0, import_vitest.expect)(c.get("http://c/")).toBeDefined();
40
+ });
41
+ (0, import_vitest.it)("counts get as a use for LRU ordering", () => {
42
+ const c = new import_blueprintCache.BlueprintCache(2);
43
+ c.put("http://a/", bp("http://a/", "ha"));
44
+ c.put("http://b/", bp("http://b/", "hb"));
45
+ c.get("http://a/");
46
+ c.put("http://c/", bp("http://c/", "hc"));
47
+ (0, import_vitest.expect)(c.get("http://a/")).toBeDefined();
48
+ (0, import_vitest.expect)(c.get("http://b/")).toBeUndefined();
49
+ (0, import_vitest.expect)(c.get("http://c/")).toBeDefined();
50
+ });
51
+ (0, import_vitest.it)("clear() empties the cache", () => {
52
+ const c = new import_blueprintCache.BlueprintCache(2);
53
+ c.put("http://a/", bp("http://a/", "ha"));
54
+ c.clear();
55
+ (0, import_vitest.expect)(c.get("http://a/")).toBeUndefined();
56
+ });
57
+ });