diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2f24b1e..52800260 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,33 @@ jobs: run: bun run lint - name: Test - run: bun test --coverage + run: bun test --coverage ./tests/ + + playground-e2e: + name: Playground E2E (Playwright) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('bun.lock') }} + + - name: Install Playwright browsers + run: bunx playwright install --with-deps chromium + + - name: Run Playwright playground tests + run: bun run test:e2e build: name: Build diff --git a/bun.lock b/bun.lock index 163b75ec..982a9f46 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,7 @@ "@biomejs/biome": "^1.9.4", "@types/bun": "^1.1.14", "fast-check": "^3.22.0", + "playwright": "1.59.1", }, "peerDependencies": { "typescript": "^5.7.0", @@ -41,6 +42,12 @@ "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + + "playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="], + + "playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="], + "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], diff --git a/docs/playground.md b/docs/playground.md index 7f08e62b..409c5c04 100644 --- a/docs/playground.md +++ b/docs/playground.md @@ -118,6 +118,54 @@ cp node_modules/typescript/lib/typescript.js ./playground/dist/typescript.js The CI pipeline (`pages.yml`) runs this automatically during deployment. +## End-to-End Cell Execution Tests + +To make sure every code cell on every playground page actually works (no +TypeScript errors, no runtime errors, real output), the project ships a +Playwright-based test suite under `tests-e2e/playground-cells.test.ts`. + +It launches headless Chromium, navigates to every `playground/*.html` page, +clicks **▶ Run** on every `.playground-block`, and asserts that the cell +output is not an error and is not the "(no output …)" sentinel. + +```bash +bun install +bunx playwright install --with-deps chromium +bun run test:e2e +``` + +CI runs this in the dedicated `playground-e2e` job (see `.github/workflows/ci.yml`). + +### Known-failures allowlist + +A large number of pages currently have at least one broken cell — most often +because: + +1. The "TypeScript" cell actually contains Python source (so TS lexing fails + on the `import pandas as pd` line). +2. A cell references a variable defined in a previous cell. **Each cell runs + in its own `new Function()` scope, so nothing persists between cells** — + every cell needs its own `import { … } from "tsb"` and its own data setup. +3. A cell never calls `console.log()` — the playground only shows what the + user explicitly logs. + +The file `tests-e2e/known-failures.json` enumerates the (file → cell numbers) +that are currently broken so CI can pass while progress is made. Each entry +should be **removed from the allowlist as the corresponding cell is fixed** — +the test framework also fails if a cell now passes but is still listed +(forward-progress check). + +### Authoring rule + +Every cell on every playground page **must** be self-contained: + +- Import everything it uses from `"tsb"` directly inside the cell. +- Re-declare any helper data it depends on inside the cell. +- Call `console.log(…)` (or `console.warn` / `console.error`) so output is + visible. + +See `playground/merge_ordered.html` for the canonical pattern. + ## Non-Goals (Current Scope) - **Infinite loop protection**: long-running or infinite loops will hang the diff --git a/package.json b/package.json index 848ae544..076a8a67 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ } }, "scripts": { - "test": "bun test", + "test": "bun test ./tests/", + "test:e2e": "bun test --timeout 600000 tests-e2e", "lint": "biome check .", "lint:fix": "biome check --write .", "typecheck": "tsc --noEmit", @@ -23,8 +24,9 @@ }, "devDependencies": { "@biomejs/biome": "^1.9.4", + "@types/bun": "^1.1.14", "fast-check": "^3.22.0", - "@types/bun": "^1.1.14" + "playwright": "1.59.1" }, "peerDependencies": { "typescript": "^5.7.0" diff --git a/playground/hash_pandas_object.html b/playground/hash_pandas_object.html index 134212d7..4f8a20d7 100644 --- a/playground/hash_pandas_object.html +++ b/playground/hash_pandas_object.html @@ -244,7 +244,7 @@

2 · DataFrame row hashing

import { DataFrame, hashPandasObject } from "tsb";
 
-const df = new DataFrame({
+const df = DataFrame.fromColumns({
   id:   [1, 2, 3],
   name: ["Alice", "Bob", "Alice"],
   age:  [30, 25, 30],
@@ -276,7 +276,7 @@ 

3 · Deduplication with hashes

import { DataFrame, hashPandasObject } from "tsb";
 
-const df = new DataFrame({
+const df = DataFrame.fromColumns({
   a: [1, 2, 1, 3],
   b: ["x", "y", "x", "z"],
 });
diff --git a/playground/merge_ordered.html b/playground/merge_ordered.html
index 8f1a2c2f..423261b7 100644
--- a/playground/merge_ordered.html
+++ b/playground/merge_ordered.html
@@ -176,13 +176,13 @@ 

Basic outer ordered merge

}); const result = mergeOrdered(left, right, { on: "date" }); +console.log(result.toString()); // date | price | volume // 1 | 10 | null // 2 | null | 200 // 3 | 30 | 300 // 5 | 50 | null -// 6 | null | 600 -console.log(result); +// 6 | null | 600
Click ▶ Run to execute
Ctrl+Enter to run · Tab to indent
@@ -198,17 +198,28 @@

Forward-fill after merge

- +// 6 | 50 | 600
Click ▶ Run to execute
Ctrl+Enter to run · Tab to indent
@@ -224,8 +235,14 @@

Inner join variant

-
Click ▶ Run to execute
@@ -243,10 +260,13 @@

Different key column names per side

- +// B | 3 | 300 | 300
Click ▶ Run to execute
Ctrl+Enter to run · Tab to indent
@@ -308,10 +330,13 @@

Overlapping non-key columns — suffixes

-