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
14 changes: 13 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,21 @@ requires a rebuild+redeploy, not just a file edit.
| `npm run version-sync` | Sync all version references to root `package.json` |
| `npm run check:version` | Verify all version references match (CI gate — run before every commit that touches versions) |
| `npm run check:llm-guide` | Verify `docs/llm-guide.md` only references live tokens (CI gate) |
| `npm run check:macros` | Verify `.sf-*` macro classes match `docs/macros.md` (not currently wired into CI — run manually before committing macro changes) |
| `npm run check:registry` | Verify `token-registry.json` is in sync with source (CI gate) |
| `npm run audit:check` | Verify `docs/registry.json` matches source without writing (CI gate) |
| `npm run lint:css` | Lint all CSS source with stylelint (CI gate) |
| `npm run lint:css:fix` | Lint CSS source and auto-fix violations |
| `npm run docs` | Regenerate docs and sync configurator API index |
| `npm run docs:tokens` | Regenerate `docs/tokens.md` only |
| `npm run docs:index` | Regenerate `docs/token-index.md` + `.json` only |
| `npm run docs:api` | Regenerate `docs/api-index.md` + `.json` only |
| `npm run docs:classes` | Regenerate `docs/classes.md` only |
| `npm run gen:registry` | Regenerate `token-registry.json` (stable id assignments) |
| `npm run configurator:sync` | Push `docs/api-index.json` → `configurator/src/data/api-index.generated.json` |
| `npm run audit` | Audit CSS tokens for consistency |
| `npm run audit` | Audit CSS tokens for consistency, writing `docs/registry.json` |
| `npm run watch` | Rebuild CSS bundles on file change (dev watch mode) |
| `npm run test:unit` | Run the Node `--test` unit suite only, without the Playwright e2e build. Some tests read from `badges/` — run `npm run build` first on a clean checkout (CI instead downloads the `badges` artifact from the `build` job before this step) |
| `npm test` | Full suite: build → unit → Playwright e2e |

## Release process
Expand Down
2 changes: 1 addition & 1 deletion docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Mapping concepts from popular CSS frameworks to SLASHED, plus intra-project
upgrade notes.

## SLASHED 0.6.25 → next
## SLASHED 0.6.25 → 0.6.26

### `base` relative shade aliases removed

Expand Down
28 changes: 28 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Test conventions

Two runners, split by whether a test needs a real browser:

- **`*.spec.js`** — Playwright (`npm test`). Runs across chromium/firefox/webkit.
Use for anything that renders CSS and reads computed styles, layout geometry,
or visual output — imports `test`/`expect` from `@playwright/test`.
- **`*.test.js`** — Node's built-in test runner (`npm run test:unit`), single
process, no browser. Use for pure fs/regex/data-shape checks that never touch
a DOM — imports `test`/`describe` from `node:test` and `assert` from
`node:assert/strict`. Wire new files into `pretest`/`test:unit` in
`package.json`.

If a test doesn't call `page.*` or read a computed style, it almost certainly
belongs in `*.test.js`, not `*.spec.js` — see SL-027 (`coverage.test.js` used
to be Playwright-discovered despite never touching a browser, tripling its CI
cost for no reason).

## `tier1-p*` numbering (SL-032)

`tier1-p2-coverage`, `tier1-p7-oldengine`, `tier1-p8-modern`, and
`tier1-p10-contrast` number their properties P2/P7/P8/P10 with no P1,
P3-P6, or P9 anywhere in the repo. These numbers come from the
tier-1-color-fallback feature's original property enumeration (see each
file's own `Property N:` header comment) — only a subset of the enumerated
properties warranted a dedicated automated test; the rest were covered by
other existing tests, manual review, or judged unnecessary to automate. The
gaps are not dead history — don't renumber these files sequentially.
9 changes: 2 additions & 7 deletions tests/a11y-patterns.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,11 @@
// .sf-focus-parent, .sf-clickable-parent,
// touch-target token, forced-colors, disabled cursor.
import { test, expect } from '@playwright/test';
import path from 'node:path';

const BUNDLE = path.join(process.cwd(), 'badges', 'slashed.optimal.css');
import { renderWithBundle, NO_TRANSITIONS_STYLE } from './render-helpers.js';

