pw_web_ui: Create mocks for testing WebSerial
Node and the browser have different stream APIs. This CL creates mocks for testing web_serial_transport, which uses the browser API.
Change-Id: Ice382bb89b10a2fa23fd4a39f9217e1d0c32a9cd
diff --git a/package.json b/package.json
index e5a1881..cdda168 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"@bazel/typescript": "^1.7.0",
"@types/jasmine": "^3.5.10",
"jasmine": "^3.5.0",
+ "rxjs": "^6.5.5",
"typescript": "^3.9.3"
}
}
diff --git a/pw_web_ui/src/transport/BUILD b/pw_web_ui/src/transport/BUILD
index a0387f3..8af9cb8 100644
--- a/pw_web_ui/src/transport/BUILD
+++ b/pw_web_ui/src/transport/BUILD
@@ -19,32 +19,61 @@
ts_library(
- name = "transport_lib",
+ name = "device_transport_lib",
srcs = [
- "web_serial_transport.ts",
+ "device_transport.ts",
],
- deps = [],
+ deps = [
+ "@npm//rxjs",
+ ],
)
ts_library(
- name = "transport_lib_test",
+ name = "serial_mock_lib",
+ srcs = [
+ "serial_mock.ts",
+ ],
+ deps = [
+ "//pw_web_ui/types:serial_lib",
+ "@npm//jasmine",
+ "@npm//@types/jasmine",
+ "@npm//rxjs",
+ ],
+)
+
+ts_library(
+ name = "web_serial_transport_lib",
+ srcs = [
+ "web_serial_transport.ts",
+ ],
+ deps = [
+ ":device_transport_lib",
+ "//pw_web_ui/types:serial_lib",
+ "@npm//rxjs",
+ ],
+)
+
+ts_library(
+ name = "web_serial_transport_lib_test",
testonly = True,
srcs = [
"web_serial_transport_test.ts",
],
deps = [
- ":transport_lib",
+ ":serial_mock_lib",
+ ":web_serial_transport_lib",
"@npm//jasmine",
"@npm//@types/jasmine",
+ "@npm//rxjs",
],
)
jasmine_node_test(
name = "test",
srcs = [
- ":transport_lib_test",
+ ":web_serial_transport_lib_test",
],
deps = [
- ":transport_lib",
+ ":web_serial_transport_lib",
],
)
diff --git a/pw_web_ui/src/transport/serial_mock.ts b/pw_web_ui/src/transport/serial_mock.ts
new file mode 100644
index 0000000..38557c5
--- /dev/null
+++ b/pw_web_ui/src/transport/serial_mock.ts
@@ -0,0 +1,193 @@
+// Copyright 2020 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.
+
+import { Subject } from 'rxjs';
+
+
+/**
+ * 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_ui 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 = jasmine.createSpy('openSpy', async (options?: SerialOptions) => { });
+
+ /**
+ * A spy for closing the serial port.
+ */
+ close = jasmine.createSpy('closeSpy', () => { });
+}
+
+
+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.');
+ }
+}
diff --git a/yarn.lock b/yarn.lock
index 50bb783..10152b9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -691,6 +691,13 @@
dependencies:
glob "^7.1.3"
+rxjs@^6.5.5:
+ version "6.5.5"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec"
+ integrity sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==
+ dependencies:
+ tslib "^1.9.0"
+
"semver@2 || 3 || 4 || 5", semver@^5.5.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
@@ -832,7 +839,7 @@
read-pkg-up "^4.0.0"
require-main-filename "^2.0.0"
-tslib@^1.8.1:
+tslib@^1.8.1, tslib@^1.9.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==