Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
87806bb
Add new-page E2E tests
microbit-grace Oct 11, 2024
f213876
Prettier
microbit-grace Oct 25, 2024
149ba99
Setup cookies
microbit-grace Oct 25, 2024
2caa80b
Remove WIP data samples test
microbit-grace Oct 25, 2024
9c13361
Add workflow 10 mins build timeout
microbit-grace Oct 25, 2024
44be331
WIP mock connections
microbit-grace Jan 22, 2025
3cde798
Merge branch 'e2e' of https://github.com/microbit-foundation/ml-train…
microbit-grace Jan 22, 2025
ea855be
Fix start session and resume session tests
microbit-grace Jan 22, 2025
3a8e616
Update to the latest microbit-connection library
microbit-grace Jan 23, 2025
2c97803
Happy bluetooth connection flow
microbit-grace Jan 23, 2025
bd67da5
Bluetooth no device selected for flashing test
microbit-grace Jan 23, 2025
cfae3f5
Comment out E2E tests temporarily
microbit-grace Jan 23, 2025
274c4c2
Bluetooth no device selected for connecting test
microbit-grace Jan 23, 2025
c1bf6a4
Add radio bridge happy flow test
microbit-grace Jan 23, 2025
52893a5
Radio no device selected for flashing
microbit-grace Jan 24, 2025
15c63e3
Use cookie mechanism to activate isMockDeviceMode
microbit-grace Jan 24, 2025
3c5c44a
Fix minor lint
microbit-grace Jan 24, 2025
d353173
Make connection dialog tests more reliable by removing loading steps
microbit-grace Jan 24, 2025
49c48f3
Uncomment build.yml e2e workflow
microbit-grace Jan 24, 2025
ad89519
Add unused methods into mock usb & bluetooth
microbit-grace Jan 24, 2025
b5c81bb
Merge branch 'main' of https://github.com/microbit-foundation/ml-trai…
microbit-grace Feb 14, 2025
e0f1c60
Adjust to fit changes in microbit-connection lib
microbit-grace Feb 14, 2025
d70f96a
Update mocks following changes in microbit-connection
microbit-grace Feb 14, 2025
b1fbba4
Add test model test
microbit-grace Feb 17, 2025
a61a634
Remove not needed type casting
microbit-grace Feb 17, 2025
db61b1d
Empty commit because CloudFlare is taking an unusually long time?
microbit-grace Feb 17, 2025
c928fd8
Enter bluetooth pattern via input fields instead
microbit-grace Feb 17, 2025
5c2100c
More straight line test code. Removed some awkward tests in favour of…
microbit-grace Feb 17, 2025
0278cf8
Upgrade microbit-connection
microbit-grace Feb 17, 2025
360cc63
Organise imports
microbit-grace Feb 17, 2025
fa911b2
Fix build
microbit-grace Feb 17, 2025
8e9c791
Attempt to fix E2E test.
microbit-grace Feb 18, 2025
1f28286
Inline connection happy flow dialog steps and reinstate other tests
microbit-grace Feb 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ concurrency:

