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
30 changes: 29 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,35 @@ 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__/
packages/bones/.vitest-attachments/

# ── Demo app ───────────────────────────────────────────────
demo-check:
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ test-results
.fallow
docs/superpowers/
.superpowers
.worktrees
.worktrees
packages/bones/tests/browser/__screenshots__/**/*-darwin.png
3 changes: 2 additions & 1 deletion packages/bones/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions packages/bones/tests/browser/smoke.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
90 changes: 90 additions & 0 deletions packages/bones/tests/browser/visual.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/// <reference types="vite-plus/client" />
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 (
<div
data-testid="react-card"
data-bone-animate="none"
style={{ width: 320, padding: 16, font: "16px/1.5 sans-serif", background: "#fff" }}
>
<div {...bone("block")} style={{ width: 48, height: 48 }} />
<h3 {...bone("text")}>Placeholder title</h3>
<p {...bone("text")}>Placeholder body copy that runs a little longer.</p>
</div>
);
}

const CONTENT = `
<img alt="" width="48" height="48" />
<h3>Real title</h3>
<p>Real body copy that runs a little longer than one line at this width.</p>
<button type="button">Save</button>
`;

// 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<Locator>` 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(<ReactCard />);
// @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(
`<bones-boundary force data-testid="element-card" data-bone-animate="none" style="${CARD_STYLE} display: block;">${CONTENT}</bones-boundary>`,
);
// @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(
`<div aria-busy="true" data-testid="bare-card" data-bone-animate="none" style="${CARD_STYLE}">${CONTENT}</div>`,
);
// @ts-expect-error — see the file-level comment above.
await expect(page.getByTestId("bare-card")).toMatchScreenshot("bare-busy");
Comment thread
hunterbecton marked this conversation as resolved.
});
54 changes: 53 additions & 1 deletion packages/bones/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineConfig } from "vite-plus";
import { playwright } from "vite-plus/test/browser-playwright";

export default defineConfig({
staged: {
Expand Down Expand Up @@ -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: {
Expand Down
29 changes: 29 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading