browser-commander 0.23.0 → 0.24.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.
@@ -0,0 +1,107 @@
1
+ /** Native common launch and commander contracts for every JS engine (#124). */
2
+ import assert from 'node:assert/strict';
3
+ import { existsSync } from 'node:fs';
4
+ import { createServer } from 'node:http';
5
+ import { describe, it } from 'node:test';
6
+ import { launchBrowser } from '../../src/browser/launcher.js';
7
+ import { makeBrowserCommander } from '../../src/factory.js';
8
+ import {
9
+ CHROME_LAUNCH_OPTIONS,
10
+ PARITY_CHROME,
11
+ } from '../helpers/e2e-browser.js';
12
+
13
+ // feature-parity: engines.webdriver@native-typed engines.cli-matrix@native-typed
14
+ describe('Common native engine API', { skip: !process.env.RUN_E2E }, () => {
15
+ it(
16
+ 'selenium restores portable cookies and BiDi origin state',
17
+ { timeout: 60000 },
18
+ async () => {
19
+ const server = createServer((_request, response) => {
20
+ response.end('<title>Portable state</title>');
21
+ });
22
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
23
+ const origin = `http://127.0.0.1:${server.address().port}`;
24
+ let session;
25
+ try {
26
+ session = await launchBrowser({
27
+ engine: 'selenium',
28
+ launch: 'engine',
29
+ bidi: true,
30
+ headless: true,
31
+ executablePath: PARITY_CHROME,
32
+ ...CHROME_LAUNCH_OPTIONS,
33
+ storageState: {
34
+ cookies: [{ name: 'session', value: 'restored', url: origin }],
35
+ origins: [
36
+ { origin, localStorage: [{ name: 'theme', value: 'dark' }] },
37
+ ],
38
+ },
39
+ });
40
+ await session.page.goto(origin);
41
+ assert.equal(
42
+ await session.page.evaluate(() =>
43
+ globalThis.localStorage.getItem('theme')
44
+ ),
45
+ 'dark'
46
+ );
47
+ assert.match(
48
+ await session.page.evaluate(() => document.cookie),
49
+ /session=restored/u
50
+ );
51
+ } finally {
52
+ await session?.close();
53
+ await new Promise((resolve) => server.close(resolve));
54
+ }
55
+ }
56
+ );
57
+ for (const engine of ['playwright', 'puppeteer', 'selenium']) {
58
+ for (const launch of ['real', 'engine']) {
59
+ it(
60
+ `${engine}/${launch}: input, click, native handles, PDF, cleanup`,
61
+ { timeout: 120000 },
62
+ async () => {
63
+ const session = await launchBrowser({
64
+ engine,
65
+ launch,
66
+ headless: true,
67
+ executablePath: PARITY_CHROME,
68
+ ...CHROME_LAUNCH_OPTIONS,
69
+ });
70
+ const commander = makeBrowserCommander({
71
+ page: session.page,
72
+ enableNetworkTracking: false,
73
+ enableNavigationManager: false,
74
+ });
75
+ const profile = session.userDataDir;
76
+ try {
77
+ assert.equal(commander.engine, engine);
78
+ await commander.goto({
79
+ url: `data:text/html,${encodeURIComponent('<input id="name"><button id="go" onclick="document.title=document.querySelector(\'#name\').value">Go</button>')}`,
80
+ });
81
+ await commander.fill({ selector: '#name', text: 'Ada' });
82
+ assert.equal(
83
+ await commander.inputValue({ selector: '#name' }),
84
+ 'Ada'
85
+ );
86
+ await commander.click({ selector: '#go' });
87
+ assert.equal(await session.page.title(), 'Ada');
88
+ assert.ok(
89
+ Buffer.from(await session.page.pdf({ format: 'A4' }))
90
+ .subarray(0, 5)
91
+ .equals(Buffer.from('%PDF-'))
92
+ );
93
+ if (engine === 'selenium') {
94
+ assert.equal(await session.driver.getTitle(), 'Ada');
95
+ }
96
+ } finally {
97
+ await commander.destroy();
98
+ await session.close();
99
+ }
100
+ if (session.temporaryProfile) {
101
+ assert.equal(existsSync(profile), false);
102
+ }
103
+ }
104
+ );
105
+ }
106
+ }
107
+ });
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { describe, it, before, after } from 'node:test';
11
11
  import assert from 'node:assert';
