From b0f27951fb55841e8b8a888dd27da6dea9bf551e Mon Sep 17 00:00:00 2001 From: Hunter Garrett Date: Sun, 23 Aug 2026 22:43:40 -0400 Subject: [PATCH 1/6] test: add a browser-mode Vitest project (BON-4) Co-Authored-By: Claude Fable 5 --- packages/bones/package.json | 3 +- packages/bones/tests/browser/smoke.test.ts | 14 ++++++ packages/bones/vite.config.ts | 54 +++++++++++++++++++++- pnpm-lock.yaml | 29 ++++++++++++ 4 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 packages/bones/tests/browser/smoke.test.ts diff --git a/packages/bones/package.json b/packages/bones/package.json index cd8485d..b787eec 100644 --- a/packages/bones/package.json +++ b/packages/bones/package.json @@ -43,7 +43,7 @@ "dev": "vp pack --watch", "test": "vp test", "check": "vp check", - "health": "vp test --coverage && fallow health --root . --coverage coverage/coverage-final.json --format json --quiet", + "health": "vp test --project unit --coverage && fallow health --root . --coverage coverage/coverage-final.json --format json --quiet", "prepublishOnly": "vp run build" }, "devDependencies": { @@ -55,6 +55,7 @@ "@typescript/native-preview": "7.0.0-dev.20260328.1", "@vitest/coverage-v8": "^4.1.5", "jsdom": "^29.0.2", + "playwright": "^1.62.1", "react": "^19.2.5", "typescript": "^6.0.2", "vite-plus": "^0.1.14" diff --git a/packages/bones/tests/browser/smoke.test.ts b/packages/bones/tests/browser/smoke.test.ts new file mode 100644 index 0000000..445bd6e --- /dev/null +++ b/packages/bones/tests/browser/smoke.test.ts @@ -0,0 +1,14 @@ +import { expect, test } from "vite-plus/test"; + +// Proves the browser project runs in a real layout engine: jsdom returns +// all-zero rects, Chromium cannot. +test("layout engine is real", () => { + const el = document.createElement("div"); + el.textContent = "measure me"; + el.style.font = "16px/1.5 monospace"; + document.body.append(el); + const rect = el.getBoundingClientRect(); + expect(rect.height).toBeGreaterThan(0); + expect(rect.width).toBeGreaterThan(0); + el.remove(); +}); diff --git a/packages/bones/vite.config.ts b/packages/bones/vite.config.ts index 8f41a84..17a8a33 100644 --- a/packages/bones/vite.config.ts +++ b/packages/bones/vite.config.ts @@ -1,4 +1,5 @@ import { defineConfig } from "vite-plus"; +import { playwright } from "vite-plus/test/browser-playwright"; export default defineConfig({ staged: { @@ -28,11 +29,62 @@ export default defineConfig({ }, }, test: { - environment: "jsdom", coverage: { provider: "v8", include: ["src/**"], }, + projects: [ + { + extends: true, + test: { + name: "unit", + environment: "jsdom", + include: ["tests/*.test.{ts,tsx}"], + }, + }, + { + extends: true, + test: { + name: "browser", + include: ["tests/browser/**/*.test.{ts,tsx}"], + browser: { + enabled: true, + headless: true, + provider: playwright(), + instances: [{ browser: "chromium" }], + expect: { + // @ts-expect-error — vite-plus's vendored `ToMatchScreenshotComparators` + // is never populated by `vite-plus/test/browser-playwright`'s ambient + // `declare module "vitest/node"` augmentation, which targets the real + // `vitest` package rather than vite-plus's internal copy, so this + // (otherwise-valid) config object has no type to check against. + toMatchScreenshot: { + comparatorName: "pixelmatch", + comparatorOptions: { allowedMismatchedPixelRatio: 0.01 }, + // Platform-suffixed baselines: CI (linux) baselines are + // committed; local darwin ones are gitignored (Task 3). + resolveScreenshotPath: ({ + testFileDirectory, + screenshotDirectory, + testFileName, + arg, + browserName, + ext, + }: { + testFileDirectory: string; + screenshotDirectory: string; + testFileName: string; + arg: string; + browserName: string; + ext: string; + }) => + `${testFileDirectory}/${screenshotDirectory}/${testFileName}/${arg}-${browserName}-${process.platform}${ext}`, + }, + }, + }, + }, + }, + ], }, lint: { options: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16ea812..675eef6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -132,6 +132,9 @@ importers: jsdom: specifier: ^29.0.2 version: 29.0.2(@noble/hashes@1.8.0) + playwright: + specifier: ^1.62.1 + version: 1.62.1 react: specifier: ^19.2.5 version: 19.2.5 @@ -2319,6 +2322,11 @@ packages: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3070,6 +3078,16 @@ packages: resolution: {integrity: sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==} hasBin: true + playwright-core@1.62.1: + resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.62.1: + resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + engines: {node: '>=20'} + hasBin: true + pngjs@7.0.0: resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} engines: {node: '>=14.19.0'} @@ -5482,6 +5500,9 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -6553,6 +6574,14 @@ snapshots: dependencies: pngjs: 7.0.0 + playwright-core@1.62.1: {} + + playwright@1.62.1: + dependencies: + playwright-core: 1.62.1 + optionalDependencies: + fsevents: 2.3.2 + pngjs@7.0.0: {} postcss@8.4.31: From 33a9d71f685d670c1a9b19a0c072dd0b43f56ce4 Mon Sep 17 00:00:00 2001 From: Hunter Garrett Date: Sun, 23 Aug 2026 22:48:24 -0400 Subject: [PATCH 2/6] ci: run the bones browser project in its own job (BON-4) Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc207f0..f9302a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,7 +54,33 @@ jobs: cache: true - name: Run tests - run: vp run --filter @camp.dev/bones test -- --run + run: vp run --filter @camp.dev/bones test -- --run --project unit + + bones-visual: + name: "Bones: Visual Regression" + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Vite+ + uses: voidzero-dev/setup-vp@v1 + with: + cache: true + + - name: Install Playwright Chromium + working-directory: packages/bones + run: vp exec playwright install --with-deps chromium + + - name: Run browser tests + run: vp run --filter @camp.dev/bones test -- --run --project browser + + - name: Upload screenshots + if: failure() + uses: actions/upload-artifact@v4 + with: + name: bones-visual-output + path: packages/bones/tests/browser/__screenshots__/ # ── Demo app ─────────────────────────────────────────────── demo-check: From 8430e88c2805f7aa8409f17d50f4bce451b0ab9c Mon Sep 17 00:00:00 2001 From: Hunter Garrett Date: Sun, 23 Aug 2026 23:00:59 -0400 Subject: [PATCH 3/6] test: screenshot the three skeleton renderers (BON-4) Co-Authored-By: Claude Fable 5 --- .gitignore | 3 +- packages/bones/tests/browser/visual.test.tsx | 90 ++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 packages/bones/tests/browser/visual.test.tsx diff --git a/.gitignore b/.gitignore index 05346ea..02d12ef 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ test-results .fallow docs/superpowers/ .superpowers -.worktrees \ No newline at end of file +.worktrees +packages/bones/tests/browser/__screenshots__/**/*-darwin.png \ No newline at end of file diff --git a/packages/bones/tests/browser/visual.test.tsx b/packages/bones/tests/browser/visual.test.tsx new file mode 100644 index 0000000..d9bbb16 --- /dev/null +++ b/packages/bones/tests/browser/visual.test.tsx @@ -0,0 +1,90 @@ +/// +import type { ReactNode } from "react"; +import { flushSync } from "react-dom"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, expect, test } from "vite-plus/test"; +import { page } from "vite-plus/test/browser"; +import "../../src/element/index.ts"; +import "../../src/css/auto.css"; +import { createBones, forceBones } from "../../src/react/index.ts"; + +// --------------------------------------------------------------------------- +// One card, three renderers. auto.css restates the text-bar geometry from +// bones.css and only a selector-text test pins the copies; these screenshots +// pin what a user actually sees. data-bone-animate="none" everywhere so the +// shimmer cannot make an image non-deterministic. +// --------------------------------------------------------------------------- + +const CARD_STYLE = "width: 320px; padding: 16px; font: 16px/1.5 sans-serif; background: #fff;"; + +const roots: Root[] = []; + +afterEach(() => { + for (const root of roots.splice(0)) root.unmount(); + document.body.innerHTML = ""; +}); + +function mountHtml(html: string): HTMLElement { + document.body.insertAdjacentHTML("beforeend", html); + return document.body.lastElementChild as HTMLElement; +} + +function renderReact(node: ReactNode): HTMLElement { + const host = document.createElement("div"); + document.body.append(host); + const root = createRoot(host); + roots.push(root); + flushSync(() => root.render(node)); + return host.firstElementChild as HTMLElement; +} + +function ReactCard() { + const { bone } = createBones<{ title: string }>(forceBones); + return ( +
+
+

Placeholder title

+

Placeholder body copy that runs a little longer.

+
+ ); +} + +const CONTENT = ` + +

Real title

+

Real body copy that runs a little longer than one line at this width.

+ +`; + +// Each `toMatchScreenshot` call below needs a suppression for the same +// upstream typing gap documented on the `toMatchScreenshot` config key in +// vite.config.ts: vite-plus-test's `declare module "vitest/node"` +// augmentation targets the real `vitest` package, not vite-plus's vendored +// internal copy, so `Assertion` here has no merged type even +// though the matcher exists and passes at runtime (proven by this file). + +test("react renderer with forceBones", async () => { + renderReact(); + // @ts-expect-error — see the file-level comment above. + await expect(page.getByTestId("react-card")).toMatchScreenshot("react-force"); +}); + +test("element renderer with auto.css", async () => { + mountHtml( + `${CONTENT}`, + ); + // @ts-expect-error — see the file-level comment above. + await expect(page.getByTestId("element-card")).toMatchScreenshot("element-force"); +}); + +test("bare aria-busy region with auto.css", async () => { + mountHtml( + `
${CONTENT}
`, + ); + // @ts-expect-error — see the file-level comment above. + await expect(page.getByTestId("bare-card")).toMatchScreenshot("bare-busy"); +}); From 53cd42b223c11a700002595fe665a5441d1875ba Mon Sep 17 00:00:00 2001 From: Hunter Garrett Date: Sun, 23 Aug 2026 23:08:20 -0400 Subject: [PATCH 4/6] ci: upload vitest attachment screenshots from the visual job (BON-4) Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9302a3..44f6835 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,9 @@ jobs: uses: actions/upload-artifact@v4 with: name: bones-visual-output - path: packages/bones/tests/browser/__screenshots__/ + path: | + packages/bones/tests/browser/__screenshots__/ + packages/bones/.vitest-attachments/ # ── Demo app ─────────────────────────────────────────────── demo-check: From d4df3dd1ce8bd4402196d1e948f76d6a1e104c3f Mon Sep 17 00:00:00 2001 From: Hunter Garrett Date: Sun, 23 Aug 2026 23:12:20 -0400 Subject: [PATCH 5/6] ci: temporarily regenerate visual baselines with --update (BON-4) Reverted once the linux references are harvested and committed. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44f6835..68a2048 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,10 +73,10 @@ jobs: run: vp exec playwright install --with-deps chromium - name: Run browser tests - run: vp run --filter @camp.dev/bones test -- --run --project browser + run: vp run --filter @camp.dev/bones test -- --run --update --project browser - name: Upload screenshots - if: failure() + if: always() uses: actions/upload-artifact@v4 with: name: bones-visual-output From bbe32c984bd145267aa3d2052552365f95aa59f6 Mon Sep 17 00:00:00 2001 From: Hunter Garrett Date: Sun, 23 Aug 2026 23:14:10 -0400 Subject: [PATCH 6/6] test: commit linux screenshot baselines (BON-4) Harvested from the one-off --update CI run, which is reverted here. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 4 ++-- .../visual.test.tsx/bare-busy-chromium-linux.png | Bin 0 -> 2804 bytes .../element-force-chromium-linux.png | Bin 0 -> 2804 bytes .../react-force-chromium-linux.png | Bin 0 -> 894 bytes 4 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 packages/bones/tests/browser/__screenshots__/visual.test.tsx/bare-busy-chromium-linux.png create mode 100644 packages/bones/tests/browser/__screenshots__/visual.test.tsx/element-force-chromium-linux.png create mode 100644 packages/bones/tests/browser/__screenshots__/visual.test.tsx/react-force-chromium-linux.png diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68a2048..44f6835 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,10 +73,10 @@ jobs: run: vp exec playwright install --with-deps chromium - name: Run browser tests - run: vp run --filter @camp.dev/bones test -- --run --update --project browser + run: vp run --filter @camp.dev/bones test -- --run --project browser - name: Upload screenshots - if: always() + if: failure() uses: actions/upload-artifact@v4 with: name: bones-visual-output diff --git a/packages/bones/tests/browser/__screenshots__/visual.test.tsx/bare-busy-chromium-linux.png b/packages/bones/tests/browser/__screenshots__/visual.test.tsx/bare-busy-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..cec014415fed772b4f75233a45c1684f909854a4 GIT binary patch literal 2804 zcmc&$3rtg27%q`iK{2t8&G4$rZbjQDqCp-*5Cel-SZT{GY9Q()Y?=4~bSaG5s+1)# z#_wl>5h`Lys1Tj1T5&zkC^&-C^yB@f5O z&eKt|!dQ#l)lRX^ev*lmlF62=x|EJvCuub1-$tviBFBZ^o9o0cf!60`Mk0$Txrx%aeeE5npL z`-UPv!DPWt@Y#{vWif^Zq#A)wlQVt^_)@LGI=*vTz0rk<1{F)?2sztjR6gJC2aK3hez4l%uL_A<2+&yI@RwqTuovA;dXH@&JtjS5 zpJm&zlQV9xS|%-?g>g=L0^N&32uT=~_Ry35a%oiy0j5Sd{H53-R3l}i_sk2B61%@b zj}(=qXU?^%uC63}@DeB4R4bWgS*_NojZ7V%Hz6-DR$5r@a+KjR(xX%3MFTDFHk(y9 ze%nts3Z`GSwnp0$AYFb?2@Q>nebbXxtRthe{@&2g=DNB%Owjz}lB@ZR<-Q~XPV&1h z2X2Z?k2-5Zckbi`Xpso>W=y8Cyv;1uykVS~&!!zab}Xor)0AT^N>kJu`rDKZI-UA3 zzai3Gr%5i)%r`RNoW%nc6XovXp=XWl?Q3;YmX$)Gut_p=Wh9{9Q0#X=L+qx>QOFC_ zXbEI=1Ek^gQ$$lupuKk^tP@ zN(jW#2eaQ5OZCE#Qcj`&zX{QuJ8qrpl1*(A+jarU&Hj*v?Z2=nFfWPKE0(022%VFq z4-XHM1)#J&_O(504Yxdbw8w`|ha3};R}qni{4hX$^cgyQ8$a0 z3jLv`3;pYV%4d~we8a?3F@lbySGJWU@$9vY>18MMwz$hi&9~pVDV`>1@?=tNNo4Ax zfSldCfgOmZDG#^>30x_U#k$$%Iui8#?(WRDd}~H0#y=Sz8PWFmf}tV-m{#DZT0)OO zfE@fIM0Q>^pO{;R)w>`CsZn4ZA0HnY8an#;aSPoxy1eLMdfwz@&Y+d$!t`U1^dSqV z_(f5xESKkaM%EYGa1lg58KUj5V)py^_yiT`g`bN=m0~3j99rKtXcG>KfoJ{m7D*hn zJk*c|%p>Gr&N`VUOLxv4IT=!_=}|)u;bw0ld6kaRwh-n-^GJXSw>r6f}wxS+o1#*}nl)$;eg! literal 0 HcmV?d00001 diff --git a/packages/bones/tests/browser/__screenshots__/visual.test.tsx/element-force-chromium-linux.png b/packages/bones/tests/browser/__screenshots__/visual.test.tsx/element-force-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..cec014415fed772b4f75233a45c1684f909854a4 GIT binary patch literal 2804 zcmc&$3rtg27%q`iK{2t8&G4$rZbjQDqCp-*5Cel-SZT{GY9Q()Y?=4~bSaG5s+1)# z#_wl>5h`Lys1Tj1T5&zkC^&-C^yB@f5O z&eKt|!dQ#l)lRX^ev*lmlF62=x|EJvCuub1-$tviBFBZ^o9o0cf!60`Mk0$Txrx%aeeE5npL z`-UPv!DPWt@Y#{vWif^Zq#A)wlQVt^_)@LGI=*vTz0rk<1{F)?2sztjR6gJC2aK3hez4l%uL_A<2+&yI@RwqTuovA;dXH@&JtjS5 zpJm&zlQV9xS|%-?g>g=L0^N&32uT=~_Ry35a%oiy0j5Sd{H53-R3l}i_sk2B61%@b zj}(=qXU?^%uC63}@DeB4R4bWgS*_NojZ7V%Hz6-DR$5r@a+KjR(xX%3MFTDFHk(y9 ze%nts3Z`GSwnp0$AYFb?2@Q>nebbXxtRthe{@&2g=DNB%Owjz}lB@ZR<-Q~XPV&1h z2X2Z?k2-5Zckbi`Xpso>W=y8Cyv;1uykVS~&!!zab}Xor)0AT^N>kJu`rDKZI-UA3 zzai3Gr%5i)%r`RNoW%nc6XovXp=XWl?Q3;YmX$)Gut_p=Wh9{9Q0#X=L+qx>QOFC_ zXbEI=1Ek^gQ$$lupuKk^tP@ zN(jW#2eaQ5OZCE#Qcj`&zX{QuJ8qrpl1*(A+jarU&Hj*v?Z2=nFfWPKE0(022%VFq z4-XHM1)#J&_O(504Yxdbw8w`|ha3};R}qni{4hX$^cgyQ8$a0 z3jLv`3;pYV%4d~we8a?3F@lbySGJWU@$9vY>18MMwz$hi&9~pVDV`>1@?=tNNo4Ax zfSldCfgOmZDG#^>30x_U#k$$%Iui8#?(WRDd}~H0#y=Sz8PWFmf}tV-m{#DZT0)OO zfE@fIM0Q>^pO{;R)w>`CsZn4ZA0HnY8an#;aSPoxy1eLMdfwz@&Y+d$!t`U1^dSqV z_(f5xESKkaM%EYGa1lg58KUj5V)py^_yiT`g`bN=m0~3j99rKtXcG>KfoJ{m7D*hn zJk*c|%p>Gr&N`VUOLxv4IT=!_=}|)u;bw0ld6kaRwh-n-^GJXSw>r6f}wxS+o1#*}nl)$;eg! literal 0 HcmV?d00001 diff --git a/packages/bones/tests/browser/__screenshots__/visual.test.tsx/react-force-chromium-linux.png b/packages/bones/tests/browser/__screenshots__/visual.test.tsx/react-force-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..f7cb7bd10dbd7bd2535a2b3b25f955509b048f74 GIT binary patch literal 894 zcmeAS@N?(olHy`uVBq!ia0y~yV3Y>3*Kx1`Nw(OP!3+${uAVNAAr*{o4?1Q^bBeGY zSiW2>!l|$KYWa9}oAz$kHnfhU83&4htj zs)3RBKm$y8LrVMl_3LZgk3TkSKdgA*`{pN(@$vCLe}B*4x46H*f6u;sY(MHywcs#` zD#9C{I09WFFE9VIFD@>Qsm9-1uEav-)30Ar4el6jr^rTC>zv!uZoi%O`|rHcSmr-9 zr#C$*v3go#m)@^xT~k;0tZFY?h2HhortjasudJ>^ut$q9U?F{E%{{3rPt#k9?_xJZL z=bzts|9#|j(*x(X>v#kGSY#u&`S_%{(^5}9E#f>-e-V2$k?GZGZ#Lvc1Ks`Zd-b(a z-VdL3Pfxo1a?0nQb8M%*`SA1S%-3I~9_)-+e|^vX{mrsjE6zMD;P^17?|AaJj53S)=YcX$i%!aUm;Lzs+#lqV{r5Mo z%WjyTA31G~-|{)Rt2mb9_vIJYs