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 { 18 SerialConnectionEvent, 19 SerialPort, 20 Serial, 21 SerialPortRequestOptions, 22 SerialOptions, 23} from 'pigweedjs/types/serial'; 24/** 25 * AsyncQueue is a queue that allows values to be dequeued 26 * before they are enqueued, returning a promise that resolves 27 * once the value is available. 28 */ 29class AsyncQueue<T> { 30 private queue: T[] = []; 31 private requestQueue: Array<(val: T) => unknown> = []; 32 33 /** 34 * Enqueue val into the queue. 35 * @param {T} val 36 */ 37 enqueue(val: T) { 38 const callback = this.requestQueue.shift(); 39 if (callback) { 40 callback(val); 41 } else { 42 this.queue.push(val); 43 } 44 } 45 46 /** 47 * Dequeue a value from the queue, returning a promise 48 * if the queue is empty. 49 */ 50 async dequeue(): Promise<T> { 51 const val = this.queue.shift(); 52 if (val !== undefined) { 53 return val; 54 } else { 55 const queuePromise = new Promise<T>((resolve) => { 56 this.requestQueue.push(resolve); 57 }); 58 return queuePromise; 59 } 60 } 61} 62 63/** 64 * SerialPortMock is a mock for Chrome's upcoming SerialPort interface. 65 * Since pw_web only depends on a subset of the interface, this mock 66 * only implements that subset. 67 */ 68class SerialPortMock implements SerialPort { 69 private deviceData = new AsyncQueue<{ 70 data?: Uint8Array; 71 done?: boolean; 72 error?: Error; 73 }>(); 74 75 /** 76 * Simulate the device sending data to the browser. 77 * @param {Uint8Array} data 78 */ 79 dataFromDevice(data: Uint8Array) { 80 this.deviceData.enqueue({ data }); 81 } 82 83 /** 84 * Simulate the device closing the connection with the browser. 85 */ 86 closeFromDevice() { 87 this.deviceData.enqueue({ done: true }); 88 } 89 90 /** 91 * Simulate an error in the device's read stream. 92 * @param {Error} error 93 */ 94 errorFromDevice(error: Error) { 95 this.deviceData.enqueue({ error }); 96 } 97 98 /** 99 * An rxjs subject tracking data sent to the (fake) device. 100 */ 101 dataToDevice = new Subject<Uint8Array>(); 102 103 /** 104 * The ReadableStream of bytes from the device. 105 */ 106 readable = new ReadableStream<Uint8Array>({ 107 pull: async (controller) => { 108 const { data, done, error } = await this.deviceData.dequeue(); 109 if (done) { 110 controller.close(); 111 return; 112 } 113 if (error) { 114 throw error; 115 } 116 if (data) { 117 controller.enqueue(data); 118 } 119 }, 120 }); 121 122 /** 123 * The WritableStream of bytes to the device. 124 */ 125 writable = new WritableStream<Uint8Array>({ 126 write: (chunk) => { 127 this.dataToDevice.next(chunk); 128 }, 129 }); 130 131 /** 132 * A spy for opening the serial port. 133 */ 134 open = jest.fn(async (options?: SerialOptions) => { 135 // Do nothing. 136 }); 137 138 /** 139 * A spy for closing the serial port. 140 */ 141 close = jest.fn(() => { 142 // Do nothing. 143 }); 144} 145 146export class SerialMock implements Serial { 147 serialPort = new SerialPortMock(); 148 dataToDevice = this.serialPort.dataToDevice; 149 dataFromDevice = (data: Uint8Array) => { 150 this.serialPort.dataFromDevice(data); 151 }; 152 closeFromDevice = () => { 153 this.serialPort.closeFromDevice(); 154 }; 155 errorFromDevice = (error: Error) => { 156 this.serialPort.errorFromDevice(error); 157 }; 158 159 /** 160 * Request the port from the browser. 161 */ 162 async requestPort(options?: SerialPortRequestOptions) { 163 return this.serialPort; 164 } 165 166 // The rest of the methods are unimplemented 167 // and only exist to ensure SerialMock implements Serial 168 169 onconnect(): ((this: this, ev: SerialConnectionEvent) => any) | null { 170 throw new Error('Method not implemented.'); 171 } 172 173 ondisconnect(): ((this: this, ev: SerialConnectionEvent) => any) | null { 174 throw new Error('Method not implemented.'); 175 } 176 177 getPorts(): Promise<SerialPort[]> { 178 throw new Error('Method not implemented.'); 179 } 180 181 addEventListener( 182 type: 'connect' | 'disconnect', 183 listener: (this: this, ev: SerialConnectionEvent) => any, 184 useCapture?: boolean, 185 ): void; 186 187 addEventListener( 188 type: string, 189 listener: EventListener | EventListenerObject | null, 190 options?: boolean | AddEventListenerOptions, 191 ): void; 192 193 addEventListener(type: any, listener: any, options?: any) { 194 console.info('Silently skipping event listeners in mock mode.'); 195 } 196 197 removeEventListener( 198 type: 'connect' | 'disconnect', 199 callback: (this: this, ev: SerialConnectionEvent) => any, 200 useCapture?: boolean, 201 ): void; 202 203 removeEventListener( 204 type: string, 205 callback: EventListener | EventListenerObject | null, 206 options?: boolean | EventListenerOptions, 207 ): void; 208 209 removeEventListener(type: any, callback: any, options?: any) { 210 throw new Error('Method not implemented.'); 211 } 212 213 dispatchEvent(event: Event): boolean { 214 throw new Error('Method not implemented.'); 215 } 216} 217