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.
@@ -11,6 +11,47 @@
11
11
  import { isNavigationError } from './navigation-safety.js';
12
12
  import { TIMING } from './constants.js';
13
13
 
14
+ async function setManagedContent(options = {}) {
15
+ const {
16
+ page,
17
+ log,
18
+ html,
19
+ waitUntil = 'load',
20
+ timeout = 60000,
21
+ currentUrl,
22
+ triggerNavigationStart,
23
+ updateCurrentUrl,
24
+ waitForPageReady,
25
+ completeNavigation,
26
+ } = options;
27
+
28
+ if (typeof html !== 'string') {
29
+ throw new Error('html is required in options');
30
+ }
31
+
32
+ log.debug(() => '🚀 Loading in-memory HTML content');
33
+
34
+ try {
35
+ // Treat document replacement as a controlled navigation so active page
36
+ // triggers stop before the old document is discarded.
37
+ await triggerNavigationStart({ url: currentUrl, isExternal: false });
38
+ await page.setContent(html, { waitUntil, timeout });
39
+
40
+ // setContent normally preserves the URL, but inline scripts can navigate.
41
+ updateCurrentUrl();
42
+ await waitForPageReady({ timeout, reason: 'after setContent' });
43
+ return true;
44
+ } catch (error) {
45
+ // Do not leave the manager in its loading state when setContent fails.
46
+ completeNavigation();
47
+ if (isNavigationError(error)) {
48
+ log.debug(() => '⚠️ Content loading was interrupted, recovering...');
49
+ return false;
50
+ }
51
+ throw error;
52
+ }
53
+ }
54
+
14
55
  /**
15
56
  * Create a NavigationManager instance for a page
16
57
  * @param {Object} options - Configuration options
@@ -341,6 +382,18 @@ export function createNavigationManager(options = {}) {
341
382
  }
342
383
  }
343
384
 
385
+ const setContent = (opts = {}) =>
386
+ setManagedContent({
387
+ page,
388
+ log,
389
+ ...opts,
390
+ currentUrl,
391
+ triggerNavigationStart,
392
+ updateCurrentUrl: () => (currentUrl = page.url()),
393
+ waitForPageReady,
394
+ completeNavigation,
395
+ });
396
+
344
397
  /**
345
398
  * Wait for any pending navigation to complete
346
399
  * @param {Object} options - Configuration options
@@ -373,27 +426,6 @@ export function createNavigationManager(options = {}) {
373
426
  return await navigationPromise;
374
427
  }
375
428
 
376
- /**
377
- * Check if we're currently navigating
378
- */
379
- function isCurrentlyNavigating() {
380
- return isNavigating;
381
- }
382
-
383
- /**
384
- * Get current URL
385
- */
386
- function getCurrentUrl() {
387
- return currentUrl;
388
- }
389
-
390
- /**
391
- * Get current session ID
392
- */
393
- function getSessionId() {
394
- return sessionId;
395
- }
396
-
397
429
  /**
398
430
  * Get the current abort signal
399
431
  * Use this to check if operations should be aborted due to navigation
@@ -476,13 +508,14 @@ export function createNavigationManager(options = {}) {
476
508
  return {
477
509
  // Navigation
478
510
  navigate,
511
+ setContent,
479
512
  waitForNavigation,
480
513
  waitForPageReady,
481
514
 
482
515
  // State
483
- isNavigating: isCurrentlyNavigating,
484
- getCurrentUrl,
485
- getSessionId,
516
+ isNavigating: () => isNavigating,
517
+ getCurrentUrl: () => currentUrl,
518
+ getSessionId: () => sessionId,
486
519
 
487
520
  // Abort handling - use these to stop operations when navigation occurs
488
521
  getAbortSignal,
@@ -2,6 +2,61 @@ import { isNavigationError } from '../core/navigation-safety.js';
2
2
  import { getLocatorOrElement } from './locators.js';
3
3
  import { createEngineAdapter } from '../core/engine-adapter.js';
4
4
 
5
+ /**
6
+ * Get the full HTML of the current page
7
+ * @param {Object} options - Configuration options
8
+ * @param {Object} options.page - Browser page object
9
+ * @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
10
+ * @param {Object} options.adapter - Engine adapter (optional, will be created if not provided)
11
+ * @returns {Promise<string|null>} - Serialized page HTML, or null during navigation
12
+ */
13
+ export async function content(options = {}) {
14
+ const { page, engine, adapter: providedAdapter } = options;
15
+
16
+ try {
17
+ const adapter = providedAdapter || createEngineAdapter(page, engine);
18
+ return await adapter.getPageContent();
19
+ } catch (error) {
20
+ if (isNavigationError(error)) {
21
+ console.log('⚠️ Navigation detected during content, returning null');
22
+ return null;
23
+ }
24
+ throw error;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Get rendered text from an element, defaulting to the document body
30
+ * @param {Object} options - Configuration options
31
+ * @param {Object} options.page - Browser page object
32
+ * @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
33
+ * @param {string|Object} [options.selector='body'] - CSS selector or element
34
+ * @param {Object} options.adapter - Engine adapter (optional, will be created if not provided)
35
+ * @returns {Promise<string|null>} - Rendered text, or null if unavailable
36
+ */
37
+ export async function innerText(options = {}) {
38
+ const { page, engine, selector = 'body', adapter: providedAdapter } = options;
39
+
40
+ try {
41
+ const adapter = providedAdapter || createEngineAdapter(page, engine);
42
+ const locatorOrElement = await getLocatorOrElement({
43
+ page,
44
+ engine,
45
+ selector,
46
+ });
47
+ if (!locatorOrElement) {
48
+ return null;
49
+ }
50
+ return await adapter.getInnerText(locatorOrElement);
51
+ } catch (error) {
52
+ if (isNavigationError(error)) {
53
+ console.log('⚠️ Navigation detected during innerText, returning null');
54
+ return null;
55
+ }
56
+ throw error;
57
+ }
58
+ }
59
+
5
60
  /**
6
61
  * Get text content
7
62
  * @param {Object} options - Configuration options
package/src/exports.js CHANGED
@@ -43,10 +43,12 @@ export {
43
43
 
44
44
  // Re-export browser management
45
45
  export { launchBrowser } from './browser/launcher.js';
46
+ export { saveStorageState } from './browser/storage-state.js';
46
47
  export { emulateMedia } from './browser/media.js';
47
48
  export {
48
49
  waitForUrlStabilization,
49
50
  goto,
51
+ setContent,
50
52
  waitForNavigation,
51
53
  waitForPageReady,
52
54
  waitAfterAction,
@@ -78,6 +80,8 @@ export {
78
80
  export { isVisible, isEnabled, count } from './elements/visibility.js';
79
81
 
80
82
  export {
83
+ content,
84
+ innerText,
81
85
  textContent,
82
86
  inputValue,
83
87
  getAttribute,
@@ -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
  });
@@ -44,6 +44,7 @@ export function createMockPlaywrightPage(options = {}) {
44
44
  },
45
45
  focus: async () => {},
46
46
  textContent: async () => elementData.textContent,
47
+ innerText: async () => elementData.innerText ?? elementData.textContent,
47
48
  inputValue: async () => elementData.value,
48
49
  getAttribute: async (name) => elementData[name] || null,
49
50
  isVisible: async () => elementData.visible,
@@ -56,6 +57,7 @@ export function createMockPlaywrightPage(options = {}) {
56
57
  const mockEl = {
57
58
  tagName: 'DIV',
58
59
  textContent: elementData.textContent,
60
+ innerText: elementData.innerText ?? elementData.textContent,
59
61
  value: elementData.value,
60
62
  className: elementData.className,
61
63
  disabled: !elementData.enabled,
@@ -88,6 +90,7 @@ export function createMockPlaywrightPage(options = {}) {
88
90
  _isPlaywrightPage: true,
89
91
  url: () => url,
90
92
  goto: async (targetUrl, opts = {}) => {},
93
+ setContent: async (html, opts = {}) => {},
91
94
  waitForNavigation: async (opts = {}) => {},
92
95
  waitForSelector: async (sel, opts = {}) => locatorMock(sel),
93
96
  $: async (sel) => locatorMock(sel),
@@ -105,6 +108,7 @@ export function createMockPlaywrightPage(options = {}) {
105
108
  return locs.map((l) => l.evaluate(fn, ...args));
106
109
  },
107
110
  locator: locatorMock,
111
+ content: async () => '<html><body>Mock page</body></html>',
108
112
  // Playwright's page.evaluate() only accepts a single argument (not spread)
109
113
  // This is the key difference from Puppeteer
110
114
  evaluate: async (fn, arg) => {
@@ -193,6 +197,7 @@ export function createMockPuppeteerPage(options = {}) {
193
197
  _isPuppeteerPage: true,
194
198
  url: () => url,
195
199
  goto: async (targetUrl, opts = {}) => {},
200
+ setContent: async (html, opts = {}) => {},
196
201
  waitForNavigation: async (opts = {}) => {},
197
202
  waitForSelector: async (sel, opts = {}) => elementMock(sel),
198
203
  $: async (sel) => {
@@ -213,6 +218,8 @@ export function createMockPuppeteerPage(options = {}) {
213
218
  const mockEl = {
214
219
  tagName: 'DIV',
215
220
  textContent: elementData.textContent || 'Mock text',
221
+ innerText:
222
+ elementData.innerText ?? elementData.textContent ?? 'Mock text',
216
223
  value: elementData.value || '',
217
224
  className: elementData.className || 'mock-class',
218
225
  disabled: !elementData.enabled,
@@ -252,6 +259,7 @@ export function createMockPuppeteerPage(options = {}) {
252
259
  const mockEl = {
253
260
  tagName: 'DIV',
254
261
  textContent: 'Mock text',
262
+ innerText: 'Mock text',
255
263
  value: '',
256
264
  className: 'mock-class',
257
265
  disabled: false,
@@ -280,6 +288,7 @@ export function createMockPuppeteerPage(options = {}) {
280
288
  return fn;
281
289
  }
282
290
  },
291
+ content: async () => '<html><body>Mock page</body></html>',
283
292
  mainFrame: () => ({
284
293
  url: () => url,
285
294
  }),
@@ -434,6 +443,7 @@ export function createMockNavigationManager(options = {}) {
434
443
  url = opts.url || url;
435
444
  return true;
436
445
  },
446
+ setContent: async (opts = {}) => true,
437
447
  waitForNavigation: async (opts = {}) => true,
438
448
  waitForPageReady: async (opts = {}) => true,
439
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'
@@ -129,6 +133,14 @@ describe('bindings', () => {
129
133
  typeof bindings.textContent === 'function',
130
134
  'textContent should be a function'
131
135
  );
136
+ assert.ok(
137
+ typeof bindings.content === 'function',
138
+ 'content should be a function'
139
+ );
140
+ assert.ok(
141
+ typeof bindings.innerText === 'function',
142
+ 'innerText should be a function'
143
+ );
132
144
  assert.ok(
133
145
  typeof bindings.inputValue === 'function',
134
146
  'inputValue should be a function'
@@ -204,6 +216,62 @@ describe('bindings', () => {
204
216
  assert.strictEqual(url, 'https://example.com');
205
217
  });
206
218
 
219
+ it('should expose page content with no arguments', async () => {
220
+ const page = createMockPlaywrightPage();
221
+ const bindings = createBoundFunctions({
222
+ page,
223
+ engine: 'playwright',
224
+ log: createMockLogger(),
225
+ });
226
+
227
+ assert.strictEqual(
228
+ await bindings.content(),
229
+ '<html><body>Mock page</body></html>'
230
+ );
231
+ });
232
+
233
+ it('should expose innerText with a selector or the body by default', async () => {
234
+ const page = createMockPlaywrightPage({
235
+ elements: {
236
+ body: { innerText: 'Body text', count: 1 },
237
+ main: { innerText: 'Main text', count: 1 },
238
+ },
239
+ });
240
+ const bindings = createBoundFunctions({
241
+ page,
242
+ engine: 'playwright',
243
+ log: createMockLogger(),
244
+ });
245
+
246
+ assert.strictEqual(await bindings.innerText(), 'Body text');
247
+ assert.strictEqual(await bindings.innerText('main'), 'Main text');
248
+ });
249
+
250
+ it('should evaluate a function with positional arguments', async () => {
251
+ const page = createMockPlaywrightPage();
252
+ const bindings = createBoundFunctions({
253
+ page,
254
+ engine: 'playwright',
255
+ log: createMockLogger(),
256
+ });
257
+
258
+ assert.strictEqual(await bindings.evaluate((a, b) => a + b, 2, 3), 5);
259
+ });
260
+
261
+ it('should preserve the evaluate options-object API', async () => {
262
+ const page = createMockPlaywrightPage();
263
+ const bindings = createBoundFunctions({
264
+ page,
265
+ engine: 'playwright',
266
+ log: createMockLogger(),
267
+ });
268
+
269
+ assert.strictEqual(
270
+ await bindings.evaluate({ fn: (a, b) => a + b, args: [2, 3] }),
271
+ 5
272
+ );
273
+ });
274
+
207
275
  it('should pre-bind log to functions', async () => {
208
276
  const page = createMockPlaywrightPage();
209
277
  const log = createMockLogger({ collectLogs: true });
@@ -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,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
+ });