browser-commander 0.20.0 → 0.21.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 +145 -0
- package/README.md +216 -32
- package/bin/browser-commander.js +16 -0
- package/eslint.config.js +10 -0
- package/examples/react-test-app/src/App.jsx +24 -23
- package/extension/background.js +14 -0
- package/extension/manifest.json +12 -0
- package/extension/relay-handler.js +353 -0
- package/package.json +19 -4
- package/src/browser/attach/differences.js +131 -0
- package/src/browser/attach/extension-relay.js +402 -0
- package/src/browser/attach/index.js +81 -0
- package/src/browser/attach/snapshot-launch.js +146 -0
- package/src/browser/attach/snapshot.js +374 -0
- package/src/browser/attach/websocket.js +615 -0
- package/src/browser/browser-cookie-cache.js +12 -17
- package/src/browser/browser-cookie-credentials.js +6 -9
- package/src/browser/browser-cookies.js +5 -2
- package/src/browser/cdp-session.js +94 -0
- package/src/browser/connector.js +42 -4
- package/src/browser/debugging-port.js +155 -0
- package/src/browser/launch-options.js +63 -52
- package/src/browser/launch-restrictions.json +147 -0
- package/src/browser/launcher.js +329 -133
- package/src/browser/media.js +5 -0
- package/src/browser/migration/bookmarks.js +73 -0
- package/src/browser/migration/chromium-crypto.js +89 -0
- package/src/browser/migration/cookies.js +155 -0
- package/src/browser/migration/extensions.js +187 -0
- package/src/browser/migration/firefox-bookmarks.js +144 -0
- package/src/browser/migration/firefox-der.js +84 -0
- package/src/browser/migration/firefox-nss.js +281 -0
- package/src/browser/migration/firefox.js +334 -0
- package/src/browser/migration/fs-utils.js +51 -0
- package/src/browser/migration/history.js +103 -0
- package/src/browser/migration/index.js +307 -0
- package/src/browser/migration/os-crypt-keys.js +106 -0
- package/src/browser/migration/passwords.js +171 -0
- package/src/browser/migration/preferences.js +132 -0
- package/src/browser/migration/sqlite-snapshot.js +138 -0
- package/src/browser/open-in-user-browser.js +112 -0
- package/src/browser/parity.js +439 -0
- package/src/browser/profile-directory.js +102 -0
- package/src/browser/real-browser.js +513 -306
- package/src/browser/restrictions.js +148 -0
- package/src/browser/system-browser.js +232 -0
- package/src/browser/webdriver.js +572 -0
- package/src/cli/args.js +253 -0
- package/src/cli/commands.js +332 -0
- package/src/cli/dispatcher.js +99 -0
- package/src/cli/handle-methods.js +136 -0
- package/src/cli/handles.js +273 -0
- package/src/cli/high-level-methods.js +310 -0
- package/src/cli/main.js +67 -0
- package/src/cli/modules.js +100 -0
- package/src/cli/rpc-error.js +77 -0
- package/src/cli/script.js +77 -0
- package/src/cli/serve.js +144 -0
- package/src/cli/sessions.js +195 -0
- package/src/core/constants.js +7 -1
- package/src/core/engine-adapter.js +107 -1
- package/src/core/engine-detection.js +18 -3
- package/src/core/navigation-safety.js +4 -0
- package/src/core/webdriver-conversions.js +347 -0
- package/src/core/webdriver-events.js +229 -0
- package/src/core/webdriver-page.js +716 -0
- package/src/downloads/attach.js +6 -0
- package/src/elements/selectors.js +3 -2
- package/src/elements/visibility.js +4 -0
- package/src/exports.js +61 -2
- package/src/factory.js +19 -4
- package/src/fingerprint/apply.js +6 -0
- package/src/fingerprint/automation-parity.js +7 -8
- package/src/fingerprint/limitations.js +1 -1
- package/src/fingerprint/limitations.json +45 -2
- package/src/parity/harness.js +303 -0
- package/src/parity/probe.js +838 -0
- package/src/parity/version-page.js +144 -0
- package/src/utilities/subprocess.js +199 -0
- package/tests/e2e/api-coverage.e2e.test.js +174 -0
- package/tests/e2e/cli.e2e.test.js +160 -0
- package/tests/e2e/fingerprint-parity.e2e.test.js +104 -46
- package/tests/e2e/google-signin-probe.e2e.test.js +112 -0
- package/tests/e2e/measure-parity.e2e.test.js +118 -0
- package/tests/e2e/playwright.e2e.test.js +25 -17
- package/tests/e2e/puppeteer.e2e.test.js +17 -23
- package/tests/e2e/webdriver.e2e.test.js +239 -0
- package/tests/fixtures/firefox-nss-fixtures.mjs +211 -0
- package/tests/helpers/cli-fakes.js +214 -0
- package/tests/helpers/declared-api.js +80 -0
- package/tests/helpers/e2e-browser.js +40 -14
- package/tests/helpers/fake-chrome.js +124 -0
- package/tests/helpers/migration-fixtures.js +242 -0
- package/tests/helpers/temp-directory.js +24 -0
- package/tests/helpers/webdriver-mocks.js +183 -0
- package/tests/unit/browser/attach/extension-relay.test.js +360 -0
- package/tests/unit/browser/attach/snapshot-launch.test.js +312 -0
- package/tests/unit/browser/attach/snapshot.test.js +390 -0
- package/tests/unit/browser/attach/websocket.test.js +240 -0
- package/tests/unit/browser/browser-cookies.test.js +2 -32
- package/tests/unit/browser/cdp-session.test.js +162 -0
- package/tests/unit/browser/connector.test.js +23 -0
- package/tests/unit/browser/launch-building-blocks.test.js +238 -0
- package/tests/unit/browser/launch-options.test.js +41 -22
- package/tests/unit/browser/launcher.test.js +281 -102
- package/tests/unit/browser/media.test.js +13 -1
- package/tests/unit/browser/migration/bookmarks.test.js +69 -0
- package/tests/unit/browser/migration/chromium-crypto.test.js +94 -0
- package/tests/unit/browser/migration/cookies.test.js +94 -0
- package/tests/unit/browser/migration/extensions.test.js +104 -0
- package/tests/unit/browser/migration/firefox-nss.test.js +113 -0
- package/tests/unit/browser/migration/firefox.test.js +139 -0
- package/tests/unit/browser/migration/history.test.js +48 -0
- package/tests/unit/browser/migration/index.test.js +117 -0
- package/tests/unit/browser/migration/os-crypt-keys.test.js +95 -0
- package/tests/unit/browser/migration/passwords.test.js +144 -0
- package/tests/unit/browser/migration/preferences.test.js +77 -0
- package/tests/unit/browser/migration/sqlite-snapshot.test.js +75 -0
- package/tests/unit/browser/open-in-user-browser.test.js +123 -0
- package/tests/unit/browser/parity.test.js +281 -0
- package/tests/unit/browser/real-browser.test.js +458 -33
- package/tests/unit/browser/webdriver.test.js +473 -0
- package/tests/unit/cli/bin.test.js +69 -0
- package/tests/unit/cli/declared-api.test.js +49 -0
- package/tests/unit/cli/dispatcher.test.js +429 -0
- package/tests/unit/cli/handles.test.js +170 -0
- package/tests/unit/cli/main.test.js +409 -0
- package/tests/unit/cli/serve.test.js +167 -0
- package/tests/unit/core/webdriver-conversions.test.js +235 -0
- package/tests/unit/core/webdriver-page.test.js +534 -0
- package/tests/unit/fingerprint/automation-parity.test.js +17 -4
- package/tests/unit/fingerprint/limitations.test.js +1 -1
- package/tests/unit/parity/harness.test.js +138 -0
- package/tests/unit/scripts/ci-timeout-budgets.test.js +4 -0
- package/tests/unit/scripts/feature-parity.test.js +221 -0
- package/tests/unit/traces/reader.test.js +1 -0
- package/tests/unit/traces/recorder.test.js +1 -0
- package/tests/unit/utilities/command-stream-only.test.js +28 -0
- package/tests/unit/utilities/subprocess.test.js +80 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { once } from 'node:events';
|
|
3
|
+
import http from 'node:http';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import { afterEach, describe, it } from 'node:test';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
acceptUpgrade,
|
|
9
|
+
CLOSE_CODES,
|
|
10
|
+
computeAcceptKey,
|
|
11
|
+
connectWebSocket,
|
|
12
|
+
encodeFrame,
|
|
13
|
+
FrameDecoder,
|
|
14
|
+
OPCODES,
|
|
15
|
+
WebSocketProtocolError,
|
|
16
|
+
} from '../../../../src/browser/attach/websocket.js';
|
|
17
|
+
|
|
18
|
+
const servers = [];
|
|
19
|
+
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
for (const server of servers.splice(0)) {
|
|
22
|
+
server.closeAllConnections?.();
|
|
23
|
+
await new Promise((resolve) => server.close(resolve));
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* An HTTP server that upgrades every request and hands the server-side
|
|
29
|
+
* connection to the test.
|
|
30
|
+
*/
|
|
31
|
+
async function startServer() {
|
|
32
|
+
const connections = [];
|
|
33
|
+
const server = http.createServer((request, response) => {
|
|
34
|
+
response.writeHead(426).end();
|
|
35
|
+
});
|
|
36
|
+
server.on('upgrade', (request, socket, head) => {
|
|
37
|
+
const connection = acceptUpgrade(request, socket, head, {
|
|
38
|
+
maxPayload: 1024,
|
|
39
|
+
});
|
|
40
|
+
if (connection) {
|
|
41
|
+
connections.push(connection);
|
|
42
|
+
server.emit('websocket', connection);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
server.listen(0, '127.0.0.1');
|
|
46
|
+
await once(server, 'listening');
|
|
47
|
+
servers.push(server);
|
|
48
|
+
return {
|
|
49
|
+
url: `ws://127.0.0.1:${server.address().port}/`,
|
|
50
|
+
server,
|
|
51
|
+
connections,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function connectPair() {
|
|
56
|
+
const { url, server } = await startServer();
|
|
57
|
+
const accepted = once(server, 'websocket');
|
|
58
|
+
const client = await connectWebSocket(url);
|
|
59
|
+
const [serverSide] = await accepted;
|
|
60
|
+
return { client, serverSide, url };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('WebSocket framing', () => {
|
|
64
|
+
it('computes the accept key of RFC 6455 section 1.3', () => {
|
|
65
|
+
assert.equal(
|
|
66
|
+
computeAcceptKey('dGhlIHNhbXBsZSBub25jZQ=='),
|
|
67
|
+
's3pPLMBiTxaQ9kYGzzhZRbK+xOo='
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('round-trips 7-bit, 16-bit and 64-bit lengths, masked, byte by byte', () => {
|
|
72
|
+
for (const size of [0, 125, 126, 65_535, 70_000]) {
|
|
73
|
+
const payload = Buffer.alloc(size, 'x');
|
|
74
|
+
const decoder = new FrameDecoder({ requireMask: true });
|
|
75
|
+
const frame = encodeFrame(OPCODES.text, payload, { mask: true });
|
|
76
|
+
const frames = [];
|
|
77
|
+
const step = size > 1000 ? 4096 : 1;
|
|
78
|
+
for (let offset = 0; offset < frame.length; offset += step) {
|
|
79
|
+
frames.push(...decoder.push(frame.subarray(offset, offset + step)));
|
|
80
|
+
}
|
|
81
|
+
assert.equal(frames.length, 1, `size ${size}`);
|
|
82
|
+
assert.equal(frames[0].opcode, OPCODES.text);
|
|
83
|
+
assert.equal(frames[0].fin, true);
|
|
84
|
+
assert.ok(frames[0].payload.equals(payload), `size ${size}`);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('decodes several frames from one chunk', () => {
|
|
89
|
+
const decoder = new FrameDecoder();
|
|
90
|
+
const frames = decoder.push(
|
|
91
|
+
Buffer.concat([
|
|
92
|
+
encodeFrame(OPCODES.text, 'a', { fin: false }),
|
|
93
|
+
encodeFrame(OPCODES.continuation, 'b'),
|
|
94
|
+
encodeFrame(OPCODES.ping, 'p'),
|
|
95
|
+
])
|
|
96
|
+
);
|
|
97
|
+
assert.deepEqual(
|
|
98
|
+
frames.map(({ fin, opcode, payload }) => [fin, opcode, String(payload)]),
|
|
99
|
+
[
|
|
100
|
+
[false, OPCODES.text, 'a'],
|
|
101
|
+
[true, OPCODES.continuation, 'b'],
|
|
102
|
+
[true, OPCODES.ping, 'p'],
|
|
103
|
+
]
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('rejects frames that break the protocol', () => {
|
|
108
|
+
const cases = [
|
|
109
|
+
[{ requireMask: true }, encodeFrame(OPCODES.text, 'x'), 1002],
|
|
110
|
+
[
|
|
111
|
+
{ forbidMask: true },
|
|
112
|
+
encodeFrame(OPCODES.text, 'x', { mask: true }),
|
|
113
|
+
1002,
|
|
114
|
+
],
|
|
115
|
+
[{}, Buffer.from([0xc1, 0x00]), 1002],
|
|
116
|
+
[{}, Buffer.from([0x83, 0x00]), 1002],
|
|
117
|
+
[{}, encodeFrame(OPCODES.ping, Buffer.alloc(126)), 1002],
|
|
118
|
+
[{}, encodeFrame(OPCODES.ping, 'x', { fin: false }), 1002],
|
|
119
|
+
[{ maxPayload: 10 }, encodeFrame(OPCODES.text, Buffer.alloc(11)), 1009],
|
|
120
|
+
[
|
|
121
|
+
{ maxPayload: 10 },
|
|
122
|
+
encodeFrame(OPCODES.text, Buffer.alloc(70_000)),
|
|
123
|
+
1009,
|
|
124
|
+
],
|
|
125
|
+
];
|
|
126
|
+
for (const [options, frame, code] of cases) {
|
|
127
|
+
assert.throws(
|
|
128
|
+
() => new FrameDecoder(options).push(frame),
|
|
129
|
+
(error) =>
|
|
130
|
+
error instanceof WebSocketProtocolError && error.code === code
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('WebSocketConnection', () => {
|
|
137
|
+
it('exchanges text messages both ways and answers pings', async () => {
|
|
138
|
+
const { client, serverSide } = await connectPair();
|
|
139
|
+
|
|
140
|
+
const toServer = once(serverSide, 'message');
|
|
141
|
+
client.send('héllo');
|
|
142
|
+
assert.deepEqual(await toServer, ['héllo']);
|
|
143
|
+
|
|
144
|
+
const toClient = once(client, 'message');
|
|
145
|
+
serverSide.send(JSON.stringify({ id: 1 }));
|
|
146
|
+
assert.deepEqual(await toClient, ['{"id":1}']);
|
|
147
|
+
|
|
148
|
+
const pong = once(client, 'pong');
|
|
149
|
+
client.ping(Buffer.from('beat'));
|
|
150
|
+
assert.equal(String((await pong)[0]), 'beat');
|
|
151
|
+
|
|
152
|
+
const closed = Promise.all([
|
|
153
|
+
once(client, 'close'),
|
|
154
|
+
once(serverSide, 'close'),
|
|
155
|
+
]);
|
|
156
|
+
client.close(CLOSE_CODES.normal, 'bye');
|
|
157
|
+
const [[clientClose], [serverClose]] = await closed;
|
|
158
|
+
assert.deepEqual(serverClose, { code: 1000, reason: 'bye' });
|
|
159
|
+
assert.equal(clientClose.code, 1000);
|
|
160
|
+
assert.equal(client.send('late'), false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('joins fragmented messages', async () => {
|
|
164
|
+
const { client, serverSide } = await connectPair();
|
|
165
|
+
const message = once(serverSide, 'message');
|
|
166
|
+
client.socket.write(
|
|
167
|
+
Buffer.concat([
|
|
168
|
+
encodeFrame(OPCODES.text, 'frag', { mask: true, fin: false }),
|
|
169
|
+
encodeFrame(OPCODES.ping, '', { mask: true }),
|
|
170
|
+
encodeFrame(OPCODES.continuation, 'mented', { mask: true }),
|
|
171
|
+
])
|
|
172
|
+
);
|
|
173
|
+
assert.deepEqual(await message, ['fragmented']);
|
|
174
|
+
client.terminate();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
for (const [name, frame, code] of [
|
|
178
|
+
['an unmasked client frame', encodeFrame(OPCODES.text, 'x'), 1002],
|
|
179
|
+
[
|
|
180
|
+
'invalid UTF-8',
|
|
181
|
+
encodeFrame(OPCODES.text, Buffer.from([0xc3, 0x28]), { mask: true }),
|
|
182
|
+
1007,
|
|
183
|
+
],
|
|
184
|
+
[
|
|
185
|
+
'a binary message',
|
|
186
|
+
encodeFrame(OPCODES.binary, 'x', { mask: true }),
|
|
187
|
+
1003,
|
|
188
|
+
],
|
|
189
|
+
[
|
|
190
|
+
'a message over maxPayload',
|
|
191
|
+
encodeFrame(OPCODES.text, Buffer.alloc(2048), { mask: true }),
|
|
192
|
+
1009,
|
|
193
|
+
],
|
|
194
|
+
[
|
|
195
|
+
'a stray continuation frame',
|
|
196
|
+
encodeFrame(OPCODES.continuation, 'x', { mask: true }),
|
|
197
|
+
1002,
|
|
198
|
+
],
|
|
199
|
+
]) {
|
|
200
|
+
it(`closes with ${code} on ${name}`, async () => {
|
|
201
|
+
const { client, serverSide } = await connectPair();
|
|
202
|
+
const closed = once(client, 'close');
|
|
203
|
+
const serverClosed = once(serverSide, 'close');
|
|
204
|
+
client.socket.write(frame);
|
|
205
|
+
const [{ code: received }] = await closed;
|
|
206
|
+
assert.equal(received, code);
|
|
207
|
+
await serverClosed;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
it('refuses a bad handshake with 400 and an old version with 426', async () => {
|
|
212
|
+
const { url } = await startServer();
|
|
213
|
+
const { port } = new URL(url);
|
|
214
|
+
const statusOf = (headers) =>
|
|
215
|
+
new Promise((resolve, reject) => {
|
|
216
|
+
const socket = net.connect(port, '127.0.0.1', () => {
|
|
217
|
+
socket.write(`GET / HTTP/1.1\r\nHost: x\r\n${headers}\r\n`);
|
|
218
|
+
});
|
|
219
|
+
let text = '';
|
|
220
|
+
socket.on('data', (chunk) => {
|
|
221
|
+
text += chunk;
|
|
222
|
+
});
|
|
223
|
+
socket.on('end', () => resolve(Number(text.split(' ')[1])));
|
|
224
|
+
socket.on('error', reject);
|
|
225
|
+
});
|
|
226
|
+
assert.equal(
|
|
227
|
+
await statusOf(
|
|
228
|
+
'Connection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Key: short\r\nSec-WebSocket-Version: 13\r\n'
|
|
229
|
+
),
|
|
230
|
+
400
|
|
231
|
+
);
|
|
232
|
+
assert.equal(
|
|
233
|
+
await statusOf(
|
|
234
|
+
'Connection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 8\r\n'
|
|
235
|
+
),
|
|
236
|
+
426
|
|
237
|
+
);
|
|
238
|
+
assert.throws(() => connectWebSocket('http://127.0.0.1/'), TypeError);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
listBrowserProfiles as publicListBrowserProfiles,
|
|
38
38
|
readBrowserCookies as publicReadBrowserCookies,
|
|
39
39
|
} from '../../../src/index.js';
|
|
40
|
+
import { writeFirefoxCookies } from '../../helpers/migration-fixtures.js';
|
|
40
41
|
|
|
41
42
|
const CHROME_EPOCH_OFFSET_SECONDS = 11_644_473_600;
|
|
42
43
|
const execFile = promisify(execFileCallback);
|
|
@@ -136,43 +137,12 @@ async function createFirefoxProfile({ homeDir, rows }) {
|
|
|
136
137
|
const root = path.join(homeDir, '.mozilla', 'firefox');
|
|
137
138
|
const profileName = 'fixture.default-release';
|
|
138
139
|
const profilePath = path.join(root, profileName);
|
|
139
|
-
const cookiePath = path.join(profilePath, 'cookies.sqlite');
|
|
140
140
|
await mkdir(profilePath, { recursive: true });
|
|
141
141
|
await writeFile(
|
|
142
142
|
path.join(root, 'profiles.ini'),
|
|
143
143
|
`[Profile0]\nName=default-release\nIsRelative=1\nPath=${profileName}\nDefault=1\n`
|
|
144
144
|
);
|
|
145
|
-
const
|
|
146
|
-
database.exec(`
|
|
147
|
-
CREATE TABLE moz_cookies (
|
|
148
|
-
name TEXT,
|
|
149
|
-
value TEXT,
|
|
150
|
-
host TEXT,
|
|
151
|
-
path TEXT,
|
|
152
|
-
expiry INTEGER,
|
|
153
|
-
isSecure INTEGER,
|
|
154
|
-
isHttpOnly INTEGER,
|
|
155
|
-
sameSite INTEGER
|
|
156
|
-
);
|
|
157
|
-
`);
|
|
158
|
-
const insert = database.prepare(`
|
|
159
|
-
INSERT INTO moz_cookies
|
|
160
|
-
(name, value, host, path, expiry, isSecure, isHttpOnly, sameSite)
|
|
161
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
162
|
-
`);
|
|
163
|
-
for (const row of rows) {
|
|
164
|
-
insert.run(
|
|
165
|
-
row.name,
|
|
166
|
-
row.value,
|
|
167
|
-
row.host,
|
|
168
|
-
row.path ?? '/',
|
|
169
|
-
row.expiry ?? 0,
|
|
170
|
-
row.secure ? 1 : 0,
|
|
171
|
-
row.httpOnly ? 1 : 0,
|
|
172
|
-
row.sameSite ?? 0
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
database.close();
|
|
145
|
+
const cookiePath = await writeFirefoxCookies(profilePath, rows);
|
|
176
146
|
return { cookiePath, profilePath, root };
|
|
177
147
|
}
|
|
178
148
|
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createCdpSession,
|
|
6
|
+
wrapCdpSession,
|
|
7
|
+
} from '../../../src/browser/cdp-session.js';
|
|
8
|
+
import { makeBrowserCommander } from '../../../src/factory.js';
|
|
9
|
+
import * as publicApi from '../../../src/index.js';
|
|
10
|
+
|
|
11
|
+
function createEngineSession() {
|
|
12
|
+
const calls = [];
|
|
13
|
+
const listeners = [];
|
|
14
|
+
return {
|
|
15
|
+
calls,
|
|
16
|
+
listeners,
|
|
17
|
+
detachCount: 0,
|
|
18
|
+
send(method, params) {
|
|
19
|
+
calls.push({ method, params });
|
|
20
|
+
return Promise.resolve({ echoed: method });
|
|
21
|
+
},
|
|
22
|
+
on(event, listener) {
|
|
23
|
+
listeners.push(['on', event, listener]);
|
|
24
|
+
},
|
|
25
|
+
once(event, listener) {
|
|
26
|
+
listeners.push(['once', event, listener]);
|
|
27
|
+
},
|
|
28
|
+
off(event, listener) {
|
|
29
|
+
listeners.push(['off', event, listener]);
|
|
30
|
+
},
|
|
31
|
+
detach() {
|
|
32
|
+
this.detachCount += 1;
|
|
33
|
+
return Promise.resolve();
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createPlaywrightPage(session) {
|
|
39
|
+
const context = {
|
|
40
|
+
targets: [],
|
|
41
|
+
newCDPSession(target) {
|
|
42
|
+
context.targets.push(target);
|
|
43
|
+
return Promise.resolve(session);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
const page = { context: () => context, locator: () => ({}) };
|
|
47
|
+
return { page, context };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('createCdpSession', () => {
|
|
51
|
+
it('opens a Playwright session through page.context().newCDPSession(page)', async () => {
|
|
52
|
+
const session = createEngineSession();
|
|
53
|
+
const { page, context } = createPlaywrightPage(session);
|
|
54
|
+
|
|
55
|
+
const cdp = await createCdpSession(page, { engine: 'playwright' });
|
|
56
|
+
|
|
57
|
+
assert.equal(cdp.engine, 'playwright');
|
|
58
|
+
assert.equal(cdp.session, session);
|
|
59
|
+
assert.deepEqual(context.targets, [page]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('opens a Puppeteer session through page.createCDPSession()', async () => {
|
|
63
|
+
const session = createEngineSession();
|
|
64
|
+
const page = { createCDPSession: () => Promise.resolve(session) };
|
|
65
|
+
|
|
66
|
+
const cdp = await createCdpSession(page, { engine: 'puppeteer' });
|
|
67
|
+
|
|
68
|
+
assert.equal(cdp.engine, 'puppeteer');
|
|
69
|
+
assert.equal(cdp.session, session);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('detects the engine from the page when it is not given', async () => {
|
|
73
|
+
const session = createEngineSession();
|
|
74
|
+
const { page } = createPlaywrightPage(session);
|
|
75
|
+
|
|
76
|
+
assert.equal((await createCdpSession(page)).engine, 'playwright');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('accepts the older {browser, page, engine} form', async () => {
|
|
80
|
+
const session = createEngineSession();
|
|
81
|
+
const page = { createCDPSession: () => Promise.resolve(session) };
|
|
82
|
+
|
|
83
|
+
const cdp = await createCdpSession({
|
|
84
|
+
browser: {},
|
|
85
|
+
page,
|
|
86
|
+
engine: 'puppeteer',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
assert.equal(cdp.session, session);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('requires a page', async () => {
|
|
93
|
+
await assert.rejects(() => createCdpSession(undefined), /requires a page/u);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('the uniform CDP session surface', () => {
|
|
98
|
+
it('sends commands with empty params by default', async () => {
|
|
99
|
+
const session = createEngineSession();
|
|
100
|
+
const cdp = wrapCdpSession(session, 'puppeteer');
|
|
101
|
+
|
|
102
|
+
assert.deepEqual(await cdp.send('Browser.getVersion'), {
|
|
103
|
+
echoed: 'Browser.getVersion',
|
|
104
|
+
});
|
|
105
|
+
await cdp.send('Runtime.evaluate', { expression: '1' });
|
|
106
|
+
assert.deepEqual(session.calls, [
|
|
107
|
+
{ method: 'Browser.getVersion', params: {} },
|
|
108
|
+
{ method: 'Runtime.evaluate', params: { expression: '1' } },
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('forwards listeners and chains', () => {
|
|
113
|
+
const session = createEngineSession();
|
|
114
|
+
const cdp = wrapCdpSession(session, 'playwright');
|
|
115
|
+
const listener = () => {};
|
|
116
|
+
|
|
117
|
+
assert.equal(cdp.on('Network.requestWillBeSent', listener), cdp);
|
|
118
|
+
assert.equal(cdp.once('Page.loadEventFired', listener), cdp);
|
|
119
|
+
assert.equal(cdp.off('Network.requestWillBeSent', listener), cdp);
|
|
120
|
+
assert.deepEqual(
|
|
121
|
+
session.listeners.map(([kind, event]) => `${kind}:${event}`),
|
|
122
|
+
[
|
|
123
|
+
'on:Network.requestWillBeSent',
|
|
124
|
+
'once:Page.loadEventFired',
|
|
125
|
+
'off:Network.requestWillBeSent',
|
|
126
|
+
]
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('detaches once', async () => {
|
|
131
|
+
const session = createEngineSession();
|
|
132
|
+
const cdp = wrapCdpSession(session, 'playwright');
|
|
133
|
+
|
|
134
|
+
await cdp.detach();
|
|
135
|
+
await cdp.detach();
|
|
136
|
+
|
|
137
|
+
assert.equal(session.detachCount, 1);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('public cdpSession entry points', () => {
|
|
142
|
+
it('exports createCdpSession and wrapCdpSession', () => {
|
|
143
|
+
assert.equal(publicApi.createCdpSession, createCdpSession);
|
|
144
|
+
assert.equal(publicApi.wrapCdpSession, wrapCdpSession);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('opens a session from a commander', async () => {
|
|
148
|
+
const session = createEngineSession();
|
|
149
|
+
const { page } = createPlaywrightPage(session);
|
|
150
|
+
const commander = makeBrowserCommander({
|
|
151
|
+
page,
|
|
152
|
+
enableNetworkTracking: false,
|
|
153
|
+
enableNavigationManager: false,
|
|
154
|
+
enableDialogManager: false,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const cdp = await commander.createCdpSession();
|
|
158
|
+
|
|
159
|
+
assert.equal(cdp.engine, 'playwright');
|
|
160
|
+
assert.equal(cdp.session, session);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -3,6 +3,7 @@ import { describe, it } from 'node:test';
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
connectBrowser,
|
|
6
|
+
pickForegroundPage,
|
|
6
7
|
connectBrowserWithDependencies,
|
|
7
8
|
} from '../../../src/browser/connector.js';
|
|
8
9
|
import { connectBrowser as publicConnectBrowser } from '../../../src/index.js';
|
|
@@ -127,3 +128,25 @@ describe('connectBrowser', () => {
|
|
|
127
128
|
);
|
|
128
129
|
});
|
|
129
130
|
});
|
|
131
|
+
|
|
132
|
+
describe('pickForegroundPage', () => {
|
|
133
|
+
const page = (id, state) => ({ id, evaluate: async () => state });
|
|
134
|
+
|
|
135
|
+
it('prefers the visible tab over the first one', async () => {
|
|
136
|
+
const picked = await pickForegroundPage([
|
|
137
|
+
page('ntp', 'hidden'),
|
|
138
|
+
page('whats-new', 'visible'),
|
|
139
|
+
]);
|
|
140
|
+
assert.equal(picked.id, 'whats-new');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('falls back to the first tab when none reports visible', async () => {
|
|
144
|
+
const failing = {
|
|
145
|
+
id: 'crashed',
|
|
146
|
+
evaluate: async () => Promise.reject(new Error('gone')),
|
|
147
|
+
};
|
|
148
|
+
const picked = await pickForegroundPage([failing, page('b', 'hidden')]);
|
|
149
|
+
assert.equal(picked.id, 'crashed');
|
|
150
|
+
assert.equal(await pickForegroundPage([]), undefined);
|
|
151
|
+
});
|
|
152
|
+
});
|