12
+ import { CHROME_LAUNCH_OPTIONS } from '../helpers/e2e-browser.js';
12
13
 
13
14
  // Dynamic import for playwright since it may not be installed
14
15
  let playwright;
@@ -25,23 +26,16 @@ describe(
25
26
  const BASE_URL = process.env.TEST_URL || 'http://localhost:3000';
26
27
 
27
28
  before(async () => {
28
- try {
29
- playwright = await import('playwright');
30
- const module = await import('../../src/index.js');
31
- createCommander = module.createCommander;
32
-
33
- browser = await playwright.chromium.launch({
34
- headless: process.env.HEADLESS !== 'false',
35
- });
36
- const context = await browser.newContext();
37
- page = await context.newPage();
38
- commander = createCommander({ page, verbose: true });
39
- } catch (error) {
40
- console.log(
41
- 'Skipping E2E tests - playwright not available or test app not running'
42
- );
43
- console.log('Error:', error.message);
44
- }
29
+ playwright = await import('playwright');
30
+ const module = await import('../../src/index.js');
31
+ createCommander = module.createCommander;
32
+ browser = await playwright.chromium.launch({
33
+ headless: process.env.HEADLESS !== 'false',
34
+ ...CHROME_LAUNCH_OPTIONS,
35
+ });
36
+ const context = await browser.newContext();
37
+ page = await context.newPage();
38
+ commander = createCommander({ page, verbose: true });
45
39
  });
46
40
 
47
41
  after(async () => {
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { describe, it, before, after } from 'node:test';
11
11
  import assert from 'node:assert';
12
+ import { CHROME_LAUNCH_OPTIONS } from '../helpers/e2e-browser.js';
12
13
 
13
14
  // Dynamic import for puppeteer since it may not be installed
14
15
  let puppeteer;
@@ -22,22 +23,15 @@ describe('E2E Tests - Puppeteer Engine', { skip: !process.env.RUN_E2E }, () => {
22
23
  const BASE_URL = process.env.TEST_URL || 'http://localhost:3000';
23
24
 
24
25
  before(async () => {
25
- try {
26
- puppeteer = await import('puppeteer');
27
- const module = await import('../../src/index.js');
28
- createCommander = module.createCommander;
29
-
30
- browser = await puppeteer.default.launch({
31
- headless: process.env.HEADLESS !== 'false' ? 'new' : false,
32
- });
33
- page = await browser.newPage();
34
- commander = createCommander({ page, verbose: true });
35
- } catch (error) {
36
- console.log(
37
- 'Skipping E2E tests - puppeteer not available or test app not running'
38
- );
39
- console.log('Error:', error.message);
40
- }
26
+ puppeteer = await import('puppeteer');
27
+ const module = await import('../../src/index.js');
28
+ createCommander = module.createCommander;
29
+ browser = await puppeteer.default.launch({
30
+ headless: process.env.HEADLESS !== 'false',
31
+ ...CHROME_LAUNCH_OPTIONS,
32
+ });
33
+ page = await browser.newPage();
34
+ commander = createCommander({ page, verbose: true });
41
35
  });
42
36
 
43
37
  after(async () => {
@@ -352,7 +346,9 @@ describe('E2E Tests - Puppeteer Engine', { skip: !process.env.RUN_E2E }, () => {
352
346
  }
353
347
 
354
348
  // Scroll to specific item
355
- await commander.scroll({ selector: '[data-testid="scroll-item-15"]' });
349
+ await commander.scrollIntoView({
350
+ locatorOrElement: await page.$('[data-testid="scroll-item-15"]'),
351
+ });
356
352
  await commander.wait({ ms: 500 });
357
353
 
358
354
  // Target should be visible
@@ -272,7 +272,7 @@ describe('dispatcher: high-level methods', () => {
272
272
  await assert.rejects(dispatcher.dispatch('nope'), { code: -32601 });
273
273
  await assert.rejects(dispatcher.dispatch('version', []), { code: -32602 });
274
274
  await assert.rejects(
275
- dispatcher.dispatch('session.launch', { engine: 'selenium' }),
275
+ dispatcher.dispatch('session.launch', { engine: 'unknown' }),
276
276
  { code: -32602 }
277
277
  );
278
278
  await assert.rejects(
@@ -351,7 +351,7 @@ describe('dispatcher: generic handle methods', () => {
351
351
  'launched with puppeteer'
352
352
  );
353
353
  await assert.rejects(
354
- dispatcher.dispatch('handle.root', { name: 'selenium' }),
354
+ dispatcher.dispatch('handle.root', { name: 'unknown' }),
355
355
  { code: -32602 }
356
356
  );
357
357
  });
@@ -0,0 +1,186 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it } from 'node:test';
3
+ import { createDispatcher } from '../../../src/cli/dispatcher.js';
4
+ import { buildLaunchOptions } from '../../../src/cli/sessions.js';
5
+ import { launchBrowserWithDependencies } from '../../../src/browser/launcher.js';
6
+ import { connectBrowserWithDependencies } from '../../../src/browser/connector.js';
7
+
8
+ // feature-parity: engines.webdriver@native-typed engines.cli-matrix@native-typed
9
+
10
+ describe('Selenium uses the common engine entry points', () => {
11
+ it('restores portable cookies and origin state through the common launcher', async () => {
12
+ const visited = [];
13
+ const seeded = [];
14
+ const page = {
15
+ url: () => 'about:blank',
16
+ goto: async (url) => {
17
+ visited.push(url);
18
+ },
19
+ setCookie: async (...cookies) => {
20
+ seeded.push(...cookies);
21
+ },
22
+ evaluateOnNewDocument: async () => {},
23
+ evaluate: async () => {},
24
+ bringToFront: async () => {},
25
+ };
26
+ const cookie = {
27
+ name: 'session',
28
+ value: 'value',
29
+ domain: 'example.com',
30
+ path: '/',
31
+ secure: true,
32
+ };
33
+ const result = await launchBrowserWithDependencies(
34
+ {
35
+ engine: 'selenium',
36
+ launch: 'engine',
37
+ storageState: {
38
+ cookies: [cookie],
39
+ origins: [
40
+ {
41
+ origin: 'https://example.com',
42
+ localStorage: [{ name: 'theme', value: 'dark' }],
43
+ },
44
+ ],
45
+ },
46
+ },
47
+ {
48
+ launchWebDriver: async () => ({
49
+ driver: {},
50
+ page,
51
+ close: async () => {},
52
+ }),
53
+ settleMs: 0,
54
+ }
55
+ );
56
+ assert.deepEqual(seeded, [cookie]);
57
+ assert.deepEqual(visited, ['https://example.com', 'about:blank']);
58
+ await result.close();
59
+ });
60
+ it('preserves WebDriver browser and driver options in the CLI', () => {
61
+ assert.deepEqual(
62
+ buildLaunchOptions({
63
+ engine: 'selenium',
64
+ browser: 'firefox',
65
+ driverPath: '/driver',
66
+ bidi: true,
67
+ }),
68
+ {
69
+ engine: 'selenium',
70
+ headless: false,
71
+ browser: 'firefox',
72
+ driverPath: '/driver',
73
+ bidi: true,
74
+ }
75
+ );
76
+ });
77
+
78
+ it('launches through the native driver without loading a CDP engine', async () => {
79
+ const page = { bringToFront: async () => {} };
80
+ const driver = {};
81
+ let options;
82
+ let closed = false;
83
+ const result = await launchBrowserWithDependencies(
84
+ { engine: 'selenium', launch: 'engine', extraArgs: ['--no-sandbox'] },
85
+ {
86
+ launchWebDriver: async (value) => {
87
+ options = value;
88
+ return {
89
+ driver,
90
+ page,
91
+ close: async () => {
92
+ closed = true;
93
+ },
94
+ };
95
+ },
96
+ settleMs: 0,
97
+ }
98
+ );
99
+ assert.equal(result.browser, driver);
100
+ assert.equal(result.page, page);
101
+ assert.deepEqual(options.args, ['--no-sandbox']);
102
+ await result.close();
103
+ assert.equal(closed, true);
104
+ });
105
+
106
+ it('connects to a Selenium Grid through the common connector', async () => {
107
+ const driver = {};
108
+ const page = {};
109
+ let options;
110
+ const result = await connectBrowserWithDependencies(
111
+ {
112
+ engine: 'selenium',
113
+ serverUrl: 'http://grid:4444',
114
+ capabilities: { browserName: 'firefox' },
115
+ },
116
+ {
117
+ connectWebDriver: async (value) => {
118
+ options = value;
119
+ return { driver, page, close: async () => {} };
120
+ },
121
+ }
122
+ );
123
+ assert.equal(result.browser, driver);
124
+ assert.equal(options.capabilities.browserName, 'firefox');
125
+ });
126
+
127
+ it('exposes the raw Selenium module and driver over generic handles', async () => {
128
+ const driver = { getTitle: async () => 'native driver' };
129
+ const page = {
130
+ waitForSelector: async () => ({
131
+ clear: async () => {},
132
+ sendKeys: async () => {},
133
+ }),
134
+ url: () => 'about:blank',
135
+ };
136
+ const dispatcher = createDispatcher({
137
+ dependencies: {
138
+ loadEngine: async (name) => {
139
+ assert.equal(name, 'selenium');
140
+ return { Builder: class Builder {} };
141
+ },
142
+ launchBrowser: async () => ({
143
+ browser: driver,
144
+ driver,
145
+ page,
146
+ close: async () => {},
147
+ }),
148
+ },
149
+ });
150
+ try {
151
+ const root = await dispatcher.dispatch('handle.root', {
152
+ name: 'selenium',
153
+ });
154
+ assert.ok(root.$handle);
155
+ const builder = await dispatcher.dispatch('handle.get', {
156
+ handle: root,
157
+ property: 'Builder',
158
+ });
159
+ const instance = await dispatcher.dispatch('handle.construct', {
160
+ handle: builder,
161
+ });
162
+ assert.equal(
163
+ (await dispatcher.dispatch('handle.describe', { handle: instance }))
164
+ .type,
165
+ 'Builder'
166
+ );
167
+ await dispatcher.dispatch('session.launch', { engine: 'selenium' });
168
+ await dispatcher.dispatch('page.fill', {
169
+ selector: '#name',
170
+ value: 'Ada',
171
+ });
172
+ const handles = await dispatcher.dispatch('handle.root', {
173
+ name: 'session:s1',
174
+ });
175
+ assert.equal(
176
+ await dispatcher.dispatch('handle.call', {
177
+ handle: handles.browser,
178
+ method: 'getTitle',
179
+ }),
180
+ 'native driver'
181
+ );
182
+ } finally {
183
+ await dispatcher.close();
184
+ }
185
+ });
186
+ });
@@ -26,12 +26,13 @@ describe('browser-commander/tests', () => {
26
26
  assert.deepStrictEqual(normalizeBrowserTestEngines(), [
27
27
  'playwright',
28
28
  'puppeteer',
29
+ 'selenium',
29
30
  ]);
30
31
  assert.deepStrictEqual(normalizeBrowserTestEngines('Playwright'), [
31
32
  'playwright',
32
33
  ]);
33
34
  assert.throws(
34
- () => normalizeBrowserTestEngines('selenium'),
35
+ () => normalizeBrowserTestEngines('unknown'),
35
36
  /Unsupported browser test engine/
36
37
  );
37
38
  });
@@ -544,8 +544,8 @@ describe('trace links export (issue #94)', () => {
544
544
  // The notation's formatter and parser decide whether an export can be read
545
545
  // back, so the range this package depends on and the version the tests
546
546
  // proved are the same range.
547
- assert.strictEqual(manifest.dependencies['links-notation'], '^0.20.0');
548
- assert.match(installed.version, /^0\.20\./);
547
+ assert.strictEqual(manifest.dependencies['links-notation'], '^0.22.0');
548
+ assert.match(installed.version, /^0\.22\./);
549
549
  });
550
550
 
551
551
  it('should export a bundle named by path as readily as an open one', async () => {