Skip to content
Draft
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ bun install
bun run dev # Vite on 127.0.0.1:5173
bun run typecheck # incremental app + test typecheck
bun run test:smoke # fast browser health, no captures
bun run test:grader # inspect the live Three.js scene through Playwright
bun run check:fast # typecheck + browser health
bun run check:final # typecheck + production build + visual captures
bun run capture # agent-readable JPEG screenshots into artifacts/
Expand All @@ -28,6 +29,7 @@ src/main.ts # Three.js entry (swap for task code)
src/style.css
tests/browser-health.smoke.test.ts # fast screenshot-free health check
tests/smoke.test.ts # final interaction and capture check
grader/ # self-contained live Three.js Playwright grader
playwright.config.ts
vite.config.ts
tsconfig.json
Expand Down
11 changes: 11 additions & 0 deletions grader/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Three.js grader

This suite reads the application's live Three.js scene from Playwright. It
checks that the app completes a render with a connected WebGL canvas and that
its render loop continues to advance.

Run it from the repository root:

```bash
bun run test:grader
```
14 changes: 14 additions & 0 deletions grader/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { expect, test as playwrightTest } from "@playwright/test";
import { attachThree, type Three } from "./three-playwright/index.js";

interface ThreeFixtures {
three: Three;
}

export const test = playwrightTest.extend<ThreeFixtures>({
three: async ({ page }, useThree) => {
await useThree(await attachThree(page));
},
});

export { expect };
19 changes: 19 additions & 0 deletions grader/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { defineConfig } from "@playwright/test";

export default defineConfig({
testDir: ".",
testMatch: "**/*.spec.ts",
fullyParallel: false,
workers: 1,
use: {
baseURL: "http://127.0.0.1:5173",
deviceScaleFactor: 1,
viewport: { width: 960, height: 540 },
},
webServer: {
command: "bun run dev",
reuseExistingServer: true,
timeout: 120_000,
url: "http://127.0.0.1:5173",
},
});
56 changes: 56 additions & 0 deletions grader/scene.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { expect, test } from "./fixtures.js";

test("exposes the live Three.js render context", async ({ page, three }) => {
const browserErrors: string[] = [];
page.on("console", (message) => {
if (message.type() === "error") browserErrors.push(`console: ${message.text()}`);
});
page.on("pageerror", (error) => browserErrors.push(`page: ${error.message}`));
page.on("crash", () => browserErrors.push("page crashed"));

await page.goto("/", { waitUntil: "domcontentloaded" });

const snapshot = await three(({ camera, renderer, scene }) => ({
cameraType: camera.type,
canvas: {
connected: renderer.domElement.isConnected,
height: renderer.domElement.height,
width: renderer.domElement.width,
},
frame: renderer.info.render.frame,
rendererIsWebGl: Reflect.get(renderer, "isWebGLRenderer") === true,
sceneType: scene.type,
sceneUuid: scene.uuid,
}));

expect(snapshot).toMatchObject({
cameraType: "PerspectiveCamera",
canvas: { connected: true },
rendererIsWebGl: true,
sceneType: "Scene",
});
expect(snapshot.canvas.width).toBeGreaterThan(0);
expect(snapshot.canvas.height).toBeGreaterThan(0);
expect(snapshot.frame).toBeGreaterThan(0);
expect(snapshot.sceneUuid).toMatch(/^[0-9a-f-]{36}$/i);
expect(browserErrors).toEqual([]);
});

test("keeps rendering the same scene", async ({ page, three }) => {
await page.goto("/", { waitUntil: "domcontentloaded" });

const first = await three(({ renderer, scene }) => ({
frame: renderer.info.render.frame,
sceneUuid: scene.uuid,
}));

await expect
.poll(() => three(({ renderer }) => renderer.info.render.frame), {
message: "the Three.js render loop should continue producing frames",
timeout: 5_000,
})
.toBeGreaterThan(first.frame);

const sceneUuid = await three(({ scene }) => scene.uuid);
expect(sceneUuid).toBe(first.sceneUuid);
});
Loading