diff --git a/package.json b/package.json
index a9299c6a..025efd4c 100644
--- a/package.json
+++ b/package.json
@@ -110,6 +110,7 @@
"typed-redux-saga": "^1.5.0",
"typescript": "~4.7.4",
"usehooks-ts": "^2.6.0",
+ "user-agent-data-types": "^0.3.0",
"web-vitals": "^2.1.4",
"webpack": "^5.73.0",
"webpack-dev-server": "^4.9.3",
diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts
index 222dc10e..3366c193 100644
--- a/src/react-app-env.d.ts
+++ b/src/react-app-env.d.ts
@@ -3,6 +3,7 @@
///
///
+///
declare namespace NodeJS {
interface ProcessEnv {
diff --git a/src/utils/os.test.ts b/src/utils/os.test.ts
index 51346573..c02617ef 100644
--- a/src/utils/os.test.ts
+++ b/src/utils/os.test.ts
@@ -1,7 +1,28 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2021 The Pybricks Authors
+// Copyright (c) 2021-2022 The Pybricks Authors
-import { isAndroid, isMacOS, isWindows } from './os';
+import { isAndroid, isIOS, isLinux, isMacOS, isWindows } from './os';
+import { defined } from '.';
+
+class TestUserAgentData implements NavigatorUAData {
+ getHighEntropyValues(_hints: string[]): Promise {
+ throw new Error('Method not implemented.');
+ }
+ toJSON(): UALowEntropyJSON {
+ throw new Error('Method not implemented.');
+ }
+ get brands(): NavigatorUABrandVersion[] {
+ throw new Error('Method not implemented.');
+ }
+ get mobile(): boolean {
+ throw new Error('Method not implemented.');
+ }
+ get platform(): string {
+ return 'test-agent';
+ }
+}
+
+Object.defineProperty(navigator, 'userAgentData', { value: new TestUserAgentData() });
afterEach(() => {
jest.resetAllMocks();
@@ -9,33 +30,59 @@ afterEach(() => {
describe('isAndroid', () => {
test('is true', () => {
- jest.spyOn(navigator, 'userAgent', 'get').mockReturnValue('Android');
+ defined(navigator.userAgentData);
+ jest.spyOn(navigator.userAgentData, 'platform', 'get').mockReturnValue(
+ 'Android',
+ );
expect(isAndroid()).toBeTruthy();
});
test('is false', () => {
- jest.spyOn(navigator, 'userAgent', 'get').mockReturnValue('Linux');
expect(isAndroid()).toBeFalsy();
});
});
describe('isMacOS', () => {
test('is true', () => {
- jest.spyOn(navigator, 'platform', 'get').mockReturnValue('MacIntel');
+ defined(navigator.userAgentData);
+ jest.spyOn(navigator.userAgentData, 'platform', 'get').mockReturnValue('macOS');
expect(isMacOS()).toBeTruthy();
});
test('is false', () => {
- jest.spyOn(navigator, 'platform', 'get').mockReturnValue('Win32');
expect(isMacOS()).toBeFalsy();
});
});
describe('isWindows', () => {
test('is true', () => {
- jest.spyOn(navigator, 'platform', 'get').mockReturnValue('Win32');
+ defined(navigator.userAgentData);
+ jest.spyOn(navigator.userAgentData, 'platform', 'get').mockReturnValue(
+ 'Windows',
+ );
expect(isWindows()).toBeTruthy();
});
test('is false', () => {
- jest.spyOn(navigator, 'platform', 'get').mockReturnValue('MacIntel');
expect(isWindows()).toBeFalsy();
});
});
+
+describe('isLinux', () => {
+ test('is true', () => {
+ defined(navigator.userAgentData);
+ jest.spyOn(navigator.userAgentData, 'platform', 'get').mockReturnValue('Linux');
+ expect(isLinux()).toBeTruthy();
+ });
+ test('is false', () => {
+ expect(isLinux()).toBeFalsy();
+ });
+});
+
+describe('isIOS', () => {
+ test('is true', () => {
+ defined(navigator.userAgentData);
+ jest.spyOn(navigator.userAgentData, 'platform', 'get').mockReturnValue('iOS');
+ expect(isIOS()).toBeTruthy();
+ });
+ test('is false', () => {
+ expect(isIOS()).toBeFalsy();
+ });
+});
diff --git a/src/utils/os.ts b/src/utils/os.ts
index 00913d16..eba07f90 100644
--- a/src/utils/os.ts
+++ b/src/utils/os.ts
@@ -1,17 +1,14 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2021 The Pybricks Authors
+// Copyright (c) 2021-2022 The Pybricks Authors
// Utility functions for dealing with operating systems.
-// TODO: replace with navigator.userAgentData when it is more widely available
-// https://developer.mozilla.org/en-US/docs/Web/API/User-Agent_Client_Hints_API
-
/**
* Tests if we are running on Android.
* @returns `true` if running on Android, otherwise `false`.
*/
export function isAndroid(): boolean {
- return /android/i.test(navigator.userAgent);
+ return navigator.userAgentData?.platform === 'Android';
}
/**
@@ -19,7 +16,7 @@ export function isAndroid(): boolean {
* @returns `true` if running on macOS, otherwise `false`.
*/
export function isMacOS(): boolean {
- return /mac/i.test(navigator.platform);
+ return navigator.userAgentData?.platform === 'macOS';
}
/**
@@ -27,5 +24,21 @@ export function isMacOS(): boolean {
* @returns `true` if running on Windows, otherwise `false`.
*/
export function isWindows(): boolean {
- return /win/i.test(navigator.platform);
+ return navigator.userAgentData?.platform === 'Windows';
+}
+
+/**
+ * Tests if we are running on Linux.
+ * @returns `true` if running on Linux, otherwise `false`.
+ */
+export function isLinux(): boolean {
+ return navigator.userAgentData?.platform === 'Linux';
+}
+
+/**
+ * Tests if we are running on iOS.
+ * @returns `true` if running on iOS, otherwise `false`.
+ */
+export function isIOS(): boolean {
+ return navigator.userAgentData?.platform === 'iOS';
}
diff --git a/yarn.lock b/yarn.lock
index db1f1ded..526c34d4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2410,6 +2410,7 @@ __metadata:
typed-redux-saga: ^1.5.0
typescript: ~4.7.4
usehooks-ts: ^2.6.0
+ user-agent-data-types: ^0.3.0
web-vitals: ^2.1.4
webpack: ^5.73.0
webpack-dev-server: ^4.9.3
@@ -14736,6 +14737,13 @@ __metadata:
languageName: node
linkType: hard
+"user-agent-data-types@npm:^0.3.0":
+ version: 0.3.0
+ resolution: "user-agent-data-types@npm:0.3.0"
+ checksum: 73a61ddfba17e7289a1312fc98c4f1f98d6353e6ace3e3159b36735e6a679b05bcb2afdfb8e602d690a89e6af44669783045ed9059f4bd0890f8e0ee8a772ee5
+ languageName: node
+ linkType: hard
+
"util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1":
version: 1.0.2
resolution: "util-deprecate@npm:1.0.2"