blob: 8fb48c4dadfe70f1527de7a476cc9126c1f3f758 [file] [log] [blame]
// 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<T> {
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<T> {
const val = this.queue.shift();
if (val !== undefined) {
return val;
} else {
const queuePromise = new Promise<T>(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<Uint8Array>();
/**
* The ReadableStream of bytes from the device.
*/
readable = new ReadableStream<Uint8Array>({
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<Uint8Array>({
write: chunk => {
this.dataToDevice.next(chunk);
},
});
/**
* A spy for opening the serial port.
*/
open = jest.fn(async (options?: SerialOptions) => { });
/**
* A spy for closing the serial port.
*/
close = jest.fn(() => { });
}
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<SerialPort[]> {
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) {
throw new Error('Method not implemented.');
}
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.');
}
}