browser-commander 0.16.1 → 0.17.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/.changeset/config.json +1 -1
- package/.changeset/merged-loud-river.md +7 -0
- package/.jscpd-baseline.json +257 -0
- package/.jscpd.json +1 -3
- package/.prettierignore +1 -0
- package/CHANGELOG.md +8 -0
- package/eslint.config.js +23 -1
- package/package.json +13 -24
- package/scripts/changeset-version.mjs +2 -6
- package/scripts/create-github-release.mjs +7 -8
- package/scripts/create-manual-changeset.mjs +7 -8
- package/scripts/format-github-release.mjs +6 -7
- package/scripts/format-release-notes.mjs +7 -8
- package/scripts/instant-version-bump.mjs +7 -8
- package/scripts/publish-to-npm.mjs +7 -8
- package/scripts/setup-npm.mjs +2 -6
- package/scripts/version-and-commit.mjs +7 -8
- package/src/browser/browser-cookie-credentials.js +3 -1
- package/src/browser/browser-cookies.js +6 -2
- package/src/browser/browser-profiles.js +1 -1
- package/src/browser/launch-options.js +31 -8
- package/src/browser/launcher.js +16 -0
- package/src/exports.js +34 -0
- package/src/fingerprint/apply.js +116 -0
- package/src/fingerprint/automation-parity.js +224 -0
- package/src/fingerprint/cdp-overrides.js +176 -0
- package/src/fingerprint/derive.js +104 -0
- package/src/fingerprint/init-payload.js +181 -0
- package/src/fingerprint/init-script.js +104 -0
- package/src/fingerprint/limitations.js +107 -0
- package/src/fingerprint/limitations.json +96 -0
- package/src/fingerprint/presets.js +244 -0
- package/src/fingerprint/profile.js +428 -0
- package/src/tests/index.js +2 -1
- package/tests/e2e/fingerprint-parity.e2e.test.js +212 -0
- package/tests/helpers/mocks.js +4 -2
- package/tests/helpers/repo.js +74 -0
- package/tests/unit/browser/launch-options.test.js +9 -1
- package/tests/unit/experiments/fingerprint-parity-harness.test.js +97 -0
- package/tests/unit/fingerprint/apply.test.js +271 -0
- package/tests/unit/fingerprint/automation-parity.test.js +212 -0
- package/tests/unit/fingerprint/cdp-overrides.test.js +227 -0
- package/tests/unit/fingerprint/derive.test.js +105 -0
- package/tests/unit/fingerprint/init-script.test.js +357 -0
- package/tests/unit/fingerprint/limitations.test.js +184 -0
- package/tests/unit/fingerprint/presets.test.js +135 -0
- package/tests/unit/fingerprint/profile.test.js +282 -0
- package/tests/unit/scripts/check-pipeline-status.test.js +133 -0
- package/tests/unit/scripts/check-web-archive.test.js +191 -0
- package/tests/unit/scripts/ci-timeout-budgets.test.js +311 -0
- package/tests/unit/scripts/ci-workflow-policy.test.js +193 -157
- package/tests/unit/scripts/debug-print.test.js +67 -0
- package/tests/unit/scripts/jscpd-config.test.js +141 -0
- package/tests/unit/scripts/pre-commit-config.test.js +209 -0
- package/tests/unit/scripts/read-manifest.test.js +194 -0
- package/tests/unit/scripts/run-with-budget-warning.test.js +115 -0
- package/tests/unit/scripts/use-module-adoption.test.js +74 -0
- package/tests/unit/scripts/use-module-integration.test.js +64 -0
- package/tests/unit/scripts/use-module.test.js +182 -0
- package/.husky/pre-commit +0 -1
- package/.prettierrc +0 -10
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
applyAutomationParityArgs,
|
|
6
|
+
AUTOMATION_CONTROLLED_OFF_ARG,
|
|
7
|
+
AUTOMATION_CONTROLLED_TRIGGERS,
|
|
8
|
+
detectAutomationControlledTriggers,
|
|
9
|
+
disablesAutomationControlled,
|
|
10
|
+
ENGINE_PARITY_IGNORED_DEFAULT_ARGS,
|
|
11
|
+
parityIgnoredDefaultArgs,
|
|
12
|
+
PLAYWRIGHT_HEADLESS_POINTER_ARG,
|
|
13
|
+
PLAYWRIGHT_SOFTWARE_WEBGL_ARG,
|
|
14
|
+
} from '../../../src/fingerprint/automation-parity.js';
|
|
15
|
+
|
|
16
|
+
describe('AutomationControlled triggers', () => {
|
|
17
|
+
it('documents a reason for every trigger', () => {
|
|
18
|
+
assert.ok(AUTOMATION_CONTROLLED_TRIGGERS.length > 0);
|
|
19
|
+
for (const trigger of AUTOMATION_CONTROLLED_TRIGGERS) {
|
|
20
|
+
assert.equal(typeof trigger.switch, 'string');
|
|
21
|
+
assert.ok(trigger.reason.length > 0, `${trigger.switch} needs a reason`);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('finds each switch Chromium maps onto the feature', () => {
|
|
26
|
+
const found = detectAutomationControlledTriggers([
|
|
27
|
+
'--enable-automation',
|
|
28
|
+
'--headless=new',
|
|
29
|
+
'--remote-debugging-pipe',
|
|
30
|
+
'--remote-debugging-port=0',
|
|
31
|
+
]).map((entry) => entry.switch);
|
|
32
|
+
|
|
33
|
+
assert.deepEqual(found, [
|
|
34
|
+
'--enable-automation',
|
|
35
|
+
'--headless',
|
|
36
|
+
'--remote-debugging-pipe',
|
|
37
|
+
'--remote-debugging-port=0',
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('reports the argument as written next to the switch it matched', () => {
|
|
42
|
+
assert.deepEqual(
|
|
43
|
+
detectAutomationControlledTriggers(['--headless=new'])[0].argument,
|
|
44
|
+
'--headless=new'
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('leaves a fixed debugging port alone', () => {
|
|
49
|
+
// runtime_features.cc treats only an ephemeral port as automation, because
|
|
50
|
+
// a specific port is what a human attaching a debugger passes.
|
|
51
|
+
assert.deepEqual(
|
|
52
|
+
detectAutomationControlledTriggers(['--remote-debugging-port=9222']),
|
|
53
|
+
[]
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('finds nothing in an ordinary command line', () => {
|
|
58
|
+
assert.deepEqual(
|
|
59
|
+
detectAutomationControlledTriggers([
|
|
60
|
+
'--no-first-run',
|
|
61
|
+
'--window-size=1280,720',
|
|
62
|
+
]),
|
|
63
|
+
[]
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('rejects a non-array or a non-string argument', () => {
|
|
68
|
+
assert.throws(
|
|
69
|
+
() => detectAutomationControlledTriggers('--headless'),
|
|
70
|
+
/must be an array of strings/u
|
|
71
|
+
);
|
|
72
|
+
assert.throws(
|
|
73
|
+
() => detectAutomationControlledTriggers([1]),
|
|
74
|
+
/must be an array of strings/u
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('applying automation parity to a command line', () => {
|
|
80
|
+
it('appends the switch that turns the feature back off', () => {
|
|
81
|
+
assert.deepEqual(applyAutomationParityArgs(['--no-first-run']), [
|
|
82
|
+
'--no-first-run',
|
|
83
|
+
AUTOMATION_CONTROLLED_OFF_ARG,
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('works on an empty command line and on no command line at all', () => {
|
|
88
|
+
assert.deepEqual(applyAutomationParityArgs([]), [
|
|
89
|
+
AUTOMATION_CONTROLLED_OFF_ARG,
|
|
90
|
+
]);
|
|
91
|
+
assert.deepEqual(applyAutomationParityArgs(), [
|
|
92
|
+
AUTOMATION_CONTROLLED_OFF_ARG,
|
|
93
|
+
]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('is idempotent', () => {
|
|
97
|
+
const once = applyAutomationParityArgs(['--no-first-run']);
|
|
98
|
+
assert.deepEqual(applyAutomationParityArgs(once), once);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('extends an existing feature list instead of duplicating the switch', () => {
|
|
102
|
+
// Chrome keeps only the last --disable-blink-features occurrence, so a
|
|
103
|
+
// second one would silently drop the caller's features.
|
|
104
|
+
assert.deepEqual(
|
|
105
|
+
applyAutomationParityArgs(['--disable-blink-features=AcceptCHFrame']),
|
|
106
|
+
['--disable-blink-features=AcceptCHFrame,AutomationControlled']
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('does not copy the input array back to the caller', () => {
|
|
111
|
+
const args = ['--no-first-run'];
|
|
112
|
+
assert.notEqual(applyAutomationParityArgs(args), args);
|
|
113
|
+
assert.deepEqual(args, ['--no-first-run']);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('rejects a non-array', () => {
|
|
117
|
+
assert.throws(
|
|
118
|
+
() => applyAutomationParityArgs('--no-first-run'),
|
|
119
|
+
/must be an array of strings/u
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('recognises the feature inside a longer list', () => {
|
|
124
|
+
assert.equal(
|
|
125
|
+
disablesAutomationControlled([
|
|
126
|
+
'--disable-blink-features=AcceptCHFrame, AutomationControlled',
|
|
127
|
+
]),
|
|
128
|
+
true
|
|
129
|
+
);
|
|
130
|
+
assert.equal(
|
|
131
|
+
disablesAutomationControlled(['--disable-blink-features=AcceptCHFrame']),
|
|
132
|
+
false
|
|
133
|
+
);
|
|
134
|
+
assert.equal(disablesAutomationControlled([]), false);
|
|
135
|
+
// Enabling is not disabling.
|
|
136
|
+
assert.equal(
|
|
137
|
+
disablesAutomationControlled([
|
|
138
|
+
'--enable-blink-features=AutomationControlled',
|
|
139
|
+
]),
|
|
140
|
+
false
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('engine default switches to suppress', () => {
|
|
146
|
+
it('suppresses --enable-automation on both engines', () => {
|
|
147
|
+
for (const engine of ['playwright', 'puppeteer']) {
|
|
148
|
+
assert.ok(
|
|
149
|
+
parityIgnoredDefaultArgs(engine).includes('--enable-automation'),
|
|
150
|
+
`${engine} must suppress --enable-automation`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('suppresses the Playwright headless pointer switch only when headless', () => {
|
|
156
|
+
// Playwright forces hover:hover and pointer:fine in headless Chrome, which
|
|
157
|
+
// a real headless browser never reports; see parity-headless.json.
|
|
158
|
+
assert.ok(
|
|
159
|
+
!parityIgnoredDefaultArgs('playwright').includes(
|
|
160
|
+
PLAYWRIGHT_HEADLESS_POINTER_ARG
|
|
161
|
+
)
|
|
162
|
+
);
|
|
163
|
+
assert.ok(
|
|
164
|
+
parityIgnoredDefaultArgs('playwright', { headless: true }).includes(
|
|
165
|
+
PLAYWRIGHT_HEADLESS_POINTER_ARG
|
|
166
|
+
)
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('suppresses the Playwright software-WebGL switch in both modes', () => {
|
|
171
|
+
// Playwright 1.62 pushes --enable-unsafe-swiftshader on every platform, so
|
|
172
|
+
// a machine with no usable GPU answers getContext('webgl') with a
|
|
173
|
+
// SwiftShader context where a hand-started Chrome answers null. Measured in
|
|
174
|
+
// analysis-artifacts/parity-webgl-swiftshader.json; the switch has to go in
|
|
175
|
+
// both modes, because headless Chrome turns SwiftShader on by itself.
|
|
176
|
+
for (const headless of [false, true]) {
|
|
177
|
+
assert.ok(
|
|
178
|
+
parityIgnoredDefaultArgs('playwright', { headless }).includes(
|
|
179
|
+
PLAYWRIGHT_SOFTWARE_WEBGL_ARG
|
|
180
|
+
),
|
|
181
|
+
`playwright must suppress ${PLAYWRIGHT_SOFTWARE_WEBGL_ARG} when headless=${headless}`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('has nothing extra to suppress for headless Puppeteer', () => {
|
|
187
|
+
assert.deepEqual(
|
|
188
|
+
parityIgnoredDefaultArgs('puppeteer', { headless: true }),
|
|
189
|
+
['--enable-automation']
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('rejects an unknown engine by name', () => {
|
|
194
|
+
assert.throws(
|
|
195
|
+
() => parityIgnoredDefaultArgs('selenium'),
|
|
196
|
+
/unknown engine "selenium"/u
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('does not let a caller mutate the shared table', () => {
|
|
201
|
+
assert.ok(Object.isFrozen(ENGINE_PARITY_IGNORED_DEFAULT_ARGS));
|
|
202
|
+
assert.ok(
|
|
203
|
+
Object.isFrozen(ENGINE_PARITY_IGNORED_DEFAULT_ARGS.playwright.always)
|
|
204
|
+
);
|
|
205
|
+
const result = parityIgnoredDefaultArgs('playwright');
|
|
206
|
+
result.push('--mutated');
|
|
207
|
+
assert.deepEqual(parityIgnoredDefaultArgs('playwright'), [
|
|
208
|
+
'--enable-automation',
|
|
209
|
+
PLAYWRIGHT_SOFTWARE_WEBGL_ARG,
|
|
210
|
+
]);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
|
|
4
|
+
import { buildCdpEmulationCommands } from '../../../src/fingerprint/cdp-overrides.js';
|
|
5
|
+
import { resolveFingerprintProfile } from '../../../src/fingerprint/profile.js';
|
|
6
|
+
|
|
7
|
+
function methodsOf(profile) {
|
|
8
|
+
return buildCdpEmulationCommands(resolveFingerprintProfile(profile)).map(
|
|
9
|
+
(command) => command.method
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function paramsOf(profile, method) {
|
|
14
|
+
const command = buildCdpEmulationCommands(
|
|
15
|
+
resolveFingerprintProfile(profile)
|
|
16
|
+
).find((entry) => entry.method === method);
|
|
17
|
+
return command?.params;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('CDP emulation commands', () => {
|
|
21
|
+
it('rejects anything that is not a profile object', () => {
|
|
22
|
+
assert.throws(
|
|
23
|
+
() => buildCdpEmulationCommands(null),
|
|
24
|
+
/must be a normalized fingerprint profile/u
|
|
25
|
+
);
|
|
26
|
+
assert.throws(
|
|
27
|
+
() => buildCdpEmulationCommands('windows'),
|
|
28
|
+
/must be a normalized fingerprint profile/u
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('emits nothing for an empty profile', () => {
|
|
33
|
+
assert.deepEqual(
|
|
34
|
+
buildCdpEmulationCommands(resolveFingerprintProfile({})),
|
|
35
|
+
[]
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('sends only the commands the profile asks for', () => {
|
|
40
|
+
assert.deepEqual(methodsOf({ timezoneId: 'Europe/Berlin' }), [
|
|
41
|
+
'Emulation.setTimezoneOverride',
|
|
42
|
+
]);
|
|
43
|
+
assert.deepEqual(methodsOf({ hardwareConcurrency: 4 }), [
|
|
44
|
+
'Emulation.setHardwareConcurrencyOverride',
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('keeps a stable command order for a full profile', () => {
|
|
49
|
+
assert.deepEqual(
|
|
50
|
+
methodsOf({
|
|
51
|
+
userAgent:
|
|
52
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
|
|
53
|
+
'(KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
|
|
54
|
+
timezoneId: 'Europe/Berlin',
|
|
55
|
+
locale: 'de-DE',
|
|
56
|
+
hardwareConcurrency: 12,
|
|
57
|
+
screen: { width: 2560, height: 1440 },
|
|
58
|
+
viewport: { width: 1280, height: 720 },
|
|
59
|
+
maxTouchPoints: 0,
|
|
60
|
+
colorScheme: 'dark',
|
|
61
|
+
geolocation: { latitude: 52.52, longitude: 13.405, accuracy: 20 },
|
|
62
|
+
}),
|
|
63
|
+
[
|
|
64
|
+
'Emulation.setUserAgentOverride',
|
|
65
|
+
'Emulation.setTimezoneOverride',
|
|
66
|
+
'Emulation.setLocaleOverride',
|
|
67
|
+
'Emulation.setHardwareConcurrencyOverride',
|
|
68
|
+
'Emulation.setDeviceMetricsOverride',
|
|
69
|
+
'Emulation.setTouchEmulationEnabled',
|
|
70
|
+
'Emulation.setEmulatedMedia',
|
|
71
|
+
'Emulation.setGeolocationOverride',
|
|
72
|
+
]
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('supplies the required empty user agent when only the language changes', () => {
|
|
77
|
+
const params = paramsOf(
|
|
78
|
+
{ languages: ['fr-FR', 'fr'] },
|
|
79
|
+
'Emulation.setUserAgentOverride'
|
|
80
|
+
);
|
|
81
|
+
// userAgent is a required protocol parameter; an empty string means
|
|
82
|
+
// "leave it alone" while acceptLanguage still takes effect.
|
|
83
|
+
assert.equal(params.userAgent, '');
|
|
84
|
+
assert.equal(params.acceptLanguage, 'fr-FR,fr');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('carries the client hints, including the deprecated full version', () => {
|
|
88
|
+
const params = paramsOf(
|
|
89
|
+
{
|
|
90
|
+
userAgent:
|
|
91
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
|
|
92
|
+
'(KHTML, like Gecko) Chrome/140.0.7000.55 Safari/537.36',
|
|
93
|
+
platform: 'Win32',
|
|
94
|
+
},
|
|
95
|
+
'Emulation.setUserAgentOverride'
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
assert.equal(params.platform, 'Win32');
|
|
99
|
+
assert.equal(params.userAgentMetadata.platform, 'Windows');
|
|
100
|
+
// fullVersionList does not cover the uaFullVersion hint, so the deprecated
|
|
101
|
+
// fullVersion field has to travel with it.
|
|
102
|
+
assert.equal(params.userAgentMetadata.fullVersion, '140.0.7000.55');
|
|
103
|
+
assert.equal(params.userAgentMetadata.bitness, '64');
|
|
104
|
+
assert.equal(params.userAgentMetadata.wow64, false);
|
|
105
|
+
assert.deepEqual(params.userAgentMetadata.formFactors, ['Desktop']);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('always fills the protocol-required metadata fields', () => {
|
|
109
|
+
const params = paramsOf(
|
|
110
|
+
{
|
|
111
|
+
userAgent: 'custom agent',
|
|
112
|
+
userAgentData: { brands: [{ brand: 'Custom', version: '1' }] },
|
|
113
|
+
},
|
|
114
|
+
'Emulation.setUserAgentOverride'
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Chrome rejects setUserAgentOverride when any of these is missing.
|
|
118
|
+
for (const field of [
|
|
119
|
+
'platform',
|
|
120
|
+
'platformVersion',
|
|
121
|
+
'architecture',
|
|
122
|
+
'model',
|
|
123
|
+
'mobile',
|
|
124
|
+
]) {
|
|
125
|
+
assert.ok(field in params.userAgentMetadata, `${field} must be present`);
|
|
126
|
+
}
|
|
127
|
+
assert.equal(params.userAgentMetadata.mobile, false);
|
|
128
|
+
assert.ok(!('bitness' in params.userAgentMetadata));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('copies the brand entries instead of sharing them with the profile', () => {
|
|
132
|
+
const profile = resolveFingerprintProfile({
|
|
133
|
+
userAgent:
|
|
134
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
|
|
135
|
+
'(KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
|
|
136
|
+
});
|
|
137
|
+
const [{ params }] = buildCdpEmulationCommands(profile);
|
|
138
|
+
|
|
139
|
+
assert.notEqual(
|
|
140
|
+
params.userAgentMetadata.brands,
|
|
141
|
+
profile.userAgentData.brands
|
|
142
|
+
);
|
|
143
|
+
assert.notEqual(
|
|
144
|
+
params.userAgentMetadata.brands[0],
|
|
145
|
+
profile.userAgentData.brands[0]
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('leaves the window size alone when only the screen is described', () => {
|
|
150
|
+
const params = paramsOf(
|
|
151
|
+
{ screen: { width: 2560, height: 1440 } },
|
|
152
|
+
'Emulation.setDeviceMetricsOverride'
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Zeroes mean "no override" for the viewport, so a screen-only profile
|
|
156
|
+
// does not resize the window it was applied to.
|
|
157
|
+
assert.deepEqual(params, {
|
|
158
|
+
width: 0,
|
|
159
|
+
height: 0,
|
|
160
|
+
deviceScaleFactor: 0,
|
|
161
|
+
mobile: false,
|
|
162
|
+
screenWidth: 2560,
|
|
163
|
+
screenHeight: 1440,
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('sends the viewport without screen dimensions when no screen is set', () => {
|
|
168
|
+
assert.deepEqual(
|
|
169
|
+
paramsOf(
|
|
170
|
+
{
|
|
171
|
+
viewport: {
|
|
172
|
+
width: 1280,
|
|
173
|
+
height: 720,
|
|
174
|
+
deviceScaleFactor: 2,
|
|
175
|
+
mobile: true,
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
'Emulation.setDeviceMetricsOverride'
|
|
179
|
+
),
|
|
180
|
+
{ width: 1280, height: 720, deviceScaleFactor: 2, mobile: true }
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('disables touch emulation for a desktop profile that names zero touch points', () => {
|
|
185
|
+
// maxTouchPoints must stay at least 1 because the protocol rejects 0, so
|
|
186
|
+
// "no touch" is expressed through enabled instead.
|
|
187
|
+
assert.deepEqual(
|
|
188
|
+
paramsOf({ maxTouchPoints: 0 }, 'Emulation.setTouchEmulationEnabled'),
|
|
189
|
+
{ enabled: false, maxTouchPoints: 1 }
|
|
190
|
+
);
|
|
191
|
+
assert.deepEqual(
|
|
192
|
+
paramsOf({ maxTouchPoints: 5 }, 'Emulation.setTouchEmulationEnabled'),
|
|
193
|
+
{ enabled: true, maxTouchPoints: 5 }
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('collects every media preference into a single command', () => {
|
|
198
|
+
assert.deepEqual(
|
|
199
|
+
paramsOf(
|
|
200
|
+
{
|
|
201
|
+
colorScheme: 'dark',
|
|
202
|
+
reducedMotion: 'reduce',
|
|
203
|
+
forcedColors: 'active',
|
|
204
|
+
},
|
|
205
|
+
'Emulation.setEmulatedMedia'
|
|
206
|
+
),
|
|
207
|
+
{
|
|
208
|
+
features: [
|
|
209
|
+
{ name: 'prefers-reduced-motion', value: 'reduce' },
|
|
210
|
+
{ name: 'forced-colors', value: 'active' },
|
|
211
|
+
{ name: 'prefers-color-scheme', value: 'dark' },
|
|
212
|
+
],
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('passes geolocation through as its own copy', () => {
|
|
218
|
+
const geolocation = { latitude: 48.85, longitude: 2.35, accuracy: 10 };
|
|
219
|
+
const params = paramsOf(
|
|
220
|
+
{ geolocation },
|
|
221
|
+
'Emulation.setGeolocationOverride'
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
assert.deepEqual(params, geolocation);
|
|
225
|
+
assert.notEqual(params, geolocation);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
|
|
4
|
+
import { deriveUserAgentData } from '../../../src/fingerprint/derive.js';
|
|
5
|
+
|
|
6
|
+
describe('client hints derived from a user agent', () => {
|
|
7
|
+
it('returns null when the string names no Chrome version', () => {
|
|
8
|
+
assert.equal(
|
|
9
|
+
deriveUserAgentData(
|
|
10
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' +
|
|
11
|
+
'AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15'
|
|
12
|
+
),
|
|
13
|
+
null
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('rejects a non-string user agent', () => {
|
|
18
|
+
assert.throws(() => deriveUserAgentData(undefined), /must be a string/u);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('maps a Windows user agent onto the Windows platform hints', () => {
|
|
22
|
+
const data = deriveUserAgentData(
|
|
23
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
|
|
24
|
+
'(KHTML, like Gecko) Chrome/140.0.7000.55 Safari/537.36'
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
assert.equal(data.platform, 'Windows');
|
|
28
|
+
assert.equal(data.architecture, 'x86');
|
|
29
|
+
assert.equal(data.bitness, '64');
|
|
30
|
+
assert.equal(data.mobile, false);
|
|
31
|
+
assert.deepEqual(data.formFactors, ['Desktop']);
|
|
32
|
+
// Chrome froze the user agent at "Windows NT 10.0" and reports the real
|
|
33
|
+
// version only through platformVersion, where 13 and up mean Windows 11.
|
|
34
|
+
assert.equal(data.platformVersion, '15.0.0');
|
|
35
|
+
assert.equal(data.fullVersion, '140.0.7000.55');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('pads a major-only Chrome version into a four part full version', () => {
|
|
39
|
+
const data = deriveUserAgentData(
|
|
40
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
|
|
41
|
+
'(KHTML, like Gecko) Chrome/141 Safari/537.36'
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
assert.equal(data.fullVersion, '141.0.0.0');
|
|
45
|
+
assert.deepEqual(
|
|
46
|
+
data.brands.map((entry) => entry.version),
|
|
47
|
+
['141', '141', '24']
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('parses the macOS version out of the user agent', () => {
|
|
52
|
+
const data = deriveUserAgentData(
|
|
53
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +
|
|
54
|
+
'(KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36'
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
assert.equal(data.platform, 'macOS');
|
|
58
|
+
assert.equal(data.platformVersion, '10.15.7');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('reports an Android phone as mobile and recovers the model', () => {
|
|
62
|
+
const data = deriveUserAgentData(
|
|
63
|
+
'Mozilla/5.0 (Linux; Android 14; Pixel 8 Build/UQ1A.240105.004) ' +
|
|
64
|
+
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36'
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
assert.equal(data.platform, 'Android');
|
|
68
|
+
assert.equal(data.platformVersion, '14');
|
|
69
|
+
assert.equal(data.mobile, true);
|
|
70
|
+
assert.equal(data.model, 'Pixel 8');
|
|
71
|
+
assert.deepEqual(data.formFactors, ['Mobile']);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('recognises Linux and Chrome OS', () => {
|
|
75
|
+
assert.equal(
|
|
76
|
+
deriveUserAgentData(
|
|
77
|
+
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' +
|
|
78
|
+
'(KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36'
|
|
79
|
+
).platform,
|
|
80
|
+
'Linux'
|
|
81
|
+
);
|
|
82
|
+
assert.equal(
|
|
83
|
+
deriveUserAgentData(
|
|
84
|
+
'Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 ' +
|
|
85
|
+
'(KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36'
|
|
86
|
+
).platform,
|
|
87
|
+
'Chrome OS'
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('includes the GREASE brand so the brand list has a real browser shape', () => {
|
|
92
|
+
const data = deriveUserAgentData(
|
|
93
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
|
|
94
|
+
'(KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36'
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
assert.equal(data.brands.length, 3);
|
|
98
|
+
assert.ok(data.brands.some((entry) => entry.brand === 'Not=A?Brand'));
|
|
99
|
+
assert.ok(
|
|
100
|
+
data.fullVersionList.every(
|
|
101
|
+
(entry) => entry.version.split('.').length === 4
|
|
102
|
+
)
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
});
|