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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ jobs:

- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
# A stalled download otherwise holds the job until its 40-minute timeout (seen on PR #15).
timeout-minutes: 10
run: npx playwright install --with-deps chromium firefox webkit

- name: Install Playwright system dependencies
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ Full doc: `docs/architecture/grid-row-meta.md`
- Anything that depends on layout, `ResizeObserver`, or scrolling (virtualization, container height, drag) must be a `*.browser.test.tsx` — jsdom has no layout engine and reports a 0px viewport (the grid falls back to 600px), so such bugs are invisible there
- Browser tests run in Chromium, Firefox and WebKit: `npx playwright install chromium firefox webkit` once. One engine only: `npx vitest run --project browser --browser.name=firefox`
- Browser test files run one at a time (`fileParallelism: false`): several test pages open at once in one Firefox steal focus and pointer input from each other
- `lib/publicApi.test.ts` fails when a type declared in `lib/types/*` is not exported from `lib/index.ts` (`GridAi*` / `GridAgent*` from `/ai`). A type that is internal on purpose goes in `INTERNAL_TYPES` with a comment
- **Vitest vs Playwright Test:** render `<DataGrid>` with props and assert → Vitest (`*.test.tsx`, or `*.browser.test.tsx` for layout). Needs the built package, a real app, a dev/preview server or a file download → the e2e package smoke suite (`e2e/`, `npm run test:smoke`). Vitest aliases the package to `lib/`, so only the smoke suite catches packaging bugs (bundled `react/jsx-runtime`, missing CSS in `dist/`, `.d.ts` that fails `skipLibCheck: false`)
- Smoke suite: `scripts/smoke.mjs` builds + packs to `.pack/`, `npm ci`s each fixture in `e2e/fixtures/{react19,react18,nopeers,nextjs}` and unpacks the tarball there, builds them (strict tsc + vite), then runs `e2e/smoke.spec.ts` in Chromium/Firefox/WebKit × both fixtures against `vite preview`. Scenarios via `?scenario=` (`basic`, `flex`, `grouping`, `editing`; React 19 also `pinned`, `dark`). Any console error/warning fails a test. `nopeers` is a webpack 5 app without `exceljs`/`jspdf` (`nopeers.spec.ts`, port 4321): every optional-peer `import()` must stay inside a `try` block, which webpack and Turbopack treat as optional (Vite tolerates a missing peer anyway, so the Vite fixtures cannot catch this). `nextjs` is a Next.js 16 App Router app (Turbopack, `next build` + `next start`, port 4323, `e2e/nextjs.spec.ts`): Server Component → `'use client'` grid, stylesheet in the layout, no optional peers, no aliases; its `next.config.ts` only pins `turbopack.root` to the fixture so the repo's own `exceljs` is not found. A specifier hidden in a variable (`import(name)`) builds but breaks apps that do install the peers: browsers cannot resolve bare specifiers, and Vite's lib build strips `webpackIgnore` / `@vite-ignore` comments. Fixtures are outside root lint/typecheck/vitest; their lockfiles must not carry `integrity` for the tarball entry. Guide: `docs/contributing/testing.md`
- Keep browser tests engine-neutral: no CDP (`cdp()` is Chromium-only; use a Playwright-backed command such as `commands.setColorScheme` from `vitest.config.ts`), Tab-order anchors are text inputs (WebKit on macOS skips buttons on Tab), compare scroll positions with what the browser actually took (Firefox snaps them to device pixels), and wait for CSS animations to finish before measuring. Skip an engine only with `it.skipIf(server.browser === '...')` (`server` from `vitest/browser`) and a comment saying why
Expand Down
30 changes: 30 additions & 0 deletions lib/publicApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,25 @@ function readEntryExports(program: ts.Program, file: string = LIB_INDEX): Set<st
*/
const AI_TYPE_NAME = /^Grid(?:Ai|Agent)[A-Z]/;

const TYPES_DIR = path.join(ROOT, 'lib', 'types');

/** Types declared in lib/types that are internal on purpose: store shapes, not part of any public signature. */
const INTERNAL_TYPES = new Set([
'GridInternalState',
'GridPinnedColumns', // store shape of pinned columns; the prop is GridColumnPinning
'GridPinnedRows', // store shape of pinned rows; the prop is GridRowPinning
]);

/** Every name a domain file under lib/types exports, with the file that declares it. */
function declaredTypeNames(program: ts.Program): Map<string, string> {
const found = new Map<string, string>();
for (const file of fs.readdirSync(TYPES_DIR)) {
if (!file.endsWith('.ts') || file === 'index.ts' || file.includes('.test.')) continue;
for (const name of readEntryExports(program, path.join(TYPES_DIR, file))) found.set(name, `lib/types/${file}`);
}
return found;
}

const readDoc = (file: string) => fs.readFileSync(file, 'utf8');
const componentDocs = fs.readdirSync(COMPONENT_DOCS_DIR)
.filter((f) => f.endsWith('.md'))
Expand Down Expand Up @@ -224,6 +243,17 @@ describe('public API matches the reference docs', () => {
expect(missing).toEqual([]);
});

it('exports every type declared in lib/types from its entry point, unless it is internal', () => {
const declared = declaredTypeNames(program);
expect(declared.size).toBeGreaterThan(50);
const missing = [...declared]
.filter(([name]) => !INTERNAL_TYPES.has(name))
.filter(([name]) => !(AI_TYPE_NAME.test(name) ? aiExports : rootExports).has(name))
.map(([name, file]) => `${name} (${file})`);
expect(missing).toEqual([]);
expect([...INTERNAL_TYPES].filter((name) => !declared.has(name))).toEqual([]);
});

it('exports every name the docs import from the ai entry point, and the root does not', () => {
const imports = documentedImports('/ai');
expect(imports.size).toBeGreaterThan(1);
Expand Down
Loading