diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..664a3ec --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,12 @@ +#!/usr/bin/env sh +# Block pushes unless the full test suite passes. + +set -eu + +echo "🧪 Running npm test before push..." +npm test --silent || { + echo "❌ npm test failed; push aborted." + exit 1 +} + +echo "✅ npm test passed; proceeding with push." diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c6e138..cb21f8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,8 +14,8 @@ jobs: - uses: actions/setup-node@v4 with: { node-version: '20' } - run: npm ci - - name: Unit & property tests - run: npm test + - name: Core test suite + run: npm run test:core - name: Upload coverage reports uses: actions/upload-artifact@v4 with: diff --git a/docs/codeCoverageIgnoreGuidelines.md b/docs/codeCoverageIgnoreGuidelines.md index bd05c49..dccd754 100644 --- a/docs/codeCoverageIgnoreGuidelines.md +++ b/docs/codeCoverageIgnoreGuidelines.md @@ -5,7 +5,7 @@ This document defines how to use coverage-ignore comments in this repository. It ## TL;DR - `/* istanbul ignore next */` excludes the **next AST node** from coverage. -- Use ignores **sparingly** and **only** for code that is *truly* untestable or irrelevant to product behavior. +- Use ignores **sparingly** and **only** for code that is _truly_ untestable or irrelevant to product behavior. - Every ignore **must include a reason** right next to it. - Prefer tests, refactors, or config-level excludes over in-source ignores. @@ -30,24 +30,33 @@ Use an ignore only when exercising the code in automated tests is impractical or 1. **Unreachable defensive code** Exhaustive switch fallthroughs, invariant guards, or “should never happen” paths that exist purely as safety nets. + ```ts - type Kind = "A" | "B" - function assertNever(x: never): never { throw new Error("unreachable") } + type Kind = 'A' | 'B'; + function assertNever(x: never): never { + throw new Error('unreachable'); + } switch (kind) { - case "A": handleA(); break - case "B": handleB(); break + case 'A': + handleA(); + break; + case 'B': + handleB(); + break; /* istanbul ignore next -- defensive, unreachable by construction */ - default: assertNever(kind as never) + default: + assertNever(kind as never); } + ``` 2. **Platform-/environment-specific branches** Behavior that cannot be exercised in CI or across all supported OSes without unrealistic setups. ```ts - if (process.platform === "win32") { + if (process.platform === 'win32') { /* istanbul ignore next -- requires native Windows console; not in CI image */ - enableWindowsConsoleMode() + enableWindowsConsoleMode(); } ``` @@ -61,11 +70,11 @@ Use an ignore only when exercising the code in automated tests is impractical or ## When it is **not** acceptable -* To boost coverage percentages or hide missing tests. -* On **business logic** or any behavior affecting users. -* Broadly before `if`/`switch`/function declarations that mask multiple branches or large regions. -* As a substitute for a **small refactor** that would make testing feasible (e.g., splitting out side effects, injecting dependencies). -* For convenience when a test is mildly inconvenient to write (e.g., mocking a timer or a rejected promise). +- To boost coverage percentages or hide missing tests. +- On **business logic** or any behavior affecting users. +- Broadly before `if`/`switch`/function declarations that mask multiple branches or large regions. +- As a substitute for a **small refactor** that would make testing feasible (e.g., splitting out side effects, injecting dependencies). +- For convenience when a test is mildly inconvenient to write (e.g., mocking a timer or a rejected promise). --- @@ -97,10 +106,10 @@ Use an ignore only when exercising the code in automated tests is impractical or ## Preferred alternatives to ignores -* **Write a focused test**: Use dependency injection, seam extraction, or a small adapter to isolate side effects. -* **Refactor for testability**: Split logic from I/O; return values instead of printing; pass a clock/random source. -* **Use config excludes for generated code**: Keep production logic fully measured. -* **Switch directive, not scope**: Prefer `ignore if/else` over `ignore next` when only one branch is untestable. +- **Write a focused test**: Use dependency injection, seam extraction, or a small adapter to isolate side effects. +- **Refactor for testability**: Split logic from I/O; return values instead of printing; pass a clock/random source. +- **Use config excludes for generated code**: Keep production logic fully measured. +- **Switch directive, not scope**: Prefer `ignore if/else` over `ignore next` when only one branch is untestable. --- @@ -135,14 +144,14 @@ Jest example (if using V8 coverage): // jest.config.js module.exports = { collectCoverage: true, - coverageProvider: "v8", + coverageProvider: 'v8', coveragePathIgnorePatterns: [ - "/node_modules/", - "/dist/", - "/build/", - "\\.gen\\." - ] -} + '/node_modules/', + '/dist/', + '/build/', + '\\.gen\\.', + ], +}; ``` > Align comment style with the active provider: `istanbul` for Babel/nyc instrumentation; `c8` for V8. @@ -155,28 +164,32 @@ module.exports = { ```js // scripts/check-coverage-ignores.mjs -import { readFileSync } from "node:fs"; -import { globby } from "globby"; +import { readFileSync } from 'node:fs'; +import { globby } from 'globby'; -const files = await globby(["src/**/*.{ts,tsx,js,jsx}"], { gitignore: true }); +const files = await globby(['src/**/*.{ts,tsx,js,jsx}'], { gitignore: true }); const offenders = []; const re = /(istanbul|c8)\s+ignore\s+(next|if|else|file)/; for (const f of files) { - const lines = readFileSync(f, "utf8").split("\n"); + const lines = readFileSync(f, 'utf8').split('\n'); for (let i = 0; i < lines.length; i++) { if (re.test(lines[i])) { const hasReason = - /--\s*[A-Za-z0-9]/.test(lines[i]) || (i > 0 && /--\s*[A-Za-z0-9]/.test(lines[i - 1])); - if (!hasReason) offenders.push(`${f}:${i + 1}: missing reason after ignore comment`); + /--\s*[A-Za-z0-9]/.test(lines[i]) || + (i > 0 && /--\s*[A-Za-z0-9]/.test(lines[i - 1])); + if (!hasReason) + offenders.push(`${f}:${i + 1}: missing reason after ignore comment`); } } } if (offenders.length) { - console.error("Coverage ignore comments require an inline reason (use `-- reason`)."); - console.error(offenders.join("\n")); + console.error( + 'Coverage ignore comments require an inline reason (use `-- reason`).' + ); + console.error(offenders.join('\n')); process.exit(1); } ``` @@ -200,7 +213,13 @@ Optional ESLint guard (warn on any usage): "no-restricted-comments": [ "warn", { - "terms": ["istanbul ignore next", "istanbul ignore if", "istanbul ignore else", "istanbul ignore file", "c8 ignore next"], + "terms": [ + "istanbul ignore next", + "istanbul ignore if", + "istanbul ignore else", + "istanbul ignore file", + "c8 ignore next" + ], "location": "anywhere", "message": "Coverage ignore detected: add `-- reason` and ensure policy compliance." } @@ -217,11 +236,10 @@ Optional ESLint guard (warn on any usage): ```ts if (cacheEnabled) { - warmCache() -} -/* istanbul ignore else -- cold path is a telemetry-only fallback */ -else { - coldStartWithTelemetry() + warmCache(); +} else { + /* istanbul ignore else -- cold path is a telemetry-only fallback */ + coldStartWithTelemetry(); } ``` @@ -231,7 +249,7 @@ else { // Calls a native API that only exists on macOS ≥ 13: if (isDarwin13Plus()) { /* istanbul ignore next -- native API unavailable in CI runners */ - enableFancyTerminal() + enableFancyTerminal(); } ``` @@ -252,18 +270,15 @@ nyc.exclude += ["src/generated/**"] // in package.json nyc config ``` 2. **Classify** - - * ✅ Legitimate (add/verify reason, minimize scope) - * 🟡 Replaceable (write a test or refactor) - * 🔴 Remove/ban (business logic, overly broad) + - ✅ Legitimate (add/verify reason, minimize scope) + - 🟡 Replaceable (write a test or refactor) + - 🔴 Remove/ban (business logic, overly broad) 3. **Refactor & test** - - * Extract logic from side effects; inject collaborators; mock clocks/randomness. + - Extract logic from side effects; inject collaborators; mock clocks/randomness. 4. **Guard** - - * Add CI script and ESLint rule to prevent regressions. + - Add CI script and ESLint rule to prevent regressions. --- @@ -282,7 +297,7 @@ A: Use one approach consistently. If switching to V8 coverage, update directives ## Checklist for new code -* [ ] Coverage added for changed behavior. -* [ ] No new `istanbul`/`c8` ignores **unless** justified and minimal. -* [ ] Each ignore has `-- reason` and (optionally) a ticket reference. -* [ ] Generated/vendor code excluded via config, not inline comments. \ No newline at end of file +- [ ] Coverage added for changed behavior. +- [ ] No new `istanbul`/`c8` ignores **unless** justified and minimal. +- [ ] Each ignore has `-- reason` and (optionally) a ticket reference. +- [ ] Generated/vendor code excluded via config, not inline comments. diff --git a/docs/tests-and-lint-as-precommit-hook.md b/docs/tests-and-lint-as-precommit-hook.md index 32a4a48..e3d1aee 100644 --- a/docs/tests-and-lint-as-precommit-hook.md +++ b/docs/tests-and-lint-as-precommit-hook.md @@ -1,10 +1,13 @@ -Instructions to add a **native Git pre-commit hook** that blocks commits unless **lint** and **tests** pass: +Instructions to add a **native Git pre-push hook** that blocks pushes unless your selected test command succeeds. --- ## 0) Prereqs (once) -Ensure `package.json` has lint and test scripts defined. +Confirm the command you plan to run (default `npm test`) already covers the checks you want to enforce before pushing. + +- In this template, `npm test` runs `test:core` (format + lint + dependency checks + unit/property tests) followed by `test:e2e:index` (Playwright). +- If you only want the lighter core suite at push time, swap in `npm run test:core --silent` in Step 2. --- @@ -15,45 +18,33 @@ mkdir -p .githooks git config core.hooksPath .githooks ``` -> This tells Git to use `.githooks/` (which is tracked in the repo) instead of the untracked `.git/hooks/`. +This tells Git to use the tracked `.githooks/` directory instead of `.git/hooks/`. --- -## 2) Create the pre-commit hook +## 2) Create the pre-push hook -Create `.githooks/pre-commit` with the following content: +Create `.githooks/pre-push` with the following content: ```bash #!/usr/bin/env sh -# Abort the commit if lint or tests fail. -# Works on macOS/Linux and in Git Bash on Windows. - set -eu -echo "🔎 Linting..." -npm run -s lint || { echo "❌ Lint failed"; exit 1; } - -echo "🧪 Running tests..." -npm test --silent || { echo "❌ Tests failed"; exit 1; } +echo "Running npm test before push..." +npm test --silent || { + echo "npm test failed; push aborted." + exit 1 +} -echo "✅ Pre-commit checks passed" +echo "npm test passed; proceeding with push." ``` -Make it executable (choose one): +Mark it executable (choose one): ```bash -# Portable way (works on Windows too): -git update-index --chmod=+x .githooks/pre-commit - -# Or the POSIX way: -chmod +x .githooks/pre-commit -``` - -Commit the hook: - -```bash -git add .githooks/pre-commit -git commit -m "chore: add native pre-commit hook for lint and tests" +git update-index --chmod=+x .githooks/pre-push +# or +chmod +x .githooks/pre-push ``` --- @@ -61,13 +52,15 @@ git commit -m "chore: add native pre-commit hook for lint and tests" ## 3) Quick self-test ```bash -git commit --allow-empty -m "test hook" -# Expect the hook to run; commit only succeeds if lint & tests pass. +./.githooks/pre-push ``` +Running the hook manually should execute the same checks Git will run before a push. + --- ## Additional Notes -- Developers can bypass local hooks with `--no-verify`, so keep CI with required checks as the final gate. -- For faster commits, consider moving long-running suites to a `pre-push` hook or to CI. +- Developers can bypass local hooks with `--no-verify`, so keep CI as the final gate. +- If you want a lighter-weight push check, swap `npm test --silent` for `npm run test:core --silent` to skip e2e tests. +- Re-run the `git config core.hooksPath .githooks` command on every machine or clone so Git picks up the tracked hooks. diff --git a/docs/threeJs un test environments.md b/docs/threeJs un test environments.md index aa4a0a0..e3936a4 100644 --- a/docs/threeJs un test environments.md +++ b/docs/threeJs un test environments.md @@ -10,26 +10,26 @@ General playbook: start with pure math in Node; add jsdom only when the DOM is i **A. Pure unit tests (math & data structures) — Node env** -* What to test: `Color`, `Vector*`, `Matrix4`, material parameter objects, helpers with no DOM. -* Environment: `testEnvironment: "node"` (fast, no DOM). -* ESM note: Three.js is ESM; configure Jest for ESM (see §3). Jest’s ESM support is still labeled “experimental”, so follow their steps (e.g., `--experimental-vm-modules`) ([jestjs.io][1]). +- What to test: `Color`, `Vector*`, `Matrix4`, material parameter objects, helpers with no DOM. +- Environment: `testEnvironment: "node"` (fast, no DOM). +- ESM note: Three.js is ESM; configure Jest for ESM (see §3). Jest’s ESM support is still labeled “experimental”, so follow their steps (e.g., `--experimental-vm-modules`) ([jestjs.io][1]). **B. DOM integration (no WebGL) — jsdom env** -* What to test: code that touches `document`, sizes a ``, event wiring, etc. -* Environment: `jest-environment-jsdom` (install it—Jest stopped bundling it in v28+) ([jestjs.io][2]). -* Canvas note: jsdom treats `` like a `
` unless you add the `canvas` package or a mock; otherwise `getContext` throws “Not implemented” ([GitHub][3]). +- What to test: code that touches `document`, sizes a ``, event wiring, etc. +- Environment: `jest-environment-jsdom` (install it—Jest stopped bundling it in v28+) ([jestjs.io][2]). +- Canvas note: jsdom treats `` like a `
` unless you add the `canvas` package or a mock; otherwise `getContext` throws “Not implemented” ([GitHub][3]). **C. WebGL integration in Node — “real” GL via headless-gl** -* Goal: run `WebGLRenderer` for pixel-level assertions without a browser. -* Tooling: inject a WebGL context from **headless-gl** (`gl` package). It provides a WebGL context in Node, aiming at WebGL 1.0.3 (WebGL2 support is marked experimental) ([GitHub][4]). +- Goal: run `WebGLRenderer` for pixel-level assertions without a browser. +- Tooling: inject a WebGL context from **headless-gl** (`gl` package). It provides a WebGL context in Node, aiming at WebGL 1.0.3 (WebGL2 support is marked experimental) ([GitHub][4]). **D. Visual/E2E — real browser (Playwright)** -* Goal: catch rendering regressions with screenshots. -* Use Playwright’s built-in screenshot snapshots: `await expect(page).toHaveScreenshot()` ([Playwright][5]). -* For headless WebGL performance/stability, pass GPU flags like `--use-gl=egl`/`--use-gl=desktop` (Chrome/Chromium); teams report big wins with those on CI ([Michel Krämer’s portfolio and blog][6]). +- Goal: catch rendering regressions with screenshots. +- Use Playwright’s built-in screenshot snapshots: `await expect(page).toHaveScreenshot()` ([Playwright][5]). +- For headless WebGL performance/stability, pass GPU flags like `--use-gl=egl`/`--use-gl=desktop` (Chrome/Chromium); teams report big wins with those on CI ([Michel Krämer’s portfolio and blog][6]). --- @@ -38,15 +38,13 @@ General playbook: start with pure math in Node; add jsdom only when the DOM is i 1. **Keep math logic separate** from renderer code. Most of Three’s classes are pure and test nicely in Node. 2. **When you need `WebGLRenderer` in Jest**, prefer: - - * **Node + headless-gl** for fast, deterministic pixel tests (WebGL1 feature set), or - * **Playwright** for WebGL2 features, GPU-specific paths, and full browser semantics. + - **Node + headless-gl** for fast, deterministic pixel tests (WebGL1 feature set), or + - **Playwright** for WebGL2 features, GPU-specific paths, and full browser semantics. 3. **For DOM-only tests**, use jsdom and (optionally) `canvas`/`jest-canvas-mock` only when you touch `` APIs; don’t pay the cost otherwise. jsdom’s canvas support requires the `canvas` peer dependency; without it, `` behaves like a `
` and `getContext` is unimplemented ([GitHub][3]). 4. **Do serialized/state snapshots instead of object graph snapshots**: - - * Use `object3D.toJSON()` and `ObjectLoader` for stable, text-based snapshots of scenes/meshes/materials ([threejs.org][7]). + - Use `object3D.toJSON()` and `ObjectLoader` for stable, text-based snapshots of scenes/meshes/materials ([threejs.org][7]). 5. **When you need pixels**, render to a **`WebGLRenderTarget`** and read with `renderer.readRenderTargetPixels(...)`. It wraps `gl.readPixels` and works well in Node+headless-gl. Be mindful of types (reads `UnsignedByteType`) and limitations (not MRT/MSAA targets) ([threejs.org][8]). @@ -74,10 +72,10 @@ General playbook: start with pure math in Node; add jsdom only when the DOM is i ```js /** @type {import('jest').Config} */ export default { - testEnvironment: "node", - transform: {}, // don't transpile to CJS; keep ESM - extensionsToTreatAsEsm: [".ts", ".tsx", ".jsx"], - moduleNameMapper: {}, // if you alias paths + testEnvironment: 'node', + transform: {}, // don't transpile to CJS; keep ESM + extensionsToTreatAsEsm: ['.ts', '.tsx', '.jsx'], + moduleNameMapper: {}, // if you alias paths }; ``` @@ -95,16 +93,16 @@ npm i -D jest-environment-jsdom canvas ```js export default { - testEnvironment: "jsdom", - setupFilesAfterEnv: ["/test/setup-canvas.ts"] -} + testEnvironment: 'jsdom', + setupFilesAfterEnv: ['/test/setup-canvas.ts'], +}; ``` **`test/setup-canvas.ts`** ```ts // If you need a 2D canvas API in jsdom: -import "canvas"; // jsdom will pick this up automatically (peer dep) +import 'canvas'; // jsdom will pick this up automatically (peer dep) // Or, alternative: jest-canvas-mock if you only need basic APIs: // import "jest-canvas-mock"; ``` @@ -123,8 +121,8 @@ npm i -D gl ```js export default { - testEnvironment: "jsdom", // we want a DOM + our own WebGL context - setupFilesAfterEnv: ["/test/setup-webgl.ts"], + testEnvironment: 'jsdom', // we want a DOM + our own WebGL context + setupFilesAfterEnv: ['/test/setup-webgl.ts'], transform: {}, }; ``` @@ -133,11 +131,11 @@ export default { ```ts // Inject a WebGL (WebGL1) context for .getContext('webgl') -import createGL from "gl"; +import createGL from 'gl'; const orig = HTMLCanvasElement.prototype.getContext; -HTMLCanvasElement.prototype.getContext = function(type: string, attrs?: any) { - if (type === "webgl" || type === "experimental-webgl") { +HTMLCanvasElement.prototype.getContext = function (type: string, attrs?: any) { + if (type === 'webgl' || type === 'experimental-webgl') { const w = this.width || 1; const h = this.height || 1; const gl = createGL(w, h, { preserveDrawingBuffer: true, ...attrs }); @@ -147,21 +145,24 @@ HTMLCanvasElement.prototype.getContext = function(type: string, attrs?: any) { }; ``` -* `gl` creates a WebGL context in Node and targets WebGL 1.0.3; WebGL2 is experimental. For WebGL2 features, prefer Playwright (browser) tests ([GitHub][4]). -* Community pattern: mock `getContext` to return headless-gl in Jest/jsdom ([three.js forum][9]). +- `gl` creates a WebGL context in Node and targets WebGL 1.0.3; WebGL2 is experimental. For WebGL2 features, prefer Playwright (browser) tests ([GitHub][4]). +- Community pattern: mock `getContext` to return headless-gl in Jest/jsdom ([three.js forum][9]). **Example pixel test (Node):** ```ts -import * as THREE from "three"; +import * as THREE from 'three'; -test("renders red pixel", () => { - const canvas = document.createElement("canvas"); +test('renders red pixel', () => { + const canvas = document.createElement('canvas'); canvas.width = canvas.height = 4; - const renderer = new THREE.WebGLRenderer({ canvas, preserveDrawingBuffer: true }); + const renderer = new THREE.WebGLRenderer({ + canvas, + preserveDrawingBuffer: true, + }); const scene = new THREE.Scene(); - scene.background = new THREE.Color("red"); + scene.background = new THREE.Color('red'); const camera = new THREE.PerspectiveCamera(60, 1, 0.1, 10); camera.position.z = 1; @@ -175,8 +176,8 @@ test("renders red pixel", () => { const buf = new Uint8Array(4); renderer.readRenderTargetPixels(rt, 0, 0, 1, 1, buf); // wraps gl.readPixels expect(buf[0]).toBe(255); // R - expect(buf[1]).toBe(0); // G - expect(buf[2]).toBe(0); // B + expect(buf[1]).toBe(0); // G + expect(buf[2]).toBe(0); // B rt.dispose(); renderer.dispose(); // frees GPU resources @@ -203,11 +204,9 @@ export default defineConfig({ use: { headless: true, // Enable GPU in headless Chromium for WebGL stability/perf in CI: - launchOptions: { args: ['--use-gl=egl'] } // or '--use-gl=desktop' + launchOptions: { args: ['--use-gl=egl'] }, // or '--use-gl=desktop' }, - projects: [ - { name: 'Chromium', use: { ...devices['Desktop Chrome'] } }, - ] + projects: [{ name: 'Chromium', use: { ...devices['Desktop Chrome'] } }], }); ``` @@ -232,29 +231,28 @@ test('canvas rendering is stable', async ({ page }) => { ## 4) Patterns for reliable, maintainable Three.js tests -* **Prefer small offscreen buffers.** Render to a tiny `WebGLRenderTarget` (e.g., 64×64) for speed and stability, then read a pixel/patch with `readRenderTargetPixels` ([threejs.org][8]). - -* **Snapshot structure, not instances.** Three objects contain cycles and methods; JSON-snapshot with `object.toJSON()` and reconstruct via `ObjectLoader` when needed ([threejs.org][7]). +- **Prefer small offscreen buffers.** Render to a tiny `WebGLRenderTarget` (e.g., 64×64) for speed and stability, then read a pixel/patch with `readRenderTargetPixels` ([threejs.org][8]). -* **Be deterministic.** Seed `Math.random` (e.g., with `seedrandom`) for particle initializers or jittered sampling; keep camera and lights fixed. +- **Snapshot structure, not instances.** Three objects contain cycles and methods; JSON-snapshot with `object.toJSON()` and reconstruct via `ObjectLoader` when needed ([threejs.org][7]). -* **Stabilize renderer output.** Fix renderer size, color space and tone mapping in tests to avoid diff noise. (E.g., set size, background, and avoid dynamic exposure.) +- **Be deterministic.** Seed `Math.random` (e.g., with `seedrandom`) for particle initializers or jittered sampling; keep camera and lights fixed. -* **Clean up.** Call `dispose()` on `WebGLRenderer`, `WebGLRenderTarget`, `Geometry/BufferGeometry`, `Material` and `Texture`s you create. Do not render after disposing the renderer; it removes important listeners and should end its lifecycle ([threejs.org][8]). +- **Stabilize renderer output.** Fix renderer size, color space and tone mapping in tests to avoid diff noise. (E.g., set size, background, and avoid dynamic exposure.) -* **jsdom gotchas.** +- **Clean up.** Call `dispose()` on `WebGLRenderer`, `WebGLRenderTarget`, `Geometry/BufferGeometry`, `Material` and `Texture`s you create. Do not render after disposing the renderer; it removes important listeners and should end its lifecycle ([threejs.org][8]). - * Without `canvas` (or a mock), `HTMLCanvasElement.prototype.getContext` is unimplemented and returns errors; add `canvas` or a mock package ([GitHub][3]). - * A canvas can only have one context type; requesting `2d` first will prevent a later WebGL context on the same canvas ([MDN Web Docs][10]). +- **jsdom gotchas.** + - Without `canvas` (or a mock), `HTMLCanvasElement.prototype.getContext` is unimplemented and returns errors; add `canvas` or a mock package ([GitHub][3]). + - A canvas can only have one context type; requesting `2d` first will prevent a later WebGL context on the same canvas ([MDN Web Docs][10]). -* **Know headless-gl’s limits.** WebGL2 features may not work; if you depend on them, run those tests in a real browser via Playwright ([GitHub][4]). +- **Know headless-gl’s limits.** WebGL2 features may not work; if you depend on them, run those tests in a real browser via Playwright ([GitHub][4]). --- ## 5) Optional: packaged mocks & visual matchers -* **`jest-webgl-canvas-mock`** (combines `jest-canvas-mock` + a WebGL mock) — good for libraries like Pixi or basic WebGL expectations when you don’t need real GPU behavior ([npm][11]). -* **`jest-image-snapshot`** — pixelmatch-based image comparator for Jest; useful when you can produce PNGs (e.g., via Playwright screenshots or `toDataURL` with `canvas`) ([GitHub][12]). +- **`jest-webgl-canvas-mock`** (combines `jest-canvas-mock` + a WebGL mock) — good for libraries like Pixi or basic WebGL expectations when you don’t need real GPU behavior ([npm][11]). +- **`jest-image-snapshot`** — pixelmatch-based image comparator for Jest; useful when you can produce PNGs (e.g., via Playwright screenshots or `toDataURL` with `canvas`) ([GitHub][12]). --- @@ -267,19 +265,19 @@ Run fast unit tests in Node, integration in jsdom+headless-gl: export default { projects: [ { - displayName: "unit", - testEnvironment: "node", - testMatch: ["/test/unit/**/*.test.(ts|js)"], + displayName: 'unit', + testEnvironment: 'node', + testMatch: ['/test/unit/**/*.test.(ts|js)'], transform: {}, }, { - displayName: "int-webgl", - testEnvironment: "jsdom", - setupFilesAfterEnv: ["/test/setup-webgl.ts"], - testMatch: ["/test/integration/**/*.test.(ts|js)"], + displayName: 'int-webgl', + testEnvironment: 'jsdom', + setupFilesAfterEnv: ['/test/setup-webgl.ts'], + testMatch: ['/test/integration/**/*.test.(ts|js)'], transform: {}, - } - ] + }, + ], }; ``` @@ -287,21 +285,21 @@ export default { ## 7) CI notes -* **Playwright + GPU:** on Linux runners, flags like `--use-gl=egl` are often necessary; Firefox headless historically disables WebGL, so prefer Chromium for WebGL E2E in headless mode ([Michel Krämer’s portfolio and blog][6]). -* **headless-gl system deps:** Linux may need Mesa/GL dev libs; see README for required packages and hints for CI images ([GitHub][4]). +- **Playwright + GPU:** on Linux runners, flags like `--use-gl=egl` are often necessary; Firefox headless historically disables WebGL, so prefer Chromium for WebGL E2E in headless mode ([Michel Krämer’s portfolio and blog][6]). +- **headless-gl system deps:** Linux may need Mesa/GL dev libs; see README for required packages and hints for CI images ([GitHub][4]). --- ## 8) Quick reference links -* Jest ESM guide (how to enable, flags, mocking in ESM) ([jestjs.io][1]) -* jsdom README — canvas support via `canvas` package ([GitHub][3]) -* headless-gl README (WebGL in Node; WebGL1 target, WebGL2 experimental; system deps) ([GitHub][4]) -* Playwright screenshot/snapshot assertions ([Playwright][5]) -* `WebGLRenderer.readRenderTargetPixels` and `dispose()` docs ([threejs.org][8]) -* `Object3D.toJSON` / `ObjectLoader` for serializing scene graphs ([threejs.org][7]) -* jsdom “getContext” not implemented error background ([Stack Overflow][13]) -* GPU flags for headless Chromium (`--use-gl=egl`/`--use-gl=desktop`) ([Michel Krämer’s portfolio and blog][6]) +- Jest ESM guide (how to enable, flags, mocking in ESM) ([jestjs.io][1]) +- jsdom README — canvas support via `canvas` package ([GitHub][3]) +- headless-gl README (WebGL in Node; WebGL1 target, WebGL2 experimental; system deps) ([GitHub][4]) +- Playwright screenshot/snapshot assertions ([Playwright][5]) +- `WebGLRenderer.readRenderTargetPixels` and `dispose()` docs ([threejs.org][8]) +- `Object3D.toJSON` / `ObjectLoader` for serializing scene graphs ([threejs.org][7]) +- jsdom “getContext” not implemented error background ([Stack Overflow][13]) +- GPU flags for headless Chromium (`--use-gl=egl`/`--use-gl=desktop`) ([Michel Krämer’s portfolio and blog][6]) --- @@ -318,16 +316,16 @@ Install `canvas` or a dedicated mock package; by default jsdom doesn’t impleme --- -[1]: https://jestjs.io/docs/ecmascript-modules "ECMAScript Modules · Jest" -[2]: https://jestjs.io/docs/next/tutorial-jquery?utm_source=chatgpt.com "DOM Manipulation" -[3]: https://github.com/jsdom/jsdom "GitHub - jsdom/jsdom: A JavaScript implementation of various web standards, for use with Node.js" -[4]: https://github.com/stackgl/headless-gl "GitHub - stackgl/headless-gl: Windowless WebGL for node.js" -[5]: https://playwright.dev/docs/test-snapshots?utm_source=chatgpt.com "Visual comparisons" -[6]: https://michelkraemer.com/enable-gpu-for-slow-playwright-tests-in-headless-mode/?utm_source=chatgpt.com "Enable GPU to speed up slow Playwright tests in headless ..." -[7]: https://threejs.org/docs/api/en/loaders/ObjectLoader.html?utm_source=chatgpt.com "ObjectLoader – three.js docs" -[8]: https://threejs.org/docs/?utm_source=chatgpt.com "WebGLRenderer#readRenderTargetPixels" -[9]: https://discourse.threejs.org/t/suggestions-for-unit-testing-with-headless-gl-and-webgl-2/66891?utm_source=chatgpt.com "Suggestions for unit testing with headless-gl and WebGL 2" -[10]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext?utm_source=chatgpt.com "HTMLCanvasElement: getContext() method - Web APIs - MDN" -[11]: https://www.npmjs.com/package/jest-webgl-canvas-mock?utm_source=chatgpt.com "jest-webgl-canvas-mock" -[12]: https://github.com/americanexpress/jest-image-snapshot?utm_source=chatgpt.com "americanexpress/jest-image-snapshot" -[13]: https://stackoverflow.com/questions/48828759/unit-test-raises-error-because-of-getcontext-is-not-implemented?utm_source=chatgpt.com "unit test raises error because of .getContext() is not ..." +[1]: https://jestjs.io/docs/ecmascript-modules 'ECMAScript Modules · Jest' +[2]: https://jestjs.io/docs/next/tutorial-jquery?utm_source=chatgpt.com 'DOM Manipulation' +[3]: https://github.com/jsdom/jsdom 'GitHub - jsdom/jsdom: A JavaScript implementation of various web standards, for use with Node.js' +[4]: https://github.com/stackgl/headless-gl 'GitHub - stackgl/headless-gl: Windowless WebGL for node.js' +[5]: https://playwright.dev/docs/test-snapshots?utm_source=chatgpt.com 'Visual comparisons' +[6]: https://michelkraemer.com/enable-gpu-for-slow-playwright-tests-in-headless-mode/?utm_source=chatgpt.com 'Enable GPU to speed up slow Playwright tests in headless ...' +[7]: https://threejs.org/docs/api/en/loaders/ObjectLoader.html?utm_source=chatgpt.com 'ObjectLoader – three.js docs' +[8]: https://threejs.org/docs/?utm_source=chatgpt.com 'WebGLRenderer#readRenderTargetPixels' +[9]: https://discourse.threejs.org/t/suggestions-for-unit-testing-with-headless-gl-and-webgl-2/66891?utm_source=chatgpt.com 'Suggestions for unit testing with headless-gl and WebGL 2' +[10]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext?utm_source=chatgpt.com 'HTMLCanvasElement: getContext() method - Web APIs - MDN' +[11]: https://www.npmjs.com/package/jest-webgl-canvas-mock?utm_source=chatgpt.com 'jest-webgl-canvas-mock' +[12]: https://github.com/americanexpress/jest-image-snapshot?utm_source=chatgpt.com 'americanexpress/jest-image-snapshot' +[13]: https://stackoverflow.com/questions/48828759/unit-test-raises-error-because-of-getcontext-is-not-implemented?utm_source=chatgpt.com 'unit test raises error because of .getContext() is not ...' diff --git a/package.json b/package.json index eb9b593..6ae58f5 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "TemplateJs is a minimal single-page web app template designed for quick deployment. It follows an AI-assisted iterative development process:", "main": "index.js", "scripts": { - "test": "npm run format && npm run lint && npm run check:all && npm run test:unit", + "test": "npm run test:core && npm run test:e2e:index", + "test:core": "npm run format && npm run lint && npm run check:all && npm run test:unit", "format": "prettier --write --ignore-unknown --no-error-on-unmatched-pattern \"src\" \"pages\" \"config\" \"docs\" index.html README.md package.json", "test:unit": "jest --coverage --config config/jest.config.js", "lint": "eslint . --config config/eslint.config.js", @@ -17,6 +18,7 @@ "validate:all": "npm run test && npm run mutation", "serve:static": "serve -l 4173 .", "test:e2e": "playwright test --config playwright-ui-tests/playwright.config.js", + "test:e2e:index": "playwright test --config playwright-ui-tests/playwright.config.js --project=chromium playwright-ui-tests/index.spec.js", "test:e2e:artifacts": "PLAYWRIGHT_CAPTURE=1 playwright test --config playwright-ui-tests/playwright.config.js", "test:e2e:headed": "playwright test --config playwright-ui-tests/playwright.config.js --headed", "test:e2e:ui": "playwright test --config playwright-ui-tests/playwright.config.js --ui" diff --git a/pages/about.html b/pages/about.html index d9246e7..8e71a1f 100644 --- a/pages/about.html +++ b/pages/about.html @@ -77,9 +77,7 @@