async function setup(page, html) {
await page.setViewportSize({ width: 800, height: 600 });
await page.setContent(`<!doctype html><html><body style="margin:0">${html}</body></html>`);
await page.addStyleTag({ path: BUNDLE });
// Disable transitions so computed property reads are stable (no mid-animation values).
await page.addStyleTag({ content: '*, *::before, *::after { transition: none !important; animation-duration: 0s !important; }' });
await renderWithBundle(page, html, { width: 800, height: 600, extraStyle: NO_TRANSITIONS_STYLE });
}

// ── .sr-only ────────────────────────────────────────────────────
Expand Down
4 changes: 1 addition & 3 deletions tests/container-queries.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
// setupInContainer pins font-size to 16px so em-based CQ thresholds are stable
// across browsers (WebKit 26+ resolves em against inherited font-size, not 16px).
import { test, expect } from '@playwright/test';
import path from 'node:path';

const BUNDLE = path.join(process.cwd(), 'badges', 'slashed.optimal.css');
import { BUNDLE } from './render-helpers.js';

// Wraps grid HTML in a container of a known inline-size so anonymous
// @container queries fire against that width, not the viewport.
Expand Down
14 changes: 7 additions & 7 deletions tests/layers.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @ts-check
import { test, expect } from '@playwright/test';
import path from 'node:path';
import { BUNDLE } from './render-helpers.js';

