blob: b82aedadaa4988815c4d5d7e2cce75d94125ec19 [file] [log] [blame]
// Returns a SerialPort instance and associated FakeSerialPort instance.
async function getFakeSerialPort(fake) {
let token = fake.addPort();
let fakePort = fake.getFakePort(token);
let ports = await navigator.serial.getPorts();
assert_equals(ports.length, 1);
let port = ports[0];
assert_true(port instanceof SerialPort);
return { port, fakePort };
}
// Compare two Uint8Arrays.
function compareArrays(actual, expected) {
assert_true(actual instanceof Uint8Array, 'actual is Uint8Array');
assert_true(expected instanceof Uint8Array, 'expected is Uint8Array');
assert_equals(actual.byteLength, expected.byteLength, 'lengths equal');
for (let i = 0; i < expected.byteLength; ++i)
assert_equals(actual[i], expected[i], `Mismatch at position ${i}.`);
}
// Pull from |reader| until it reports done and return the data as a combined
// Uint8Array.
async function readAll(reader) {
const chunks = [];
while (true) {
let { value, done } = await reader.read();
if (done) {
// It would be better to allocate |buffer| up front with the number of
// of bytes expected but this is the best that can be done without a BYOB
// reader to control the amount of data read.
const length =
chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
const buffer = new Uint8Array(length);
chunks.reduce((offset, chunk) => {
buffer.set(chunk, offset);
return offset + chunk.byteLength;
}, 0);
return buffer;
}
chunks.push(value);
}
}
// 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() {
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) {
let {result, numBytes} = this._producer.writeData(chunk);
if (result == Mojo.RESULT_OK) {
if (numBytes < chunk.byteLength)
return this.write(chunk.slice(numBytes), controller);
} 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();
return this.write(chunk, controller);
}
}
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 blink.mojom.SerialPort.
class FakeSerialPort {
constructor() {
this.inputSignals_ = { dcd: false, cts: false, ri: false, dsr: false };
this.outputSignals_ = { dtr: false, rts: false, brk: false };
}
bind(request) {
assert_equals(this.binding, undefined, 'Port is still open');
this.binding = new mojo.Binding(device.mojom.SerialPort,
this, request);
this.binding.setConnectionErrorHandler(() => {
// OS typically clears DTR on close.
this.outputSignals_.dtr = false;
this.writable_.getWriter().close();
this.binding = undefined;
});
}
write(data) {
let writer = this.writable_.getWriter();
writer.write(data);
writer.releaseLock();
}
async read() {
let reader = this.readable_.getReader();
let result = await reader.read();
reader.releaseLock();
return result;
}
simulateParityError() {
this.writable_.getWriter().close();
this.writable_ = undefined;
this.client_.onReadError(device.mojom.SerialReceiveError.PARITY_ERROR);
}
simulateInputSignals(signals) {
this.inputSignals_ = signals;
}
get outputSignals() {
return this.outputSignals_;
}
waitForErrorCleared() {
if (this.writable_)
return Promise.resolve();
if (!this.errorClearedPromise_) {
this.errorClearedPromise_ = new Promise((resolve) => {
this.errorCleared_ = resolve;
});
}
return this.errorClearedPromise_;
}
async open(options, in_stream, out_stream, client) {
this.options_ = options;
this.client_ = client;
this.readable_ = new ReadableStream(new DataPipeSource(in_stream));
this.writable_ = new WritableStream(new DataPipeSink(out_stream));
// OS typically sets DTR on open.
this.outputSignals_.dtr = true;
return { success: true };
}
async clearSendError(in_stream) {}
async clearReadError(out_stream) {
this.writable_ = new WritableStream(new DataPipeSink(out_stream));
if (this.errorCleared_)
this.errorCleared_();
}
async flush() {
return { success: false };
}
async getControlSignals() {
return { signals: this.inputSignals_ };
}
async setControlSignals(signals) {
if (signals.hasDtr) {
this.outputSignals_.dtr = signals.dtr;
}
if (signals.hasRts) {
this.outputSignals_.rts = signals.rts;
}
if (signals.hasBrk) {
this.outputSignals_.brk = signals.brk;
}
return { success: true };
}
async configurePort(options) {
this.options_ = options;
return { success: true };
}
async getPortInfo() {
return {
bitrate: this.options_.bitrate,
data_bits: this.options_.data_bits,
parity_bit: this.options_.parity_bit,
stop_bits: this.options_.stop_bits,
cts_flow_control: this.options_.has_cts_flow_control ?
this.options_.cts_flow_control : false
};
}
async setBreak() {
return { success: false };
}
async clearBreak() {
return { success: false };
}
}
// Implementation of blink.mojom.SerialService.
class FakeSerialService {
constructor() {
this.interceptor_ =
new MojoInterfaceInterceptor(blink.mojom.SerialService.name, "context", true);
this.interceptor_.oninterfacerequest = e => this.bind(e.handle);
this.bindingSet_ = new mojo.BindingSet(blink.mojom.SerialService);
this.nextToken_ = 0;
this.reset();
}
start() {
this.interceptor_.start();
}
stop() {
this.interceptor_.stop();
}
reset() {
this.ports_ = new Map();
this.selectedPort_ = null;
}
addPort(vendorId, productId) {
let info = new blink.mojom.SerialPortInfo();
if (vendorId !== undefined) {
info.hasVendorId = true;
info.vendorId = vendorId;
}
if (productId !== undefined) {
info.hasProductId = true;
info.productId = productId;
}
let token = ++this.nextToken_;
info.token = new mojoBase.mojom.UnguessableToken();
info.token.high = 0;
info.token.low = token;
let record = {
portInfo: info,
fakePort: new FakeSerialPort(),
};
this.ports_.set(token, record);
return token;
}
removePort(token) {
this.ports_.delete(token);
}
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.bindingSet_.addBinding(this, handle);
}
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 getPort(token, port_receiver) {
let record = this.ports_.get(token.low);
if (record !== undefined) {
record.fakePort.bind(port_receiver);
} else {
port_receiver.close();
}
}
}
let fakeSerialService = new FakeSerialService();
function serial_test(func, name, properties) {
promise_test(async (test) => {
fakeSerialService.start();
try {
await func(test, fakeSerialService);
} finally {
fakeSerialService.stop();
fakeSerialService.reset();
}
}, name, properties);
}
function trustedClick() {
return new Promise(resolve => {
let button = document.createElement('button');
button.textContent = 'click to continue test';
button.style.display = 'block';
button.style.fontSize = '20px';
button.style.padding = '10px';
button.onclick = () => {
document.body.removeChild(button);
resolve();
};
document.body.appendChild(button);
test_driver.click(button);
});
}