Skip to content

Commit 42e7964

Browse files
Add optional Playwright fuzz smoke test scaffold.
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent c7033f5 commit 42e7964

4 files changed

Lines changed: 187 additions & 2 deletions

File tree

package-lock.json

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

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@
3737
"clean": "rm -rf dist",
3838
"generate-svg-icon": "tsx scripts/opticsToSVG.ts",
3939
"icons": "npm run generate-svg-icon && tsx scripts/generate-icons.ts",
40-
"prepare": "git rev-parse --is-inside-work-tree >/dev/null 2>&1 && git config core.hooksPath .githooks || true"
40+
"prepare": "git rev-parse --is-inside-work-tree >/dev/null 2>&1 && git config core.hooksPath .githooks || true",
41+
"test:fuzz": "playwright test --project=chromium",
42+
"test:fuzz:quick": "FUZZ_DURATION=10 playwright test --project=chromium"
4143
},
4244
"dependencies": {
4345
"scenerystack": "^3.0.0"
@@ -54,7 +56,9 @@
5456
"typescript": "^7.0.2",
5557
"vite": "^8.1.5",
5658
"vite-plugin-pwa": "^1.3.0",
57-
"vitest": "^4.1.10"
59+
"vitest": "^4.1.10",
60+
"@playwright/test": "^1.61.1",
61+
"playwright": "^1.61.1"
5862
},
5963
"engines": {
6064
"node": ">=22.0.0"

playwright.config.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Playwright configuration for optional fuzz testing (OpticsLab smoke).
3+
*/
4+
5+
import { defineConfig } from "@playwright/test";
6+
7+
export default defineConfig({
8+
testDir: "./tests/fuzz",
9+
timeout: 5 * 60 * 1000,
10+
expect: {
11+
timeout: 10_000,
12+
},
13+
fullyParallel: false,
14+
forbidOnly: !!process.env.CI,
15+
retries: 0,
16+
workers: 1,
17+
reporter: [["list"], ["html", { open: "never" }]],
18+
use: {
19+
baseURL: "http://localhost:5173",
20+
trace: "retain-on-failure",
21+
video: "retain-on-failure",
22+
screenshot: "only-on-failure",
23+
},
24+
webServer: {
25+
command: "npm run start",
26+
url: "http://localhost:5173",
27+
reuseExistingServer: !process.env.CI,
28+
timeout: 120_000,
29+
},
30+
projects: [
31+
{
32+
name: "chromium",
33+
use: {
34+
browserName: "chromium",
35+
},
36+
},
37+
],
38+
});

tests/fuzz/fuzz.spec.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* Optional Playwright fuzz smoke for the OpticsLab simulation.
3+
*
4+
* Usage:
5+
* npm run test:fuzz
6+
* npm run test:fuzz:quick
7+
* FUZZ_SEED=12345 npm run test:fuzz
8+
*/
9+
10+
import { expect, test } from "@playwright/test";
11+
12+
const FUZZ_DURATION = parseInt(process.env["FUZZ_DURATION"] || "15", 10) * 1000;
13+
const FUZZ_SEED = process.env["FUZZ_SEED"] || Math.floor(Math.random() * 1_000_000).toString();
14+
const FUZZ_RATE = process.env["FUZZ_RATE"] || "100";
15+
const FUZZ_POINTERS = process.env["FUZZ_POINTERS"] || "1";
16+
17+
interface ConsoleMessage {
18+
type: string;
19+
text: string;
20+
location: string;
21+
timestamp: number;
22+
}
23+
24+
test.describe("Fuzz Testing", () => {
25+
test("should run without console errors", async ({ page }) => {
26+
const errors: ConsoleMessage[] = [];
27+
const assertions: ConsoleMessage[] = [];
28+
const startTime = Date.now();
29+
30+
const fuzzUrl = `/?fuzz&randomSeed=${FUZZ_SEED}&fuzzRate=${FUZZ_RATE}&fuzzPointers=${FUZZ_POINTERS}`;
31+
32+
page.on("console", (msg) => {
33+
const type = msg.type();
34+
const text = msg.text();
35+
const location = msg.location();
36+
const timestamp = Date.now() - startTime;
37+
const message: ConsoleMessage = {
38+
type,
39+
text,
40+
location: `${location.url}:${location.lineNumber}:${location.columnNumber}`,
41+
timestamp,
42+
};
43+
if (type === "error") {
44+
errors.push(message);
45+
} else if (text.includes("Assertion failed") || text.includes("AssertionError")) {
46+
assertions.push(message);
47+
}
48+
});
49+
50+
page.on("pageerror", (error) => {
51+
errors.push({
52+
type: "pageerror",
53+
text: error.message,
54+
location: error.stack || "unknown",
55+
timestamp: Date.now() - startTime,
56+
});
57+
});
58+
59+
await page.goto(fuzzUrl);
60+
await page.waitForSelector("#sim", { timeout: 30_000 });
61+
62+
const checkInterval = 2000;
63+
let elapsed = 0;
64+
while (elapsed < FUZZ_DURATION) {
65+
const waitTime = Math.min(checkInterval, FUZZ_DURATION - elapsed);
66+
await page.waitForTimeout(waitTime);
67+
elapsed += waitTime;
68+
try {
69+
await page.evaluate(() => window.document.hasFocus);
70+
} catch {
71+
break;
72+
}
73+
}
74+
75+
expect(errors.length, `Found ${errors.length} console errors`).toBe(0);
76+
expect(assertions.length, `Found ${assertions.length} assertion failures`).toBe(0);
77+
});
78+
});

0 commit comments

Comments
 (0)