Skip to content
Merged
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
3 changes: 2 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"build": "vite build",
"preview": "vite preview",
"lint": "tsc --noEmit",
"test:e2e": "playwright test"
"test:e2e": "playwright test",
"screenshots": "playwright test --config=playwright.screenshots.config.ts"
},
"dependencies": {
"@fluffylabs/shared-ui": "^0.4.6",
Expand Down
30 changes: 30 additions & 0 deletions apps/web/playwright.screenshots.config.ts
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,
},
});
52 changes: 52 additions & 0 deletions apps/web/screenshots/01-load.screenshot.ts
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.
Comment on lines +45 to +47
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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
+import { readFile } from "node:fs/promises";
 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");
+const MANUAL_HEX_FIXTURE = path.join(FIXTURES, "generic", "load-manual.hex");
@@
     await page
       .getByTestId("manual-input-field")
-      .fill("0x00 03 00 01 00 0d 00 08 00 02 00 07 00 01");
+      .fill((await readFile(MANUAL_HEX_FIXTURE, "utf8")).trim());

As per coding guidelines, Use fixtures/ directory for example programs and test data; do NOT embed hex strings in source code.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.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.
import { readFile } from "node:fs/promises";
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");
const MANUAL_HEX_FIXTURE = path.join(FIXTURES, "generic", "load-manual.hex");
const hexPayload = (await readFile(MANUAL_HEX_FIXTURE, "utf8")).trim();
await page
.getByTestId("manual-input-field")
.fill(hexPayload);
// Blur so the byte count appears.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/screenshots/01-load.screenshot.ts` around lines 45 - 47, The hex
payload string passed into .getByTestId("manual-input-field").fill(...) is
hardcoded; move that payload into a fixtures file (e.g.,
fixtures/manual-hex-payload.hex), update the test to read the fixture (via your
test fixture helper or a simple fs.readFileSync/await readFile) and pass the
file contents to .fill, trimming any trailing newline; replace the literal
string in the .fill call with the variable holding the fixture content so the
test complies with the repo's test-data rule and is reusable.

await page.getByTestId("manual-input-field").blur();
await settle(page);
await capture(page, "load-manual.png");
});
});
23 changes: 23 additions & 0 deletions apps/web/screenshots/02-config.screenshot.ts
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");
});
});
59 changes: 59 additions & 0 deletions apps/web/screenshots/03-debugger.screenshot.ts
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");
});
});
46 changes: 46 additions & 0 deletions apps/web/screenshots/04-settings.screenshot.ts
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Assert toggle state after enabling ananas to reduce flakiness.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!isChecked) {
await ananasSwitch.click();
}
if (!isChecked) {
await ananasSwitch.click();
await expect(ananasSwitch).toBeChecked();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/screenshots/04-settings.screenshot.ts` around lines 29 - 31, After
clicking the switch when isChecked is false, explicitly assert the control is
now checked to avoid flakiness: after await ananasSwitch.click() (in the code
that references isChecked and ananasSwitch) add an await that verifies the
switch state (e.g., await ananasSwitch.isChecked() asserted to true or using
your test framework's expect(await ananasSwitch.isChecked()).toBe(true)) before
proceeding to close the drawer so the enabled state is deterministic.

// 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");
});
});
25 changes: 25 additions & 0 deletions apps/web/screenshots/05-persistence.screenshot.ts
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");
});
});
88 changes: 88 additions & 0 deletions apps/web/screenshots/06-host-calls.screenshot.ts
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");
});
});
Loading
Loading