Skip to content

Commit 8e472c3

Browse files
First-cut new session, connection, and testing model page E2E tests (#583)
* Add timeout for GitHub workflow. * New session/page tests. * Upgraded microbit-connection lib to latest. * Sets up cookies for triggering mocking of devices for E2E. * Mock of usb, bluetooth, and radio bridge connection * Bluetooth connection E2E test - happy flow and device not selected flow. * Radio bridge connection E2E test - happy flow and device not selected flow. * Initial testing model page and MakeCode editor test.
1 parent d0eb461 commit 8e472c3

24 files changed

+1371
-42
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ concurrency:
1313

1414
jobs:
1515
build:
16+
timeout-minutes: 10
1617
runs-on: ubuntu-latest
1718
permissions:
1819
contents: read

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"@emotion/react": "^11.11.4",
6363
"@emotion/styled": "^11.11.5",
6464
"@microbit/makecode-embed": "^0.1.0",
65-
"@microbit/microbit-connection": "^0.0.0-alpha.33",
65+
"@microbit/microbit-connection": "^0.0.0-alpha.35",
6666
"@microbit/ml-header-generator": "^0.4.3",
6767
"@microbit/smoothie": "^1.37.0-microbit.2",
6868
"@tensorflow/tfjs": "^4.20.0",

src/App.tsx

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,21 @@
77
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
88
import { ChakraProvider, useToast } from "@chakra-ui/react";
99
import { MakeCodeFrameDriver } from "@microbit/makecode-embed/react";
10+
import {
11+
createRadioBridgeConnection,
12+
createWebBluetoothConnection,
13+
createWebUSBConnection,
14+
} from "@microbit/microbit-connection";
1015
import React, { ReactNode, useEffect, useMemo, useRef } from "react";
1116
import { useIntl } from "react-intl";
1217
import {
18+
createBrowserRouter,
1319
Outlet,
1420
RouterProvider,
1521
ScrollRestoration,
16-
createBrowserRouter,
1722
useNavigate,
1823
} from "react-router-dom";
24+
import "theme-package/fonts/fonts.css";
1925
import { BufferedDataProvider } from "./buffered-data-hooks";
2026
import EditCodeDialog from "./components/EditCodeDialog";
2127
import ErrorBoundary from "./components/ErrorBoundary";
@@ -25,9 +31,14 @@ import { ConnectProvider } from "./connect-actions-hooks";
2531
import { ConnectStatusProvider } from "./connect-status-hooks";
2632
import { ConnectionStageProvider } from "./connection-stage-hooks";
2733
import { deployment, useDeployment } from "./deployment";
34+
import { MockWebBluetoothConnection } from "./device/mockBluetooth";
35+
import { MockRadioBridgeConnection } from "./device/mockRadioBridge";
36+
import { MockWebUSBConnection } from "./device/mockUsb";
2837
import { ProjectProvider } from "./hooks/project-hooks";
2938
import { LoggingProvider } from "./logging/logging-hooks";
39+
import { hasMakeCodeMlExtension } from "./makecode/utils";
3040
import TranslationProvider from "./messages/TranslationProvider";
41+
import { PostImportDialogState } from "./model";
3142
import CodePage from "./pages/CodePage";
3243
import DataSamplesPage from "./pages/DataSamplesPage";
3344
import HomePage from "./pages/HomePage";
@@ -43,16 +54,29 @@ import {
4354
createNewPageUrl,
4455
createTestingModelPageUrl,
4556
} from "./urls";
46-
import { hasMakeCodeMlExtension } from "./makecode/utils";
47-
import { PostImportDialogState } from "./model";
48-
import "theme-package/fonts/fonts.css";
4957

5058
export interface ProviderLayoutProps {
5159
children: ReactNode;
5260
}
5361

62+
const isMockDeviceMode = () =>
63+
// We use a cookie set from the e2e tests. Avoids having separate test and live builds.
64+
Boolean(
65+
document.cookie.split("; ").find((row) => row.startsWith("mockDevice="))
66+
);
67+
5468
const logging = deployment.logging;
5569

70+
const usb = isMockDeviceMode()
71+
? new MockWebUSBConnection()
72+
: createWebUSBConnection({ logging });
73+
const bluetooth = isMockDeviceMode()
74+
? new MockWebBluetoothConnection()
75+
: createWebBluetoothConnection({ logging });
76+
const radioBridge = isMockDeviceMode()
77+
? new MockRadioBridgeConnection(usb)
78+
: createRadioBridgeConnection(usb, { logging });
79+
5680
const Providers = ({ children }: ProviderLayoutProps) => {
5781
const deployment = useDeployment();
5882
const { ConsentProvider } = deployment.compliance;
@@ -63,7 +87,7 @@ const Providers = ({ children }: ProviderLayoutProps) => {
6387
<ConsentProvider>
6488
<TranslationProvider>
6589
<ConnectStatusProvider>
66-
<ConnectProvider>
90+
<ConnectProvider {...{ usb, bluetooth, radioBridge }}>
6791
<BufferedDataProvider>
6892
<ConnectionStageProvider>
6993
{children}

src/connect-actions-hooks.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,17 @@ const ConnectContext = createContext<ConnectContextValue | null>(null);
3232

3333
interface ConnectProviderProps {
3434
children: ReactNode;
35+
usb: MicrobitWebUSBConnection;
36+
bluetooth: MicrobitWebBluetoothConnection;
37+
radioBridge: MicrobitRadioBridgeConnection;
3538
}
3639

37-
export const ConnectProvider = ({ children }: ConnectProviderProps) => {
38-
const usb = useRef(new MicrobitWebUSBConnection()).current;
39-
const logging = useRef(useLogging()).current;
40-
const bluetooth = useRef(
41-
new MicrobitWebBluetoothConnection({ logging })
42-
).current;
43-
const radioBridge = useRef(
44-
new MicrobitRadioBridgeConnection(usb, { logging })
45-
).current;
40+
export const ConnectProvider = ({
41+
children,
42+
usb,
43+
bluetooth,
44+
radioBridge,
45+
}: ConnectProviderProps) => {
4646
const [isInitialized, setIsInitialized] = useState<boolean>(false);
4747

4848
useEffect(() => {

src/device/mockBluetooth.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import {
2+
BoardVersion,
3+
ConnectionStatus,
4+
ConnectionStatusEvent,
5+
DeviceConnectionEventMap,
6+
LedMatrix,
7+
MicrobitWebBluetoothConnection,
8+
ServiceConnectionEventMap,
9+
TypedEventTarget,
10+
} from "@microbit/microbit-connection";
11+
12+
export class MockWebBluetoothConnection
13+
extends TypedEventTarget<DeviceConnectionEventMap & ServiceConnectionEventMap>
14+
implements MicrobitWebBluetoothConnection
15+
{
16+
status: ConnectionStatus = ConnectionStatus.NO_AUTHORIZED_DEVICE;
17+
private connectResults: ConnectionStatus[] = [];
18+
19+
constructor() {
20+
super();
21+
// Make globally available to allow e2e tests to configure interactions.
22+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
23+
(window as any).mockBluetooth = this;
24+
}
25+
private setStatus(newStatus: ConnectionStatus) {
26+
this.status = newStatus;
27+
this.dispatchTypedEvent("status", new ConnectionStatusEvent(newStatus));
28+
}
29+
30+
async initialize(): Promise<void> {
31+
this.setStatus(ConnectionStatus.NO_AUTHORIZED_DEVICE);
32+
await new Promise((resolve) => setTimeout(resolve, 100));
33+
}
34+
35+
dispose(): void {}
36+
37+
mockConnectResults(results: ConnectionStatus[]) {
38+
this.connectResults = results;
39+
}
40+
41+
async connect(): Promise<ConnectionStatus> {
42+
if (this.connectResults.length > 0) {
43+
for (const result of this.connectResults) {
44+
this.setStatus(result);
45+
await new Promise((resolve) => setTimeout(resolve, 100));
46+
}
47+
return this.status;
48+
}
49+
this.setStatus(ConnectionStatus.CONNECTING);
50+
await new Promise((resolve) => setTimeout(resolve, 100));
51+
this.setStatus(ConnectionStatus.CONNECTED);
52+
return this.status;
53+
}
54+
55+
getBoardVersion(): BoardVersion | undefined {
56+
return "V2";
57+
}
58+
59+
async disconnect(): Promise<void> {}
60+
async serialWrite(_data: string): Promise<void> {}
61+
setNameFilter(_name: string): void {}
62+
63+
clearDevice(): void {
64+
this.setStatus(ConnectionStatus.NO_AUTHORIZED_DEVICE);
65+
}
66+
67+
async getAccelerometerData(): Promise<undefined> {}
68+
async getAccelerometerPeriod(): Promise<undefined> {}
69+
async setAccelerometerPeriod(_value: number): Promise<void> {}
70+
async setLedText(_text: string): Promise<void> {}
71+
async getLedScrollingDelay(): Promise<undefined> {}
72+
async setLedScrollingDelay(_delayInMillis: number): Promise<void> {}
73+
async getLedMatrix(): Promise<undefined> {}
74+
async setLedMatrix(_matrix: LedMatrix): Promise<void> {}
75+
async getMagnetometerData(): Promise<undefined> {}
76+
async getMagnetometerBearing(): Promise<undefined> {}
77+
async getMagnetometerPeriod(): Promise<undefined> {}
78+
async setMagnetometerPeriod(_value: number): Promise<void> {}
79+
async triggerMagnetometerCalibration(): Promise<void> {}
80+
async uartWrite(_data: Uint8Array): Promise<void> {}
81+
}

src/device/mockRadioBridge.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {
2+
BoardVersion,
3+
ConnectionStatus,
4+
ConnectionStatusEvent,
5+
DeviceConnectionEventMap,
6+
MicrobitRadioBridgeConnection,
7+
MicrobitWebUSBConnection,
8+
ServiceConnectionEventMap,
9+
TypedEventTarget,
10+
} from "@microbit/microbit-connection";
11+
12+
export class MockRadioBridgeConnection
13+
extends TypedEventTarget<DeviceConnectionEventMap & ServiceConnectionEventMap>
14+
implements MicrobitRadioBridgeConnection
15+
{
16+
status: ConnectionStatus;
17+
18+
constructor(private delegate: MicrobitWebUSBConnection) {
19+
super();
20+
this.status = this.statusFromDelegate();
21+
}
22+
23+
private statusFromDelegate(): ConnectionStatus {
24+
return this.delegate.status == ConnectionStatus.CONNECTED
25+
? ConnectionStatus.DISCONNECTED
26+
: this.delegate.status;
27+
}
28+
29+
async initialize(): Promise<void> {}
30+
dispose(): void {}
31+
setRemoteDeviceId(_deviceId: number): void {}
32+
async disconnect(): Promise<void> {}
33+
34+
private setStatus(newStatus: ConnectionStatus) {
35+
this.status = newStatus;
36+
this.dispatchTypedEvent("status", new ConnectionStatusEvent(newStatus));
37+
}
38+
39+
async connect(): Promise<ConnectionStatus> {
40+
await this.delegate.connect();
41+
this.setStatus(ConnectionStatus.CONNECTING);
42+
await new Promise((resolve) => setTimeout(resolve, 100));
43+
this.setStatus(ConnectionStatus.CONNECTED);
44+
return this.status;
45+
}
46+
47+
getBoardVersion(): BoardVersion | undefined {
48+
return this.delegate.getBoardVersion();
49+
}
50+
51+
serialWrite(data: string): Promise<void> {
52+
return this.delegate.serialWrite(data);
53+
}
54+
async clearDevice(): Promise<void> {
55+
await this.delegate.clearDevice();
56+
}
57+
}

src/device/mockUsb.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import {
2+
AfterRequestDevice,
3+
BeforeRequestDevice,
4+
BoardVersion,
5+
ConnectionStatus,
6+
ConnectionStatusEvent,
7+
DeviceConnectionEventMap,
8+
FlashDataSource,
9+
FlashEvent,
10+
FlashOptions,
11+
MicrobitWebUSBConnection,
12+
SerialConnectionEventMap,
13+
TypedEventTarget,
14+
} from "@microbit/microbit-connection";
15+
16+
/**
17+
* A mock USB connection used during end-to-end testing.
18+
*/
19+
export class MockWebUSBConnection
20+
extends TypedEventTarget<DeviceConnectionEventMap & SerialConnectionEventMap>
21+
implements MicrobitWebUSBConnection
22+
{
23+
status: ConnectionStatus = ConnectionStatus.NO_AUTHORIZED_DEVICE;
24+
25+
private fakeDeviceId: number | undefined = 123;
26+
27+
constructor() {
28+
super();
29+
// Make globally available to allow e2e tests to configure interactions.
30+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
31+
(window as any).mockUsb = this;
32+
this.fakeDeviceId = Math.round(Math.random() * 1000);
33+
}
34+
async initialize(): Promise<void> {}
35+
dispose(): void {}
36+
37+
mockDeviceId(deviceId: number | undefined) {
38+
this.fakeDeviceId = deviceId;
39+
}
40+
41+
private setStatus(newStatus: ConnectionStatus) {
42+
this.status = newStatus;
43+
this.dispatchTypedEvent("status", new ConnectionStatusEvent(newStatus));
44+
}
45+
46+
async connect(): Promise<ConnectionStatus> {
47+
this.dispatchTypedEvent("beforerequestdevice", new BeforeRequestDevice());
48+
await new Promise((resolve) => setTimeout(resolve, 100));
49+
this.dispatchTypedEvent("afterrequestdevice", new AfterRequestDevice());
50+
await new Promise((resolve) => setTimeout(resolve, 100));
51+
this.setStatus(ConnectionStatus.CONNECTED);
52+
return this.status;
53+
}
54+
55+
getDeviceId(): number | undefined {
56+
return this.fakeDeviceId;
57+
}
58+
59+
getBoardVersion(): BoardVersion | undefined {
60+
return "V2";
61+
}
62+
63+
async flash(
64+
_dataSource: FlashDataSource,
65+
options: FlashOptions
66+
): Promise<void> {
67+
await new Promise((resolve) => setTimeout(resolve, 100));
68+
options.progress(50, options.partial);
69+
await new Promise((resolve) => setTimeout(resolve, 100));
70+
options.progress(undefined, options.partial);
71+
this.dispatchTypedEvent("flash", new FlashEvent());
72+
}
73+
74+
async disconnect(): Promise<void> {}
75+
async serialWrite(_data: string): Promise<void> {}
76+
77+
clearDevice(): void {
78+
this.fakeDeviceId = undefined;
79+
this.setStatus(ConnectionStatus.NO_AUTHORIZED_DEVICE);
80+
}
81+
82+
setRequestDeviceExclusionFilters(
83+
_exclusionFilters: USBDeviceFilter[]
84+
): void {}
85+
getDevice(): USBDevice | undefined {
86+
return undefined;
87+
}
88+
async softwareReset(): Promise<void> {}
89+
}

0 commit comments

Comments
 (0)