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.
- package/.jscpd-baseline.json +47 -29
- package/CHANGELOG.md +8 -0
- package/README.md +12 -3
- package/eslint.config.js +1 -1
- package/examples/react-test-app/package-lock.json +890 -0
- package/examples/react-test-app/package.json +4 -4
- package/package.json +17 -17
- package/src/browser/connector.js +46 -4
- package/src/browser/launcher.js +26 -1
- package/src/browser/real-browser.js +8 -2
- package/src/browser/storage-state.js +27 -0
- package/src/browser/webdriver.js +18 -2
- package/src/cli/args.js +5 -0
- package/src/cli/commands.js +10 -6
- package/src/cli/handle-methods.js +19 -2
- package/src/cli/high-level-methods.js +20 -3
- package/src/cli/modules.js +5 -1
- package/src/cli/sessions.js +14 -2
- package/src/tests/index.js +1 -0
- package/tests/e2e/engine-matrix.e2e.test.js +107 -0
- package/tests/e2e/playwright.e2e.test.js +11 -17
- package/tests/e2e/puppeteer.e2e.test.js +13 -17
- package/tests/unit/cli/dispatcher.test.js +2 -2
- package/tests/unit/cli/selenium-parity.test.js +186 -0
- package/tests/unit/tests/index.test.js +2 -1
- package/tests/unit/traces/links.test.js +2 -2
|
@@ -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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
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: '
|
|
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: '
|
|
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('
|
|
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.
|
|
548
|
-
assert.match(installed.version, /^0\.
|
|
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 () => {
|