browser-commander 0.21.0 → 0.21.1
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.json +2 -1
- package/.prettierignore +1 -0
- package/CHANGELOG.md +14 -0
- package/package.json +4 -2
- package/src/browser/attach/snapshot.js +4 -4
- package/src/browser/browser-cookie-cache.js +14 -2
- package/src/browser/profile-directory.js +109 -6
- package/src/browser/real-browser.js +38 -6
- package/src/cli/args.js +47 -0
- package/src/cli/sessions.js +4 -0
- package/src/traces/bundle.js +3 -2
- package/src/traces/observers.js +14 -5
- package/src/traces/recorder.js +3 -0
- package/src/traces/viewer.js +16 -8
- package/tests/e2e/playwright.e2e.test.js +1 -0
- package/tests/e2e/puppeteer.e2e.test.js +1 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.html +2 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.png +1 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.state.json +54 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.html +2 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.png +1 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.state.json +60 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.html +2 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.png +1 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.state.json +60 -0
- package/tests/fixtures/traces/conformance/expected/bundle/events.ndjson +18 -0
- package/tests/fixtures/traces/conformance/expected/bundle/manifest.json +77 -0
- package/tests/fixtures/traces/conformance/expected/bundle/mutations/0001.ndjson +2 -0
- package/tests/fixtures/traces/conformance/expected/bundle/mutations/0002.ndjson +1 -0
- package/tests/fixtures/traces/conformance/expected/bundle/viewer.html +309 -0
- package/tests/fixtures/traces/conformance/expected/redaction.json +86 -0
- package/tests/fixtures/traces/conformance/expected/trace.lino +27 -0
- package/tests/fixtures/traces/conformance/scenario.json +249 -0
- package/tests/unit/browser/attach/extension-relay.test.js +1 -1
- package/tests/unit/browser/attach/snapshot-launch.test.js +10 -1
- package/tests/unit/browser/attach/snapshot.test.js +23 -4
- package/tests/unit/browser/browser-cookie-lock.test.js +102 -0
- package/tests/unit/browser/launch-building-blocks.test.js +127 -4
- package/tests/unit/browser/migration/index.test.js +1 -1
- package/tests/unit/browser/open-in-user-browser.test.js +1 -1
- package/tests/unit/browser/parity.test.js +1 -1
- package/tests/unit/browser/real-browser.test.js +1 -1
- package/tests/unit/browser/storage-state.test.js +2 -0
- package/tests/unit/cli/main.test.js +30 -2
- package/tests/unit/core/navigation-manager.test.js +11 -1
- package/tests/unit/playwright-protocol-coverage.test.js +231 -0
- package/tests/unit/puppeteer-bindings-coverage.test.js +214 -0
- package/tests/unit/scripts/ci-timeout-budgets.test.js +23 -0
- package/tests/unit/scripts/feature-parity.test.js +111 -5
- package/tests/unit/traces/conformance.test.js +92 -0
- package/tests/unit/traces/reader.test.js +1 -1
- package/tests/unit/traces/recorder.test.js +1 -1
- package/tests/unit/traces/viewer.test.js +78 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "One continuous trace recorded against a fake page by JavaScript, Python and Rust; all three must write these bytes.",
|
|
3
|
+
"clock": { "now": 1767225600000, "monotonic": 1234.5 },
|
|
4
|
+
"engine": "playwright",
|
|
5
|
+
"options": {
|
|
6
|
+
"mode": "continuous",
|
|
7
|
+
"commanderVersion": "0.0.0-conformance",
|
|
8
|
+
"privacy": {
|
|
9
|
+
"redactPatterns": ["sk-[a-z0-9]+"],
|
|
10
|
+
"redactQueryParams": ["ticket"]
|
|
11
|
+
},
|
|
12
|
+
"limits": { "maxQueuedMutations": 100 },
|
|
13
|
+
"links": true
|
|
14
|
+
},
|
|
15
|
+
"snapshot": {
|
|
16
|
+
"html": "<!DOCTYPE html>\n<html><head><title>Start</title></head><body><form><input name=\"user\" value=\"alice\"><input type=\"checkbox\" name=\"agree\"><select name=\"plan\"><option>a</option></select></form></body></html>",
|
|
17
|
+
"truncated": false,
|
|
18
|
+
"state": {
|
|
19
|
+
"url": "https://user:pw@example.com/start?ticket=t-1&lang=en#access_token=abc&view=1",
|
|
20
|
+
"title": "Start – “quoted” 'single' \\ back",
|
|
21
|
+
"readyState": "complete",
|
|
22
|
+
"scroll": { "x": 0, "y": 12.5 },
|
|
23
|
+
"viewport": { "width": 1280, "height": 720 },
|
|
24
|
+
"activeElement": null,
|
|
25
|
+
"controls": [
|
|
26
|
+
{
|
|
27
|
+
"path": "form > input:nth-of-type(1)",
|
|
28
|
+
"tag": "input",
|
|
29
|
+
"type": "text",
|
|
30
|
+
"name": "user",
|
|
31
|
+
"id": null,
|
|
32
|
+
"focused": false,
|
|
33
|
+
"value": "alice",
|
|
34
|
+
"redacted": false
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"path": "form > input:nth-of-type(2)",
|
|
38
|
+
"tag": "input",
|
|
39
|
+
"type": "checkbox",
|
|
40
|
+
"name": "agree",
|
|
41
|
+
"id": null,
|
|
42
|
+
"focused": false,
|
|
43
|
+
"value": "on",
|
|
44
|
+
"checked": false,
|
|
45
|
+
"redacted": false
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"path": "form > select",
|
|
49
|
+
"tag": "select",
|
|
50
|
+
"type": null,
|
|
51
|
+
"name": "plan",
|
|
52
|
+
"id": null,
|
|
53
|
+
"focused": false,
|
|
54
|
+
"value": "a",
|
|
55
|
+
"selected": [0],
|
|
56
|
+
"redacted": false
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"frames": []
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"steps": [
|
|
63
|
+
{
|
|
64
|
+
"op": "queueMutations",
|
|
65
|
+
"dropped": 2,
|
|
66
|
+
"batches": [
|
|
67
|
+
{
|
|
68
|
+
"at": 1767225600200,
|
|
69
|
+
"records": [
|
|
70
|
+
{
|
|
71
|
+
"type": "attributes",
|
|
72
|
+
"target": 4,
|
|
73
|
+
"name": "class",
|
|
74
|
+
"value": "busy"
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"at": 1767225600100,
|
|
80
|
+
"records": [
|
|
81
|
+
{
|
|
82
|
+
"type": "live-state",
|
|
83
|
+
"target": 2,
|
|
84
|
+
"property": "value",
|
|
85
|
+
"value": "bo"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"type": "childList",
|
|
89
|
+
"target": 1,
|
|
90
|
+
"added": [{ "id": 9, "html": "<p>new
line</p>" }],
|
|
91
|
+
"removed": [],
|
|
92
|
+
"index": 3
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"op": "emit",
|
|
100
|
+
"event": "framenavigated",
|
|
101
|
+
"data": {
|
|
102
|
+
"url": "https://example.com/next?token=abc&q=a b",
|
|
103
|
+
"main": true
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"op": "emit",
|
|
108
|
+
"event": "framenavigated",
|
|
109
|
+
"data": { "url": "https://ads.example.com/frame", "main": false }
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"op": "emit",
|
|
113
|
+
"event": "console",
|
|
114
|
+
"data": {
|
|
115
|
+
"type": "log",
|
|
116
|
+
"text": "key sk-abc123 loaded\n\ttab \"and\" 'quotes'"
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"op": "emit",
|
|
121
|
+
"event": "pageerror",
|
|
122
|
+
"data": {
|
|
123
|
+
"message": "boom: (unbalanced",
|
|
124
|
+
"stack": "Error: boom\n at main.js:1:2"
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"op": "emit",
|
|
129
|
+
"event": "requestfailed",
|
|
130
|
+
"data": {
|
|
131
|
+
"url": "https://api.example.com/v1?api_key=k&page=2",
|
|
132
|
+
"method": "POST",
|
|
133
|
+
"errorText": "net::ERR_FAILED"
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"op": "dialog",
|
|
138
|
+
"data": { "type": "confirm", "message": "Continue? #yes" }
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"op": "download",
|
|
142
|
+
"phase": "completed",
|
|
143
|
+
"artifact": {
|
|
144
|
+
"id": "download-1",
|
|
145
|
+
"suggestedFilename": "report final.csv",
|
|
146
|
+
"path": "/downloads/report final.csv",
|
|
147
|
+
"checksum": "sha256:00ff",
|
|
148
|
+
"bytes": 42,
|
|
149
|
+
"url": "https://files.example.com/r.csv?signature=s"
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"op": "interaction",
|
|
154
|
+
"action": "goto",
|
|
155
|
+
"target": "https://example.com/next?code=abc"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"op": "interaction",
|
|
159
|
+
"action": "clickButton",
|
|
160
|
+
"target": "#go",
|
|
161
|
+
"fail": "button #go is not visible"
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"op": "setSnapshot",
|
|
165
|
+
"snapshot": {
|
|
166
|
+
"html": "<!DOCTYPE html>\n<html><body><form><input name=\"user\" value=\"bob\"><textarea>sk-zzz9 note</textarea></form></body></html>",
|
|
167
|
+
"truncated": true,
|
|
168
|
+
"state": {
|
|
169
|
+
"url": "https://example.com/next?token=abc",
|
|
170
|
+
"title": "Next",
|
|
171
|
+
"readyState": "interactive",
|
|
172
|
+
"scroll": { "x": 0, "y": 0 },
|
|
173
|
+
"viewport": { "width": 1280, "height": 720 },
|
|
174
|
+
"activeElement": "form > textarea",
|
|
175
|
+
"controls": [
|
|
176
|
+
{
|
|
177
|
+
"path": "form > input:nth-of-type(1)",
|
|
178
|
+
"tag": "input",
|
|
179
|
+
"type": "text",
|
|
180
|
+
"name": "user",
|
|
181
|
+
"id": null,
|
|
182
|
+
"focused": false,
|
|
183
|
+
"value": "bob",
|
|
184
|
+
"redacted": false
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"path": "form > input:nth-of-type(2)",
|
|
188
|
+
"tag": "input",
|
|
189
|
+
"type": "checkbox",
|
|
190
|
+
"name": "agree",
|
|
191
|
+
"id": null,
|
|
192
|
+
"focused": false,
|
|
193
|
+
"value": "on",
|
|
194
|
+
"checked": true,
|
|
195
|
+
"redacted": false
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"path": "form > textarea",
|
|
199
|
+
"tag": "textarea",
|
|
200
|
+
"type": null,
|
|
201
|
+
"name": null,
|
|
202
|
+
"id": "note",
|
|
203
|
+
"focused": true,
|
|
204
|
+
"value": "sk-zzz9 note",
|
|
205
|
+
"scroll": { "x": 0, "y": 3 },
|
|
206
|
+
"redacted": false
|
|
207
|
+
}
|
|
208
|
+
],
|
|
209
|
+
"frames": [
|
|
210
|
+
{ "url": "https://ads.example.com/frame", "crossOrigin": true }
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
{ "op": "checkpoint", "name": "after click", "actor": "automation" },
|
|
216
|
+
{
|
|
217
|
+
"op": "queueMutations",
|
|
218
|
+
"dropped": 0,
|
|
219
|
+
"batches": [
|
|
220
|
+
{
|
|
221
|
+
"at": 1767225600300,
|
|
222
|
+
"records": [
|
|
223
|
+
{ "type": "characterData", "target": 7, "value": "done ✓ 😀" }
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"op": "event",
|
|
230
|
+
"name": "note",
|
|
231
|
+
"data": {
|
|
232
|
+
"authorization": "Bearer xyz",
|
|
233
|
+
"detail": "uses sk-q1w2",
|
|
234
|
+
"pageUrl": "https://example.com/?session=s1",
|
|
235
|
+
"count": 3,
|
|
236
|
+
"ratio": 0.25,
|
|
237
|
+
"whole": 2.0,
|
|
238
|
+
"nested": { "list": [1, "two", null, true] }
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"op": "checkpoint",
|
|
243
|
+
"name": "final",
|
|
244
|
+
"actor": "caller",
|
|
245
|
+
"reason": "failure"
|
|
246
|
+
},
|
|
247
|
+
{ "op": "stop" }
|
|
248
|
+
]
|
|
249
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
|
-
import { access, mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { describe, it } from 'node:test';
|
|
5
5
|
|
|
@@ -192,6 +192,15 @@ describe('snapshot attach launch option', () => {
|
|
|
192
192
|
]);
|
|
193
193
|
|
|
194
194
|
await access(result.userDataDir);
|
|
195
|
+
assert.equal(
|
|
196
|
+
JSON.parse(
|
|
197
|
+
await readFile(
|
|
198
|
+
path.join(result.userDataDir, 'Profile 1', 'Preferences'),
|
|
199
|
+
'utf8'
|
|
200
|
+
)
|
|
201
|
+
).browser.check_default_browser,
|
|
202
|
+
false
|
|
203
|
+
);
|
|
195
204
|
browserProcess.exit(0);
|
|
196
205
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
197
206
|
await assert.rejects(access(result.userDataDir), { code: 'ENOENT' });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// feature-parity: attach.snapshot
|
|
1
|
+
// feature-parity: attach.snapshot@native-typed
|
|
2
2
|
import assert from 'node:assert';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import {
|
|
@@ -151,9 +151,16 @@ describe('snapshotUserDataDir', () => {
|
|
|
151
151
|
assert.equal(report.target, to);
|
|
152
152
|
assert.deepEqual(report.warnings, []);
|
|
153
153
|
assert.equal(report.copied.databases, 1);
|
|
154
|
+
const localState = JSON.parse(
|
|
155
|
+
await readFile(path.join(to, 'Local State'), 'utf8')
|
|
156
|
+
);
|
|
157
|
+
assert.deepEqual(localState.os_crypt, { key: 'k' });
|
|
158
|
+
assert.equal(localState.browser.default_browser_infobar_declined_count, 5);
|
|
159
|
+
assert.equal(localState.browser.default_browser_declined_count, 5);
|
|
154
160
|
assert.equal(
|
|
155
|
-
await
|
|
156
|
-
|
|
161
|
+
(await readProfileJson(path.join(to, 'Default'), 'Preferences')).browser
|
|
162
|
+
.check_default_browser,
|
|
163
|
+
false
|
|
157
164
|
);
|
|
158
165
|
assert.deepEqual(
|
|
159
166
|
await readProfileJson(path.join(to, 'Default'), 'Bookmarks'),
|
|
@@ -258,7 +265,10 @@ describe('snapshotUserDataDir', () => {
|
|
|
258
265
|
|
|
259
266
|
const copy = path.join(to, 'Default', 'History');
|
|
260
267
|
// One self-contained file: the committed WAL content is folded in.
|
|
261
|
-
assert.deepEqual(await readdir(path.join(to, 'Default')), [
|
|
268
|
+
assert.deepEqual(await readdir(path.join(to, 'Default')), [
|
|
269
|
+
'History',
|
|
270
|
+
'Preferences',
|
|
271
|
+
]);
|
|
262
272
|
assert.equal(report.copied.databases, 1);
|
|
263
273
|
assert.equal(countRows(copy), 6);
|
|
264
274
|
const database = new BetterSqlite3(copy, { readonly: true });
|
|
@@ -335,6 +345,15 @@ describe('snapshotUserDataDir', () => {
|
|
|
335
345
|
try {
|
|
336
346
|
assert.equal(report.source.profile, 'Profile 1');
|
|
337
347
|
await stat(path.join(report.target, 'Profile 1', 'Preferences'));
|
|
348
|
+
assert.equal(
|
|
349
|
+
(
|
|
350
|
+
await readProfileJson(
|
|
351
|
+
path.join(report.target, 'Profile 1'),
|
|
352
|
+
'Preferences'
|
|
353
|
+
)
|
|
354
|
+
).browser.check_default_browser,
|
|
355
|
+
false
|
|
356
|
+
);
|
|
338
357
|
await assert.rejects(stat(path.join(report.target, 'Default')), {
|
|
339
358
|
code: 'ENOENT',
|
|
340
359
|
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { syncBuiltinESMExports } from 'node:module';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { test } from 'node:test';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
clearBrowserCookieMemoryCache,
|
|
10
|
+
getCachedCredential,
|
|
11
|
+
} from '../../../src/browser/browser-cookie-cache.js';
|
|
12
|
+
|
|
13
|
+
async function cleanup(t, directory) {
|
|
14
|
+
t.mock.restoreAll();
|
|
15
|
+
syncBuiltinESMExports();
|
|
16
|
+
clearBrowserCookieMemoryCache();
|
|
17
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test('retries a Windows delete-pending credential lock without rereading the credential', async (t) => {
|
|
21
|
+
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'bc-cookie-lock-'));
|
|
22
|
+
const realOpen = fs.open;
|
|
23
|
+
let attempted = false;
|
|
24
|
+
let reads = 0;
|
|
25
|
+
// Model the observed Windows exclusive-open EPERM while the previous
|
|
26
|
+
// reader's lock is pending deletion. Other file operations stay real.
|
|
27
|
+
t.mock.method(fs, 'open', async (filename, ...args) => {
|
|
28
|
+
if (filename.endsWith('.lock') && !attempted) {
|
|
29
|
+
attempted = true;
|
|
30
|
+
throw Object.assign(new Error('lock pending deletion'), {
|
|
31
|
+
code: 'EPERM',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return realOpen(filename, ...args);
|
|
35
|
+
});
|
|
36
|
+
syncBuiltinESMExports();
|
|
37
|
+
try {
|
|
38
|
+
const options = {
|
|
39
|
+
cache: { enabled: true, dir: directory, ttlSeconds: 60 },
|
|
40
|
+
identity: 'windows-lock-contention',
|
|
41
|
+
refresh: false,
|
|
42
|
+
metadata: {},
|
|
43
|
+
platform: 'win32',
|
|
44
|
+
create: async () => {
|
|
45
|
+
reads += 1;
|
|
46
|
+
return Buffer.from('credential');
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
assert.deepEqual(
|
|
50
|
+
await getCachedCredential(options),
|
|
51
|
+
Buffer.from('credential')
|
|
52
|
+
);
|
|
53
|
+
clearBrowserCookieMemoryCache();
|
|
54
|
+
assert.deepEqual(
|
|
55
|
+
await getCachedCredential(options),
|
|
56
|
+
Buffer.from('credential')
|
|
57
|
+
);
|
|
58
|
+
assert.equal(reads, 1);
|
|
59
|
+
assert.ok(attempted);
|
|
60
|
+
} finally {
|
|
61
|
+
await cleanup(t, directory);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('does not retry permanent permission errors or Unix EPERM', async (t) => {
|
|
66
|
+
const directory = await fs.mkdtemp(
|
|
67
|
+
path.join(os.tmpdir(), 'bc-cookie-denied-')
|
|
68
|
+
);
|
|
69
|
+
const realOpen = fs.open;
|
|
70
|
+
try {
|
|
71
|
+
for (const [platform, code] of [
|
|
72
|
+
['win32', 'EACCES'],
|
|
73
|
+
['linux', 'EPERM'],
|
|
74
|
+
]) {
|
|
75
|
+
const error = Object.assign(new Error('access denied'), { code });
|
|
76
|
+
t.mock.method(fs, 'open', async (filename, ...args) => {
|
|
77
|
+
if (filename.endsWith('.lock')) {
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
return realOpen(filename, ...args);
|
|
81
|
+
});
|
|
82
|
+
syncBuiltinESMExports();
|
|
83
|
+
await assert.rejects(
|
|
84
|
+
getCachedCredential({
|
|
85
|
+
cache: { enabled: true, dir: directory, ttlSeconds: 60 },
|
|
86
|
+
identity: platform,
|
|
87
|
+
refresh: false,
|
|
88
|
+
metadata: {},
|
|
89
|
+
platform,
|
|
90
|
+
create: async () => {
|
|
91
|
+
throw new Error('must not read credentials');
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
(received) => received === error
|
|
95
|
+
);
|
|
96
|
+
t.mock.restoreAll();
|
|
97
|
+
syncBuiltinESMExports();
|
|
98
|
+
}
|
|
99
|
+
} finally {
|
|
100
|
+
await cleanup(t, directory);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
@@ -6,6 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
import { PassThrough } from 'node:stream';
|
|
7
7
|
import {
|
|
8
8
|
access,
|
|
9
|
+
mkdir,
|
|
9
10
|
mkdtemp,
|
|
10
11
|
readFile,
|
|
11
12
|
rm,
|
|
@@ -22,8 +23,10 @@ import {
|
|
|
22
23
|
FIRST_RUN_SENTINEL,
|
|
23
24
|
INITIAL_LOCAL_STATE,
|
|
24
25
|
LOCAL_STATE_FILE,
|
|
26
|
+
PREFERENCES_FILE,
|
|
25
27
|
TEMPORARY_PROFILE_PREFIX,
|
|
26
28
|
createTemporaryUserDataDir,
|
|
29
|
+
configureUserDataDir,
|
|
27
30
|
prepareUserDataDir,
|
|
28
31
|
removeUserDataDir,
|
|
29
32
|
} from '../../../src/browser/profile-directory.js';
|
|
@@ -106,6 +109,11 @@ describe('launch restrictions', () => {
|
|
|
106
109
|
});
|
|
107
110
|
|
|
108
111
|
describe('profile directory', () => {
|
|
112
|
+
const readPreferences = async (directory) =>
|
|
113
|
+
JSON.parse(
|
|
114
|
+
await readFile(path.join(directory, 'Default', 'Preferences'), 'utf8')
|
|
115
|
+
);
|
|
116
|
+
|
|
109
117
|
it('creates a fresh profile with the First Run sentinel', async () => {
|
|
110
118
|
const dir = await createTemporaryUserDataDir();
|
|
111
119
|
try {
|
|
@@ -113,15 +121,25 @@ describe('profile directory', () => {
|
|
|
113
121
|
await access(path.join(dir, FIRST_RUN_SENTINEL));
|
|
114
122
|
assert.deepEqual(
|
|
115
123
|
JSON.parse(await readFile(path.join(dir, LOCAL_STATE_FILE), 'utf8')),
|
|
116
|
-
|
|
124
|
+
{
|
|
125
|
+
browser: {
|
|
126
|
+
...INITIAL_LOCAL_STATE.browser,
|
|
127
|
+
default_browser_infobar_declined_count: 5,
|
|
128
|
+
default_browser_declined_count: 5,
|
|
129
|
+
},
|
|
130
|
+
fre: INITIAL_LOCAL_STATE.fre,
|
|
131
|
+
}
|
|
117
132
|
);
|
|
133
|
+
assert.deepEqual(await readPreferences(dir), {
|
|
134
|
+
browser: { check_default_browser: false },
|
|
135
|
+
});
|
|
118
136
|
} finally {
|
|
119
137
|
await removeUserDataDir(dir);
|
|
120
138
|
}
|
|
121
139
|
await assert.rejects(() => access(dir), /ENOENT/);
|
|
122
140
|
});
|
|
123
141
|
|
|
124
|
-
it('
|
|
142
|
+
it('preserves other Local State values that Chrome already wrote', async () => {
|
|
125
143
|
const dir = await mkdtemp(path.join(os.tmpdir(), 'bc-profile-test-'));
|
|
126
144
|
try {
|
|
127
145
|
const localState = path.join(dir, LOCAL_STATE_FILE);
|
|
@@ -130,9 +148,61 @@ describe('profile directory', () => {
|
|
|
130
148
|
'{"browser":{"enabled_labs_experiments":[]}}'
|
|
131
149
|
);
|
|
132
150
|
await prepareUserDataDir(dir);
|
|
151
|
+
assert.deepEqual(JSON.parse(await readFile(localState, 'utf8')), {
|
|
152
|
+
browser: {
|
|
153
|
+
enabled_labs_experiments: [],
|
|
154
|
+
default_browser_infobar_declined_count: 5,
|
|
155
|
+
default_browser_declined_count: 5,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
} finally {
|
|
159
|
+
await rm(dir, { recursive: true, force: true });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('deep-merges settings and lets the named default-browser option override a copied preference', async () => {
|
|
164
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), 'bc-profile-test-'));
|
|
165
|
+
try {
|
|
166
|
+
await prepareUserDataDir(dir);
|
|
167
|
+
await writeFile(
|
|
168
|
+
path.join(dir, 'Default', 'Preferences'),
|
|
169
|
+
JSON.stringify({
|
|
170
|
+
browser: { check_default_browser: true, show_home_button: false },
|
|
171
|
+
intl: { accept_languages: 'en' },
|
|
172
|
+
})
|
|
173
|
+
);
|
|
174
|
+
await prepareUserDataDir(dir, {
|
|
175
|
+
defaultBrowserCheck: false,
|
|
176
|
+
preferences: { browser: { show_home_button: true } },
|
|
177
|
+
localState: { browser: { extra: 1 } },
|
|
178
|
+
});
|
|
179
|
+
assert.deepEqual(await readPreferences(dir), {
|
|
180
|
+
browser: { check_default_browser: false, show_home_button: true },
|
|
181
|
+
intl: { accept_languages: 'en' },
|
|
182
|
+
});
|
|
183
|
+
assert.deepEqual(
|
|
184
|
+
JSON.parse(await readFile(path.join(dir, LOCAL_STATE_FILE), 'utf8')),
|
|
185
|
+
{
|
|
186
|
+
browser: {
|
|
187
|
+
last_whats_new_version: 9999,
|
|
188
|
+
default_browser_infobar_declined_count: 5,
|
|
189
|
+
default_browser_declined_count: 5,
|
|
190
|
+
extra: 1,
|
|
191
|
+
},
|
|
192
|
+
fre: { has_user_seen_fre: true },
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
await prepareUserDataDir(dir, { defaultBrowserCheck: true });
|
|
133
196
|
assert.equal(
|
|
134
|
-
|
|
135
|
-
|
|
197
|
+
JSON.parse(
|
|
198
|
+
await readFile(path.join(dir, 'Default', 'Preferences'), 'utf8')
|
|
199
|
+
).browser.check_default_browser,
|
|
200
|
+
true
|
|
201
|
+
);
|
|
202
|
+
assert.equal(
|
|
203
|
+
JSON.parse(await readFile(path.join(dir, LOCAL_STATE_FILE), 'utf8'))
|
|
204
|
+
.browser.default_browser_declined_count,
|
|
205
|
+
0
|
|
136
206
|
);
|
|
137
207
|
} finally {
|
|
138
208
|
await rm(dir, { recursive: true, force: true });
|
|
@@ -153,6 +223,59 @@ describe('profile directory', () => {
|
|
|
153
223
|
await rm(parent, { recursive: true, force: true });
|
|
154
224
|
}
|
|
155
225
|
});
|
|
226
|
+
|
|
227
|
+
it('allows an explicit first-run flow in a new profile', async () => {
|
|
228
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), 'bc-profile-test-'));
|
|
229
|
+
try {
|
|
230
|
+
await prepareUserDataDir(dir, { firstRun: true });
|
|
231
|
+
await assert.rejects(
|
|
232
|
+
() => access(path.join(dir, FIRST_RUN_SENTINEL)),
|
|
233
|
+
/ENOENT/
|
|
234
|
+
);
|
|
235
|
+
} finally {
|
|
236
|
+
await rm(dir, { recursive: true, force: true });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('rejects malformed profile settings before writing them', async () => {
|
|
241
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), 'bc-profile-test-'));
|
|
242
|
+
try {
|
|
243
|
+
await assert.rejects(
|
|
244
|
+
prepareUserDataDir(dir, { preferences: { browser: false } }),
|
|
245
|
+
/preferences.browser/u
|
|
246
|
+
);
|
|
247
|
+
await assert.rejects(
|
|
248
|
+
prepareUserDataDir(dir, { preferences: JSON.parse('{"__proto__":1}') }),
|
|
249
|
+
/profile setting key/u
|
|
250
|
+
);
|
|
251
|
+
} finally {
|
|
252
|
+
await rm(dir, { recursive: true, force: true });
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('configures a selected snapshot profile instead of Default', async () => {
|
|
257
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), 'bc-profile-test-'));
|
|
258
|
+
try {
|
|
259
|
+
const selected = path.join(dir, 'Profile 1');
|
|
260
|
+
await mkdir(selected);
|
|
261
|
+
await writeFile(
|
|
262
|
+
path.join(selected, 'Preferences'),
|
|
263
|
+
'{"browser":{"check_default_browser":true}}'
|
|
264
|
+
);
|
|
265
|
+
await configureUserDataDir(dir, { profileDirectory: 'Profile 1' });
|
|
266
|
+
assert.equal(
|
|
267
|
+
JSON.parse(await readFile(path.join(selected, 'Preferences'), 'utf8'))
|
|
268
|
+
.browser.check_default_browser,
|
|
269
|
+
false
|
|
270
|
+
);
|
|
271
|
+
await assert.rejects(
|
|
272
|
+
() => access(path.join(dir, PREFERENCES_FILE)),
|
|
273
|
+
/ENOENT/
|
|
274
|
+
);
|
|
275
|
+
} finally {
|
|
276
|
+
await rm(dir, { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
156
279
|
});
|
|
157
280
|
|
|
158
281
|
describe('debugging port', () => {
|