jobs:
build:
timeout-minutes: 10
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down Expand Up @@ -42,18 +43,19 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: node ./bin/print-ci-env.cjs >> $GITHUB_ENV
- run: npm run ci
- name: Run Playwright tests
if: env.STAGE == 'REVIEW' || env.STAGE == 'STAGING'
uses: docker://mcr.microsoft.com/playwright:v1.45.0-jammy
with:
args: npx playwright test
- name: Store reports
if: (env.STAGE == 'REVIEW' || env.STAGE == 'STAGING') && failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 3
# TODO: Uncomment E2E tests once ready to test
# - name: Run Playwright tests
# if: env.STAGE == 'REVIEW' || env.STAGE == 'STAGING'
# uses: docker://mcr.microsoft.com/playwright:v1.45.0-jammy
# with:
# args: npx playwright test
# - name: Store reports
# if: (env.STAGE == 'REVIEW' || env.STAGE == 'STAGING') && failure()
# uses: actions/upload-artifact@v4
# with:
# name: playwright-report
# path: playwright-report/
# retention-days: 3
- run: npx website-deploy-aws
if: github.repository_owner == 'microbit-foundation' && (env.STAGE == 'REVIEW' || success())
env:
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@microbit/makecode-embed": "^0.0.0-alpha.7",
"@microbit/microbit-connection": "^0.0.0-alpha.30",
"@microbit/microbit-connection": "^0.0.0-alpha.32",
"@microbit/ml-header-generator": "^0.4.3",
"@microbit/smoothie": "^1.37.0-microbit.2",
"@tensorflow/tfjs": "^4.20.0",
Expand Down
24 changes: 23 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,35 @@ import {
import { hasMakeCodeMlExtension } from "./makecode/utils";
import { PostImportDialogState } from "./model";
import "theme-package/fonts/fonts.css";
import { MockWebUSBConnection } from "./device/mockUsb";
import {
MicrobitRadioBridgeConnection,
MicrobitWebBluetoothConnection,
MicrobitWebUSBConnection,
} from "@microbit/microbit-connection";
import { MockWebBluetoothConnection } from "./device/mockBluetooth";

export interface ProviderLayoutProps {
children: ReactNode;
}

const isMockDeviceMode = () => true;
// TODO: Use cookie mechanism for isMockDeviceMode.
// We use a cookie set from the e2e tests. Avoids having separate test and live builds.
// Boolean(
// document.cookie.split("; ").find((row) => row.startsWith("mockDevice="))
// );

const logging = deployment.logging;

const usb = isMockDeviceMode()
? (new MockWebUSBConnection() as unknown as MicrobitWebUSBConnection)
: new MicrobitWebUSBConnection({ logging });
const bluetooth = isMockDeviceMode()
? (new MockWebBluetoothConnection() as unknown as MicrobitWebBluetoothConnection)
: new MicrobitWebBluetoothConnection({ logging });
const radioBridge = new MicrobitRadioBridgeConnection(usb, { logging });

const Providers = ({ children }: ProviderLayoutProps) => {
const deployment = useDeployment();
const { ConsentProvider } = deployment.compliance;
Expand All @@ -63,7 +85,7 @@ const Providers = ({ children }: ProviderLayoutProps) => {
<ConsentProvider>
<TranslationProvider>
<ConnectStatusProvider>
<ConnectProvider>
<ConnectProvider {...{ usb, bluetooth, radioBridge }}>
<BufferedDataProvider>
<ConnectionStageProvider>
{children}
Expand Down
18 changes: 9 additions & 9 deletions src/connect-actions-hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,17 @@ const ConnectContext = createContext<ConnectContextValue | null>(null);

interface ConnectProviderProps {
children: ReactNode;
usb: MicrobitWebUSBConnection;
bluetooth: MicrobitWebBluetoothConnection;
radioBridge: MicrobitRadioBridgeConnection;
}

export const ConnectProvider = ({ children }: ConnectProviderProps) => {
const usb = useRef(new MicrobitWebUSBConnection()).current;
const logging = useRef(useLogging()).current;
const bluetooth = useRef(
new MicrobitWebBluetoothConnection({ logging })
).current;
const radioBridge = useRef(
new MicrobitRadioBridgeConnection(usb, { logging })
).current;
export const ConnectProvider = ({
children,
usb,
bluetooth,
radioBridge,
}: ConnectProviderProps) => {
const [isInitialized, setIsInitialized] = useState<boolean>(false);

useEffect(() => {
Expand Down
37 changes: 37 additions & 0 deletions src/device/mockBluetooth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {
BoardVersion,
ConnectionStatus,
ConnectionStatusEvent,
DeviceConnectionEventMap,
DeviceWebBluetoothConnection,
TypedEventTarget,
} from "@microbit/microbit-connection";

export class MockWebBluetoothConnection
extends TypedEventTarget<DeviceConnectionEventMap>
implements DeviceWebBluetoothConnection
{
status: ConnectionStatus = ConnectionStatus.NO_AUTHORIZED_DEVICE;

async initialize(): Promise<void> {}
dispose(): void {}

private mockStatus(newStatus: ConnectionStatus) {
this.dispatchTypedEvent("status", new ConnectionStatusEvent(newStatus));
}

async connect(): Promise<ConnectionStatus> {
this.mockStatus(ConnectionStatus.CONNECTING);
await new Promise((resolve) => setTimeout(resolve, 100));
this.mockStatus(ConnectionStatus.CONNECTED);
return ConnectionStatus.CONNECTED;
}
getBoardVersion(): BoardVersion | undefined {
return "V2";
}
async disconnect(): Promise<void> {}
async serialWrite(_data: string): Promise<void> {}

clearDevice(): void {}
setNameFilter(_name: string): void {}
}
70 changes: 70 additions & 0 deletions src/device/mockUsb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {
AfterRequestDevice,
BeforeRequestDevice,
BoardVersion,
ConnectionStatus,
ConnectionStatusEvent,
DeviceConnectionEventMap,
DeviceWebUSBConnection,
FlashDataSource,
FlashOptions,
TypedEventTarget,
} from "@microbit/microbit-connection";

/**
* A mock USB connection used during end-to-end testing.
*/
export class MockWebUSBConnection
extends TypedEventTarget<DeviceConnectionEventMap>
implements DeviceWebUSBConnection
{
status: ConnectionStatus = ConnectionStatus.NO_AUTHORIZED_DEVICE;

private fakeDeviceId: number | undefined = 123;

constructor() {
super();
// Make globally available to allow e2e tests to configure interactions.
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
(window as any).mockUsb = this;
this.fakeDeviceId = Math.round(Math.random() * 1000);
}

async initialize(): Promise<void> {}
dispose(): void {}

mockDeviceId(deviceId: number | undefined) {
this.fakeDeviceId = deviceId;
}

private mockStatus(newStatus: ConnectionStatus) {
this.dispatchTypedEvent("status", new ConnectionStatusEvent(newStatus));
}

async connect(): Promise<ConnectionStatus> {
this.dispatchTypedEvent("beforerequestdevice", new BeforeRequestDevice());
await new Promise((resolve) => setTimeout(resolve, 100));
this.dispatchTypedEvent("afterrequestdevice", new AfterRequestDevice());
await new Promise((resolve) => setTimeout(resolve, 100));
this.mockStatus(ConnectionStatus.CONNECTED);
return ConnectionStatus.CONNECTED;
}
getDeviceId(): number | undefined {
return this.fakeDeviceId;
}
getBoardVersion(): BoardVersion | undefined {
return "V2";
}
async flash(
_dataSource: FlashDataSource,
options: FlashOptions
): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, 100));
options.progress(50, options.partial);

Check failure on line 63 in src/device/mockUsb.ts

View workflow job for this annotation

GitHub Actions / build

Unsafe call of an `any` typed value

Check failure on line 63 in src/device/mockUsb.ts

View workflow job for this annotation

GitHub Actions / build

Unsafe member access .progress on an `error` typed value

Check failure on line 63 in src/device/mockUsb.ts

View workflow job for this annotation

GitHub Actions / build

Unsafe member access .partial on an `error` typed value
await new Promise((resolve) => setTimeout(resolve, 100));
options.progress(undefined, options.partial);

Check failure on line 65 in src/device/mockUsb.ts

View workflow job for this annotation

GitHub Actions / build

Unsafe call of an `any` typed value

Check failure on line 65 in src/device/mockUsb.ts

View workflow job for this annotation

GitHub Actions / build

Unsafe member access .progress on an `error` typed value

Check failure on line 65 in src/device/mockUsb.ts

View workflow job for this annotation

GitHub Actions / build

Unsafe member access .partial on an `error` typed value
}
async disconnect(): Promise<void> {}
async serialWrite(_data: string): Promise<void> {}
clearDevice(): void {}
}
67 changes: 67 additions & 0 deletions src/e2e/app/connection-dialogs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* (c) 2024, Micro:bit Educational Foundation and contributors
*
* SPDX-License-Identifier: MIT
*/
import { expect, type Page } from "@playwright/test";
import { MockWebUSBConnection } from "../../device/mockUsb";

export class ConnectionDialogs {
constructor(public readonly page: Page) {}

async close() {
await this.page.getByLabel("Close").click();
}

async waitForText(name: string) {
await this.page.getByText(name).waitFor();
}

private async clickNext() {
await this.page.getByRole("button", { name: "Next" }).click();
}

async bluetoothDownloadProgram() {
await this.waitForText("What you need to connect using Web Bluetooth");
await this.clickNext();
await this.waitForText("Connect USB cable to micro:bit");
await this.clickNext();
await this.waitForText("Download data collection program to micro:bit");
await this.clickNext();
}

async expectManualTransferProgramDialog() {
await expect(
this.page.getByText("Transfer saved hex file to micro:bit")
).toBeVisible();
}

async mockUsbDeviceNotSelected() {
await this.page.evaluate(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
const mockUsb = (window as any).mockUsb as MockWebUSBConnection;
mockUsb.mockDeviceId(undefined);
});
}

async bluetoothConnect() {
await this.bluetoothDownloadProgram();
await this.waitForText("Downloading the data collection program");
await this.waitForText("Disconnect USB and connect battery pack");
await this.clickNext();
await this.waitForText("Copy pattern");
await this.enterBluetoothPattern();
await this.clickNext();
await this.waitForText("Connect to micro:bit using Web Bluetooth");
await this.clickNext();
await this.waitForText("Connect using Web Bluetooth");
}

async enterBluetoothPattern() {
await this.page.locator(".css-1jvu5j > .chakra-button").first().click();
await this.page.locator("div:nth-child(11) > .chakra-button").click();
await this.page.locator("div:nth-child(17) > .chakra-button").click();
await this.page.locator("div:nth-child(23) > .chakra-button").click();
await this.page.locator("div:nth-child(29) > .chakra-button").click();
}
}
50 changes: 49 additions & 1 deletion src/e2e/app/data-samples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,61 @@
*
* SPDX-License-Identifier: MIT
*/
import { type Page } from "@playwright/test";
import { expect, Locator, type Page } from "@playwright/test";
import { Navbar } from "./shared";
import { ConnectionDialogs } from "./connection-dialogs";

export class DataSamplesPage {
public readonly navbar: Navbar;
private url: string;
private heading: Locator;
private connectBtn: Locator;

constructor(public readonly page: Page) {
this.url = `http://localhost:5173${
process.env.CI ? process.env.BASE_URL : "/"
}data-samples`;
this.navbar = new Navbar(page);
this.heading = this.page.getByRole("heading", { name: "Data samples" });
this.connectBtn = this.page.getByLabel("Connect to micro:bit");
}

async goto(flags: string[] = ["open"]) {
const response = await this.page.goto(this.url);
await this.page.evaluate(
(flags) => localStorage.setItem("flags", flags.join(",")),
flags
);
return response;
}

expectUrl() {
expect(this.page.url()).toEqual(this.url);
}

async closeDialog() {
await this.page.getByLabel("Close").click();
}

async connect() {
await this.connectBtn.click();
return new ConnectionDialogs(this.page);
}

async expectConnected() {
await expect(this.connectBtn).toBeHidden();
await expect(
this.page.getByText("Your data collection micro:bit is connected!")
).toBeVisible();
}

async expectOnPage() {
await expect(this.heading).toBeVisible();
this.expectUrl();
}

async expectCorrectInitialState() {
this.expectUrl();
await expect(this.heading).toBeVisible({ timeout: 10000 });
}
}
Loading
Loading