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
11 changes: 6 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,24 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v7

- name: Setup Node
uses: actions/setup-node@v7
- name: Setup Vite+
uses: voidzero-dev/setup-vp@v1.19.0
with:
node-version: 'lts/*'
cache: npm
cache: true
- name: Install dependencies
run: npm ci

- name: Install browser engines with Linux dependencies
if: ${{ hashFiles('vitest.test.browser.config.ts', 'playwright.config.*') != '' && runner.os == 'Linux' }}
if: ${{ hashFiles('playwright.config.*') != '' && runner.os == 'Linux' }}
run: npx playwright install --with-deps chromium firefox webkit

- name: Install browser engines
if: ${{ hashFiles('vitest.test.browser.config.ts', 'playwright.config.*') != '' && runner.os != 'Linux' }}
if: ${{ hashFiles('playwright.config.*') != '' && runner.os != 'Linux' }}
run: npx playwright install chromium firefox webkit

- name: Format check
if: runner.os != 'Windows'
run: npm run fmt --if-present -- --check

- name: Lint
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,8 @@ bench-results.txt
bench-profiles/
.vitest-attachments/
__screenshots__/

# Playwright
test-results/
playwright-report/
blob-report/
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ theme composition belong in `@askrjs/themes`.
benchmark coverage, and test-suite conventions.
- `tests/jsdom/` covers DOM-backed internal behavior.
- `tests/browser/components/` owns public behavior, accessibility, and
determinism coverage.
determinism coverage. These run on native `@playwright/test`
(`playwright.config.ts`): each `<name>.spec.ts` runs in Node and drives the
page, while the component tree it mounts lives in the mirrored
`tests/browser/scenarios/<same path>.tsx` module, loaded lazily by
`tests/browser/harness.ts`.
- `benches/` contains the four benchmark tiers.
- `docs/` contains the package overview, composition guidance, and standing
regression-coverage rules.
Expand Down
File renamed without changes.
44 changes: 18 additions & 26 deletions package-lock.json

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

