commit 2068fda7545e2ad5671dfa88ad1d3517971e53e6 parent a15d121cec22a3468b897b9b0272d61179b56efb Author: Przemyslaw Gorszkowski <pgorszkowski@igalia.com> Date: Thu, 4 Dec 2025 16:56:32 +0000 Bug 2003811 [wpt PR 56457] - Add test: BFCache support for a page with an open WebSocket connectio…, a=testonly Automatic update from web-platform-tests Add test: BFCache support for a page with an open WebSocket connection, but close it in pagehide -- wpt-commits: a2446707db56f0553914e63b1b06d28d10d91bac wpt-pr: 56457 Diffstat:
7 files changed, 419 insertions(+), 0 deletions(-)
diff --git a/testing/web-platform/tests/websockets/back-forward-cache-with-open-websocket-connection-and-close-it-in-pagehide.window.js b/testing/web-platform/tests/websockets/back-forward-cache-with-open-websocket-connection-and-close-it-in-pagehide.window.js @@ -0,0 +1,28 @@ +// META: title=Testing BFCache support for a page with an open WebSocket connection, but close it in pagehide. +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/utils.js +// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js +// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js +// META: script=/websockets/constants.sub.js +// META: script=resources/websockets-test-helpers.sub.js + +'use strict'; + +promise_test(async t => { + const rcHelper = new RemoteContextHelper(); + // Open a window with noopener so that BFCache will work. + const rc1 = await rcHelper.addWindow( + /*config=*/ null, /*options=*/ { features: 'noopener' }); + + await openWebSocketAndCloseItInPageHide(rc1); + + // The page should be eligible for BFCache because the WebSocket connection will be closed in `pagehide`. + // `pagehide` is dispatched before BFCache + await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ true); + + // Read WebSocket event flags + const { wsError, wsClose } = await readWebSocketCloseAndErrorFlags(rc1); + + assert_false(wsError, 'WebSocket should not have error'); + assert_true(wsClose, 'WebSocket should have been closed via pagehide'); +}); diff --git a/testing/web-platform/tests/websockets/resources/websockets-test-helpers.sub.js b/testing/web-platform/tests/websockets/resources/websockets-test-helpers.sub.js @@ -23,3 +23,36 @@ async function openThenCloseWebSocket(remoteContextHelper) { }, [SCHEME_DOMAIN_PORT]); assert_equals(return_value, 42); } + +// Opens a new WebSocket connection and close it in pagehide event listener. +async function openWebSocketAndCloseItInPageHide(remoteContextHelper) { + window.wsErrorOccurred = false; + window.wsCloseOccurred = false; + + let return_value = await remoteContextHelper.executeScript((domain) => { + return new Promise((resolve) => { + var testWebSocket = new WebSocket(domain + '/echo'); + testWebSocket.onopen = () => { + // Close WebSocket during pagehide (BFCache entry) + window.addEventListener( + 'pagehide', + () => testWebSocket.close() + ); + resolve(42); + }; + testWebSocket.onerror = () => { window.wsErrorOccurred = true; }; + testWebSocket.onclose = () => { window.wsCloseOccurred = true; }; + }); + }, [SCHEME_DOMAIN_PORT]); + assert_equals(return_value, 42); +} + +// Reads wsErrorOccurred and wsCloseOccurred from the remote context. +async function readWebSocketCloseAndErrorFlags(remoteContext) { + return await remoteContext.executeScript(() => { + return { + wsError: window.wsErrorOccurred === true, + wsClose: window.wsCloseOccurred === true + }; + }); +} diff --git a/testing/web-platform/tests/webxr/layers/xrLayerInit.https.html b/testing/web-platform/tests/webxr/layers/xrLayerInit.https.html @@ -0,0 +1,92 @@ +<!doctype html> +<title>Tests for errors with invalid XRLayerInit parameters.</title> +<link rel="help" href="https://immersive-web.github.io/layers/#dom-xrwebglbinding-createquadlayer"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="./xr_layer_promise_test.js"></script> + +<canvas id="webgl-canvas"></canvas> + +<script> + + function testCommonXRLayerInitErrors(createLayerFn, valid_init, t, gl) { + const is_webgl2 = gl instanceof WebGL2RenderingContext; + return new Promise((resolve, reject) => { + const max_texture_size = gl.getParameter(gl.MAX_TEXTURE_SIZE); + if (!is_webgl2) { + // Check an exception on texture-array. + t.step(() => { + // texture-array supported for webgl2 only. + let invalid_texture_type = Object.assign({}, valid_init, { textureType: 'texture-array' }); + assert_throws_js(TypeError, () => createLayerFn(invalid_texture_type), "texture-array for webgl2 only"); + }); + + // Check an exception on webgl2 color formats. + t.step(() => { + [ + 0x8058, // GL_RGBA8 + 0x8051, // GL_RGB8 + 0x8C41, // GL_SRGB8 + 0x8C43 // GL_SRGB8_ALPHA8 + ].forEach((colorFormat) => { + let invalid_color_format = Object.assign({}, valid_init, { colorFormat }); + assert_throws_js(TypeError, () => createLayerFn(invalid_color_format), "colorFormat for webgl2 only"); + }); + }); + } + + t.step(() => { + // Check an exception for invalid transform object. + let invalid_transform = Object.assign({}, valid_init, { transform: { x: 0, y: 0, z: 0 } }); + assert_throws_js(TypeError, () => createLayerFn(invalid_transform), "Invalid transform object"); + }); + + t.step(() => { + // viewPixelWidth and viewPixelHeight must be greater than 0. + let invalid_pixel_width = Object.assign({}, valid_init, { viewPixelWidth: 0 }); + assert_throws_js(TypeError, () => createLayerFn(invalid_pixel_width), "viewPixelWidth is 0"); + }); + + t.step(() => { + let invalid_pixel_height = Object.assign({}, valid_init, { viewPixelHeight: 0 }); + assert_throws_js(TypeError, () => createLayerFn(invalid_pixel_height), "viewPixelHeight is 0"); + }); + + // viewPixelWidth and viewPixelHeight must not be too large. + t.step(() => { + let large_pixel_width = Object.assign({}, valid_init, { viewPixelWidth: max_texture_size + 1 }); + assert_throws_js(TypeError, () => createLayerFn(large_pixel_width), "viewPixelWidth is too large"); + }); + + t.step(() => { + let large_pixel_height = Object.assign({}, valid_init, { viewPixelHeight: max_texture_size + 1 }); + assert_throws_js(TypeError, () => createLayerFn(large_pixel_height), "viewPixelHeight is too large"); + }); + + // Layout cannot be 'default'. + t.step(() => { + let invalid_layout = Object.assign({}, valid_init, { layout: 'default' }); + assert_throws_js(TypeError, () => createLayerFn(invalid_layout), "layout is 'default'"); + resolve(); + }); + }); + }; + + function testCompositionLayer(xrSession, deviceController, t, { gl, xrBinding, xrSpace }) { + const valid_init = { + space: xrSpace, + viewPixelWidth: 1024, + viewPixelHeight: 1024 + }; + return Promise.resolve() + .then(testCommonXRLayerInitErrors(xrBinding.createQuadLayer.bind(xrBinding), valid_init, t, gl)) + .then(testCommonXRLayerInitErrors(xrBinding.createCylinderLayer.bind(xrBinding), valid_init, t, gl)) + .then(testCommonXRLayerInitErrors(xrBinding.createEquirectLayer.bind(xrBinding), valid_init, t, gl)); + } + + // This method tests XRLayerInit parameters, which are common to the Quad,Cylinder and Equirect layers. + xr_layer_promise_test("Ensure XrWebGLBinding's create layer methods throw the appropriate errors.", + testCompositionLayer, TRACKED_IMMERSIVE_DEVICE, 'immersive-vr', { requiredFeatures: ['layers'] }); +</script> +\ No newline at end of file diff --git a/testing/web-platform/tests/webxr/layers/xrWebGLBinding_createCylinderLayer.https.html b/testing/web-platform/tests/webxr/layers/xrWebGLBinding_createCylinderLayer.https.html @@ -0,0 +1,57 @@ +<!doctype html> +<title>XRWebGLBinding::createCylinderLayer</title> +<link rel="help" href="https://immersive-web.github.io/layers/#dom-xrwebglbinding-createquadlayer"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="./xr_layer_promise_test.js"></script> + +<canvas id="webgl-canvas"></canvas> + +<script> + + function testCreateCylinderLayer(xrSession, deviceController, t, { gl, xrBinding, xrSpace }) { + return new Promise((resolve, reject) => { + const valid_init = { + space: xrSpace, + viewPixelWidth: 1024, + viewPixelHeight: 1024 + }; + + t.step(() => { + // Cylinder radius must be greater than or equal to 0. + let invalid_radius = Object.assign({}, valid_init, { radius: -5 }); + assert_throws_js(TypeError, () => xrBinding.createCylinderLayer(invalid_radius), "radius is negative"); + }); + + t.step(() => { + // Cylinder aspectRatio must be greater than 0. + let invalid_aspect_ratio = Object.assign({}, valid_init, { aspectRatio: 0 }); + assert_throws_js(TypeError, () => xrBinding.createCylinderLayer(invalid_aspect_ratio), "aspectRatio is 0"); + }); + + t.step(() => { + // Cylinder central angle must be greater than or equal to 0. + let invalid_central_angle = Object.assign({}, valid_init, { centralAngle: -6 }); + assert_throws_js(TypeError, () => xrBinding.createCylinderLayer(invalid_central_angle), "centralAngle is negative"); + }); + + t.step(() => { + // Cylinder central angle should not be greater than 2pi. + let invalid_central_angle = Object.assign({}, valid_init, { centralAngle: 7.0 }); + assert_throws_js(TypeError, () => xrBinding.createCylinderLayer(invalid_central_angle), "centralAngle is greater than 2pi"); + }); + + // Test that a valid init works. + t.step(() => { + const layer = xrBinding.createCylinderLayer(valid_init); + assert_true(layer instanceof XRCylinderLayer, "Valid init parameters must create an XRCylinderLayer"); + resolve(); + }); + }); + } + + xr_layer_promise_test("Ensure XrWebGLBinding::createCylinderLayer throws the appropriate errors", + testCreateCylinderLayer, TRACKED_IMMERSIVE_DEVICE, 'immersive-vr', { requiredFeatures: ['layers'] }); +</script> diff --git a/testing/web-platform/tests/webxr/layers/xrWebGLBinding_createEquirectLayer.https.html b/testing/web-platform/tests/webxr/layers/xrWebGLBinding_createEquirectLayer.https.html @@ -0,0 +1,78 @@ +<!doctype html> +<title>XRWebGLBinding::createEquirectLayer</title> +<link rel="help" href="https://immersive-web.github.io/layers/#dom-xrwebglbinding-createquadlayer"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="./xr_layer_promise_test.js"></script> + +<canvas id="webgl-canvas"></canvas> + +<script> + + function testCreateEquirectLayer(xrSession, deviceController, t, { gl, xrBinding, xrSpace }) { + return new Promise((resolve, reject) => { + const valid_init = { + space: xrSpace, + viewPixelWidth: 1024, + viewPixelHeight: 1024 + }; + + t.step(() => { + // Check that create method clamps negative radius and angles according to spec. + let init_clamp_values = Object.assign({}, valid_init, { + radius: -2, + centralHorizontalAngle: -5, + upperVerticalAngle: -3, + lowerVerticalAngle: -3 + }); + + let layer = xrBinding.createEquirectLayer(init_clamp_values); + assert_equals(layer.radius, 0, "radius should be zero."); + assert_equals(layer.centralHorizontalAngle, 0, + "centralHorizontalAngle should be zero."); + assert_true( + Math.abs(layer.upperVerticalAngle + Math.PI / 2) < FLOAT_EPSILON, + "upperVerticalAngle should be -pi/2."); + assert_true( + Math.abs(layer.lowerVerticalAngle + Math.PI / 2) < FLOAT_EPSILON, + "lowerVerticalAngle should be -pi/2."); + }); + + t.step(() => { + // Check that create method clamps negative radius and angles according to spec. + let init_clamp_values = Object.assign({}, valid_init, { + radius: 2, + centralHorizontalAngle: 7, + upperVerticalAngle: 3, + lowerVerticalAngle: 3 + }); + + let layer = xrBinding.createEquirectLayer(init_clamp_values); + assert_equals(layer.radius, 2, + "The radius value should be the same as provided to the init."); + assert_true( + Math.abs(layer.centralHorizontalAngle - 2 * Math.PI) < FLOAT_EPSILON, + "centralHorizontalAngle should not be greater than a 2*pi."); + assert_true( + Math.abs(layer.upperVerticalAngle - Math.PI / 2) < FLOAT_EPSILON, + "upperVerticalAngle should be pi/2."); + assert_true( + Math.abs(layer.lowerVerticalAngle - Math.PI / 2) < FLOAT_EPSILON, + "lowerVerticalAngle should be pi/2."); + }); + + // Test that a valid init works. + t.step(() => { + const layer = xrBinding.createEquirectLayer(valid_init); + assert_true(layer instanceof XREquirectLayer, "Valid init parameters must create an XREquirectLayer."); + resolve(); + }); + }); + } + + xr_layer_promise_test("Ensure XrWebGLBinding::createEquirectLayer throws the appropriate errors", + testCreateEquirectLayer, TRACKED_IMMERSIVE_DEVICE, 'immersive-vr', { requiredFeatures: ['layers'] }); + +</script> diff --git a/testing/web-platform/tests/webxr/layers/xrWebGLBinding_createQuadLayer.https.html b/testing/web-platform/tests/webxr/layers/xrWebGLBinding_createQuadLayer.https.html @@ -0,0 +1,43 @@ +<!doctype html> +<title>XRWebGLBinding::createQuadLayer</title> +<link rel="help" href="https://immersive-web.github.io/layers/#dom-xrwebglbinding-createquadlayer"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="./xr_layer_promise_test.js"></script> + +<canvas id="webgl-canvas"></canvas> + +<script> + function testCreateQuadLayer(xrSession, deviceController, t, { gl, xrBinding, xrSpace }) { + return new Promise((resolve, reject) => { + const valid_init = { + space: xrSpace, + viewPixelWidth: 1024, + viewPixelHeight: 1024 + }; + // Width and height must be greater than 0. + t.step(() => { + let invalid_width = Object.assign({}, valid_init, { width: 0 }); + assert_throws_js(TypeError, () => xrBinding.createQuadLayer(invalid_width), "width is 0"); + }); + + t.step(() => { + let invalid_height = Object.assign({}, valid_init, { height: 0 }); + assert_throws_js(TypeError, () => xrBinding.createQuadLayer(invalid_height), "height is 0"); + }); + + // Test that a valid init works. + t.step(() => { + const layer = xrBinding.createQuadLayer(valid_init); + assert_true(layer instanceof XRQuadLayer, "Valid init parameters must create an XRQuadLayer"); + resolve(); + }); + }); + } + + xr_layer_promise_test("Ensure XrWebGLBinding::createQuadLayer throws the appropriate errors.", + testCreateQuadLayer, TRACKED_IMMERSIVE_DEVICE, 'immersive-vr', { requiredFeatures: ['layers'] }); + +</script> diff --git a/testing/web-platform/tests/webxr/layers/xr_layer_promise_test.js b/testing/web-platform/tests/webxr/layers/xr_layer_promise_test.js @@ -0,0 +1,87 @@ +'use strict'; + +// A test function that runs the common steps for requesting an XR session. +// After the session is created, it is initialize the XRWebGLBinding +// and local XRSpace objects for the session. These components are essential +// for tests involving WebXR layers. +function xr_layer_promise_test( + name, func, fakeDeviceInit, sessionMode, sessionInit, properties, + glcontextPropertiesParam) { + const glcontextProperties = (glcontextPropertiesParam) ? glcontextPropertiesParam : {}; + + function runTest(t, glContext) { + let testSession; + let testDeviceController; + let sessionObjects = {gl: glContext}; + + // Ensure that any pending sessions are ended when done. This needs to + // use a cleanup function to ensure proper sequencing. If this were + // done in a .then() for the success case, a test that expected + // failure would already be marked done at the time that runs, and the + // shutdown would interfere with the next test which may have started. + t.add_cleanup(async () => { + // If a session was created, end it. + if (testSession) { + await testSession.end().catch(() => {}); + } + }); + + return navigator.xr.test.simulateDeviceConnection(fakeDeviceInit) + .then((controller) => { + testDeviceController = controller; + return sessionObjects.gl.makeXRCompatible(); + }) + .then(() => new Promise((resolve, reject) => { + // Perform the session request in a user gesture. + xr_debug(name, 'simulateUserActivation'); + navigator.xr.test.simulateUserActivation(() => { + xr_debug(name, 'document.hasFocus()=' + document.hasFocus()); + navigator.xr.requestSession(sessionMode, sessionInit || {}) + .then(async (session) => { + xr_debug(name, 'session start'); + testSession = session; + session.mode = sessionMode; + session.sessionInit = sessionInit; + // This method creates test specific session objects. + sessionObjects.xrBinding = new XRWebGLBinding(session, sessionObjects.gl); + // Request a 'local' reference space which is required for layers creation. + sessionObjects.xrSpace = await session.requestReferenceSpace('local'); + if (!sessionObjects.xrSpace) { + reject("Local space is required for layers test."); + return; + } + xr_debug(name, 'session.visibilityState=' + session.visibilityState); + try { + resolve(func(session, testDeviceController, t, sessionObjects)); + } catch(err) { + reject("Test function failed with: " + err); + } + }) + .catch((err) => { + xr_debug(name, 'error: ' + err); + reject( + 'Session with params ' + + JSON.stringify(sessionMode) + + ' was rejected on device ' + + JSON.stringify(fakeDeviceInit) + + ' with error: ' + err); + }); + }); + })); + } + + xr_promise_test( + name + ' - webgl', + runTest, + properties, + 'webgl', + {alpha: false, antialias: false, ...glcontextProperties} + ); + xr_promise_test( + name + ' - webgl2', + runTest, + properties, + 'webgl2', + {alpha: false, antialias: false, ...glcontextProperties} + ); +}