summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webxr/resources
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/webxr/resources')
-rw-r--r--testing/web-platform/tests/webxr/resources/webxr_check.html17
-rw-r--r--testing/web-platform/tests/webxr/resources/webxr_math_utils.js67
-rw-r--r--testing/web-platform/tests/webxr/resources/webxr_test_asserts.js185
-rw-r--r--testing/web-platform/tests/webxr/resources/webxr_test_constants.js205
-rw-r--r--testing/web-platform/tests/webxr/resources/webxr_test_constants_fake_depth.js78
-rw-r--r--testing/web-platform/tests/webxr/resources/webxr_test_constants_fake_world.js79
-rw-r--r--testing/web-platform/tests/webxr/resources/webxr_util.js241
7 files changed, 872 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webxr/resources/webxr_check.html b/testing/web-platform/tests/webxr/resources/webxr_check.html
new file mode 100644
index 0000000000..2d8e5b387d
--- /dev/null
+++ b/testing/web-platform/tests/webxr/resources/webxr_check.html
@@ -0,0 +1,17 @@
+<script src=webxr_util.js></script>
+<script>
+'use strict';
+let definedObjects = [];
+let undefinedObjects = [];
+
+forEachWebxrObject((obj, name) => {
+ if(obj == undefined) {
+ undefinedObjects.push(name);
+ } else {
+ definedObjects.push(name);
+ }
+});
+
+window.parent.postMessage({ undefinedObjects, definedObjects}, '*');
+
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/webxr/resources/webxr_math_utils.js b/testing/web-platform/tests/webxr/resources/webxr_math_utils.js
new file mode 100644
index 0000000000..54a61c1854
--- /dev/null
+++ b/testing/web-platform/tests/webxr/resources/webxr_math_utils.js
@@ -0,0 +1,67 @@
+// |matrix| - Float32Array, |input| - point-like dict (must have x, y, z, w)
+let transform_point_by_matrix = function(matrix, input) {
+ return {
+ x : matrix[0] * input.x + matrix[4] * input.y + matrix[8] * input.z + matrix[12] * input.w,
+ y : matrix[1] * input.x + matrix[5] * input.y + matrix[9] * input.z + matrix[13] * input.w,
+ z : matrix[2] * input.x + matrix[6] * input.y + matrix[10] * input.z + matrix[14] * input.w,
+ w : matrix[3] * input.x + matrix[7] * input.y + matrix[11] * input.z + matrix[15] * input.w,
+ };
+}
+
+// Creates a unit-length quaternion.
+// |input| - point-like dict (must have x, y, z, w)
+let normalize_quaternion = function(input) {
+ const length_squared = input.x * input.x + input.y * input.y + input.z * input.z + input.w * input.w;
+ const length = Math.sqrt(length_squared);
+
+ return {x : input.x / length, y : input.y / length, z : input.z / length, w : input.w / length};
+}
+
+// Returns negated quaternion.
+// |input| - point-like dict (must have x, y, z, w)
+let flip_quaternion = function(input) {
+ return {x : -input.x, y : -input.y, z : -input.z, w : -input.w};
+}
+
+// |input| - point-like dict (must have x, y, z, w)
+let conjugate_quaternion = function(input) {
+ return {x : -input.x, y : -input.y, z : -input.z, w : input.w};
+}
+
+let multiply_quaternions = function(q1, q2) {
+ return {
+ w : q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z,
+ x : q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y,
+ y : q1.w * q2.y - q1.x * q2.z + q1.y * q2.w + q1.z * q2.x,
+ z : q1.w * q2.z + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w,
+ }
+}
+
+// |point| - point-like dict (must have x, y, z, w)
+let normalize_perspective = function(point) {
+ if(point.w == 0 || point.w == 1) return point;
+
+ return {
+ x : point.x / point.w,
+ y : point.y / point.w,
+ z : point.z / point.w,
+ w : 1
+ };
+}
+
+// |quaternion| - point-like dict (must have x, y, z, w),
+// |input| - point-like dict (must have x, y, z, w)
+let transform_point_by_quaternion = function(quaternion, input) {
+ const q_normalized = normalize_quaternion(quaternion);
+ const q_conj = conjugate_quaternion(q_normalized);
+ const p_in = normalize_perspective(input);
+
+ // construct a quaternion out of the point (take xyz & zero the real part).
+ const p = {x : p_in.x, y : p_in.y, z : p_in.z, w : 0};
+
+ // transform the input point
+ const p_mul = multiply_quaternions( q_normalized, multiply_quaternions(p, q_conj) );
+
+ // add back the w component of the input
+ return { x : p_mul.x, y : p_mul.y, z : p_mul.z, w : p_in.w };
+}
diff --git a/testing/web-platform/tests/webxr/resources/webxr_test_asserts.js b/testing/web-platform/tests/webxr/resources/webxr_test_asserts.js
new file mode 100644
index 0000000000..a82f7aaf90
--- /dev/null
+++ b/testing/web-platform/tests/webxr/resources/webxr_test_asserts.js
@@ -0,0 +1,185 @@
+// Utility assert functions.
+// Relies on resources/testharness.js to be included before this file.
+// Relies on webxr_test_constants.js to be included before this file.
+// Relies on webxr_math_utils.js to be included before this file.
+
+
+// |p1|, |p2| - objects with x, y, z, w components that are floating point numbers.
+// Returns the name of mismatching component between p1 and p2.
+const get_mismatched_component = function(p1, p2, epsilon = FLOAT_EPSILON) {
+ for (const v of ['x', 'y', 'z', 'w']) {
+ if (Math.abs(p1[v] - p2[v]) > epsilon) {
+ return v;
+ }
+ }
+
+ return null;
+}
+
+// |p1|, |p2| - objects with x, y, z, w components that are floating point numbers.
+// |epsilon| - float specifying precision
+// |prefix| - string used as a prefix for logging
+const assert_point_approx_equals = function(p1, p2, epsilon = FLOAT_EPSILON, prefix = "") {
+ if (p1 == null && p2 == null) {
+ return;
+ }
+
+ assert_not_equals(p1, null, prefix + "p1 must be non-null");
+ assert_not_equals(p2, null, prefix + "p2 must be non-null");
+
+ const mismatched_component = get_mismatched_component(p1, p2, epsilon);
+
+ if (mismatched_component !== null) {
+ let error_message = prefix + ' Point comparison failed.\n';
+ error_message += ` p1: {x: ${p1.x}, y: ${p1.y}, z: ${p1.z}, w: ${p1.w}}\n`;
+ error_message += ` p2: {x: ${p2.x}, y: ${p2.y}, z: ${p2.z}, w: ${p2.w}}\n`;
+ error_message += ` Difference in component ${mismatched_component} exceeded the given epsilon.\n`;
+ assert_approx_equals(p2[mismatched_component], p1[mismatched_component], epsilon, error_message);
+ }
+};
+
+const assert_orientation_approx_equals = function(q1, q2, epsilon = FLOAT_EPSILON, prefix = "") {
+ if (q1 == null && q2 == null) {
+ return;
+ }
+
+ assert_not_equals(q1, null, prefix + "q1 must be non-null");
+ assert_not_equals(q2, null, prefix + "q2 must be non-null");
+
+ const q2_flipped = flip_quaternion(q2);
+
+ const mismatched_component = get_mismatched_component(q1, q2, epsilon);
+ const mismatched_component_flipped = get_mismatched_component(q1, q2_flipped, epsilon);
+
+ if (mismatched_component !== null && mismatched_component_flipped !== null) {
+ // q1 doesn't match neither q2 nor -q2, so it definitely does not represent the same orientations,
+ // log an assert failure.
+ let error_message = prefix + ' Orientation comparison failed.\n';
+ error_message += ` p1: {x: ${q1.x}, y: ${q1.y}, z: ${q1.z}, w: ${q1.w}}\n`;
+ error_message += ` p2: {x: ${q2.x}, y: ${q2.y}, z: ${q2.z}, w: ${q2.w}}\n`;
+ error_message += ` Difference in component ${mismatched_component} exceeded the given epsilon.\n`;
+ assert_approx_equals(q2[mismatched_component], q1[mismatched_component], epsilon, error_message);
+ }
+}
+
+// |p1|, |p2| - objects with x, y, z, w components that are floating point numbers.
+// |epsilon| - float specifying precision
+// |prefix| - string used as a prefix for logging
+const assert_point_significantly_not_equals = function(p1, p2, epsilon = FLOAT_EPSILON, prefix = "") {
+
+ assert_not_equals(p1, null, prefix + "p1 must be non-null");
+ assert_not_equals(p2, null, prefix + "p2 must be non-null");
+
+ let mismatched_component = get_mismatched_component(p1, p2, epsilon);
+
+ if (mismatched_component === null) {
+ let error_message = prefix + ' Point comparison failed.\n';
+ error_message += ` p1: {x: ${p1.x}, y: ${p1.y}, z: ${p1.z}, w: ${p1.w}}\n`;
+ error_message += ` p2: {x: ${p2.x}, y: ${p2.y}, z: ${p2.z}, w: ${p2.w}}\n`;
+ error_message += ` Difference in components did not exceeded the given epsilon.\n`;
+ assert_unreached(error_message);
+ }
+};
+
+// |t1|, |t2| - objects containing position and orientation.
+// |epsilon| - float specifying precision
+// |prefix| - string used as a prefix for logging
+const assert_transform_approx_equals = function(t1, t2, epsilon = FLOAT_EPSILON, prefix = "") {
+ if (t1 == null && t2 == null) {
+ return;
+ }
+
+ assert_not_equals(t1, null, prefix + "t1 must be non-null");
+ assert_not_equals(t2, null, prefix + "t2 must be non-null");
+
+ assert_point_approx_equals(t1.position, t2.position, epsilon, prefix + "positions must be equal");
+ assert_orientation_approx_equals(t1.orientation, t2.orientation, epsilon, prefix + "orientations must be equal");
+};
+
+// |m1|, |m2| - arrays of floating point numbers
+// |epsilon| - float specifying precision
+// |prefix| - string used as a prefix for logging
+const assert_matrix_approx_equals = function(m1, m2, epsilon = FLOAT_EPSILON, prefix = "") {
+ if (m1 == null && m2 == null) {
+ return;
+ }
+
+ assert_not_equals(m1, null, prefix + "m1 must be non-null");
+ assert_not_equals(m2, null, prefix + "m2 must be non-null");
+
+ assert_equals(m1.length, 16, prefix + "m1 must have length of 16");
+ assert_equals(m2.length, 16, prefix + "m2 must have length of 16");
+
+ let mismatched_element = -1;
+ for (let i = 0; i < 16; ++i) {
+ if (Math.abs(m1[i] - m2[i]) > epsilon) {
+ mismatched_element = i;
+ break;
+ }
+ }
+
+ if (mismatched_element > -1) {
+ let error_message = prefix + 'Matrix comparison failed.\n';
+ error_message += ' Difference in element ' + mismatched_element +
+ ' exceeded the given epsilon.\n';
+
+ error_message += ' Matrix 1: [' + m1.join(',') + ']\n';
+ error_message += ' Matrix 2: [' + m2.join(',') + ']\n';
+
+ assert_approx_equals(
+ m1[mismatched_element], m2[mismatched_element], epsilon,
+ error_message);
+ }
+};
+
+// |m1|, |m2| - arrays of floating point numbers
+// |epsilon| - float specifying precision
+// |prefix| - string used as a prefix for logging
+const assert_matrix_significantly_not_equals = function(m1, m2, epsilon = FLOAT_EPSILON, prefix = "") {
+ if (m1 == null && m2 == null) {
+ return;
+ }
+
+ assert_not_equals(m1, null, prefix + "m1 must be non-null");
+ assert_not_equals(m2, null, prefix + "m2 must be non-null");
+
+ assert_equals(m1.length, 16, prefix + "m1 must have length of 16");
+ assert_equals(m2.length, 16, prefix + "m2 must have length of 16");
+
+ let mismatch = false;
+ for (let i = 0; i < 16; ++i) {
+ if (Math.abs(m1[i] - m2[i]) > epsilon) {
+ mismatch = true;
+ break;
+ }
+ }
+
+ if (!mismatch) {
+ let m1_str = '[';
+ let m2_str = '[';
+ for (let i = 0; i < 16; ++i) {
+ m1_str += m1[i] + (i < 15 ? ', ' : '');
+ m2_str += m2[i] + (i < 15 ? ', ' : '');
+ }
+ m1_str += ']';
+ m2_str += ']';
+
+ let error_message = prefix + 'Matrix comparison failed.\n';
+ error_message +=
+ ' No element exceeded the given epsilon ' + epsilon + '.\n';
+
+ error_message += ' Matrix A: ' + m1_str + '\n';
+ error_message += ' Matrix B: ' + m2_str + '\n';
+
+ assert_unreached(error_message);
+ }
+};
+
+// |r1|, |r2| - XRRay objects
+// |epsilon| - float specifying precision
+// |prefix| - string used as a prefix for logging
+const assert_ray_approx_equals = function(r1, r2, epsilon = FLOAT_EPSILON, prefix = "") {
+ assert_point_approx_equals(r1.origin, r2.origin, epsilon, prefix + "origin:");
+ assert_point_approx_equals(r1.direction, r2.direction, epsilon, prefix + "direction:");
+ assert_matrix_approx_equals(r1.matrix, r2.matrix, epsilon, prefix + "matrix:");
+};
diff --git a/testing/web-platform/tests/webxr/resources/webxr_test_constants.js b/testing/web-platform/tests/webxr/resources/webxr_test_constants.js
new file mode 100644
index 0000000000..d0d4fa2747
--- /dev/null
+++ b/testing/web-platform/tests/webxr/resources/webxr_test_constants.js
@@ -0,0 +1,205 @@
+// assert_equals can fail when comparing floats due to precision errors, so
+// use assert_approx_equals with this constant instead
+const FLOAT_EPSILON = 0.001;
+
+// Identity matrix
+const IDENTITY_MATRIX = [1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, 0, 1];
+
+const IDENTITY_TRANSFORM = {
+ position: [0, 0, 0],
+ orientation: [0, 0, 0, 1],
+};
+
+// A valid pose matrix/transform for when we don't care about specific values
+// Note that these two should be identical, just different representations
+const VALID_POSE_MATRIX = [0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 1, 0, 0, 0,
+ 1, 1, 1, 1];
+
+const VALID_POSE_TRANSFORM = {
+ position: [1, 1, 1],
+ orientation: [0.5, 0.5, 0.5, 0.5]
+};
+
+const VALID_PROJECTION_MATRIX =
+ [1, 0, 0, 0, 0, 1, 0, 0, 3, 2, -1, -1, 0, 0, -0.2, 0];
+
+// This is a decomposed version of the above.
+const VALID_FIELD_OF_VIEW = {
+ upDegrees: 71.565,
+ downDegrees: -45,
+ leftDegrees:-63.4349,
+ rightDegrees: 75.9637
+};
+
+// A valid input grip matrix for when we don't care about specific values
+const VALID_GRIP = [1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 4, 3, 2, 1];
+
+const VALID_GRIP_TRANSFORM = {
+ position: [4, 3, 2],
+ orientation: [0, 0, 0, 1]
+};
+
+// A valid input pointer offset for when we don't care about specific values
+const VALID_POINTER = [1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, 1, 1];
+
+const VALID_POINTER_TRANSFORM = {
+ position: [0, 0, 1],
+ orientation: [0, 0, 0, 1]
+};
+
+// A Valid Local to floor matrix/transform for when we don't care about specific
+// values. Note that these should be identical, just different representations.
+const VALID_FLOOR_ORIGIN_MATRIX = [1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 1, 1.65, -1, 1];
+
+const VALID_FLOOR_ORIGIN = {
+ position: [-1.0, -1.65, 1.0],
+ orientation: [0, 0, 0, 1]
+};
+
+const VALID_BOUNDS = [
+ { x: 3.0, z: -2.0 },
+ { x: 3.5, z: 0.0 },
+ { x: 3.0, z: 2.0 },
+ { x: -3.0, z: 2.0 },
+ { x: -3.5, z: 0.0 },
+ { x: -3.0, z: -2.0 }
+];
+
+const VALID_RESOLUTION = {
+ width: 200,
+ height: 200
+};
+
+const LEFT_OFFSET = {
+ position: [-0.1, 0, 0],
+ orientation: [0, 0, 0, 1]
+};
+
+const RIGHT_OFFSET = {
+ position: [0.1, 0, 0],
+ orientation: [0, 0, 0, 1]
+};
+
+const FIRST_PERSON_OFFSET = {
+ position: [0, 0.1, 0],
+ orientation: [0, 0, 0, 1]
+};
+
+const VALID_VIEWS = [{
+ eye:"left",
+ projectionMatrix: VALID_PROJECTION_MATRIX,
+ viewOffset: LEFT_OFFSET,
+ resolution: VALID_RESOLUTION
+ }, {
+ eye:"right",
+ projectionMatrix: VALID_PROJECTION_MATRIX,
+ viewOffset: RIGHT_OFFSET,
+ resolution: VALID_RESOLUTION
+ },
+];
+
+const VALID_SECONDARY_VIEWS = [{
+ eye: "none",
+ projectionMatrix: VALID_PROJECTION_MATRIX,
+ viewOffset: FIRST_PERSON_OFFSET,
+ resolution: VALID_RESOLUTION,
+ isFirstPersonObserver: true
+ }
+];
+
+const NON_IMMERSIVE_VIEWS = [{
+ eye: "none",
+ projectionMatrix: VALID_PROJECTION_MATRIX,
+ viewOffset: IDENTITY_TRANSFORM,
+ resolution: VALID_RESOLUTION,
+ }
+];
+
+const ALL_FEATURES = [
+ 'viewer',
+ 'local',
+ 'local-floor',
+ 'bounded-floor',
+ 'unbounded',
+ 'hit-test',
+ 'dom-overlay',
+ 'light-estimation',
+ 'anchors',
+ 'depth-sensing',
+ 'secondary-views',
+ 'camera-access',
+ 'layers'
+];
+
+const TRACKED_IMMERSIVE_DEVICE = {
+ supportsImmersive: true,
+ supportedModes: [ "inline", "immersive-vr"],
+ views: VALID_VIEWS,
+ secondaryViews: VALID_SECONDARY_VIEWS,
+ viewerOrigin: IDENTITY_TRANSFORM,
+ supportedFeatures: ALL_FEATURES,
+ environmentBlendMode: "opaque",
+ interactionMode: "world-space"
+};
+
+const IMMERSIVE_AR_DEVICE = {
+ supportsImmersive: true,
+ supportedModes: [ "inline", "immersive-ar"],
+ views: VALID_VIEWS,
+ viewerOrigin: IDENTITY_TRANSFORM,
+ supportedFeatures: ALL_FEATURES,
+ environmentBlendMode: "additive",
+ interactionMode: "screen-space"
+};
+
+const VALID_NON_IMMERSIVE_DEVICE = {
+ supportsImmersive: false,
+ supportedModes: ["inline"],
+ views: NON_IMMERSIVE_VIEWS,
+ viewerOrigin: IDENTITY_TRANSFORM,
+ supportedFeatures: ALL_FEATURES,
+ environmentBlendMode: "opaque",
+ interactionMode: "screen-space"
+};
+
+const VALID_CONTROLLER = {
+ handedness: "none",
+ targetRayMode: "tracked-pointer",
+ pointerOrigin: VALID_POINTER_TRANSFORM,
+ profiles: []
+};
+
+const RIGHT_CONTROLLER = {
+ handedness: "right",
+ targetRayMode: "tracked-pointer",
+ pointerOrigin: VALID_POINTER_TRANSFORM,
+ profiles: []
+};
+
+const SCREEN_CONTROLLER = {
+ handedness: "none",
+ targetRayMode: "screen",
+ pointerOrigin: VALID_POINTER_TRANSFORM,
+ profiles: []
+};
+
+// From: https://immersive-web.github.io/webxr/#default-features
+const DEFAULT_FEATURES = {
+ "inline": ["viewer"],
+ "immersive-vr": ["viewer", "local"],
+ "immersive-ar": ["viewer", "local"],
+};
diff --git a/testing/web-platform/tests/webxr/resources/webxr_test_constants_fake_depth.js b/testing/web-platform/tests/webxr/resources/webxr_test_constants_fake_depth.js
new file mode 100644
index 0000000000..36890d398d
--- /dev/null
+++ b/testing/web-platform/tests/webxr/resources/webxr_test_constants_fake_depth.js
@@ -0,0 +1,78 @@
+'use strict';
+
+// This file introduces constants used to mock depth data for depth sensing API.
+
+const convertDepthBufferToArrayBuffer = function (data, desiredFormat) {
+ if(desiredFormat == "luminance-alpha") {
+ const result = new ArrayBuffer(data.length * 2); // each entry has 2 bytes
+ const view = new Uint16Array(result);
+
+ for(let i = 0; i < data.length; ++i) {
+ view[i] = data[i];
+ }
+
+ return new Uint8Array(result);
+ } else if(desiredFormat == "float32") {
+ const result = new ArrayBuffer(data.length * 4); // each entry has 4 bytes
+ const view = new Float32Array(result);
+
+ for(let i = 0; i < data.length; ++i) {
+ view[i] = data[i];
+ }
+
+ return new Uint8Array(result);
+ } else {
+ throw new Error("Unrecognized data format!");
+ }
+}
+
+// Let's assume that the depth values are in cm, Xcm = x * 1/100m
+const RAW_VALUE_TO_METERS = 1/100;
+
+const createDepthSensingData = function() {
+ const depthSensingBufferHeight = 5;
+ const depthSensingBufferWidth = 7;
+ const depthSensingBuffer = [
+ 1, 1, 1, 1, 1, 1, 1, // first row
+ 1, 2, 3, 4, 5, 6, 7,
+ 1, 4, 9, 16, 25, 36, 49,
+ 1, 8, 27, 64, 125, 216, 343,
+ 1, 16, 81, 256, 625, 1296, 2401,
+ ]; // depthSensingBuffer value at column c, row r is Math.pow(c+1, r).
+
+ // Let's assume that the origin of the depth buffer is in the bottom right
+ // corner, with X's growing to the left and Y's growing upwards.
+ // This corresponds to the origin at 2401 in the above matrix, with X axis
+ // growing from 2401 towards 1296, and Y axis growing from 2401 towards 343.
+ // This corresponds to a rotation around Z axis by 180 degrees, with origin at [1,1].
+ const depthSensingBufferFromViewerTransform = {
+ position: [1, 1, 0],
+ orientation: [0, 0, 1, 0],
+ };
+
+ return {
+ depthData: convertDepthBufferToArrayBuffer(depthSensingBuffer, "luminance-alpha"),
+ width: depthSensingBufferWidth,
+ height: depthSensingBufferHeight,
+ normDepthBufferFromNormView: depthSensingBufferFromViewerTransform,
+ rawValueToMeters: RAW_VALUE_TO_METERS,
+ };
+};
+
+const DEPTH_SENSING_DATA = createDepthSensingData();
+
+// Returns expected depth value at |column|, |row| coordinates, expressed
+// in depth buffer's coordinate system.
+const getExpectedValueAt = function(column, row) {
+ return Math.pow(column+1, row) * RAW_VALUE_TO_METERS;
+};
+
+const VALID_DEPTH_CONFIG_CPU_USAGE = {
+ usagePreference: ['cpu-optimized'],
+ dataFormatPreference: ['luminance-alpha', 'float32'],
+};
+
+const VALID_DEPTH_CONFIG_GPU_USAGE = {
+ usagePreference: ['gpu-optimized'],
+ dataFormatPreference: ['luminance-alpha', 'float32'],
+};
diff --git a/testing/web-platform/tests/webxr/resources/webxr_test_constants_fake_world.js b/testing/web-platform/tests/webxr/resources/webxr_test_constants_fake_world.js
new file mode 100644
index 0000000000..7e428e2155
--- /dev/null
+++ b/testing/web-platform/tests/webxr/resources/webxr_test_constants_fake_world.js
@@ -0,0 +1,79 @@
+'use strict';
+
+// This file introduces constants used to mock fake world for the purposes of hit test.
+
+// Generates FakeXRWorldInit dictionary with given dimensions.
+// The generated fake world will have floor and front wall treated as planes,
+// side walls treated as meshes, and ceiling treated as points.
+// width - X axis, in meters
+// height - Y axis, in meters
+// length - Z axis, in meters
+function createFakeWorld(
+ width, height, length,
+ front_wall_and_floor_type = "plane",
+ side_walls_type = "mesh",
+ ceiling_type = "point") {
+ // Vertices:
+ const BOTTOM_LEFT_FRONT = { x: -width/2, y: 0, z: -length/2, w: 1};
+ const BOTTOM_RIGHT_FRONT = { x: width/2, y: 0, z: -length/2, w: 1};
+
+ const TOP_LEFT_FRONT = { x: -width/2, y: height, z: -length/2, w: 1};
+ const TOP_RIGHT_FRONT = { x: width/2, y: height, z: -length/2, w: 1};
+
+ const BOTTOM_LEFT_BACK = { x: -width/2, y: 0, z: length/2, w: 1};
+ const BOTTOM_RIGHT_BACK = { x: width/2, y: 0, z: length/2, w: 1};
+
+ const TOP_LEFT_BACK = { x: -width/2, y: height, z: length/2, w: 1};
+ const TOP_RIGHT_BACK = { x: width/2, y: height, z: length/2, w: 1};
+
+ // Faces:
+ const FRONT_WALL_AND_FLOOR_FACES = [
+ // Front wall:
+ { vertices: [BOTTOM_LEFT_FRONT, BOTTOM_RIGHT_FRONT, TOP_RIGHT_FRONT] },
+ { vertices: [BOTTOM_LEFT_FRONT, TOP_RIGHT_FRONT, TOP_LEFT_FRONT] },
+ // Floor:
+ { vertices: [BOTTOM_LEFT_FRONT, BOTTOM_RIGHT_FRONT, BOTTOM_RIGHT_BACK] },
+ { vertices: [BOTTOM_LEFT_FRONT, BOTTOM_LEFT_BACK, BOTTOM_RIGHT_BACK] },
+ ];
+
+ const CEILING_FACES = [
+ // Ceiling:
+ { vertices: [TOP_LEFT_FRONT, TOP_RIGHT_FRONT, TOP_RIGHT_BACK] },
+ { vertices: [TOP_LEFT_FRONT, TOP_LEFT_BACK, TOP_RIGHT_BACK] },
+ ];
+
+ const SIDE_WALLS_FACES = [
+ // Left:
+ { vertices: [BOTTOM_LEFT_FRONT, TOP_LEFT_FRONT, TOP_LEFT_BACK] },
+ { vertices: [BOTTOM_LEFT_FRONT, BOTTOM_LEFT_BACK, TOP_LEFT_BACK] },
+ // Right:
+ { vertices: [BOTTOM_RIGHT_FRONT, TOP_RIGHT_FRONT, TOP_RIGHT_BACK] },
+ { vertices: [BOTTOM_RIGHT_FRONT, BOTTOM_RIGHT_BACK, TOP_RIGHT_BACK] },
+ ];
+
+ // Regions:
+ const FRONT_WALL_AND_FLOOR_REGION = {
+ type: front_wall_and_floor_type,
+ faces: FRONT_WALL_AND_FLOOR_FACES,
+ };
+
+ const SIDE_WALLS_REGION = {
+ type: side_walls_type,
+ faces: SIDE_WALLS_FACES,
+ };
+
+ const CEILING_REGION = {
+ type: ceiling_type,
+ faces: CEILING_FACES,
+ };
+
+ return {
+ hitTestRegions : [
+ FRONT_WALL_AND_FLOOR_REGION,
+ SIDE_WALLS_REGION,
+ CEILING_REGION,
+ ]
+ };
+}
+
+const VALID_FAKE_WORLD = createFakeWorld(5.0, 2.0, 5.0);
diff --git a/testing/web-platform/tests/webxr/resources/webxr_util.js b/testing/web-platform/tests/webxr/resources/webxr_util.js
new file mode 100644
index 0000000000..625f76450e
--- /dev/null
+++ b/testing/web-platform/tests/webxr/resources/webxr_util.js
@@ -0,0 +1,241 @@
+'use strict';
+
+// These tests rely on the User Agent providing an implementation of the
+// WebXR Testing API (https://github.com/immersive-web/webxr-test-api).
+//
+// In Chromium-based browsers, this implementation is provided by a JavaScript
+// shim in order to reduce the amount of test-only code shipped to users. To
+// enable these tests the browser must be run with these options:
+//
+// --enable-blink-features=MojoJS,MojoJSTest
+
+// Debugging message helper, by default does nothing. Implementations can
+// override this.
+var xr_debug = function(name, msg) {};
+
+function xr_promise_test(name, func, properties, glContextType, glContextProperties) {
+ promise_test(async (t) => {
+ if (glContextType === 'webgl2') {
+ // Fast fail on platforms not supporting WebGL2.
+ assert_implements('WebGL2RenderingContext' in window, 'webgl2 not supported.');
+ }
+ // Perform any required test setup:
+ xr_debug(name, 'setup');
+
+ assert_implements(navigator.xr, 'missing navigator.xr - ensure test is run in a secure context.');
+
+ // Only set up once.
+ if (!navigator.xr.test) {
+ // Load test-only API helpers.
+ const script = document.createElement('script');
+ script.src = '/resources/test-only-api.js';
+ script.async = false;
+ const p = new Promise((resolve, reject) => {
+ script.onload = () => { resolve(); };
+ script.onerror = e => { reject(e); };
+ })
+ document.head.appendChild(script);
+ await p;
+
+ if (isChromiumBased) {
+ // Chrome setup
+ await loadChromiumResources();
+ } else if (isWebKitBased) {
+ // WebKit setup
+ await setupWebKitWebXRTestAPI();
+ }
+ }
+
+ // Either the test api needs to be polyfilled and it's not set up above, or
+ // something happened to one of the known polyfills and it failed to be
+ // setup properly. Either way, the fact that xr_promise_test is being used
+ // means that the tests expect navigator.xr.test to be set. By rejecting now
+ // we can hopefully provide a clearer indication of what went wrong.
+ assert_implements(navigator.xr.test, 'missing navigator.xr.test, even after attempted load');
+
+ let gl = null;
+ let canvas = null;
+ if (glContextType) {
+ canvas = document.createElement('canvas');
+ document.body.appendChild(canvas);
+ gl = canvas.getContext(glContextType, glContextProperties);
+ }
+
+ // Ensure that any devices are disconnected when done. 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.
+ t.add_cleanup(async () => {
+ // Ensure system state is cleaned up.
+ xr_debug(name, 'cleanup');
+ await navigator.xr.test.disconnectAllDevices();
+ });
+
+ xr_debug(name, 'main');
+ return func(t, gl);
+ }, name, properties);
+}
+
+// A utility function for waiting one animation frame before running the callback
+//
+// This is only needed after calling FakeXRDevice methods outside of an animation frame
+//
+// This is so that we can paper over the potential race allowed by the "next animation frame"
+// concept https://immersive-web.github.io/webxr-test-api/#xrsession-next-animation-frame
+function requestSkipAnimationFrame(session, callback) {
+ session.requestAnimationFrame(() => {
+ session.requestAnimationFrame(callback);
+ });
+}
+
+// A test function which runs through the common steps of requesting a session.
+// Calls the passed in test function with the session, the controller for the
+// device, and the test object.
+function xr_session_promise_test(
+ name, func, fakeDeviceInit, sessionMode, sessionInit, properties,
+ glcontextPropertiesParam, gllayerPropertiesParam) {
+ const glcontextProperties = (glcontextPropertiesParam) ? glcontextPropertiesParam : {};
+ const gllayerProperties = (gllayerPropertiesParam) ? gllayerPropertiesParam : {};
+
+ 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((session) => {
+ xr_debug(name, 'session start');
+ testSession = session;
+ session.mode = sessionMode;
+ session.sessionInit = sessionInit;
+ let glLayer = new XRWebGLLayer(session, sessionObjects.gl, gllayerProperties);
+ glLayer.context = sessionObjects.gl;
+ // Session must have a baseLayer or frame requests
+ // will be ignored.
+ session.updateRenderState({
+ baseLayer: glLayer
+ });
+ sessionObjects.glLayer = glLayer;
+ 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});
+}
+
+
+// This function wraps the provided function in a
+// simulateUserActivation() call, and resolves the promise with the
+// result of func(), or an error if one is thrown
+function promise_simulate_user_activation(func) {
+ return new Promise((resolve, reject) => {
+ navigator.xr.test.simulateUserActivation(() => {
+ try { let a = func(); resolve(a); } catch(e) { reject(e); }
+ });
+ });
+}
+
+// This functions calls a callback with each API object as specified
+// by https://immersive-web.github.io/webxr/spec/latest/, allowing
+// checks to be made on all ojects.
+// Arguements:
+// callback: A callback function with two arguements, the first
+// being the API object, the second being the name of
+// that API object.
+function forEachWebxrObject(callback) {
+ callback(window.navigator.xr, 'navigator.xr');
+ callback(window.XRSession, 'XRSession');
+ callback(window.XRSessionCreationOptions, 'XRSessionCreationOptions');
+ callback(window.XRFrameRequestCallback, 'XRFrameRequestCallback');
+ callback(window.XRPresentationContext, 'XRPresentationContext');
+ callback(window.XRFrame, 'XRFrame');
+ callback(window.XRLayer, 'XRLayer');
+ callback(window.XRView, 'XRView');
+ callback(window.XRViewport, 'XRViewport');
+ callback(window.XRViewerPose, 'XRViewerPose');
+ callback(window.XRWebGLLayer, 'XRWebGLLayer');
+ callback(window.XRWebGLLayerInit, 'XRWebGLLayerInit');
+ callback(window.XRCoordinateSystem, 'XRCoordinateSystem');
+ callback(window.XRFrameOfReference, 'XRFrameOfReference');
+ callback(window.XRStageBounds, 'XRStageBounds');
+ callback(window.XRSessionEvent, 'XRSessionEvent');
+ callback(window.XRCoordinateSystemEvent, 'XRCoordinateSystemEvent');
+}
+
+// Code for loading test API in Chromium.
+async function loadChromiumResources() {
+ await loadScript('/resources/chromium/webxr-test-math-helper.js');
+ await import('/resources/chromium/webxr-test.js');
+ await loadScript('/resources/testdriver.js');
+ await loadScript('/resources/testdriver-vendor.js');
+
+ // This infrastructure is also used by Chromium-specific internal tests that
+ // may need additional resources (e.g. internal API extensions), this allows
+ // those tests to rely on this infrastructure while ensuring that no tests
+ // make it into public WPTs that rely on APIs outside of the webxr test API.
+ if (typeof(additionalChromiumResources) !== 'undefined') {
+ for (const path of additionalChromiumResources) {
+ await loadScript(path);
+ }
+ }
+
+ xr_debug = navigator.xr.test.Debug;
+}
+
+function setupWebKitWebXRTestAPI() {
+ // WebKit setup. The internals object is used by the WebKit test runner
+ // to provide JS access to internal APIs. In this case it's used to
+ // ensure that XRTest is only exposed to wpt tests.
+ navigator.xr.test = internals.xrTest;
+ return Promise.resolve();
+}