7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -221,16 +221,15 @@
"build": "vp pack",
"clean": "npx rimraf dist node_modules",
"dev": "vp pack --watch",
"bench": "npm run bench:tier1 && npm run bench:tier2 && npm run bench:tier3 && npm run bench:tier4",
"bench": "npm run bench:tier1 && npm run bench:tier2 && npm run bench:tier3",
"bench:tier1": "cross-env NODE_ENV=production vp test bench --run --reporter=default --config vitest.bench.tier1.config.ts",
"bench:tier2": "cross-env NODE_ENV=production vp test bench --run --reporter=default --config vitest.bench.tier2.config.ts",
"bench:tier3": "cross-env NODE_ENV=production vp test bench --run --reporter=default --config vitest.bench.tier3.config.ts",
"bench:tier4": "cross-env NODE_ENV=production vp test bench --run --reporter=default --config vitest.bench.tier4.config.ts",
"fmt": "vp fmt .",
"lint": "vp lint .",
"typecheck": "npm run test:types",
"test": "npm run test:unit && npm run test:integrity && npm run test:jsdom && npm run test:browser",
"test:browser": "vp test run --config vitest.test.browser.config.ts",
"test:browser": "playwright test",
"test:component": "npm run test:browser",
"test:jsdom": "npm run build && vp test run --config vitest.test.jsdom.config.ts",
"test:types": "tsc -p tsconfig.type-tests.json --noEmit",
Expand All @@ -246,8 +245,8 @@
"devDependencies": {
"@askrjs/askr": ">=0.2.3 <0.3.0",
"@askrjs/vite": ">=0.2.1 <0.3.0",
"@playwright/test": "1.62.1",
"@types/node": "^26.3.0",
"@vitest/browser-playwright": "4.1.11",
"axe-core": "^4.13.0",
"cross-env": "^10.1.0",
"jsdom": "^30.0.1",
Expand Down
46 changes: 46 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { defineConfig, devices } from '@playwright/test';

const HOST = '127.0.0.1';
const PORT = 4318;
const BASE_URL = `http://${HOST}:${PORT}`;

export default defineConfig({
testDir: './tests/browser',
testMatch: '**/*.spec.ts',
fullyParallel: true,
workers: 1,
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 1 : 0,
reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list',
timeout: 60_000,
expect: { timeout: 10_000 },
use: {
baseURL: BASE_URL,
trace: 'retain-on-failure',
viewport: { width: 1280, height: 900 },
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
webServer: {
// `vp dev` rather than `vite`: vite-plus aliases the `vite` package to a
// core package that ships no CLI binary, and installing a real `vite`
// alongside it makes `vp pack` refuse to run at all.
//
// `--strictPort` turns a port collision into an immediate failure instead
// of a server listening on a port nobody is polling.
//
// `--host 127.0.0.1` must match `webServer.url`: Vite otherwise binds
// `localhost`, which CI runners can resolve to `::1` while Playwright's
// readiness probe polls the literal IPv4 address — a silent startup
// timeout that passes locally.
command: `npx vp dev --config vite.harness.config.ts --host ${HOST} --port ${PORT} --strictPort`,
url: `${BASE_URL}/tests/browser/harness.html`,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
stderr: 'pipe',
timeout: 120_000,
},
});
30 changes: 30 additions & 0 deletions tests/browser/assertions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { expect, test } from '@playwright/test';

/**
* One `(label, first, second)` triple produced by `deterministicRender` in a
* scenario module. The render pair has to cross the Node/browser boundary as
* data because the JSX factory it compares cannot.
*/
export interface DeterministicRender {
label: string;
first: string;
second: string;
}

/**
* Asserts every render pair a determinism scenario produced is stable.
*
* Each pair gets its own `test.step`, which is how the label survives the port:
* vitest's `expect(actual, message)` has no Playwright equivalent, so the step
* name is what names the failing tree in the report.
*/
export async function expectDeterministic(
renders: DeterministicRender[]
): Promise<void> {
expect(renders.length).toBeGreaterThan(0);
for (const render of renders) {
await test.step(`deterministic render: ${render.label}`, () => {
expect(render.first).toBe(render.second);
});
}
}
17 changes: 0 additions & 17 deletions tests/browser/browser-console.setup.ts

This file was deleted.

12 changes: 12 additions & 0 deletions tests/browser/components/accordion/a11y.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { expect, test } from '../../fixtures';

test.describe('Accordion - Accessibility', () => {
test('should have no automated axe violations given open accordion item', async ({
render,
axeViolations,
}) => {
await render('axeOpenItem');

expect(await axeViolations()).toEqual([]);
});
});
16 changes: 16 additions & 0 deletions tests/browser/components/accordion/determinism.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {
type DeterministicRender,
expectDeterministic,
} from '../../assertions';
import { test } from '../../fixtures';

test.describe('Accordion - Determinism', () => {
test('should render deterministic accordion markup', async ({
render,
run,
}) => {
await render('accordionMarkup');

await expectDeterministic(await run<DeterministicRender[]>('renders'));
});
});
12 changes: 12 additions & 0 deletions tests/browser/components/avatar/a11y.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { expect, test } from '../../fixtures';

test.describe('Avatar - Accessibility', () => {
test('should have no automated axe violations given image and fallback', async ({
render,
axeViolations,
}) => {
await render('axeImageAndFallback');

expect(await axeViolations()).toEqual([]);
});
});
36 changes: 36 additions & 0 deletions tests/browser/components/avatar/behavior.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { expect, test } from '../../fixtures';

test.describe('Avatar - Behavior', () => {
test('should keep fallback visible until image load event', async ({
render,
root,
page,
run,
}) => {
let releaseImage!: () => Promise<void>;
await page.route('**/avatar.png', async (route) => {
await new Promise<void>((resolve) => {
releaseImage = async () => {
await route.fulfill({
status: 200,
contentType: 'image/gif',
body: Buffer.from(
'R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==',
'base64'
),
});
resolve();
};
});
});

await render('fallbackUntilLoad');
const fallbackSelector = await run<string>('fallbackSelector');

await expect(root.locator(fallbackSelector)).toHaveText('JD');

await releaseImage();

await expect(root.locator(fallbackSelector)).toHaveCount(0);
});
});
13 changes: 13 additions & 0 deletions tests/browser/components/avatar/determinism.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {
type DeterministicRender,
expectDeterministic,
} from '../../assertions';
import { test } from '../../fixtures';

test.describe('Avatar - Determinism', () => {
test('should render deterministic avatar markup', async ({ render, run }) => {
await render('avatarMarkup');

await expectDeterministic(await run<DeterministicRender[]>('renders'));
});
});
Loading