gsd-pi 2.7.1 → 2.8.0
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/README.md +12 -5
- package/dist/loader.js +0 -0
- package/dist/modes/interactive/theme/dark.json +85 -0
- package/dist/modes/interactive/theme/light.json +84 -0
- package/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/dist/modes/interactive/theme/theme.js +949 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
- package/node_modules/cliui/CHANGELOG.md +121 -0
- package/node_modules/color-convert/CHANGELOG.md +54 -0
- package/node_modules/esprima/ChangeLog +235 -0
- package/node_modules/mz/HISTORY.md +66 -0
- package/node_modules/proper-lockfile/CHANGELOG.md +108 -0
- package/node_modules/source-map/CHANGELOG.md +301 -0
- package/node_modules/thenify/History.md +11 -0
- package/node_modules/thenify-all/History.md +11 -0
- package/node_modules/y18n/CHANGELOG.md +100 -0
- package/node_modules/yargs/CHANGELOG.md +88 -0
- package/node_modules/yargs-parser/CHANGELOG.md +263 -0
- package/package.json +5 -2
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
- package/src/resources/extensions/browser-tools/capture.ts +165 -0
- package/src/resources/extensions/browser-tools/evaluate-helpers.ts +184 -0
- package/src/resources/extensions/browser-tools/index.ts +47 -4985
- package/src/resources/extensions/browser-tools/lifecycle.ts +265 -0
- package/src/resources/extensions/browser-tools/package.json +5 -1
- package/src/resources/extensions/browser-tools/refs.ts +264 -0
- package/src/resources/extensions/browser-tools/settle.ts +197 -0
- package/src/resources/extensions/browser-tools/state.ts +408 -0
- package/src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +652 -0
- package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +614 -0
- package/src/resources/extensions/browser-tools/tools/assertions.ts +342 -0
- package/src/resources/extensions/browser-tools/tools/forms.ts +801 -0
- package/src/resources/extensions/browser-tools/tools/inspection.ts +492 -0
- package/src/resources/extensions/browser-tools/tools/intent.ts +614 -0
- package/src/resources/extensions/browser-tools/tools/interaction.ts +865 -0
- package/src/resources/extensions/browser-tools/tools/navigation.ts +232 -0
- package/src/resources/extensions/browser-tools/tools/pages.ts +303 -0
- package/src/resources/extensions/browser-tools/tools/refs.ts +541 -0
- package/src/resources/extensions/browser-tools/tools/screenshot.ts +83 -0
- package/src/resources/extensions/browser-tools/tools/session.ts +400 -0
- package/src/resources/extensions/browser-tools/tools/wait.ts +247 -0
- package/src/resources/extensions/browser-tools/utils.ts +660 -0
- package/src/resources/extensions/gsd/git-service.ts +3 -0
- package/src/resources/extensions/shared/interview-ui.ts +1 -1
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browser-tools — Node-side unit tests
|
|
3
|
+
*
|
|
4
|
+
* Uses jiti for TypeScript imports (the resolve-ts ESM hook breaks on core.js),
|
|
5
|
+
* node:test for the runner, and node:assert/strict for assertions.
|
|
6
|
+
*
|
|
7
|
+
* Tests pure functions from utils.ts, state.ts accessors, evaluate-helpers.ts
|
|
8
|
+
* syntax, and constrainScreenshot from capture.ts.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { describe, it, beforeEach } = require("node:test");
|
|
12
|
+
const assert = require("node:assert/strict");
|
|
13
|
+
const jiti = require("jiti")(__filename, { interopDefault: true, debug: false });
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Module imports via jiti
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
parseRef,
|
|
21
|
+
formatVersionedRef,
|
|
22
|
+
staleRefGuidance,
|
|
23
|
+
formatCompactStateSummary,
|
|
24
|
+
verificationFromChecks,
|
|
25
|
+
verificationLine,
|
|
26
|
+
sanitizeArtifactName,
|
|
27
|
+
isCriticalResourceType,
|
|
28
|
+
getUrlHash,
|
|
29
|
+
firstErrorLine,
|
|
30
|
+
formatArtifactTimestamp,
|
|
31
|
+
} = jiti("../utils.ts");
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
getBrowser,
|
|
35
|
+
setBrowser,
|
|
36
|
+
getContext,
|
|
37
|
+
setContext,
|
|
38
|
+
getActiveFrame,
|
|
39
|
+
setActiveFrame,
|
|
40
|
+
getSessionStartedAt,
|
|
41
|
+
setSessionStartedAt,
|
|
42
|
+
getSessionArtifactDir,
|
|
43
|
+
setSessionArtifactDir,
|
|
44
|
+
getCurrentRefMap,
|
|
45
|
+
setCurrentRefMap,
|
|
46
|
+
getRefVersion,
|
|
47
|
+
setRefVersion,
|
|
48
|
+
getRefMetadata,
|
|
49
|
+
setRefMetadata,
|
|
50
|
+
getLastActionBeforeState,
|
|
51
|
+
setLastActionBeforeState,
|
|
52
|
+
getLastActionAfterState,
|
|
53
|
+
setLastActionAfterState,
|
|
54
|
+
resetAllState,
|
|
55
|
+
} = jiti("../state.ts");
|
|
56
|
+
|
|
57
|
+
const { EVALUATE_HELPERS_SOURCE } = jiti("../evaluate-helpers.ts");
|
|
58
|
+
|
|
59
|
+
const { constrainScreenshot } = jiti("../capture.ts");
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// utils.ts — parseRef
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
describe("parseRef", () => {
|
|
66
|
+
it("parses a valid versioned ref", () => {
|
|
67
|
+
const result = parseRef("@v3:e12");
|
|
68
|
+
assert.deepStrictEqual(result, {
|
|
69
|
+
key: "e12",
|
|
70
|
+
version: 3,
|
|
71
|
+
display: "@v3:e12",
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("parses a ref without leading @", () => {
|
|
76
|
+
const result = parseRef("v1:e5");
|
|
77
|
+
assert.deepStrictEqual(result, {
|
|
78
|
+
key: "e5",
|
|
79
|
+
version: 1,
|
|
80
|
+
display: "@v1:e5",
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("handles legacy (unversioned) format", () => {
|
|
85
|
+
const result = parseRef("@e7");
|
|
86
|
+
assert.deepStrictEqual(result, {
|
|
87
|
+
key: "e7",
|
|
88
|
+
version: null,
|
|
89
|
+
display: "@e7",
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("trims whitespace", () => {
|
|
94
|
+
const result = parseRef(" @v2:e1 ");
|
|
95
|
+
assert.equal(result.key, "e1");
|
|
96
|
+
assert.equal(result.version, 2);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("is case-insensitive", () => {
|
|
100
|
+
const result = parseRef("@V10:E3");
|
|
101
|
+
assert.equal(result.key, "e3");
|
|
102
|
+
assert.equal(result.version, 10);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// utils.ts — formatVersionedRef
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
describe("formatVersionedRef", () => {
|
|
111
|
+
it("formats a versioned ref string", () => {
|
|
112
|
+
assert.equal(formatVersionedRef(5, "e3"), "@v5:e3");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("formats version 0", () => {
|
|
116
|
+
assert.equal(formatVersionedRef(0, "e1"), "@v0:e1");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// utils.ts — staleRefGuidance
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
describe("staleRefGuidance", () => {
|
|
125
|
+
it("includes the ref display and reason", () => {
|
|
126
|
+
const result = staleRefGuidance("@v2:e5", "element removed");
|
|
127
|
+
assert.ok(result.includes("@v2:e5"));
|
|
128
|
+
assert.ok(result.includes("element removed"));
|
|
129
|
+
assert.ok(result.includes("browser_snapshot_refs"));
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// utils.ts — formatCompactStateSummary
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
describe("formatCompactStateSummary", () => {
|
|
138
|
+
it("formats a compact page state into a readable summary", () => {
|
|
139
|
+
/** @type {import('../state.ts').CompactPageState} */
|
|
140
|
+
const mockState = {
|
|
141
|
+
url: "http://localhost:3000/dashboard",
|
|
142
|
+
title: "Dashboard",
|
|
143
|
+
focus: "input#search",
|
|
144
|
+
headings: ["Welcome", "Recent Activity"],
|
|
145
|
+
bodyText: "",
|
|
146
|
+
counts: {
|
|
147
|
+
landmarks: 3,
|
|
148
|
+
buttons: 5,
|
|
149
|
+
links: 12,
|
|
150
|
+
inputs: 2,
|
|
151
|
+
},
|
|
152
|
+
dialog: { count: 0, title: "" },
|
|
153
|
+
selectorStates: {},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const summary = formatCompactStateSummary(mockState);
|
|
157
|
+
assert.ok(summary.includes("Title: Dashboard"));
|
|
158
|
+
assert.ok(summary.includes("URL: http://localhost:3000/dashboard"));
|
|
159
|
+
assert.ok(summary.includes("3 landmarks"));
|
|
160
|
+
assert.ok(summary.includes("5 buttons"));
|
|
161
|
+
assert.ok(summary.includes("12 links"));
|
|
162
|
+
assert.ok(summary.includes("2 inputs"));
|
|
163
|
+
assert.ok(summary.includes("Focused: input#search"));
|
|
164
|
+
assert.ok(summary.includes('H1 "Welcome"'));
|
|
165
|
+
assert.ok(summary.includes('H2 "Recent Activity"'));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("omits focus line when empty", () => {
|
|
169
|
+
const mockState = {
|
|
170
|
+
url: "http://example.com",
|
|
171
|
+
title: "Test",
|
|
172
|
+
focus: "",
|
|
173
|
+
headings: [],
|
|
174
|
+
bodyText: "",
|
|
175
|
+
counts: { landmarks: 0, buttons: 0, links: 0, inputs: 0 },
|
|
176
|
+
dialog: { count: 0, title: "" },
|
|
177
|
+
selectorStates: {},
|
|
178
|
+
};
|
|
179
|
+
const summary = formatCompactStateSummary(mockState);
|
|
180
|
+
assert.ok(!summary.includes("Focused:"));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("includes dialog title when present", () => {
|
|
184
|
+
const mockState = {
|
|
185
|
+
url: "http://example.com",
|
|
186
|
+
title: "Test",
|
|
187
|
+
focus: "",
|
|
188
|
+
headings: [],
|
|
189
|
+
bodyText: "",
|
|
190
|
+
counts: { landmarks: 0, buttons: 0, links: 0, inputs: 0 },
|
|
191
|
+
dialog: { count: 1, title: "Confirm Delete" },
|
|
192
|
+
selectorStates: {},
|
|
193
|
+
};
|
|
194
|
+
const summary = formatCompactStateSummary(mockState);
|
|
195
|
+
assert.ok(summary.includes('Active dialog: "Confirm Delete"'));
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// utils.ts — verificationFromChecks
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
describe("verificationFromChecks", () => {
|
|
204
|
+
it("returns verified=true when at least one check passes", () => {
|
|
205
|
+
const checks = [
|
|
206
|
+
{ name: "url_changed", passed: true },
|
|
207
|
+
{ name: "title_changed", passed: false },
|
|
208
|
+
];
|
|
209
|
+
const result = verificationFromChecks(checks);
|
|
210
|
+
assert.equal(result.verified, true);
|
|
211
|
+
assert.ok(result.verificationSummary.includes("PASS"));
|
|
212
|
+
assert.ok(result.verificationSummary.includes("url_changed"));
|
|
213
|
+
assert.equal(result.retryHint, undefined);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("returns verified=false when no checks pass", () => {
|
|
217
|
+
const checks = [
|
|
218
|
+
{ name: "url_changed", passed: false },
|
|
219
|
+
{ name: "title_changed", passed: false },
|
|
220
|
+
];
|
|
221
|
+
const result = verificationFromChecks(checks, "try clicking again");
|
|
222
|
+
assert.equal(result.verified, false);
|
|
223
|
+
assert.ok(result.verificationSummary.includes("SOFT-FAIL"));
|
|
224
|
+
assert.equal(result.retryHint, "try clicking again");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("lists multiple passing checks", () => {
|
|
228
|
+
const checks = [
|
|
229
|
+
{ name: "a", passed: true },
|
|
230
|
+
{ name: "b", passed: true },
|
|
231
|
+
];
|
|
232
|
+
const result = verificationFromChecks(checks);
|
|
233
|
+
assert.ok(result.verificationSummary.includes("a"));
|
|
234
|
+
assert.ok(result.verificationSummary.includes("b"));
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// utils.ts — verificationLine
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
describe("verificationLine", () => {
|
|
243
|
+
it("formats a verification result into a single line", () => {
|
|
244
|
+
const result = {
|
|
245
|
+
verified: true,
|
|
246
|
+
checks: [],
|
|
247
|
+
verificationSummary: "PASS (url_changed)",
|
|
248
|
+
};
|
|
249
|
+
const line = verificationLine(result);
|
|
250
|
+
assert.equal(line, "Verification: PASS (url_changed)");
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// utils.ts — sanitizeArtifactName
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
describe("sanitizeArtifactName", () => {
|
|
259
|
+
it("passes through valid names", () => {
|
|
260
|
+
assert.equal(sanitizeArtifactName("my-trace", "default"), "my-trace");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("replaces special characters with hyphens", () => {
|
|
264
|
+
assert.equal(sanitizeArtifactName("hello world!@#", "default"), "hello-world");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("strips leading/trailing hyphens", () => {
|
|
268
|
+
assert.equal(sanitizeArtifactName(" --foo-- ", "default"), "foo");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("returns fallback for empty string", () => {
|
|
272
|
+
assert.equal(sanitizeArtifactName("", "fallback"), "fallback");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("returns fallback for whitespace-only string", () => {
|
|
276
|
+
assert.equal(sanitizeArtifactName(" ", "fallback"), "fallback");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("returns fallback for all-special-chars string", () => {
|
|
280
|
+
assert.equal(sanitizeArtifactName("@#$%", "default"), "default");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("preserves dots and underscores", () => {
|
|
284
|
+
assert.equal(sanitizeArtifactName("file_name.ext", "default"), "file_name.ext");
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// utils.ts — isCriticalResourceType
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
describe("isCriticalResourceType", () => {
|
|
293
|
+
it("returns true for document", () => {
|
|
294
|
+
assert.equal(isCriticalResourceType("document"), true);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("returns true for fetch", () => {
|
|
298
|
+
assert.equal(isCriticalResourceType("fetch"), true);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("returns true for xhr", () => {
|
|
302
|
+
assert.equal(isCriticalResourceType("xhr"), true);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("returns false for image", () => {
|
|
306
|
+
assert.equal(isCriticalResourceType("image"), false);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("returns false for font", () => {
|
|
310
|
+
assert.equal(isCriticalResourceType("font"), false);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("returns false for stylesheet", () => {
|
|
314
|
+
assert.equal(isCriticalResourceType("stylesheet"), false);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("returns false for script", () => {
|
|
318
|
+
assert.equal(isCriticalResourceType("script"), false);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
// utils.ts — getUrlHash
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
describe("getUrlHash", () => {
|
|
327
|
+
it("returns the hash from a URL", () => {
|
|
328
|
+
assert.equal(getUrlHash("http://example.com/page#section"), "#section");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("returns empty string when no hash", () => {
|
|
332
|
+
assert.equal(getUrlHash("http://example.com/page"), "");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("returns empty string for invalid URL", () => {
|
|
336
|
+
assert.equal(getUrlHash("not-a-url"), "");
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
// utils.ts — firstErrorLine
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
describe("firstErrorLine", () => {
|
|
345
|
+
it("extracts first line from an Error", () => {
|
|
346
|
+
const err = new Error("line1\nline2\nline3");
|
|
347
|
+
assert.equal(firstErrorLine(err), "line1");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("handles string errors", () => {
|
|
351
|
+
assert.equal(firstErrorLine("something broke"), "something broke");
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("handles null/undefined", () => {
|
|
355
|
+
assert.equal(firstErrorLine(null), "unknown error");
|
|
356
|
+
assert.equal(firstErrorLine(undefined), "unknown error");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("handles objects without message property", () => {
|
|
360
|
+
// {} has no .message, so falls to String({}) = "[object Object]"
|
|
361
|
+
assert.equal(firstErrorLine({}), "[object Object]");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("handles objects with empty message", () => {
|
|
365
|
+
assert.equal(firstErrorLine({ message: "" }), "unknown error");
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
// utils.ts — formatArtifactTimestamp
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
describe("formatArtifactTimestamp", () => {
|
|
374
|
+
it("formats a timestamp into an ISO-like string with dashes", () => {
|
|
375
|
+
// 2024-01-15T10:30:45.123Z
|
|
376
|
+
const ts = new Date("2024-01-15T10:30:45.123Z").getTime();
|
|
377
|
+
const result = formatArtifactTimestamp(ts);
|
|
378
|
+
// Should replace colons and dots with dashes
|
|
379
|
+
assert.ok(!result.includes(":"));
|
|
380
|
+
assert.ok(!result.includes("."));
|
|
381
|
+
assert.ok(result.includes("2024-01-15"));
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
// evaluate-helpers.ts — EVALUATE_HELPERS_SOURCE
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
describe("EVALUATE_HELPERS_SOURCE", () => {
|
|
390
|
+
it("is a parseable string (valid JavaScript)", () => {
|
|
391
|
+
assert.doesNotThrow(() => {
|
|
392
|
+
new Function(EVALUATE_HELPERS_SOURCE);
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const expectedFunctions = [
|
|
397
|
+
"cssPath",
|
|
398
|
+
"simpleHash",
|
|
399
|
+
"isVisible",
|
|
400
|
+
"isEnabled",
|
|
401
|
+
"inferRole",
|
|
402
|
+
"accessibleName",
|
|
403
|
+
"isInteractiveEl",
|
|
404
|
+
"domPath",
|
|
405
|
+
"selectorHints",
|
|
406
|
+
];
|
|
407
|
+
|
|
408
|
+
for (const fnName of expectedFunctions) {
|
|
409
|
+
it(`contains assignment for pi.${fnName}`, () => {
|
|
410
|
+
assert.ok(
|
|
411
|
+
EVALUATE_HELPERS_SOURCE.includes(`pi.${fnName} = function`),
|
|
412
|
+
`Expected pi.${fnName} = function assignment in source`,
|
|
413
|
+
);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// ---------------------------------------------------------------------------
|
|
419
|
+
// state.ts — accessor round-trips
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
|
|
422
|
+
describe("state accessors", () => {
|
|
423
|
+
beforeEach(() => {
|
|
424
|
+
resetAllState();
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("setBrowser/getBrowser round-trip", () => {
|
|
428
|
+
assert.equal(getBrowser(), null);
|
|
429
|
+
const fakeBrowser = { close: () => {} };
|
|
430
|
+
setBrowser(fakeBrowser);
|
|
431
|
+
assert.equal(getBrowser(), fakeBrowser);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("setContext/getContext round-trip", () => {
|
|
435
|
+
assert.equal(getContext(), null);
|
|
436
|
+
const fakeContext = { newPage: () => {} };
|
|
437
|
+
setContext(fakeContext);
|
|
438
|
+
assert.equal(getContext(), fakeContext);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("setActiveFrame/getActiveFrame round-trip", () => {
|
|
442
|
+
assert.equal(getActiveFrame(), null);
|
|
443
|
+
const fakeFrame = { name: () => "test" };
|
|
444
|
+
setActiveFrame(fakeFrame);
|
|
445
|
+
assert.equal(getActiveFrame(), fakeFrame);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("setSessionStartedAt/getSessionStartedAt round-trip", () => {
|
|
449
|
+
assert.equal(getSessionStartedAt(), null);
|
|
450
|
+
setSessionStartedAt(1234567890);
|
|
451
|
+
assert.equal(getSessionStartedAt(), 1234567890);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("setSessionArtifactDir/getSessionArtifactDir round-trip", () => {
|
|
455
|
+
assert.equal(getSessionArtifactDir(), null);
|
|
456
|
+
setSessionArtifactDir("/tmp/artifacts");
|
|
457
|
+
assert.equal(getSessionArtifactDir(), "/tmp/artifacts");
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("setCurrentRefMap/getCurrentRefMap round-trip", () => {
|
|
461
|
+
assert.deepStrictEqual(getCurrentRefMap(), {});
|
|
462
|
+
const refMap = { e1: { ref: "e1", tag: "button" } };
|
|
463
|
+
setCurrentRefMap(refMap);
|
|
464
|
+
assert.deepStrictEqual(getCurrentRefMap(), refMap);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("setRefVersion/getRefVersion round-trip", () => {
|
|
468
|
+
assert.equal(getRefVersion(), 0);
|
|
469
|
+
setRefVersion(5);
|
|
470
|
+
assert.equal(getRefVersion(), 5);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("setRefMetadata/getRefMetadata round-trip", () => {
|
|
474
|
+
assert.equal(getRefMetadata(), null);
|
|
475
|
+
const metadata = { url: "http://test.com", timestamp: 123, interactiveOnly: true, limit: 40, version: 1 };
|
|
476
|
+
setRefMetadata(metadata);
|
|
477
|
+
assert.deepStrictEqual(getRefMetadata(), metadata);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("setLastActionBeforeState/getLastActionBeforeState round-trip", () => {
|
|
481
|
+
assert.equal(getLastActionBeforeState(), null);
|
|
482
|
+
const state = { url: "http://test.com", title: "Test", focus: "", headings: [], bodyText: "", counts: { landmarks: 0, buttons: 0, links: 0, inputs: 0 }, dialog: { count: 0, title: "" }, selectorStates: {} };
|
|
483
|
+
setLastActionBeforeState(state);
|
|
484
|
+
assert.deepStrictEqual(getLastActionBeforeState(), state);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it("setLastActionAfterState/getLastActionAfterState round-trip", () => {
|
|
488
|
+
assert.equal(getLastActionAfterState(), null);
|
|
489
|
+
const state = { url: "http://test.com/after", title: "After", focus: "", headings: [], bodyText: "", counts: { landmarks: 0, buttons: 0, links: 0, inputs: 0 }, dialog: { count: 0, title: "" }, selectorStates: {} };
|
|
490
|
+
setLastActionAfterState(state);
|
|
491
|
+
assert.deepStrictEqual(getLastActionAfterState(), state);
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// ---------------------------------------------------------------------------
|
|
496
|
+
// state.ts — resetAllState
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
|
|
499
|
+
describe("resetAllState", () => {
|
|
500
|
+
it("clears all state back to defaults", () => {
|
|
501
|
+
// Set various state values
|
|
502
|
+
setBrowser({ close: () => {} });
|
|
503
|
+
setContext({ newPage: () => {} });
|
|
504
|
+
setActiveFrame({ name: () => "frame" });
|
|
505
|
+
setSessionStartedAt(9999);
|
|
506
|
+
setSessionArtifactDir("/tmp/test");
|
|
507
|
+
setCurrentRefMap({ e1: {} });
|
|
508
|
+
setRefVersion(10);
|
|
509
|
+
setRefMetadata({ url: "http://x", timestamp: 1, interactiveOnly: true, limit: 40, version: 1 });
|
|
510
|
+
setLastActionBeforeState({ url: "before" });
|
|
511
|
+
setLastActionAfterState({ url: "after" });
|
|
512
|
+
|
|
513
|
+
// Reset
|
|
514
|
+
resetAllState();
|
|
515
|
+
|
|
516
|
+
// Verify all cleared
|
|
517
|
+
assert.equal(getBrowser(), null);
|
|
518
|
+
assert.equal(getContext(), null);
|
|
519
|
+
assert.equal(getActiveFrame(), null);
|
|
520
|
+
assert.equal(getSessionStartedAt(), null);
|
|
521
|
+
assert.equal(getSessionArtifactDir(), null);
|
|
522
|
+
assert.deepStrictEqual(getCurrentRefMap(), {});
|
|
523
|
+
assert.equal(getRefVersion(), 0);
|
|
524
|
+
assert.equal(getRefMetadata(), null);
|
|
525
|
+
assert.equal(getLastActionBeforeState(), null);
|
|
526
|
+
assert.equal(getLastActionAfterState(), null);
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
// capture.ts — constrainScreenshot
|
|
532
|
+
// ---------------------------------------------------------------------------
|
|
533
|
+
|
|
534
|
+
describe("constrainScreenshot", () => {
|
|
535
|
+
// Helper: create a synthetic JPEG buffer via sharp
|
|
536
|
+
async function createTestJpeg(width, height) {
|
|
537
|
+
const sharp = require("sharp");
|
|
538
|
+
return sharp({
|
|
539
|
+
create: {
|
|
540
|
+
width,
|
|
541
|
+
height,
|
|
542
|
+
channels: 3,
|
|
543
|
+
background: { r: 128, g: 128, b: 128 },
|
|
544
|
+
},
|
|
545
|
+
})
|
|
546
|
+
.jpeg({ quality: 80 })
|
|
547
|
+
.toBuffer();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Helper: create a synthetic PNG buffer via sharp
|
|
551
|
+
async function createTestPng(width, height) {
|
|
552
|
+
const sharp = require("sharp");
|
|
553
|
+
return sharp({
|
|
554
|
+
create: {
|
|
555
|
+
width,
|
|
556
|
+
height,
|
|
557
|
+
channels: 4,
|
|
558
|
+
background: { r: 128, g: 128, b: 128, alpha: 1 },
|
|
559
|
+
},
|
|
560
|
+
})
|
|
561
|
+
.png()
|
|
562
|
+
.toBuffer();
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
it("passes through a small JPEG unchanged", async () => {
|
|
566
|
+
const buf = await createTestJpeg(800, 600);
|
|
567
|
+
const result = await constrainScreenshot(null, buf, "image/jpeg", 80);
|
|
568
|
+
// Should return the same buffer (no resize needed)
|
|
569
|
+
assert.equal(Buffer.isBuffer(result), true);
|
|
570
|
+
const sharp = require("sharp");
|
|
571
|
+
const meta = await sharp(result).metadata();
|
|
572
|
+
assert.equal(meta.width, 800);
|
|
573
|
+
assert.equal(meta.height, 600);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it("resizes an oversized JPEG within 1568px", async () => {
|
|
577
|
+
const buf = await createTestJpeg(3000, 2000);
|
|
578
|
+
const result = await constrainScreenshot(null, buf, "image/jpeg", 80);
|
|
579
|
+
assert.equal(Buffer.isBuffer(result), true);
|
|
580
|
+
|
|
581
|
+
const sharp = require("sharp");
|
|
582
|
+
const meta = await sharp(result).metadata();
|
|
583
|
+
// Both dimensions should be <= 1568
|
|
584
|
+
assert.ok(meta.width <= 1568, `width ${meta.width} should be <= 1568`);
|
|
585
|
+
assert.ok(meta.height <= 1568, `height ${meta.height} should be <= 1568`);
|
|
586
|
+
// Aspect ratio preserved: 3000/2000 = 1.5, so width = 1568, height ~= 1045
|
|
587
|
+
assert.equal(meta.width, 1568);
|
|
588
|
+
assert.ok(meta.height > 1000 && meta.height < 1100);
|
|
589
|
+
assert.equal(meta.format, "jpeg");
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it("resizes an oversized PNG and returns PNG", async () => {
|
|
593
|
+
const buf = await createTestPng(2500, 1800);
|
|
594
|
+
const result = await constrainScreenshot(null, buf, "image/png", 80);
|
|
595
|
+
assert.equal(Buffer.isBuffer(result), true);
|
|
596
|
+
|
|
597
|
+
const sharp = require("sharp");
|
|
598
|
+
const meta = await sharp(result).metadata();
|
|
599
|
+
assert.ok(meta.width <= 1568, `width ${meta.width} should be <= 1568`);
|
|
600
|
+
assert.ok(meta.height <= 1568, `height ${meta.height} should be <= 1568`);
|
|
601
|
+
assert.equal(meta.format, "png");
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it("handles an image where only height exceeds the limit", async () => {
|
|
605
|
+
const buf = await createTestJpeg(1000, 2000);
|
|
606
|
+
const result = await constrainScreenshot(null, buf, "image/jpeg", 80);
|
|
607
|
+
const sharp = require("sharp");
|
|
608
|
+
const meta = await sharp(result).metadata();
|
|
609
|
+
assert.ok(meta.width <= 1568);
|
|
610
|
+
assert.ok(meta.height <= 1568);
|
|
611
|
+
// Height was the constraining dimension
|
|
612
|
+
assert.equal(meta.height, 1568);
|
|
613
|
+
});
|
|
614
|
+
});
|