From 4d14ee4b31e568c35d62d1bd62a698c5924a0e6c Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Sat, 6 Mar 2021 21:37:52 +0000 Subject: [PATCH 01/11] Wire up xterm. It seems a better bet to me than hterm. I'd like to at least try it out. --- package-lock.json | 5 +++++ package.json | 3 ++- src/serial/SerialArea.tsx | 9 ++------- src/serial/XTerm.tsx | 16 ++++++++++++++++ 4 files changed, 25 insertions(+), 8 deletions(-) create mode 100644 src/serial/XTerm.tsx diff --git a/package-lock.json b/package-lock.json index 27feaf223..5751a104a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18168,6 +18168,11 @@ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, + "xterm": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.10.0.tgz", + "integrity": "sha512-Wn66I8YpSVkgP3R95GjABC6Eb21pFfnCSnyIqKIIoUI13ohvwd0KGVzUDfyEFfSAzKbPJfrT2+vt7SfUXBZQKQ==" + }, "y18n": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", diff --git a/package.json b/package.json index fad994655..bad7e41d5 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "react-scripts": "4.0.3", "react-spaces": "^0.2.0", "typescript": "^4.2.3", - "web-vitals": "^1.1.0" + "web-vitals": "^1.1.0", + "xterm": "^4.10.0" }, "scripts": { "ci": "npm run test && npm run build", diff --git a/src/serial/SerialArea.tsx b/src/serial/SerialArea.tsx index 9ff471286..fb1db7b3f 100644 --- a/src/serial/SerialArea.tsx +++ b/src/serial/SerialArea.tsx @@ -1,18 +1,13 @@ import { Flex } from "@chakra-ui/react"; import React from "react"; -import Placeholder from "../common/Placeholder"; import ProjectActionBar from "../project/ProjectActionBar"; +import XTerm from "./XTerm"; const SerialArea = () => { return ( - + ); }; diff --git a/src/serial/XTerm.tsx b/src/serial/XTerm.tsx new file mode 100644 index 000000000..453994d7e --- /dev/null +++ b/src/serial/XTerm.tsx @@ -0,0 +1,16 @@ +import { useEffect, useRef } from "react"; +import { Terminal } from "xterm"; +import "xterm/css/xterm.css"; + +const XTerm = () => { + const ref = useRef(null); + useEffect(() => { + if (ref.current) { + const terminal = new Terminal(); + terminal.open(ref.current); + } + }, []); + return
; +}; + +export default XTerm; From 766e16f0cb0077fdbb4ac2f269c539e132f3bb97 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Sat, 6 Mar 2021 21:48:30 +0000 Subject: [PATCH 02/11] Make integration points clear. --- src/serial/XTerm.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/serial/XTerm.tsx b/src/serial/XTerm.tsx index 453994d7e..4bdf40ad1 100644 --- a/src/serial/XTerm.tsx +++ b/src/serial/XTerm.tsx @@ -7,6 +7,8 @@ const XTerm = () => { useEffect(() => { if (ref.current) { const terminal = new Terminal(); + terminal.write("Data from serial written here"); + terminal.onData((data) => console.log("Send this via serial", data)); terminal.open(ref.current); } }, []); From bce6496fd684c7b3f74da5a6255dd81530e74446 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Sat, 6 Mar 2021 21:57:27 +0000 Subject: [PATCH 03/11] WIP sketch of wiring. Next step reinstate serial. --- src/serial/XTerm.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/serial/XTerm.tsx b/src/serial/XTerm.tsx index 4bdf40ad1..d8efc825b 100644 --- a/src/serial/XTerm.tsx +++ b/src/serial/XTerm.tsx @@ -1,17 +1,26 @@ import { useEffect, useRef } from "react"; import { Terminal } from "xterm"; import "xterm/css/xterm.css"; +import { EVENT_SERIAL_DATA } from "../device/device"; +import { useDevice } from "../device/device-hooks"; const XTerm = () => { + const device = useDevice(); const ref = useRef(null); useEffect(() => { if (ref.current) { const terminal = new Terminal(); - terminal.write("Data from serial written here"); - terminal.onData((data) => console.log("Send this via serial", data)); terminal.open(ref.current); + + device.on(EVENT_SERIAL_DATA, (data) => { + terminal.write(data); + }); + terminal.onData((data) => { + device.serialWrite(data); + }); + // TODO: some kind of reset event? } - }, []); + }, [device]); return
; }; From d05fd0c4b323e23be2e41fe0b40028b47d21e6f1 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Sun, 7 Mar 2021 11:38:45 +0000 Subject: [PATCH 04/11] Sketch of key moving parts. Untested. --- src/device/dap-wrapper.ts | 21 ++++++++++++++------- src/device/device.ts | 15 +++++++++++++++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/device/dap-wrapper.ts b/src/device/dap-wrapper.ts index 0ceb17a6e..de73cc047 100644 --- a/src/device/dap-wrapper.ts +++ b/src/device/dap-wrapper.ts @@ -30,12 +30,6 @@ export class DAPWrapper { this.cortexM = new CortexM(this.transport); } - private recreateDAP(): void { - this.transport = new WebUSB(this.device); - this.daplink = new DAPLink(this.transport); - this.cortexM = new CortexM(this.transport); - } - /** * The page size. Throws if we've not connected. */ @@ -70,7 +64,9 @@ export class DAPWrapper { // Only fully reconnect after the first time this object has reconnected. if (!this.reconnected) { this.reconnected = true; - this.recreateDAP(); + this.transport = new WebUSB(this.device); + this.daplink = new DAPLink(this.transport); + this.cortexM = new CortexM(this.transport); // TODO: does this make sense to reallocate then disconnect? await this.disconnectAsync(); } @@ -81,6 +77,17 @@ export class DAPWrapper { this._numPages = await this.cortexM.readMem32(FICR.CODESIZE); } + startSerial(listener: (data: string) => void): void { + this.daplink.setSerialBaudrate(115200); + this.daplink.on(DAPLink.EVENT_SERIAL_DATA, listener); + this.daplink.startSerialRead(1); + } + + stopSerial(listener: (data: string) => void): void { + this.daplink.stopSerialRead(); + this.daplink.removeListener(DAPLink.EVENT_SERIAL_DATA, listener); + } + async disconnectAsync(): Promise { if ( this.device.opened && diff --git a/src/device/device.ts b/src/device/device.ts index 3f3a10bf7..0c6c14867 100644 --- a/src/device/device.ts +++ b/src/device/device.ts @@ -1,3 +1,4 @@ +import { DAPLink } from "dapjs"; import EventEmitter from "events"; import { FlashDataSource } from "../fs/fs"; import translation from "../translation"; @@ -119,6 +120,10 @@ export class MicrobitWebUSBConnection extends EventEmitter { */ private connection: DAPWrapper | undefined; + private serialListener = (data: string) => { + this.emit(EVENT_SERIAL_DATA, data); + }; + async initialize(): Promise { if (navigator.usb) { navigator.usb.addEventListener("disconnect", this.handleDisconnect); @@ -177,6 +182,7 @@ export class MicrobitWebUSBConnection extends EventEmitter { if (!this.connection) { throw new Error("Must be connected now"); } + this.connection.stopSerial(this.serialListener); const partial = options.partial; const progress = options.progress || (() => {}); @@ -191,6 +197,7 @@ export class MicrobitWebUSBConnection extends EventEmitter { } else { await flashing.fullFlashAsync(data.intelHex, progress); } + this.connection.startSerial(this.serialListener); } finally { progress(undefined); } @@ -239,6 +246,12 @@ export class MicrobitWebUSBConnection extends EventEmitter { } } + serialWrite(data: string): void { + if (this.connection) { + this.connection.daplink.serialWrite(data); + } + } + private handleDisconnect = (event: USBConnectionEvent) => { if (event.device === this.device) { log("Disconnect event"); @@ -256,6 +269,8 @@ export class MicrobitWebUSBConnection extends EventEmitter { this.connection = new DAPWrapper(device); } await this.connection.reconnectAsync(); + this.connection.startSerial(this.serialListener); + return ConnectionStatus.CONNECTED; } From 44046ecb845e7116af225113c618b7535ee6b884 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Sun, 7 Mar 2021 14:32:46 +0000 Subject: [PATCH 05/11] Terminal resize support. --- package-lock.json | 5 +++++ package.json | 3 ++- src/serial/SerialArea.tsx | 2 +- src/serial/XTerm.tsx | 46 +++++++++++++++++++++++++++++---------- 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5751a104a..649930f8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18173,6 +18173,11 @@ "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.10.0.tgz", "integrity": "sha512-Wn66I8YpSVkgP3R95GjABC6Eb21pFfnCSnyIqKIIoUI13ohvwd0KGVzUDfyEFfSAzKbPJfrT2+vt7SfUXBZQKQ==" }, + "xterm-addon-fit": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz", + "integrity": "sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==" + }, "y18n": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", diff --git a/package.json b/package.json index bad7e41d5..517e9ede6 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "react-spaces": "^0.2.0", "typescript": "^4.2.3", "web-vitals": "^1.1.0", - "xterm": "^4.10.0" + "xterm": "^4.10.0", + "xterm-addon-fit": "^0.5.0" }, "scripts": { "ci": "npm run test && npm run build", diff --git a/src/serial/SerialArea.tsx b/src/serial/SerialArea.tsx index fb1db7b3f..753ca3e37 100644 --- a/src/serial/SerialArea.tsx +++ b/src/serial/SerialArea.tsx @@ -7,7 +7,7 @@ const SerialArea = () => { return ( - + ); }; diff --git a/src/serial/XTerm.tsx b/src/serial/XTerm.tsx index d8efc825b..0809f88a5 100644 --- a/src/serial/XTerm.tsx +++ b/src/serial/XTerm.tsx @@ -1,27 +1,51 @@ +import { Box, BoxProps } from "@chakra-ui/layout"; import { useEffect, useRef } from "react"; import { Terminal } from "xterm"; import "xterm/css/xterm.css"; import { EVENT_SERIAL_DATA } from "../device/device"; import { useDevice } from "../device/device-hooks"; +import { FitAddon } from "xterm-addon-fit"; +import useIsUnmounted from "../common/use-is-unmounted"; -const XTerm = () => { +const XTerm = (props: BoxProps) => { const device = useDevice(); const ref = useRef(null); + const isUnmounted = useIsUnmounted(); useEffect(() => { - if (ref.current) { - const terminal = new Terminal(); - terminal.open(ref.current); + if (ref.current && !isUnmounted()) { + const term = new Terminal({}); + const fitAddon = new FitAddon(); + term.loadAddon(fitAddon); + term.open(ref.current); - device.on(EVENT_SERIAL_DATA, (data) => { - terminal.write(data); + // Watch for resize and change terminal rows/cols accordingly. + // This can result in a slither of space at the bottom, so backgrounds + // should match. + const resizeObserver = new ResizeObserver((entries) => { + if (!Array.isArray(entries) || !entries.length) { + return; + } + fitAddon.fit(); }); - terminal.onData((data) => { - device.serialWrite(data); - }); - // TODO: some kind of reset event? + resizeObserver.observe(ref.current); + + // Input/output data. + const serialListener = (data: string) => { + term.write(data); + }; + device.on(EVENT_SERIAL_DATA, serialListener); + term.onData(device.serialWrite.bind(device)); + // TODO: some kind of reset event, to clear the terminal? + // or depend on the connection status? + + return () => { + device.removeListener(EVENT_SERIAL_DATA, serialListener); + resizeObserver.disconnect(); + term.dispose(); + }; } }, [device]); - return
; + return ; }; export default XTerm; From abbb194b1e1cbc749e69ec6135e0d27fd4b944fa Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Mon, 8 Mar 2021 11:50:45 +0000 Subject: [PATCH 06/11] Bigger --- src/workbench/Workbench.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/workbench/Workbench.tsx b/src/workbench/Workbench.tsx index 805a8354d..13932b2b5 100644 --- a/src/workbench/Workbench.tsx +++ b/src/workbench/Workbench.tsx @@ -30,7 +30,7 @@ const Workbench = () => { /> From 2414c4050c7b8e06f6b7cc3b524ee1baddba6b50 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Mon, 8 Mar 2021 20:53:07 +0000 Subject: [PATCH 07/11] Update to new main. Interaction between flashing and serial is totally broken and needs review. --- src/device/dap-wrapper.ts | 6 +++--- src/device/device.ts | 37 +++++++++++--------------------- src/project/DeviceConnection.tsx | 8 ++----- src/serial/XTerm.tsx | 2 +- 4 files changed, 18 insertions(+), 35 deletions(-) diff --git a/src/device/dap-wrapper.ts b/src/device/dap-wrapper.ts index de73cc047..276a1c971 100644 --- a/src/device/dap-wrapper.ts +++ b/src/device/dap-wrapper.ts @@ -77,10 +77,10 @@ export class DAPWrapper { this._numPages = await this.cortexM.readMem32(FICR.CODESIZE); } - startSerial(listener: (data: string) => void): void { - this.daplink.setSerialBaudrate(115200); + async startSerial(listener: (data: string) => void): Promise { + await this.daplink.setSerialBaudrate(115200); this.daplink.on(DAPLink.EVENT_SERIAL_DATA, listener); - this.daplink.startSerialRead(1); + await this.daplink.startSerialRead(1); } stopSerial(listener: (data: string) => void): void { diff --git a/src/device/device.ts b/src/device/device.ts index 0c6c14867..b9719628c 100644 --- a/src/device/device.ts +++ b/src/device/device.ts @@ -1,4 +1,3 @@ -import { DAPLink } from "dapjs"; import EventEmitter from "events"; import { FlashDataSource } from "../fs/fs"; import translation from "../translation"; @@ -84,20 +83,6 @@ export enum ConnectionStatus { CONNECTED = "CONNECTED", } -/** - * Controls whether a request to connect can prompt the user. - */ -export enum ConnectionMode { - /** - * Prompt the user to connect if required. - */ - INTERACTIVE, - /** - * Connect only to a pre-approved device without prompting the user. - */ - NON_INTERACTIVE, -} - export const EVENT_STATUS = "status"; export const EVENT_SERIAL_DATA = "serial_data"; export const EVENT_SERIAL_ERROR = "serial_error"; @@ -147,9 +132,9 @@ export class MicrobitWebUSBConnection extends EventEmitter { * @param interactive whether we can prompt the user to choose a device. * @returns the final connection status. */ - async connect(mode: ConnectionMode): Promise { + async connect(): Promise { return this.withEnrichedErrors(async () => { - this.setStatus(await this.connectInternal(mode)); + await this.connectInternal(true); return this.status; }); } @@ -174,15 +159,16 @@ export class MicrobitWebUSBConnection extends EventEmitter { } ): Promise { if (!this.connection) { - await this.connect(ConnectionMode.INTERACTIVE); + await this.connectInternal(false); } else { // TODO: Maybe reinstate v2's timeout here. + // Do we need to stop serial? + this.connection.stopSerial(this.serialListener); await this.connection.reconnectAsync(); } if (!this.connection) { throw new Error("Must be connected now"); } - this.connection.stopSerial(this.serialListener); const partial = options.partial; const progress = options.progress || (() => {}); @@ -197,6 +183,8 @@ export class MicrobitWebUSBConnection extends EventEmitter { } else { await flashing.fullFlashAsync(data.intelHex, progress); } + + // Reinstate serial. Is this quick enough to capture output? this.connection.startSerial(this.serialListener); } finally { progress(undefined); @@ -261,17 +249,16 @@ export class MicrobitWebUSBConnection extends EventEmitter { } }; - private async connectInternal( - mode: ConnectionMode - ): Promise { + private async connectInternal(serial: boolean): Promise { if (!this.connection) { const device = await this.chooseDevice(); this.connection = new DAPWrapper(device); } await this.connection.reconnectAsync(); - this.connection.startSerial(this.serialListener); - - return ConnectionStatus.CONNECTED; + if (serial) { + this.connection.startSerial(this.serialListener); + } + this.setStatus(ConnectionStatus.CONNECTED); } private async chooseDevice(): Promise { diff --git a/src/project/DeviceConnection.tsx b/src/project/DeviceConnection.tsx index 44497153b..577ccea20 100644 --- a/src/project/DeviceConnection.tsx +++ b/src/project/DeviceConnection.tsx @@ -4,11 +4,7 @@ import Separate from "../common/Separate"; import useActionFeedback, { ActionFeedback, } from "../common/use-action-feedback"; -import { - ConnectionMode, - ConnectionStatus, - WebUSBError, -} from "../device/device"; +import { ConnectionStatus, WebUSBError } from "../device/device"; import { useConnectionStatus, useDevice } from "../device/device-hooks"; import DownloadButton from "./DownloadButton"; import FlashButton from "./FlashButton"; @@ -36,7 +32,7 @@ const DeviceConnection = () => { }); } else { try { - await device.connect(ConnectionMode.INTERACTIVE); + await device.connect(); } catch (e) { handleWebUSBError(actionFeedback, e); } diff --git a/src/serial/XTerm.tsx b/src/serial/XTerm.tsx index 0809f88a5..a3b2e404c 100644 --- a/src/serial/XTerm.tsx +++ b/src/serial/XTerm.tsx @@ -44,7 +44,7 @@ const XTerm = (props: BoxProps) => { term.dispose(); }; } - }, [device]); + }, [device, isUnmounted]); return ; }; From 66b65a089f0a830c76f5f5269196ccb5c76fa2c6 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Tue, 9 Mar 2021 14:26:22 +0000 Subject: [PATCH 08/11] Serial experience works OK on the happy path. Probably still broken in other cases but it's a useful sketch of the interaction. --- src/device/dap-wrapper.ts | 14 +++++------ src/device/device.ts | 43 +++++++++++++++++++++++--------- src/project/DeviceConnection.tsx | 6 ++++- src/serial/XTerm.tsx | 30 ++++++++++++++++------ 4 files changed, 65 insertions(+), 28 deletions(-) diff --git a/src/device/dap-wrapper.ts b/src/device/dap-wrapper.ts index 276a1c971..17872d9f5 100644 --- a/src/device/dap-wrapper.ts +++ b/src/device/dap-wrapper.ts @@ -22,7 +22,7 @@ export class DAPWrapper { _pageSize: number | undefined; _numPages: number | undefined; - private reconnected: boolean = false; + private initialConnectionComplete: boolean = false; constructor(public device: USBDevice) { this.transport = new WebUSB(this.device); @@ -61,18 +61,18 @@ export class DAPWrapper { // Drawn from https://github.com/microsoft/pxt-microbit/blob/dec5b8ce72d5c2b4b0b20aafefce7474a6f0c7b2/editor/extension.tsx#L119 async reconnectAsync(): Promise { - // Only fully reconnect after the first time this object has reconnected. - if (!this.reconnected) { - this.reconnected = true; + if (this.initialConnectionComplete) { + await this.disconnectAsync(); + this.transport = new WebUSB(this.device); this.daplink = new DAPLink(this.transport); this.cortexM = new CortexM(this.transport); - // TODO: does this make sense to reallocate then disconnect? - await this.disconnectAsync(); + } else { + this.initialConnectionComplete = true; } + await this.daplink.connect(); await this.cortexM.connect(); - this._pageSize = await this.cortexM.readMem32(FICR.CODEPAGESIZE); this._numPages = await this.cortexM.readMem32(FICR.CODESIZE); } diff --git a/src/device/device.ts b/src/device/device.ts index b9719628c..026083f6d 100644 --- a/src/device/device.ts +++ b/src/device/device.ts @@ -85,6 +85,7 @@ export enum ConnectionStatus { export const EVENT_STATUS = "status"; export const EVENT_SERIAL_DATA = "serial_data"; +export const EVENT_SERIAL_RESET = "serial_reset"; export const EVENT_SERIAL_ERROR = "serial_error"; /** @@ -151,6 +152,13 @@ export class MicrobitWebUSBConnection extends EventEmitter { ); } + private async stopSerial() { + if (this.connection) { + this.connection.stopSerial(this.serialListener); + } + this.emit(EVENT_SERIAL_RESET, {}); + } + private async flashInternal( dataSource: FlashDataSource, options: { @@ -161,9 +169,9 @@ export class MicrobitWebUSBConnection extends EventEmitter { if (!this.connection) { await this.connectInternal(false); } else { - // TODO: Maybe reinstate v2's timeout here. - // Do we need to stop serial? - this.connection.stopSerial(this.serialListener); + log("Stopping serial before flash"); + this.stopSerial(); + log("Reconnecting before flash"); await this.connection.reconnectAsync(); } if (!this.connection) { @@ -184,8 +192,15 @@ export class MicrobitWebUSBConnection extends EventEmitter { await flashing.fullFlashAsync(data.intelHex, progress); } - // Reinstate serial. Is this quick enough to capture output? - this.connection.startSerial(this.serialListener); + // Can we avoid doing this? Is there a chance we miss program output? + log("Reinstating serial after flash"); + await this.connection.reconnectAsync(); + + // This is async but won't return until we've finished serial. + // TODO: consider error handling here. + this.connection + .startSerial(this.serialListener) + .then(() => log("Finished listening for serial data")); } finally { progress(undefined); } @@ -197,15 +212,15 @@ export class MicrobitWebUSBConnection extends EventEmitter { async disconnect(): Promise { try { if (this.connection) { - const old = this.connection; - this.connection = undefined; - await old.disconnectAsync(); - log("Disconnection complete"); + this.stopSerial(); + this.connection.disconnectAsync(); } } catch (e) { log("Error during disconnection:\r\n" + e); + } finally { + this.connection = undefined; + this.setStatus(ConnectionStatus.NOT_CONNECTED); } - this.setStatus(ConnectionStatus.NOT_CONNECTED); } private setStatus(newStatus: ConnectionStatus) { @@ -236,13 +251,17 @@ export class MicrobitWebUSBConnection extends EventEmitter { serialWrite(data: string): void { if (this.connection) { - this.connection.daplink.serialWrite(data); + try { + this.connection.daplink.serialWrite(data); + } catch (e) { + console.log("Serial write error"); + console.error(e); + } } } private handleDisconnect = (event: USBConnectionEvent) => { if (event.device === this.device) { - log("Disconnect event"); this.connection = undefined; this.device = undefined; this.setStatus(ConnectionStatus.NO_AUTHORIZED_DEVICE); diff --git a/src/project/DeviceConnection.tsx b/src/project/DeviceConnection.tsx index 577ccea20..0139a0167 100644 --- a/src/project/DeviceConnection.tsx +++ b/src/project/DeviceConnection.tsx @@ -23,7 +23,11 @@ const DeviceConnection = () => { const device = useDevice(); const handleToggleConnected = useCallback(async () => { if (connected) { - await device.disconnect(); + try { + await device.disconnect(); + } catch (e) { + handleWebUSBError(actionFeedback, e); + } } else { if (!supported) { actionFeedback.expectedError({ diff --git a/src/serial/XTerm.tsx b/src/serial/XTerm.tsx index a3b2e404c..6b3ab1cbb 100644 --- a/src/serial/XTerm.tsx +++ b/src/serial/XTerm.tsx @@ -1,19 +1,23 @@ import { Box, BoxProps } from "@chakra-ui/layout"; import { useEffect, useRef } from "react"; import { Terminal } from "xterm"; -import "xterm/css/xterm.css"; -import { EVENT_SERIAL_DATA } from "../device/device"; -import { useDevice } from "../device/device-hooks"; import { FitAddon } from "xterm-addon-fit"; +import "xterm/css/xterm.css"; import useIsUnmounted from "../common/use-is-unmounted"; +import { EVENT_SERIAL_DATA, EVENT_SERIAL_RESET } from "../device/device"; +import { useDevice } from "../device/device-hooks"; const XTerm = (props: BoxProps) => { const device = useDevice(); + const ref = useRef(null); const isUnmounted = useIsUnmounted(); useEffect(() => { if (ref.current && !isUnmounted()) { - const term = new Terminal({}); + const term = new Terminal({ + fontFamily: "monospace", + fontSize: 18, + }); const fitAddon = new FitAddon(); term.loadAddon(fitAddon); term.open(ref.current); @@ -31,20 +35,30 @@ const XTerm = (props: BoxProps) => { // Input/output data. const serialListener = (data: string) => { - term.write(data); + try { + term.write(data); + } catch (e) { + console.log("Serial error"); + console.error(e); + } }; device.on(EVENT_SERIAL_DATA, serialListener); + const resetListener = () => { + term.reset(); + }; + device.on(EVENT_SERIAL_RESET, resetListener); term.onData(device.serialWrite.bind(device)); - // TODO: some kind of reset event, to clear the terminal? - // or depend on the connection status? - return () => { + device.removeListener(EVENT_SERIAL_RESET, resetListener); device.removeListener(EVENT_SERIAL_DATA, serialListener); resizeObserver.disconnect(); term.dispose(); }; } }, [device, isUnmounted]); + + // The terminal itself is sized based on the number of rows, + // so we need a background that matches the theme. return ; }; From ce9c25821aa3fdfedd0f83c90bb636e9ae95d269 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Tue, 9 Mar 2021 14:31:57 +0000 Subject: [PATCH 09/11] Tweak error handling. --- src/device/device.ts | 13 +++++-------- src/serial/XTerm.tsx | 7 +------ 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/device/device.ts b/src/device/device.ts index 026083f6d..d60b1c895 100644 --- a/src/device/device.ts +++ b/src/device/device.ts @@ -197,7 +197,7 @@ export class MicrobitWebUSBConnection extends EventEmitter { await this.connection.reconnectAsync(); // This is async but won't return until we've finished serial. - // TODO: consider error handling here. + // TODO: consider error handling here, via an event? this.connection .startSerial(this.serialListener) .then(() => log("Finished listening for serial data")); @@ -249,15 +249,12 @@ export class MicrobitWebUSBConnection extends EventEmitter { } } - serialWrite(data: string): void { - if (this.connection) { - try { + serialWrite(data: string): Promise { + return this.withEnrichedErrors(async () => { + if (this.connection) { this.connection.daplink.serialWrite(data); - } catch (e) { - console.log("Serial write error"); - console.error(e); } - } + }); } private handleDisconnect = (event: USBConnectionEvent) => { diff --git a/src/serial/XTerm.tsx b/src/serial/XTerm.tsx index 6b3ab1cbb..de4bba20d 100644 --- a/src/serial/XTerm.tsx +++ b/src/serial/XTerm.tsx @@ -35,12 +35,7 @@ const XTerm = (props: BoxProps) => { // Input/output data. const serialListener = (data: string) => { - try { - term.write(data); - } catch (e) { - console.log("Serial error"); - console.error(e); - } + term.write(data); }; device.on(EVENT_SERIAL_DATA, serialListener); const resetListener = () => { From 48bdeca4380ab5c6f3572579d9d6b831ad54f082 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Tue, 9 Mar 2021 16:06:21 +0000 Subject: [PATCH 10/11] Enough jest/jsdom setup to keep the terminal happy --- package-lock.json | 436 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 1 + src/setupTests.ts | 20 ++- 3 files changed, 454 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 649930f8a..4682c844f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3982,6 +3982,12 @@ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==" }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -4116,6 +4122,42 @@ "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -5259,6 +5301,17 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001197.tgz", "integrity": "sha512-8aE+sqBqtXz4G8g35Eg/XEaFr2N7rd/VQ6eABGBmNtcB8cN6qNJhMi6oSFy4UWWZgqgL3filHT8Nha4meu3tsw==" }, + "canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.7.0.tgz", + "integrity": "sha512-pzCxtkHb+5su5MQjTtepMDlIOtaXo277x0C0u3nMOxtkhTyQ+h2yNKhlROAaDllWgRyePAUitC08sXw26Eb6aw==", + "dev": true, + "requires": { + "nan": "^2.14.0", + "node-pre-gyp": "^0.15.0", + "simple-get": "^3.0.3" + } + }, "capture-exit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", @@ -5418,6 +5471,12 @@ "q": "^1.1.2" } }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, "collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -5601,6 +5660,12 @@ "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==" }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, "constants-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", @@ -6197,6 +6262,15 @@ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, + "decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "dev": true, + "requires": { + "mimic-response": "^2.0.0" + } + }, "dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -6215,6 +6289,12 @@ "regexp.prototype.flags": "^1.2.0" } }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -6340,6 +6420,12 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -6364,6 +6450,12 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "dev": true + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -8272,6 +8364,59 @@ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -8452,6 +8597,12 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -8921,6 +9072,15 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==" }, + "ignore-walk": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", + "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", + "dev": true, + "requires": { + "minimatch": "^3.0.4" + } + }, "immer": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", @@ -11457,6 +11617,12 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, + "mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "dev": true + }, "min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -11656,8 +11822,7 @@ "nan": { "version": "2.14.2", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", - "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", - "optional": true + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" }, "nanoid": { "version": "3.1.20", @@ -11695,6 +11860,28 @@ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" }, + "needle": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.6.0.tgz", + "integrity": "sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg==", + "dev": true, + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -11832,11 +12019,111 @@ } } }, + "node-pre-gyp": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.15.0.tgz", + "integrity": "sha512-7QcZa8/fpaU/BKenjcaeFF9hLz2+7S9AqyXFhlH/rilsQ/hPZKK32RtR5EQHJElgu+q5RfbJ34KriI79UWaorA==", + "dev": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.3", + "needle": "^2.5.0", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4.4.2" + }, + "dependencies": { + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "dev": true, + "requires": { + "minipass": "^2.6.0" + } + }, + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "dev": true, + "requires": { + "minipass": "^2.9.0" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "tar": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, "node-releases": { "version": "1.1.71", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.71.tgz", "integrity": "sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==" }, + "nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dev": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -11876,6 +12163,32 @@ "sort-keys": "^1.0.0" } }, + "npm-bundled": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", + "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", + "dev": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true + }, + "npm-packlist": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", + "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", + "dev": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -11884,6 +12197,18 @@ "path-key": "^2.0.0" } }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, "nrf-intel-hex": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/nrf-intel-hex/-/nrf-intel-hex-1.3.0.tgz", @@ -11902,6 +12227,12 @@ "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=" }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, "nwsapi": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", @@ -12127,6 +12458,28 @@ "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=" }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, "p-each-series": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", @@ -13782,6 +14135,26 @@ } } }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + } + } + }, "react": { "version": "17.0.1", "resolved": "https://registry.npmjs.org/react/-/react-17.0.1.tgz", @@ -15135,6 +15508,23 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true + }, + "simple-get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", + "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", + "dev": true, + "requires": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -17881,6 +18271,48 @@ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", diff --git a/package.json b/package.json index 517e9ede6..8c25c58dd 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ ] }, "devDependencies": { + "canvas": "^2.7.0", "prettier": "^2.2.1" } } diff --git a/src/setupTests.ts b/src/setupTests.ts index 8f2609b7b..f4ec639ce 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -2,4 +2,22 @@ // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom'; +import "@testing-library/jest-dom"; + +global.matchMedia = + global.matchMedia || + function () { + return { + matches: false, + addListener: jest.fn(), + removeListener: jest.fn(), + }; + }; + +class MockResizeObserver { + observe = jest.fn(); + unobserve = jest.fn(); + disconnect = jest.fn(); +} + +global.ResizeObserver = global.ResizeObserver || MockResizeObserver; From 1df96b500881ddf6140f3d9bf6f37e413f00b5d2 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Tue, 9 Mar 2021 16:34:52 +0000 Subject: [PATCH 11/11] Don't set NODE_ENV=production It means we miss out on dev depenencies that we need for the tests. It's not relevant to the final built artifact as `npm run build` forces it to be production (it's a CRA feature). Explains the CI-only failure due to canvas lib. --- netlify.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/netlify.toml b/netlify.toml index 93e577e03..79bcd1aa9 100644 --- a/netlify.toml +++ b/netlify.toml @@ -3,7 +3,6 @@ command = "npm run ci" [build.environment] CI = "true" - NODE_ENV = "production" [[redirects]] from = "/*" to = "/index.html"