Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: playwright E2E testing #397

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
51 changes: 51 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Playwright Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci

- name: Cache node modules
id: cache-npm
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ hashFiles('**/package-lock.json') }}

- if: ${{ steps.cache-npm.outputs.cache-hit == 'false' }}
name: List the state of node modules
continue-on-error: true
run: npm list

- name: Install
run: npm install

- name: CI
run: npm run ci
env:
CI: true

- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ out
npm-debug.log*
yarn-debug.log*
yarn-error.log*

playwright-report/
test-results/
5 changes: 5 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
<<<<<<< Updated upstream
projects/src/samples/project_06/04_pong_asm.ts
web/public/pico.min.css
=======
simulator/src/projects/project_06/04_pong.ts
simulator/src/projects/project_06/04_pong_asm.ts
>>>>>>> Stashed changes
web/src/locales
3 changes: 3 additions & 0 deletions components/src/runbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const Runbar = (props: {
<button
className="flex-0"
disabled={props.disabled}
data-testid="runbar-frame"
onClick={() => runner.actions.frame()}
data-tooltip={props.overrideTooltips?.step ?? `Step`}
data-placement="bottom"
Expand All @@ -87,6 +88,7 @@ export const Runbar = (props: {
? runner.actions.stop()
: runner.actions.start()
}
data-testid="runbar-run-pause"
data-tooltip={
runner.state.running
? props.overrideTooltips?.pause ?? `Pause`
Expand All @@ -105,6 +107,7 @@ export const Runbar = (props: {
}
runner.actions.reset();
}}
data-testid="runbar-reset"
data-tooltip={props.overrideTooltips?.reset ?? `Reset`}
data-placement="bottom"
>
Expand Down
47 changes: 47 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# NAND2Testris Web IDE e2e Tests

This is a brief cookbook for NAND2Tetris web ide Playwright e2e tests.

All commands should be run from the root folder, not this /e2e folder.

Before running any commands, run the server with `npm start` in a separate terminal.

## Install Playwright dependencies

```
npx playwright install
```

## Run all e2e tests

```
npm run e2e
```

## Only run tests for chip

```
npm run e2e -- chip
```

Replace chip with cpu, asm, vm, etc.

## Only run tests in Chromium

```
npm run e2e -- --project chromium
```

## Debug Tests

Use the Playwright VSCode plugin.

## View Tests

```
npm run e2e -- --ui
```

## More Info

Review the Playwright documentation and e2e folder.
13 changes: 13 additions & 0 deletions e2e/asm.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { expect } from "@playwright/test";
import { test } from "./util/base.ts";

test.describe("asm", () => {
test("has title", async ({ page }) => {
await page.goto("asm");

await expect(page).toHaveTitle(/NAND2Tetris/);
await expect(page.getByText("Assembler")).toBeVisible();
await expect(page.getByText("Compiled with problems:")).not.toBeAttached();
await expect(page.getByText("Uncaught runtime errors:")).not.toBeAttached();
});
});
39 changes: 39 additions & 0 deletions e2e/chip.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { expect } from "@playwright/test";
import { test } from "./util/base.ts";

const NOT = `CHIP Not {
IN in;
OUT out;

PARTS:
Nand(a=in, b=in, out=out);
}`;

test.describe("chip", () => {
test("has title", async ({ page }) => {
await page.goto("chip");

await expect(page).toHaveTitle(/NAND2Tetris/);
await expect(page.getByText("Hardware Simulator")).toBeVisible();
await expect(page.getByText("Compiled with problems:")).not.toBeAttached();
await expect(page.getByText("Uncaught runtime errors:")).not.toBeAttached();
});

test("simple chip", async ({ page, monaco }) => {
await page.goto("chip?monaco=false");
await page.getByRole("button", { name: "Accept" }).click();
await page.getByTestId("project-picker").selectOption("Project 1");
await page.getByTestId("chip-picker").selectOption("Not");

await monaco.toggleMonaco();
await monaco.write(NOT, "hdl");
await expect(page.getByText("HDL code: No syntax errors")).toBeVisible();

await page.getByTestId("runbar-run-pause").click();
await expect(
page.getByText(
"Simulation successful: The output file is identical to the compare file",
),
).toBeVisible();
});
});
13 changes: 13 additions & 0 deletions e2e/compiler.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { expect } from "@playwright/test";
import { test } from "./util/base.ts";

test.describe("compiler", () => {
test("has title", async ({ page }) => {
await page.goto("compiler");

await expect(page).toHaveTitle(/NAND2Tetris/);
await expect(page.getByText("Jack Compiler")).toBeVisible();
await expect(page.getByText("Compiled with problems:")).not.toBeAttached();
await expect(page.getByText("Uncaught runtime errors:")).not.toBeAttached();
});
});
13 changes: 13 additions & 0 deletions e2e/cpu.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { expect } from "@playwright/test";
import { test } from "./util/base.ts";

test.describe("cpu", () => {
test("has title", async ({ page }) => {
await page.goto("cpu");

await expect(page).toHaveTitle(/NAND2Tetris/);
await expect(page.getByText("CPU Emulator")).toBeVisible();
await expect(page.getByText("Compiled with problems:")).not.toBeAttached();
await expect(page.getByText("Uncaught runtime errors:")).not.toBeAttached();
});
});
52 changes: 52 additions & 0 deletions e2e/util/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { Locator, Page } from "@playwright/test";
import { test as base } from "@playwright/test";

export class MonacoPage {
private usingMonaco = true;
public readonly monacoEditor: Locator;
constructor(readonly page: Page) {
this.monacoEditor = page.locator(".monaco-editor").nth(0);
}

async toggleMonaco() {
await this.page.getByText("settings", { exact: true }).click();
await this.page.getByText("Use Monaco Editor").click();
await this.page
.locator("header")
.filter({ hasText: /^Settings$/ })
.getByRole("link")
.click();
this.usingMonaco = !this.usingMonaco;
}

async clearEditor(editor: string) {
if (this.usingMonaco) {
await this.monacoEditor.click();
await this.page.keyboard.press("ControlOrMeta+KeyA");
await this.page.keyboard.press("Backspace");
await this.page.keyboard.press("ControlOrMeta+KeyA");
await this.page.keyboard.press("Backspace");
} else {
await this.page.getByTestId(`editor-${editor}`).clear();
}
}

async write(text: string, editor: string) {
await this.clearEditor(editor);
if (this.usingMonaco) {
await this.monacoEditor.click();
} else {
await this.page.getByTestId(`editor-${editor}`);
}
for (const line of text.split("\n")) {
await this.page.keyboard.type(`${line}\n`);
}
}
}

export const test = base.extend<{ monaco: MonacoPage }>({
monaco: async ({ page }, use) => {
const monaco = new MonacoPage(page);
await use(monaco);
},
});
13 changes: 13 additions & 0 deletions e2e/vm.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { expect } from "@playwright/test";
import { test } from "./util/base.ts";

test.describe("vm", () => {
test("has title", async ({ page }) => {
await page.goto("vm");

await expect(page).toHaveTitle(/NAND2Tetris/);
await expect(page.getByText("VM Emulator")).toBeVisible();
await expect(page.getByText("Compiled with problems:")).not.toBeAttached();
await expect(page.getByText("Uncaught runtime errors:")).not.toBeAttached();
});
});
62 changes: 62 additions & 0 deletions package-lock.json

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

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"test": "CI=true npm test -w simulator && CI=true npm test -w components && CI=true npm test -w web",
"web": "npm run start -w web",
"start": "npm run web",
"e2e": "playwright test",
"preinstall-cli": "npm run build -w projects && npm run build -w runner && npm run build -w simulator && npm run build -w cli",
"install-cli": "npm i -g cli"
},
Expand All @@ -48,5 +49,8 @@
"engines": {
"node": ">=16",
"npm": ">=7"
},
"dependencies": {
"@playwright/test": "^1.45.1"
}
}
Loading
Loading