/**
* Layer ordering invariant test.
Expand All @@ -26,7 +26,7 @@ test.describe('Layer ordering', () => {
</style>
<div class="layer-check">test</div>
`);
await page.addStyleTag({ path: path.join(process.cwd(), 'badges', 'slashed.optimal.css') });
await page.addStyleTag({ path: BUNDLE });

const color = await page.locator('.layer-check').evaluate(el =>
getComputedStyle(el).color
Expand All @@ -42,7 +42,7 @@ test.describe('Layer ordering', () => {
</style>
<div class="layer-check-2">test</div>
`);
await page.addStyleTag({ path: path.join(process.cwd(), 'badges', 'slashed.optimal.css') });
await page.addStyleTag({ path: BUNDLE });

const opacity = await page.locator('.layer-check-2').evaluate(el =>
getComputedStyle(el).opacity
Expand All @@ -58,7 +58,7 @@ test.describe('Layer ordering', () => {
</style>
<div class="layer-check-3">test</div>
`);
await page.addStyleTag({ path: path.join(process.cwd(), 'badges', 'slashed.optimal.css') });
await page.addStyleTag({ path: BUNDLE });

const opacity = await page.locator('.layer-check-3').evaluate(el =>
getComputedStyle(el).opacity
Expand All @@ -76,7 +76,7 @@ test.describe('Layer ordering', () => {
</style>
<div class="layer-check-4">test</div>
`);
await page.addStyleTag({ path: path.join(process.cwd(), 'badges', 'slashed.optimal.css') });
await page.addStyleTag({ path: BUNDLE });

const color = await page.locator('.layer-check-4').evaluate(el =>
getComputedStyle(el).color
Expand All @@ -94,7 +94,7 @@ test.describe('Layer ordering', () => {
</style>
<div class="layer-check-5">test</div>
`);
await page.addStyleTag({ path: path.join(process.cwd(), 'badges', 'slashed.optimal.css') });
await page.addStyleTag({ path: BUNDLE });

const opacity = await page.locator('.layer-check-5').evaluate(el =>
getComputedStyle(el).opacity
Expand All @@ -113,7 +113,7 @@ test.describe('Layer ordering', () => {
</style>
<div class="layer-check-6">test</div>
`);
await page.addStyleTag({ path: path.join(process.cwd(), 'badges', 'slashed.optimal.css') });
await page.addStyleTag({ path: BUNDLE });

const opacity = await page.locator('.layer-check-6').evaluate(el =>
getComputedStyle(el).opacity
Expand Down
8 changes: 2 additions & 6 deletions tests/layout.spec.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
// @ts-check
// Behavioural tests for core/layout.css — every layout primitive.
import { test, expect } from '@playwright/test';
import path from 'node:path';

const BUNDLE = path.join(process.cwd(), 'badges', 'slashed.optimal.css');
import { renderWithBundle } from './render-helpers.js';

async function setup(page, html) {
await page.setViewportSize({ width: 1200, height: 900 });
await page.setContent(`<!doctype html><html><body style="margin:0">${html}</body></html>`);
await page.addStyleTag({ path: BUNDLE });
await renderWithBundle(page, html, { width: 1200, height: 900 });
}

// ── .sf-section ────────────────────────────────────────────────
Expand Down
10 changes: 4 additions & 6 deletions tests/macros.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@
// Each macro is exercised against a synthetic fixture and its
// observable effect verified via getComputedStyle / layout reads.
import { test, expect } from '@playwright/test';
import path from 'node:path';

const BUNDLE = path.join(process.cwd(), 'badges', 'slashed.optimal.css');
import { renderWithBundle } from './render-helpers.js';

async function setup(page, html) {
await page.setViewportSize({ width: 800, height: 600 });
await page.setContent(`<!doctype html><html><body>${html}</body></html>`);
await page.addStyleTag({ path: BUNDLE });
// Unlike the other spec files' setup(), body has no margin:0 here —
// preserved as-is (bodyMargin: null omits the style attribute entirely).
await renderWithBundle(page, html, { width: 800, height: 600, bodyMargin: null });
}

test.describe('macro: .sf-flow', () => {
Expand Down
8 changes: 4 additions & 4 deletions tests/print.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @ts-check
import { test, expect } from '@playwright/test';
import path from 'node:path';
import { BUNDLE } from './render-helpers.js';

/**
* Print color preservation smoke test.
Expand All @@ -12,7 +12,7 @@ test.describe('Print styles', () => {
test('<mark> retains non-transparent background in print', async ({ page }) => {
await page.emulateMedia({ media: 'print' });
await page.setContent(`<p><mark>Highlighted text</mark></p>`);
await page.addStyleTag({ path: path.join(process.cwd(), 'badges', 'slashed.optimal.css') });
await page.addStyleTag({ path: BUNDLE });

const bg = await page.locator('mark').evaluate(el =>
getComputedStyle(el).backgroundColor
Expand All @@ -29,7 +29,7 @@ test.describe('Print styles', () => {
<div class="no-print" id="hidden-el">Should be hidden</div>
<div id="visible-el">Should be visible</div>
`);
await page.addStyleTag({ path: path.join(process.cwd(), 'badges', 'slashed.optimal.css') });
await page.addStyleTag({ path: BUNDLE });
await page.emulateMedia({ media: 'print' });

const hiddenDisplay = await page.locator('#hidden-el').evaluate(el =>
Expand All @@ -47,7 +47,7 @@ test.describe('Print styles', () => {
await page.setContent(`
<div class="print-color-exact" style="background: rgb(100, 150, 200);">Coloured</div>
`);
await page.addStyleTag({ path: path.join(process.cwd(), 'badges', 'slashed.optimal.css') });
await page.addStyleTag({ path: BUNDLE });
await page.emulateMedia({ media: 'print' });

const pca = await page.locator('.print-color-exact').evaluate(el =>
Expand Down
40 changes: 40 additions & 0 deletions tests/render-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Shared Playwright test-fixture helpers (SL-033). The optimal bundle path
// and the viewport/doctype/addStyleTag boilerplate were duplicated across
// a11y-patterns.spec.js, layout.spec.js, macros.spec.js, states-full.spec.js,
// and typography.spec.js with near-identical setup() functions. The three
// real differences between them (viewport size, whether <body> gets
// style="margin:0", and whether transitions are disabled) are now the
// options renderWithBundle() takes — see each call site for its values.
//
// container-queries.spec.js's setupInContainer() wraps content in a sized
// container div and pins font-size for @container em thresholds; that shape
// is genuinely different from the flat case here, so it keeps its own local
// function, but imports BUNDLE from here instead of redefining it.
//
// layers.spec.js and print.spec.js inline page.setContent per-test (each
// test stages different competing rules in its own <style> block) — only
// their repeated BUNDLE path.join() is shared here.
import path from 'node:path';

export const BUNDLE = path.join(process.cwd(), 'badges', 'slashed.optimal.css');

export const NO_TRANSITIONS_STYLE =
'*, *::before, *::after { transition: none !important; animation-duration: 0s !important; }';

/**
* Load the optimal bundle and set page content inside a minimal
* <!doctype html><html><body> wrapper.
* @param {import('@playwright/test').Page} page
* @param {string} html
* @param {{ width?: number, height?: number, bodyMargin?: string | null, extraStyle?: string, bundle?: string }} [options]
*/
export async function renderWithBundle(page, html, options = {}) {
const { width = 1200, height = 900, bodyMargin = '0', extraStyle, bundle = BUNDLE } = options;
await page.setViewportSize({ width, height });
const bodyAttr = bodyMargin != null ? ` style="margin:${bodyMargin}"` : '';
await page.setContent(`<!doctype html><html><body${bodyAttr}>${html}</body></html>`);
await page.addStyleTag({ path: bundle });
if (extraStyle) {
await page.addStyleTag({ content: extraStyle });
}
}
9 changes: 2 additions & 7 deletions tests/states-full.spec.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
// @ts-check
// Full coverage of every .is-* class in core/states.css.
import { test, expect } from '@playwright/test';
import path from 'node:path';

