blob: b7b303b6f14d47b98558f26dfc7e4b5c377f18f3 [file] [log] [blame]
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;
}
};
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 {
constructor(sensorRequest, handle, offset, size, reportingMode) {
this.client_ = null;
this.startShouldFail_ = false;
this.notifyOnReadingChange_ = true;
this.reportingMode_ = reportingMode;
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;
let rv = handle.mapBuffer(offset, size);
if (rv.result != Mojo.RESULT_OK) {
throw new Error("MockSensor(): Failed to map shared buffer");
}
this.buffer_ = new Float64Array(rv.buffer);
this.buffer_.fill(0);
this.receiver_ = new SensorReceiver(this);
this.receiver_.$.bindHandle(sensorRequest.handle);
}
// 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();
}
// Sets fake data that is used to deliver sensor reading updates.
async setSensorReading(readingData) {
this.readingData_ = new RingBuffer(readingData);
return this;
}
// 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, 2);
this.buffer_[1] = 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");
}
this.buffer_.set(reading, 2);
}
// For all tests sensor reading should have monotonically
// increasing timestamp.
this.buffer_[1] = 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);
const rv = Mojo.createSharedBuffer(this.sharedBufferSizeInBytes_);
if (rv.result != Mojo.RESULT_OK) {
throw new Error("MockSensorProvider: Failed to map shared buffer");
}
this.sharedBufferHandle_ = 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 mockSensor = new MockSensor(
sensor.$.bindNewPipeAndPassReceiver(), this.sharedBufferHandle_,
offset, this.readingSizeInBytes_, reportingMode);
this.activeSensors_.set(type, mockSensor);
this.activeSensors_.get(type).client_ = new SensorClientRemote();
}
const rv = this.sharedBufferHandle_.duplicateBufferHandle();
if (rv.result != Mojo.RESULT_OK) {
throw new Error("getSensor(): failed to duplicate Mojo buffer handler");
}
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_);
}
// Chromium applies some rounding and other privacy-related measures that
// can cause ALS not to report a reading when it has not changed beyond a
// certain threshold compared to the previous illuminance value. Make
// each reading return a different value that is significantly different
// from the previous one when setSensorReading() is not called by client
// code (e.g. run_generic_sensor_iframe_tests()).
if (type == SensorType.AMBIENT_LIGHT) {
this.activeSensors_.get(type).setSensorReading([
[window.performance.now() * 100],
[(window.performance.now() + 50) * 100]
]);
}
const client = this.activeSensors_.get(type).client_;
const initParams = {
sensor,
clientReceiver: client.$.bindNewPipeAndPassReceiver(),
memory: 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', false);
}
}
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;
})();