browser-commander 0.11.0 → 0.13.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/CHANGELOG.md +16 -0
- package/README.md +104 -0
- package/examples/pdf-generation.js +7 -4
- package/package.json +1 -1
- package/scripts/run-tests.mjs +19 -1
- package/src/README.md +13 -0
- package/src/bindings.js +8 -0
- package/src/browser/connector.js +163 -0
- package/src/browser/launcher.js +17 -0
- package/src/browser/navigation.js +58 -0
- package/src/browser/real-browser.js +439 -0
- package/src/browser/storage-state.js +133 -0
- package/src/core/navigation-manager.js +57 -24
- package/src/exports.js +4 -0
- package/tests/e2e/playwright.e2e.test.js +19 -0
- package/tests/e2e/puppeteer.e2e.test.js +19 -0
- package/tests/helpers/mocks.js +3 -0
- package/tests/unit/bindings.test.js +4 -0
- package/tests/unit/browser/connector.test.js +129 -0
- package/tests/unit/browser/navigation.test.js +61 -0
- package/tests/unit/browser/real-browser.test.js +165 -0
- package/tests/unit/browser/storage-state.test.js +148 -0
- package/tests/unit/core/navigation-manager.test.js +62 -0
- package/tests/unit/scripts/ci-workflow-policy.test.js +73 -0
- package/tests/unit/scripts/npm-release-helpers.test.js +20 -7
|
@@ -522,5 +522,24 @@ describe(
|
|
|
522
522
|
// Test passes if no error is thrown
|
|
523
523
|
});
|
|
524
524
|
});
|
|
525
|
+
|
|
526
|
+
describe('In-memory Content', () => {
|
|
527
|
+
it('should load HTML through commander.setContent', async () => {
|
|
528
|
+
if (!commander) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const result = await commander.setContent({
|
|
533
|
+
html: '<main><h1 data-testid="content-title">In memory</h1></main>',
|
|
534
|
+
waitUntil: 'load',
|
|
535
|
+
});
|
|
536
|
+
const title = await commander.textContent({
|
|
537
|
+
selector: '[data-testid="content-title"]',
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
assert.strictEqual(result.loaded, true);
|
|
541
|
+
assert.strictEqual(title, 'In memory');
|
|
542
|
+
});
|
|
543
|
+
});
|
|
525
544
|
}
|
|
526
545
|
);
|
|
@@ -476,4 +476,23 @@ describe('E2E Tests - Puppeteer Engine', { skip: !process.env.RUN_E2E }, () => {
|
|
|
476
476
|
assert.strictEqual(modalClosed, false);
|
|
477
477
|
});
|
|
478
478
|
});
|
|
479
|
+
|
|
480
|
+
describe('In-memory Content', () => {
|
|
481
|
+
it('should load HTML through commander.setContent', async () => {
|
|
482
|
+
if (!commander) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const result = await commander.setContent({
|
|
487
|
+
html: '<main><h1 data-testid="content-title">In memory</h1></main>',
|
|
488
|
+
waitUntil: 'load',
|
|
489
|
+
});
|
|
490
|
+
const title = await commander.textContent({
|
|
491
|
+
selector: '[data-testid="content-title"]',
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
assert.strictEqual(result.loaded, true);
|
|
495
|
+
assert.strictEqual(title, 'In memory');
|
|
496
|
+
});
|
|
497
|
+
});
|
|
479
498
|
});
|
package/tests/helpers/mocks.js
CHANGED
|
@@ -90,6 +90,7 @@ export function createMockPlaywrightPage(options = {}) {
|
|
|
90
90
|
_isPlaywrightPage: true,
|
|
91
91
|
url: () => url,
|
|
92
92
|
goto: async (targetUrl, opts = {}) => {},
|
|
93
|
+
setContent: async (html, opts = {}) => {},
|
|
93
94
|
waitForNavigation: async (opts = {}) => {},
|
|
94
95
|
waitForSelector: async (sel, opts = {}) => locatorMock(sel),
|
|
95
96
|
$: async (sel) => locatorMock(sel),
|
|
@@ -196,6 +197,7 @@ export function createMockPuppeteerPage(options = {}) {
|
|
|
196
197
|
_isPuppeteerPage: true,
|
|
197
198
|
url: () => url,
|
|
198
199
|
goto: async (targetUrl, opts = {}) => {},
|
|
200
|
+
setContent: async (html, opts = {}) => {},
|
|
199
201
|
waitForNavigation: async (opts = {}) => {},
|
|
200
202
|
waitForSelector: async (sel, opts = {}) => elementMock(sel),
|
|
201
203
|
$: async (sel) => {
|
|
@@ -441,6 +443,7 @@ export function createMockNavigationManager(options = {}) {
|
|
|
441
443
|
url = opts.url || url;
|
|
442
444
|
return true;
|
|
443
445
|
},
|
|
446
|
+
setContent: async (opts = {}) => true,
|
|
444
447
|
waitForNavigation: async (opts = {}) => true,
|
|
445
448
|
waitForPageReady: async (opts = {}) => true,
|
|
446
449
|
isNavigating: () => navigating,
|
|
@@ -61,6 +61,10 @@ describe('bindings', () => {
|
|
|
61
61
|
typeof bindings.goto === 'function',
|
|
62
62
|
'goto should be a function'
|
|
63
63
|
);
|
|
64
|
+
assert.ok(
|
|
65
|
+
typeof bindings.setContent === 'function',
|
|
66
|
+
'setContent should be a function'
|
|
67
|
+
);
|
|
64
68
|
assert.ok(
|
|
65
69
|
typeof bindings.getUrl === 'function',
|
|
66
70
|
'getUrl should be a function'
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
connectBrowser,
|
|
6
|
+
connectBrowserWithDependencies,
|
|
7
|
+
} from '../../../src/browser/connector.js';
|
|
8
|
+
import { connectBrowser as publicConnectBrowser } from '../../../src/index.js';
|
|
9
|
+
|
|
10
|
+
describe('connectBrowser', () => {
|
|
11
|
+
it('is exported from the package API', () => {
|
|
12
|
+
assert.equal(publicConnectBrowser, connectBrowser);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('attaches Playwright over an HTTP CDP endpoint and reuses its first page', async () => {
|
|
16
|
+
const calls = [];
|
|
17
|
+
const page = { id: 'playwright-page' };
|
|
18
|
+
const context = {
|
|
19
|
+
pages: () => [page],
|
|
20
|
+
addCookies: async (cookies) => calls.push(['cookies', cookies]),
|
|
21
|
+
};
|
|
22
|
+
const browser = { contexts: () => [context] };
|
|
23
|
+
const chromium = {
|
|
24
|
+
connectOverCDP: async (endpoint, options) => {
|
|
25
|
+
calls.push(['connect', endpoint, options]);
|
|
26
|
+
return browser;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const seedCookies = [
|
|
30
|
+
{ name: 'SID', value: 'saved', domain: '.example.com', path: '/' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const result = await connectBrowserWithDependencies(
|
|
34
|
+
{
|
|
35
|
+
engine: 'playwright',
|
|
36
|
+
cdpEndpoint: 'http://127.0.0.1:9222',
|
|
37
|
+
slowMo: 25,
|
|
38
|
+
timeout: 5_000,
|
|
39
|
+
seedCookies,
|
|
40
|
+
},
|
|
41
|
+
{ loadPlaywright: async () => ({ chromium }) }
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
assert.equal(result.browser, browser);
|
|
45
|
+
assert.equal(result.page, page);
|
|
46
|
+
assert.deepEqual(calls, [
|
|
47
|
+
['connect', 'http://127.0.0.1:9222', { slowMo: 25, timeout: 5_000 }],
|
|
48
|
+
['cookies', seedCookies],
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('maps a WebSocket endpoint to Puppeteer and creates a page when needed', async () => {
|
|
53
|
+
const calls = [];
|
|
54
|
+
const page = {
|
|
55
|
+
id: 'new-puppeteer-page',
|
|
56
|
+
setCookie: async (...cookies) => calls.push(['cookies', cookies]),
|
|
57
|
+
};
|
|
58
|
+
const browser = {
|
|
59
|
+
pages: async () => [],
|
|
60
|
+
newPage: async () => {
|
|
61
|
+
calls.push(['newPage']);
|
|
62
|
+
return page;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
const puppeteer = {
|
|
66
|
+
connect: async (options) => {
|
|
67
|
+
calls.push(['connect', options]);
|
|
68
|
+
return browser;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
const seedCookies = [
|
|
72
|
+
{ name: 'session', value: 'saved', domain: '.example.com', path: '/' },
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const result = await connectBrowserWithDependencies(
|
|
76
|
+
{
|
|
77
|
+
engine: 'puppeteer',
|
|
78
|
+
wsEndpoint: 'ws://127.0.0.1:9222/devtools/browser/id',
|
|
79
|
+
slowMo: 10,
|
|
80
|
+
protocolTimeout: 30_000,
|
|
81
|
+
seedCookies,
|
|
82
|
+
},
|
|
83
|
+
{ loadPuppeteer: async () => ({ default: puppeteer }) }
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
assert.equal(result.browser, browser);
|
|
87
|
+
assert.equal(result.page, page);
|
|
88
|
+
assert.deepEqual(calls, [
|
|
89
|
+
[
|
|
90
|
+
'connect',
|
|
91
|
+
{
|
|
92
|
+
browserWSEndpoint: 'ws://127.0.0.1:9222/devtools/browser/id',
|
|
93
|
+
defaultViewport: null,
|
|
94
|
+
slowMo: 10,
|
|
95
|
+
protocolTimeout: 30_000,
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
['newPage'],
|
|
99
|
+
['cookies', seedCookies],
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('requires exactly one endpoint and a supported engine', async () => {
|
|
104
|
+
await assert.rejects(
|
|
105
|
+
() => connectBrowserWithDependencies({ engine: 'playwright' }, {}),
|
|
106
|
+
/exactly one of cdpEndpoint or wsEndpoint/
|
|
107
|
+
);
|
|
108
|
+
await assert.rejects(
|
|
109
|
+
() =>
|
|
110
|
+
connectBrowserWithDependencies(
|
|
111
|
+
{
|
|
112
|
+
engine: 'playwright',
|
|
113
|
+
cdpEndpoint: 'http://127.0.0.1:9222',
|
|
114
|
+
wsEndpoint: 'ws://127.0.0.1:9222/devtools/browser/id',
|
|
115
|
+
},
|
|
116
|
+
{}
|
|
117
|
+
),
|
|
118
|
+
/exactly one of cdpEndpoint or wsEndpoint/
|
|
119
|
+
);
|
|
120
|
+
await assert.rejects(
|
|
121
|
+
() =>
|
|
122
|
+
connectBrowserWithDependencies(
|
|
123
|
+
{ engine: 'invalid', cdpEndpoint: 'http://127.0.0.1:9222' },
|
|
124
|
+
{}
|
|
125
|
+
),
|
|
126
|
+
/Invalid engine: invalid/
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
verifyNavigation,
|
|
6
6
|
waitForUrlStabilization,
|
|
7
7
|
goto,
|
|
8
|
+
setContent,
|
|
8
9
|
waitForNavigation,
|
|
9
10
|
waitForPageReady,
|
|
10
11
|
waitAfterAction,
|
|
@@ -203,6 +204,66 @@ describe('navigation', () => {
|
|
|
203
204
|
});
|
|
204
205
|
});
|
|
205
206
|
|
|
207
|
+
describe('setContent', () => {
|
|
208
|
+
it('should throw when html is not provided', async () => {
|
|
209
|
+
const page = createMockPlaywrightPage();
|
|
210
|
+
|
|
211
|
+
await assert.rejects(() => setContent({ page }), /html is required/);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should load content using navigationManager', async () => {
|
|
215
|
+
const page = createMockPlaywrightPage();
|
|
216
|
+
const navigationManager = createMockNavigationManager();
|
|
217
|
+
let receivedOptions;
|
|
218
|
+
navigationManager.setContent = async (options) => {
|
|
219
|
+
receivedOptions = options;
|
|
220
|
+
return true;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const result = await setContent({
|
|
224
|
+
page,
|
|
225
|
+
navigationManager,
|
|
226
|
+
html: '<h1>Hi</h1>',
|
|
227
|
+
waitUntil: 'networkidle',
|
|
228
|
+
timeout: 5000,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
assert.deepStrictEqual(receivedOptions, {
|
|
232
|
+
html: '<h1>Hi</h1>',
|
|
233
|
+
waitUntil: 'networkidle',
|
|
234
|
+
timeout: 5000,
|
|
235
|
+
});
|
|
236
|
+
assert.deepStrictEqual(result, {
|
|
237
|
+
loaded: true,
|
|
238
|
+
actualUrl: 'https://example.com',
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should load content directly without navigationManager', async () => {
|
|
243
|
+
const page = createMockPlaywrightPage();
|
|
244
|
+
let receivedArguments;
|
|
245
|
+
page.setContent = async (...args) => {
|
|
246
|
+
receivedArguments = args;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const result = await setContent({
|
|
250
|
+
page,
|
|
251
|
+
html: '',
|
|
252
|
+
waitUntil: 'load',
|
|
253
|
+
timeout: 5000,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
assert.deepStrictEqual(receivedArguments, [
|
|
257
|
+
'',
|
|
258
|
+
{ waitUntil: 'load', timeout: 5000 },
|
|
259
|
+
]);
|
|
260
|
+
assert.deepStrictEqual(result, {
|
|
261
|
+
loaded: true,
|
|
262
|
+
actualUrl: 'https://example.com',
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
206
267
|
describe('waitForNavigation', () => {
|
|
207
268
|
it('should delegate to navigationManager if available', async () => {
|
|
208
269
|
const page = createMockPlaywrightPage();
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { afterEach, describe, it } from 'node:test';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
assertDedicatedUserDataDir,
|
|
9
|
+
buildRealBrowserArgs,
|
|
10
|
+
launchAndConnectRealBrowser,
|
|
11
|
+
launchAndConnectRealBrowserWithDependencies,
|
|
12
|
+
} from '../../../src/browser/real-browser.js';
|
|
13
|
+
import { launchAndConnectRealBrowser as publicHelper } from '../../../src/index.js';
|
|
14
|
+
|
|
15
|
+
describe('launchAndConnectRealBrowser', () => {
|
|
16
|
+
let temporaryDirectory;
|
|
17
|
+
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
if (temporaryDirectory) {
|
|
20
|
+
await rm(temporaryDirectory, { recursive: true, force: true });
|
|
21
|
+
temporaryDirectory = undefined;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('is exported from the package API', () => {
|
|
26
|
+
assert.equal(publicHelper, launchAndConnectRealBrowser);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('builds a loopback-only CDP command with a dedicated profile', () => {
|
|
30
|
+
const args = buildRealBrowserArgs({
|
|
31
|
+
userDataDir: '/tmp/browser-commander-dedicated',
|
|
32
|
+
remoteDebuggingPort: 9333,
|
|
33
|
+
headless: true,
|
|
34
|
+
args: ['--lang=en-US'],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
assert.deepEqual(args, [
|
|
38
|
+
'--remote-debugging-address=127.0.0.1',
|
|
39
|
+
'--remote-debugging-port=9333',
|
|
40
|
+
'--user-data-dir=/tmp/browser-commander-dedicated',
|
|
41
|
+
'--no-first-run',
|
|
42
|
+
'--no-default-browser-check',
|
|
43
|
+
'--headless=new',
|
|
44
|
+
'--lang=en-US',
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('rejects custom arguments that could bypass protected CDP settings', () => {
|
|
49
|
+
for (const argument of [
|
|
50
|
+
'--remote-debugging-address=0.0.0.0',
|
|
51
|
+
'--remote-debugging-port=9222',
|
|
52
|
+
'--user-data-dir=/tmp/other',
|
|
53
|
+
]) {
|
|
54
|
+
assert.throws(
|
|
55
|
+
() =>
|
|
56
|
+
buildRealBrowserArgs({
|
|
57
|
+
userDataDir: '/tmp/browser-commander-dedicated',
|
|
58
|
+
remoteDebuggingPort: 0,
|
|
59
|
+
args: [argument],
|
|
60
|
+
}),
|
|
61
|
+
/managed by launchAndConnectRealBrowser/
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('rejects Chrome default user-data directories', () => {
|
|
67
|
+
for (const profile of ['google-chrome', 'google-chrome-beta']) {
|
|
68
|
+
assert.throws(
|
|
69
|
+
() =>
|
|
70
|
+
assertDedicatedUserDataDir(
|
|
71
|
+
path.join(os.homedir(), '.config', profile),
|
|
72
|
+
{
|
|
73
|
+
platform: 'linux',
|
|
74
|
+
homeDir: os.homedir(),
|
|
75
|
+
environment: {},
|
|
76
|
+
}
|
|
77
|
+
),
|
|
78
|
+
/dedicated userDataDir/
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('spawns, waits, connects, and returns process metadata', async () => {
|
|
84
|
+
temporaryDirectory = await mkdtemp(
|
|
85
|
+
path.join(os.tmpdir(), 'browser-commander-real-browser-test-')
|
|
86
|
+
);
|
|
87
|
+
const calls = [];
|
|
88
|
+
const browserProcess = { exitCode: null, kill: () => calls.push(['kill']) };
|
|
89
|
+
const browser = { id: 'browser' };
|
|
90
|
+
const page = { id: 'page' };
|
|
91
|
+
|
|
92
|
+
const result = await launchAndConnectRealBrowserWithDependencies(
|
|
93
|
+
{
|
|
94
|
+
engine: 'puppeteer',
|
|
95
|
+
channel: 'chrome',
|
|
96
|
+
userDataDir: temporaryDirectory,
|
|
97
|
+
remoteDebuggingPort: 0,
|
|
98
|
+
seedCookies: [{ name: 'SID', value: 'saved' }],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
resolveExecutable: async () => '/opt/google/chrome',
|
|
102
|
+
spawnBrowser: (executablePath, args) => {
|
|
103
|
+
calls.push(['spawn', executablePath, args]);
|
|
104
|
+
return browserProcess;
|
|
105
|
+
},
|
|
106
|
+
waitForEndpoint: async (options) => {
|
|
107
|
+
calls.push(['wait', options.remoteDebuggingPort]);
|
|
108
|
+
return 'http://127.0.0.1:9444';
|
|
109
|
+
},
|
|
110
|
+
connect: async (options) => {
|
|
111
|
+
calls.push(['connect', options]);
|
|
112
|
+
return { browser, page };
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
assert.equal(result.browser, browser);
|
|
118
|
+
assert.equal(result.page, page);
|
|
119
|
+
assert.equal(result.browserProcess, browserProcess);
|
|
120
|
+
assert.equal(result.cdpEndpoint, 'http://127.0.0.1:9444');
|
|
121
|
+
assert.equal(result.executablePath, '/opt/google/chrome');
|
|
122
|
+
assert.equal(result.userDataDir, temporaryDirectory);
|
|
123
|
+
assert.deepEqual(calls[1], ['wait', 0]);
|
|
124
|
+
assert.deepEqual(calls[2], [
|
|
125
|
+
'connect',
|
|
126
|
+
{
|
|
127
|
+
engine: 'puppeteer',
|
|
128
|
+
cdpEndpoint: 'http://127.0.0.1:9444',
|
|
129
|
+
seedCookies: [{ name: 'SID', value: 'saved' }],
|
|
130
|
+
verbose: false,
|
|
131
|
+
},
|
|
132
|
+
]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('terminates the spawned browser when connection fails', async () => {
|
|
136
|
+
temporaryDirectory = await mkdtemp(
|
|
137
|
+
path.join(os.tmpdir(), 'browser-commander-real-browser-test-')
|
|
138
|
+
);
|
|
139
|
+
let killed = false;
|
|
140
|
+
const browserProcess = {
|
|
141
|
+
exitCode: null,
|
|
142
|
+
kill: () => {
|
|
143
|
+
killed = true;
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
await assert.rejects(
|
|
148
|
+
() =>
|
|
149
|
+
launchAndConnectRealBrowserWithDependencies(
|
|
150
|
+
{ userDataDir: temporaryDirectory },
|
|
151
|
+
{
|
|
152
|
+
resolveExecutable: async () => '/opt/google/chrome',
|
|
153
|
+
spawnBrowser: () => browserProcess,
|
|
154
|
+
waitForEndpoint: async () => 'http://127.0.0.1:9222',
|
|
155
|
+
connect: async () => {
|
|
156
|
+
throw new Error('connection failed');
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
),
|
|
160
|
+
/connection failed/
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
assert.equal(killed, true);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, describe, it } from 'node:test';
|
|
5
|
+
import assert from 'node:assert';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
loadStorageState,
|
|
9
|
+
restorePlaywrightStorageState,
|
|
10
|
+
restorePuppeteerStorageState,
|
|
11
|
+
saveStorageState,
|
|
12
|
+
} from '../../../src/browser/storage-state.js';
|
|
13
|
+
import { saveStorageState as publicSaveStorageState } from '../../../src/index.js';
|
|
14
|
+
|
|
15
|
+
describe('browser storage state', () => {
|
|
16
|
+
let temporaryDirectory;
|
|
17
|
+
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
if (temporaryDirectory) {
|
|
20
|
+
await rm(temporaryDirectory, { recursive: true, force: true });
|
|
21
|
+
temporaryDirectory = undefined;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('loads storage state from a path and accepts an object unchanged', async () => {
|
|
26
|
+
temporaryDirectory = await mkdtemp(
|
|
27
|
+
path.join(os.tmpdir(), 'browser-commander-storage-state-')
|
|
28
|
+
);
|
|
29
|
+
const filePath = path.join(temporaryDirectory, 'state.json');
|
|
30
|
+
const state = {
|
|
31
|
+
cookies: [{ name: 'session', value: 'saved', domain: '.example.com' }],
|
|
32
|
+
origins: [],
|
|
33
|
+
};
|
|
34
|
+
await writeFile(filePath, JSON.stringify(state));
|
|
35
|
+
|
|
36
|
+
assert.deepEqual(await loadStorageState(filePath), state);
|
|
37
|
+
assert.equal(await loadStorageState(state), state);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('rejects invalid storage state input', async () => {
|
|
41
|
+
await assert.rejects(
|
|
42
|
+
() => loadStorageState(null),
|
|
43
|
+
/storageState must be a file path or an object/
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('exports saveStorageState from the package API', () => {
|
|
48
|
+
assert.equal(publicSaveStorageState, saveStorageState);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('restores Puppeteer cookies and localStorage before navigation', async () => {
|
|
52
|
+
const calls = [];
|
|
53
|
+
const state = {
|
|
54
|
+
cookies: [{ name: 'session', value: 'saved', domain: '.example.com' }],
|
|
55
|
+
origins: [
|
|
56
|
+
{
|
|
57
|
+
origin: 'https://example.com',
|
|
58
|
+
localStorage: [{ name: 'theme', value: 'dark' }],
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
const page = {
|
|
63
|
+
setCookie: async (...cookies) => calls.push(['cookies', cookies]),
|
|
64
|
+
evaluateOnNewDocument: async (fn, value) =>
|
|
65
|
+
calls.push(['new-document', fn, value]),
|
|
66
|
+
evaluate: async (fn, value) => calls.push(['current-page', fn, value]),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
await restorePuppeteerStorageState({ page, storageState: state });
|
|
70
|
+
|
|
71
|
+
assert.deepEqual(calls[0], ['cookies', state.cookies]);
|
|
72
|
+
assert.equal(calls[1][0], 'new-document');
|
|
73
|
+
assert.equal(calls[1][2], state.origins);
|
|
74
|
+
assert.equal(calls[2][0], 'current-page');
|
|
75
|
+
assert.equal(calls[2][2], state.origins);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('restores Playwright cookies and localStorage before navigation', async () => {
|
|
79
|
+
const calls = [];
|
|
80
|
+
const state = {
|
|
81
|
+
cookies: [{ name: 'session', value: 'saved', domain: '.example.com' }],
|
|
82
|
+
origins: [
|
|
83
|
+
{
|
|
84
|
+
origin: 'https://example.com',
|
|
85
|
+
localStorage: [{ name: 'theme', value: 'dark' }],
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
const page = {
|
|
90
|
+
evaluate: async (fn, value) => calls.push(['current-page', fn, value]),
|
|
91
|
+
};
|
|
92
|
+
const context = {
|
|
93
|
+
addCookies: async (cookies) => calls.push(['cookies', cookies]),
|
|
94
|
+
addInitScript: async (fn, value) =>
|
|
95
|
+
calls.push(['new-document', fn, value]),
|
|
96
|
+
pages: () => [page],
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
await restorePlaywrightStorageState({ context, storageState: state });
|
|
100
|
+
|
|
101
|
+
assert.deepEqual(calls[0], ['cookies', state.cookies]);
|
|
102
|
+
assert.equal(calls[1][0], 'new-document');
|
|
103
|
+
assert.equal(calls[1][2], state.origins);
|
|
104
|
+
assert.equal(calls[2][0], 'current-page');
|
|
105
|
+
assert.equal(calls[2][2], state.origins);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('delegates Playwright saves to the page context', async () => {
|
|
109
|
+
const savedState = { cookies: [], origins: [] };
|
|
110
|
+
const calls = [];
|
|
111
|
+
const page = {
|
|
112
|
+
context: () => ({
|
|
113
|
+
storageState: async (options) => {
|
|
114
|
+
calls.push(options);
|
|
115
|
+
return savedState;
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
assert.equal(await saveStorageState(page, '/tmp/state.json'), savedState);
|
|
121
|
+
assert.deepEqual(calls, [{ path: '/tmp/state.json' }]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('saves Puppeteer cookies and current-origin localStorage', async () => {
|
|
125
|
+
temporaryDirectory = await mkdtemp(
|
|
126
|
+
path.join(os.tmpdir(), 'browser-commander-storage-state-')
|
|
127
|
+
);
|
|
128
|
+
const filePath = path.join(temporaryDirectory, 'state.json');
|
|
129
|
+
const cookies = [
|
|
130
|
+
{ name: 'session', value: 'saved', domain: '.example.com' },
|
|
131
|
+
];
|
|
132
|
+
const localStorage = [{ name: 'theme', value: 'dark' }];
|
|
133
|
+
const page = {
|
|
134
|
+
browserContext: () => ({}),
|
|
135
|
+
cookies: async () => cookies,
|
|
136
|
+
url: () => 'https://example.com/inbox',
|
|
137
|
+
evaluate: async () => localStorage,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const state = await saveStorageState(page, filePath);
|
|
141
|
+
|
|
142
|
+
assert.deepEqual(state, {
|
|
143
|
+
cookies,
|
|
144
|
+
origins: [{ origin: 'https://example.com', localStorage }],
|
|
145
|
+
});
|
|
146
|
+
assert.deepEqual(JSON.parse(await readFile(filePath, 'utf8')), state);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { createNavigationManager } from '../../../src/core/navigation-manager.js';
|
|
4
|
+
import {
|
|
5
|
+
createMockPlaywrightPage,
|
|
6
|
+
createMockLogger,
|
|
7
|
+
} from '../../helpers/mocks.js';
|
|
8
|
+
|
|
9
|
+
describe('navigation manager', () => {
|
|
10
|
+
describe('setContent', () => {
|
|
11
|
+
it('should apply the full managed navigation lifecycle', async () => {
|
|
12
|
+
const page = createMockPlaywrightPage();
|
|
13
|
+
const log = createMockLogger();
|
|
14
|
+
const calls = [];
|
|
15
|
+
page.setContent = async (...args) => calls.push(['setContent', ...args]);
|
|
16
|
+
|
|
17
|
+
const manager = createNavigationManager({
|
|
18
|
+
page,
|
|
19
|
+
engine: 'playwright',
|
|
20
|
+
log,
|
|
21
|
+
});
|
|
22
|
+
manager.configure({ redirectStabilizationTime: 0 });
|
|
23
|
+
manager.onSessionCleanup(() => calls.push(['cleanup']));
|
|
24
|
+
manager.on('onNavigationStart', () => calls.push(['navigationStart']));
|
|
25
|
+
manager.on('onPageReady', () => calls.push(['pageReady']));
|
|
26
|
+
|
|
27
|
+
const loaded = await manager.setContent({
|
|
28
|
+
html: '<h1>Hi</h1>',
|
|
29
|
+
waitUntil: 'networkidle',
|
|
30
|
+
timeout: 5000,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
assert.strictEqual(loaded, true);
|
|
34
|
+
assert.strictEqual(manager.getSessionId(), 1);
|
|
35
|
+
assert.strictEqual(manager.isNavigating(), false);
|
|
36
|
+
assert.deepStrictEqual(calls, [
|
|
37
|
+
['cleanup'],
|
|
38
|
+
['navigationStart'],
|
|
39
|
+
[
|
|
40
|
+
'setContent',
|
|
41
|
+
'<h1>Hi</h1>',
|
|
42
|
+
{ waitUntil: 'networkidle', timeout: 5000 },
|
|
43
|
+
],
|
|
44
|
+
['pageReady'],
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should require html while accepting an empty string', async () => {
|
|
49
|
+
const page = createMockPlaywrightPage();
|
|
50
|
+
const log = createMockLogger();
|
|
51
|
+
const manager = createNavigationManager({
|
|
52
|
+
page,
|
|
53
|
+
engine: 'playwright',
|
|
54
|
+
log,
|
|
55
|
+
});
|
|
56
|
+
manager.configure({ redirectStabilizationTime: 0 });
|
|
57
|
+
|
|
58
|
+
await assert.rejects(() => manager.setContent(), /html is required/);
|
|
59
|
+
await assert.doesNotReject(() => manager.setContent({ html: '' }));
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|