summaryrefslogtreecommitdiffstats
path: root/test/wpt/tests/resources/chromium
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-21 20:56:19 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-21 20:56:19 +0000
commit0b6210cd37b68b94252cb798598b12974a20e1c1 (patch)
treee371686554a877842d95aa94f100bee552ff2a8e /test/wpt/tests/resources/chromium
parentInitial commit. (diff)
downloadnode-undici-0b6210cd37b68b94252cb798598b12974a20e1c1.tar.xz
node-undici-0b6210cd37b68b94252cb798598b12974a20e1c1.zip
Adding upstream version 5.28.2+dfsg1+~cs23.11.12.3.upstream/5.28.2+dfsg1+_cs23.11.12.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'test/wpt/tests/resources/chromium')
-rw-r--r--test/wpt/tests/resources/chromium/README.md7
-rw-r--r--test/wpt/tests/resources/chromium/contacts_manager_mock.js90
-rw-r--r--test/wpt/tests/resources/chromium/content-index-helpers.js9
-rw-r--r--test/wpt/tests/resources/chromium/enable-hyperlink-auditing.js2
-rw-r--r--test/wpt/tests/resources/chromium/fake-hid.js297
-rw-r--r--test/wpt/tests/resources/chromium/fake-serial.js443
-rw-r--r--test/wpt/tests/resources/chromium/generic_sensor_mocks.js519
-rw-r--r--test/wpt/tests/resources/chromium/generic_sensor_mocks.js.headers1
-rw-r--r--test/wpt/tests/resources/chromium/mock-barcodedetection.js136
-rw-r--r--test/wpt/tests/resources/chromium/mock-barcodedetection.js.headers1
-rw-r--r--test/wpt/tests/resources/chromium/mock-battery-monitor.headers1
-rw-r--r--test/wpt/tests/resources/chromium/mock-battery-monitor.js61
-rw-r--r--test/wpt/tests/resources/chromium/mock-direct-sockets.js94
-rw-r--r--test/wpt/tests/resources/chromium/mock-facedetection.js130
-rw-r--r--test/wpt/tests/resources/chromium/mock-facedetection.js.headers1
-rw-r--r--test/wpt/tests/resources/chromium/mock-idle-detection.js80
-rw-r--r--test/wpt/tests/resources/chromium/mock-imagecapture.js309
-rw-r--r--test/wpt/tests/resources/chromium/mock-managed-config.js91
-rw-r--r--test/wpt/tests/resources/chromium/mock-pressure-service.js134
-rw-r--r--test/wpt/tests/resources/chromium/mock-pressure-service.js.headers1
-rw-r--r--test/wpt/tests/resources/chromium/mock-subapps.js89
-rw-r--r--test/wpt/tests/resources/chromium/mock-textdetection.js92
-rw-r--r--test/wpt/tests/resources/chromium/mock-textdetection.js.headers1
-rw-r--r--test/wpt/tests/resources/chromium/nfc-mock.js437
-rw-r--r--test/wpt/tests/resources/chromium/web-bluetooth-test.js629
-rw-r--r--test/wpt/tests/resources/chromium/web-bluetooth-test.js.headers1
-rw-r--r--test/wpt/tests/resources/chromium/webusb-child-test.js47
-rw-r--r--test/wpt/tests/resources/chromium/webusb-child-test.js.headers1
-rw-r--r--test/wpt/tests/resources/chromium/webusb-test.js583
-rw-r--r--test/wpt/tests/resources/chromium/webusb-test.js.headers1
-rw-r--r--test/wpt/tests/resources/chromium/webxr-test-math-helper.js298
-rw-r--r--test/wpt/tests/resources/chromium/webxr-test-math-helper.js.headers1
-rw-r--r--test/wpt/tests/resources/chromium/webxr-test.js2125
-rw-r--r--test/wpt/tests/resources/chromium/webxr-test.js.headers1
34 files changed, 6713 insertions, 0 deletions
diff --git a/test/wpt/tests/resources/chromium/README.md b/test/wpt/tests/resources/chromium/README.md
new file mode 100644
index 0000000..be090b3
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/README.md
@@ -0,0 +1,7 @@
+This directory contains Chromium-specific test resources, including mocks for
+test-only APIs implemented with
+[MojoJS](https://chromium.googlesource.com/chromium/src/+/main/mojo/public/js/README.md).
+
+Please do **not** copy `*.mojom.m.js` into this directory. Follow this doc if you
+want to add new MojoJS-backed mocks:
+https://chromium.googlesource.com/chromium/src/+/main/docs/testing/web_platform_tests.md#mojojs
diff --git a/test/wpt/tests/resources/chromium/contacts_manager_mock.js b/test/wpt/tests/resources/chromium/contacts_manager_mock.js
new file mode 100644
index 0000000..0496852
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/contacts_manager_mock.js
@@ -0,0 +1,90 @@
+// Copyright 2018 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {ContactsManager, ContactsManagerReceiver} from '/gen/third_party/blink/public/mojom/contacts/contacts_manager.mojom.m.js';
+
+self.WebContactsTest = (() => {
+ class MockContacts {
+ constructor() {
+ this.receiver_ = new ContactsManagerReceiver(this);
+
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(ContactsManager.$interfaceName);
+ this.interceptor_.oninterfacerequest =
+ e => this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+
+ this.selectedContacts_ = [];
+ }
+
+ formatAddress_(address) {
+ // These are all required fields in the mojo definition.
+ return {
+ country: address.country || '',
+ addressLine: address.addressLine || [],
+ region: address.region || '',
+ city: address.city || '',
+ dependentLocality: address.dependentLocality || '',
+ postalCode: address.postCode || '',
+ sortingCode: address.sortingCode || '',
+ organization: address.organization || '',
+ recipient: address.recipient || '',
+ phone: address.phone || '',
+ };
+ }
+
+ async select(multiple, includeNames, includeEmails, includeTel, includeAddresses, includeIcons) {
+ if (this.selectedContacts_ === null)
+ return {contacts: null};
+
+ const contactInfos = await Promise.all(this.selectedContacts_.map(async contact => {
+ const contactInfo = {};
+ if (includeNames)
+ contactInfo.name = contact.name || [];
+ if (includeEmails)
+ contactInfo.email = contact.email || [];
+ if (includeTel)
+ contactInfo.tel = contact.tel || [];
+ if (includeAddresses) {
+ contactInfo.address = (contact.address || []).map(address => this.formatAddress_(address));
+ }
+ if (includeIcons) {
+ contactInfo.icon = await Promise.all(
+ (contact.icon || []).map(async blob => ({
+ mimeType: blob.type,
+ data: (await blob.text()).split('').map(s => s.charCodeAt(0)),
+ })));
+ }
+ return contactInfo;
+ }));
+
+ if (!contactInfos.length) return {contacts: []};
+ if (!multiple) return {contacts: [contactInfos[0]]};
+ return {contacts: contactInfos};
+ }
+
+ setSelectedContacts(contacts) {
+ this.selectedContacts_ = contacts;
+ }
+
+ reset() {
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+ }
+ }
+
+ const mockContacts = new MockContacts();
+
+ class ContactsTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ setSelectedContacts(contacts) {
+ mockContacts.setSelectedContacts(contacts);
+ }
+ }
+
+ return ContactsTestChromium;
+})();
diff --git a/test/wpt/tests/resources/chromium/content-index-helpers.js b/test/wpt/tests/resources/chromium/content-index-helpers.js
new file mode 100644
index 0000000..936fe84
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/content-index-helpers.js
@@ -0,0 +1,9 @@
+import {ContentIndexService} from '/gen/third_party/blink/public/mojom/content_index/content_index.mojom.m.js';
+
+// Returns a promise if the chromium based browser fetches icons for
+// content-index.
+export async function fetchesIcons() {
+ const remote = ContentIndexService.getRemote();
+ const {iconSizes} = await remote.getIconSizes();
+ return iconSizes.length > 0;
+};
diff --git a/test/wpt/tests/resources/chromium/enable-hyperlink-auditing.js b/test/wpt/tests/resources/chromium/enable-hyperlink-auditing.js
new file mode 100644
index 0000000..263f651
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/enable-hyperlink-auditing.js
@@ -0,0 +1,2 @@
+if (window.testRunner)
+ testRunner.overridePreference("WebKitHyperlinkAuditingEnabled", 1);
diff --git a/test/wpt/tests/resources/chromium/fake-hid.js b/test/wpt/tests/resources/chromium/fake-hid.js
new file mode 100644
index 0000000..70a0149
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/fake-hid.js
@@ -0,0 +1,297 @@
+import {HidConnectionReceiver, HidDeviceInfo} from '/gen/services/device/public/mojom/hid.mojom.m.js';
+import {HidService, HidServiceReceiver} from '/gen/third_party/blink/public/mojom/hid/hid.mojom.m.js';
+
+// Fake implementation of device.mojom.HidConnection. HidConnection represents
+// an open connection to a HID device and can be used to send and receive
+// reports.
+class FakeHidConnection {
+ constructor(client) {
+ this.client_ = client;
+ this.receiver_ = new HidConnectionReceiver(this);
+ this.expectedWrites_ = [];
+ this.expectedGetFeatureReports_ = [];
+ this.expectedSendFeatureReports_ = [];
+ }
+
+ bindNewPipeAndPassRemote() {
+ return this.receiver_.$.bindNewPipeAndPassRemote();
+ }
+
+ // Simulate an input report sent from the device to the host. The connection
+ // client's onInputReport method will be called with the provided |reportId|
+ // and |buffer|.
+ simulateInputReport(reportId, reportData) {
+ if (this.client_) {
+ this.client_.onInputReport(reportId, reportData);
+ }
+ }
+
+ // Specify the result for an expected call to write. If |success| is true the
+ // write will be successful, otherwise it will simulate a failure. The
+ // parameters of the next write call must match |reportId| and |buffer|.
+ queueExpectedWrite(success, reportId, reportData) {
+ this.expectedWrites_.push({
+ params: {reportId, data: reportData},
+ result: {success},
+ });
+ }
+
+ // Specify the result for an expected call to getFeatureReport. If |success|
+ // is true the operation is successful, otherwise it will simulate a failure.
+ // The parameter of the next getFeatureReport call must match |reportId|.
+ queueExpectedGetFeatureReport(success, reportId, reportData) {
+ this.expectedGetFeatureReports_.push({
+ params: {reportId},
+ result: {success, buffer: reportData},
+ });
+ }
+
+ // Specify the result for an expected call to sendFeatureReport. If |success|
+ // is true the operation is successful, otherwise it will simulate a failure.
+ // The parameters of the next sendFeatureReport call must match |reportId| and
+ // |buffer|.
+ queueExpectedSendFeatureReport(success, reportId, reportData) {
+ this.expectedSendFeatureReports_.push({
+ params: {reportId, data: reportData},
+ result: {success},
+ });
+ }
+
+ // Asserts that there are no more expected operations.
+ assertExpectationsMet() {
+ assert_equals(this.expectedWrites_.length, 0);
+ assert_equals(this.expectedGetFeatureReports_.length, 0);
+ assert_equals(this.expectedSendFeatureReports_.length, 0);
+ }
+
+ read() {}
+
+ // Implementation of HidConnection::Write. Causes an assertion failure if
+ // there are no expected write operations, or if the parameters do not match
+ // the expected call.
+ async write(reportId, buffer) {
+ let expectedWrite = this.expectedWrites_.shift();
+ assert_not_equals(expectedWrite, undefined);
+ assert_equals(reportId, expectedWrite.params.reportId);
+ let actual = new Uint8Array(buffer);
+ compareDataViews(
+ new DataView(actual.buffer, actual.byteOffset),
+ new DataView(
+ expectedWrite.params.data.buffer,
+ expectedWrite.params.data.byteOffset));
+ return expectedWrite.result;
+ }
+
+ // Implementation of HidConnection::GetFeatureReport. Causes an assertion
+ // failure if there are no expected write operations, or if the parameters do
+ // not match the expected call.
+ async getFeatureReport(reportId) {
+ let expectedGetFeatureReport = this.expectedGetFeatureReports_.shift();
+ assert_not_equals(expectedGetFeatureReport, undefined);
+ assert_equals(reportId, expectedGetFeatureReport.params.reportId);
+ return expectedGetFeatureReport.result;
+ }
+
+ // Implementation of HidConnection::SendFeatureReport. Causes an assertion
+ // failure if there are no expected write operations, or if the parameters do
+ // not match the expected call.
+ async sendFeatureReport(reportId, buffer) {
+ let expectedSendFeatureReport = this.expectedSendFeatureReports_.shift();
+ assert_not_equals(expectedSendFeatureReport, undefined);
+ assert_equals(reportId, expectedSendFeatureReport.params.reportId);
+ let actual = new Uint8Array(buffer);
+ compareDataViews(
+ new DataView(actual.buffer, actual.byteOffset),
+ new DataView(
+ expectedSendFeatureReport.params.data.buffer,
+ expectedSendFeatureReport.params.data.byteOffset));
+ return expectedSendFeatureReport.result;
+ }
+}
+
+
+// A fake implementation of the HidService mojo interface. HidService manages
+// HID device access for clients in the render process. Typically, when a client
+// requests access to a HID device a chooser dialog is shown with a list of
+// available HID devices. Selecting a device from the chooser also grants
+// permission for the client to access that device.
+//
+// The fake implementation allows tests to simulate connected devices. It also
+// skips the chooser dialog and instead allows tests to specify which device
+// should be selected. All devices are treated as if the user had already
+// granted permission. It is possible to revoke permission with forget() later.
+class FakeHidService {
+ constructor() {
+ this.interceptor_ = new MojoInterfaceInterceptor(HidService.$interfaceName);
+ this.interceptor_.oninterfacerequest = e => this.bind(e.handle);
+ this.receiver_ = new HidServiceReceiver(this);
+ this.nextGuidValue_ = 0;
+ this.simulateConnectFailure_ = false;
+ this.reset();
+ }
+
+ start() {
+ this.interceptor_.start();
+ }
+
+ stop() {
+ this.interceptor_.stop();
+ }
+
+ reset() {
+ this.devices_ = new Map();
+ this.allowedDevices_ = new Map();
+ this.fakeConnections_ = new Map();
+ this.selectedDevices_ = [];
+ }
+
+ // Creates and returns a HidDeviceInfo with the specified device IDs.
+ makeDevice(vendorId, productId) {
+ let guidValue = ++this.nextGuidValue_;
+ let info = new HidDeviceInfo();
+ info.guid = 'guid-' + guidValue.toString();
+ info.physicalDeviceId = 'physical-device-id-' + guidValue.toString();
+ info.vendorId = vendorId;
+ info.productId = productId;
+ info.productName = 'product name';
+ info.serialNumber = '0';
+ info.reportDescriptor = new Uint8Array();
+ info.collections = [];
+ info.deviceNode = 'device node';
+ return info;
+ }
+
+ // Simulates a connected device the client has already been granted permission
+ // to. Returns the key used to store the device in the map. The key is either
+ // the physical device ID, or the device GUID if it has no physical device ID.
+ addDevice(deviceInfo, grantPermission = true) {
+ let key = deviceInfo.physicalDeviceId;
+ if (key.length === 0)
+ key = deviceInfo.guid;
+
+ let devices = this.devices_.get(key) || [];
+ devices.push(deviceInfo);
+ this.devices_.set(key, devices);
+
+ if (grantPermission) {
+ let allowedDevices = this.allowedDevices_.get(key) || [];
+ allowedDevices.push(deviceInfo);
+ this.allowedDevices_.set(key, allowedDevices);
+ }
+
+ if (this.client_)
+ this.client_.deviceAdded(deviceInfo);
+ return key;
+ }
+
+ // Simulates disconnecting a connected device.
+ removeDevice(key) {
+ let devices = this.devices_.get(key);
+ this.devices_.delete(key);
+ if (this.client_ && devices) {
+ devices.forEach(deviceInfo => {
+ this.client_.deviceRemoved(deviceInfo);
+ });
+ }
+ }
+
+ // Simulates updating the device information for a connected device.
+ changeDevice(deviceInfo) {
+ let key = deviceInfo.physicalDeviceId;
+ if (key.length === 0)
+ key = deviceInfo.guid;
+
+ let devices = this.devices_.get(key) || [];
+ let i = devices.length;
+ while (i--) {
+ if (devices[i].guid == deviceInfo.guid)
+ devices.splice(i, 1);
+ }
+ devices.push(deviceInfo);
+ this.devices_.set(key, devices);
+
+ let allowedDevices = this.allowedDevices_.get(key) || [];
+ let j = allowedDevices.length;
+ while (j--) {
+ if (allowedDevices[j].guid == deviceInfo.guid)
+ allowedDevices.splice(j, 1);
+ }
+ allowedDevices.push(deviceInfo);
+ this.allowedDevices_.set(key, allowedDevices);
+
+ if (this.client_)
+ this.client_.deviceChanged(deviceInfo);
+ return key;
+ }
+
+ // Sets a flag that causes the next call to connect() to fail.
+ simulateConnectFailure() {
+ this.simulateConnectFailure_ = true;
+ }
+
+ // Sets the key of the device that will be returned as the selected item the
+ // next time requestDevice is called. The device with this key must have been
+ // previously added with addDevice.
+ setSelectedDevice(key) {
+ this.selectedDevices_ = this.devices_.get(key);
+ }
+
+ // Returns the fake HidConnection object for this device, if there is one. A
+ // connection is created once the device is opened.
+ getFakeConnection(guid) {
+ return this.fakeConnections_.get(guid);
+ }
+
+ bind(handle) {
+ this.receiver_.$.bindHandle(handle);
+ }
+
+ registerClient(client) {
+ this.client_ = client;
+ }
+
+ // Returns an array of connected devices the client has already been granted
+ // permission to access.
+ async getDevices() {
+ let devices = [];
+ this.allowedDevices_.forEach((value) => {
+ devices = devices.concat(value);
+ });
+ return {devices};
+ }
+
+ // Simulates a device chooser prompt, returning |selectedDevices_| as the
+ // simulated selection. |options| is ignored.
+ async requestDevice(options) {
+ return {devices: this.selectedDevices_};
+ }
+
+ // Returns a fake connection to the device with the specified GUID. If
+ // |connectionClient| is not null, its onInputReport method will be called
+ // when input reports are received. If simulateConnectFailure() was called
+ // then a null connection is returned instead, indicating failure.
+ async connect(guid, connectionClient) {
+ if (this.simulateConnectFailure_) {
+ this.simulateConnectFailure_ = false;
+ return {connection: null};
+ }
+ const fakeConnection = new FakeHidConnection(connectionClient);
+ this.fakeConnections_.set(guid, fakeConnection);
+ return {connection: fakeConnection.bindNewPipeAndPassRemote()};
+ }
+
+ // Removes the allowed device.
+ async forget(deviceInfo) {
+ for (const [key, value] of this.allowedDevices_) {
+ for (const device of value) {
+ if (device.guid == deviceInfo.guid) {
+ this.allowedDevices_.delete(key);
+ break;
+ }
+ }
+ }
+ return {success: true};
+ }
+}
+
+export const fakeHidService = new FakeHidService();
diff --git a/test/wpt/tests/resources/chromium/fake-serial.js b/test/wpt/tests/resources/chromium/fake-serial.js
new file mode 100644
index 0000000..e1e4d57
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/fake-serial.js
@@ -0,0 +1,443 @@
+import {SerialPortFlushMode, SerialPortRemote, SerialReceiveError, SerialPortReceiver, SerialSendError} from '/gen/services/device/public/mojom/serial.mojom.m.js';
+import {SerialService, SerialServiceReceiver} from '/gen/third_party/blink/public/mojom/serial/serial.mojom.m.js';
+
+// Implementation of an UnderlyingSource to create a ReadableStream from a Mojo
+// data pipe consumer handle.
+class DataPipeSource {
+ constructor(consumer) {
+ this.consumer_ = consumer;
+ }
+
+ async pull(controller) {
+ let chunk = new ArrayBuffer(64);
+ let {result, numBytes} = this.consumer_.readData(chunk);
+ if (result == Mojo.RESULT_OK) {
+ controller.enqueue(new Uint8Array(chunk, 0, numBytes));
+ return;
+ } else if (result == Mojo.RESULT_FAILED_PRECONDITION) {
+ controller.close();
+ return;
+ } else if (result == Mojo.RESULT_SHOULD_WAIT) {
+ await this.readable();
+ return this.pull(controller);
+ }
+ }
+
+ cancel() {
+ if (this.watcher_)
+ this.watcher_.cancel();
+ this.consumer_.close();
+ }
+
+ readable() {
+ return new Promise((resolve) => {
+ this.watcher_ =
+ this.consumer_.watch({ readable: true, peerClosed: true }, () => {
+ this.watcher_.cancel();
+ this.watcher_ = undefined;
+ resolve();
+ });
+ });
+ }
+}
+
+// Implementation of an UnderlyingSink to create a WritableStream from a Mojo
+// data pipe producer handle.
+class DataPipeSink {
+ constructor(producer) {
+ this._producer = producer;
+ }
+
+ async write(chunk, controller) {
+ while (true) {
+ let {result, numBytes} = this._producer.writeData(chunk);
+ if (result == Mojo.RESULT_OK) {
+ if (numBytes == chunk.byteLength) {
+ return;
+ }
+ chunk = chunk.slice(numBytes);
+ } else if (result == Mojo.RESULT_FAILED_PRECONDITION) {
+ throw new DOMException('The pipe is closed.', 'InvalidStateError');
+ } else if (result == Mojo.RESULT_SHOULD_WAIT) {
+ await this.writable();
+ }
+ }
+ }
+
+ close() {
+ assert_equals(undefined, this._watcher);
+ this._producer.close();
+ }
+
+ abort(reason) {
+ if (this._watcher)
+ this._watcher.cancel();
+ this._producer.close();
+ }
+
+ writable() {
+ return new Promise((resolve) => {
+ this._watcher =
+ this._producer.watch({ writable: true, peerClosed: true }, () => {
+ this._watcher.cancel();
+ this._watcher = undefined;
+ resolve();
+ });
+ });
+ }
+}
+
+// Implementation of device.mojom.SerialPort.
+class FakeSerialPort {
+ constructor() {
+ this.inputSignals_ = {
+ dataCarrierDetect: false,
+ clearToSend: false,
+ ringIndicator: false,
+ dataSetReady: false
+ };
+ this.inputSignalFailure_ = false;
+ this.outputSignals_ = {
+ dataTerminalReady: false,
+ requestToSend: false,
+ break: false
+ };
+ this.outputSignalFailure_ = false;
+ }
+
+ open(options, client) {
+ if (this.receiver_ !== undefined) {
+ // Port already open.
+ return null;
+ }
+
+ let port = new SerialPortRemote();
+ this.receiver_ = new SerialPortReceiver(this);
+ this.receiver_.$.bindHandle(port.$.bindNewPipeAndPassReceiver().handle);
+
+ this.options_ = options;
+ this.client_ = client;
+ // OS typically sets DTR on open.
+ this.outputSignals_.dataTerminalReady = true;
+
+ return port;
+ }
+
+ write(data) {
+ return this.writer_.write(data);
+ }
+
+ read() {
+ return this.reader_.read();
+ }
+
+ // Reads from the port until at least |targetLength| is read or the stream is
+ // closed. The data is returned as a combined Uint8Array.
+ readWithLength(targetLength) {
+ return readWithLength(this.reader_, targetLength);
+ }
+
+ simulateReadError(error) {
+ this.writer_.close();
+ this.writer_.releaseLock();
+ this.writer_ = undefined;
+ this.writable_ = undefined;
+ this.client_.onReadError(error);
+ }
+
+ simulateParityError() {
+ this.simulateReadError(SerialReceiveError.PARITY_ERROR);
+ }
+
+ simulateDisconnectOnRead() {
+ this.simulateReadError(SerialReceiveError.DISCONNECTED);
+ }
+
+ simulateWriteError(error) {
+ this.reader_.cancel();
+ this.reader_ = undefined;
+ this.readable_ = undefined;
+ this.client_.onSendError(error);
+ }
+
+ simulateSystemErrorOnWrite() {
+ this.simulateWriteError(SerialSendError.SYSTEM_ERROR);
+ }
+
+ simulateDisconnectOnWrite() {
+ this.simulateWriteError(SerialSendError.DISCONNECTED);
+ }
+
+ simulateInputSignals(signals) {
+ this.inputSignals_ = signals;
+ }
+
+ simulateInputSignalFailure(fail) {
+ this.inputSignalFailure_ = fail;
+ }
+
+ get outputSignals() {
+ return this.outputSignals_;
+ }
+
+ simulateOutputSignalFailure(fail) {
+ this.outputSignalFailure_ = fail;
+ }
+
+ writable() {
+ if (this.writable_)
+ return Promise.resolve();
+
+ if (!this.writablePromise_) {
+ this.writablePromise_ = new Promise((resolve) => {
+ this.writableResolver_ = resolve;
+ });
+ }
+
+ return this.writablePromise_;
+ }
+
+ readable() {
+ if (this.readable_)
+ return Promise.resolve();
+
+ if (!this.readablePromise_) {
+ this.readablePromise_ = new Promise((resolve) => {
+ this.readableResolver_ = resolve;
+ });
+ }
+
+ return this.readablePromise_;
+ }
+
+ async startWriting(in_stream) {
+ this.readable_ = new ReadableStream(new DataPipeSource(in_stream));
+ this.reader_ = this.readable_.getReader();
+ if (this.readableResolver_) {
+ this.readableResolver_();
+ this.readableResolver_ = undefined;
+ this.readablePromise_ = undefined;
+ }
+ }
+
+ async startReading(out_stream) {
+ this.writable_ = new WritableStream(new DataPipeSink(out_stream));
+ this.writer_ = this.writable_.getWriter();
+ if (this.writableResolver_) {
+ this.writableResolver_();
+ this.writableResolver_ = undefined;
+ this.writablePromise_ = undefined;
+ }
+ }
+
+ async flush(mode) {
+ switch (mode) {
+ case SerialPortFlushMode.kReceive:
+ this.writer_.abort();
+ this.writer_.releaseLock();
+ this.writer_ = undefined;
+ this.writable_ = undefined;
+ break;
+ case SerialPortFlushMode.kTransmit:
+ this.reader_.cancel();
+ this.reader_ = undefined;
+ this.readable_ = undefined;
+ break;
+ }
+ }
+
+ async drain() {
+ await this.reader_.closed;
+ }
+
+ async getControlSignals() {
+ if (this.inputSignalFailure_) {
+ return {signals: null};
+ }
+
+ const signals = {
+ dcd: this.inputSignals_.dataCarrierDetect,
+ cts: this.inputSignals_.clearToSend,
+ ri: this.inputSignals_.ringIndicator,
+ dsr: this.inputSignals_.dataSetReady
+ };
+ return {signals};
+ }
+
+ async setControlSignals(signals) {
+ if (this.outputSignalFailure_) {
+ return {success: false};
+ }
+
+ if (signals.hasDtr) {
+ this.outputSignals_.dataTerminalReady = signals.dtr;
+ }
+ if (signals.hasRts) {
+ this.outputSignals_.requestToSend = signals.rts;
+ }
+ if (signals.hasBrk) {
+ this.outputSignals_.break = signals.brk;
+ }
+ return { success: true };
+ }
+
+ async configurePort(options) {
+ this.options_ = options;
+ return { success: true };
+ }
+
+ async getPortInfo() {
+ return {
+ bitrate: this.options_.bitrate,
+ dataBits: this.options_.datBits,
+ parityBit: this.options_.parityBit,
+ stopBits: this.options_.stopBits,
+ ctsFlowControl:
+ this.options_.hasCtsFlowControl && this.options_.ctsFlowControl,
+ };
+ }
+
+ async close() {
+ // OS typically clears DTR on close.
+ this.outputSignals_.dataTerminalReady = false;
+ if (this.writer_) {
+ this.writer_.close();
+ this.writer_.releaseLock();
+ this.writer_ = undefined;
+ }
+ this.writable_ = undefined;
+
+ // Close the receiver asynchronously so the reply to this message can be
+ // sent first.
+ const receiver = this.receiver_;
+ this.receiver_ = undefined;
+ setTimeout(() => {
+ receiver.$.close();
+ }, 0);
+
+ return {};
+ }
+}
+
+// Implementation of blink.mojom.SerialService.
+class FakeSerialService {
+ constructor() {
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(SerialService.$interfaceName);
+ this.interceptor_.oninterfacerequest = e => this.bind(e.handle);
+ this.receiver_ = new SerialServiceReceiver(this);
+ this.clients_ = [];
+ this.nextToken_ = 0;
+ this.reset();
+ }
+
+ start() {
+ this.interceptor_.start();
+ }
+
+ stop() {
+ this.interceptor_.stop();
+ }
+
+ reset() {
+ this.ports_ = new Map();
+ this.selectedPort_ = null;
+ }
+
+ addPort(info) {
+ let portInfo = {};
+ if (info?.usbVendorId !== undefined) {
+ portInfo.hasUsbVendorId = true;
+ portInfo.usbVendorId = info.usbVendorId;
+ }
+ if (info?.usbProductId !== undefined) {
+ portInfo.hasUsbProductId = true;
+ portInfo.usbProductId = info.usbProductId;
+ }
+
+ let token = ++this.nextToken_;
+ portInfo.token = {high: 0n, low: BigInt(token)};
+
+ let record = {
+ portInfo: portInfo,
+ fakePort: new FakeSerialPort(),
+ };
+ this.ports_.set(token, record);
+
+ for (let client of this.clients_) {
+ client.onPortAdded(portInfo);
+ }
+
+ return token;
+ }
+
+ removePort(token) {
+ let record = this.ports_.get(token);
+ if (record === undefined) {
+ return;
+ }
+
+ this.ports_.delete(token);
+
+ for (let client of this.clients_) {
+ client.onPortRemoved(record.portInfo);
+ }
+ }
+
+ setSelectedPort(token) {
+ this.selectedPort_ = this.ports_.get(token);
+ }
+
+ getFakePort(token) {
+ let record = this.ports_.get(token);
+ if (record === undefined)
+ return undefined;
+ return record.fakePort;
+ }
+
+ bind(handle) {
+ this.receiver_.$.bindHandle(handle);
+ }
+
+ async setClient(client_remote) {
+ this.clients_.push(client_remote);
+ }
+
+ async getPorts() {
+ return {
+ ports: Array.from(this.ports_, ([token, record]) => record.portInfo)
+ };
+ }
+
+ async requestPort(filters) {
+ if (this.selectedPort_)
+ return { port: this.selectedPort_.portInfo };
+ else
+ return { port: null };
+ }
+
+ async openPort(token, options, client) {
+ let record = this.ports_.get(Number(token.low));
+ if (record !== undefined) {
+ return {port: record.fakePort.open(options, client)};
+ } else {
+ return {port: null};
+ }
+ }
+
+ async forgetPort(token) {
+ let record = this.ports_.get(Number(token.low));
+ if (record === undefined) {
+ return {success: false};
+ }
+
+ this.ports_.delete(Number(token.low));
+ if (record.fakePort.receiver_) {
+ record.fakePort.receiver_.$.close();
+ record.fakePort.receiver_ = undefined;
+ }
+ return {success: true};
+ }
+}
+
+export const fakeSerialService = new FakeSerialService();
diff --git a/test/wpt/tests/resources/chromium/generic_sensor_mocks.js b/test/wpt/tests/resources/chromium/generic_sensor_mocks.js
new file mode 100644
index 0000000..98a29c2
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/generic_sensor_mocks.js
@@ -0,0 +1,519 @@
+import {ReportingMode, Sensor, SensorClientRemote, SensorReceiver, SensorRemote, SensorType} from '/gen/services/device/public/mojom/sensor.mojom.m.js';
+import {SensorCreationResult, SensorInitParams_READ_BUFFER_SIZE_FOR_TESTS, SensorProvider, SensorProviderReceiver} from '/gen/services/device/public/mojom/sensor_provider.mojom.m.js';
+
+// A "sliding window" that iterates over |data| and returns one item at a
+// time, advancing and wrapping around as needed. |data| must be an array of
+// arrays.
+self.RingBuffer = class {
+ constructor(data) {
+ this.bufferPosition_ = 0;
+ // Validate |data|'s format and deep-copy every element.
+ this.data_ = Array.from(data, element => {
+ if (!Array.isArray(element)) {
+ throw new TypeError('Every |data| element must be an array.');
+ }
+ return Array.from(element);
+ })
+ }
+
+ next() {
+ const value = this.data_[this.bufferPosition_];
+ this.bufferPosition_ = (this.bufferPosition_ + 1) % this.data_.length;
+ return { done: false, value: value };
+ }
+
+ value() {
+ return this.data_[this.bufferPosition_];
+ }
+
+ [Symbol.iterator]() {
+ return this;
+ }
+};
+
+class DefaultSensorTraits {
+ // https://w3c.github.io/sensors/#threshold-check-algorithm
+ static isSignificantlyDifferent(reading1, reading2) {
+ return true;
+ }
+
+ // https://w3c.github.io/sensors/#reading-quantization-algorithm
+ static roundToMultiple(reading) {
+ return reading;
+ }
+
+ // https://w3c.github.io/ambient-light/#ambient-light-threshold-check-algorithm
+ static areReadingsEqual(reading1, reading2) {
+ return false;
+ }
+}
+
+class AmbientLightSensorTraits extends DefaultSensorTraits {
+ // https://w3c.github.io/ambient-light/#reduce-sensor-accuracy
+ static #ROUNDING_MULTIPLE = 50;
+ static #SIGNIFICANCE_THRESHOLD = 25;
+
+ // https://w3c.github.io/ambient-light/#ambient-light-threshold-check-algorithm
+ static isSignificantlyDifferent([illuminance1], [illuminance2]) {
+ return Math.abs(illuminance1 - illuminance2) >=
+ this.#SIGNIFICANCE_THRESHOLD;
+ }
+
+ // https://w3c.github.io/ambient-light/#ambient-light-reading-quantization-algorithm
+ static roundToMultiple(reading) {
+ const illuminance = reading[0];
+ const scaledValue =
+ illuminance / AmbientLightSensorTraits.#ROUNDING_MULTIPLE;
+ let roundedReading = reading.splice();
+
+ if (illuminance < 0.0) {
+ roundedReading[0] = -AmbientLightSensorTraits.#ROUNDING_MULTIPLE *
+ Math.floor(-scaledValue + 0.5);
+ } else {
+ roundedReading[0] = AmbientLightSensorTraits.#ROUNDING_MULTIPLE *
+ Math.floor(scaledValue + 0.5);
+ }
+
+ return roundedReading;
+ }
+
+ // https://w3c.github.io/ambient-light/#ambient-light-threshold-check-algorithm
+ static areReadingsEqual([illuminance1], [illuminance2]) {
+ return illuminance1 === illuminance2;
+ }
+}
+
+self.GenericSensorTest = (() => {
+ // Default sensor frequency in default configurations.
+ const DEFAULT_FREQUENCY = 5;
+
+ // Class that mocks Sensor interface defined in
+ // https://cs.chromium.org/chromium/src/services/device/public/mojom/sensor.mojom
+ class MockSensor {
+ static #BUFFER_OFFSET_TIMESTAMP = 1;
+ static #BUFFER_OFFSET_READINGS = 2;
+
+ constructor(sensorRequest, buffer, reportingMode, sensorType) {
+ this.client_ = null;
+ this.startShouldFail_ = false;
+ this.notifyOnReadingChange_ = true;
+ this.reportingMode_ = reportingMode;
+ this.sensorType_ = sensorType;
+ this.sensorReadingTimerId_ = null;
+ this.readingData_ = null;
+ this.requestedFrequencies_ = [];
+ // The Blink implementation (third_party/blink/renderer/modules/sensor/sensor.cc)
+ // sets a timestamp by creating a DOMHighResTimeStamp from a given platform timestamp.
+ // In this mock implementation we use a starting value
+ // and an increment step value that resemble a platform timestamp reasonably enough.
+ this.timestamp_ = window.performance.timeOrigin;
+ // |buffer| represents a SensorReadingSharedBuffer on the C++ side in
+ // Chromium. It consists, in this order, of a
+ // SensorReadingField<OneWriterSeqLock> (an 8-byte union that includes
+ // 32-bit integer used by the lock class), and a SensorReading consisting
+ // of an 8-byte timestamp and 4 8-byte reading fields.
+ //
+ // |this.buffer_[0]| is zeroed by default, which allows OneWriterSeqLock
+ // to work with our custom memory buffer that did not actually create a
+ // OneWriterSeqLock instance. It is never changed manually here.
+ //
+ // Use MockSensor.#BUFFER_OFFSET_TIMESTAMP and
+ // MockSensor.#BUFFER_OFFSET_READINGS to access the other positions in
+ // |this.buffer_| without having to hardcode magic numbers in the code.
+ this.buffer_ = buffer;
+ this.buffer_.fill(0);
+ this.receiver_ = new SensorReceiver(this);
+ this.receiver_.$.bindHandle(sensorRequest.handle);
+ this.lastRawReading_ = null;
+ this.lastRoundedReading_ = null;
+
+ if (sensorType == SensorType.AMBIENT_LIGHT) {
+ this.sensorTraits = AmbientLightSensorTraits;
+ } else {
+ this.sensorTraits = DefaultSensorTraits;
+ }
+ }
+
+ // Returns default configuration.
+ async getDefaultConfiguration() {
+ return { frequency: DEFAULT_FREQUENCY };
+ }
+
+ // Adds configuration for the sensor and starts reporting fake data
+ // through setSensorReading function.
+ async addConfiguration(configuration) {
+ this.requestedFrequencies_.push(configuration.frequency);
+ // Sort using descending order.
+ this.requestedFrequencies_.sort(
+ (first, second) => { return second - first });
+
+ if (!this.startShouldFail_ )
+ this.startReading();
+
+ return { success: !this.startShouldFail_ };
+ }
+
+ // Removes sensor configuration from the list of active configurations and
+ // stops notification about sensor reading changes if
+ // requestedFrequencies_ is empty.
+ removeConfiguration(configuration) {
+ const index = this.requestedFrequencies_.indexOf(configuration.frequency);
+ if (index == -1)
+ return;
+
+ this.requestedFrequencies_.splice(index, 1);
+ if (this.requestedFrequencies_.length === 0)
+ this.stopReading();
+ }
+
+ // ConfigureReadingChangeNotifications(bool enabled)
+ // Configures whether to report a reading change when in ON_CHANGE
+ // reporting mode.
+ configureReadingChangeNotifications(notifyOnReadingChange) {
+ this.notifyOnReadingChange_ = notifyOnReadingChange;
+ }
+
+ resume() {
+ this.startReading();
+ }
+
+ suspend() {
+ this.stopReading();
+ }
+
+ // Mock functions
+
+ // Resets mock Sensor state.
+ reset() {
+ this.stopReading();
+ this.startShouldFail_ = false;
+ this.requestedFrequencies_ = [];
+ this.notifyOnReadingChange_ = true;
+ this.readingData_ = null;
+ this.buffer_.fill(0);
+ this.receiver_.$.close();
+ this.lastRawReading_ = null;
+ this.lastRoundedReading_ = null;
+ }
+
+ // Sets fake data that is used to deliver sensor reading updates.
+ setSensorReading(readingData) {
+ this.readingData_ = new RingBuffer(readingData);
+ }
+
+ // This is a workaround to accommodate Blink's Device Orientation
+ // implementation. In general, all tests should use setSensorReading()
+ // instead.
+ setSensorReadingImmediately(readingData) {
+ this.setSensorReading(readingData);
+
+ const reading = this.readingData_.value();
+ this.buffer_.set(reading, MockSensor.#BUFFER_OFFSET_READINGS);
+ this.buffer_[MockSensor.#BUFFER_OFFSET_TIMESTAMP] = this.timestamp_++;
+ }
+
+ // Sets flag that forces sensor to fail when addConfiguration is invoked.
+ setStartShouldFail(shouldFail) {
+ this.startShouldFail_ = shouldFail;
+ }
+
+ startReading() {
+ if (this.readingData_ != null) {
+ this.stopReading();
+ }
+ let maxFrequencyUsed = this.requestedFrequencies_[0];
+ let timeout = (1 / maxFrequencyUsed) * 1000;
+ this.sensorReadingTimerId_ = window.setInterval(() => {
+ if (this.readingData_) {
+ // |buffer_| is a TypedArray, so we need to make sure pass an
+ // array to set().
+ const reading = this.readingData_.next().value;
+ if (!Array.isArray(reading)) {
+ throw new TypeError("startReading(): The readings passed to " +
+ "setSensorReading() must be arrays");
+ }
+
+ if (this.reportingMode_ == ReportingMode.ON_CHANGE &&
+ this.lastRawReading_ !== null &&
+ !this.sensorTraits.isSignificantlyDifferent(
+ this.lastRawReading_, reading)) {
+ // In case new value is not significantly different compared to
+ // old value, new value is not sent.
+ return;
+ }
+
+ this.lastRawReading_ = reading.slice();
+ const roundedReading = this.sensorTraits.roundToMultiple(reading);
+
+ if (this.reportingMode_ == ReportingMode.ON_CHANGE &&
+ this.lastRoundedReading_ !== null &&
+ this.sensorTraits.areReadingsEqual(
+ roundedReading, this.lastRoundedReading_)) {
+ // In case new rounded value is not different compared to old
+ // value, new value is not sent.
+ return;
+ }
+ this.buffer_.set(roundedReading, MockSensor.#BUFFER_OFFSET_READINGS);
+ this.lastRoundedReading_ = roundedReading;
+ }
+
+ // For all tests sensor reading should have monotonically
+ // increasing timestamp.
+ this.buffer_[MockSensor.#BUFFER_OFFSET_TIMESTAMP] = this.timestamp_++;
+
+ if (this.reportingMode_ === ReportingMode.ON_CHANGE &&
+ this.notifyOnReadingChange_) {
+ this.client_.sensorReadingChanged();
+ }
+ }, timeout);
+ }
+
+ stopReading() {
+ if (this.sensorReadingTimerId_ != null) {
+ window.clearInterval(this.sensorReadingTimerId_);
+ this.sensorReadingTimerId_ = null;
+ }
+ }
+
+ getSamplingFrequency() {
+ if (this.requestedFrequencies_.length == 0) {
+ throw new Error("getSamplingFrequency(): No configured frequency");
+ }
+ return this.requestedFrequencies_[0];
+ }
+
+ isReadingData() {
+ return this.sensorReadingTimerId_ != null;
+ }
+ }
+
+ // Class that mocks SensorProvider interface defined in
+ // https://cs.chromium.org/chromium/src/services/device/public/mojom/sensor_provider.mojom
+ class MockSensorProvider {
+ constructor() {
+ this.readingSizeInBytes_ =
+ Number(SensorInitParams_READ_BUFFER_SIZE_FOR_TESTS);
+ this.sharedBufferSizeInBytes_ =
+ this.readingSizeInBytes_ * (SensorType.MAX_VALUE + 1);
+ let rv = Mojo.createSharedBuffer(this.sharedBufferSizeInBytes_);
+ if (rv.result != Mojo.RESULT_OK) {
+ throw new Error('MockSensorProvider: Failed to create shared buffer');
+ }
+ const handle = rv.handle;
+ rv = handle.mapBuffer(0, this.sharedBufferSizeInBytes_);
+ if (rv.result != Mojo.RESULT_OK) {
+ throw new Error("MockSensorProvider: Failed to map shared buffer");
+ }
+ this.shmemArrayBuffer_ = rv.buffer;
+ rv = handle.duplicateBufferHandle({readOnly: true});
+ if (rv.result != Mojo.RESULT_OK) {
+ throw new Error(
+ 'MockSensorProvider: failed to duplicate shared buffer');
+ }
+ this.readOnlySharedBufferHandle_ = rv.handle;
+ this.activeSensors_ = new Map();
+ this.resolveFuncs_ = new Map();
+ this.getSensorShouldFail_ = new Map();
+ this.permissionsDenied_ = new Map();
+ this.maxFrequency_ = 60;
+ this.minFrequency_ = 1;
+ this.mojomSensorType_ = new Map([
+ ['Accelerometer', SensorType.ACCELEROMETER],
+ ['LinearAccelerationSensor', SensorType.LINEAR_ACCELERATION],
+ ['GravitySensor', SensorType.GRAVITY],
+ ['AmbientLightSensor', SensorType.AMBIENT_LIGHT],
+ ['Gyroscope', SensorType.GYROSCOPE],
+ ['Magnetometer', SensorType.MAGNETOMETER],
+ ['AbsoluteOrientationSensor',
+ SensorType.ABSOLUTE_ORIENTATION_QUATERNION],
+ ['AbsoluteOrientationEulerAngles',
+ SensorType.ABSOLUTE_ORIENTATION_EULER_ANGLES],
+ ['RelativeOrientationSensor',
+ SensorType.RELATIVE_ORIENTATION_QUATERNION],
+ ['RelativeOrientationEulerAngles',
+ SensorType.RELATIVE_ORIENTATION_EULER_ANGLES],
+ ['ProximitySensor', SensorType.PROXIMITY]
+ ]);
+ this.receiver_ = new SensorProviderReceiver(this);
+
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(SensorProvider.$interfaceName);
+ this.interceptor_.oninterfacerequest = e => {
+ this.bindToPipe(e.handle);
+ };
+ this.interceptor_.start();
+ }
+
+ // Returns initialized Sensor proxy to the client.
+ async getSensor(type) {
+ if (this.getSensorShouldFail_.get(type)) {
+ return {result: SensorCreationResult.ERROR_NOT_AVAILABLE,
+ initParams: null};
+ }
+ if (this.permissionsDenied_.get(type)) {
+ return {result: SensorCreationResult.ERROR_NOT_ALLOWED,
+ initParams: null};
+ }
+
+ const offset = type * this.readingSizeInBytes_;
+ const reportingMode = ReportingMode.ON_CHANGE;
+
+ const sensor = new SensorRemote();
+ if (!this.activeSensors_.has(type)) {
+ const shmemView = new Float64Array(
+ this.shmemArrayBuffer_, offset,
+ this.readingSizeInBytes_ / Float64Array.BYTES_PER_ELEMENT);
+ const mockSensor = new MockSensor(
+ sensor.$.bindNewPipeAndPassReceiver(), shmemView, reportingMode,
+ type);
+ this.activeSensors_.set(type, mockSensor);
+ this.activeSensors_.get(type).client_ = new SensorClientRemote();
+ }
+
+ const rv = this.readOnlySharedBufferHandle_.duplicateBufferHandle(
+ {readOnly: true});
+ if (rv.result != Mojo.RESULT_OK) {
+ throw new Error('getSensor(): failed to duplicate shared buffer');
+ }
+
+ const defaultConfig = { frequency: DEFAULT_FREQUENCY };
+ // Consider sensor traits to meet assertions in C++ code (see
+ // services/device/public/cpp/generic_sensor/sensor_traits.h)
+ if (type == SensorType.AMBIENT_LIGHT || type == SensorType.MAGNETOMETER) {
+ this.maxFrequency_ = Math.min(10, this.maxFrequency_);
+ }
+
+ const client = this.activeSensors_.get(type).client_;
+ const initParams = {
+ sensor,
+ clientReceiver: client.$.bindNewPipeAndPassReceiver(),
+ memory: {buffer: rv.handle},
+ bufferOffset: BigInt(offset),
+ mode: reportingMode,
+ defaultConfiguration: defaultConfig,
+ minimumFrequency: this.minFrequency_,
+ maximumFrequency: this.maxFrequency_
+ };
+
+ if (this.resolveFuncs_.has(type)) {
+ for (let resolveFunc of this.resolveFuncs_.get(type)) {
+ resolveFunc(this.activeSensors_.get(type));
+ }
+ this.resolveFuncs_.delete(type);
+ }
+
+ return {result: SensorCreationResult.SUCCESS, initParams};
+ }
+
+ // Binds object to mojo message pipe
+ bindToPipe(pipe) {
+ this.receiver_.$.bindHandle(pipe);
+ }
+
+ // Mock functions
+
+ // Resets state of mock SensorProvider between test runs.
+ reset() {
+ for (const sensor of this.activeSensors_.values()) {
+ sensor.reset();
+ }
+ this.activeSensors_.clear();
+ this.resolveFuncs_.clear();
+ this.getSensorShouldFail_.clear();
+ this.permissionsDenied_.clear();
+ this.maxFrequency_ = 60;
+ this.minFrequency_ = 1;
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+ }
+
+ // Sets flag that forces mock SensorProvider to fail when getSensor() is
+ // invoked.
+ setGetSensorShouldFail(sensorType, shouldFail) {
+ this.getSensorShouldFail_.set(this.mojomSensorType_.get(sensorType),
+ shouldFail);
+ }
+
+ setPermissionsDenied(sensorType, permissionsDenied) {
+ this.permissionsDenied_.set(this.mojomSensorType_.get(sensorType),
+ permissionsDenied);
+ }
+
+ // Returns mock sensor that was created in getSensor to the layout test.
+ getCreatedSensor(sensorType) {
+ const type = this.mojomSensorType_.get(sensorType);
+ if (typeof type != "number") {
+ throw new TypeError(`getCreatedSensor(): Invalid sensor type ${sensorType}`);
+ }
+
+ if (this.activeSensors_.has(type)) {
+ return Promise.resolve(this.activeSensors_.get(type));
+ }
+
+ return new Promise(resolve => {
+ if (!this.resolveFuncs_.has(type)) {
+ this.resolveFuncs_.set(type, []);
+ }
+ this.resolveFuncs_.get(type).push(resolve);
+ });
+ }
+
+ // Sets the maximum frequency for a concrete sensor.
+ setMaximumSupportedFrequency(frequency) {
+ this.maxFrequency_ = frequency;
+ }
+
+ // Sets the minimum frequency for a concrete sensor.
+ setMinimumSupportedFrequency(frequency) {
+ this.minFrequency_ = frequency;
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ sensorProvider: null
+ }
+
+ class GenericSensorTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ async initialize() {
+ if (testInternal.initialized)
+ throw new Error('Call reset() before initialize().');
+
+ // Grant sensor permissions for Chromium testdriver.
+ // testdriver.js only works in the top-level browsing context, so do
+ // nothing if we're in e.g. an iframe.
+ if (window.parent === window) {
+ for (const entry
+ of ['accelerometer', 'gyroscope', 'magnetometer',
+ 'ambient-light-sensor']) {
+ await test_driver.set_permission({name: entry}, 'granted');
+ }
+ }
+
+ testInternal.sensorProvider = new MockSensorProvider;
+ testInternal.initialized = true;
+ }
+ // Resets state of sensor mocks between test runs.
+ async reset() {
+ if (!testInternal.initialized)
+ throw new Error('Call initialize() before reset().');
+ testInternal.sensorProvider.reset();
+ testInternal.sensorProvider = null;
+ testInternal.initialized = false;
+
+ // Wait for an event loop iteration to let any pending mojo commands in
+ // the sensor provider finish.
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+
+ getSensorProvider() {
+ return testInternal.sensorProvider;
+ }
+ }
+
+ return GenericSensorTestChromium;
+})();
diff --git a/test/wpt/tests/resources/chromium/generic_sensor_mocks.js.headers b/test/wpt/tests/resources/chromium/generic_sensor_mocks.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/generic_sensor_mocks.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/resources/chromium/mock-barcodedetection.js b/test/wpt/tests/resources/chromium/mock-barcodedetection.js
new file mode 100644
index 0000000..b0d2e0a
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-barcodedetection.js
@@ -0,0 +1,136 @@
+import {BarcodeDetectionReceiver, BarcodeFormat} from '/gen/services/shape_detection/public/mojom/barcodedetection.mojom.m.js';
+import {BarcodeDetectionProvider, BarcodeDetectionProviderReceiver} from '/gen/services/shape_detection/public/mojom/barcodedetection_provider.mojom.m.js';
+
+self.BarcodeDetectionTest = (() => {
+ // Class that mocks BarcodeDetectionProvider interface defined in
+ // https://cs.chromium.org/chromium/src/services/shape_detection/public/mojom/barcodedetection_provider.mojom
+ class MockBarcodeDetectionProvider {
+ constructor() {
+ this.receiver_ = new BarcodeDetectionProviderReceiver(this);
+
+ this.interceptor_ = new MojoInterfaceInterceptor(
+ BarcodeDetectionProvider.$interfaceName);
+ this.interceptor_.oninterfacerequest = e => {
+ if (this.should_close_pipe_on_request_)
+ e.handle.close();
+ else
+ this.receiver_.$.bindHandle(e.handle);
+ }
+ this.interceptor_.start();
+ this.should_close_pipe_on_request_ = false;
+ }
+
+ createBarcodeDetection(request, options) {
+ this.mockService_ = new MockBarcodeDetection(request, options);
+ }
+
+ enumerateSupportedFormats() {
+ return {
+ supportedFormats: [
+ BarcodeFormat.AZTEC,
+ BarcodeFormat.DATA_MATRIX,
+ BarcodeFormat.QR_CODE,
+ ]
+ };
+ }
+
+ getFrameData() {
+ return this.mockService_.bufferData_;
+ }
+
+ getFormats() {
+ return this.mockService_.options_.formats;
+ }
+
+ reset() {
+ this.mockService_ = null;
+ this.should_close_pipe_on_request_ = false;
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+ }
+
+ // simulate a 'no implementation available' case
+ simulateNoImplementation() {
+ this.should_close_pipe_on_request_ = true;
+ }
+ }
+
+ // Class that mocks BarcodeDetection interface defined in
+ // https://cs.chromium.org/chromium/src/services/shape_detection/public/mojom/barcodedetection.mojom
+ class MockBarcodeDetection {
+ constructor(request, options) {
+ this.options_ = options;
+ this.receiver_ = new BarcodeDetectionReceiver(this);
+ this.receiver_.$.bindHandle(request.handle);
+ }
+
+ detect(bitmapData) {
+ this.bufferData_ =
+ new Uint32Array(getArrayBufferFromBigBuffer(bitmapData.pixelData));
+ return {
+ results: [
+ {
+ rawValue : "cats",
+ boundingBox: { x: 1.0, y: 1.0, width: 100.0, height: 100.0 },
+ format: BarcodeFormat.QR_CODE,
+ cornerPoints: [
+ { x: 1.0, y: 1.0 },
+ { x: 101.0, y: 1.0 },
+ { x: 101.0, y: 101.0 },
+ { x: 1.0, y: 101.0 }
+ ],
+ },
+ {
+ rawValue : "dogs",
+ boundingBox: { x: 2.0, y: 2.0, width: 50.0, height: 50.0 },
+ format: BarcodeFormat.CODE_128,
+ cornerPoints: [
+ { x: 2.0, y: 2.0 },
+ { x: 52.0, y: 2.0 },
+ { x: 52.0, y: 52.0 },
+ { x: 2.0, y: 52.0 }
+ ],
+ },
+ ],
+ };
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ MockBarcodeDetectionProvider: null
+ }
+
+ class BarcodeDetectionTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ initialize() {
+ if (testInternal.initialized)
+ throw new Error('Call reset() before initialize().');
+
+ testInternal.MockBarcodeDetectionProvider = new MockBarcodeDetectionProvider;
+ testInternal.initialized = true;
+ }
+
+ // Resets state of barcode detection mocks between test runs.
+ async reset() {
+ if (!testInternal.initialized)
+ throw new Error('Call initialize() before reset().');
+ testInternal.MockBarcodeDetectionProvider.reset();
+ testInternal.MockBarcodeDetectionProvider = null;
+ testInternal.initialized = false;
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+
+ MockBarcodeDetectionProvider() {
+ return testInternal.MockBarcodeDetectionProvider;
+ }
+ }
+
+ return BarcodeDetectionTestChromium;
+})();
+
+self.BarcodeFormat = BarcodeFormat;
diff --git a/test/wpt/tests/resources/chromium/mock-barcodedetection.js.headers b/test/wpt/tests/resources/chromium/mock-barcodedetection.js.headers
new file mode 100644
index 0000000..6c61a34
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-barcodedetection.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8 \ No newline at end of file
diff --git a/test/wpt/tests/resources/chromium/mock-battery-monitor.headers b/test/wpt/tests/resources/chromium/mock-battery-monitor.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-battery-monitor.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/resources/chromium/mock-battery-monitor.js b/test/wpt/tests/resources/chromium/mock-battery-monitor.js
new file mode 100644
index 0000000..8fa27bc
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-battery-monitor.js
@@ -0,0 +1,61 @@
+import {BatteryMonitor, BatteryMonitorReceiver} from '/gen/services/device/public/mojom/battery_monitor.mojom.m.js';
+
+class MockBatteryMonitor {
+ constructor() {
+ this.receiver_ = new BatteryMonitorReceiver(this);
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(BatteryMonitor.$interfaceName);
+ this.interceptor_.oninterfacerequest = e =>
+ this.receiver_.$.bindHandle(e.handle);
+ this.reset();
+ }
+
+ start() {
+ this.interceptor_.start();
+ }
+
+ stop() {
+ this.interceptor_.stop();
+ }
+
+ reset() {
+ this.pendingRequests_ = [];
+ this.status_ = null;
+ this.lastKnownStatus_ = null;
+ }
+
+ queryNextStatus() {
+ const result = new Promise(resolve => this.pendingRequests_.push(resolve));
+ this.runCallbacks_();
+ return result;
+ }
+
+ setBatteryStatus(charging, chargingTime, dischargingTime, level) {
+ this.status_ = {charging, chargingTime, dischargingTime, level};
+ this.lastKnownStatus_ = this.status_;
+ this.runCallbacks_();
+ }
+
+ verifyBatteryStatus(manager) {
+ assert_not_equals(manager, undefined);
+ assert_not_equals(this.lastKnownStatus_, null);
+ assert_equals(manager.charging, this.lastKnownStatus_.charging);
+ assert_equals(manager.chargingTime, this.lastKnownStatus_.chargingTime);
+ assert_equals(
+ manager.dischargingTime, this.lastKnownStatus_.dischargingTime);
+ assert_equals(manager.level, this.lastKnownStatus_.level);
+ }
+
+ runCallbacks_() {
+ if (!this.status_ || !this.pendingRequests_.length)
+ return;
+
+ let result = {status: this.status_};
+ while (this.pendingRequests_.length) {
+ this.pendingRequests_.pop()(result);
+ }
+ this.status_ = null;
+ }
+}
+
+export const mockBatteryMonitor = new MockBatteryMonitor();
diff --git a/test/wpt/tests/resources/chromium/mock-direct-sockets.js b/test/wpt/tests/resources/chromium/mock-direct-sockets.js
new file mode 100644
index 0000000..6d557f7
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-direct-sockets.js
@@ -0,0 +1,94 @@
+'use strict';
+
+import {DirectSocketsService, DirectSocketsServiceReceiver} from '/gen/third_party/blink/public/mojom/direct_sockets/direct_sockets.mojom.m.js';
+
+self.DirectSocketsServiceTest = (() => {
+ // Class that mocks DirectSocketsService interface defined in
+ // https://source.chromium.org/chromium/chromium/src/third_party/blink/public/mojom/direct_sockets/direct_sockets.mojom
+ class MockDirectSocketsService {
+ constructor() {
+ this.interceptor_ = new MojoInterfaceInterceptor(DirectSocketsService.$interfaceName);
+ this.receiver_ = new DirectSocketsServiceReceiver(this);
+ this.interceptor_.oninterfacerequest = e =>
+ this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+ }
+
+ reset() {
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+ }
+
+ openTCPSocket(
+ options,
+ receiver,
+ observer) {
+ return Promise.resolve({
+ // return result = net:Error::NOT_IMPLEMENTED (code -11)
+ result: -11
+ });
+ }
+
+ openConnectedUDPSocket(
+ options,
+ receiver,
+ listener) {
+ return Promise.resolve({
+ // return result = net:Error::NOT_IMPLEMENTED (code -11)
+ result: -11
+ });
+ }
+
+ openBoundUDPSocket(
+ options,
+ receiver,
+ listener) {
+ return Promise.resolve({
+ // return result = net:Error::NOT_IMPLEMENTED (code -11)
+ result: -11
+ });
+ }
+
+ openTCPServerSocket(
+ options,
+ receiver) {
+ return Promise.resolve({
+ // return result = net:Error::NOT_IMPLEMENTED (code -11)
+ result: -11
+ });
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ mockDirectSocketsService: null
+ }
+
+ class DirectSocketsServiceTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ initialize() {
+ if (!testInternal.initialized) {
+ testInternal = {
+ mockDirectSocketsService: new MockDirectSocketsService(),
+ initialized: true
+ };
+ }
+ }
+
+ async reset() {
+ if (testInternal.initialized) {
+ testInternal.mockDirectSocketsService.reset();
+ testInternal = {
+ mockDirectSocketsService: null,
+ initialized: false
+ };
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+ }
+ }
+
+ return DirectSocketsServiceTestChromium;
+})();
diff --git a/test/wpt/tests/resources/chromium/mock-facedetection.js b/test/wpt/tests/resources/chromium/mock-facedetection.js
new file mode 100644
index 0000000..7ae6586
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-facedetection.js
@@ -0,0 +1,130 @@
+import {FaceDetectionReceiver, LandmarkType} from '/gen/services/shape_detection/public/mojom/facedetection.mojom.m.js';
+import {FaceDetectionProvider, FaceDetectionProviderReceiver} from '/gen/services/shape_detection/public/mojom/facedetection_provider.mojom.m.js';
+
+self.FaceDetectionTest = (() => {
+ // Class that mocks FaceDetectionProvider interface defined in
+ // https://cs.chromium.org/chromium/src/services/shape_detection/public/mojom/facedetection_provider.mojom
+ class MockFaceDetectionProvider {
+ constructor() {
+ this.receiver_ = new FaceDetectionProviderReceiver(this);
+
+ this.interceptor_ = new MojoInterfaceInterceptor(
+ FaceDetectionProvider.$interfaceName);
+ this.interceptor_.oninterfacerequest =
+ e => this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+ }
+
+ createFaceDetection(request, options) {
+ this.mockService_ = new MockFaceDetection(request, options);
+ }
+
+ getFrameData() {
+ return this.mockService_.bufferData_;
+ }
+
+ getMaxDetectedFaces() {
+ return this.mockService_.maxDetectedFaces_;
+ }
+
+ getFastMode () {
+ return this.mockService_.fastMode_;
+ }
+
+ reset() {
+ this.mockService_ = null;
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+ }
+ }
+
+ // Class that mocks FaceDetection interface defined in
+ // https://cs.chromium.org/chromium/src/services/shape_detection/public/mojom/facedetection.mojom
+ class MockFaceDetection {
+ constructor(request, options) {
+ this.maxDetectedFaces_ = options.maxDetectedFaces;
+ this.fastMode_ = options.fastMode;
+ this.receiver_ = new FaceDetectionReceiver(this);
+ this.receiver_.$.bindHandle(request.handle);
+ }
+
+ detect(bitmapData) {
+ this.bufferData_ =
+ new Uint32Array(getArrayBufferFromBigBuffer(bitmapData.pixelData));
+ return Promise.resolve({
+ results: [
+ {
+ boundingBox: {x: 1.0, y: 1.0, width: 100.0, height: 100.0},
+ landmarks: [{
+ type: LandmarkType.EYE,
+ locations: [{x: 4.0, y: 5.0}]
+ },
+ {
+ type: LandmarkType.EYE,
+ locations: [
+ {x: 4.0, y: 5.0}, {x: 5.0, y: 4.0}, {x: 6.0, y: 3.0},
+ {x: 7.0, y: 4.0}, {x: 8.0, y: 5.0}, {x: 7.0, y: 6.0},
+ {x: 6.0, y: 7.0}, {x: 5.0, y: 6.0}
+ ]
+ }]
+ },
+ {
+ boundingBox: {x: 2.0, y: 2.0, width: 200.0, height: 200.0},
+ landmarks: [{
+ type: LandmarkType.NOSE,
+ locations: [{x: 100.0, y: 50.0}]
+ },
+ {
+ type: LandmarkType.NOSE,
+ locations: [
+ {x: 80.0, y: 50.0}, {x: 70.0, y: 60.0}, {x: 60.0, y: 70.0},
+ {x: 70.0, y: 60.0}, {x: 80.0, y: 70.0}, {x: 90.0, y: 80.0},
+ {x: 100.0, y: 70.0}, {x: 90.0, y: 60.0}, {x: 80.0, y: 50.0}
+ ]
+ }]
+ },
+ {
+ boundingBox: {x: 3.0, y: 3.0, width: 300.0, height: 300.0},
+ landmarks: []
+ },
+ ]
+ });
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ MockFaceDetectionProvider: null
+ }
+
+ class FaceDetectionTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ initialize() {
+ if (testInternal.initialized)
+ throw new Error('Call reset() before initialize().');
+
+ testInternal.MockFaceDetectionProvider = new MockFaceDetectionProvider;
+ testInternal.initialized = true;
+ }
+
+ // Resets state of face detection mocks between test runs.
+ async reset() {
+ if (!testInternal.initialized)
+ throw new Error('Call initialize() before reset().');
+ testInternal.MockFaceDetectionProvider.reset();
+ testInternal.MockFaceDetectionProvider = null;
+ testInternal.initialized = false;
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+
+ MockFaceDetectionProvider() {
+ return testInternal.MockFaceDetectionProvider;
+ }
+ }
+
+ return FaceDetectionTestChromium;
+})();
diff --git a/test/wpt/tests/resources/chromium/mock-facedetection.js.headers b/test/wpt/tests/resources/chromium/mock-facedetection.js.headers
new file mode 100644
index 0000000..6c61a34
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-facedetection.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8 \ No newline at end of file
diff --git a/test/wpt/tests/resources/chromium/mock-idle-detection.js b/test/wpt/tests/resources/chromium/mock-idle-detection.js
new file mode 100644
index 0000000..54fe5dd
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-idle-detection.js
@@ -0,0 +1,80 @@
+import {IdleManager, IdleManagerError, IdleManagerReceiver} from '/gen/third_party/blink/public/mojom/idle/idle_manager.mojom.m.js';
+
+/**
+ * This is a testing framework that enables us to test the user idle detection
+ * by intercepting the connection between the renderer and the browser and
+ * exposing a mocking API for tests.
+ *
+ * Usage:
+ *
+ * 1) Include <script src="mock.js"></script> in your file.
+ * 2) Set expectations
+ * expect(addMonitor).andReturn((threshold, monitorPtr, callback) => {
+ * // mock behavior
+ * })
+ * 3) Call navigator.idle.query()
+ *
+ * The mocking API is blink agnostic and is designed such that other engines
+ * could implement it too. Here are the symbols that are exposed to tests:
+ *
+ * - function addMonitor(): the main/only function that can be mocked.
+ * - function expect(): the main/only function that enables us to mock it.
+ * - function close(): disconnects the interceptor.
+ * - enum UserIdleState {IDLE, ACTIVE}: blink agnostic constants.
+ * - enum ScreenIdleState {LOCKED, UNLOCKED}: blink agnostic constants.
+ */
+
+class FakeIdleMonitor {
+ addMonitor(threshold, monitorPtr, callback) {
+ return this.handler.addMonitor(threshold, monitorPtr);
+ }
+ setHandler(handler) {
+ this.handler = handler;
+ return this;
+ }
+ setBinding(binding) {
+ this.binding = binding;
+ return this;
+ }
+ close() {
+ this.binding.$.close();
+ }
+}
+
+self.IdleDetectorError = {};
+
+self.addMonitor = function addMonitor(threshold, monitorPtr, callback) {
+ throw new Error("expected to be overriden by tests");
+}
+
+async function close() {
+ interceptor.close();
+}
+
+self.expect = function(call) {
+ return {
+ andReturn(callback) {
+ let handler = {};
+ handler[call.name] = callback;
+ interceptor.setHandler(handler);
+ }
+ };
+};
+
+function intercept() {
+ let result = new FakeIdleMonitor();
+
+ let binding = new IdleManagerReceiver(result);
+ let interceptor = new MojoInterfaceInterceptor(IdleManager.$interfaceName);
+ interceptor.oninterfacerequest = e => binding.$.bindHandle(e.handle);
+ interceptor.start();
+
+ self.IdleDetectorError.SUCCESS = IdleManagerError.kSuccess;
+ self.IdleDetectorError.PERMISSION_DISABLED =
+ IdleManagerError.kPermissionDisabled;
+
+ result.setBinding(binding);
+ return result;
+}
+
+const interceptor = intercept();
diff --git a/test/wpt/tests/resources/chromium/mock-imagecapture.js b/test/wpt/tests/resources/chromium/mock-imagecapture.js
new file mode 100644
index 0000000..8424e1e
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-imagecapture.js
@@ -0,0 +1,309 @@
+import {BackgroundBlurMode, FillLightMode, ImageCapture, ImageCaptureReceiver, MeteringMode, RedEyeReduction} from '/gen/media/capture/mojom/image_capture.mojom.m.js';
+
+self.ImageCaptureTest = (() => {
+ // Class that mocks ImageCapture interface defined in
+ // https://cs.chromium.org/chromium/src/media/capture/mojom/image_capture.mojom
+ class MockImageCapture {
+ constructor() {
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(ImageCapture.$interfaceName);
+ this.interceptor_.oninterfacerequest =
+ e => this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+
+ this.state_ = {
+ state: {
+ supportedWhiteBalanceModes: [
+ MeteringMode.SINGLE_SHOT,
+ MeteringMode.CONTINUOUS
+ ],
+ currentWhiteBalanceMode: MeteringMode.CONTINUOUS,
+ supportedExposureModes: [
+ MeteringMode.MANUAL,
+ MeteringMode.SINGLE_SHOT,
+ MeteringMode.CONTINUOUS
+ ],
+ currentExposureMode: MeteringMode.MANUAL,
+ supportedFocusModes: [
+ MeteringMode.MANUAL,
+ MeteringMode.SINGLE_SHOT
+ ],
+ currentFocusMode: MeteringMode.MANUAL,
+ pointsOfInterest: [{
+ x: 0.4,
+ y: 0.6
+ }],
+
+ exposureCompensation: {
+ min: -200.0,
+ max: 200.0,
+ current: 33.0,
+ step: 33.0
+ },
+ exposureTime: {
+ min: 100.0,
+ max: 100000.0,
+ current: 1000.0,
+ step: 100.0
+ },
+ colorTemperature: {
+ min: 2500.0,
+ max: 6500.0,
+ current: 6000.0,
+ step: 1000.0
+ },
+ iso: {
+ min: 100.0,
+ max: 12000.0,
+ current: 400.0,
+ step: 1.0
+ },
+
+ brightness: {
+ min: 1.0,
+ max: 10.0,
+ current: 5.0,
+ step: 1.0
+ },
+ contrast: {
+ min: 2.0,
+ max: 9.0,
+ current: 5.0,
+ step: 1.0
+ },
+ saturation: {
+ min: 3.0,
+ max: 8.0,
+ current: 6.0,
+ step: 1.0
+ },
+ sharpness: {
+ min: 4.0,
+ max: 7.0,
+ current: 7.0,
+ step: 1.0
+ },
+
+ focusDistance: {
+ min: 1.0,
+ max: 10.0,
+ current: 3.0,
+ step: 1.0
+ },
+
+ pan: {
+ min: 0.0,
+ max: 10.0,
+ current: 5.0,
+ step: 2.0
+ },
+
+ tilt: {
+ min: 0.0,
+ max: 10.0,
+ current: 5.0,
+ step: 2.0
+ },
+
+ zoom: {
+ min: 0.0,
+ max: 10.0,
+ current: 5.0,
+ step: 5.0
+ },
+
+ supportsTorch: true,
+ torch: false,
+
+ redEyeReduction: RedEyeReduction.CONTROLLABLE,
+ height: {
+ min: 240.0,
+ max: 2448.0,
+ current: 240.0,
+ step: 2.0
+ },
+ width: {
+ min: 320.0,
+ max: 3264.0,
+ current: 320.0,
+ step: 3.0
+ },
+ fillLightMode: [FillLightMode.AUTO, FillLightMode.FLASH],
+
+ supportedBackgroundBlurModes: [
+ BackgroundBlurMode.OFF,
+ BackgroundBlurMode.BLUR
+ ],
+ backgroundBlurMode: BackgroundBlurMode.OFF,
+ }
+ };
+ this.panTiltZoomPermissionStatus_ = null;
+ this.settings_ = null;
+ this.receiver_ = new ImageCaptureReceiver(this);
+ }
+
+ reset() {
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+ }
+
+ async getPhotoState(source_id) {
+ const shouldKeepPanTiltZoom = await this.isPanTiltZoomPermissionGranted();
+ if (shouldKeepPanTiltZoom)
+ return Promise.resolve(this.state_);
+
+ const newState = {...this.state_};
+ newState.state.pan = {};
+ newState.state.tilt = {};
+ newState.state.zoom = {};
+ return Promise.resolve(newState);
+ }
+
+ async setPhotoOptions(source_id, settings) {
+ const isAllowedToControlPanTiltZoom = await this.isPanTiltZoomPermissionGranted();
+ if (!isAllowedToControlPanTiltZoom &&
+ (settings.hasPan || settings.hasTilt || settings.hasZoom)) {
+ return Promise.resolve({ success: false });
+ }
+ this.settings_ = settings;
+ if (settings.hasIso)
+ this.state_.state.iso.current = settings.iso;
+ if (settings.hasHeight)
+ this.state_.state.height.current = settings.height;
+ if (settings.hasWidth)
+ this.state_.state.width.current = settings.width;
+ if (settings.hasPan)
+ this.state_.state.pan.current = settings.pan;
+ if (settings.hasTilt)
+ this.state_.state.tilt.current = settings.tilt;
+ if (settings.hasZoom)
+ this.state_.state.zoom.current = settings.zoom;
+ if (settings.hasFocusMode)
+ this.state_.state.currentFocusMode = settings.focusMode;
+ if (settings.hasFocusDistance)
+ this.state_.state.focusDistance.current = settings.focusDistance;
+
+ if (settings.pointsOfInterest.length > 0) {
+ this.state_.state.pointsOfInterest =
+ settings.pointsOfInterest;
+ }
+
+ if (settings.hasExposureMode)
+ this.state_.state.currentExposureMode = settings.exposureMode;
+
+ if (settings.hasExposureCompensation) {
+ this.state_.state.exposureCompensation.current =
+ settings.exposureCompensation;
+ }
+ if (settings.hasExposureTime) {
+ this.state_.state.exposureTime.current =
+ settings.exposureTime;
+ }
+ if (settings.hasWhiteBalanceMode) {
+ this.state_.state.currentWhiteBalanceMode =
+ settings.whiteBalanceMode;
+ }
+ if (settings.hasFillLightMode)
+ this.state_.state.fillLightMode = [settings.fillLightMode];
+ if (settings.hasRedEyeReduction)
+ this.state_.state.redEyeReduction = settings.redEyeReduction;
+ if (settings.hasColorTemperature) {
+ this.state_.state.colorTemperature.current =
+ settings.colorTemperature;
+ }
+ if (settings.hasBrightness)
+ this.state_.state.brightness.current = settings.brightness;
+ if (settings.hasContrast)
+ this.state_.state.contrast.current = settings.contrast;
+ if (settings.hasSaturation)
+ this.state_.state.saturation.current = settings.saturation;
+ if (settings.hasSharpness)
+ this.state_.state.sharpness.current = settings.sharpness;
+
+ if (settings.hasTorch)
+ this.state_.state.torch = settings.torch;
+
+ if (settings.hasBackgroundBlurMode)
+ this.state_.state.backgroundBlurMode = [settings.backgroundBlurMode];
+
+ return Promise.resolve({
+ success: true
+ });
+ }
+
+ takePhoto(source_id) {
+ return Promise.resolve({
+ blob: {
+ mimeType: 'image/cat',
+ data: new Array(2)
+ }
+ });
+ }
+
+ async isPanTiltZoomPermissionGranted() {
+ if (!this.panTiltZoomPermissionStatus_) {
+ this.panTiltZoomPermissionStatus_ = await navigator.permissions.query({
+ name: "camera",
+ panTiltZoom: true
+ });
+ }
+ return this.panTiltZoomPermissionStatus_.state == "granted";
+ }
+
+ state() {
+ return this.state_.state;
+ }
+
+ turnOffBackgroundBlurMode() {
+ this.state_.state.backgroundBlurMode = BackgroundBlurMode.OFF;
+ }
+ turnOnBackgroundBlurMode() {
+ this.state_.state.backgroundBlurMode = BackgroundBlurMode.BLUR;
+ }
+ turnOffSupportedBackgroundBlurModes() {
+ this.state_.state.supportedBackgroundBlurModes = [BackgroundBlurMode.OFF];
+ }
+ turnOnSupportedBackgroundBlurModes() {
+ this.state_.state.supportedBackgroundBlurModes = [BackgroundBlurMode.BLUR];
+ }
+
+ options() {
+ return this.settings_;
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ mockImageCapture: null
+ }
+
+ class ImageCaptureTestChromium {
+
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ initialize() {
+ if (testInternal.initialized)
+ throw new Error('Call reset() before initialize().');
+
+ testInternal.mockImageCapture = new MockImageCapture;
+ testInternal.initialized = true;
+ }
+ // Resets state of image capture mocks between test runs.
+ async reset() {
+ if (!testInternal.initialized)
+ throw new Error('Call initialize() before reset().');
+ testInternal.mockImageCapture.reset();
+ testInternal.mockImageCapture = null;
+ testInternal.initialized = false;
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+ mockImageCapture() {
+ return testInternal.mockImageCapture;
+ }
+ }
+
+ return ImageCaptureTestChromium;
+})();
diff --git a/test/wpt/tests/resources/chromium/mock-managed-config.js b/test/wpt/tests/resources/chromium/mock-managed-config.js
new file mode 100644
index 0000000..c9980e1
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-managed-config.js
@@ -0,0 +1,91 @@
+'use strict'
+
+import{ManagedConfigurationObserverRemote, ManagedConfigurationService, ManagedConfigurationServiceReceiver} from '/gen/third_party/blink/public/mojom/device/device.mojom.m.js';
+
+
+self.ManagedConfigTest = (() => {
+ // Class that mocks ManagedConfigurationService interface defined in
+ // https://source.chromium.org/chromium/chromium/src/third_party/blink/public/mojom/device/device.mojom
+ class MockManagedConfig {
+ constructor() {
+ this.receiver_ = new ManagedConfigurationServiceReceiver(this);
+ this.interceptor_ = new MojoInterfaceInterceptor(
+ ManagedConfigurationService.$interfaceName);
+ this.interceptor_.oninterfacerequest = e =>
+ this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+ this.subscription_ = null;
+ this.reset();
+ }
+
+ reset() {
+ this.configuration_ = null;
+ this.onObserverAdd_ = null;
+ }
+
+ async getManagedConfiguration(keys) {
+ if (this.configuration_ === null) {
+ return {};
+ }
+
+ return {
+ configurations: Object.keys(this.configuration_)
+ .filter(key => keys.includes(key))
+ .reduce(
+ (obj, key) => {
+ obj[key] =
+ JSON.stringify(this.configuration_[key]);
+ return obj;
+ },
+ {})
+ };
+ }
+
+ subscribeToManagedConfiguration(remote) {
+ this.subscription_ = remote;
+ if (this.onObserverAdd_ !== null) {
+ this.onObserverAdd_();
+ }
+ }
+
+ setManagedConfig(value) {
+ this.configuration_ = value;
+ if (this.subscription_ !== null) {
+ this.subscription_.onConfigurationChanged();
+ }
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ mockManagedConfig: null
+ }
+
+ class ManagedConfigTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ initialize() {
+ if (testInternal.mockManagedConfig !== null) {
+ testInternal.mockManagedConfig.reset();
+ return;
+ }
+
+ testInternal.mockManagedConfig = new MockManagedConfig;
+ testInternal.initialized = true;
+ }
+
+ setManagedConfig(config) {
+ testInternal.mockManagedConfig.setManagedConfig(config);
+ }
+
+ async nextObserverAdded() {
+ await new Promise(resolve => {
+ testInternal.mockManagedConfig.onObserverAdd_ = resolve;
+ });
+ }
+ }
+
+ return ManagedConfigTestChromium;
+})();
diff --git a/test/wpt/tests/resources/chromium/mock-pressure-service.js b/test/wpt/tests/resources/chromium/mock-pressure-service.js
new file mode 100644
index 0000000..02d10f8
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-pressure-service.js
@@ -0,0 +1,134 @@
+import {PressureManager, PressureManagerReceiver, PressureStatus} from '/gen/services/device/public/mojom/pressure_manager.mojom.m.js'
+import {PressureSource, PressureState} from '/gen/services/device/public/mojom/pressure_update.mojom.m.js'
+
+class MockPressureService {
+ constructor() {
+ this.receiver_ = new PressureManagerReceiver(this);
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(PressureManager.$interfaceName);
+ this.interceptor_.oninterfacerequest = e => {
+ this.receiver_.$.bindHandle(e.handle);
+ };
+ this.reset();
+ this.mojomSourceType_ = new Map([['cpu', PressureSource.kCpu]]);
+ this.mojomStateType_ = new Map([
+ ['nominal', PressureState.kNominal], ['fair', PressureState.kFair],
+ ['serious', PressureState.kSerious], ['critical', PressureState.kCritical]
+ ]);
+ this.pressureServiceReadingTimerId_ = null;
+ }
+
+ start() {
+ this.interceptor_.start();
+ }
+
+ stop() {
+ this.stopPlatformCollector();
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+
+ // Wait for an event loop iteration to let any pending mojo commands in
+ // the pressure service finish.
+ return new Promise(resolve => setTimeout(resolve, 0));
+ }
+
+ reset() {
+ this.observers_ = [];
+ this.pressureUpdate_ = null;
+ this.pressureServiceReadingTimerId_ = null;
+ this.pressureStatus_ = PressureStatus.kOk;
+ this.updatesDelivered_ = 0;
+ }
+
+ async addClient(observer, source) {
+ if (this.observers_.indexOf(observer) >= 0)
+ throw new Error('addClient() has already been called');
+
+ // TODO(crbug.com/1342184): Consider other sources.
+ // For now, "cpu" is the only source.
+ if (source !== PressureSource.kCpu)
+ throw new Error('Call addClient() with a wrong PressureSource');
+
+ observer.onConnectionError.addListener(() => {
+ // Remove this observer from observer array.
+ this.observers_.splice(this.observers_.indexOf(observer), 1);
+ });
+ this.observers_.push(observer);
+
+ return {status: this.pressureStatus_};
+ }
+
+ startPlatformCollector(sampleRate) {
+ if (sampleRate === 0)
+ return;
+
+ if (this.pressureServiceReadingTimerId_ != null)
+ this.stopPlatformCollector();
+
+ // The following code for calculating the timestamp was taken from
+ // https://source.chromium.org/chromium/chromium/src/+/main:third_party/
+ // blink/web_tests/http/tests/resources/
+ // geolocation-mock.js;l=131;drc=37a9b6c03b9bda9fcd62fc0e5e8016c278abd31f
+
+ // The new Date().getTime() returns the number of milliseconds since the
+ // UNIX epoch (1970-01-01 00::00:00 UTC), while |internalValue| of the
+ // device.mojom.PressureUpdate represents the value of microseconds since
+ // the Windows FILETIME epoch (1601-01-01 00:00:00 UTC). So add the delta
+ // when sets the |internalValue|. See more info in //base/time/time.h.
+ const windowsEpoch = Date.UTC(1601, 0, 1, 0, 0, 0, 0);
+ const unixEpoch = Date.UTC(1970, 0, 1, 0, 0, 0, 0);
+ // |epochDeltaInMs| equals to base::Time::kTimeTToMicrosecondsOffset.
+ const epochDeltaInMs = unixEpoch - windowsEpoch;
+
+ const timeout = (1 / sampleRate) * 1000;
+ this.pressureServiceReadingTimerId_ = self.setInterval(() => {
+ if (this.pressureUpdate_ === null || this.observers_.length === 0)
+ return;
+ this.pressureUpdate_.timestamp = {
+ internalValue: BigInt((new Date().getTime() + epochDeltaInMs) * 1000)
+ };
+ for (let observer of this.observers_)
+ observer.onPressureUpdated(this.pressureUpdate_);
+ this.updatesDelivered_++;
+ }, timeout);
+ }
+
+ stopPlatformCollector() {
+ if (this.pressureServiceReadingTimerId_ != null) {
+ self.clearInterval(this.pressureServiceReadingTimerId_);
+ this.pressureServiceReadingTimerId_ = null;
+ }
+ this.updatesDelivered_ = 0;
+ }
+
+ updatesDelivered() {
+ return this.updatesDelivered_;
+ }
+
+ setPressureUpdate(source, state) {
+ if (!this.mojomSourceType_.has(source))
+ throw new Error(`PressureSource '${source}' is invalid`);
+
+ if (!this.mojomStateType_.has(state))
+ throw new Error(`PressureState '${state}' is invalid`);
+
+ this.pressureUpdate_ = {
+ source: this.mojomSourceType_.get(source),
+ state: this.mojomStateType_.get(state),
+ };
+ }
+
+ setExpectedFailure(expectedException) {
+ assert_true(
+ expectedException instanceof DOMException,
+ 'setExpectedFailure() expects a DOMException instance');
+ if (expectedException.name === 'NotSupportedError') {
+ this.pressureStatus_ = PressureStatus.kNotSupported;
+ } else {
+ throw new TypeError(
+ `Unexpected DOMException '${expectedException.name}'`);
+ }
+ }
+}
+
+export const mockPressureService = new MockPressureService();
diff --git a/test/wpt/tests/resources/chromium/mock-pressure-service.js.headers b/test/wpt/tests/resources/chromium/mock-pressure-service.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-pressure-service.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/resources/chromium/mock-subapps.js b/test/wpt/tests/resources/chromium/mock-subapps.js
new file mode 100644
index 0000000..b819367
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-subapps.js
@@ -0,0 +1,89 @@
+'use strict';
+
+import {SubAppsService, SubAppsServiceReceiver, SubAppsServiceResultCode} from '/gen/third_party/blink/public/mojom/subapps/sub_apps_service.mojom.m.js';
+
+self.SubAppsServiceTest = (() => {
+ // Class that mocks SubAppsService interface defined in /third_party/blink/public/mojom/subapps/sub_apps_service.mojom
+
+ class MockSubAppsService {
+ constructor() {
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(SubAppsService.$interfaceName);
+ this.receiver_ = new SubAppsServiceReceiver(this);
+ this.interceptor_.oninterfacerequest =
+ e => this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+ }
+
+ reset() {
+ this.interceptor_.stop();
+ this.receiver_.$.close();
+ }
+
+ add(sub_apps) {
+ return Promise.resolve({
+ result: testInternal.addCallReturnValue,
+ });
+ }
+
+ list() {
+ return Promise.resolve({
+ result: {
+ resultCode: testInternal.serviceResultCode,
+ subAppsList: testInternal.listCallReturnValue,
+ }
+ });
+ }
+
+ remove() {
+ return Promise.resolve({
+ result: testInternal.removeCallReturnValue,
+ });
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ mockSubAppsService: null,
+ serviceResultCode: 0,
+ addCallReturnValue: [],
+ listCallReturnValue: [],
+ removeCallReturnValue: [],
+ }
+
+ class SubAppsServiceTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ initialize(service_result_code, add_call_return_value, list_call_return_value, remove_call_return_value) {
+ if (!testInternal.initialized) {
+ testInternal = {
+ mockSubAppsService: new MockSubAppsService(),
+ initialized: true,
+ serviceResultCode: service_result_code,
+ addCallReturnValue: add_call_return_value,
+ listCallReturnValue: list_call_return_value,
+ removeCallReturnValue: remove_call_return_value,
+ };
+ };
+ }
+
+ async reset() {
+ if (testInternal.initialized) {
+ testInternal.mockSubAppsService.reset();
+ testInternal = {
+ mockSubAppsService: null,
+ initialized: false,
+ serviceResultCode: 0,
+ addCallReturnValue: [],
+ listCallReturnValue: [],
+ removeCallReturnValue: [],
+ };
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+ }
+ }
+
+ return SubAppsServiceTestChromium;
+})();
diff --git a/test/wpt/tests/resources/chromium/mock-textdetection.js b/test/wpt/tests/resources/chromium/mock-textdetection.js
new file mode 100644
index 0000000..52ca987
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-textdetection.js
@@ -0,0 +1,92 @@
+import {TextDetection, TextDetectionReceiver} from '/gen/services/shape_detection/public/mojom/textdetection.mojom.m.js';
+
+self.TextDetectionTest = (() => {
+ // Class that mocks TextDetection interface defined in
+ // https://cs.chromium.org/chromium/src/services/shape_detection/public/mojom/textdetection.mojom
+ class MockTextDetection {
+ constructor() {
+ this.receiver_ = new TextDetectionReceiver(this);
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(TextDetection.$interfaceName);
+ this.interceptor_.oninterfacerequest =
+ e => this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+ }
+
+ detect(bitmapData) {
+ this.bufferData_ =
+ new Uint32Array(getArrayBufferFromBigBuffer(bitmapData.pixelData));
+ return Promise.resolve({
+ results: [
+ {
+ rawValue : "cats",
+ boundingBox: { x: 1.0, y: 1.0, width: 100.0, height: 100.0 },
+ cornerPoints: [
+ { x: 1.0, y: 1.0 },
+ { x: 101.0, y: 1.0 },
+ { x: 101.0, y: 101.0 },
+ { x: 1.0, y: 101.0 }
+ ]
+ },
+ {
+ rawValue : "dogs",
+ boundingBox: { x: 2.0, y: 2.0, width: 50.0, height: 50.0 },
+ cornerPoints: [
+ { x: 2.0, y: 2.0 },
+ { x: 52.0, y: 2.0 },
+ { x: 52.0, y: 52.0 },
+ { x: 2.0, y: 52.0 }
+ ]
+ },
+ ],
+ });
+ }
+
+ getFrameData() {
+ return this.bufferData_;
+ }
+
+ reset() {
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+ }
+
+ }
+
+ let testInternal = {
+ initialized: false,
+ MockTextDetection: null
+ }
+
+ class TextDetectionTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ initialize() {
+ if (testInternal.initialized)
+ throw new Error('Call reset() before initialize().');
+
+ testInternal.MockTextDetection = new MockTextDetection;
+ testInternal.initialized = true;
+ }
+
+ // Resets state of text detection mocks between test runs.
+ async reset() {
+ if (!testInternal.initialized)
+ throw new Error('Call initialize() before reset().');
+ testInternal.MockTextDetection.reset();
+ testInternal.MockTextDetection = null;
+ testInternal.initialized = false;
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+
+ MockTextDetection() {
+ return testInternal.MockTextDetection;
+ }
+ }
+
+ return TextDetectionTestChromium;
+
+})();
diff --git a/test/wpt/tests/resources/chromium/mock-textdetection.js.headers b/test/wpt/tests/resources/chromium/mock-textdetection.js.headers
new file mode 100644
index 0000000..6c61a34
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/mock-textdetection.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8 \ No newline at end of file
diff --git a/test/wpt/tests/resources/chromium/nfc-mock.js b/test/wpt/tests/resources/chromium/nfc-mock.js
new file mode 100644
index 0000000..31a71b9
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/nfc-mock.js
@@ -0,0 +1,437 @@
+import {NDEFErrorType, NDEFRecordTypeCategory, NFC, NFCReceiver} from '/gen/services/device/public/mojom/nfc.mojom.m.js';
+
+// Converts between NDEFMessageInit https://w3c.github.io/web-nfc/#dom-ndefmessage
+// and mojom.NDEFMessage structure, so that watch function can be tested.
+function toMojoNDEFMessage(message) {
+ let ndefMessage = {data: []};
+ for (let record of message.records) {
+ ndefMessage.data.push(toMojoNDEFRecord(record));
+ }
+ return ndefMessage;
+}
+
+function toMojoNDEFRecord(record) {
+ let nfcRecord = {};
+ // Simply checks the existence of ':' to decide whether it's an external
+ // type or a local type. As a mock, no need to really implement the validation
+ // algorithms for them.
+ if (record.recordType.startsWith(':')) {
+ nfcRecord.category = NDEFRecordTypeCategory.kLocal;
+ } else if (record.recordType.search(':') != -1) {
+ nfcRecord.category = NDEFRecordTypeCategory.kExternal;
+ } else {
+ nfcRecord.category = NDEFRecordTypeCategory.kStandardized;
+ }
+ nfcRecord.recordType = record.recordType;
+ nfcRecord.mediaType = record.mediaType;
+ nfcRecord.id = record.id;
+ if (record.recordType == 'text') {
+ nfcRecord.encoding = record.encoding == null? 'utf-8': record.encoding;
+ nfcRecord.lang = record.lang == null? 'en': record.lang;
+ }
+ nfcRecord.data = toByteArray(record.data);
+ if (record.data != null && record.data.records !== undefined) {
+ // |record.data| may be an NDEFMessageInit, i.e. the payload is a message.
+ nfcRecord.payloadMessage = toMojoNDEFMessage(record.data);
+ }
+ return nfcRecord;
+}
+
+// Converts JS objects to byte array.
+function toByteArray(data) {
+ if (data instanceof ArrayBuffer)
+ return new Uint8Array(data);
+ else if (ArrayBuffer.isView(data))
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
+
+ let byteArray = new Uint8Array(0);
+ let tmpData = data;
+ if (typeof tmpData === 'object' || typeof tmpData === 'number')
+ tmpData = JSON.stringify(tmpData);
+
+ if (typeof tmpData === 'string')
+ byteArray = new TextEncoder().encode(tmpData);
+
+ return byteArray;
+}
+
+// Compares NDEFRecords that were provided / received by the mock service.
+// TODO: Use different getters to get received record data,
+// see spec changes at https://github.com/w3c/web-nfc/pull/243.
+self.compareNDEFRecords = function(providedRecord, receivedRecord) {
+ assert_equals(providedRecord.recordType, receivedRecord.recordType);
+
+ if (providedRecord.id === undefined) {
+ assert_equals(null, receivedRecord.id);
+ } else {
+ assert_equals(providedRecord.id, receivedRecord.id);
+ }
+
+ if (providedRecord.mediaType === undefined) {
+ assert_equals(null, receivedRecord.mediaType);
+ } else {
+ assert_equals(providedRecord.mediaType, receivedRecord.mediaType);
+ }
+
+ assert_not_equals(providedRecord.recordType, 'empty');
+
+ if (providedRecord.recordType == 'text') {
+ assert_equals(
+ providedRecord.encoding == null? 'utf-8': providedRecord.encoding,
+ receivedRecord.encoding);
+ assert_equals(providedRecord.lang == null? 'en': providedRecord.lang,
+ receivedRecord.lang);
+ } else {
+ assert_equals(null, receivedRecord.encoding);
+ assert_equals(null, receivedRecord.lang);
+ }
+
+ assert_array_equals(toByteArray(providedRecord.data),
+ new Uint8Array(receivedRecord.data));
+}
+
+// Compares NDEFWriteOptions structures that were provided to API and
+// received by the mock mojo service.
+self.assertNDEFWriteOptionsEqual = function(provided, received) {
+ if (provided.overwrite !== undefined)
+ assert_equals(provided.overwrite, !!received.overwrite);
+ else
+ assert_equals(!!received.overwrite, true);
+}
+
+// Compares NDEFReaderOptions structures that were provided to API and
+// received by the mock mojo service.
+self.assertNDEFReaderOptionsEqual = function(provided, received) {
+ if (provided.url !== undefined)
+ assert_equals(provided.url, received.url);
+ else
+ assert_equals(received.url, '');
+
+ if (provided.mediaType !== undefined)
+ assert_equals(provided.mediaType, received.mediaType);
+ else
+ assert_equals(received.mediaType, '');
+
+ if (provided.recordType !== undefined) {
+ assert_equals(provided.recordType, received.recordType);
+ }
+}
+
+function createNDEFError(type) {
+ return {error: (type != null ? {errorType: type, errorMessage: ''} : null)};
+}
+
+self.WebNFCTest = (() => {
+ class MockNFC {
+ constructor() {
+ this.receiver_ = new NFCReceiver(this);
+
+ this.interceptor_ = new MojoInterfaceInterceptor(NFC.$interfaceName);
+ this.interceptor_.oninterfacerequest = e => {
+ if (this.should_close_pipe_on_request_)
+ e.handle.close();
+ else
+ this.receiver_.$.bindHandle(e.handle);
+ }
+
+ this.interceptor_.start();
+
+ this.hw_status_ = NFCHWStatus.ENABLED;
+ this.pushed_message_ = null;
+ this.pending_write_options_ = null;
+ this.pending_push_promise_func_ = null;
+ this.push_completed_ = true;
+ this.pending_make_read_only_promise_func_ = null;
+ this.make_read_only_completed_ = true;
+ this.client_ = null;
+ this.watchers_ = [];
+ this.reading_messages_ = [];
+ this.operations_suspended_ = false;
+ this.is_formatted_tag_ = false;
+ this.data_transfer_failed_ = false;
+ this.should_close_pipe_on_request_ = false;
+ }
+
+ // NFC delegate functions.
+ async push(message, options) {
+ const error = this.getHWError();
+ if (error)
+ return error;
+ // Cancels previous pending push operation.
+ if (this.pending_push_promise_func_) {
+ this.cancelPendingPushOperation();
+ }
+
+ this.pushed_message_ = message;
+ this.pending_write_options_ = options;
+ return new Promise(resolve => {
+ if (this.operations_suspended_ || !this.push_completed_) {
+ // Leaves the push pending.
+ this.pending_push_promise_func_ = resolve;
+ } else if (this.is_formatted_tag_ && !options.overwrite) {
+ // Resolves with NotAllowedError if there are NDEF records on the device
+ // and overwrite is false.
+ resolve(createNDEFError(NDEFErrorType.NOT_ALLOWED));
+ } else if (this.data_transfer_failed_) {
+ // Resolves with NetworkError if data transfer fails.
+ resolve(createNDEFError(NDEFErrorType.IO_ERROR));
+ } else {
+ resolve(createNDEFError(null));
+ }
+ });
+ }
+
+ async cancelPush() {
+ this.cancelPendingPushOperation();
+ return createNDEFError(null);
+ }
+
+ async makeReadOnly(options) {
+ const error = this.getHWError();
+ if (error)
+ return error;
+ // Cancels previous pending makeReadOnly operation.
+ if (this.pending_make_read_only_promise_func_) {
+ this.cancelPendingMakeReadOnlyOperation();
+ }
+
+ if (this.operations_suspended_ || !this.make_read_only_completed_) {
+ // Leaves the makeReadOnly pending.
+ return new Promise(resolve => {
+ this.pending_make_read_only_promise_func_ = resolve;
+ });
+ } else if (this.data_transfer_failed_) {
+ // Resolves with NetworkError if data transfer fails.
+ return createNDEFError(NDEFErrorType.IO_ERROR);
+ } else {
+ return createNDEFError(null);
+ }
+ }
+
+ async cancelMakeReadOnly() {
+ this.cancelPendingMakeReadOnlyOperation();
+ return createNDEFError(null);
+ }
+
+ setClient(client) {
+ this.client_ = client;
+ }
+
+ async watch(id) {
+ assert_true(id > 0);
+ const error = this.getHWError();
+ if (error) {
+ return error;
+ }
+
+ this.watchers_.push({id: id});
+ // Ignores reading if NFC operation is suspended
+ // or the NFC tag does not expose NDEF technology.
+ if (!this.operations_suspended_) {
+ // Triggers onWatch if the new watcher matches existing messages.
+ for (let message of this.reading_messages_) {
+ this.client_.onWatch(
+ [id], fake_tag_serial_number, toMojoNDEFMessage(message));
+ }
+ }
+
+ return createNDEFError(null);
+ }
+
+ cancelWatch(id) {
+ let index = this.watchers_.findIndex(value => value.id === id);
+ if (index !== -1) {
+ this.watchers_.splice(index, 1);
+ }
+ }
+
+ getHWError() {
+ if (this.hw_status_ === NFCHWStatus.DISABLED)
+ return createNDEFError(NDEFErrorType.NOT_READABLE);
+ if (this.hw_status_ === NFCHWStatus.NOT_SUPPORTED)
+ return createNDEFError(NDEFErrorType.NOT_SUPPORTED);
+ return null;
+ }
+
+ setHWStatus(status) {
+ this.hw_status_ = status;
+ }
+
+ pushedMessage() {
+ return this.pushed_message_;
+ }
+
+ writeOptions() {
+ return this.pending_write_options_;
+ }
+
+ watchOptions() {
+ assert_not_equals(this.watchers_.length, 0);
+ return this.watchers_[this.watchers_.length - 1].options;
+ }
+
+ setPendingPushCompleted(result) {
+ this.push_completed_ = result;
+ }
+
+ setPendingMakeReadOnlyCompleted(result) {
+ this.make_read_only_completed_ = result;
+ }
+
+ reset() {
+ this.hw_status_ = NFCHWStatus.ENABLED;
+ this.watchers_ = [];
+ this.reading_messages_ = [];
+ this.operations_suspended_ = false;
+ this.cancelPendingPushOperation();
+ this.cancelPendingMakeReadOnlyOperation();
+ this.is_formatted_tag_ = false;
+ this.data_transfer_failed_ = false;
+ this.should_close_pipe_on_request_ = false;
+ }
+
+ cancelPendingPushOperation() {
+ if (this.pending_push_promise_func_) {
+ this.pending_push_promise_func_(
+ createNDEFError(NDEFErrorType.OPERATION_CANCELLED));
+ this.pending_push_promise_func_ = null;
+ }
+
+ this.pushed_message_ = null;
+ this.pending_write_options_ = null;
+ this.push_completed_ = true;
+ }
+
+ cancelPendingMakeReadOnlyOperation() {
+ if (this.pending_make_read_only_promise_func_) {
+ this.pending_make_read_only_promise_func_(
+ createNDEFError(NDEFErrorType.OPERATION_CANCELLED));
+ this.pending_make_read_only_promise_func_ = null;
+ }
+
+ this.make_read_only_completed_ = true;
+ }
+
+ // Sets message that is used to deliver NFC reading updates.
+ setReadingMessage(message) {
+ this.reading_messages_.push(message);
+ // Ignores reading if NFC operation is suspended.
+ if(this.operations_suspended_) return;
+ // when overwrite is false, the write algorithm will read the NFC tag
+ // to determine if it has NDEF records on it.
+ if (this.pending_write_options_ && this.pending_write_options_.overwrite)
+ return;
+ // Triggers onWatch if the new message matches existing watchers.
+ for (let watcher of this.watchers_) {
+ this.client_.onWatch(
+ [watcher.id], fake_tag_serial_number,
+ toMojoNDEFMessage(message));
+ }
+ }
+
+ // Suspends all pending NFC operations. Could be used when web page
+ // visibility is lost.
+ suspendNFCOperations() {
+ this.operations_suspended_ = true;
+ }
+
+ // Resumes all suspended NFC operations.
+ resumeNFCOperations() {
+ this.operations_suspended_ = false;
+ // Resumes pending NFC reading.
+ for (let watcher of this.watchers_) {
+ for (let message of this.reading_messages_) {
+ this.client_.onWatch(
+ [watcher.id], fake_tag_serial_number,
+ toMojoNDEFMessage(message));
+ }
+ }
+ // Resumes pending push operation.
+ if (this.pending_push_promise_func_ && this.push_completed_) {
+ this.pending_push_promise_func_(createNDEFError(null));
+ this.pending_push_promise_func_ = null;
+ }
+ // Resumes pending makeReadOnly operation.
+ if (this.pending_make_read_only_promise_func_ &&
+ this.make_read_only_completed_) {
+ this.pending_make_read_only_promise_func_(createNDEFError(null));
+ this.pending_make_read_only_promise_func_ = null;
+ }
+ }
+
+ // Simulates the device coming in proximity does not expose NDEF technology.
+ simulateNonNDEFTagDiscovered() {
+ // Notify NotSupportedError to all active readers.
+ if (this.watchers_.length != 0) {
+ this.client_.onError({
+ errorType: NDEFErrorType.NOT_SUPPORTED,
+ errorMessage: ''
+ });
+ }
+ // Reject the pending push with NotSupportedError.
+ if (this.pending_push_promise_func_) {
+ this.pending_push_promise_func_(
+ createNDEFError(NDEFErrorType.NOT_SUPPORTED));
+ this.pending_push_promise_func_ = null;
+ }
+ // Reject the pending makeReadOnly with NotSupportedError.
+ if (this.pending_make_read_only_promise_func_) {
+ this.pending_make_read_only_promise_func_(
+ createNDEFError(NDEFErrorType.NOT_SUPPORTED));
+ this.pending_make_read_only_promise_func_ = null;
+ }
+ }
+
+ setIsFormattedTag(isFormatted) {
+ this.is_formatted_tag_ = isFormatted;
+ }
+
+ simulateDataTransferFails() {
+ this.data_transfer_failed_ = true;
+ }
+
+ simulateClosedPipe() {
+ this.should_close_pipe_on_request_ = true;
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ mockNFC: null
+ }
+
+ class NFCTestChromium {
+ constructor() {
+ Object.freeze(this); // Makes it immutable.
+ }
+
+ async initialize() {
+ if (testInternal.initialized)
+ throw new Error('Call reset() before initialize().');
+
+ // Grant nfc permissions for Chromium testdriver.
+ await test_driver.set_permission({ name: 'nfc' }, 'granted');
+
+ if (testInternal.mockNFC == null) {
+ testInternal.mockNFC = new MockNFC();
+ }
+ testInternal.initialized = true;
+ }
+
+ // Reuses the nfc mock but resets its state between test runs.
+ async reset() {
+ if (!testInternal.initialized)
+ throw new Error('Call initialize() before reset().');
+ testInternal.mockNFC.reset();
+ testInternal.initialized = false;
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+
+ getMockNFC() {
+ return testInternal.mockNFC;
+ }
+ }
+
+ return NFCTestChromium;
+})();
diff --git a/test/wpt/tests/resources/chromium/web-bluetooth-test.js b/test/wpt/tests/resources/chromium/web-bluetooth-test.js
new file mode 100644
index 0000000..ecea5e7
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/web-bluetooth-test.js
@@ -0,0 +1,629 @@
+'use strict';
+
+const content = {};
+const bluetooth = {};
+const MOJO_CHOOSER_EVENT_TYPE_MAP = {};
+
+function toMojoCentralState(state) {
+ switch (state) {
+ case 'absent':
+ return bluetooth.mojom.CentralState.ABSENT;
+ case 'powered-off':
+ return bluetooth.mojom.CentralState.POWERED_OFF;
+ case 'powered-on':
+ return bluetooth.mojom.CentralState.POWERED_ON;
+ default:
+ throw `Unsupported value ${state} for state.`;
+ }
+}
+
+// Converts bluetooth.mojom.WriteType to a string. If |writeType| is
+// invalid, this method will throw.
+function writeTypeToString(writeType) {
+ switch (writeType) {
+ case bluetooth.mojom.WriteType.kNone:
+ return 'none';
+ case bluetooth.mojom.WriteType.kWriteDefaultDeprecated:
+ return 'default-deprecated';
+ case bluetooth.mojom.WriteType.kWriteWithResponse:
+ return 'with-response';
+ case bluetooth.mojom.WriteType.kWriteWithoutResponse:
+ return 'without-response';
+ default:
+ throw `Unknown bluetooth.mojom.WriteType: ${writeType}`;
+ }
+}
+
+// Canonicalizes UUIDs and converts them to Mojo UUIDs.
+function canonicalizeAndConvertToMojoUUID(uuids) {
+ let canonicalUUIDs = uuids.map(val => ({uuid: BluetoothUUID.getService(val)}));
+ return canonicalUUIDs;
+}
+
+// Converts WebIDL a record<DOMString, BufferSource> to a map<K, array<uint8>> to
+// use for Mojo, where the value for K is calculated using keyFn.
+function convertToMojoMap(record, keyFn, isNumberKey = false) {
+ let map = new Map();
+ for (const [key, value] of Object.entries(record)) {
+ let buffer = ArrayBuffer.isView(value) ? value.buffer : value;
+ if (isNumberKey) {
+ let numberKey = parseInt(key);
+ if (Number.isNaN(numberKey))
+ throw `Map key ${key} is not a number`;
+ map.set(keyFn(numberKey), Array.from(new Uint8Array(buffer)));
+ continue;
+ }
+ map.set(keyFn(key), Array.from(new Uint8Array(buffer)));
+ }
+ return map;
+}
+
+function ArrayToMojoCharacteristicProperties(arr) {
+ const struct = {};
+ arr.forEach(property => { struct[property] = true; });
+ return struct;
+}
+
+class FakeBluetooth {
+ constructor() {
+ this.fake_bluetooth_ptr_ = new bluetooth.mojom.FakeBluetoothRemote();
+ this.fake_bluetooth_ptr_.$.bindNewPipeAndPassReceiver().bindInBrowser('process');
+ this.fake_central_ = null;
+ }
+
+ // Set it to indicate whether the platform supports BLE. For example,
+ // Windows 7 is a platform that doesn't support Low Energy. On the other
+ // hand Windows 10 is a platform that does support LE, even if there is no
+ // Bluetooth radio present.
+ async setLESupported(supported) {
+ if (typeof supported !== 'boolean') throw 'Type Not Supported';
+ await this.fake_bluetooth_ptr_.setLESupported(supported);
+ }
+
+ // Returns a promise that resolves with a FakeCentral that clients can use
+ // to simulate events that a device in the Central/Observer role would
+ // receive as well as monitor the operations performed by the device in the
+ // Central/Observer role.
+ // Calls sets LE as supported.
+ //
+ // A "Central" object would allow its clients to receive advertising events
+ // and initiate connections to peripherals i.e. operations of two roles
+ // defined by the Bluetooth Spec: Observer and Central.
+ // See Bluetooth 4.2 Vol 3 Part C 2.2.2 "Roles when Operating over an
+ // LE Physical Transport".
+ async simulateCentral({state}) {
+ if (this.fake_central_)
+ throw 'simulateCentral() should only be called once';
+
+ await this.setLESupported(true);
+
+ let {fakeCentral: fake_central_ptr} =
+ await this.fake_bluetooth_ptr_.simulateCentral(
+ toMojoCentralState(state));
+ this.fake_central_ = new FakeCentral(fake_central_ptr);
+ return this.fake_central_;
+ }
+
+ // Returns true if there are no pending responses.
+ async allResponsesConsumed() {
+ let {consumed} = await this.fake_bluetooth_ptr_.allResponsesConsumed();
+ return consumed;
+ }
+
+ // Returns a promise that resolves with a FakeChooser that clients can use to
+ // simulate chooser events.
+ async getManualChooser() {
+ if (typeof this.fake_chooser_ === 'undefined') {
+ this.fake_chooser_ = new FakeChooser();
+ }
+ return this.fake_chooser_;
+ }
+}
+
+// FakeCentral allows clients to simulate events that a device in the
+// Central/Observer role would receive as well as monitor the operations
+// performed by the device in the Central/Observer role.
+class FakeCentral {
+ constructor(fake_central_ptr) {
+ this.fake_central_ptr_ = fake_central_ptr;
+ this.peripherals_ = new Map();
+ }
+
+ // Simulates a peripheral with |address|, |name|, |manufacturerData| and
+ // |known_service_uuids| that has already been connected to the system. If the
+ // peripheral existed already it updates its name, manufacturer data, and
+ // known UUIDs. |known_service_uuids| should be an array of
+ // BluetoothServiceUUIDs
+ // https://webbluetoothcg.github.io/web-bluetooth/#typedefdef-bluetoothserviceuuid
+ //
+ // Platforms offer methods to retrieve devices that have already been
+ // connected to the system or weren't connected through the UA e.g. a user
+ // connected a peripheral through the system's settings. This method is
+ // intended to simulate peripherals that those methods would return.
+ async simulatePreconnectedPeripheral(
+ {address, name, manufacturerData = {}, knownServiceUUIDs = []}) {
+ await this.fake_central_ptr_.simulatePreconnectedPeripheral(
+ address, name,
+ convertToMojoMap(manufacturerData, Number, true /* isNumberKey */),
+ canonicalizeAndConvertToMojoUUID(knownServiceUUIDs));
+
+ return this.fetchOrCreatePeripheral_(address);
+ }
+
+ // Simulates an advertisement packet described by |scanResult| being received
+ // from a device. If central is currently scanning, the device will appear on
+ // the list of discovered devices.
+ async simulateAdvertisementReceived(scanResult) {
+ // Create a deep-copy to prevent the original |scanResult| from being
+ // modified when the UUIDs, manufacturer, and service data are converted.
+ let clonedScanResult = JSON.parse(JSON.stringify(scanResult));
+
+ if ('uuids' in scanResult.scanRecord) {
+ clonedScanResult.scanRecord.uuids =
+ canonicalizeAndConvertToMojoUUID(scanResult.scanRecord.uuids);
+ }
+
+ // Convert the optional appearance and txPower fields to the corresponding
+ // Mojo structures, since Mojo does not support optional interger values. If
+ // the fields are undefined, set the hasValue field as false and value as 0.
+ // Otherwise, set the hasValue field as true and value with the field value.
+ const has_appearance = 'appearance' in scanResult.scanRecord;
+ clonedScanResult.scanRecord.appearance = {
+ hasValue: has_appearance,
+ value: (has_appearance ? scanResult.scanRecord.appearance : 0)
+ }
+
+ const has_tx_power = 'txPower' in scanResult.scanRecord;
+ clonedScanResult.scanRecord.txPower = {
+ hasValue: has_tx_power,
+ value: (has_tx_power ? scanResult.scanRecord.txPower : 0)
+ }
+
+ // Convert manufacturerData from a record<DOMString, BufferSource> into a
+ // map<uint8, array<uint8>> for Mojo.
+ if ('manufacturerData' in scanResult.scanRecord) {
+ clonedScanResult.scanRecord.manufacturerData = convertToMojoMap(
+ scanResult.scanRecord.manufacturerData, Number,
+ true /* isNumberKey */);
+ }
+
+ // Convert serviceData from a record<DOMString, BufferSource> into a
+ // map<string, array<uint8>> for Mojo.
+ if ('serviceData' in scanResult.scanRecord) {
+ clonedScanResult.scanRecord.serviceData.serviceData = convertToMojoMap(
+ scanResult.scanRecord.serviceData, BluetoothUUID.getService,
+ false /* isNumberKey */);
+ }
+
+ await this.fake_central_ptr_.simulateAdvertisementReceived(
+ clonedScanResult);
+
+ return this.fetchOrCreatePeripheral_(clonedScanResult.deviceAddress);
+ }
+
+ // Simulates a change in the central device described by |state|. For example,
+ // setState('powered-off') can be used to simulate the central device powering
+ // off.
+ //
+ // This method should be used for any central state changes after
+ // simulateCentral() has been called to create a FakeCentral object.
+ async setState(state) {
+ await this.fake_central_ptr_.setState(toMojoCentralState(state));
+ }
+
+ // Create a fake_peripheral object from the given address.
+ fetchOrCreatePeripheral_(address) {
+ let peripheral = this.peripherals_.get(address);
+ if (peripheral === undefined) {
+ peripheral = new FakePeripheral(address, this.fake_central_ptr_);
+ this.peripherals_.set(address, peripheral);
+ }
+ return peripheral;
+ }
+}
+
+class FakePeripheral {
+ constructor(address, fake_central_ptr) {
+ this.address = address;
+ this.fake_central_ptr_ = fake_central_ptr;
+ }
+
+ // Adds a fake GATT Service with |uuid| to be discovered when discovering
+ // the peripheral's GATT Attributes. Returns a FakeRemoteGATTService
+ // corresponding to this service. |uuid| should be a BluetoothServiceUUIDs
+ // https://webbluetoothcg.github.io/web-bluetooth/#typedefdef-bluetoothserviceuuid
+ async addFakeService({uuid}) {
+ let {serviceId: service_id} = await this.fake_central_ptr_.addFakeService(
+ this.address, {uuid: BluetoothUUID.getService(uuid)});
+
+ if (service_id === null) throw 'addFakeService failed';
+
+ return new FakeRemoteGATTService(
+ service_id, this.address, this.fake_central_ptr_);
+ }
+
+ // Sets the next GATT Connection request response to |code|. |code| could be
+ // an HCI Error Code from BT 4.2 Vol 2 Part D 1.3 List Of Error Codes or a
+ // number outside that range returned by specific platforms e.g. Android
+ // returns 0x101 to signal a GATT failure
+ // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE
+ async setNextGATTConnectionResponse({code}) {
+ let {success} =
+ await this.fake_central_ptr_.setNextGATTConnectionResponse(
+ this.address, code);
+
+ if (success !== true) throw 'setNextGATTConnectionResponse failed.';
+ }
+
+ // Sets the next GATT Discovery request response for peripheral with
+ // |address| to |code|. |code| could be an HCI Error Code from
+ // BT 4.2 Vol 2 Part D 1.3 List Of Error Codes or a number outside that
+ // range returned by specific platforms e.g. Android returns 0x101 to signal
+ // a GATT failure
+ // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE
+ //
+ // The following procedures defined at BT 4.2 Vol 3 Part G Section 4.
+ // "GATT Feature Requirements" are used to discover attributes of the
+ // GATT Server:
+ // - Primary Service Discovery
+ // - Relationship Discovery
+ // - Characteristic Discovery
+ // - Characteristic Descriptor Discovery
+ // This method aims to simulate the response once all of these procedures
+ // have completed or if there was an error during any of them.
+ async setNextGATTDiscoveryResponse({code}) {
+ let {success} =
+ await this.fake_central_ptr_.setNextGATTDiscoveryResponse(
+ this.address, code);
+
+ if (success !== true) throw 'setNextGATTDiscoveryResponse failed.';
+ }
+
+ // Simulates a GATT disconnection from the peripheral with |address|.
+ async simulateGATTDisconnection() {
+ let {success} =
+ await this.fake_central_ptr_.simulateGATTDisconnection(this.address);
+
+ if (success !== true) throw 'simulateGATTDisconnection failed.';
+ }
+
+ // Simulates an Indication from the peripheral's GATT `Service Changed`
+ // Characteristic from BT 4.2 Vol 3 Part G 7.1. This Indication is signaled
+ // when services, characteristics, or descriptors are changed, added, or
+ // removed.
+ //
+ // The value for `Service Changed` is a range of attribute handles that have
+ // changed. However, this testing specification works at an abstracted
+ // level and does not expose setting attribute handles when adding
+ // attributes. Consequently, this simulate method should include the full
+ // range of all the peripheral's attribute handle values.
+ async simulateGATTServicesChanged() {
+ let {success} =
+ await this.fake_central_ptr_.simulateGATTServicesChanged(this.address);
+
+ if (success !== true) throw 'simulateGATTServicesChanged failed.';
+ }
+}
+
+class FakeRemoteGATTService {
+ constructor(service_id, peripheral_address, fake_central_ptr) {
+ this.service_id_ = service_id;
+ this.peripheral_address_ = peripheral_address;
+ this.fake_central_ptr_ = fake_central_ptr;
+ }
+
+ // Adds a fake GATT Characteristic with |uuid| and |properties|
+ // to this fake service. The characteristic will be found when discovering
+ // the peripheral's GATT Attributes. Returns a FakeRemoteGATTCharacteristic
+ // corresponding to the added characteristic.
+ async addFakeCharacteristic({uuid, properties}) {
+ let {characteristicId: characteristic_id} =
+ await this.fake_central_ptr_.addFakeCharacteristic(
+ {uuid: BluetoothUUID.getCharacteristic(uuid)},
+ ArrayToMojoCharacteristicProperties(properties),
+ this.service_id_,
+ this.peripheral_address_);
+
+ if (characteristic_id === null) throw 'addFakeCharacteristic failed';
+
+ return new FakeRemoteGATTCharacteristic(
+ characteristic_id, this.service_id_,
+ this.peripheral_address_, this.fake_central_ptr_);
+ }
+
+ // Removes the fake GATT service from its fake peripheral.
+ async remove() {
+ let {success} =
+ await this.fake_central_ptr_.removeFakeService(
+ this.service_id_,
+ this.peripheral_address_);
+
+ if (!success) throw 'remove failed';
+ }
+}
+
+class FakeRemoteGATTCharacteristic {
+ constructor(characteristic_id, service_id, peripheral_address,
+ fake_central_ptr) {
+ this.ids_ = [characteristic_id, service_id, peripheral_address];
+ this.descriptors_ = [];
+ this.fake_central_ptr_ = fake_central_ptr;
+ }
+
+ // Adds a fake GATT Descriptor with |uuid| to be discovered when
+ // discovering the peripheral's GATT Attributes. Returns a
+ // FakeRemoteGATTDescriptor corresponding to this descriptor. |uuid| should
+ // be a BluetoothDescriptorUUID
+ // https://webbluetoothcg.github.io/web-bluetooth/#typedefdef-bluetoothdescriptoruuid
+ async addFakeDescriptor({uuid}) {
+ let {descriptorId: descriptor_id} =
+ await this.fake_central_ptr_.addFakeDescriptor(
+ {uuid: BluetoothUUID.getDescriptor(uuid)}, ...this.ids_);
+
+ if (descriptor_id === null) throw 'addFakeDescriptor failed';
+
+ let fake_descriptor = new FakeRemoteGATTDescriptor(
+ descriptor_id, ...this.ids_, this.fake_central_ptr_);
+ this.descriptors_.push(fake_descriptor);
+
+ return fake_descriptor;
+ }
+
+ // Sets the next read response for characteristic to |code| and |value|.
+ // |code| could be a GATT Error Response from
+ // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range
+ // returned by specific platforms e.g. Android returns 0x101 to signal a GATT
+ // failure.
+ // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE
+ async setNextReadResponse(gatt_code, value=null) {
+ if (gatt_code === 0 && value === null) {
+ throw '|value| can\'t be null if read should success.';
+ }
+ if (gatt_code !== 0 && value !== null) {
+ throw '|value| must be null if read should fail.';
+ }
+
+ let {success} =
+ await this.fake_central_ptr_.setNextReadCharacteristicResponse(
+ gatt_code, value, ...this.ids_);
+
+ if (!success) throw 'setNextReadCharacteristicResponse failed';
+ }
+
+ // Sets the next write response for this characteristic to |code|. If
+ // writing to a characteristic that only supports 'write_without_response'
+ // the set response will be ignored.
+ // |code| could be a GATT Error Response from
+ // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range
+ // returned by specific platforms e.g. Android returns 0x101 to signal a GATT
+ // failure.
+ async setNextWriteResponse(gatt_code) {
+ let {success} =
+ await this.fake_central_ptr_.setNextWriteCharacteristicResponse(
+ gatt_code, ...this.ids_);
+
+ if (!success) throw 'setNextWriteCharacteristicResponse failed';
+ }
+
+ // Sets the next subscribe to notifications response for characteristic with
+ // |characteristic_id| in |service_id| and in |peripheral_address| to
+ // |code|. |code| could be a GATT Error Response from BT 4.2 Vol 3 Part F
+ // 3.4.1.1 Error Response or a number outside that range returned by
+ // specific platforms e.g. Android returns 0x101 to signal a GATT failure.
+ async setNextSubscribeToNotificationsResponse(gatt_code) {
+ let {success} =
+ await this.fake_central_ptr_.setNextSubscribeToNotificationsResponse(
+ gatt_code, ...this.ids_);
+
+ if (!success) throw 'setNextSubscribeToNotificationsResponse failed';
+ }
+
+ // Sets the next unsubscribe to notifications response for characteristic with
+ // |characteristic_id| in |service_id| and in |peripheral_address| to
+ // |code|. |code| could be a GATT Error Response from BT 4.2 Vol 3 Part F
+ // 3.4.1.1 Error Response or a number outside that range returned by
+ // specific platforms e.g. Android returns 0x101 to signal a GATT failure.
+ async setNextUnsubscribeFromNotificationsResponse(gatt_code) {
+ let {success} =
+ await this.fake_central_ptr_.setNextUnsubscribeFromNotificationsResponse(
+ gatt_code, ...this.ids_);
+
+ if (!success) throw 'setNextUnsubscribeToNotificationsResponse failed';
+ }
+
+ // Returns true if notifications from the characteristic have been subscribed
+ // to.
+ async isNotifying() {
+ let {success, isNotifying} =
+ await this.fake_central_ptr_.isNotifying(...this.ids_);
+
+ if (!success) throw 'isNotifying failed';
+
+ return isNotifying;
+ }
+
+ // Gets the last successfully written value to the characteristic and its
+ // write type. Write type is one of 'none', 'default-deprecated',
+ // 'with-response', 'without-response'. Returns {lastValue: null,
+ // lastWriteType: 'none'} if no value has yet been written to the
+ // characteristic.
+ async getLastWrittenValue() {
+ let {success, value, writeType} =
+ await this.fake_central_ptr_.getLastWrittenCharacteristicValue(
+ ...this.ids_);
+
+ if (!success) throw 'getLastWrittenCharacteristicValue failed';
+
+ return {lastValue: value, lastWriteType: writeTypeToString(writeType)};
+ }
+
+ // Removes the fake GATT Characteristic from its fake service.
+ async remove() {
+ let {success} =
+ await this.fake_central_ptr_.removeFakeCharacteristic(...this.ids_);
+
+ if (!success) throw 'remove failed';
+ }
+}
+
+class FakeRemoteGATTDescriptor {
+ constructor(descriptor_id,
+ characteristic_id,
+ service_id,
+ peripheral_address,
+ fake_central_ptr) {
+ this.ids_ = [
+ descriptor_id, characteristic_id, service_id, peripheral_address];
+ this.fake_central_ptr_ = fake_central_ptr;
+ }
+
+ // Sets the next read response for descriptor to |code| and |value|.
+ // |code| could be a GATT Error Response from
+ // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range
+ // returned by specific platforms e.g. Android returns 0x101 to signal a GATT
+ // failure.
+ // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE
+ async setNextReadResponse(gatt_code, value=null) {
+ if (gatt_code === 0 && value === null) {
+ throw '|value| cannot be null if read should succeed.';
+ }
+ if (gatt_code !== 0 && value !== null) {
+ throw '|value| must be null if read should fail.';
+ }
+
+ let {success} =
+ await this.fake_central_ptr_.setNextReadDescriptorResponse(
+ gatt_code, value, ...this.ids_);
+
+ if (!success) throw 'setNextReadDescriptorResponse failed';
+ }
+
+ // Sets the next write response for this descriptor to |code|.
+ // |code| could be a GATT Error Response from
+ // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range
+ // returned by specific platforms e.g. Android returns 0x101 to signal a GATT
+ // failure.
+ async setNextWriteResponse(gatt_code) {
+ let {success} =
+ await this.fake_central_ptr_.setNextWriteDescriptorResponse(
+ gatt_code, ...this.ids_);
+
+ if (!success) throw 'setNextWriteDescriptorResponse failed';
+ }
+
+ // Gets the last successfully written value to the descriptor.
+ // Returns null if no value has yet been written to the descriptor.
+ async getLastWrittenValue() {
+ let {success, value} =
+ await this.fake_central_ptr_.getLastWrittenDescriptorValue(
+ ...this.ids_);
+
+ if (!success) throw 'getLastWrittenDescriptorValue failed';
+
+ return value;
+ }
+
+ // Removes the fake GATT Descriptor from its fake characteristic.
+ async remove() {
+ let {success} =
+ await this.fake_central_ptr_.removeFakeDescriptor(...this.ids_);
+
+ if (!success) throw 'remove failed';
+ }
+}
+
+// FakeChooser allows clients to simulate user actions on a Bluetooth chooser,
+// and records the events produced by the Bluetooth chooser.
+class FakeChooser {
+ constructor() {
+ let fakeBluetoothChooserFactoryRemote =
+ new content.mojom.FakeBluetoothChooserFactoryRemote();
+ fakeBluetoothChooserFactoryRemote.$.bindNewPipeAndPassReceiver().bindInBrowser('process');
+
+ this.fake_bluetooth_chooser_ptr_ =
+ new content.mojom.FakeBluetoothChooserRemote();
+ this.fake_bluetooth_chooser_client_receiver_ =
+ new content.mojom.FakeBluetoothChooserClientReceiver(this);
+ fakeBluetoothChooserFactoryRemote.createFakeBluetoothChooser(
+ this.fake_bluetooth_chooser_ptr_.$.bindNewPipeAndPassReceiver(),
+ this.fake_bluetooth_chooser_client_receiver_.$.associateAndPassRemote());
+
+ this.events_ = new Array();
+ this.event_listener_ = null;
+ }
+
+ // If the chooser has received more events than |numOfEvents| this function
+ // will reject the promise, else it will wait until |numOfEvents| events are
+ // received before resolving with an array of |FakeBluetoothChooserEvent|
+ // objects.
+ async waitForEvents(numOfEvents) {
+ return new Promise(resolve => {
+ if (this.events_.length > numOfEvents) {
+ throw `Asked for ${numOfEvents} event(s), but received ` +
+ `${this.events_.length}.`;
+ }
+
+ this.event_listener_ = () => {
+ if (this.events_.length === numOfEvents) {
+ let result = Array.from(this.events_);
+ this.event_listener_ = null;
+ this.events_ = [];
+ resolve(result);
+ }
+ };
+ this.event_listener_();
+ });
+ }
+
+ async selectPeripheral(peripheral) {
+ if (!(peripheral instanceof FakePeripheral)) {
+ throw '|peripheral| must be an instance of FakePeripheral';
+ }
+ await this.fake_bluetooth_chooser_ptr_.selectPeripheral(peripheral.address);
+ }
+
+ async cancel() {
+ await this.fake_bluetooth_chooser_ptr_.cancel();
+ }
+
+ async rescan() {
+ await this.fake_bluetooth_chooser_ptr_.rescan();
+ }
+
+ onEvent(chooserEvent) {
+ chooserEvent.type = MOJO_CHOOSER_EVENT_TYPE_MAP[chooserEvent.type];
+ this.events_.push(chooserEvent);
+ if (this.event_listener_ !== null) {
+ this.event_listener_();
+ }
+ }
+}
+
+async function initializeChromiumResources() {
+ content.mojom = await import(
+ '/gen/content/web_test/common/fake_bluetooth_chooser.mojom.m.js');
+ bluetooth.mojom = await import(
+ '/gen/device/bluetooth/public/mojom/test/fake_bluetooth.mojom.m.js');
+
+ const map = MOJO_CHOOSER_EVENT_TYPE_MAP;
+ const types = content.mojom.ChooserEventType;
+ map[types.CHOOSER_OPENED] = 'chooser-opened';
+ map[types.CHOOSER_CLOSED] = 'chooser-closed';
+ map[types.ADAPTER_REMOVED] = 'adapter-removed';
+ map[types.ADAPTER_DISABLED] = 'adapter-disabled';
+ map[types.ADAPTER_ENABLED] = 'adapter-enabled';
+ map[types.DISCOVERY_FAILED_TO_START] = 'discovery-failed-to-start';
+ map[types.DISCOVERING] = 'discovering';
+ map[types.DISCOVERY_IDLE] = 'discovery-idle';
+ map[types.ADD_OR_UPDATE_DEVICE] = 'add-or-update-device';
+
+ // If this line fails, it means that current environment does not support the
+ // Web Bluetooth Test API.
+ try {
+ navigator.bluetooth.test = new FakeBluetooth();
+ } catch {
+ throw 'Web Bluetooth Test API is not implemented on this ' +
+ 'environment. See the bluetooth README at ' +
+ 'https://github.com/web-platform-tests/wpt/blob/master/bluetooth/README.md#web-bluetooth-testing';
+ }
+}
diff --git a/test/wpt/tests/resources/chromium/web-bluetooth-test.js.headers b/test/wpt/tests/resources/chromium/web-bluetooth-test.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/web-bluetooth-test.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/resources/chromium/webusb-child-test.js b/test/wpt/tests/resources/chromium/webusb-child-test.js
new file mode 100644
index 0000000..21412f6
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/webusb-child-test.js
@@ -0,0 +1,47 @@
+'use strict';
+
+// This polyfill prepares a child context to be attached to a parent context.
+// The parent must call navigator.usb.test.attachToContext() to attach to the
+// child context.
+(() => {
+ if (this.constructor.name === 'DedicatedWorkerGlobalScope' ||
+ this !== window.top) {
+
+ // Run Chromium specific set up code.
+ if (typeof MojoInterfaceInterceptor !== 'undefined') {
+ let messageChannel = new MessageChannel();
+ messageChannel.port1.onmessage = async (messageEvent) => {
+ if (messageEvent.data.type === 'Attach') {
+ messageEvent.data.interfaces.forEach(interfaceName => {
+ let interfaceInterceptor =
+ new MojoInterfaceInterceptor(interfaceName);
+ interfaceInterceptor.oninterfacerequest =
+ e => messageChannel.port1.postMessage({
+ type: interfaceName,
+ handle: e.handle
+ }, [e.handle]);
+ interfaceInterceptor.start();
+ });
+
+ // Wait for a call to GetDevices() to ensure that the interface
+ // handles are forwarded to the parent context.
+ try {
+ await navigator.usb.getDevices();
+ } catch (e) {
+ // This can happen in case of, for example, testing usb disallowed
+ // iframe.
+ console.error(`getDevices() throws error: ${e.name}: ${e.message}`);
+ }
+
+ messageChannel.port1.postMessage({ type: 'Complete' });
+ }
+ };
+
+ let message = { type: 'ReadyForAttachment', port: messageChannel.port2 };
+ if (typeof Window !== 'undefined')
+ parent.postMessage(message, '*', [messageChannel.port2]);
+ else
+ postMessage(message, [messageChannel.port2]);
+ }
+ }
+})();
diff --git a/test/wpt/tests/resources/chromium/webusb-child-test.js.headers b/test/wpt/tests/resources/chromium/webusb-child-test.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/webusb-child-test.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/resources/chromium/webusb-test.js b/test/wpt/tests/resources/chromium/webusb-test.js
new file mode 100644
index 0000000..7cca63d
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/webusb-test.js
@@ -0,0 +1,583 @@
+'use strict';
+
+// This polyfill library implements the WebUSB Test API as specified here:
+// https://wicg.github.io/webusb/test/
+
+(() => {
+
+// These variables are logically members of the USBTest class but are defined
+// here to hide them from being visible as fields of navigator.usb.test.
+let internal = {
+ intialized: false,
+
+ webUsbService: null,
+ webUsbServiceInterceptor: null,
+
+ messagePort: null,
+};
+
+let mojom = {};
+
+async function loadMojomDefinitions() {
+ const deviceMojom =
+ await import('/gen/services/device/public/mojom/usb_device.mojom.m.js');
+ const serviceMojom = await import(
+ '/gen/third_party/blink/public/mojom/usb/web_usb_service.mojom.m.js');
+ return {
+ ...deviceMojom,
+ ...serviceMojom,
+ };
+}
+
+function getMessagePort(target) {
+ return new Promise(resolve => {
+ target.addEventListener('message', messageEvent => {
+ if (messageEvent.data.type === 'ReadyForAttachment') {
+ if (internal.messagePort === null) {
+ internal.messagePort = messageEvent.data.port;
+ }
+ resolve();
+ }
+ }, {once: true});
+ });
+}
+
+// Converts an ECMAScript String object to an instance of
+// mojo_base.mojom.String16.
+function mojoString16ToString(string16) {
+ return String.fromCharCode.apply(null, string16.data);
+}
+
+// Converts an instance of mojo_base.mojom.String16 to an ECMAScript String.
+function stringToMojoString16(string) {
+ let array = new Array(string.length);
+ for (var i = 0; i < string.length; ++i) {
+ array[i] = string.charCodeAt(i);
+ }
+ return { data: array }
+}
+
+function fakeDeviceInitToDeviceInfo(guid, init) {
+ let deviceInfo = {
+ guid: guid + "",
+ usbVersionMajor: init.usbVersionMajor,
+ usbVersionMinor: init.usbVersionMinor,
+ usbVersionSubminor: init.usbVersionSubminor,
+ classCode: init.deviceClass,
+ subclassCode: init.deviceSubclass,
+ protocolCode: init.deviceProtocol,
+ vendorId: init.vendorId,
+ productId: init.productId,
+ deviceVersionMajor: init.deviceVersionMajor,
+ deviceVersionMinor: init.deviceVersionMinor,
+ deviceVersionSubminor: init.deviceVersionSubminor,
+ manufacturerName: stringToMojoString16(init.manufacturerName),
+ productName: stringToMojoString16(init.productName),
+ serialNumber: stringToMojoString16(init.serialNumber),
+ activeConfiguration: init.activeConfigurationValue,
+ configurations: []
+ };
+ init.configurations.forEach(config => {
+ var configInfo = {
+ configurationValue: config.configurationValue,
+ configurationName: stringToMojoString16(config.configurationName),
+ selfPowered: false,
+ remoteWakeup: false,
+ maximumPower: 0,
+ interfaces: [],
+ extraData: new Uint8Array()
+ };
+ config.interfaces.forEach(iface => {
+ var interfaceInfo = {
+ interfaceNumber: iface.interfaceNumber,
+ alternates: []
+ };
+ iface.alternates.forEach(alternate => {
+ var alternateInfo = {
+ alternateSetting: alternate.alternateSetting,
+ classCode: alternate.interfaceClass,
+ subclassCode: alternate.interfaceSubclass,
+ protocolCode: alternate.interfaceProtocol,
+ interfaceName: stringToMojoString16(alternate.interfaceName),
+ endpoints: [],
+ extraData: new Uint8Array()
+ };
+ alternate.endpoints.forEach(endpoint => {
+ var endpointInfo = {
+ endpointNumber: endpoint.endpointNumber,
+ packetSize: endpoint.packetSize,
+ synchronizationType: mojom.UsbSynchronizationType.NONE,
+ usageType: mojom.UsbUsageType.DATA,
+ pollingInterval: 0,
+ extraData: new Uint8Array()
+ };
+ switch (endpoint.direction) {
+ case "in":
+ endpointInfo.direction = mojom.UsbTransferDirection.INBOUND;
+ break;
+ case "out":
+ endpointInfo.direction = mojom.UsbTransferDirection.OUTBOUND;
+ break;
+ }
+ switch (endpoint.type) {
+ case "bulk":
+ endpointInfo.type = mojom.UsbTransferType.BULK;
+ break;
+ case "interrupt":
+ endpointInfo.type = mojom.UsbTransferType.INTERRUPT;
+ break;
+ case "isochronous":
+ endpointInfo.type = mojom.UsbTransferType.ISOCHRONOUS;
+ break;
+ }
+ alternateInfo.endpoints.push(endpointInfo);
+ });
+ interfaceInfo.alternates.push(alternateInfo);
+ });
+ configInfo.interfaces.push(interfaceInfo);
+ });
+ deviceInfo.configurations.push(configInfo);
+ });
+ return deviceInfo;
+}
+
+function convertMojoDeviceFilters(input) {
+ let output = [];
+ input.forEach(filter => {
+ output.push(convertMojoDeviceFilter(filter));
+ });
+ return output;
+}
+
+function convertMojoDeviceFilter(input) {
+ let output = {};
+ if (input.hasVendorId)
+ output.vendorId = input.vendorId;
+ if (input.hasProductId)
+ output.productId = input.productId;
+ if (input.hasClassCode)
+ output.classCode = input.classCode;
+ if (input.hasSubclassCode)
+ output.subclassCode = input.subclassCode;
+ if (input.hasProtocolCode)
+ output.protocolCode = input.protocolCode;
+ if (input.serialNumber)
+ output.serialNumber = mojoString16ToString(input.serialNumber);
+ return output;
+}
+
+class FakeDevice {
+ constructor(deviceInit) {
+ this.info_ = deviceInit;
+ this.opened_ = false;
+ this.currentConfiguration_ = null;
+ this.claimedInterfaces_ = new Map();
+ }
+
+ getConfiguration() {
+ if (this.currentConfiguration_) {
+ return Promise.resolve({
+ value: this.currentConfiguration_.configurationValue });
+ } else {
+ return Promise.resolve({ value: 0 });
+ }
+ }
+
+ open() {
+ assert_false(this.opened_);
+ this.opened_ = true;
+ return Promise.resolve({result: {success: mojom.UsbOpenDeviceSuccess.OK}});
+ }
+
+ close() {
+ assert_true(this.opened_);
+ this.opened_ = false;
+ return Promise.resolve();
+ }
+
+ setConfiguration(value) {
+ assert_true(this.opened_);
+
+ let selectedConfiguration = this.info_.configurations.find(
+ configuration => configuration.configurationValue == value);
+ // Blink should never request an invalid configuration.
+ assert_not_equals(selectedConfiguration, undefined);
+ this.currentConfiguration_ = selectedConfiguration;
+ return Promise.resolve({ success: true });
+ }
+
+ async claimInterface(interfaceNumber) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ assert_false(this.claimedInterfaces_.has(interfaceNumber),
+ 'interface already claimed');
+
+ const protectedInterfaces = new Set([
+ mojom.USB_AUDIO_CLASS,
+ mojom.USB_HID_CLASS,
+ mojom.USB_MASS_STORAGE_CLASS,
+ mojom.USB_SMART_CARD_CLASS,
+ mojom.USB_VIDEO_CLASS,
+ mojom.USB_AUDIO_VIDEO_CLASS,
+ mojom.USB_WIRELESS_CLASS,
+ ]);
+
+ let iface = this.currentConfiguration_.interfaces.find(
+ iface => iface.interfaceNumber == interfaceNumber);
+ // Blink should never request an invalid interface or alternate.
+ assert_false(iface == undefined);
+ if (iface.alternates.some(
+ alt => protectedInterfaces.has(alt.interfaceClass))) {
+ return {result: mojom.UsbClaimInterfaceResult.kProtectedClass};
+ }
+
+ this.claimedInterfaces_.set(interfaceNumber, 0);
+ return {result: mojom.UsbClaimInterfaceResult.kSuccess};
+ }
+
+ releaseInterface(interfaceNumber) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ assert_true(this.claimedInterfaces_.has(interfaceNumber));
+ this.claimedInterfaces_.delete(interfaceNumber);
+ return Promise.resolve({ success: true });
+ }
+
+ setInterfaceAlternateSetting(interfaceNumber, alternateSetting) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ assert_true(this.claimedInterfaces_.has(interfaceNumber));
+
+ let iface = this.currentConfiguration_.interfaces.find(
+ iface => iface.interfaceNumber == interfaceNumber);
+ // Blink should never request an invalid interface or alternate.
+ assert_false(iface == undefined);
+ assert_true(iface.alternates.some(
+ x => x.alternateSetting == alternateSetting));
+ this.claimedInterfaces_.set(interfaceNumber, alternateSetting);
+ return Promise.resolve({ success: true });
+ }
+
+ reset() {
+ assert_true(this.opened_);
+ return Promise.resolve({ success: true });
+ }
+
+ clearHalt(endpoint) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ // TODO(reillyg): Assert that endpoint is valid.
+ return Promise.resolve({ success: true });
+ }
+
+ async controlTransferIn(params, length, timeout) {
+ assert_true(this.opened_);
+
+ if ((params.recipient == mojom.UsbControlTransferRecipient.INTERFACE ||
+ params.recipient == mojom.UsbControlTransferRecipient.ENDPOINT) &&
+ this.currentConfiguration_ == null) {
+ return {
+ status: mojom.UsbTransferStatus.PERMISSION_DENIED,
+ };
+ }
+
+ return {
+ status: mojom.UsbTransferStatus.OK,
+ data: {
+ buffer: [
+ length >> 8, length & 0xff, params.request, params.value >> 8,
+ params.value & 0xff, params.index >> 8, params.index & 0xff
+ ]
+ }
+ };
+ }
+
+ async controlTransferOut(params, data, timeout) {
+ assert_true(this.opened_);
+
+ if ((params.recipient == mojom.UsbControlTransferRecipient.INTERFACE ||
+ params.recipient == mojom.UsbControlTransferRecipient.ENDPOINT) &&
+ this.currentConfiguration_ == null) {
+ return {
+ status: mojom.UsbTransferStatus.PERMISSION_DENIED,
+ };
+ }
+
+ return {status: mojom.UsbTransferStatus.OK, bytesWritten: data.byteLength};
+ }
+
+ genericTransferIn(endpointNumber, length, timeout) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ // TODO(reillyg): Assert that endpoint is valid.
+ let data = new Array(length);
+ for (let i = 0; i < length; ++i)
+ data[i] = i & 0xff;
+ return Promise.resolve(
+ {status: mojom.UsbTransferStatus.OK, data: {buffer: data}});
+ }
+
+ genericTransferOut(endpointNumber, data, timeout) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ // TODO(reillyg): Assert that endpoint is valid.
+ return Promise.resolve(
+ {status: mojom.UsbTransferStatus.OK, bytesWritten: data.byteLength});
+ }
+
+ isochronousTransferIn(endpointNumber, packetLengths, timeout) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ // TODO(reillyg): Assert that endpoint is valid.
+ let data = new Array(packetLengths.reduce((a, b) => a + b, 0));
+ let dataOffset = 0;
+ let packets = new Array(packetLengths.length);
+ for (let i = 0; i < packetLengths.length; ++i) {
+ for (let j = 0; j < packetLengths[i]; ++j)
+ data[dataOffset++] = j & 0xff;
+ packets[i] = {
+ length: packetLengths[i],
+ transferredLength: packetLengths[i],
+ status: mojom.UsbTransferStatus.OK
+ };
+ }
+ return Promise.resolve({data: {buffer: data}, packets: packets});
+ }
+
+ isochronousTransferOut(endpointNumber, data, packetLengths, timeout) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ // TODO(reillyg): Assert that endpoint is valid.
+ let packets = new Array(packetLengths.length);
+ for (let i = 0; i < packetLengths.length; ++i) {
+ packets[i] = {
+ length: packetLengths[i],
+ transferredLength: packetLengths[i],
+ status: mojom.UsbTransferStatus.OK
+ };
+ }
+ return Promise.resolve({ packets: packets });
+ }
+}
+
+class FakeWebUsbService {
+ constructor() {
+ this.receiver_ = new mojom.WebUsbServiceReceiver(this);
+ this.devices_ = new Map();
+ this.devicesByGuid_ = new Map();
+ this.client_ = null;
+ this.nextGuid_ = 0;
+ }
+
+ addBinding(handle) {
+ this.receiver_.$.bindHandle(handle);
+ }
+
+ addDevice(fakeDevice, info) {
+ let device = {
+ fakeDevice: fakeDevice,
+ guid: (this.nextGuid_++).toString(),
+ info: info,
+ receivers: [],
+ };
+ this.devices_.set(fakeDevice, device);
+ this.devicesByGuid_.set(device.guid, device);
+ if (this.client_)
+ this.client_.onDeviceAdded(fakeDeviceInitToDeviceInfo(device.guid, info));
+ }
+
+ async forgetDevice(guid) {
+ // Permissions are currently untestable through WPT.
+ }
+
+ removeDevice(fakeDevice) {
+ let device = this.devices_.get(fakeDevice);
+ if (!device)
+ throw new Error('Cannot remove unknown device.');
+
+ for (const receiver of device.receivers)
+ receiver.$.close();
+ this.devices_.delete(device.fakeDevice);
+ this.devicesByGuid_.delete(device.guid);
+ if (this.client_) {
+ this.client_.onDeviceRemoved(
+ fakeDeviceInitToDeviceInfo(device.guid, device.info));
+ }
+ }
+
+ removeAllDevices() {
+ this.devices_.forEach(device => {
+ for (const receiver of device.receivers)
+ receiver.$.close();
+ this.client_.onDeviceRemoved(
+ fakeDeviceInitToDeviceInfo(device.guid, device.info));
+ });
+ this.devices_.clear();
+ this.devicesByGuid_.clear();
+ }
+
+ getDevices() {
+ let devices = [];
+ this.devices_.forEach(device => {
+ devices.push(fakeDeviceInitToDeviceInfo(device.guid, device.info));
+ });
+ return Promise.resolve({ results: devices });
+ }
+
+ getDevice(guid, request) {
+ let retrievedDevice = this.devicesByGuid_.get(guid);
+ if (retrievedDevice) {
+ const receiver =
+ new mojom.UsbDeviceReceiver(new FakeDevice(retrievedDevice.info));
+ receiver.$.bindHandle(request.handle);
+ receiver.onConnectionError.addListener(() => {
+ if (retrievedDevice.fakeDevice.onclose)
+ retrievedDevice.fakeDevice.onclose();
+ });
+ retrievedDevice.receivers.push(receiver);
+ } else {
+ request.handle.close();
+ }
+ }
+
+ getPermission(options) {
+ return new Promise(resolve => {
+ if (navigator.usb.test.onrequestdevice) {
+ navigator.usb.test.onrequestdevice(
+ new USBDeviceRequestEvent(options, resolve));
+ } else {
+ resolve({ result: null });
+ }
+ });
+ }
+
+ setClient(client) {
+ this.client_ = client;
+ }
+}
+
+class USBDeviceRequestEvent {
+ constructor(options, resolve) {
+ this.filters = convertMojoDeviceFilters(options.filters);
+ this.exclusionFilters = convertMojoDeviceFilters(options.exclusionFilters);
+ this.resolveFunc_ = resolve;
+ }
+
+ respondWith(value) {
+ // Wait until |value| resolves (if it is a Promise). This function returns
+ // no value.
+ Promise.resolve(value).then(fakeDevice => {
+ let device = internal.webUsbService.devices_.get(fakeDevice);
+ let result = null;
+ if (device) {
+ result = fakeDeviceInitToDeviceInfo(device.guid, device.info);
+ }
+ this.resolveFunc_({ result: result });
+ }, () => {
+ this.resolveFunc_({ result: null });
+ });
+ }
+}
+
+// Unlike FakeDevice this class is exported to callers of USBTest.addFakeDevice.
+class FakeUSBDevice {
+ constructor() {
+ this.onclose = null;
+ }
+
+ disconnect() {
+ setTimeout(() => internal.webUsbService.removeDevice(this), 0);
+ }
+}
+
+class USBTest {
+ constructor() {
+ this.onrequestdevice = undefined;
+ }
+
+ async initialize() {
+ if (internal.initialized)
+ return;
+
+ // Be ready to handle 'ReadyForAttachment' message from child iframes.
+ if ('window' in self) {
+ getMessagePort(window);
+ }
+
+ mojom = await loadMojomDefinitions();
+ internal.webUsbService = new FakeWebUsbService();
+ internal.webUsbServiceInterceptor =
+ new MojoInterfaceInterceptor(mojom.WebUsbService.$interfaceName);
+ internal.webUsbServiceInterceptor.oninterfacerequest =
+ e => internal.webUsbService.addBinding(e.handle);
+ internal.webUsbServiceInterceptor.start();
+
+ // Wait for a call to GetDevices() to pass between the renderer and the
+ // mock in order to establish that everything is set up.
+ await navigator.usb.getDevices();
+ internal.initialized = true;
+ }
+
+ // Returns a promise that is resolved when the implementation of |usb| in the
+ // global scope for |context| is controlled by the current context.
+ attachToContext(context) {
+ if (!internal.initialized)
+ throw new Error('Call initialize() before attachToContext()');
+
+ let target = context.constructor.name === 'Worker' ? context : window;
+ return getMessagePort(target).then(() => {
+ return new Promise(resolve => {
+ internal.messagePort.onmessage = channelEvent => {
+ switch (channelEvent.data.type) {
+ case mojom.WebUsbService.$interfaceName:
+ internal.webUsbService.addBinding(channelEvent.data.handle);
+ break;
+ case 'Complete':
+ resolve();
+ break;
+ }
+ };
+ internal.messagePort.postMessage({
+ type: 'Attach',
+ interfaces: [
+ mojom.WebUsbService.$interfaceName,
+ ]
+ });
+ });
+ });
+ }
+
+ addFakeDevice(deviceInit) {
+ if (!internal.initialized)
+ throw new Error('Call initialize() before addFakeDevice().');
+
+ // |addDevice| and |removeDevice| are called in a setTimeout callback so
+ // that tests do not rely on the device being immediately available which
+ // may not be true for all implementations of this test API.
+ let fakeDevice = new FakeUSBDevice();
+ setTimeout(
+ () => internal.webUsbService.addDevice(fakeDevice, deviceInit), 0);
+ return fakeDevice;
+ }
+
+ reset() {
+ if (!internal.initialized)
+ throw new Error('Call initialize() before reset().');
+
+ // Reset the mocks in a setTimeout callback so that tests do not rely on
+ // the fact that this polyfill can do this synchronously.
+ return new Promise(resolve => {
+ setTimeout(() => {
+ if (internal.messagePort !== null)
+ internal.messagePort.close();
+ internal.messagePort = null;
+ internal.webUsbService.removeAllDevices();
+ resolve();
+ }, 0);
+ });
+ }
+}
+
+navigator.usb.test = new USBTest();
+
+})();
diff --git a/test/wpt/tests/resources/chromium/webusb-test.js.headers b/test/wpt/tests/resources/chromium/webusb-test.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/webusb-test.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/resources/chromium/webxr-test-math-helper.js b/test/wpt/tests/resources/chromium/webxr-test-math-helper.js
new file mode 100644
index 0000000..22c6c12
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/webxr-test-math-helper.js
@@ -0,0 +1,298 @@
+'use strict';
+
+// Math helper - used mainly in hit test implementation done by webxr-test.js
+class XRMathHelper {
+ static toString(p) {
+ return "[" + p.x + "," + p.y + "," + p.z + "," + p.w + "]";
+ }
+
+ static transform_by_matrix(matrix, point) {
+ return {
+ x : matrix[0] * point.x + matrix[4] * point.y + matrix[8] * point.z + matrix[12] * point.w,
+ y : matrix[1] * point.x + matrix[5] * point.y + matrix[9] * point.z + matrix[13] * point.w,
+ z : matrix[2] * point.x + matrix[6] * point.y + matrix[10] * point.z + matrix[14] * point.w,
+ w : matrix[3] * point.x + matrix[7] * point.y + matrix[11] * point.z + matrix[15] * point.w,
+ };
+ }
+
+ static neg(p) {
+ return {x : -p.x, y : -p.y, z : -p.z, w : p.w};
+ }
+
+ static sub(lhs, rhs) {
+ // .w is treated here like an entity type, 1 signifies points, 0 signifies vectors.
+ // point - point, point - vector, vector - vector are ok, vector - point is not.
+ if (lhs.w != rhs.w && lhs.w == 0.0) {
+ throw new Error("vector - point not allowed: " + toString(lhs) + "-" + toString(rhs));
+ }
+
+ return {x : lhs.x - rhs.x, y : lhs.y - rhs.y, z : lhs.z - rhs.z, w : lhs.w - rhs.w};
+ }
+
+ static add(lhs, rhs) {
+ if (lhs.w == rhs.w && lhs.w == 1.0) {
+ throw new Error("point + point not allowed", p1, p2);
+ }
+
+ return {x : lhs.x + rhs.x, y : lhs.y + rhs.y, z : lhs.z + rhs.z, w : lhs.w + rhs.w};
+ }
+
+ static cross(lhs, rhs) {
+ if (lhs.w != 0.0 || rhs.w != 0.0) {
+ throw new Error("cross product not allowed: " + toString(lhs) + "x" + toString(rhs));
+ }
+
+ return {
+ x : lhs.y * rhs.z - lhs.z * rhs.y,
+ y : lhs.z * rhs.x - lhs.x * rhs.z,
+ z : lhs.x * rhs.y - lhs.y * rhs.x,
+ w : 0
+ };
+ }
+
+ static dot(lhs, rhs) {
+ if (lhs.w != 0 || rhs.w != 0) {
+ throw new Error("dot product not allowed: " + toString(lhs) + "x" + toString(rhs));
+ }
+
+ return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z;
+ }
+
+ static mul(scalar, vector) {
+ if (vector.w != 0) {
+ throw new Error("scalar * vector not allowed", scalar, vector);
+ }
+
+ return {x : vector.x * scalar, y : vector.y * scalar, z : vector.z * scalar, w : vector.w};
+ }
+
+ static length(vector) {
+ return Math.sqrt(XRMathHelper.dot(vector, vector));
+ }
+
+ static normalize(vector) {
+ const l = XRMathHelper.length(vector);
+ return XRMathHelper.mul(1.0/l, vector);
+ }
+
+ // All |face|'s points and |point| must be co-planar.
+ static pointInFace(point, face) {
+ const normalize = XRMathHelper.normalize;
+ const sub = XRMathHelper.sub;
+ const length = XRMathHelper.length;
+ const cross = XRMathHelper.cross;
+
+ let onTheRight = null;
+ let previous_point = face[face.length - 1];
+
+ // |point| is in |face| if it's on the same side of all the edges.
+ for (let i = 0; i < face.length; ++i) {
+ const current_point = face[i];
+
+ const edge_direction = normalize(sub(current_point, previous_point));
+ const turn_direction = normalize(sub(point, current_point));
+
+ const sin_turn_angle = length(cross(edge_direction, turn_direction));
+
+ if (onTheRight == null) {
+ onTheRight = sin_turn_angle >= 0;
+ } else {
+ if (onTheRight && sin_turn_angle < 0) return false;
+ if (!onTheRight && sin_turn_angle > 0) return false;
+ }
+
+ previous_point = current_point;
+ }
+
+ return true;
+ }
+
+ static det2x2(m00, m01, m10, m11) {
+ return m00 * m11 - m01 * m10;
+ }
+
+ static det3x3(
+ m00, m01, m02,
+ m10, m11, m12,
+ m20, m21, m22
+ ){
+ const det2x2 = XRMathHelper.det2x2;
+
+ return m00 * det2x2(m11, m12, m21, m22)
+ - m01 * det2x2(m10, m12, m20, m22)
+ + m02 * det2x2(m10, m11, m20, m21);
+ }
+
+ static det4x4(
+ m00, m01, m02, m03,
+ m10, m11, m12, m13,
+ m20, m21, m22, m23,
+ m30, m31, m32, m33
+ ) {
+ const det3x3 = XRMathHelper.det3x3;
+
+ return m00 * det3x3(m11, m12, m13,
+ m21, m22, m23,
+ m31, m32, m33)
+ - m01 * det3x3(m10, m12, m13,
+ m20, m22, m23,
+ m30, m32, m33)
+ + m02 * det3x3(m10, m11, m13,
+ m20, m21, m23,
+ m30, m31, m33)
+ - m03 * det3x3(m10, m11, m12,
+ m20, m21, m22,
+ m30, m31, m32);
+ }
+
+ static inv2(m) {
+ // mij - i-th column, j-th row
+ const m00 = m[0], m01 = m[1], m02 = m[2], m03 = m[3];
+ const m10 = m[4], m11 = m[5], m12 = m[6], m13 = m[7];
+ const m20 = m[8], m21 = m[9], m22 = m[10], m23 = m[11];
+ const m30 = m[12], m31 = m[13], m32 = m[14], m33 = m[15];
+
+ const det = det4x4(
+ m00, m01, m02, m03,
+ m10, m11, m12, m13,
+ m20, m21, m22, m23,
+ m30, m31, m32, m33
+ );
+ }
+
+ static transpose(m) {
+ const result = Array(16);
+ for (let i = 0; i < 4; i++) {
+ for (let j = 0; j < 4; j++) {
+ result[i * 4 + j] = m[j * 4 + i];
+ }
+ }
+ return result;
+ }
+
+ // Inverts the matrix, ported from transformation_matrix.cc.
+ static inverse(m) {
+ const det3x3 = XRMathHelper.det3x3;
+
+ // mij - i-th column, j-th row
+ const m00 = m[0], m01 = m[1], m02 = m[2], m03 = m[3];
+ const m10 = m[4], m11 = m[5], m12 = m[6], m13 = m[7];
+ const m20 = m[8], m21 = m[9], m22 = m[10], m23 = m[11];
+ const m30 = m[12], m31 = m[13], m32 = m[14], m33 = m[15];
+
+ const det = XRMathHelper.det4x4(
+ m00, m01, m02, m03,
+ m10, m11, m12, m13,
+ m20, m21, m22, m23,
+ m30, m31, m32, m33
+ );
+
+ if (Math.abs(det) < 0.0001) {
+ return null;
+ }
+
+ const invDet = 1.0 / det;
+ // Calculate `comatrix * 1/det`:
+ const result2 = [
+ // First column (m0r):
+ invDet * det3x3(m11, m12, m13, m21, m22, m23, m32, m32, m33),
+ -invDet * det3x3(m10, m12, m13, m20, m22, m23, m30, m32, m33),
+ invDet * det3x3(m10, m11, m13, m20, m21, m23, m30, m31, m33),
+ -invDet * det3x3(m10, m11, m12, m20, m21, m22, m30, m31, m32),
+ // Second column (m1r):
+ -invDet * det3x3(m01, m02, m03, m21, m22, m23, m32, m32, m33),
+ invDet * det3x3(m00, m02, m03, m20, m22, m23, m30, m32, m33),
+ -invDet * det3x3(m00, m01, m03, m20, m21, m23, m30, m31, m33),
+ invDet * det3x3(m00, m01, m02, m20, m21, m22, m30, m31, m32),
+ // Third column (m2r):
+ invDet * det3x3(m01, m02, m03, m11, m12, m13, m31, m32, m33),
+ -invDet * det3x3(m00, m02, m03, m10, m12, m13, m30, m32, m33),
+ invDet * det3x3(m00, m01, m03, m10, m11, m13, m30, m31, m33),
+ -invDet * det3x3(m00, m01, m02, m10, m11, m12, m30, m31, m32),
+ // Fourth column (m3r):
+ -invDet * det3x3(m01, m02, m03, m11, m12, m13, m21, m22, m23),
+ invDet * det3x3(m00, m02, m03, m10, m12, m13, m20, m22, m23),
+ -invDet * det3x3(m00, m01, m03, m10, m11, m13, m20, m21, m23),
+ invDet * det3x3(m00, m01, m02, m10, m11, m12, m20, m21, m22),
+ ];
+
+ // Actual inverse is `1/det * transposed(comatrix)`:
+ return XRMathHelper.transpose(result2);
+ }
+
+ static mul4x4(m1, m2) {
+ if (m1 == null || m2 == null) {
+ return null;
+ }
+
+ const result = Array(16);
+
+ for (let row = 0; row < 4; row++) {
+ for (let col = 0; col < 4; col++) {
+ result[4 * col + row] = 0;
+ for(let i = 0; i < 4; i++) {
+ result[4 * col + row] += m1[4 * i + row] * m2[4 * col + i];
+ }
+ }
+ }
+
+ return result;
+ }
+
+ // Decomposes a matrix, with the assumption that the passed in matrix is
+ // a rigid transformation (i.e. position and rotation *only*!).
+ // The result is an object with `position` and `orientation` keys, which should
+ // be compatible with FakeXRRigidTransformInit.
+ // The implementation should match the behavior of gfx::Transform, but assumes
+ // that scale, skew & perspective are not present in the matrix so it could be
+ // simplified.
+ static decomposeRigidTransform(m) {
+ const m00 = m[0], m01 = m[1], m02 = m[2], m03 = m[3];
+ const m10 = m[4], m11 = m[5], m12 = m[6], m13 = m[7];
+ const m20 = m[8], m21 = m[9], m22 = m[10], m23 = m[11];
+ const m30 = m[12], m31 = m[13], m32 = m[14], m33 = m[15];
+
+ const position = [m30, m31, m32];
+ const orientation = [0, 0, 0, 0];
+
+ const trace = m00 + m11 + m22;
+ if (trace > 0) {
+ const S = Math.sqrt(trace + 1) * 2;
+ orientation[3] = 0.25 * S;
+ orientation[0] = (m12 - m21) / S;
+ orientation[1] = (m20 - m02) / S;
+ orientation[2] = (m01 - m10) / S;
+ } else if (m00 > m11 && m00 > m22) {
+ const S = Math.sqrt(1.0 + m00 - m11 - m22) * 2;
+ orientation[3] = (m12 - m21) / S;
+ orientation[0] = 0.25 * S;
+ orientation[1] = (m01 + m10) / S;
+ orientation[2] = (m20 + m02) / S;
+ } else if (m11 > m22) {
+ const S = Math.sqrt(1.0 + m11 - m00 - m22) * 2;
+ orientation[3] = (m20 - m02) / S;
+ orientation[0] = (m01 + m10) / S;
+ orientation[1] = 0.25 * S;
+ orientation[2] = (m12 + m21) / S;
+ } else {
+ const S = Math.sqrt(1.0 + m22 - m00 - m11) * 2;
+ orientation[3] = (m01 - m10) / S;
+ orientation[0] = (m20 + m02) / S;
+ orientation[1] = (m12 + m21) / S;
+ orientation[2] = 0.25 * S;
+ }
+
+ return { position, orientation };
+ }
+
+ static identity() {
+ return [
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, 0, 1
+ ];
+ };
+}
+
+XRMathHelper.EPSILON = 0.001;
diff --git a/test/wpt/tests/resources/chromium/webxr-test-math-helper.js.headers b/test/wpt/tests/resources/chromium/webxr-test-math-helper.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/webxr-test-math-helper.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/test/wpt/tests/resources/chromium/webxr-test.js b/test/wpt/tests/resources/chromium/webxr-test.js
new file mode 100644
index 0000000..c5eb1bd
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/webxr-test.js
@@ -0,0 +1,2125 @@
+import * as vrMojom from '/gen/device/vr/public/mojom/vr_service.mojom.m.js';
+import * as xrSessionMojom from '/gen/device/vr/public/mojom/xr_session.mojom.m.js';
+import {GamepadHand, GamepadMapping} from '/gen/device/gamepad/public/mojom/gamepad.mojom.m.js';
+
+// This polyfill library implements the WebXR Test API as specified here:
+// https://github.com/immersive-web/webxr-test-api
+
+const defaultMojoFromFloor = {
+ matrix: [1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 0, -1.65, 0, 1]
+};
+const default_stage_parameters = {
+ mojoFromFloor: defaultMojoFromFloor,
+ bounds: null
+};
+
+const default_framebuffer_scale = 0.7;
+
+function getMatrixFromTransform(transform) {
+ const x = transform.orientation[0];
+ const y = transform.orientation[1];
+ const z = transform.orientation[2];
+ const w = transform.orientation[3];
+
+ const m11 = 1.0 - 2.0 * (y * y + z * z);
+ const m21 = 2.0 * (x * y + z * w);
+ const m31 = 2.0 * (x * z - y * w);
+
+ const m12 = 2.0 * (x * y - z * w);
+ const m22 = 1.0 - 2.0 * (x * x + z * z);
+ const m32 = 2.0 * (y * z + x * w);
+
+ const m13 = 2.0 * (x * z + y * w);
+ const m23 = 2.0 * (y * z - x * w);
+ const m33 = 1.0 - 2.0 * (x * x + y * y);
+
+ const m14 = transform.position[0];
+ const m24 = transform.position[1];
+ const m34 = transform.position[2];
+
+ // Column-major linearized order is expected.
+ return [m11, m21, m31, 0,
+ m12, m22, m32, 0,
+ m13, m23, m33, 0,
+ m14, m24, m34, 1];
+}
+
+function getPoseFromTransform(transform) {
+ const [px, py, pz] = transform.position;
+ const [ox, oy, oz, ow] = transform.orientation;
+ return {
+ position: {x: px, y: py, z: pz},
+ orientation: {x: ox, y: oy, z: oz, w: ow},
+ };
+}
+
+function composeGFXTransform(fakeTransformInit) {
+ return {matrix: getMatrixFromTransform(fakeTransformInit)};
+}
+
+// Value equality for camera image init objects - they must contain `width` &
+// `height` properties and may contain `pixels` property.
+function isSameCameraImageInit(rhs, lhs) {
+ return lhs.width === rhs.width && lhs.height === rhs.height && lhs.pixels === rhs.pixels;
+}
+
+class ChromeXRTest {
+ constructor() {
+ this.mockVRService_ = new MockVRService();
+ }
+
+ // WebXR Test API
+ simulateDeviceConnection(init_params) {
+ return Promise.resolve(this.mockVRService_._addRuntime(init_params));
+ }
+
+ disconnectAllDevices() {
+ this.mockVRService_._removeAllRuntimes();
+ return Promise.resolve();
+ }
+
+ simulateUserActivation(callback) {
+ if (window.top !== window) {
+ // test_driver.click only works for the toplevel frame. This alternate
+ // Chrome-specific method is sufficient for starting an XR session in an
+ // iframe, and is used in platform-specific tests.
+ //
+ // TODO(https://github.com/web-platform-tests/wpt/issues/20282): use
+ // a cross-platform method if available.
+ xr_debug('simulateUserActivation', 'use eventSender');
+ document.addEventListener('click', callback);
+ eventSender.mouseMoveTo(0, 0);
+ eventSender.mouseDown();
+ eventSender.mouseUp();
+ document.removeEventListener('click', callback);
+ return;
+ }
+ const button = document.createElement('button');
+ button.textContent = 'click to continue test';
+ button.style.display = 'block';
+ button.style.fontSize = '20px';
+ button.style.padding = '10px';
+ button.onclick = () => {
+ callback();
+ document.body.removeChild(button);
+ };
+ document.body.appendChild(button);
+ test_driver.click(button);
+ }
+
+ // Helper method leveraged by chrome-specific setups.
+ Debug(name, msg) {
+ console.log(new Date().toISOString() + ' DEBUG[' + name + '] ' + msg);
+ }
+}
+
+// Mocking class definitions
+
+// Mock service implements the VRService mojo interface.
+class MockVRService {
+ constructor() {
+ this.receiver_ = new vrMojom.VRServiceReceiver(this);
+ this.runtimes_ = [];
+
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(vrMojom.VRService.$interfaceName);
+ this.interceptor_.oninterfacerequest =
+ e => this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+ }
+
+ // WebXR Test API Implementation Helpers
+ _addRuntime(fakeDeviceInit) {
+ const runtime = new MockRuntime(fakeDeviceInit, this);
+ this.runtimes_.push(runtime);
+
+ if (this.client_) {
+ this.client_.onDeviceChanged();
+ }
+
+ return runtime;
+ }
+
+ _removeAllRuntimes() {
+ if (this.client_) {
+ this.client_.onDeviceChanged();
+ }
+
+ this.runtimes_ = [];
+ }
+
+ _removeRuntime(device) {
+ const index = this.runtimes_.indexOf(device);
+ if (index >= 0) {
+ this.runtimes_.splice(index, 1);
+ if (this.client_) {
+ this.client_.onDeviceChanged();
+ }
+ }
+ }
+
+ // VRService overrides
+ setClient(client) {
+ if (this.client_) {
+ throw new Error("setClient should only be called once");
+ }
+
+ this.client_ = client;
+ }
+
+ requestSession(sessionOptions) {
+ const requests = [];
+ // Request a session from all the runtimes.
+ for (let i = 0; i < this.runtimes_.length; i++) {
+ requests[i] = this.runtimes_[i]._requestRuntimeSession(sessionOptions);
+ }
+
+ return Promise.all(requests).then((results) => {
+ // Find and return the first successful result.
+ for (let i = 0; i < results.length; i++) {
+ if (results[i].session) {
+ // Construct a dummy metrics recorder
+ const metricsRecorderPtr = new vrMojom.XRSessionMetricsRecorderRemote();
+ metricsRecorderPtr.$.bindNewPipeAndPassReceiver().handle.close();
+
+ const success = {
+ session: results[i].session,
+ metricsRecorder: metricsRecorderPtr,
+ };
+
+ return {result: {success}};
+ }
+ }
+
+ // If there were no successful results, returns a null session.
+ return {
+ result: {failureReason: vrMojom.RequestSessionError.NO_RUNTIME_FOUND}
+ };
+ });
+ }
+
+ supportsSession(sessionOptions) {
+ const requests = [];
+ // Check supports on all the runtimes.
+ for (let i = 0; i < this.runtimes_.length; i++) {
+ requests[i] = this.runtimes_[i]._runtimeSupportsSession(sessionOptions);
+ }
+
+ return Promise.all(requests).then((results) => {
+ // Find and return the first successful result.
+ for (let i = 0; i < results.length; i++) {
+ if (results[i].supportsSession) {
+ return results[i];
+ }
+ }
+
+ // If there were no successful results, returns false.
+ return {supportsSession: false};
+ });
+ }
+
+ exitPresent() {
+ return Promise.resolve();
+ }
+
+ setFramesThrottled(throttled) {
+ this.setFramesThrottledImpl(throttled);
+ }
+
+ // We cannot override the mojom interceptors via the prototype; so this method
+ // and the above indirection exist to allow overrides by internal code.
+ setFramesThrottledImpl(throttled) {}
+
+ // Only handles asynchronous calls to makeXrCompatible. Synchronous calls are
+ // not supported in Javascript.
+ makeXrCompatible() {
+ if (this.runtimes_.length == 0) {
+ return {
+ xrCompatibleResult: vrMojom.XrCompatibleResult.kNoDeviceAvailable
+ };
+ }
+ return {xrCompatibleResult: vrMojom.XrCompatibleResult.kAlreadyCompatible};
+ }
+}
+
+class FakeXRAnchorController {
+ constructor() {
+ // Private properties.
+ this.device_ = null;
+ this.id_ = null;
+ this.dirty_ = true;
+
+ // Properties backing up public attributes / methods.
+ this.deleted_ = false;
+ this.paused_ = false;
+ this.anchorOrigin_ = XRMathHelper.identity();
+ }
+
+ // WebXR Test API (Anchors Extension)
+ get deleted() {
+ return this.deleted_;
+ }
+
+ pauseTracking() {
+ if(!this.paused_) {
+ this.paused_ = true;
+ this.dirty_ = true;
+ }
+ }
+
+ resumeTracking() {
+ if(this.paused_) {
+ this.paused_ = false;
+ this.dirty_ = true;
+ }
+ }
+
+ stopTracking() {
+ if(!this.deleted_) {
+ this.device_._deleteAnchorController(this.id_);
+
+ this.deleted_ = true;
+ this.dirty_ = true;
+ }
+ }
+
+ setAnchorOrigin(anchorOrigin) {
+ this.anchorOrigin_ = getMatrixFromTransform(anchorOrigin);
+ this.dirty_ = true;
+ }
+
+ // Internal implementation:
+ set id(value) {
+ this.id_ = value;
+ }
+
+ set device(value) {
+ this.device_ = value;
+ }
+
+ get dirty() {
+ return this.dirty_;
+ }
+
+ get paused() {
+ return this.paused_;
+ }
+
+ _markProcessed() {
+ this.dirty_ = false;
+ }
+
+ _getAnchorOrigin() {
+ return this.anchorOrigin_;
+ }
+}
+
+// Implements XRFrameDataProvider and XRPresentationProvider. Maintains a mock
+// for XRPresentationProvider. Implements FakeXRDevice test API.
+class MockRuntime {
+ // Mapping from string feature names to the corresponding mojo types.
+ // This is exposed as a member for extensibility.
+ static _featureToMojoMap = {
+ 'viewer': xrSessionMojom.XRSessionFeature.REF_SPACE_VIEWER,
+ 'local': xrSessionMojom.XRSessionFeature.REF_SPACE_LOCAL,
+ 'local-floor': xrSessionMojom.XRSessionFeature.REF_SPACE_LOCAL_FLOOR,
+ 'bounded-floor': xrSessionMojom.XRSessionFeature.REF_SPACE_BOUNDED_FLOOR,
+ 'unbounded': xrSessionMojom.XRSessionFeature.REF_SPACE_UNBOUNDED,
+ 'hit-test': xrSessionMojom.XRSessionFeature.HIT_TEST,
+ 'dom-overlay': xrSessionMojom.XRSessionFeature.DOM_OVERLAY,
+ 'light-estimation': xrSessionMojom.XRSessionFeature.LIGHT_ESTIMATION,
+ 'anchors': xrSessionMojom.XRSessionFeature.ANCHORS,
+ 'depth-sensing': xrSessionMojom.XRSessionFeature.DEPTH,
+ 'secondary-views': xrSessionMojom.XRSessionFeature.SECONDARY_VIEWS,
+ 'camera-access': xrSessionMojom.XRSessionFeature.CAMERA_ACCESS,
+ 'layers': xrSessionMojom.XRSessionFeature.LAYERS,
+ };
+
+ static _sessionModeToMojoMap = {
+ "inline": xrSessionMojom.XRSessionMode.kInline,
+ "immersive-vr": xrSessionMojom.XRSessionMode.kImmersiveVr,
+ "immersive-ar": xrSessionMojom.XRSessionMode.kImmersiveAr,
+ };
+
+ static _environmentBlendModeToMojoMap = {
+ "opaque": vrMojom.XREnvironmentBlendMode.kOpaque,
+ "alpha-blend": vrMojom.XREnvironmentBlendMode.kAlphaBlend,
+ "additive": vrMojom.XREnvironmentBlendMode.kAdditive,
+ };
+
+ static _interactionModeToMojoMap = {
+ "screen-space": vrMojom.XRInteractionMode.kScreenSpace,
+ "world-space": vrMojom.XRInteractionMode.kWorldSpace,
+ };
+
+ constructor(fakeDeviceInit, service) {
+ this.sessionClient_ = null;
+ this.presentation_provider_ = new MockXRPresentationProvider();
+
+ this.pose_ = null;
+ this.next_frame_id_ = 0;
+ this.bounds_ = null;
+ this.send_mojo_space_reset_ = false;
+ this.stageParameters_ = null;
+ this.stageParametersId_ = 1;
+
+ this.service_ = service;
+
+ this.framesOfReference = {};
+
+ this.input_sources_ = new Map();
+ this.next_input_source_index_ = 1;
+
+ // Currently active hit test subscriptons.
+ this.hitTestSubscriptions_ = new Map();
+ // Currently active transient hit test subscriptions.
+ this.transientHitTestSubscriptions_ = new Map();
+ // ID of the next subscription to be assigned.
+ this.next_hit_test_id_ = 1n;
+
+ this.anchor_controllers_ = new Map();
+ // ID of the next anchor to be assigned.
+ this.next_anchor_id_ = 1n;
+ // Anchor creation callback (initially null, can be set by tests).
+ this.anchor_creation_callback_ = null;
+
+ this.depthSensingData_ = null;
+ this.depthSensingDataDirty_ = false;
+
+ let supportedModes = [];
+ if (fakeDeviceInit.supportedModes) {
+ supportedModes = fakeDeviceInit.supportedModes.slice();
+ if (fakeDeviceInit.supportedModes.length === 0) {
+ supportedModes = ["inline"];
+ }
+ } else {
+ // Back-compat mode.
+ console.warn("Please use `supportedModes` to signal which modes are supported by this device.");
+ if (fakeDeviceInit.supportsImmersive == null) {
+ throw new TypeError("'supportsImmersive' must be set");
+ }
+
+ supportedModes = ["inline"];
+ if (fakeDeviceInit.supportsImmersive) {
+ supportedModes.push("immersive-vr");
+ }
+ }
+
+ this.supportedModes_ = this._convertModesToEnum(supportedModes);
+ if (this.supportedModes_.length == 0) {
+ console.error("Device has empty supported modes array!");
+ throw new InvalidStateError();
+ }
+
+ if (fakeDeviceInit.viewerOrigin != null) {
+ this.setViewerOrigin(fakeDeviceInit.viewerOrigin);
+ }
+
+ if (fakeDeviceInit.floorOrigin != null) {
+ this.setFloorOrigin(fakeDeviceInit.floorOrigin);
+ }
+
+ if (fakeDeviceInit.world) {
+ this.setWorld(fakeDeviceInit.world);
+ }
+
+ if (fakeDeviceInit.depthSensingData) {
+ this.setDepthSensingData(fakeDeviceInit.depthSensingData);
+ }
+
+ this.defaultFramebufferScale_ = default_framebuffer_scale;
+ this.enviromentBlendMode_ = this._convertBlendModeToEnum(fakeDeviceInit.environmentBlendMode);
+ this.interactionMode_ = this._convertInteractionModeToEnum(fakeDeviceInit.interactionMode);
+
+ // This appropriately handles if the coordinates are null
+ this.setBoundsGeometry(fakeDeviceInit.boundsCoordinates);
+
+ this.setViews(fakeDeviceInit.views, fakeDeviceInit.secondaryViews);
+
+ // Need to support webVR which doesn't have a notion of features
+ this._setFeatures(fakeDeviceInit.supportedFeatures || []);
+ }
+
+ // WebXR Test API
+ setViews(primaryViews, secondaryViews) {
+ this.cameraImage_ = null;
+ this.primaryViews_ = [];
+ this.secondaryViews_ = [];
+ let xOffset = 0;
+ if (primaryViews) {
+ this.primaryViews_ = [];
+ xOffset = this._setViews(primaryViews, xOffset, this.primaryViews_);
+ const cameraImage = this._findCameraImage(primaryViews);
+
+ if (cameraImage) {
+ this.cameraImage_ = cameraImage;
+ }
+ }
+
+ if (secondaryViews) {
+ this.secondaryViews_ = [];
+ this._setViews(secondaryViews, xOffset, this.secondaryViews_);
+ const cameraImage = this._findCameraImage(secondaryViews);
+
+ if (cameraImage) {
+ if (!isSameCameraImageInit(this.cameraImage_, cameraImage)) {
+ throw new Error("If present, camera resolutions on each view must match each other!"
+ + " Secondary views' camera doesn't match primary views.");
+ }
+
+ this.cameraImage_ = cameraImage;
+ }
+ }
+ }
+
+ disconnect() {
+ this.service_._removeRuntime(this);
+ this.presentation_provider_._close();
+ if (this.sessionClient_) {
+ this.sessionClient_.$.close();
+ this.sessionClient_ = null;
+ }
+
+ return Promise.resolve();
+ }
+
+ setViewerOrigin(origin, emulatedPosition = false) {
+ const p = origin.position;
+ const q = origin.orientation;
+ this.pose_ = {
+ orientation: { x: q[0], y: q[1], z: q[2], w: q[3] },
+ position: { x: p[0], y: p[1], z: p[2] },
+ emulatedPosition: emulatedPosition,
+ angularVelocity: null,
+ linearVelocity: null,
+ angularAcceleration: null,
+ linearAcceleration: null,
+ inputState: null,
+ poseIndex: 0
+ };
+ }
+
+ clearViewerOrigin() {
+ this.pose_ = null;
+ }
+
+ setFloorOrigin(floorOrigin) {
+ if (!this.stageParameters_) {
+ this.stageParameters_ = default_stage_parameters;
+ this.stageParameters_.bounds = this.bounds_;
+ }
+
+ // floorOrigin is passed in as mojoFromFloor.
+ this.stageParameters_.mojoFromFloor =
+ {matrix: getMatrixFromTransform(floorOrigin)};
+
+ this._onStageParametersUpdated();
+ }
+
+ clearFloorOrigin() {
+ if (this.stageParameters_) {
+ this.stageParameters_ = null;
+ this._onStageParametersUpdated();
+ }
+ }
+
+ setBoundsGeometry(bounds) {
+ if (bounds == null) {
+ this.bounds_ = null;
+ } else if (bounds.length < 3) {
+ throw new Error("Bounds must have a length of at least 3");
+ } else {
+ this.bounds_ = bounds;
+ }
+
+ // We can only set bounds if we have stageParameters set; otherwise, we
+ // don't know the transform from local space to bounds space.
+ // We'll cache the bounds so that they can be set in the future if the
+ // floorLevel transform is set, but we won't update them just yet.
+ if (this.stageParameters_) {
+ this.stageParameters_.bounds = this.bounds_;
+ this._onStageParametersUpdated();
+ }
+ }
+
+ simulateResetPose() {
+ this.send_mojo_space_reset_ = true;
+ }
+
+ simulateVisibilityChange(visibilityState) {
+ let mojoState = null;
+ switch (visibilityState) {
+ case "visible":
+ mojoState = vrMojom.XRVisibilityState.VISIBLE;
+ break;
+ case "visible-blurred":
+ mojoState = vrMojom.XRVisibilityState.VISIBLE_BLURRED;
+ break;
+ case "hidden":
+ mojoState = vrMojom.XRVisibilityState.HIDDEN;
+ break;
+ }
+ if (mojoState && this.sessionClient_) {
+ this.sessionClient_.onVisibilityStateChanged(mojoState);
+ }
+ }
+
+ simulateInputSourceConnection(fakeInputSourceInit) {
+ const index = this.next_input_source_index_;
+ this.next_input_source_index_++;
+
+ const source = new MockXRInputSource(fakeInputSourceInit, index, this);
+ this.input_sources_.set(index, source);
+ return source;
+ }
+
+ // WebXR Test API Hit Test extensions
+ setWorld(world) {
+ this.world_ = world;
+ }
+
+ clearWorld() {
+ this.world_ = null;
+ }
+
+ // WebXR Test API Anchor extensions
+ setAnchorCreationCallback(callback) {
+ this.anchor_creation_callback_ = callback;
+ }
+
+ setHitTestSourceCreationCallback(callback) {
+ this.hit_test_source_creation_callback_ = callback;
+ }
+
+ // WebXR Test API Lighting estimation extensions
+ setLightEstimate(fakeXrLightEstimateInit) {
+ if (!fakeXrLightEstimateInit.sphericalHarmonicsCoefficients) {
+ throw new TypeError("sphericalHarmonicsCoefficients must be set");
+ }
+
+ if (fakeXrLightEstimateInit.sphericalHarmonicsCoefficients.length != 27) {
+ throw new TypeError("Must supply all 27 sphericalHarmonicsCoefficients");
+ }
+
+ if (fakeXrLightEstimateInit.primaryLightDirection && fakeXrLightEstimateInit.primaryLightDirection.w != 0) {
+ throw new TypeError("W component of primaryLightDirection must be 0");
+ }
+
+ if (fakeXrLightEstimateInit.primaryLightIntensity && fakeXrLightEstimateInit.primaryLightIntensity.w != 1) {
+ throw new TypeError("W component of primaryLightIntensity must be 1");
+ }
+
+ // If the primaryLightDirection or primaryLightIntensity aren't set, we need to set them
+ // to the defaults that the spec expects. ArCore will either give us everything or nothing,
+ // so these aren't nullable on the mojom.
+ if (!fakeXrLightEstimateInit.primaryLightDirection) {
+ fakeXrLightEstimateInit.primaryLightDirection = { x: 0.0, y: 1.0, z: 0.0, w: 0.0 };
+ }
+
+ if (!fakeXrLightEstimateInit.primaryLightIntensity) {
+ fakeXrLightEstimateInit.primaryLightIntensity = { x: 0.0, y: 0.0, z: 0.0, w: 1.0 };
+ }
+
+ let c = fakeXrLightEstimateInit.sphericalHarmonicsCoefficients;
+
+ this.light_estimate_ = {
+ lightProbe: {
+ // XRSphereicalHarmonics
+ sphericalHarmonics: {
+ coefficients: [
+ { red: c[0], green: c[1], blue: c[2] },
+ { red: c[3], green: c[4], blue: c[5] },
+ { red: c[6], green: c[7], blue: c[8] },
+ { red: c[9], green: c[10], blue: c[11] },
+ { red: c[12], green: c[13], blue: c[14] },
+ { red: c[15], green: c[16], blue: c[17] },
+ { red: c[18], green: c[19], blue: c[20] },
+ { red: c[21], green: c[22], blue: c[23] },
+ { red: c[24], green: c[25], blue: c[26] }
+ ]
+ },
+ // Vector3dF
+ mainLightDirection: {
+ x: fakeXrLightEstimateInit.primaryLightDirection.x,
+ y: fakeXrLightEstimateInit.primaryLightDirection.y,
+ z: fakeXrLightEstimateInit.primaryLightDirection.z
+ },
+ // RgbTupleF32
+ mainLightIntensity: {
+ red: fakeXrLightEstimateInit.primaryLightIntensity.x,
+ green: fakeXrLightEstimateInit.primaryLightIntensity.y,
+ blue: fakeXrLightEstimateInit.primaryLightIntensity.z
+ }
+ }
+ }
+ }
+
+ // WebXR Test API depth Sensing Extensions
+ setDepthSensingData(depthSensingData) {
+ for(const key of ["depthData", "normDepthBufferFromNormView", "rawValueToMeters", "width", "height"]) {
+ if(!(key in depthSensingData)) {
+ throw new TypeError("Required key not present. Key: " + key);
+ }
+ }
+
+ if(depthSensingData.depthData != null) {
+ // Create new object w/ properties based on the depthSensingData, but
+ // convert the FakeXRRigidTransformInit into a transformation matrix object.
+ this.depthSensingData_ = Object.assign({},
+ depthSensingData, {
+ normDepthBufferFromNormView: composeGFXTransform(depthSensingData.normDepthBufferFromNormView),
+ });
+ } else {
+ throw new TypeError("`depthData` is not set");
+ }
+
+ this.depthSensingDataDirty_ = true;
+ }
+
+ clearDepthSensingData() {
+ this.depthSensingData_ = null;
+ this.depthSensingDataDirty_ = true;
+ }
+
+ // Internal Implementation/Helper Methods
+ _convertModeToEnum(sessionMode) {
+ if (sessionMode in MockRuntime._sessionModeToMojoMap) {
+ return MockRuntime._sessionModeToMojoMap[sessionMode];
+ }
+
+ throw new TypeError("Unrecognized value for XRSessionMode enum: " + sessionMode);
+ }
+
+ _convertModesToEnum(sessionModes) {
+ return sessionModes.map(mode => this._convertModeToEnum(mode));
+ }
+
+ _convertBlendModeToEnum(blendMode) {
+ if (blendMode in MockRuntime._environmentBlendModeToMojoMap) {
+ return MockRuntime._environmentBlendModeToMojoMap[blendMode];
+ } else {
+ if (this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
+ return vrMojom.XREnvironmentBlendMode.kAdditive;
+ } else if (this.supportedModes_.includes(
+ xrSessionMojom.XRSessionMode.kImmersiveVr)) {
+ return vrMojom.XREnvironmentBlendMode.kOpaque;
+ }
+ }
+ }
+
+ _convertInteractionModeToEnum(interactionMode) {
+ if (interactionMode in MockRuntime._interactionModeToMojoMap) {
+ return MockRuntime._interactionModeToMojoMap[interactionMode];
+ } else {
+ return vrMojom.XRInteractionMode.kWorldSpace;
+ }
+ }
+
+ _setViews(deviceViews, xOffset, views) {
+ for (let i = 0; i < deviceViews.length; i++) {
+ views[i] = this._getView(deviceViews[i], xOffset);
+ xOffset += deviceViews[i].resolution.width;
+ }
+
+ return xOffset;
+ }
+
+ _findCameraImage(views) {
+ const viewWithCamera = views.find(view => view.cameraImageInit);
+ if (viewWithCamera) {
+ //If we have one view with a camera resolution, all views should have the same camera resolution.
+ const allViewsHaveSameCamera = views.every(
+ view => isSameCameraImageInit(view.cameraImageInit, viewWithCamera.cameraImageInit));
+
+ if (!allViewsHaveSameCamera) {
+ throw new Error("If present, camera resolutions on each view must match each other!");
+ }
+
+ return viewWithCamera.cameraImageInit;
+ }
+
+ return null;
+ }
+
+ _onStageParametersUpdated() {
+ // Indicate for the frame loop that the stage parameters have been updated.
+ this.stageParametersId_++;
+ }
+
+ _getDefaultViews() {
+ if (this.primaryViews_) {
+ return this.primaryViews_;
+ }
+
+ const viewport_size = 20;
+ return [{
+ eye: vrMojom.XREye.kLeft,
+ fieldOfView: {
+ upDegrees: 48.316,
+ downDegrees: 50.099,
+ leftDegrees: 50.899,
+ rightDegrees: 35.197
+ },
+ mojoFromView: this._getMojoFromViewerWithOffset(composeGFXTransform({
+ position: [-0.032, 0, 0],
+ orientation: [0, 0, 0, 1]
+ })),
+ viewport: { x: 0, y: 0, width: viewport_size, height: viewport_size }
+ },
+ {
+ eye: vrMojom.XREye.kRight,
+ fieldOfView: {
+ upDegrees: 48.316,
+ downDegrees: 50.099,
+ leftDegrees: 50.899,
+ rightDegrees: 35.197
+ },
+ mojoFromView: this._getMojoFromViewerWithOffset(composeGFXTransform({
+ position: [0.032, 0, 0],
+ orientation: [0, 0, 0, 1]
+ })),
+ viewport: { x: viewport_size, y: 0, width: viewport_size, height: viewport_size }
+ }];
+ }
+
+ // This function converts between the matrix provided by the WebXR test API
+ // and the internal data representation.
+ _getView(fakeXRViewInit, xOffset) {
+ let fov = null;
+
+ if (fakeXRViewInit.fieldOfView) {
+ fov = {
+ upDegrees: fakeXRViewInit.fieldOfView.upDegrees,
+ downDegrees: fakeXRViewInit.fieldOfView.downDegrees,
+ leftDegrees: fakeXRViewInit.fieldOfView.leftDegrees,
+ rightDegrees: fakeXRViewInit.fieldOfView.rightDegrees
+ };
+ } else {
+ const m = fakeXRViewInit.projectionMatrix;
+
+ function toDegrees(tan) {
+ return Math.atan(tan) * 180 / Math.PI;
+ }
+
+ const leftTan = (1 - m[8]) / m[0];
+ const rightTan = (1 + m[8]) / m[0];
+ const upTan = (1 + m[9]) / m[5];
+ const downTan = (1 - m[9]) / m[5];
+
+ fov = {
+ upDegrees: toDegrees(upTan),
+ downDegrees: toDegrees(downTan),
+ leftDegrees: toDegrees(leftTan),
+ rightDegrees: toDegrees(rightTan)
+ };
+ }
+
+ let viewEye = vrMojom.XREye.kNone;
+ // The eye passed in corresponds to the values in the WebXR spec, which are
+ // the strings "none", "left", and "right". They should be converted to the
+ // corresponding values of XREye in vr_service.mojom.
+ switch(fakeXRViewInit.eye) {
+ case "none":
+ viewEye = vrMojom.XREye.kNone;
+ break;
+ case "left":
+ viewEye = vrMojom.XREye.kLeft;
+ break;
+ case "right":
+ viewEye = vrMojom.XREye.kRight;
+ break;
+ }
+
+ return {
+ eye: viewEye,
+ fieldOfView: fov,
+ mojoFromView: this._getMojoFromViewerWithOffset(composeGFXTransform(fakeXRViewInit.viewOffset)),
+ viewport: {
+ x: xOffset,
+ y: 0,
+ width: fakeXRViewInit.resolution.width,
+ height: fakeXRViewInit.resolution.height
+ },
+ isFirstPersonObserver: fakeXRViewInit.isFirstPersonObserver ? true : false,
+ viewOffset: composeGFXTransform(fakeXRViewInit.viewOffset)
+ };
+ }
+
+ _setFeatures(supportedFeatures) {
+ function convertFeatureToMojom(feature) {
+ if (feature in MockRuntime._featureToMojoMap) {
+ return MockRuntime._featureToMojoMap[feature];
+ } else {
+ return xrSessionMojom.XRSessionFeature.INVALID;
+ }
+ }
+
+ this.supportedFeatures_ = [];
+
+ for (let i = 0; i < supportedFeatures.length; i++) {
+ const feature = convertFeatureToMojom(supportedFeatures[i]);
+ if (feature !== xrSessionMojom.XRSessionFeature.INVALID) {
+ this.supportedFeatures_.push(feature);
+ }
+ }
+ }
+
+ // These methods are intended to be used by MockXRInputSource only.
+ _addInputSource(source) {
+ if (!this.input_sources_.has(source.source_id_)) {
+ this.input_sources_.set(source.source_id_, source);
+ }
+ }
+
+ _removeInputSource(source) {
+ this.input_sources_.delete(source.source_id_);
+ }
+
+ // These methods are intended to be used by FakeXRAnchorController only.
+ _deleteAnchorController(controllerId) {
+ this.anchor_controllers_.delete(controllerId);
+ }
+
+ // Extension point for non-standard modules.
+ _injectAdditionalFrameData(options, frameData) {
+ }
+
+ // Mojo function implementations.
+
+ // XRFrameDataProvider implementation.
+ getFrameData(options) {
+ return new Promise((resolve) => {
+
+ const populatePose = () => {
+ const mojo_space_reset = this.send_mojo_space_reset_;
+ this.send_mojo_space_reset_ = false;
+
+ if (this.pose_) {
+ this.pose_.poseIndex++;
+ }
+
+ // Setting the input_state to null tests a slightly different path than
+ // the browser tests where if the last input source is removed, the device
+ // code always sends up an empty array, but it's also valid mojom to send
+ // up a null array.
+ let input_state = null;
+ if (this.input_sources_.size > 0) {
+ input_state = [];
+ for (const input_source of this.input_sources_.values()) {
+ input_state.push(input_source._getInputSourceState());
+ }
+ }
+
+ let frame_views = this.primaryViews_;
+ for (let i = 0; i < this.primaryViews_.length; i++) {
+ this.primaryViews_[i].mojoFromView =
+ this._getMojoFromViewerWithOffset(this.primaryViews_[i].viewOffset);
+ }
+ if (this.enabledFeatures_.includes(xrSessionMojom.XRSessionFeature.SECONDARY_VIEWS)) {
+ for (let i = 0; i < this.secondaryViews_.length; i++) {
+ this.secondaryViews_[i].mojoFromView =
+ this._getMojoFromViewerWithOffset(this.secondaryViews_[i].viewOffset);
+ }
+
+ frame_views = frame_views.concat(this.secondaryViews_);
+ }
+
+ const frameData = {
+ mojoFromViewer: this.pose_,
+ views: frame_views,
+ mojoSpaceReset: mojo_space_reset,
+ inputState: input_state,
+ timeDelta: {
+ // window.performance.now() is in milliseconds, so convert to microseconds.
+ microseconds: BigInt(Math.floor(window.performance.now() * 1000)),
+ },
+ frameId: this.next_frame_id_,
+ bufferHolder: null,
+ cameraImageSize: this.cameraImage_ ? {
+ width: this.cameraImage_.width,
+ height: this.cameraImage_.height
+ } : null,
+ renderingTimeRatio: 0,
+ stageParameters: this.stageParameters_,
+ stageParametersId: this.stageParametersId_,
+ lightEstimationData: this.light_estimate_
+ };
+
+ this.next_frame_id_++;
+
+ this._calculateHitTestResults(frameData);
+
+ this._calculateAnchorInformation(frameData);
+
+ this._calculateDepthInformation(frameData);
+
+ this._injectAdditionalFrameData(options, frameData);
+
+ resolve({frameData});
+ };
+
+ if(this.sessionOptions_.mode == xrSessionMojom.XRSessionMode.kInline) {
+ // Inline sessions should not have a delay introduced since it causes them
+ // to miss a vsync blink-side and delays propagation of changes that happened
+ // within a rAFcb by one frame (e.g. setViewerOrigin() calls would take 2 frames
+ // to propagate).
+ populatePose();
+ } else {
+ // For immerive sessions, add additional delay to allow for anchor creation
+ // promises to run.
+ setTimeout(populatePose, 3); // note: according to MDN, the timeout is not exact
+ }
+ });
+ }
+
+ getEnvironmentIntegrationProvider(environmentProviderRequest) {
+ if (this.environmentProviderReceiver_) {
+ this.environmentProviderReceiver_.$.close();
+ }
+ this.environmentProviderReceiver_ =
+ new vrMojom.XREnvironmentIntegrationProviderReceiver(this);
+ this.environmentProviderReceiver_.$.bindHandle(
+ environmentProviderRequest.handle);
+ }
+
+ // XREnvironmentIntegrationProvider implementation:
+ subscribeToHitTest(nativeOriginInformation, entityTypes, ray) {
+ if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
+ // Reject outside of AR.
+ return Promise.resolve({
+ result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC,
+ subscriptionId : 0n
+ });
+ }
+
+ if (!this._nativeOriginKnown(nativeOriginInformation)) {
+ return Promise.resolve({
+ result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC,
+ subscriptionId : 0n
+ });
+ }
+
+ // Reserve the id for hit test source:
+ const id = this.next_hit_test_id_++;
+ const hitTestParameters = { isTransient: false, profileName: null };
+ const controller = new FakeXRHitTestSourceController(id);
+
+
+ return this._shouldHitTestSourceCreationSucceed(hitTestParameters, controller)
+ .then((succeeded) => {
+ if(succeeded) {
+ // Store the subscription information as-is (including controller):
+ this.hitTestSubscriptions_.set(id, { nativeOriginInformation, entityTypes, ray, controller });
+
+ return Promise.resolve({
+ result : vrMojom.SubscribeToHitTestResult.SUCCESS,
+ subscriptionId : id
+ });
+ } else {
+ return Promise.resolve({
+ result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC,
+ subscriptionId : 0n
+ });
+ }
+ });
+ }
+
+ subscribeToHitTestForTransientInput(profileName, entityTypes, ray){
+ if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
+ // Reject outside of AR.
+ return Promise.resolve({
+ result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC,
+ subscriptionId : 0n
+ });
+ }
+
+ const id = this.next_hit_test_id_++;
+ const hitTestParameters = { isTransient: true, profileName: profileName };
+ const controller = new FakeXRHitTestSourceController(id);
+
+ // Check if we have hit test source creation callback.
+ // If yes, ask it if the hit test source creation should succeed.
+ // If no, for back-compat, assume the hit test source creation succeeded.
+ return this._shouldHitTestSourceCreationSucceed(hitTestParameters, controller)
+ .then((succeeded) => {
+ if(succeeded) {
+ // Store the subscription information as-is (including controller):
+ this.transientHitTestSubscriptions_.set(id, { profileName, entityTypes, ray, controller });
+
+ return Promise.resolve({
+ result : vrMojom.SubscribeToHitTestResult.SUCCESS,
+ subscriptionId : id
+ });
+ } else {
+ return Promise.resolve({
+ result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC,
+ subscriptionId : 0n
+ });
+ }
+ });
+ }
+
+ unsubscribeFromHitTest(subscriptionId) {
+ let controller = null;
+ if(this.transientHitTestSubscriptions_.has(subscriptionId)){
+ controller = this.transientHitTestSubscriptions_.get(subscriptionId).controller;
+ this.transientHitTestSubscriptions_.delete(subscriptionId);
+ } else if(this.hitTestSubscriptions_.has(subscriptionId)){
+ controller = this.hitTestSubscriptions_.get(subscriptionId).controller;
+ this.hitTestSubscriptions_.delete(subscriptionId);
+ }
+
+ if(controller) {
+ controller.deleted = true;
+ }
+ }
+
+ createAnchor(nativeOriginInformation, nativeOriginFromAnchor) {
+ return new Promise((resolve) => {
+ if(this.anchor_creation_callback_ == null) {
+ resolve({
+ result : vrMojom.CreateAnchorResult.FAILURE,
+ anchorId : 0n
+ });
+
+ return;
+ }
+
+ const mojoFromNativeOrigin = this._getMojoFromNativeOrigin(nativeOriginInformation);
+ if(mojoFromNativeOrigin == null) {
+ resolve({
+ result : vrMojom.CreateAnchorResult.FAILURE,
+ anchorId : 0n
+ });
+
+ return;
+ }
+
+ const mojoFromAnchor = XRMathHelper.mul4x4(mojoFromNativeOrigin, nativeOriginFromAnchor);
+
+ const anchorCreationParameters = {
+ requestedAnchorOrigin: mojoFromAnchor,
+ isAttachedToEntity: false,
+ };
+
+ const anchorController = new FakeXRAnchorController();
+
+ this.anchor_creation_callback_(anchorCreationParameters, anchorController)
+ .then((result) => {
+ if(result) {
+ // If the test allowed the anchor creation,
+ // store the anchor controller & return success.
+
+ const anchor_id = this.next_anchor_id_;
+ this.next_anchor_id_++;
+
+ this.anchor_controllers_.set(anchor_id, anchorController);
+ anchorController.device = this;
+ anchorController.id = anchor_id;
+
+ resolve({
+ result : vrMojom.CreateAnchorResult.SUCCESS,
+ anchorId : anchor_id
+ });
+ } else {
+ // The test has rejected anchor creation.
+ resolve({
+ result : vrMojom.CreateAnchorResult.FAILURE,
+ anchorId : 0n
+ });
+ }
+ })
+ .catch(() => {
+ // The test threw an error, treat anchor creation as failed.
+ resolve({
+ result : vrMojom.CreateAnchorResult.FAILURE,
+ anchorId : 0n
+ });
+ });
+ });
+ }
+
+ createPlaneAnchor(planeFromAnchor, planeId) {
+ return new Promise((resolve) => {
+
+ // Not supported yet.
+
+ resolve({
+ result : vrMojom.CreateAnchorResult.FAILURE,
+ anchorId : 0n,
+ });
+ });
+ }
+
+ detachAnchor(anchorId) {}
+
+ // Utility function
+ _requestRuntimeSession(sessionOptions) {
+ return this._runtimeSupportsSession(sessionOptions).then((result) => {
+ // The JavaScript bindings convert c_style_names to camelCase names.
+ const options = {
+ transportMethod:
+ vrMojom.XRPresentationTransportMethod.SUBMIT_AS_MAILBOX_HOLDER,
+ waitForTransferNotification: true,
+ waitForRenderNotification: true,
+ waitForGpuFence: false,
+ };
+
+ let submit_frame_sink;
+ if (result.supportsSession) {
+ submit_frame_sink = {
+ clientReceiver: this.presentation_provider_._getClientReceiver(),
+ provider: this.presentation_provider_._bindProvider(sessionOptions),
+ transportOptions: options
+ };
+
+ const dataProviderPtr = new vrMojom.XRFrameDataProviderRemote();
+ this.dataProviderReceiver_ =
+ new vrMojom.XRFrameDataProviderReceiver(this);
+ this.dataProviderReceiver_.$.bindHandle(
+ dataProviderPtr.$.bindNewPipeAndPassReceiver().handle);
+ this.sessionOptions_ = sessionOptions;
+
+ this.sessionClient_ = new vrMojom.XRSessionClientRemote();
+ const clientReceiver = this.sessionClient_.$.bindNewPipeAndPassReceiver();
+
+ const enabled_features = [];
+ for (let i = 0; i < sessionOptions.requiredFeatures.length; i++) {
+ if (this.supportedFeatures_.indexOf(sessionOptions.requiredFeatures[i]) !== -1) {
+ enabled_features.push(sessionOptions.requiredFeatures[i]);
+ } else {
+ return Promise.resolve({session: null});
+ }
+ }
+
+ for (let i =0; i < sessionOptions.optionalFeatures.length; i++) {
+ if (this.supportedFeatures_.indexOf(sessionOptions.optionalFeatures[i]) !== -1) {
+ enabled_features.push(sessionOptions.optionalFeatures[i]);
+ }
+ }
+
+ this.enabledFeatures_ = enabled_features;
+
+ return Promise.resolve({
+ session: {
+ submitFrameSink: submit_frame_sink,
+ dataProvider: dataProviderPtr,
+ clientReceiver: clientReceiver,
+ enabledFeatures: enabled_features,
+ deviceConfig: {
+ defaultFramebufferScale: this.defaultFramebufferScale_,
+ supportsViewportScaling: true,
+ depthConfiguration:
+ enabled_features.includes(xrSessionMojom.XRSessionFeature.DEPTH) ? {
+ depthUsage: vrMojom.XRDepthUsage.kCPUOptimized,
+ depthDataFormat: vrMojom.XRDepthDataFormat.kLuminanceAlpha,
+ } : null,
+ views: this._getDefaultViews(),
+ },
+ enviromentBlendMode: this.enviromentBlendMode_,
+ interactionMode: this.interactionMode_
+ }
+ });
+ } else {
+ return Promise.resolve({session: null});
+ }
+ });
+ }
+
+ _runtimeSupportsSession(options) {
+ let result = this.supportedModes_.includes(options.mode);
+
+ if (options.requiredFeatures.includes(xrSessionMojom.XRSessionFeature.DEPTH)
+ || options.optionalFeatures.includes(xrSessionMojom.XRSessionFeature.DEPTH)) {
+ result &= options.depthOptions.usagePreferences.includes(vrMojom.XRDepthUsage.kCPUOptimized);
+ result &= options.depthOptions.dataFormatPreferences.includes(vrMojom.XRDepthDataFormat.kLuminanceAlpha);
+ }
+
+ return Promise.resolve({
+ supportsSession: result,
+ });
+ }
+
+ // Private functions - utilities:
+ _nativeOriginKnown(nativeOriginInformation){
+
+ if (nativeOriginInformation.inputSourceSpaceInfo !== undefined) {
+ if (!this.input_sources_.has(nativeOriginInformation.inputSourceSpaceInfo.inputSourceId)) {
+ // Unknown input source.
+ return false;
+ }
+
+ return true;
+ } else if (nativeOriginInformation.referenceSpaceType !== undefined) {
+ // Bounded_floor & unbounded ref spaces are not yet supported for AR:
+ if (nativeOriginInformation.referenceSpaceType == vrMojom.XRReferenceSpaceType.kUnbounded
+ || nativeOriginInformation.referenceSpaceType == vrMojom.XRReferenceSpaceType.kBoundedFloor) {
+ return false;
+ }
+
+ return true;
+ } else {
+ // Planes and anchors are not yet supported by the mock interface.
+ return false;
+ }
+ }
+
+ // Private functions - anchors implementation:
+
+ // Modifies passed in frameData to add anchor information.
+ _calculateAnchorInformation(frameData) {
+ if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
+ return;
+ }
+
+ frameData.anchorsData = {allAnchorsIds: [], updatedAnchorsData: []};
+ for(const [id, controller] of this.anchor_controllers_) {
+ frameData.anchorsData.allAnchorsIds.push(id);
+
+ // Send the entire anchor data over if there was a change since last GetFrameData().
+ if(controller.dirty) {
+ const anchorData = {id};
+ if(!controller.paused) {
+ anchorData.mojoFromAnchor = getPoseFromTransform(
+ XRMathHelper.decomposeRigidTransform(
+ controller._getAnchorOrigin()));
+ }
+
+ controller._markProcessed();
+
+ frameData.anchorsData.updatedAnchorsData.push(anchorData);
+ }
+ }
+ }
+
+ // Private functions - depth sensing implementation:
+
+ // Modifies passed in frameData to add anchor information.
+ _calculateDepthInformation(frameData) {
+ if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
+ return;
+ }
+
+ if (!this.enabledFeatures_.includes(xrSessionMojom.XRSessionFeature.DEPTH)) {
+ return;
+ }
+
+ // If we don't have a current depth data, we'll return null
+ // (i.e. no data is not a valid data, so it cannot be "StillValid").
+ if (this.depthSensingData_ == null) {
+ frameData.depthData = null;
+ return;
+ }
+
+ if(!this.depthSensingDataDirty_) {
+ frameData.depthData = { dataStillValid: {}};
+ return;
+ }
+
+ frameData.depthData = {
+ updatedDepthData: {
+ timeDelta: frameData.timeDelta,
+ normTextureFromNormView: this.depthSensingData_.normDepthBufferFromNormView,
+ rawValueToMeters: this.depthSensingData_.rawValueToMeters,
+ size: { width: this.depthSensingData_.width, height: this.depthSensingData_.height },
+ pixelData: { bytes: this.depthSensingData_.depthData }
+ }
+ };
+
+ this.depthSensingDataDirty_ = false;
+ }
+
+ // Private functions - hit test implementation:
+
+ // Returns a Promise<bool> that signifies whether hit test source creation should succeed.
+ // If we have a hit test source creation callback installed, invoke it and return its result.
+ // If it's not installed, for back-compat just return a promise that resolves to true.
+ _shouldHitTestSourceCreationSucceed(hitTestParameters, controller) {
+ if(this.hit_test_source_creation_callback_) {
+ return this.hit_test_source_creation_callback_(hitTestParameters, controller);
+ } else {
+ return Promise.resolve(true);
+ }
+ }
+
+ // Modifies passed in frameData to add hit test results.
+ _calculateHitTestResults(frameData) {
+ if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
+ return;
+ }
+
+ frameData.hitTestSubscriptionResults = {results: [],
+ transientInputResults: []};
+ if (!this.world_) {
+ return;
+ }
+
+ // Non-transient hit test:
+ for (const [id, subscription] of this.hitTestSubscriptions_) {
+ const mojo_from_native_origin = this._getMojoFromNativeOrigin(subscription.nativeOriginInformation);
+ if (!mojo_from_native_origin) continue;
+
+ const [mojo_ray_origin, mojo_ray_direction] = this._transformRayToMojoSpace(
+ subscription.ray,
+ mojo_from_native_origin
+ );
+
+ const results = this._hitTestWorld(mojo_ray_origin, mojo_ray_direction, subscription.entityTypes);
+ frameData.hitTestSubscriptionResults.results.push(
+ {subscriptionId: id, hitTestResults: results});
+ }
+
+ // Transient hit test:
+ const mojo_from_viewer = this._getMojoFromViewer();
+
+ for (const [id, subscription] of this.transientHitTestSubscriptions_) {
+ const result = {subscriptionId: id,
+ inputSourceIdToHitTestResults: new Map()};
+
+ // Find all input sources that match the profile name:
+ const matching_input_sources = Array.from(this.input_sources_.values())
+ .filter(input_source => input_source.profiles_.includes(subscription.profileName));
+
+ for (const input_source of matching_input_sources) {
+ const mojo_from_native_origin = input_source._getMojoFromInputSource(mojo_from_viewer);
+
+ const [mojo_ray_origin, mojo_ray_direction] = this._transformRayToMojoSpace(
+ subscription.ray,
+ mojo_from_native_origin
+ );
+
+ const results = this._hitTestWorld(mojo_ray_origin, mojo_ray_direction, subscription.entityTypes);
+
+ result.inputSourceIdToHitTestResults.set(input_source.source_id_, results);
+ }
+
+ frameData.hitTestSubscriptionResults.transientInputResults.push(result);
+ }
+ }
+
+ // Returns 2-element array [origin, direction] of a ray in mojo space.
+ // |ray| is expressed relative to native origin.
+ _transformRayToMojoSpace(ray, mojo_from_native_origin) {
+ const ray_origin = {
+ x: ray.origin.x,
+ y: ray.origin.y,
+ z: ray.origin.z,
+ w: 1
+ };
+ const ray_direction = {
+ x: ray.direction.x,
+ y: ray.direction.y,
+ z: ray.direction.z,
+ w: 0
+ };
+
+ const mojo_ray_origin = XRMathHelper.transform_by_matrix(
+ mojo_from_native_origin,
+ ray_origin);
+ const mojo_ray_direction = XRMathHelper.transform_by_matrix(
+ mojo_from_native_origin,
+ ray_direction);
+
+ return [mojo_ray_origin, mojo_ray_direction];
+ }
+
+ // Hit tests the passed in ray (expressed as origin and direction) against the mocked world data.
+ _hitTestWorld(origin, direction, entityTypes) {
+ let result = [];
+
+ for (const region of this.world_.hitTestRegions) {
+ const partial_result = this._hitTestRegion(
+ region,
+ origin, direction,
+ entityTypes);
+
+ result = result.concat(partial_result);
+ }
+
+ return result.sort((lhs, rhs) => lhs.distance - rhs.distance).map((hitTest) => {
+ delete hitTest.distance;
+ return hitTest;
+ });
+ }
+
+ // Hit tests the passed in ray (expressed as origin and direction) against world region.
+ // |entityTypes| is a set of FakeXRRegionTypes.
+ // |region| is FakeXRRegion.
+ // Returns array of XRHitResults, each entry will be decorated with the distance from the ray origin (along the ray).
+ _hitTestRegion(region, origin, direction, entityTypes) {
+ const regionNameToMojoEnum = {
+ "point": vrMojom.EntityTypeForHitTest.POINT,
+ "plane": vrMojom.EntityTypeForHitTest.PLANE,
+ "mesh":null
+ };
+
+ if (!entityTypes.includes(regionNameToMojoEnum[region.type])) {
+ return [];
+ }
+
+ const result = [];
+ for (const face of region.faces) {
+ const maybe_hit = this._hitTestFace(face, origin, direction);
+ if (maybe_hit) {
+ result.push(maybe_hit);
+ }
+ }
+
+ // The results should be sorted by distance and there should be no 2 entries with
+ // the same distance from ray origin - that would mean they are the same point.
+ // This situation is possible when a ray intersects the region through an edge shared
+ // by 2 faces.
+ return result.sort((lhs, rhs) => lhs.distance - rhs.distance)
+ .filter((val, index, array) => index === 0 || val.distance !== array[index - 1].distance);
+ }
+
+ // Hit tests the passed in ray (expressed as origin and direction) against a single face.
+ // |face|, |origin|, and |direction| are specified in world (aka mojo) coordinates.
+ // |face| is an array of DOMPointInits.
+ // Returns null if the face does not intersect with the ray, otherwise the result is
+ // an XRHitResult with matrix describing the pose of the intersection point.
+ _hitTestFace(face, origin, direction) {
+ const add = XRMathHelper.add;
+ const sub = XRMathHelper.sub;
+ const mul = XRMathHelper.mul;
+ const normalize = XRMathHelper.normalize;
+ const dot = XRMathHelper.dot;
+ const cross = XRMathHelper.cross;
+ const neg = XRMathHelper.neg;
+
+ //1. Calculate plane normal in world coordinates.
+ const point_A = face.vertices[0];
+ const point_B = face.vertices[1];
+ const point_C = face.vertices[2];
+
+ const edge_AB = sub(point_B, point_A);
+ const edge_AC = sub(point_C, point_A);
+
+ const normal = normalize(cross(edge_AB, edge_AC));
+
+ const numerator = dot(sub(point_A, origin), normal);
+ const denominator = dot(direction, normal);
+
+ if (Math.abs(denominator) < XRMathHelper.EPSILON) {
+ // Planes are nearly parallel - there's either infinitely many intersection points or 0.
+ // Both cases signify a "no hit" for us.
+ return null;
+ } else {
+ // Single intersection point between the infinite plane and the line (*not* ray).
+ // Need to calculate the hit test matrix taking into account the face vertices.
+ const distance = numerator / denominator;
+ if (distance < 0) {
+ // Line - plane intersection exists, but not the half-line - plane does not.
+ return null;
+ } else {
+ const intersection_point = add(origin, mul(distance, direction));
+ // Since we are treating the face as a solid, flip the normal so that its
+ // half-space will contain the ray origin.
+ const y_axis = denominator > 0 ? neg(normal) : normal;
+
+ let z_axis = null;
+ const cos_direction_and_y_axis = dot(direction, y_axis);
+ if (Math.abs(cos_direction_and_y_axis) > (1 - XRMathHelper.EPSILON)) {
+ // Ray and the hit test normal are co-linear - try using the 'up' or 'right' vector's projection on the face plane as the Z axis.
+ // Note: this edge case is currently not covered by the spec.
+ const up = {x: 0.0, y: 1.0, z: 0.0, w: 0.0};
+ const right = {x: 1.0, y: 0.0, z: 0.0, w: 0.0};
+
+ z_axis = Math.abs(dot(up, y_axis)) > (1 - XRMathHelper.EPSILON)
+ ? sub(up, mul(dot(right, y_axis), y_axis)) // `up is also co-linear with hit test normal, use `right`
+ : sub(up, mul(dot(up, y_axis), y_axis)); // `up` is not co-linear with hit test normal, use it
+ } else {
+ // Project the ray direction onto the plane, negate it and use as a Z axis.
+ z_axis = neg(sub(direction, mul(cos_direction_and_y_axis, y_axis))); // Z should point towards the ray origin, not away.
+ }
+
+ z_axis = normalize(z_axis);
+ const x_axis = normalize(cross(y_axis, z_axis));
+
+ // Filter out the points not in polygon.
+ if (!XRMathHelper.pointInFace(intersection_point, face)) {
+ return null;
+ }
+
+ const hitResult = {planeId: 0n};
+ hitResult.distance = distance; // Extend the object with additional information used by higher layers.
+ // It will not be serialized over mojom.
+
+ const matrix = new Array(16);
+
+ matrix[0] = x_axis.x;
+ matrix[1] = x_axis.y;
+ matrix[2] = x_axis.z;
+ matrix[3] = 0;
+
+ matrix[4] = y_axis.x;
+ matrix[5] = y_axis.y;
+ matrix[6] = y_axis.z;
+ matrix[7] = 0;
+
+ matrix[8] = z_axis.x;
+ matrix[9] = z_axis.y;
+ matrix[10] = z_axis.z;
+ matrix[11] = 0;
+
+ matrix[12] = intersection_point.x;
+ matrix[13] = intersection_point.y;
+ matrix[14] = intersection_point.z;
+ matrix[15] = 1;
+
+ hitResult.mojoFromResult = getPoseFromTransform(
+ XRMathHelper.decomposeRigidTransform(matrix));
+ return hitResult;
+ }
+ }
+ }
+
+ _getMojoFromViewer() {
+ if (!this.pose_) {
+ return XRMathHelper.identity();
+ }
+ const transform = {
+ position: [
+ this.pose_.position.x,
+ this.pose_.position.y,
+ this.pose_.position.z],
+ orientation: [
+ this.pose_.orientation.x,
+ this.pose_.orientation.y,
+ this.pose_.orientation.z,
+ this.pose_.orientation.w],
+ };
+
+ return getMatrixFromTransform(transform);
+ }
+
+ _getMojoFromViewerWithOffset(viewOffset) {
+ return { matrix: XRMathHelper.mul4x4(this._getMojoFromViewer(), viewOffset.matrix) };
+ }
+
+ _getMojoFromNativeOrigin(nativeOriginInformation) {
+ const mojo_from_viewer = this._getMojoFromViewer();
+
+ if (nativeOriginInformation.inputSourceSpaceInfo !== undefined) {
+ if (!this.input_sources_.has(nativeOriginInformation.inputSourceSpaceInfo.inputSourceId)) {
+ return null;
+ } else {
+ const inputSource = this.input_sources_.get(nativeOriginInformation.inputSourceSpaceInfo.inputSourceId);
+ return inputSource._getMojoFromInputSource(mojo_from_viewer);
+ }
+ } else if (nativeOriginInformation.referenceSpaceType !== undefined) {
+ switch (nativeOriginInformation.referenceSpaceType) {
+ case vrMojom.XRReferenceSpaceType.kLocal:
+ return XRMathHelper.identity();
+ case vrMojom.XRReferenceSpaceType.kLocalFloor:
+ if (this.stageParameters_ == null || this.stageParameters_.mojoFromFloor == null) {
+ console.warn("Standing transform not available.");
+ return null;
+ }
+ return this.stageParameters_.mojoFromFloor.matrix;
+ case vrMojom.XRReferenceSpaceType.kViewer:
+ return mojo_from_viewer;
+ case vrMojom.XRReferenceSpaceType.kBoundedFloor:
+ return null;
+ case vrMojom.XRReferenceSpaceType.kUnbounded:
+ return null;
+ default:
+ throw new TypeError("Unrecognized XRReferenceSpaceType!");
+ }
+ } else {
+ // Anchors & planes are not yet supported for hit test.
+ return null;
+ }
+ }
+}
+
+class MockXRInputSource {
+ constructor(fakeInputSourceInit, id, pairedDevice) {
+ this.source_id_ = id;
+ this.pairedDevice_ = pairedDevice;
+ this.handedness_ = fakeInputSourceInit.handedness;
+ this.target_ray_mode_ = fakeInputSourceInit.targetRayMode;
+
+ if (fakeInputSourceInit.pointerOrigin == null) {
+ throw new TypeError("FakeXRInputSourceInit.pointerOrigin is required.");
+ }
+
+ this.setPointerOrigin(fakeInputSourceInit.pointerOrigin);
+ this.setProfiles(fakeInputSourceInit.profiles);
+
+ this.primary_input_pressed_ = false;
+ if (fakeInputSourceInit.selectionStarted != null) {
+ this.primary_input_pressed_ = fakeInputSourceInit.selectionStarted;
+ }
+
+ this.primary_input_clicked_ = false;
+ if (fakeInputSourceInit.selectionClicked != null) {
+ this.primary_input_clicked_ = fakeInputSourceInit.selectionClicked;
+ }
+
+ this.primary_squeeze_pressed_ = false;
+ this.primary_squeeze_clicked_ = false;
+
+ this.mojo_from_input_ = null;
+ if (fakeInputSourceInit.gripOrigin != null) {
+ this.setGripOrigin(fakeInputSourceInit.gripOrigin);
+ }
+
+ // This properly handles if supportedButtons were not specified.
+ this.setSupportedButtons(fakeInputSourceInit.supportedButtons);
+
+ this.emulated_position_ = false;
+ this.desc_dirty_ = true;
+ }
+
+ // WebXR Test API
+ setHandedness(handedness) {
+ if (this.handedness_ != handedness) {
+ this.desc_dirty_ = true;
+ this.handedness_ = handedness;
+ }
+ }
+
+ setTargetRayMode(targetRayMode) {
+ if (this.target_ray_mode_ != targetRayMode) {
+ this.desc_dirty_ = true;
+ this.target_ray_mode_ = targetRayMode;
+ }
+ }
+
+ setProfiles(profiles) {
+ this.desc_dirty_ = true;
+ this.profiles_ = profiles;
+ }
+
+ setGripOrigin(transform, emulatedPosition = false) {
+ // grip_origin was renamed to mojo_from_input in mojo
+ this.mojo_from_input_ = composeGFXTransform(transform);
+ this.emulated_position_ = emulatedPosition;
+
+ // Technically, setting the grip shouldn't make the description dirty, but
+ // the webxr-test-api sets our pointer as mojoFromPointer; however, we only
+ // support it across mojom as inputFromPointer, so we need to recalculate it
+ // whenever the grip moves.
+ this.desc_dirty_ = true;
+ }
+
+ clearGripOrigin() {
+ // grip_origin was renamed to mojo_from_input in mojo
+ if (this.mojo_from_input_ != null) {
+ this.mojo_from_input_ = null;
+ this.emulated_position_ = false;
+ this.desc_dirty_ = true;
+ }
+ }
+
+ setPointerOrigin(transform, emulatedPosition = false) {
+ // pointer_origin is mojo_from_pointer.
+ this.desc_dirty_ = true;
+ this.mojo_from_pointer_ = composeGFXTransform(transform);
+ this.emulated_position_ = emulatedPosition;
+ }
+
+ disconnect() {
+ this.pairedDevice_._removeInputSource(this);
+ }
+
+ reconnect() {
+ this.pairedDevice_._addInputSource(this);
+ }
+
+ startSelection() {
+ this.primary_input_pressed_ = true;
+ if (this.gamepad_) {
+ this.gamepad_.buttons[0].pressed = true;
+ this.gamepad_.buttons[0].touched = true;
+ }
+ }
+
+ endSelection() {
+ if (!this.primary_input_pressed_) {
+ throw new Error("Attempted to end selection which was not started");
+ }
+
+ this.primary_input_pressed_ = false;
+ this.primary_input_clicked_ = true;
+
+ if (this.gamepad_) {
+ this.gamepad_.buttons[0].pressed = false;
+ this.gamepad_.buttons[0].touched = false;
+ }
+ }
+
+ simulateSelect() {
+ this.primary_input_clicked_ = true;
+ }
+
+ setSupportedButtons(supportedButtons) {
+ this.gamepad_ = null;
+ this.supported_buttons_ = [];
+
+ // If there are no supported buttons, we can stop now.
+ if (supportedButtons == null || supportedButtons.length < 1) {
+ return;
+ }
+
+ const supported_button_map = {};
+ this.gamepad_ = this._getEmptyGamepad();
+ for (let i = 0; i < supportedButtons.length; i++) {
+ const buttonType = supportedButtons[i].buttonType;
+ this.supported_buttons_.push(buttonType);
+ supported_button_map[buttonType] = supportedButtons[i];
+ }
+
+ // Let's start by building the button state in order of priority:
+ // Primary button is index 0.
+ this.gamepad_.buttons.push({
+ pressed: this.primary_input_pressed_,
+ touched: this.primary_input_pressed_,
+ value: this.primary_input_pressed_ ? 1.0 : 0.0
+ });
+
+ // Now add the rest of our buttons
+ this._addGamepadButton(supported_button_map['grip']);
+ this._addGamepadButton(supported_button_map['touchpad']);
+ this._addGamepadButton(supported_button_map['thumbstick']);
+ this._addGamepadButton(supported_button_map['optional-button']);
+ this._addGamepadButton(supported_button_map['optional-thumbstick']);
+
+ // Finally, back-fill placeholder buttons/axes
+ for (let i = 0; i < this.gamepad_.buttons.length; i++) {
+ if (this.gamepad_.buttons[i] == null) {
+ this.gamepad_.buttons[i] = {
+ pressed: false,
+ touched: false,
+ value: 0
+ };
+ }
+ }
+
+ for (let i=0; i < this.gamepad_.axes.length; i++) {
+ if (this.gamepad_.axes[i] == null) {
+ this.gamepad_.axes[i] = 0;
+ }
+ }
+ }
+
+ updateButtonState(buttonState) {
+ if (this.supported_buttons_.indexOf(buttonState.buttonType) == -1) {
+ throw new Error("Tried to update state on an unsupported button");
+ }
+
+ const buttonIndex = this._getButtonIndex(buttonState.buttonType);
+ const axesStartIndex = this._getAxesStartIndex(buttonState.buttonType);
+
+ if (buttonIndex == -1) {
+ throw new Error("Unknown Button Type!");
+ }
+
+ // is this a 'squeeze' button?
+ if (buttonIndex === this._getButtonIndex('grip')) {
+ // squeeze
+ if (buttonState.pressed) {
+ this.primary_squeeze_pressed_ = true;
+ } else if (this.gamepad_.buttons[buttonIndex].pressed) {
+ this.primary_squeeze_clicked_ = true;
+ this.primary_squeeze_pressed_ = false;
+ } else {
+ this.primary_squeeze_clicked_ = false;
+ this.primary_squeeze_pressed_ = false;
+ }
+ }
+
+ this.gamepad_.buttons[buttonIndex].pressed = buttonState.pressed;
+ this.gamepad_.buttons[buttonIndex].touched = buttonState.touched;
+ this.gamepad_.buttons[buttonIndex].value = buttonState.pressedValue;
+
+ if (axesStartIndex != -1) {
+ this.gamepad_.axes[axesStartIndex] = buttonState.xValue == null ? 0.0 : buttonState.xValue;
+ this.gamepad_.axes[axesStartIndex + 1] = buttonState.yValue == null ? 0.0 : buttonState.yValue;
+ }
+ }
+
+ // DOM Overlay Extensions
+ setOverlayPointerPosition(x, y) {
+ this.overlay_pointer_position_ = {x: x, y: y};
+ }
+
+ // Helpers for Mojom
+ _getInputSourceState() {
+ const input_state = {};
+
+ input_state.sourceId = this.source_id_;
+ input_state.isAuxiliary = false;
+
+ input_state.primaryInputPressed = this.primary_input_pressed_;
+ input_state.primaryInputClicked = this.primary_input_clicked_;
+
+ input_state.primarySqueezePressed = this.primary_squeeze_pressed_;
+ input_state.primarySqueezeClicked = this.primary_squeeze_clicked_;
+ // Setting the input source's "clicked" state should generate one "select"
+ // event. Reset the input value to prevent it from continuously generating
+ // events.
+ this.primary_input_clicked_ = false;
+ // Setting the input source's "clicked" state should generate one "squeeze"
+ // event. Reset the input value to prevent it from continuously generating
+ // events.
+ this.primary_squeeze_clicked_ = false;
+
+ input_state.mojoFromInput = this.mojo_from_input_;
+
+ input_state.gamepad = this.gamepad_;
+
+ input_state.emulatedPosition = this.emulated_position_;
+
+ if (this.desc_dirty_) {
+ const input_desc = {};
+
+ switch (this.target_ray_mode_) {
+ case 'gaze':
+ input_desc.targetRayMode = vrMojom.XRTargetRayMode.GAZING;
+ break;
+ case 'tracked-pointer':
+ input_desc.targetRayMode = vrMojom.XRTargetRayMode.POINTING;
+ break;
+ case 'screen':
+ input_desc.targetRayMode = vrMojom.XRTargetRayMode.TAPPING;
+ break;
+ default:
+ throw new Error('Unhandled target ray mode ' + this.target_ray_mode_);
+ }
+
+ switch (this.handedness_) {
+ case 'left':
+ input_desc.handedness = vrMojom.XRHandedness.LEFT;
+ break;
+ case 'right':
+ input_desc.handedness = vrMojom.XRHandedness.RIGHT;
+ break;
+ default:
+ input_desc.handedness = vrMojom.XRHandedness.NONE;
+ break;
+ }
+
+ // Mojo requires us to send the pointerOrigin as relative to the grip
+ // space. If we don't have a grip space, we'll just assume that there
+ // is a grip at identity. This allows tests to simulate controllers that
+ // are really just a pointer with no tracked grip, though we will end up
+ // exposing that grip space.
+ let mojo_from_input = XRMathHelper.identity();
+ switch (this.target_ray_mode_) {
+ case 'gaze':
+ case 'screen':
+ // For gaze and screen space, we won't have a mojo_from_input; however
+ // the "input" position is just the viewer, so use mojo_from_viewer.
+ mojo_from_input = this.pairedDevice_._getMojoFromViewer();
+ break;
+ case 'tracked-pointer':
+ // If we have a tracked grip position (e.g. mojo_from_input), then use
+ // that. If we don't, then we'll just set the pointer offset directly,
+ // using identity as set above.
+ if (this.mojo_from_input_) {
+ mojo_from_input = this.mojo_from_input_.matrix;
+ }
+ break;
+ default:
+ throw new Error('Unhandled target ray mode ' + this.target_ray_mode_);
+ }
+
+ // To convert mojo_from_pointer to input_from_pointer, we need:
+ // input_from_pointer = input_from_mojo * mojo_from_pointer
+ // Since we store mojo_from_input, we need to invert it here before
+ // multiplying.
+ let input_from_mojo = XRMathHelper.inverse(mojo_from_input);
+ input_desc.inputFromPointer = {};
+ input_desc.inputFromPointer.matrix =
+ XRMathHelper.mul4x4(input_from_mojo, this.mojo_from_pointer_.matrix);
+
+ input_desc.profiles = this.profiles_;
+
+ input_state.description = input_desc;
+
+ this.desc_dirty_ = false;
+ }
+
+ // Pointer data for DOM Overlay, set by setOverlayPointerPosition()
+ if (this.overlay_pointer_position_) {
+ input_state.overlayPointerPosition = this.overlay_pointer_position_;
+ this.overlay_pointer_position_ = null;
+ }
+
+ return input_state;
+ }
+
+ _getEmptyGamepad() {
+ // Mojo complains if some of the properties on Gamepad are null, so set
+ // everything to reasonable defaults that tests can override.
+ const gamepad = {
+ connected: true,
+ id: [],
+ timestamp: 0n,
+ axes: [],
+ buttons: [],
+ touchEvents: [],
+ mapping: GamepadMapping.GamepadMappingStandard,
+ displayId: 0,
+ };
+
+ switch (this.handedness_) {
+ case 'left':
+ gamepad.hand = GamepadHand.GamepadHandLeft;
+ break;
+ case 'right':
+ gamepad.hand = GamepadHand.GamepadHandRight;
+ break;
+ default:
+ gamepad.hand = GamepadHand.GamepadHandNone;
+ break;
+ }
+
+ return gamepad;
+ }
+
+ _addGamepadButton(buttonState) {
+ if (buttonState == null) {
+ return;
+ }
+
+ const buttonIndex = this._getButtonIndex(buttonState.buttonType);
+ const axesStartIndex = this._getAxesStartIndex(buttonState.buttonType);
+
+ if (buttonIndex == -1) {
+ throw new Error("Unknown Button Type!");
+ }
+
+ this.gamepad_.buttons[buttonIndex] = {
+ pressed: buttonState.pressed,
+ touched: buttonState.touched,
+ value: buttonState.pressedValue
+ };
+
+ // Add x/y value if supported.
+ if (axesStartIndex != -1) {
+ this.gamepad_.axes[axesStartIndex] = (buttonState.xValue == null ? 0.0 : buttonSate.xValue);
+ this.gamepad_.axes[axesStartIndex + 1] = (buttonState.yValue == null ? 0.0 : buttonSate.yValue);
+ }
+ }
+
+ // General Helper methods
+ _getButtonIndex(buttonType) {
+ switch (buttonType) {
+ case 'grip':
+ return 1;
+ case 'touchpad':
+ return 2;
+ case 'thumbstick':
+ return 3;
+ case 'optional-button':
+ return 4;
+ case 'optional-thumbstick':
+ return 5;
+ default:
+ return -1;
+ }
+ }
+
+ _getAxesStartIndex(buttonType) {
+ switch (buttonType) {
+ case 'touchpad':
+ return 0;
+ case 'thumbstick':
+ return 2;
+ case 'optional-thumbstick':
+ return 4;
+ default:
+ return -1;
+ }
+ }
+
+ _getMojoFromInputSource(mojo_from_viewer) {
+ return this.mojo_from_pointer_.matrix;
+ }
+}
+
+// Mojo helper classes
+class FakeXRHitTestSourceController {
+ constructor(id) {
+ this.id_ = id;
+ this.deleted_ = false;
+ }
+
+ get deleted() {
+ return this.deleted_;
+ }
+
+ // Internal setter:
+ set deleted(value) {
+ this.deleted_ = value;
+ }
+}
+
+class MockXRPresentationProvider {
+ constructor() {
+ this.receiver_ = null;
+ this.submit_frame_count_ = 0;
+ this.missing_frame_count_ = 0;
+ }
+
+ _bindProvider() {
+ const provider = new vrMojom.XRPresentationProviderRemote();
+
+ if (this.receiver_) {
+ this.receiver_.$.close();
+ }
+ this.receiver_ = new vrMojom.XRPresentationProviderReceiver(this);
+ this.receiver_.$.bindHandle(provider.$.bindNewPipeAndPassReceiver().handle);
+ return provider;
+ }
+
+ _getClientReceiver() {
+ this.submitFrameClient_ = new vrMojom.XRPresentationClientRemote();
+ return this.submitFrameClient_.$.bindNewPipeAndPassReceiver();
+ }
+
+ // XRPresentationProvider mojo implementation
+ updateLayerBounds(frameId, leftBounds, rightBounds, sourceSize) {}
+
+ submitFrameMissing(frameId, mailboxHolder, timeWaited) {
+ this.missing_frame_count_++;
+ }
+
+ submitFrame(frameId, mailboxHolder, timeWaited) {
+ this.submit_frame_count_++;
+
+ // Trigger the submit completion callbacks here. WARNING: The
+ // Javascript-based mojo mocks are *not* re-entrant. It's OK to
+ // wait for these notifications on the next frame, but waiting
+ // within the current frame would never finish since the incoming
+ // calls would be queued until the current execution context finishes.
+ this.submitFrameClient_.onSubmitFrameTransferred(true);
+ this.submitFrameClient_.onSubmitFrameRendered();
+ }
+
+ submitFrameWithTextureHandle(frameId, texture, syncToken) {}
+
+ submitFrameDrawnIntoTexture(frameId, syncToken, timeWaited) {}
+
+ // Utility methods
+ _close() {
+ if (this.receiver_) {
+ this.receiver_.$.close();
+ }
+ }
+}
+
+// Export these into the global object as a side effect of importing this
+// module.
+self.ChromeXRTest = ChromeXRTest;
+self.MockRuntime = MockRuntime;
+self.MockVRService = MockVRService;
+self.SubscribeToHitTestResult = vrMojom.SubscribeToHitTestResult;
+
+navigator.xr.test = new ChromeXRTest();
diff --git a/test/wpt/tests/resources/chromium/webxr-test.js.headers b/test/wpt/tests/resources/chromium/webxr-test.js.headers
new file mode 100644
index 0000000..6805c32
--- /dev/null
+++ b/test/wpt/tests/resources/chromium/webxr-test.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8