browser-commander 0.2.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/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.github/workflows/release.yml +296 -0
- package/.husky/pre-commit +1 -0
- package/.jscpd.json +20 -0
- package/.prettierignore +7 -0
- package/.prettierrc +10 -0
- package/CHANGELOG.md +32 -0
- package/LICENSE +24 -0
- package/README.md +320 -0
- package/bunfig.toml +3 -0
- package/deno.json +7 -0
- package/eslint.config.js +125 -0
- package/examples/react-test-app/index.html +25 -0
- package/examples/react-test-app/package.json +19 -0
- package/examples/react-test-app/src/App.jsx +473 -0
- package/examples/react-test-app/src/main.jsx +10 -0
- package/examples/react-test-app/src/styles.css +323 -0
- package/examples/react-test-app/vite.config.js +9 -0
- package/package.json +89 -0
- package/scripts/changeset-version.mjs +38 -0
- package/scripts/create-github-release.mjs +93 -0
- package/scripts/create-manual-changeset.mjs +86 -0
- package/scripts/format-github-release.mjs +83 -0
- package/scripts/format-release-notes.mjs +216 -0
- package/scripts/instant-version-bump.mjs +121 -0
- package/scripts/merge-changesets.mjs +260 -0
- package/scripts/publish-to-npm.mjs +126 -0
- package/scripts/setup-npm.mjs +37 -0
- package/scripts/validate-changeset.mjs +262 -0
- package/scripts/version-and-commit.mjs +237 -0
- package/src/ARCHITECTURE.md +270 -0
- package/src/README.md +517 -0
- package/src/bindings.js +298 -0
- package/src/browser/launcher.js +93 -0
- package/src/browser/navigation.js +513 -0
- package/src/core/constants.js +24 -0
- package/src/core/engine-adapter.js +466 -0
- package/src/core/engine-detection.js +49 -0
- package/src/core/logger.js +21 -0
- package/src/core/navigation-manager.js +503 -0
- package/src/core/navigation-safety.js +160 -0
- package/src/core/network-tracker.js +373 -0
- package/src/core/page-session.js +299 -0
- package/src/core/page-trigger-manager.js +564 -0
- package/src/core/preferences.js +46 -0
- package/src/elements/content.js +197 -0
- package/src/elements/locators.js +243 -0
- package/src/elements/selectors.js +360 -0
- package/src/elements/visibility.js +166 -0
- package/src/exports.js +121 -0
- package/src/factory.js +192 -0
- package/src/high-level/universal-logic.js +206 -0
- package/src/index.js +17 -0
- package/src/interactions/click.js +684 -0
- package/src/interactions/fill.js +383 -0
- package/src/interactions/scroll.js +341 -0
- package/src/utilities/url.js +33 -0
- package/src/utilities/wait.js +135 -0
- package/tests/e2e/playwright.e2e.test.js +442 -0
- package/tests/e2e/puppeteer.e2e.test.js +408 -0
- package/tests/helpers/mocks.js +542 -0
- package/tests/unit/bindings.test.js +218 -0
- package/tests/unit/browser/navigation.test.js +345 -0
- package/tests/unit/core/constants.test.js +72 -0
- package/tests/unit/core/engine-adapter.test.js +170 -0
- package/tests/unit/core/engine-detection.test.js +81 -0
- package/tests/unit/core/logger.test.js +80 -0
- package/tests/unit/core/navigation-safety.test.js +202 -0
- package/tests/unit/core/network-tracker.test.js +198 -0
- package/tests/unit/core/page-trigger-manager.test.js +358 -0
- package/tests/unit/elements/content.test.js +318 -0
- package/tests/unit/elements/locators.test.js +236 -0
- package/tests/unit/elements/selectors.test.js +302 -0
- package/tests/unit/elements/visibility.test.js +234 -0
- package/tests/unit/factory.test.js +174 -0
- package/tests/unit/high-level/universal-logic.test.js +299 -0
- package/tests/unit/interactions/click.test.js +340 -0
- package/tests/unit/interactions/fill.test.js +378 -0
- package/tests/unit/interactions/scroll.test.js +330 -0
- package/tests/unit/utilities/url.test.js +63 -0
- package/tests/unit/utilities/wait.test.js +207 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock utilities for unit tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a mock Playwright page object
|
|
7
|
+
*/
|
|
8
|
+
export function createMockPlaywrightPage(options = {}) {
|
|
9
|
+
const {
|
|
10
|
+
url = 'https://example.com',
|
|
11
|
+
elements = {},
|
|
12
|
+
evaluateResult = null,
|
|
13
|
+
} = options;
|
|
14
|
+
|
|
15
|
+
const eventListeners = new Map();
|
|
16
|
+
|
|
17
|
+
const locatorMock = (selector) => {
|
|
18
|
+
const elementData = elements[selector] || {
|
|
19
|
+
count: 1,
|
|
20
|
+
visible: true,
|
|
21
|
+
enabled: true,
|
|
22
|
+
textContent: 'Mock text',
|
|
23
|
+
value: '',
|
|
24
|
+
className: 'mock-class',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
count: async () => elementData.count,
|
|
29
|
+
first() {
|
|
30
|
+
return this;
|
|
31
|
+
},
|
|
32
|
+
nth(i) {
|
|
33
|
+
return this;
|
|
34
|
+
},
|
|
35
|
+
last() {
|
|
36
|
+
return this;
|
|
37
|
+
},
|
|
38
|
+
click: async (opts = {}) => {},
|
|
39
|
+
fill: async (text) => {
|
|
40
|
+
elementData.value = text;
|
|
41
|
+
},
|
|
42
|
+
type: async (text) => {
|
|
43
|
+
elementData.value = text;
|
|
44
|
+
},
|
|
45
|
+
focus: async () => {},
|
|
46
|
+
textContent: async () => elementData.textContent,
|
|
47
|
+
inputValue: async () => elementData.value,
|
|
48
|
+
getAttribute: async (name) => elementData[name] || null,
|
|
49
|
+
isVisible: async () => elementData.visible,
|
|
50
|
+
waitFor: async (opts = {}) => {
|
|
51
|
+
if (!elementData.visible && opts.state === 'visible') {
|
|
52
|
+
throw new Error('Element not visible');
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
evaluate: async (fn, arg) => {
|
|
56
|
+
const mockEl = {
|
|
57
|
+
tagName: 'DIV',
|
|
58
|
+
textContent: elementData.textContent,
|
|
59
|
+
value: elementData.value,
|
|
60
|
+
className: elementData.className,
|
|
61
|
+
disabled: !elementData.enabled,
|
|
62
|
+
checked: elementData.checked || false,
|
|
63
|
+
isConnected: true,
|
|
64
|
+
offsetWidth: elementData.visible ? 100 : 0,
|
|
65
|
+
offsetHeight: elementData.visible ? 50 : 0,
|
|
66
|
+
hasAttribute: (attr) => !!elementData[attr],
|
|
67
|
+
getAttribute: (attr) => elementData[attr] || null,
|
|
68
|
+
classList: {
|
|
69
|
+
contains: (cls) => elementData.className?.includes(cls) || false,
|
|
70
|
+
},
|
|
71
|
+
getBoundingClientRect: () => ({
|
|
72
|
+
top: 100,
|
|
73
|
+
bottom: 150,
|
|
74
|
+
left: 10,
|
|
75
|
+
right: 110,
|
|
76
|
+
width: 100,
|
|
77
|
+
height: 50,
|
|
78
|
+
}),
|
|
79
|
+
scrollIntoView: () => {},
|
|
80
|
+
dispatchEvent: () => {},
|
|
81
|
+
};
|
|
82
|
+
return fn(mockEl, arg);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
_isPlaywrightPage: true,
|
|
89
|
+
url: () => url,
|
|
90
|
+
goto: async (targetUrl, opts = {}) => {},
|
|
91
|
+
waitForNavigation: async (opts = {}) => {},
|
|
92
|
+
waitForSelector: async (sel, opts = {}) => locatorMock(sel),
|
|
93
|
+
$: async (sel) => locatorMock(sel),
|
|
94
|
+
$$: async (sel) => {
|
|
95
|
+
const count = elements[sel]?.count || 1;
|
|
96
|
+
return Array(count).fill(locatorMock(sel));
|
|
97
|
+
},
|
|
98
|
+
$eval: async (sel, fn, ...args) => {
|
|
99
|
+
const loc = locatorMock(sel);
|
|
100
|
+
return loc.evaluate(fn, ...args);
|
|
101
|
+
},
|
|
102
|
+
$$eval: async (sel, fn, ...args) => {
|
|
103
|
+
const count = elements[sel]?.count || 1;
|
|
104
|
+
const locs = Array(count).fill(locatorMock(sel));
|
|
105
|
+
return locs.map((l) => l.evaluate(fn, ...args));
|
|
106
|
+
},
|
|
107
|
+
locator: locatorMock,
|
|
108
|
+
evaluate: async (fn, ...args) => {
|
|
109
|
+
if (evaluateResult !== null) {
|
|
110
|
+
return evaluateResult;
|
|
111
|
+
}
|
|
112
|
+
// Create mock window/document context
|
|
113
|
+
const mockContext = {
|
|
114
|
+
innerHeight: 800,
|
|
115
|
+
innerWidth: 1200,
|
|
116
|
+
sessionStorage: {
|
|
117
|
+
_data: {},
|
|
118
|
+
getItem: (key) => mockContext.sessionStorage._data[key] || null,
|
|
119
|
+
setItem: (key, val) => {
|
|
120
|
+
mockContext.sessionStorage._data[key] = val;
|
|
121
|
+
},
|
|
122
|
+
removeItem: (key) => {
|
|
123
|
+
delete mockContext.sessionStorage._data[key];
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
querySelectorAll: () => [],
|
|
127
|
+
};
|
|
128
|
+
try {
|
|
129
|
+
return fn(...args);
|
|
130
|
+
} catch {
|
|
131
|
+
return fn;
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
mainFrame: () => ({
|
|
135
|
+
url: () => url,
|
|
136
|
+
}),
|
|
137
|
+
context: () => ({}),
|
|
138
|
+
bringToFront: async () => {},
|
|
139
|
+
on: (event, handler) => {
|
|
140
|
+
if (!eventListeners.has(event)) {
|
|
141
|
+
eventListeners.set(event, []);
|
|
142
|
+
}
|
|
143
|
+
eventListeners.get(event).push(handler);
|
|
144
|
+
},
|
|
145
|
+
off: (event, handler) => {
|
|
146
|
+
const handlers = eventListeners.get(event);
|
|
147
|
+
if (handlers) {
|
|
148
|
+
const idx = handlers.indexOf(handler);
|
|
149
|
+
if (idx !== -1) {
|
|
150
|
+
handlers.splice(idx, 1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
emit: (event, data) => {
|
|
155
|
+
const handlers = eventListeners.get(event);
|
|
156
|
+
if (handlers) {
|
|
157
|
+
handlers.forEach((h) => h(data));
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
click: async (sel, opts = {}) => {},
|
|
161
|
+
type: async (sel, text, opts = {}) => {},
|
|
162
|
+
keyboard: {
|
|
163
|
+
type: async (text) => {},
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Create a mock Puppeteer page object
|
|
170
|
+
*/
|
|
171
|
+
export function createMockPuppeteerPage(options = {}) {
|
|
172
|
+
const {
|
|
173
|
+
url = 'https://example.com',
|
|
174
|
+
elements = {},
|
|
175
|
+
evaluateResult = null,
|
|
176
|
+
} = options;
|
|
177
|
+
|
|
178
|
+
const eventListeners = new Map();
|
|
179
|
+
|
|
180
|
+
const elementMock = (selector) => {
|
|
181
|
+
const elementData = elements[selector] || {
|
|
182
|
+
count: 1,
|
|
183
|
+
visible: true,
|
|
184
|
+
enabled: true,
|
|
185
|
+
textContent: 'Mock text',
|
|
186
|
+
value: '',
|
|
187
|
+
className: 'mock-class',
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
click: async (opts = {}) => {},
|
|
192
|
+
focus: async () => {},
|
|
193
|
+
type: async (text) => {
|
|
194
|
+
elementData.value = text;
|
|
195
|
+
},
|
|
196
|
+
evaluate: async (fn, ...args) =>
|
|
197
|
+
page.evaluate(fn, elementMock(selector), ...args),
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const page = {
|
|
202
|
+
_isPuppeteerPage: true,
|
|
203
|
+
url: () => url,
|
|
204
|
+
goto: async (targetUrl, opts = {}) => {},
|
|
205
|
+
waitForNavigation: async (opts = {}) => {},
|
|
206
|
+
waitForSelector: async (sel, opts = {}) => elementMock(sel),
|
|
207
|
+
$: async (sel) => {
|
|
208
|
+
const elementData = elements[sel];
|
|
209
|
+
if (elementData?.count === 0) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
return elementMock(sel);
|
|
213
|
+
},
|
|
214
|
+
$$: async (sel) => {
|
|
215
|
+
const count = elements[sel]?.count || 1;
|
|
216
|
+
return Array(count)
|
|
217
|
+
.fill(null)
|
|
218
|
+
.map(() => elementMock(sel));
|
|
219
|
+
},
|
|
220
|
+
$eval: async (sel, fn, ...args) => {
|
|
221
|
+
const elementData = elements[sel] || {};
|
|
222
|
+
const mockEl = {
|
|
223
|
+
tagName: 'DIV',
|
|
224
|
+
textContent: elementData.textContent || 'Mock text',
|
|
225
|
+
value: elementData.value || '',
|
|
226
|
+
className: elementData.className || 'mock-class',
|
|
227
|
+
disabled: !elementData.enabled,
|
|
228
|
+
checked: elementData.checked || false,
|
|
229
|
+
isConnected: true,
|
|
230
|
+
offsetWidth: elementData.visible !== false ? 100 : 0,
|
|
231
|
+
offsetHeight: elementData.visible !== false ? 50 : 0,
|
|
232
|
+
hasAttribute: (attr) => !!elementData[attr],
|
|
233
|
+
getAttribute: (attr) => elementData[attr] || null,
|
|
234
|
+
classList: {
|
|
235
|
+
contains: (cls) => elementData.className?.includes(cls) || false,
|
|
236
|
+
},
|
|
237
|
+
getBoundingClientRect: () => ({
|
|
238
|
+
top: 100,
|
|
239
|
+
bottom: 150,
|
|
240
|
+
left: 10,
|
|
241
|
+
right: 110,
|
|
242
|
+
width: 100,
|
|
243
|
+
height: 50,
|
|
244
|
+
}),
|
|
245
|
+
scrollIntoView: () => {},
|
|
246
|
+
dispatchEvent: () => {},
|
|
247
|
+
};
|
|
248
|
+
return fn(mockEl, ...args);
|
|
249
|
+
},
|
|
250
|
+
$$eval: async (sel, fn, ...args) => {
|
|
251
|
+
const count = elements[sel]?.count || 1;
|
|
252
|
+
return fn(Array(count).fill({}), ...args);
|
|
253
|
+
},
|
|
254
|
+
evaluate: async (fn, ...args) => {
|
|
255
|
+
if (evaluateResult !== null) {
|
|
256
|
+
return evaluateResult;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
// For element evaluations, first arg might be an element mock
|
|
260
|
+
if (args[0] && typeof args[0] === 'object' && args[0].click) {
|
|
261
|
+
const mockEl = {
|
|
262
|
+
tagName: 'DIV',
|
|
263
|
+
textContent: 'Mock text',
|
|
264
|
+
value: '',
|
|
265
|
+
className: 'mock-class',
|
|
266
|
+
disabled: false,
|
|
267
|
+
checked: false,
|
|
268
|
+
isConnected: true,
|
|
269
|
+
offsetWidth: 100,
|
|
270
|
+
offsetHeight: 50,
|
|
271
|
+
hasAttribute: () => false,
|
|
272
|
+
getAttribute: () => null,
|
|
273
|
+
classList: { contains: () => false },
|
|
274
|
+
getBoundingClientRect: () => ({
|
|
275
|
+
top: 100,
|
|
276
|
+
bottom: 150,
|
|
277
|
+
left: 10,
|
|
278
|
+
right: 110,
|
|
279
|
+
width: 100,
|
|
280
|
+
height: 50,
|
|
281
|
+
}),
|
|
282
|
+
scrollIntoView: () => {},
|
|
283
|
+
dispatchEvent: () => {},
|
|
284
|
+
};
|
|
285
|
+
return fn(mockEl, ...args.slice(1));
|
|
286
|
+
}
|
|
287
|
+
return fn(...args);
|
|
288
|
+
} catch {
|
|
289
|
+
return fn;
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
mainFrame: () => ({
|
|
293
|
+
url: () => url,
|
|
294
|
+
}),
|
|
295
|
+
bringToFront: async () => {},
|
|
296
|
+
on: (event, handler) => {
|
|
297
|
+
if (!eventListeners.has(event)) {
|
|
298
|
+
eventListeners.set(event, []);
|
|
299
|
+
}
|
|
300
|
+
eventListeners.get(event).push(handler);
|
|
301
|
+
},
|
|
302
|
+
off: (event, handler) => {
|
|
303
|
+
const handlers = eventListeners.get(event);
|
|
304
|
+
if (handlers) {
|
|
305
|
+
const idx = handlers.indexOf(handler);
|
|
306
|
+
if (idx !== -1) {
|
|
307
|
+
handlers.splice(idx, 1);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
emit: (event, data) => {
|
|
312
|
+
const handlers = eventListeners.get(event);
|
|
313
|
+
if (handlers) {
|
|
314
|
+
handlers.forEach((h) => h(data));
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
click: async (sel, opts = {}) => {},
|
|
318
|
+
type: async (sel, text, opts = {}) => {},
|
|
319
|
+
keyboard: {
|
|
320
|
+
type: async (text) => {},
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
return page;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Create a mock logger
|
|
329
|
+
*/
|
|
330
|
+
export function createMockLogger(options = {}) {
|
|
331
|
+
const { collectLogs = false } = options;
|
|
332
|
+
const logs = [];
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
debug: (fn) => {
|
|
336
|
+
if (collectLogs) {
|
|
337
|
+
logs.push({
|
|
338
|
+
level: 'debug',
|
|
339
|
+
message: typeof fn === 'function' ? fn() : fn,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
info: (fn) => {
|
|
344
|
+
if (collectLogs) {
|
|
345
|
+
logs.push({
|
|
346
|
+
level: 'info',
|
|
347
|
+
message: typeof fn === 'function' ? fn() : fn,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
warn: (fn) => {
|
|
352
|
+
if (collectLogs) {
|
|
353
|
+
logs.push({
|
|
354
|
+
level: 'warn',
|
|
355
|
+
message: typeof fn === 'function' ? fn() : fn,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
error: (fn) => {
|
|
360
|
+
if (collectLogs) {
|
|
361
|
+
logs.push({
|
|
362
|
+
level: 'error',
|
|
363
|
+
message: typeof fn === 'function' ? fn() : fn,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
getLogs: () => logs,
|
|
368
|
+
clear: () => {
|
|
369
|
+
logs.length = 0;
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Create a mock network tracker
|
|
376
|
+
*/
|
|
377
|
+
export function createMockNetworkTracker(options = {}) {
|
|
378
|
+
const { initialPendingCount = 0, waitForIdleResult = true } = options;
|
|
379
|
+
|
|
380
|
+
let pendingCount = initialPendingCount;
|
|
381
|
+
const listeners = {
|
|
382
|
+
onRequestStart: [],
|
|
383
|
+
onRequestEnd: [],
|
|
384
|
+
onNetworkIdle: [],
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
startTracking: () => {},
|
|
389
|
+
stopTracking: () => {},
|
|
390
|
+
waitForNetworkIdle: async (opts = {}) => waitForIdleResult,
|
|
391
|
+
getPendingCount: () => pendingCount,
|
|
392
|
+
getPendingUrls: () => [],
|
|
393
|
+
reset: () => {
|
|
394
|
+
pendingCount = 0;
|
|
395
|
+
},
|
|
396
|
+
on: (event, callback) => {
|
|
397
|
+
if (listeners[event]) {
|
|
398
|
+
listeners[event].push(callback);
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
off: (event, callback) => {
|
|
402
|
+
if (listeners[event]) {
|
|
403
|
+
const idx = listeners[event].indexOf(callback);
|
|
404
|
+
if (idx !== -1) {
|
|
405
|
+
listeners[event].splice(idx, 1);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
setPendingCount: (count) => {
|
|
410
|
+
pendingCount = count;
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Create a mock navigation manager
|
|
417
|
+
*/
|
|
418
|
+
export function createMockNavigationManager(options = {}) {
|
|
419
|
+
const {
|
|
420
|
+
currentUrl = 'https://example.com',
|
|
421
|
+
isNavigating = false,
|
|
422
|
+
shouldAbortValue = false,
|
|
423
|
+
} = options;
|
|
424
|
+
|
|
425
|
+
let url = currentUrl;
|
|
426
|
+
let navigating = isNavigating;
|
|
427
|
+
const sessionId = 1;
|
|
428
|
+
let abortController = new AbortController();
|
|
429
|
+
const listeners = {
|
|
430
|
+
onNavigationStart: [],
|
|
431
|
+
onNavigationComplete: [],
|
|
432
|
+
onBeforeNavigate: [],
|
|
433
|
+
onUrlChange: [],
|
|
434
|
+
onPageReady: [],
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
navigate: async (opts = {}) => {
|
|
439
|
+
url = opts.url || url;
|
|
440
|
+
return true;
|
|
441
|
+
},
|
|
442
|
+
waitForNavigation: async (opts = {}) => true,
|
|
443
|
+
waitForPageReady: async (opts = {}) => true,
|
|
444
|
+
isNavigating: () => navigating,
|
|
445
|
+
getCurrentUrl: () => url,
|
|
446
|
+
getSessionId: () => sessionId,
|
|
447
|
+
getAbortSignal: () => abortController.signal,
|
|
448
|
+
shouldAbort: () => shouldAbortValue,
|
|
449
|
+
onSessionCleanup: (callback) => {},
|
|
450
|
+
on: (event, callback) => {
|
|
451
|
+
if (listeners[event]) {
|
|
452
|
+
listeners[event].push(callback);
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
off: (event, callback) => {
|
|
456
|
+
if (listeners[event]) {
|
|
457
|
+
const idx = listeners[event].indexOf(callback);
|
|
458
|
+
if (idx !== -1) {
|
|
459
|
+
listeners[event].splice(idx, 1);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
startListening: () => {},
|
|
464
|
+
stopListening: () => {},
|
|
465
|
+
configure: (config) => {},
|
|
466
|
+
setUrl: (newUrl) => {
|
|
467
|
+
url = newUrl;
|
|
468
|
+
},
|
|
469
|
+
setNavigating: (val) => {
|
|
470
|
+
navigating = val;
|
|
471
|
+
},
|
|
472
|
+
triggerEvent: (event, data) => {
|
|
473
|
+
if (listeners[event]) {
|
|
474
|
+
listeners[event].forEach((cb) => cb(data));
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
abort: () => {
|
|
478
|
+
abortController.abort();
|
|
479
|
+
},
|
|
480
|
+
resetAbort: () => {
|
|
481
|
+
abortController = new AbortController();
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Create navigation error for testing
|
|
488
|
+
*/
|
|
489
|
+
export function createNavigationError(
|
|
490
|
+
message = 'Execution context was destroyed'
|
|
491
|
+
) {
|
|
492
|
+
const error = new Error(message);
|
|
493
|
+
error.name = 'NavigationError';
|
|
494
|
+
return error;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Simple assertion helpers that work with Node.js test runner
|
|
499
|
+
*/
|
|
500
|
+
export const assert = {
|
|
501
|
+
equal: (actual, expected, message = '') => {
|
|
502
|
+
if (actual !== expected) {
|
|
503
|
+
throw new Error(`${message}: Expected ${expected}, got ${actual}`);
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
deepEqual: (actual, expected, message = '') => {
|
|
507
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
508
|
+
throw new Error(`${message}: Objects not equal`);
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
ok: (value, message = '') => {
|
|
512
|
+
if (!value) {
|
|
513
|
+
throw new Error(`${message}: Expected truthy value, got ${value}`);
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
notOk: (value, message = '') => {
|
|
517
|
+
if (value) {
|
|
518
|
+
throw new Error(`${message}: Expected falsy value, got ${value}`);
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
throws: async (fn, expectedError, message = '') => {
|
|
522
|
+
try {
|
|
523
|
+
await fn();
|
|
524
|
+
throw new Error(`${message}: Expected function to throw`);
|
|
525
|
+
} catch (error) {
|
|
526
|
+
if (expectedError && !error.message.includes(expectedError)) {
|
|
527
|
+
throw new Error(
|
|
528
|
+
`${message}: Expected error message to include "${expectedError}", got "${error.message}"`
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
doesNotThrow: async (fn, message = '') => {
|
|
534
|
+
try {
|
|
535
|
+
await fn();
|
|
536
|
+
} catch (error) {
|
|
537
|
+
throw new Error(
|
|
538
|
+
`${message}: Expected function not to throw, but got: ${error.message}`
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
};
|