Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion deps/cloudxr/webxr_client/helpers/LoadIWER.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ describe('loadIWERIfNeeded', () => {
} else {
delete (navigator as Navigator & { xr?: XRSystem }).xr;
}
delete testWindow.xrDevice;
delete testWindow.IWER;
delete testWindow.IWER_DevUI;
jest.restoreAllMocks();
Expand Down Expand Up @@ -79,7 +80,9 @@ describe('loadIWERIfNeeded', () => {

const device = {
installDevUI: jest.fn(),
installRuntime: jest.fn(),
installRuntime: jest.fn(() => {
isSessionSupported.mockResolvedValue(true);
}),
};
testWindow.IWER = {
XRDevice: jest.fn(() => device),
Expand Down Expand Up @@ -107,6 +110,38 @@ describe('loadIWERIfNeeded', () => {
expect(appendChild).toHaveBeenCalledTimes(2);
});

it('rejects a skipped runtime installation and allows a later retry', async () => {
const nativeXR = { isSessionSupported: jest.fn().mockResolvedValue(false) };
Object.defineProperty(navigator, 'xr', { configurable: true, value: nativeXR });
const device = { installRuntime: jest.fn() };
testWindow.IWER = { XRDevice: jest.fn(() => device), metaQuest3: {} };
jest.spyOn(document.head, 'appendChild').mockImplementation(<T extends Node>(node: T): T => {
if (node instanceof HTMLScriptElement) {
queueMicrotask(() => node.onload?.(new Event('load')));
}
return node;
});

await expect(loadIWERIfNeeded()).resolves.toEqual({
supportsImmersive: false,
iwerLoaded: false,
});
expect(window.xrDevice).toBeUndefined();
expect(navigator.xr).toBe(nativeXR);

device.installRuntime.mockImplementation(() => {
Object.defineProperty(navigator, 'xr', {
configurable: true,
value: { isSessionSupported: jest.fn().mockResolvedValue(true) },
});
});
await expect(loadIWERIfNeeded()).resolves.toEqual({
supportsImmersive: true,
iwerLoaded: true,
});
expect(window.xrDevice).toBe(device);
});

it('shares one IWER installation across concurrent callers', async () => {
let appendedScripts = 0;
let constructedDevices = 0;
Expand All @@ -116,6 +151,10 @@ describe('loadIWERIfNeeded', () => {
installDevUI: jest.fn(),
installRuntime: jest.fn(() => {
runtimeInstalls++;
Object.defineProperty(navigator, 'xr', {
configurable: true,
value: { isSessionSupported: jest.fn().mockResolvedValue(true) },
});
}),
};
testWindow.IWER = {
Expand Down
33 changes: 19 additions & 14 deletions deps/cloudxr/webxr_client/helpers/LoadIWER.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ declare global {
}

// Both versions are loaded from CDN; bump them together when upgrading IWER.
const IWER_version = '2.3.0';
const IWER_DEVUI_version = '2.3.0';
const IWER_version = '2.7.0';
const IWER_DEVUI_version = '2.7.0';
let pendingIWERLoad: Promise<IWERLoadResult> | null = null;

export interface IWERLoadResult {
Expand Down Expand Up @@ -65,7 +65,7 @@ async function loadIWER(): Promise<IWERLoadResult> {
const script = document.createElement('script');
script.src = `https://unpkg.com/iwer@${IWER_version}/build/iwer.min.js`;
script.async = true;
script.integrity = 'sha384-m8Xcl9WwdP6j/5Fv7MAs7IvvW3PmWCbNZlbF8IsJAnujyeMZi0/xgUE4mM7Tbm/j';
script.integrity = 'sha384-pcGii0Tzet8SSL0v/VOhYIzm2dynTF4sEDPQxzsbepa8wICqZkNQvx3xuRRjj6Lc';
script.crossOrigin = 'anonymous';

await new Promise<void>(resolve => {
Expand All @@ -84,7 +84,7 @@ async function loadIWER(): Promise<IWERLoadResult> {
devUIScript.src = `https://unpkg.com/@iwer/devui@${IWER_DEVUI_version}/build/iwer-devui.min.js`;
devUIScript.async = true;
devUIScript.integrity =
'sha384-TAu5gWlv92O6gkVclV8HYctTEswyA/0eoS6ULHjjLCShUVts4kDlUg6iLEKmL1r8';
'sha384-9jc9tp/DypE1hvhqLzE0xVpXYx64QqIRMDu6f3FkSIaeTqpwniy+t1ierx3y428X';
devUIScript.crossOrigin = 'anonymous';

await new Promise<void>(devUIResolve => {
Expand All @@ -110,17 +110,22 @@ async function loadIWER(): Promise<IWERLoadResult> {
console.warn('IWER DevUI not found after script load, continuing without DevUI.');
}

// This fallback runs only after the native XRSystem reports no immersive
// support. IWER 2.3 otherwise preserves that unusable navigator.xr.
// Only override the native runtime after it reports no immersive support.
await device.installRuntime({ forceInstall: true });
window.xrDevice = device;
supportsImmersive = true;
iwerLoaded = true;
// The only log that's both unconditional (fires whether or not DevUI loaded, unlike
// "IWER DevUI initialized...") and guaranteed to follow installRuntime() actually
// succeeding (unlike "IWER DevUI initialized...", which logs before this await) - the
// one a caller (e.g. a test) should wait on to know navigator.xr is truly usable.
console.info('IWER runtime installed.');
// installRuntime can return without replacing navigator.xr; do not
// enable Connect until the installed runtime confirms session support.
const [vr, ar] = await Promise.all([
navigator.xr?.isSessionSupported('immersive-vr'),
navigator.xr?.isSessionSupported('immersive-ar'),
]);
supportsImmersive = Boolean(vr || ar);
if (supportsImmersive) {
window.xrDevice = device;
iwerLoaded = true;
console.info('IWER runtime installed.');
} else {
console.warn('IWER runtime installed without immersive session support.');
}
} catch (e) {
console.warn('IWER runtime install failed:', e);
supportsImmersive = false;
Expand Down
2 changes: 1 addition & 1 deletion deps/cloudxr/webxr_client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"html-webpack-plugin": "^5.6.7",
"iwer": "^2.3.0",
"iwer": "^2.7.0",
"jest": "^30",
"jest-environment-jsdom": "^30.4.1",
"prettier": "3.5.3",
Expand Down
Loading