test(haptics): swap globalThis.navigator via defineProperty (Node 22)
CI / Build & Quality Checks (push) Successful in 1m38s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 11s
CI / Trigger Desktop Build (push) Successful in 8s
CI / Playwright smoke (e2e) (push) Successful in 9m50s

CI's Node exposes a read-only navigator getter; assigning threw
"Cannot set property navigator ... which has only a getter".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
2026-09-20 15:32:32 -04:00
co-authored by Claude Opus 5
parent 98c80f36cb
commit e3883e0fce
+17 -9
View File
@@ -2,32 +2,40 @@ import { test } from 'node:test';
import assert from 'node:assert/strict';
import { hapticsSupported, tick } from './haptics';
const g = globalThis as { navigator?: unknown; window?: unknown };
// Node 22 exposes a read-only `globalThis.navigator`; swap it via defineProperty.
const original = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
const setGlobal = (name: 'navigator' | 'window', value: unknown) =>
Object.defineProperty(globalThis, name, { value, configurable: true, writable: true });
const restore = () => {
if (original) Object.defineProperty(globalThis, 'navigator', original);
else delete (globalThis as { navigator?: unknown }).navigator;
delete (globalThis as { window?: unknown }).window;
};
test('tick is a no-op without the API, with the setting off, or under reduced motion', () => {
g.navigator = {};
setGlobal('navigator', {});
assert.equal(hapticsSupported(), false);
assert.equal(tick('ptt-on'), false);
const calls: number[] = [];
g.navigator = { vibrate: (ms: number) => calls.push(ms) === 1 };
g.window = { matchMedia: () => ({ matches: true }) };
setGlobal('navigator', { vibrate: (ms: number) => calls.push(ms) === 1 });
setGlobal('window', { matchMedia: () => ({ matches: true }) });
assert.equal(tick('ptt-on'), false);
assert.deepEqual(calls, []);
g.window = { matchMedia: () => ({ matches: false }) };
setGlobal('window', { matchMedia: () => ({ matches: false }) });
assert.equal(tick('ptt-on', false), false);
assert.deepEqual(calls, []);
restore();
});
test('tick vibrates per kind when allowed', () => {
const calls: number[] = [];
g.navigator = { vibrate: (ms: number) => calls.push(ms) > 0 };
g.window = { matchMedia: () => ({ matches: false }) };
setGlobal('navigator', { vibrate: (ms: number) => calls.push(ms) > 0 });
setGlobal('window', { matchMedia: () => ({ matches: false }) });
assert.equal(tick('ptt-on'), true);
assert.equal(tick('ptt-off'), true);
assert.equal(tick('reaction'), true);
assert.deepEqual(calls, [10, 10, 8]);
delete g.navigator;
delete g.window;
restore();
});