Skip to content

Commit 070ca92

Browse files
committed
lib: make Navigator WPT-compliant
Signed-off-by: avivkeller <me@aviv.sh>
1 parent aed4eaf commit 070ca92

36 files changed

Lines changed: 1160 additions & 7 deletions

doc/api/globals.md

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -722,7 +722,11 @@ added: v21.0.0
722722
> Stability: 1.1 - Active development. Disable this API with the
723723
> [`--no-experimental-global-navigator`][] CLI flag.
724724
725-
A partial implementation of the [Navigator API][].
725+
A partial implementation of the [Navigator API][]. All properties that the
726+
HTML specification exposes to non-`Window` environments are implemented;
727+
properties exposed only on `Window` (such as `navigator.plugins`) and most
728+
properties defined by other specifications (such as `navigator.clipboard`)
729+
are not. See below for the full list of supported options.
726730

727731
## `navigator`
728732

@@ -733,7 +737,46 @@ added: v21.0.0
733737
> Stability: 1.1 - Active development. Disable this API with the
734738
> [`--no-experimental-global-navigator`][] CLI flag.
735739
736-
A partial implementation of [`window.navigator`][].
740+
A partial implementation of [`window.navigator`][]. All properties of the
741+
[Navigator API][] that the HTML specification exposes to non-`Window`
742+
environments are implemented; properties exposed only on `Window` (such as
743+
`navigator.plugins`) and most properties defined by other specifications
744+
(such as `navigator.clipboard`) are not.
745+
746+
### `navigator.appCodeName`
747+
748+
<!-- YAML
749+
added: REPLACEME
750+
-->
751+
752+
* Type: {string}
753+
754+
The `navigator.appCodeName` read-only property returns `'Mozilla'`,
755+
the constant value mandated by the [Navigator API][] specification.
756+
757+
### `navigator.appName`
758+
759+
<!-- YAML
760+
added: REPLACEME
761+
-->
762+
763+
* Type: {string}
764+
765+
The `navigator.appName` read-only property returns `'Netscape'`,
766+
the constant value mandated by the [Navigator API][] specification.
767+
768+
### `navigator.appVersion`
769+
770+
<!-- YAML
771+
added: REPLACEME
772+
-->
773+
774+
* Type: {string}
775+
776+
The `navigator.appVersion` read-only property returns the empty string.
777+
The [Navigator API][] specification requires this value to be derived from
778+
the default `User-Agent` value and to be the empty string when that value
779+
does not start with `Mozilla/5.0 (`.
737780

738781
### `navigator.hardwareConcurrency`
739782

