1// Copyright 2022 The Pigweed Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); you may not 4// use this file except in compliance with the License. You may obtain a copy of 5// the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12// License for the specific language governing permissions and limitations under 13// the License. 14 15/* eslint-env browser */ 16import {Subject} from 'rxjs'; 17import type {SerialConnectionEvent, SerialPort, Serial, SerialPortRequestOptions, SerialOptions} from "pigweedjs/types/serial" 18/** 19 * AsyncQueue is a queue that allows values to be dequeued 20 * before they are enqueued, returning a promise that resolves 21 * once the value is available. 22 */ 23class AsyncQueue<T> { 24 private queue: T[] = []; 25 private requestQueue: Array<(val: T) => unknown> = []; 26 27 /** 28 * Enqueue val into the queue. 29 * @param {T} val 30 */ 31 enqueue(val: T) { 32 const callback = this.requestQueue.shift(); 33 if (callback) { 34 callback(val); 35 } else { 36 this.queue.push(val); 37 } 38 } 39 40 /** 41 * Dequeue a value from the queue, returning a promise 42 * if the queue is empty. 43 */ 44 async dequeue(): Promise<T> { 45 const val = this.queue.shift(); 46 if (val !== undefined) { 47 return val; 48 } else { 49 const queuePromise = new Promise<T>(resolve => { 50 this.requestQueue.push(resolve); 51 }); 52 return queuePromise; 53 } 54 } 55} 56 57/** 58 * SerialPortMock is a mock for Chrome's upcoming SerialPort interface. 59 * Since pw_web only depends on a subset of the interface, this mock 60 * only implements that subset. 61 */ 62class SerialPortMock implements SerialPort { 63 private deviceData = new AsyncQueue<{ 64 data?: Uint8Array; 65 done?: boolean; 66 error?: Error; 67 }>(); 68 69 /** 70 * Simulate the device sending data to the browser. 71 * @param {Uint8Array} data 72 */ 73 dataFromDevice(data: Uint8Array) { 74 this.deviceData.enqueue({data}); 75 } 76 77 /** 78 * Simulate the device closing the connection with the browser. 79 */ 80 closeFromDevice() { 81 this.deviceData.enqueue({done: true}); 82 } 83 84 /** 85 * Simulate an error in the device's read stream. 86 * @param {Error} error 87 */ 88 errorFromDevice(error: Error) { 89 this.deviceData.enqueue({error}); 90 } 91 92 /** 93 * An rxjs subject tracking data sent to the (fake) device. 94 */ 95 dataToDevice = new Subject<Uint8Array>(); 96 97 /** 98 * The ReadableStream of bytes from the device. 99 */ 100 readable = new ReadableStream<Uint8Array>({ 101 pull: async controller => { 102 const {data, done, error} = await this.deviceData.dequeue(); 103 if (done) { 104 controller.close(); 105 return; 106 } 107 if (error) { 108 throw error; 109 } 110 if (data) { 111 controller.enqueue(data); 112 } 113 }, 114 }); 115 116 /** 117 * The WritableStream of bytes to the device. 118 */ 119 writable = new WritableStream<Uint8Array>({ 120 write: chunk => { 121 this.dataToDevice.next(chunk); 122 }, 123 }); 124 125 /** 126 * A spy for opening the serial port. 127 */ 128 open = jest.fn(async (options?: SerialOptions) => { }); 129 130 /** 131 * A spy for closing the serial port. 132 */ 133 close = jest.fn(() => { }); 134} 135 136export class SerialMock implements Serial { 137 serialPort = new SerialPortMock(); 138 dataToDevice = this.serialPort.dataToDevice; 139 dataFromDevice = (data: Uint8Array) => { 140 this.serialPort.dataFromDevice(data); 141 }; 142 closeFromDevice = () => { 143 this.serialPort.closeFromDevice(); 144 }; 145 errorFromDevice = (error: Error) => { 146 this.serialPort.errorFromDevice(error); 147 }; 148 149 /** 150 * Request the port from the browser. 151 */ 152 async requestPort(options?: SerialPortRequestOptions) { 153 return this.serialPort; 154 } 155 156 // The rest of the methods are unimplemented 157 // and only exist to ensure SerialMock implements Serial 158 159 onconnect(): ((this: this, ev: SerialConnectionEvent) => any) | null { 160 throw new Error('Method not implemented.'); 161 } 162 163 ondisconnect(): ((this: this, ev: SerialConnectionEvent) => any) | null { 164 throw new Error('Method not implemented.'); 165 } 166 167 getPorts(): Promise<SerialPort[]> { 168 throw new Error('Method not implemented.'); 169 } 170 171 addEventListener( 172 type: 'connect' | 'disconnect', 173 listener: (this: this, ev: SerialConnectionEvent) => any, 174 useCapture?: boolean 175 ): void; 176 177 addEventListener( 178 type: string, 179 listener: EventListener | EventListenerObject | null, 180 options?: boolean | AddEventListenerOptions 181 ): void; 182 183 addEventListener(type: any, listener: any, options?: any) { 184 throw new Error('Method not implemented.'); 185 } 186 187 removeEventListener( 188 type: 'connect' | 'disconnect', 189 callback: (this: this, ev: SerialConnectionEvent) => any, 190 useCapture?: boolean 191 ): void; 192 193 removeEventListener( 194 type: string, 195 callback: EventListener | EventListenerObject | null, 196 options?: boolean | EventListenerOptions 197 ): void; 198 199 removeEventListener(type: any, callback: any, options?: any) { 200 throw new Error('Method not implemented.'); 201 } 202 203 dispatchEvent(event: Event): boolean { 204 throw new Error('Method not implemented.'); 205 } 206} 207