// Copyright 2022 The Pigweed Authors // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy of // the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. /* eslint-env browser */ import { Subject } from 'rxjs'; import type { SerialConnectionEvent, SerialPort, Serial, SerialPortRequestOptions, SerialOptions, } from 'pigweedjs/types/serial'; /** * AsyncQueue is a queue that allows values to be dequeued * before they are enqueued, returning a promise that resolves * once the value is available. */ class AsyncQueue { private queue: T[] = []; private requestQueue: Array<(val: T) => unknown> = []; /** * Enqueue val into the queue. * @param {T} val */ enqueue(val: T) { const callback = this.requestQueue.shift(); if (callback) { callback(val); } else { this.queue.push(val); } } /** * Dequeue a value from the queue, returning a promise * if the queue is empty. */ async dequeue(): Promise { const val = this.queue.shift(); if (val !== undefined) { return val; } else { const queuePromise = new Promise((resolve) => { this.requestQueue.push(resolve); }); return queuePromise; } } } /** * SerialPortMock is a mock for Chrome's upcoming SerialPort interface. * Since pw_web only depends on a subset of the interface, this mock * only implements that subset. */ class SerialPortMock implements SerialPort { private deviceData = new AsyncQueue<{ data?: Uint8Array; done?: boolean; error?: Error; }>(); /** * Simulate the device sending data to the browser. * @param {Uint8Array} data */ dataFromDevice(data: Uint8Array) { this.deviceData.enqueue({ data }); } /** * Simulate the device closing the connection with the browser. */ closeFromDevice() { this.deviceData.enqueue({ done: true }); } /** * Simulate an error in the device's read stream. * @param {Error} error */ errorFromDevice(error: Error) { this.deviceData.enqueue({ error }); } /** * An rxjs subject tracking data sent to the (fake) device. */ dataToDevice = new Subject(); /** * The ReadableStream of bytes from the device. */ readable = new ReadableStream({ pull: async (controller) => { const { data, done, error } = await this.deviceData.dequeue(); if (done) { controller.close(); return; } if (error) { throw error; } if (data) { controller.enqueue(data); } }, }); /** * The WritableStream of bytes to the device. */ writable = new WritableStream({ write: (chunk) => { this.dataToDevice.next(chunk); }, }); /** * A spy for opening the serial port. */ open = jest.fn(async (options?: SerialOptions) => { // Do nothing. }); /** * A spy for closing the serial port. */ close = jest.fn(() => { // Do nothing. }); } export class SerialMock implements Serial { serialPort = new SerialPortMock(); dataToDevice = this.serialPort.dataToDevice; dataFromDevice = (data: Uint8Array) => { this.serialPort.dataFromDevice(data); }; closeFromDevice = () => { this.serialPort.closeFromDevice(); }; errorFromDevice = (error: Error) => { this.serialPort.errorFromDevice(error); }; /** * Request the port from the browser. */ async requestPort(options?: SerialPortRequestOptions) { return this.serialPort; } // The rest of the methods are unimplemented // and only exist to ensure SerialMock implements Serial onconnect(): ((this: this, ev: SerialConnectionEvent) => any) | null { throw new Error('Method not implemented.'); } ondisconnect(): ((this: this, ev: SerialConnectionEvent) => any) | null { throw new Error('Method not implemented.'); } getPorts(): Promise { throw new Error('Method not implemented.'); } addEventListener( type: 'connect' | 'disconnect', listener: (this: this, ev: SerialConnectionEvent) => any, useCapture?: boolean, ): void; addEventListener( type: string, listener: EventListener | EventListenerObject | null, options?: boolean | AddEventListenerOptions, ): void; addEventListener(type: any, listener: any, options?: any) { console.info('Silently skipping event listeners in mock mode.'); } removeEventListener( type: 'connect' | 'disconnect', callback: (this: this, ev: SerialConnectionEvent) => any, useCapture?: boolean, ): void; removeEventListener( type: string, callback: EventListener | EventListenerObject | null, options?: boolean | EventListenerOptions, ): void; removeEventListener(type: any, callback: any, options?: any) { throw new Error('Method not implemented.'); } dispatchEvent(event: Event): boolean { throw new Error('Method not implemented.'); } }