@@ -840,6 +883,18 @@ navigator.locks.request('shared_resource', { mode: 'shared' }, async (lock) => {
840883

841884
See [`worker_threads.locks`][] for detailed API documentation.
842885

886+
### `navigator.onLine`
887+
888+
<!-- YAML
889+
added: REPLACEME
890+
-->
891+
892+
* Type: {boolean}
893+
894+
The `navigator.onLine` read-only property always returns `true`. The
895+
[Navigator API][] specification only allows returning `false` when the user
896+
agent is definitely offline, which Node.js cannot determine.
897+
843898
### `navigator.platform`
844899

845900
<!-- YAML
@@ -855,6 +910,17 @@ platform on which the Node.js instance is running.
855910
console.log(`This process is running on ${navigator.platform}`);
856911
```
857912

913+
### `navigator.product`
914+
915+
<!-- YAML
916+
added: REPLACEME
917+
-->
918+
919+
* Type: {string}
920+
921+
The `navigator.product` read-only property returns `'Gecko'`,
922+
the constant value mandated by the [Navigator API][] specification.
923+
858924
### `navigator.userAgent`
859925

860926
<!-- YAML

lib/internal/navigator.js

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ function getNavigatorPlatform(arch, platform) {
8080

8181
class Navigator {
8282
// Private properties are used to avoid brand validations: reading a private
83-
// field from a non-Navigator receiver throws a TypeError on its own.
83+
// field or calling a private method from a non-Navigator receiver throws a
84+
// TypeError on its own.
8485
#availableParallelism;
8586
#locks;
8687
#userAgent;
@@ -94,6 +95,11 @@ class Navigator {
9495
throw new ERR_ILLEGAL_CONSTRUCTOR();
9596
}
9697

98+
// Getters that do not read a private field call this method so they still
99+
// throw a TypeError on a non-Navigator receiver
100+
// (e.g. `Navigator.prototype.product`).
101+
#brandCheck() {}
102+
97103
/**
98104
* @returns {number}
99105
*/
@@ -114,10 +120,7 @@ class Navigator {
114120
* @returns {string}
115121
*/
116122
get language() {
117-
// `language` does not read a private field, so brand-check explicitly
118-
// to keep parity with the other getters when called on a non-Navigator
119-
// receiver (e.g. `Navigator.prototype.language`).
120-
this.#languages; // eslint-disable-line no-unused-expressions
123+
this.#brandCheck();
121124
// The default locale might be changed dynamically, so always invoke the
122125
// binding.
123126
return getDefaultLocale() || 'en-US';
@@ -146,6 +149,50 @@ class Navigator {
146149
this.#platform ??= getNavigatorPlatform(arch, platform);
147150
return this.#platform;
148151
}
152+
153+
/**
154+
* @returns {string}
155+
*/
156+
get appCodeName() {
157+
this.#brandCheck();
158+
return 'Mozilla';
159+
}
160+
161+
/**
162+
* @returns {string}
163+
*/
164+
get appName() {
165+
this.#brandCheck();
166+
return 'Netscape';
167+
}
168+
169+
/**
170+
* @returns {string}
171+
*/
172+
get appVersion() {
173+
this.#brandCheck();
174+
// If userAgent does not start with `Mozilla/5.0 (`, then return the empty string.
175+
return '';
176+
}
177+
178+
/**
179+
* @returns {string}
180+
*/
181+
get product() {
182+
this.#brandCheck();
183+
return 'Gecko';
184+
}
185+
186+
/**
187+
* @returns {boolean}
188+
*/
189+
get onLine() {
190+
this.#brandCheck();
191+
// "Returns true if the user agent might be online."
192+
// Since we have no way to truely check the online-ness in the navigator,
193+
// the user agent might be online, so return true.
194+
return true;
195+
}
149196
}
150197

151198
ObjectDefineProperties(Navigator.prototype, {
@@ -155,6 +202,11 @@ ObjectDefineProperties(Navigator.prototype, {
155202
userAgent: kEnumerableProperty,
156203
platform: kEnumerableProperty,
157204
locks: kEnumerableProperty,
205+
appCodeName: kEnumerableProperty,
206+
appName: kEnumerableProperty,
207+
appVersion: kEnumerableProperty,
208+
product: kEnumerableProperty,
209+
onLine: kEnumerableProperty,
158210
});
159211

160212
module.exports = {
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
rules:
2+
- plugins-and-mimetypes.html: [pdf-viewer]
3+
- navigator_user_agent.tentative.html: [ua-client-hints]
4+
- navigator_user_agent.https.tentative.html: [ua-client-hints]
5+
- protocol-*: [registerprotocolhandler]
6+
- navigator-window-controls-overlay.tentative.html: [window-controls-overlay]
7+
- navigator.any.js: [user-agent-sniffing]
8+
- get-navigatorlanguage-manual.html: [language]
9+
- navigatorlanguage.html: [language]
10+
- navigatorcookies-cookieenabled-*: [cookie-enabled]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
test(() => {
2+
assert_equals(window.clientInformation, window.navigator);
3+
}, "window.clientInformation exists and equals window.navigator");
4+
5+
test(() => {
6+
window.clientInformation = 1;
7+
assert_equals(window.clientInformation, 1);
8+
}, "window.clientInformation is Replaceable");
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!DOCTYPE html>
2+
<meta charset="utf-8">
3+
<title>NavigatorLanguage: navigator.language returns the user's preferred language</title>
4+
<link rel="author" title="Intel" href="http://www.intel.com">
5+
<link rel="help" href="https://html.spec.whatwg.org/multipage/#navigatorlanguage">
6+
<script src="/resources/testharness.js"></script>
7+
<script src="/resources/testharnessreport.js"></script>
8+
<h2>Precondition</h2>
9+
<p>The user agent's preferred language is set as English (en).</p>
10+
<div id="log"></div>
11+
<script>
12+
test(function() {
13+
assert_equals(navigator.language, "en");
14+
});
15+
</script>
16+
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[
2+
"registerContentHandler",
3+
"isProtocolHandlerRegistered",
4+
"isContentHandlerRegistered",
5+
"unregisterContentHandler"
6+
].forEach(method => {
7+
test(() => {
8+
assert_false(method in self.navigator);
9+
}, method + "() is removed");
10+
});
11+
12+
test(() => {
13+
let called = false;
14+
self.navigator.registerProtocolHandler("web+test", "%s", { toString: () => called = true });
15+
assert_false(called);
16+
}, "registerProtocolHandler has no third argument");
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<!DOCTYPE html>
2+
<meta charset=utf-8>
3+
<title>Test for lack of indexed getter on Navigator</title>
4+
<link rel="author" title="Ms2ger" href="mailto:Ms2ger@gmail.com">
5+
<link rel="help" href="https://html.spec.whatwg.org/multipage/#the-navigator-object">
6+
<script src="/resources/testharness.js"></script>
7+
<script src="/resources/testharnessreport.js"></script>
8+
<div id="log"></div>
9+
<script>
10+
test(function() {
11+
assert_false("0" in window.navigator);
12+
assert_equals(window.navigator[0], undefined);
13+
}, "window.navigator[0] should not exist");
14+
test(function() {
15+
window.navigator[0] = "pass";
16+
assert_true("0" in window.navigator);
17+
assert_equals(window.navigator[0], "pass");
18+
}, "window.navigator[0] should be settable");
19+
test(function() {
20+
assert_false("-1" in window.navigator);
21+
assert_equals(window.navigator[-1], undefined);
22+
}, "window.navigator[-1] should not exist");
23+
test(function() {
24+
window.navigator[-1] = "pass";
25+
assert_true("-1" in window.navigator);
26+
assert_equals(window.navigator[-1], "pass");
27+
}, "window.navigator[-1] should be settable");
28+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<!DOCTYPE html>
2+
<meta charset='utf-8'>
3+
<title>navigator.windowControlsOverlay</title>
4+
5+
<script src='/resources/testharness.js'></script>
6+
<script src='/resources/testharnessreport.js'></script>
7+
8+
<script>
9+
test(function(){
10+
assert_idl_attribute(navigator, 'windowControlsOverlay');
11+
}, 'the windowControlsOverlay object should exist on the navigator object');
12+
13+
test(function(){
14+
assert_idl_attribute(navigator.windowControlsOverlay, 'visible');
15+
}, 'visible should be a member of the windowControlsOverlay object');
16+
17+
test(function(){
18+
assert_false(navigator.windowControlsOverlay.visible);
19+
}, 'visible should be false');
20+
21+
test(function(){
22+
assert_idl_attribute(navigator.windowControlsOverlay, 'getTitlebarAreaRect');
23+
}, 'getTitlebarAreaRect should be a method of the windowControlsOverlay object');
24+
25+
test(function(){
26+
var rect = navigator.windowControlsOverlay.getTitlebarAreaRect();
27+
assert_true(rect instanceof DOMRect);
28+
}, 'getTitlebarAreaRect return type should be DOMRect');
29+
30+
test(function(){
31+
var rect = navigator.windowControlsOverlay.getTitlebarAreaRect();
32+
assert_equals(rect.x, 0);
33+
assert_equals(rect.y, 0);
34+
assert_equals(rect.width, 0);
35+
assert_equals(rect.height, 0);
36+
}, 'getTitlebarAreaRect should return a empty DOMRect');
37+
38+
test(function(){
39+
assert_idl_attribute(navigator.windowControlsOverlay, 'ongeometrychange');
40+
}, 'ongeometrychange should be a member of the windowControlsOverlay object');
41+
</script>

0 commit comments

Comments
 (0)