-
Notifications
You must be signed in to change notification settings - Fork 0
docs: refresh usage guide and add Playwright screenshot pipeline #12
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { defineConfig } from "@playwright/test"; | ||
|
|
||
| /** | ||
| * Dedicated Playwright config for regenerating the usage-guide screenshots. | ||
| * | ||
| * Run with: npm run screenshots | ||
| * | ||
| * Specs live in ./screenshots and drive the UI into specific states before | ||
| * calling page.screenshot(). Output is written to docs/usage-screenshots/. | ||
| */ | ||
| export default defineConfig({ | ||
| testDir: "./screenshots", | ||
| testMatch: /.*\.screenshot\.ts$/, | ||
| timeout: 60_000, | ||
| retries: 0, | ||
| fullyParallel: false, | ||
| workers: 1, | ||
| reporter: "list", | ||
| use: { | ||
| baseURL: "http://localhost:4199", | ||
| viewport: { width: 1440, height: 900 }, | ||
| deviceScaleFactor: 2, | ||
| colorScheme: "dark", | ||
| }, | ||
| webServer: { | ||
| command: "npx vite preview --port 4199", | ||
| port: 4199, | ||
| reuseExistingServer: !process.env.CI, | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import path from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { capture, expect, settle, test } from "./helpers"; | ||
|
|
||
| const HERE = path.dirname(fileURLToPath(import.meta.url)); | ||
| const FIXTURES = path.resolve(HERE, "..", "..", "..", "fixtures"); | ||
|
|
||
| test.describe("Load screens", () => { | ||
| test("load-examples: bundled example browser", async ({ page }) => { | ||
| await page.goto("/#/load"); | ||
| await page.getByTestId("load-page").waitFor(); | ||
| // Expand WAT and JSON categories so more examples are visible in the shot. | ||
| await page.getByTestId("category-toggle-wat").click(); | ||
| await page.getByTestId("category-toggle-json-test-vectors").click(); | ||
| await settle(page); | ||
| await capture(page, "load-examples.png"); | ||
| }); | ||
|
|
||
| test("load-upload: uploaded file selected", async ({ page }) => { | ||
| await page.goto("/#/load"); | ||
| await page.getByTestId("load-page").waitFor(); | ||
| const input = page.getByTestId("file-upload-input"); | ||
| await input.setInputFiles(path.join(FIXTURES, "generic", "branch.pvm")); | ||
| await expect(page.getByTestId("file-upload-selected")).toBeVisible(); | ||
| await settle(page); | ||
| await capture(page, "load-upload.png"); | ||
| }); | ||
|
|
||
| test("load-url: URL field filled", async ({ page }) => { | ||
| await page.goto("/#/load"); | ||
| await page.getByTestId("load-page").waitFor(); | ||
| await page | ||
| .getByTestId("url-input-field") | ||
| .fill( | ||
| "https://github.com/FluffyLabs/pvm-debugger/blob/main/fixtures/generic/branch.pvm", | ||
| ); | ||
| await settle(page); | ||
| await capture(page, "load-url.png"); | ||
| }); | ||
|
|
||
| test("load-manual: hex input filled", async ({ page }) => { | ||
| await page.goto("/#/load"); | ||
| await page.getByTestId("load-page").waitFor(); | ||
| await page | ||
| .getByTestId("manual-input-field") | ||
| .fill("0x00 03 00 01 00 0d 00 08 00 02 00 07 00 01"); | ||
| // Blur so the byte count appears. | ||
| await page.getByTestId("manual-input-field").blur(); | ||
| await settle(page); | ||
| await capture(page, "load-manual.png"); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import { capture, expect, settle, test } from "./helpers"; | ||
|
|
||
| test.describe("Config step", () => { | ||
| test("config-jam-builder: JAM SPI builder mode", async ({ page }) => { | ||
| await page.goto("/#/load"); | ||
| await page.getByTestId("category-toggle-wat").click(); | ||
| await page.getByTestId("example-card-add-jam").click(); | ||
| await expect(page.getByTestId("config-step")).toBeVisible(); | ||
| await settle(page); | ||
| await capture(page, "config-jam-builder.png"); | ||
| }); | ||
|
|
||
| test("config-jam-raw: JAM SPI raw mode", async ({ page }) => { | ||
| await page.goto("/#/load"); | ||
| await page.getByTestId("category-toggle-wat").click(); | ||
| await page.getByTestId("example-card-add-jam").click(); | ||
| await expect(page.getByTestId("config-step")).toBeVisible(); | ||
| await page.getByTestId("spi-raw-mode-switch").click(); | ||
| await expect(page.getByTestId("spi-raw-hex")).toBeVisible(); | ||
| await settle(page); | ||
| await capture(page, "config-jam-raw.png"); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import type { Page } from "@playwright/test"; | ||
| import { capture, expect, settle, test } from "./helpers"; | ||
|
|
||
| /** Load a generic PVM example that goes straight to the debugger. */ | ||
| async function loadGeneric(page: Page, id: string) { | ||
| await page.goto("/#/load"); | ||
| await page.getByTestId(`example-card-${id}`).click(); | ||
| await expect(page.getByTestId("debugger-page")).toBeVisible({ | ||
| timeout: 15000, | ||
| }); | ||
| } | ||
|
|
||
| test.describe("Debugger screen", () => { | ||
| test("debugger-full: layout with instructions/registers/memory", async ({ | ||
| page, | ||
| }) => { | ||
| await loadGeneric(page, "fibonacci"); | ||
| await settle(page); | ||
| await capture(page, "debugger-full.png"); | ||
| }); | ||
|
|
||
| test("debugger-stepping: after several steps", async ({ page }) => { | ||
| await loadGeneric(page, "fibonacci"); | ||
| // Advance a few instructions so register deltas and the PC marker | ||
| // move off the initial state. | ||
| for (let i = 0; i < 5; i++) { | ||
| await page.getByTestId("next-button").click(); | ||
| await page.waitForTimeout(60); | ||
| } | ||
| await settle(page); | ||
| await capture(page, "debugger-stepping.png"); | ||
| }); | ||
|
|
||
| test("registers-edit: inline register editor open", async ({ page }) => { | ||
| await loadGeneric(page, "fibonacci"); | ||
| // Click the hex value to reveal the inline editor input. | ||
| await page.getByTestId("register-hex-7").click(); | ||
| await expect(page.getByTestId("register-edit-7")).toBeVisible(); | ||
| await settle(page); | ||
| await capture(page, "registers-edit.png"); | ||
| }); | ||
|
|
||
| test("memory-panel: expanded range with bytes", async ({ page }) => { | ||
| // Game of Life has a writable memory page mapped. | ||
| await loadGeneric(page, "game-of-life"); | ||
| // Take a few steps so memory has non-zero bytes to show. | ||
| for (let i = 0; i < 30; i++) { | ||
| await page.getByTestId("next-button").click(); | ||
| await page.waitForTimeout(20); | ||
| } | ||
| // Expand the first memory range by clicking its header. | ||
| const firstHeader = page | ||
| .locator("[data-testid^='memory-range-header-']") | ||
| .first(); | ||
| await firstHeader.click(); | ||
| await settle(page); | ||
| await capture(page, "memory-panel.png"); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,46 @@ | ||||||||||||||||
| import { capture, expect, settle, test } from "./helpers"; | ||||||||||||||||
|
|
||||||||||||||||
| test.describe("Settings and multi-PVM", () => { | ||||||||||||||||
| test("settings: drawer showing PVM toggles and stepping modes", async ({ | ||||||||||||||||
| page, | ||||||||||||||||
| }) => { | ||||||||||||||||
| await page.goto("/#/load"); | ||||||||||||||||
| await page.getByTestId("example-card-fibonacci").click(); | ||||||||||||||||
| await expect(page.getByTestId("debugger-page")).toBeVisible({ | ||||||||||||||||
| timeout: 15000, | ||||||||||||||||
| }); | ||||||||||||||||
| await page.getByTestId("drawer-tab-settings").click(); | ||||||||||||||||
| await expect(page.getByTestId("settings-tab")).toBeVisible(); | ||||||||||||||||
| await settle(page); | ||||||||||||||||
| await capture(page, "settings.png"); | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| test("multiple-pvms: both typeberry and ananas enabled", async ({ page }) => { | ||||||||||||||||
| await page.goto("/#/load"); | ||||||||||||||||
| await page.getByTestId("example-card-fibonacci").click(); | ||||||||||||||||
| await expect(page.getByTestId("debugger-page")).toBeVisible({ | ||||||||||||||||
| timeout: 15000, | ||||||||||||||||
| }); | ||||||||||||||||
| await page.getByTestId("drawer-tab-settings").click(); | ||||||||||||||||
| await expect(page.getByTestId("settings-tab")).toBeVisible(); | ||||||||||||||||
| // Enable ananas alongside typeberry. | ||||||||||||||||
| const ananasSwitch = page.getByTestId("pvm-switch-ananas"); | ||||||||||||||||
| const isChecked = await ananasSwitch.isChecked().catch(() => false); | ||||||||||||||||
| if (!isChecked) { | ||||||||||||||||
| await ananasSwitch.click(); | ||||||||||||||||
| } | ||||||||||||||||
|
Comment on lines
+29
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Assert toggle state after enabling After clicking the switch, assert it is checked before closing the drawer; this makes capture state deterministic. ✅ Suggested reliability fix if (!isChecked) {
await ananasSwitch.click();
+ await expect(ananasSwitch).toBeChecked();
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| // Close drawer so PVM tabs in the top-right are in frame. | ||||||||||||||||
| await page.getByTestId("drawer-close-button").click(); | ||||||||||||||||
| // Both PVM tabs should appear. | ||||||||||||||||
| await expect(page.getByTestId("pvm-tab-typeberry")).toBeVisible(); | ||||||||||||||||
| await expect(page.getByTestId("pvm-tab-ananas")).toBeVisible(); | ||||||||||||||||
| // Advance a few steps to produce deltas between the two PVMs (usually none | ||||||||||||||||
| // for a clean example, but stepping gives the UI something real to show). | ||||||||||||||||
| for (let i = 0; i < 3; i++) { | ||||||||||||||||
| await page.getByTestId("next-button").click(); | ||||||||||||||||
| await page.waitForTimeout(100); | ||||||||||||||||
| } | ||||||||||||||||
| await settle(page); | ||||||||||||||||
| await capture(page, "multiple-pvms.png"); | ||||||||||||||||
| }); | ||||||||||||||||
| }); | ||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { capture, expect, settle, test } from "./helpers"; | ||
|
|
||
| test.describe("Persistence", () => { | ||
| test("persistence-restored: state preserved across reload", async ({ | ||
| page, | ||
| }) => { | ||
| await page.goto("/#/load"); | ||
| await page.getByTestId("example-card-fibonacci").click(); | ||
| await expect(page.getByTestId("debugger-page")).toBeVisible({ | ||
| timeout: 15000, | ||
| }); | ||
| // Take a few steps so the restored state looks "live". | ||
| for (let i = 0; i < 4; i++) { | ||
| await page.getByTestId("next-button").click(); | ||
| await page.waitForTimeout(60); | ||
| } | ||
| // Reload the page — persistence should reinstall the program. | ||
| await page.reload(); | ||
| await expect(page.getByTestId("debugger-page")).toBeVisible({ | ||
| timeout: 15000, | ||
| }); | ||
| await settle(page); | ||
| await capture(page, "persistence-restored.png"); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import type { Page } from "@playwright/test"; | ||
| import { | ||
| advanceToNextHostCall, | ||
| capture, | ||
| expect, | ||
| runToHostCall, | ||
| setAutoContinueNever, | ||
| settle, | ||
| test, | ||
| } from "./helpers"; | ||
|
|
||
| /** | ||
| * Screenshots of the sprint-42 redesigned host-call drawer. | ||
| * | ||
| * Fixture choices: | ||
| * - `io-trace` — short trace, first host call is `fetch`. Used for the | ||
| * two-column overview and the pending-changes banner. | ||
| * - `all-ecalli-accumulate` — exercises every host-call kind; hits a `log` | ||
| * call within the first few pauses. | ||
| */ | ||
|
|
||
| async function loadTraceExample(page: Page, id: "io-trace") { | ||
| await page.goto("/#/load"); | ||
| await page.getByTestId(`example-card-${id}`).click(); | ||
| await expect(page.getByTestId("debugger-page")).toBeVisible({ | ||
| timeout: 15000, | ||
| }); | ||
| } | ||
|
|
||
| async function loadAllEcalliAccumulate(page: Page) { | ||
| await page.goto("/#/load"); | ||
| await page.getByTestId("example-card-all-ecalli-accumulate").click(); | ||
| await expect(page.getByTestId("config-step")).toBeVisible({ timeout: 15000 }); | ||
| const loadBtn = page.getByTestId("config-step-load"); | ||
| await expect(loadBtn).toBeEnabled({ timeout: 10000 }); | ||
| await loadBtn.click(); | ||
| await expect(page.getByTestId("debugger-page")).toBeVisible({ | ||
| timeout: 30000, | ||
| }); | ||
| } | ||
|
|
||
| test.describe("Host call drawer", () => { | ||
| test("host-call-overview: redesigned two-column layout", async ({ page }) => { | ||
| await loadTraceExample(page, "io-trace"); | ||
| await setAutoContinueNever(page); | ||
| await runToHostCall(page); | ||
| await settle(page); | ||
| await capture(page, "host-call-overview.png"); | ||
| }); | ||
|
|
||
| test("host-call-log: decoded log message", async ({ page }) => { | ||
| await loadAllEcalliAccumulate(page); | ||
| await setAutoContinueNever(page); | ||
| await runToHostCall(page); | ||
| for (let i = 0; i < 30; i++) { | ||
| const logVisible = await page | ||
| .getByTestId("log-host-call") | ||
| .isVisible() | ||
| .catch(() => false); | ||
| if (logVisible) break; | ||
| if (!(await advanceToNextHostCall(page))) break; | ||
| } | ||
| await expect(page.getByTestId("log-host-call")).toBeVisible(); | ||
| await settle(page); | ||
| await capture(page, "host-call-log.png"); | ||
| }); | ||
|
|
||
| test("host-call-pending-changes: banner with arrow notation", async ({ | ||
| page, | ||
| }) => { | ||
| await loadTraceExample(page, "io-trace"); | ||
| await setAutoContinueNever(page); | ||
| await runToHostCall(page); | ||
| // Pending-changes is debounced; give it a moment before checking. | ||
| for (let i = 0; i < 20; i++) { | ||
| await page.waitForTimeout(500); | ||
| const pendingVisible = await page | ||
| .getByTestId("pending-changes") | ||
| .isVisible() | ||
| .catch(() => false); | ||
| if (pendingVisible) break; | ||
| if (!(await advanceToNextHostCall(page))) break; | ||
| } | ||
| await expect(page.getByTestId("pending-changes")).toBeVisible(); | ||
| await settle(page); | ||
| await capture(page, "host-call-pending-changes.png"); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move embedded manual hex payload to a fixture file.
Line 46 hardcodes a hex payload in source, which breaks the repo’s test-data rule and makes this scenario harder to maintain/reuse.
Proposed fix
As per coding guidelines,
Use fixtures/ directory for example programs and test data; do NOT embed hex strings in source code.📝 Committable suggestion
🤖 Prompt for AI Agents