const BUNDLE = path.join(process.cwd(), 'badges', 'slashed.optimal.css');
import { renderWithBundle, NO_TRANSITIONS_STYLE } from './render-helpers.js';

async function setup(page, html) {
await page.setViewportSize({ width: 800, height: 600 });
await page.setContent(`<!doctype html><html><body style="margin:0">${html}</body></html>`);
await page.addStyleTag({ path: BUNDLE });
// Disable transitions so computed property reads are stable (no mid-animation values).
await page.addStyleTag({ content: '*, *::before, *::after { transition: none !important; animation-duration: 0s !important; }' });
await renderWithBundle(page, html, { width: 800, height: 600, extraStyle: NO_TRANSITIONS_STYLE });
}

// ── Visibility ──────────────────────────────────────────────────
Expand Down
3 changes: 3 additions & 0 deletions tests/tier1-p10-contrast.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
* Verifies the mathematical invariant: dark text on a light background
* always meets 4.5:1 across the ranges used by the framework's defaults.
*
* The P2/P7/P8/P10 numbering gaps (no P1, P3-P6, P9 test files) are
* explained in tests/README.md — not dead history, don't renumber.
*
* Run: node --test tests/tier1-p10-contrast.test.js
*/
import { test, describe } from 'node:test';
Expand Down
3 changes: 3 additions & 0 deletions tests/tier1-p2-coverage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
* Property 2: tokens.css uses @supports gating — no bare light-dark() or
* oklch(from…)/color-mix() declarations outside a @supports block.
*
* The P2/P7/P8/P10 numbering gaps (no P1, P3-P6, P9 test files) are
* explained in tests/README.md — not dead history, don't renumber.
*
* Run: node --test tests/tier1-p2-coverage.test.js
*/
import { test, describe } from 'node:test';
Expand Down
3 changes: 3 additions & 0 deletions tests/tier1-p7-oldengine.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
* checks the same invariant at source level). P7 verifies the invariant
* holds in the final built artifact after bundling and minification.
*
* The P2/P7/P8/P10 numbering gaps (no P1, P3-P6, P9 test files) are
* explained in tests/README.md — not dead history, don't renumber.
*
* Run: node --test tests/tier1-p7-oldengine.test.js
*/
import { test, describe } from 'node:test';
Expand Down
3 changes: 3 additions & 0 deletions tests/tier1-p8-modern.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
* This proves that adding core/tokens.color-fallbacks.css before
* core/tokens.css did NOT change any resolved value on modern browsers.
*
* The P2/P7/P8/P10 numbering gaps (no P1, P3-P6, P9 test files) are
* explained in tests/README.md — not dead history, don't renumber.
*
* Regenerate baseline: BASELINE_CAPTURE=1 npx playwright test tests/tier1-p8-modern.spec.js --project=chromium
* Run: npx playwright test tests/tier1-p8-modern.spec.js --project=chromium
*/
Expand Down
6 changes: 2 additions & 4 deletions tests/typography.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@
import { test, expect } from '@playwright/test';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import { renderWithBundle } from './render-helpers.js';

const FIXTURE = pathToFileURL(path.join(import.meta.dirname, 'fixture.html')).href;
const BUNDLE = path.join(process.cwd(), 'badges', 'slashed.optimal.css');

async function setup(page, html) {
await page.setViewportSize({ width: 1200, height: 900 });
await page.setContent(`<!doctype html><html><body style="margin:0">${html}</body></html>`);
await page.addStyleTag({ path: BUNDLE });
await renderWithBundle(page, html, { width: 1200, height: 900 });
}

// ── Heading size scale ───────────────────────────────────────────
Expand Down