browser-commander 0.10.0 → 0.12.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 +58 -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 +29 -1
- package/src/browser/launcher.js +17 -0
- package/src/browser/navigation.js +58 -0
- package/src/browser/storage-state.js +133 -0
- package/src/core/engine-adapter.js +33 -0
- package/src/core/navigation-manager.js +57 -24
- package/src/elements/content.js +55 -0
- 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 +10 -0
- package/tests/unit/bindings.test.js +68 -0
- package/tests/unit/browser/navigation.test.js +61 -0
- package/tests/unit/browser/storage-state.test.js +148 -0
- package/tests/unit/core/navigation-manager.test.js +62 -0
- package/tests/unit/elements/content.test.js +102 -0
- package/tests/unit/scripts/ci-workflow-policy.test.js +73 -0
- package/tests/unit/scripts/npm-release-helpers.test.js +20 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add `commander.setContent()` for safely loading in-memory HTML through the managed navigation lifecycle.
|
|
8
|
+
|
|
9
|
+
Add portable cookie and localStorage restoration through `launchBrowser({ storageState })` and the `saveStorageState()` helper for Playwright and Puppeteer.
|
|
10
|
+
|
|
11
|
+
Avoid intermittent Node 24 test-runner transport failures on macOS by running test files without child-process isolation.
|
|
12
|
+
|
|
13
|
+
## 0.11.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- 8ef86eb: Add page HTML, rendered text, and positional page evaluation APIs to commander instances.
|
|
18
|
+
|
|
3
19
|
## 0.10.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -82,6 +82,12 @@ commander.pageTrigger({
|
|
|
82
82
|
// 4. Navigate - action auto-starts when page is ready
|
|
83
83
|
await commander.goto({ url: 'https://example.com' });
|
|
84
84
|
|
|
85
|
+
// Or safely replace the document with in-memory HTML
|
|
86
|
+
await commander.setContent({
|
|
87
|
+
html: '<h1>Hi</h1>',
|
|
88
|
+
waitUntil: 'networkidle',
|
|
89
|
+
});
|
|
90
|
+
|
|
85
91
|
// 5. Cleanup
|
|
86
92
|
await commander.destroy();
|
|
87
93
|
await browser.close();
|
|
@@ -100,6 +106,22 @@ const { browser, page } = await launchBrowser({
|
|
|
100
106
|
});
|
|
101
107
|
```
|
|
102
108
|
|
|
109
|
+
Reuse a saved authenticated session by passing Playwright-compatible storage
|
|
110
|
+
state as a JSON file path or object. Cookies and localStorage are restored for
|
|
111
|
+
both engines:
|
|
112
|
+
|
|
113
|
+
```javascript
|
|
114
|
+
import { launchBrowser, saveStorageState } from 'browser-commander';
|
|
115
|
+
|
|
116
|
+
const { browser, page } = await launchBrowser({
|
|
117
|
+
engine: 'playwright',
|
|
118
|
+
storageState: './gmail-state.json',
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Save the current session for a later launch.
|
|
122
|
+
await saveStorageState(page, './gmail-state.json');
|
|
123
|
+
```
|
|
124
|
+
|
|
103
125
|
## Browser Commander Tests
|
|
104
126
|
|
|
105
127
|
`browser-commander/tests` adds browser fixtures and scheduling helpers on top of
|
|
@@ -271,11 +293,26 @@ const { browser, page } = await launchBrowser({
|
|
|
271
293
|
slowMo: 150, // Slow down operations (ms)
|
|
272
294
|
verbose: false, // Enable debug logging
|
|
273
295
|
args: ['--no-sandbox', '--disable-setuid-sandbox'], // Custom Chrome args to append
|
|
296
|
+
storageState: './session-state.json', // Saved cookies and localStorage, as a path or object
|
|
274
297
|
});
|
|
275
298
|
```
|
|
276
299
|
|
|
277
300
|
The `args` option allows passing custom Chrome arguments, which is useful for headless server environments (Docker, CI/CD) that require flags like `--no-sandbox`.
|
|
278
301
|
|
|
302
|
+
The `storageState` option accepts a Playwright-compatible JSON path or object.
|
|
303
|
+
Each engine restores its cookies and origin-specific localStorage before
|
|
304
|
+
navigation, including when Playwright uses a persistent context.
|
|
305
|
+
|
|
306
|
+
### saveStorageState(page, filePath)
|
|
307
|
+
|
|
308
|
+
Save the current cookies and localStorage in Playwright's portable storage
|
|
309
|
+
state format. The helper supports pages from either engine and also returns the
|
|
310
|
+
saved object:
|
|
311
|
+
|
|
312
|
+
```javascript
|
|
313
|
+
const state = await saveStorageState(page, './session-state.json');
|
|
314
|
+
```
|
|
315
|
+
|
|
279
316
|
The `colorScheme` option allows setting the initial color scheme (`'light'`, `'dark'`, or `'no-preference'`) at launch time for screenshot services and testing tools:
|
|
280
317
|
|
|
281
318
|
```javascript
|
|
@@ -360,6 +397,27 @@ await commander.fillTextArea({
|
|
|
360
397
|
});
|
|
361
398
|
```
|
|
362
399
|
|
|
400
|
+
### Page Content Extraction
|
|
401
|
+
|
|
402
|
+
Read the current page without reaching through to the engine-specific page API:
|
|
403
|
+
|
|
404
|
+
```javascript
|
|
405
|
+
const html = await commander.content();
|
|
406
|
+
const pageText = await commander.innerText(); // document.body
|
|
407
|
+
const mainText = await commander.innerText('main');
|
|
408
|
+
const title = await commander.evaluate(() => document.title);
|
|
409
|
+
const total = await commander.evaluate((a, b) => a + b, 2, 3);
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
The existing options-object form of `evaluate` remains supported:
|
|
413
|
+
|
|
414
|
+
```javascript
|
|
415
|
+
const total = await commander.evaluate({
|
|
416
|
+
fn: (a, b) => a + b,
|
|
417
|
+
args: [2, 3],
|
|
418
|
+
});
|
|
419
|
+
```
|
|
420
|
+
|
|
363
421
|
### Keyboard Interactions
|
|
364
422
|
|
|
365
423
|
```javascript
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Example: PDF generation via browser-commander
|
|
3
3
|
*
|
|
4
|
-
* This example shows how to
|
|
5
|
-
*
|
|
4
|
+
* This example shows how to load in-memory HTML and generate a PDF through the
|
|
5
|
+
* unified commander API.
|
|
6
6
|
*
|
|
7
7
|
* Supports both Playwright and Puppeteer engines.
|
|
8
8
|
*
|
|
@@ -45,8 +45,11 @@ async function generatePdf() {
|
|
|
45
45
|
try {
|
|
46
46
|
const commander = makeBrowserCommander({ page, verbose: false });
|
|
47
47
|
|
|
48
|
-
//
|
|
49
|
-
await
|
|
48
|
+
// Load HTML assembled in memory using the managed navigation lifecycle.
|
|
49
|
+
await commander.setContent({
|
|
50
|
+
html: '<main><h1>Browser Commander</h1><p>Generated in memory.</p></main>',
|
|
51
|
+
waitUntil: 'load',
|
|
52
|
+
});
|
|
50
53
|
|
|
51
54
|
// Generate PDF using the unified API (no workarounds needed!)
|
|
52
55
|
const pdfBuffer = await commander.pdf({
|
package/package.json
CHANGED
package/scripts/run-tests.mjs
CHANGED
|
@@ -137,9 +137,27 @@ export function resolveTestFiles(targets, cwd = process.cwd()) {
|
|
|
137
137
|
return uniqueFiles.map((file) => relative(cwd, file));
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
function supportsTestIsolation(nodeVersion) {
|
|
141
|
+
const majorVersion = Number.parseInt(nodeVersion.split('.')[0], 10);
|
|
142
|
+
return Number.isInteger(majorVersion) && majorVersion >= 24;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function buildNodeTestArgs(
|
|
146
|
+
options,
|
|
147
|
+
testFiles,
|
|
148
|
+
nodeVersion = process.versions.node
|
|
149
|
+
) {
|
|
141
150
|
const args = ['--test'];
|
|
142
151
|
|
|
152
|
+
// Node's default process isolation serializes each test file's results over
|
|
153
|
+
// a child-process channel. Node 24 on macOS can intermittently corrupt that
|
|
154
|
+
// stream (ERR_TEST_FAILURE: "Unable to deserialize cloned data"). Running
|
|
155
|
+
// supported runtimes in-process removes that transport while keeping the
|
|
156
|
+
// Node 20-compatible fallback for consumers on the minimum engine version.
|
|
157
|
+
if (supportsTestIsolation(nodeVersion)) {
|
|
158
|
+
args.push('--test-isolation=none');
|
|
159
|
+
}
|
|
160
|
+
|
|
143
161
|
if (options.coverage) {
|
|
144
162
|
args.push('--experimental-test-coverage');
|
|
145
163
|
}
|
package/src/README.md
CHANGED
|
@@ -417,6 +417,19 @@ await commander.goto({
|
|
|
417
417
|
});
|
|
418
418
|
```
|
|
419
419
|
|
|
420
|
+
### commander.setContent(options)
|
|
421
|
+
|
|
422
|
+
Load an in-memory HTML document while preserving the same page-trigger cleanup
|
|
423
|
+
and readiness guarantees as `goto()`:
|
|
424
|
+
|
|
425
|
+
```javascript
|
|
426
|
+
await commander.setContent({
|
|
427
|
+
html: '<h1>Hi</h1>',
|
|
428
|
+
waitUntil: 'networkidle', // Playwright/Puppeteer option
|
|
429
|
+
timeout: 60000,
|
|
430
|
+
});
|
|
431
|
+
```
|
|
432
|
+
|
|
420
433
|
### commander.clickButton(options)
|
|
421
434
|
|
|
422
435
|
```javascript
|
package/src/bindings.js
CHANGED
|
@@ -8,6 +8,7 @@ import { getUrl, unfocusAddressBar } from './utilities/url.js';
|
|
|
8
8
|
import {
|
|
9
9
|
waitForUrlStabilization,
|
|
10
10
|
goto,
|
|
11
|
+
setContent,
|
|
11
12
|
waitForNavigation,
|
|
12
13
|
waitForPageReady,
|
|
13
14
|
waitAfterAction,
|
|
@@ -30,6 +31,8 @@ import {
|
|
|
30
31
|
} from './elements/selectors.js';
|
|
31
32
|
import { isVisible, isEnabled, count } from './elements/visibility.js';
|
|
32
33
|
import {
|
|
34
|
+
content,
|
|
35
|
+
innerText,
|
|
33
36
|
textContent,
|
|
34
37
|
inputValue,
|
|
35
38
|
getAttribute,
|
|
@@ -85,7 +88,13 @@ export function createBoundFunctions(options = {}) {
|
|
|
85
88
|
: null;
|
|
86
89
|
return wait({ ...opts, log, abortSignal: opts.abortSignal || abortSignal });
|
|
87
90
|
};
|
|
88
|
-
const evaluateBound = (
|
|
91
|
+
const evaluateBound = (fnOrOptions, ...args) => {
|
|
92
|
+
const opts =
|
|
93
|
+
typeof fnOrOptions === 'function'
|
|
94
|
+
? { fn: fnOrOptions, args }
|
|
95
|
+
: fnOrOptions;
|
|
96
|
+
return evaluate({ ...opts, page, engine });
|
|
97
|
+
};
|
|
89
98
|
const safeEvaluateBound = (opts) => safeEvaluate({ ...opts, page, engine });
|
|
90
99
|
const getUrlBound = () => getUrl({ page });
|
|
91
100
|
const unfocusAddressBarBound = (opts = {}) =>
|
|
@@ -107,6 +116,12 @@ export function createBoundFunctions(options = {}) {
|
|
|
107
116
|
waitForUrlStabilization: waitForUrlStabilizationBound,
|
|
108
117
|
navigationManager,
|
|
109
118
|
});
|
|
119
|
+
const setContentBound = (opts) =>
|
|
120
|
+
setContent({
|
|
121
|
+
...opts,
|
|
122
|
+
page,
|
|
123
|
+
navigationManager,
|
|
124
|
+
});
|
|
110
125
|
const waitForNavigationBound = (opts) =>
|
|
111
126
|
waitForNavigation({
|
|
112
127
|
...opts,
|
|
@@ -158,6 +173,16 @@ export function createBoundFunctions(options = {}) {
|
|
|
158
173
|
const countBound = (opts) => count({ ...opts, page, engine });
|
|
159
174
|
|
|
160
175
|
// Bound content
|
|
176
|
+
const contentBound = (opts = {}) => content({ ...opts, page, engine });
|
|
177
|
+
const innerTextBound = (selectorOrOptions = 'body') => {
|
|
178
|
+
const opts =
|
|
179
|
+
typeof selectorOrOptions === 'object' &&
|
|
180
|
+
selectorOrOptions !== null &&
|
|
181
|
+
Object.hasOwn(selectorOrOptions, 'selector')
|
|
182
|
+
? selectorOrOptions
|
|
183
|
+
: { selector: selectorOrOptions };
|
|
184
|
+
return innerText({ ...opts, page, engine });
|
|
185
|
+
};
|
|
161
186
|
const textContentBound = (opts) => textContent({ ...opts, page, engine });
|
|
162
187
|
const inputValueBound = (opts) => inputValue({ ...opts, page, engine });
|
|
163
188
|
const getAttributeBound = (opts) => getAttribute({ ...opts, page, engine });
|
|
@@ -294,6 +319,7 @@ export function createBoundFunctions(options = {}) {
|
|
|
294
319
|
querySelectorAll: querySelectorAllBound,
|
|
295
320
|
waitForUrlStabilization: waitForUrlStabilizationBound,
|
|
296
321
|
goto: gotoBound,
|
|
322
|
+
setContent: setContentBound,
|
|
297
323
|
getUrl: getUrlBound,
|
|
298
324
|
waitForNavigation: waitForNavigationBound,
|
|
299
325
|
waitForPageReady: waitForPageReadyBound,
|
|
@@ -302,6 +328,8 @@ export function createBoundFunctions(options = {}) {
|
|
|
302
328
|
isVisible: isVisibleWrapped,
|
|
303
329
|
isEnabled: isEnabledWrapped,
|
|
304
330
|
count: countBound,
|
|
331
|
+
content: contentBound,
|
|
332
|
+
innerText: innerTextBound,
|
|
305
333
|
textContent: textContentWrapped,
|
|
306
334
|
inputValue: inputValueWrapped,
|
|
307
335
|
locator: locatorBound,
|
package/src/browser/launcher.js
CHANGED
|
@@ -7,6 +7,11 @@ import {
|
|
|
7
7
|
buildPlaywrightLaunchOptions,
|
|
8
8
|
buildPuppeteerLaunchOptions,
|
|
9
9
|
} from './launch-options.js';
|
|
10
|
+
import {
|
|
11
|
+
loadStorageState,
|
|
12
|
+
restorePlaywrightStorageState,
|
|
13
|
+
restorePuppeteerStorageState,
|
|
14
|
+
} from './storage-state.js';
|
|
10
15
|
|
|
11
16
|
/**
|
|
12
17
|
* Launch browser with default configuration
|
|
@@ -20,6 +25,7 @@ import {
|
|
|
20
25
|
* @param {string|null} [options.colorScheme] - Emulate color scheme: 'light', 'dark', 'no-preference', or null to reset
|
|
21
26
|
* @param {string} [options.channel] - Installed browser channel, such as 'chrome', 'chrome-beta', or 'msedge'
|
|
22
27
|
* @param {string} [options.executablePath] - Explicit path to a Chrome or Chromium executable
|
|
28
|
+
* @param {string|Object} [options.storageState] - Playwright-compatible storage state path or object
|
|
23
29
|
* @returns {Promise<Object>} - Object with browser and page
|
|
24
30
|
*/
|
|
25
31
|
export async function launchBrowser(options = {}) {
|
|
@@ -33,6 +39,7 @@ export async function launchBrowser(options = {}) {
|
|
|
33
39
|
colorScheme,
|
|
34
40
|
channel,
|
|
35
41
|
executablePath,
|
|
42
|
+
storageState,
|
|
36
43
|
} = options;
|
|
37
44
|
|
|
38
45
|
// Combine default CHROME_ARGS with custom args
|
|
@@ -44,6 +51,8 @@ export async function launchBrowser(options = {}) {
|
|
|
44
51
|
);
|
|
45
52
|
}
|
|
46
53
|
|
|
54
|
+
const resolvedStorageState = await loadStorageState(storageState);
|
|
55
|
+
|
|
47
56
|
// Set environment variables to suppress warnings
|
|
48
57
|
process.env.GOOGLE_API_KEY = 'no';
|
|
49
58
|
process.env.GOOGLE_DEFAULT_CLIENT_ID = 'no';
|
|
@@ -74,6 +83,10 @@ export async function launchBrowser(options = {}) {
|
|
|
74
83
|
contextOptions
|
|
75
84
|
);
|
|
76
85
|
page = browser.pages()[0];
|
|
86
|
+
await restorePlaywrightStorageState({
|
|
87
|
+
context: browser,
|
|
88
|
+
storageState: resolvedStorageState,
|
|
89
|
+
});
|
|
77
90
|
} else {
|
|
78
91
|
const puppeteer = await import('puppeteer');
|
|
79
92
|
browser = await puppeteer.default.launch(
|
|
@@ -87,6 +100,10 @@ export async function launchBrowser(options = {}) {
|
|
|
87
100
|
);
|
|
88
101
|
const pages = await browser.pages();
|
|
89
102
|
page = pages[0];
|
|
103
|
+
await restorePuppeteerStorageState({
|
|
104
|
+
page,
|
|
105
|
+
storageState: resolvedStorageState,
|
|
106
|
+
});
|
|
90
107
|
}
|
|
91
108
|
|
|
92
109
|
if (verbose) {
|
|
@@ -348,6 +348,64 @@ export async function goto(options = {}) {
|
|
|
348
348
|
}
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Replace the current document with an in-memory HTML string.
|
|
353
|
+
* When NavigationManager is available, this uses the same managed lifecycle as
|
|
354
|
+
* goto so active page triggers are stopped before the document is replaced.
|
|
355
|
+
*
|
|
356
|
+
* @param {Object} options - Configuration options
|
|
357
|
+
* @param {Object} options.page - Browser page object
|
|
358
|
+
* @param {Object} options.navigationManager - NavigationManager instance (preferred)
|
|
359
|
+
* @param {string} options.html - HTML to load
|
|
360
|
+
* @param {string} options.waitUntil - Wait until condition (default: 'load')
|
|
361
|
+
* @param {number} options.timeout - Content loading timeout in ms (default: 60000)
|
|
362
|
+
* @returns {Promise<Object>} Content loading result
|
|
363
|
+
*/
|
|
364
|
+
export async function setContent(options = {}) {
|
|
365
|
+
const {
|
|
366
|
+
page,
|
|
367
|
+
navigationManager,
|
|
368
|
+
html,
|
|
369
|
+
waitUntil = 'load',
|
|
370
|
+
timeout = 60000,
|
|
371
|
+
} = options;
|
|
372
|
+
|
|
373
|
+
if (typeof html !== 'string') {
|
|
374
|
+
throw new Error('html is required in options');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
let loaded;
|
|
379
|
+
if (navigationManager) {
|
|
380
|
+
loaded = await navigationManager.setContent({
|
|
381
|
+
html,
|
|
382
|
+
waitUntil,
|
|
383
|
+
timeout,
|
|
384
|
+
});
|
|
385
|
+
} else {
|
|
386
|
+
await page.setContent(html, { waitUntil, timeout });
|
|
387
|
+
loaded = true;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return loaded
|
|
391
|
+
? { loaded: true, actualUrl: page.url() }
|
|
392
|
+
: {
|
|
393
|
+
loaded: false,
|
|
394
|
+
actualUrl: page.url(),
|
|
395
|
+
reason: 'content loading stopped/interrupted',
|
|
396
|
+
};
|
|
397
|
+
} catch (error) {
|
|
398
|
+
if (isNavigationError(error) || isActionStoppedError(error)) {
|
|
399
|
+
return {
|
|
400
|
+
loaded: false,
|
|
401
|
+
actualUrl: page.url(),
|
|
402
|
+
reason: 'content loading stopped/interrupted',
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
351
409
|
/**
|
|
352
410
|
* Wait for navigation
|
|
353
411
|
* @param {Object} options - Configuration options
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Load a Playwright-compatible storage state from a JSON path or object.
|
|
5
|
+
* @param {string|Object|undefined} storageState - State path or parsed state
|
|
6
|
+
* @returns {Promise<Object|undefined>} Parsed state
|
|
7
|
+
*/
|
|
8
|
+
export async function loadStorageState(storageState) {
|
|
9
|
+
if (storageState === undefined) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
if (typeof storageState === 'string') {
|
|
13
|
+
return JSON.parse(await readFile(storageState, 'utf8'));
|
|
14
|
+
}
|
|
15
|
+
if (storageState && typeof storageState === 'object') {
|
|
16
|
+
return storageState;
|
|
17
|
+
}
|
|
18
|
+
throw new TypeError('storageState must be a file path or an object');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function restoreOriginLocalStorage(origins) {
|
|
22
|
+
const originState = origins.find(
|
|
23
|
+
({ origin }) => origin === globalThis.location.origin
|
|
24
|
+
);
|
|
25
|
+
if (!originState) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
for (const { name, value } of originState.localStorage ?? []) {
|
|
29
|
+
globalThis.localStorage.setItem(name, value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Apply Playwright-compatible storage state to a Puppeteer page.
|
|
35
|
+
* @param {Object} options - Restore options
|
|
36
|
+
* @param {Object} options.page - Puppeteer page
|
|
37
|
+
* @param {Object|undefined} options.storageState - Parsed storage state
|
|
38
|
+
* @returns {Promise<void>}
|
|
39
|
+
*/
|
|
40
|
+
export async function restorePuppeteerStorageState({ page, storageState }) {
|
|
41
|
+
if (!storageState) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const cookies = storageState.cookies ?? [];
|
|
46
|
+
if (cookies.length > 0) {
|
|
47
|
+
await page.setCookie(...cookies);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const origins = storageState.origins ?? [];
|
|
51
|
+
if (origins.length > 0) {
|
|
52
|
+
await page.evaluateOnNewDocument(restoreOriginLocalStorage, origins);
|
|
53
|
+
await page.evaluate(restoreOriginLocalStorage, origins);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Apply storage state to a Playwright persistent browser context.
|
|
59
|
+
* @param {Object} options - Restore options
|
|
60
|
+
* @param {Object} options.context - Playwright browser context
|
|
61
|
+
* @param {Object|undefined} options.storageState - Parsed storage state
|
|
62
|
+
* @returns {Promise<void>}
|
|
63
|
+
*/
|
|
64
|
+
export async function restorePlaywrightStorageState({ context, storageState }) {
|
|
65
|
+
if (!storageState) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const cookies = storageState.cookies ?? [];
|
|
70
|
+
if (cookies.length > 0) {
|
|
71
|
+
await context.addCookies(cookies);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const origins = storageState.origins ?? [];
|
|
75
|
+
if (origins.length > 0) {
|
|
76
|
+
await context.addInitScript(restoreOriginLocalStorage, origins);
|
|
77
|
+
await Promise.all(
|
|
78
|
+
context
|
|
79
|
+
.pages()
|
|
80
|
+
.map((page) => page.evaluate(restoreOriginLocalStorage, origins))
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readCurrentLocalStorage() {
|
|
86
|
+
return Array.from({ length: globalThis.localStorage.length }, (_, index) => {
|
|
87
|
+
const name = globalThis.localStorage.key(index);
|
|
88
|
+
return { name, value: globalThis.localStorage.getItem(name) };
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function collectPuppeteerStorageState(page) {
|
|
93
|
+
const cookies = await page.cookies();
|
|
94
|
+
const pageUrl = page.url();
|
|
95
|
+
const origin = new URL(pageUrl).origin;
|
|
96
|
+
const origins = [];
|
|
97
|
+
|
|
98
|
+
if (origin !== 'null') {
|
|
99
|
+
origins.push({
|
|
100
|
+
origin,
|
|
101
|
+
localStorage: await page.evaluate(readCurrentLocalStorage),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { cookies, origins };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Save a page's cookies and localStorage in Playwright's storage-state format.
|
|
110
|
+
* @param {Object} page - Playwright or Puppeteer page
|
|
111
|
+
* @param {string} [filePath] - Optional JSON output path
|
|
112
|
+
* @returns {Promise<Object>} Saved storage state
|
|
113
|
+
*/
|
|
114
|
+
export async function saveStorageState(page, filePath) {
|
|
115
|
+
const context = typeof page.context === 'function' ? page.context() : null;
|
|
116
|
+
if (typeof context?.storageState === 'function') {
|
|
117
|
+
return context.storageState(filePath ? { path: filePath } : undefined);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (
|
|
121
|
+
typeof page.cookies !== 'function' ||
|
|
122
|
+
typeof page.evaluate !== 'function' ||
|
|
123
|
+
typeof page.url !== 'function'
|
|
124
|
+
) {
|
|
125
|
+
throw new TypeError('page must be a Playwright or Puppeteer page');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const storageState = await collectPuppeteerStorageState(page);
|
|
129
|
+
if (filePath) {
|
|
130
|
+
await writeFile(filePath, `${JSON.stringify(storageState, null, 2)}\n`);
|
|
131
|
+
}
|
|
132
|
+
return storageState;
|
|
133
|
+
}
|
|
@@ -120,6 +120,15 @@ export class EngineAdapter {
|
|
|
120
120
|
return notImplemented('getTextContent');
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Get rendered text from an element
|
|
125
|
+
* @param {Object} locatorOrElement - Locator or element
|
|
126
|
+
* @returns {Promise<string>} - Rendered text
|
|
127
|
+
*/
|
|
128
|
+
getInnerText(_locatorOrElement) {
|
|
129
|
+
return notImplemented('getInnerText');
|
|
130
|
+
}
|
|
131
|
+
|
|
123
132
|
/**
|
|
124
133
|
* Get input value
|
|
125
134
|
* @param {Object} locatorOrElement - Locator or element
|
|
@@ -236,6 +245,14 @@ export class EngineAdapter {
|
|
|
236
245
|
return notImplemented('evaluateOnPage');
|
|
237
246
|
}
|
|
238
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Get the full HTML of the current page
|
|
250
|
+
* @returns {Promise<string>} - Serialized page HTML
|
|
251
|
+
*/
|
|
252
|
+
getPageContent() {
|
|
253
|
+
return notImplemented('getPageContent');
|
|
254
|
+
}
|
|
255
|
+
|
|
239
256
|
/**
|
|
240
257
|
* Get the main frame
|
|
241
258
|
* @returns {Object} - Main frame
|
|
@@ -325,6 +342,10 @@ export class PlaywrightAdapter extends EngineAdapter {
|
|
|
325
342
|
return await locatorOrElement.textContent();
|
|
326
343
|
}
|
|
327
344
|
|
|
345
|
+
async getInnerText(locatorOrElement) {
|
|
346
|
+
return await locatorOrElement.innerText();
|
|
347
|
+
}
|
|
348
|
+
|
|
328
349
|
async getInputValue(locatorOrElement) {
|
|
329
350
|
return await locatorOrElement.inputValue();
|
|
330
351
|
}
|
|
@@ -401,6 +422,10 @@ export class PlaywrightAdapter extends EngineAdapter {
|
|
|
401
422
|
}
|
|
402
423
|
}
|
|
403
424
|
|
|
425
|
+
async getPageContent() {
|
|
426
|
+
return await this.page.content();
|
|
427
|
+
}
|
|
428
|
+
|
|
404
429
|
getMainFrame() {
|
|
405
430
|
return this.page.mainFrame();
|
|
406
431
|
}
|
|
@@ -469,6 +494,10 @@ export class PuppeteerAdapter extends EngineAdapter {
|
|
|
469
494
|
return await this.page.evaluate((el) => el.textContent, locatorOrElement);
|
|
470
495
|
}
|
|
471
496
|
|
|
497
|
+
async getInnerText(locatorOrElement) {
|
|
498
|
+
return await this.page.evaluate((el) => el.innerText, locatorOrElement);
|
|
499
|
+
}
|
|
500
|
+
|
|
472
501
|
async getInputValue(locatorOrElement) {
|
|
473
502
|
return await this.page.evaluate((el) => el.value, locatorOrElement);
|
|
474
503
|
}
|
|
@@ -541,6 +570,10 @@ export class PuppeteerAdapter extends EngineAdapter {
|
|
|
541
570
|
return await this.page.evaluate(fn, ...args);
|
|
542
571
|
}
|
|
543
572
|
|
|
573
|
+
async getPageContent() {
|
|
574
|
+
return await this.page.content();
|
|
575
|
+
}
|
|
576
|
+
|
|
544
577
|
getMainFrame() {
|
|
545
578
|
return this.page.mainFrame();
|
|
546
579
|
}
|