Skip to content

Commit d566820

Browse files
authored
Merge pull request #259 from githubnext/copilot/add-playwright-tests-for-playground
Add Playwright tests that execute every playground cell, fix merge_ordered
2 parents 23a2109 + 823c55a commit d566820

8 files changed

Lines changed: 467 additions & 20 deletions

File tree

.github/workflows/ci.yml

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,33 @@ jobs:
3636
run: bun run lint
3737

3838
- name: Test
39-
run: bun test --coverage
39+
run: bun test --coverage ./tests/
40+
41+
playground-e2e:
42+
name: Playground E2E (Playwright)
43+
runs-on: ubuntu-latest
44+
steps:
45+
- uses: actions/checkout@v4
46+
47+
- name: Setup Bun
48+
uses: oven-sh/setup-bun@v2
49+
with:
50+
bun-version: latest
51+
52+
- name: Install dependencies
53+
run: bun install
54+
55+
- name: Cache Playwright browsers
56+
uses: actions/cache@v4
57+
with:
58+
path: ~/.cache/ms-playwright
59+
key: playwright-${{ runner.os }}-${{ hashFiles('bun.lock') }}
60+
61+
- name: Install Playwright browsers
62+
run: bunx playwright install --with-deps chromium
63+
64+
- name: Run Playwright playground tests
65+
run: bun run test:e2e
4066

4167
build:
4268
name: Build

bun.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/playground.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,54 @@ cp node_modules/typescript/lib/typescript.js ./playground/dist/typescript.js
118118

119119
The CI pipeline (`pages.yml`) runs this automatically during deployment.
120120

121+
## End-to-End Cell Execution Tests
122+
123+
To make sure every code cell on every playground page actually works (no
124+
TypeScript errors, no runtime errors, real output), the project ships a
125+
Playwright-based test suite under `tests-e2e/playground-cells.test.ts`.
126+
127+
It launches headless Chromium, navigates to every `playground/*.html` page,
128+
clicks **▶ Run** on every `.playground-block`, and asserts that the cell
129+
output is not an error and is not the "(no output …)" sentinel.
130+
131+
```bash
132+
bun install
133+
bunx playwright install --with-deps chromium
134+
bun run test:e2e
135+
```
136+
137+
CI runs this in the dedicated `playground-e2e` job (see `.github/workflows/ci.yml`).
138+
139+
### Known-failures allowlist
140+
141+
A large number of pages currently have at least one broken cell — most often
142+
because:
143+
144+
1. The "TypeScript" cell actually contains Python source (so TS lexing fails
145+
on the `import pandas as pd` line).
146+
2. A cell references a variable defined in a previous cell. **Each cell runs
147+
in its own `new Function()` scope, so nothing persists between cells**
148+
every cell needs its own `import { … } from "tsb"` and its own data setup.
149+
3. A cell never calls `console.log()` — the playground only shows what the
150+
user explicitly logs.
151+
152+
The file `tests-e2e/known-failures.json` enumerates the (file → cell numbers)
153+
that are currently broken so CI can pass while progress is made. Each entry
154+
should be **removed from the allowlist as the corresponding cell is fixed**
155+
the test framework also fails if a cell now passes but is still listed
156+
(forward-progress check).
157+
158+
### Authoring rule
159+
160+
Every cell on every playground page **must** be self-contained:
161+
162+
- Import everything it uses from `"tsb"` directly inside the cell.
163+
- Re-declare any helper data it depends on inside the cell.
164+
- Call `console.log(…)` (or `console.warn` / `console.error`) so output is
165+
visible.
166+
167+
See `playground/merge_ordered.html` for the canonical pattern.
168+
121169
## Non-Goals (Current Scope)
122170

123171
- **Infinite loop protection**: long-running or infinite loops will hang the

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
}
1515
},
1616
"scripts": {
17-
"test": "bun test",
17+
"test": "bun test ./tests/",
18+
"test:e2e": "bun test --timeout 600000 tests-e2e",
1819
"lint": "biome check .",
1920
"lint:fix": "biome check --write .",
2021
"typecheck": "tsc --noEmit",
@@ -23,8 +24,9 @@
2324
},
2425
"devDependencies": {
2526
"@biomejs/biome": "^1.9.4",
27+
"@types/bun": "^1.1.14",
2628
"fast-check": "^3.22.0",
27-
"@types/bun": "^1.1.14"
29+
"playwright": "1.59.1"
2830
},
2931
"peerDependencies": {
3032
"typescript": "^5.7.0"

playground/hash_pandas_object.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ <h2>2 · DataFrame row hashing</h2>
244244
</div>
245245
<pre class="playground-editor" contenteditable="true" spellcheck="false">import { DataFrame, hashPandasObject } from "tsb";
246246

247-
const df = new DataFrame({
247+
const df = DataFrame.fromColumns({
248248
id: [1, 2, 3],
249249
name: ["Alice", "Bob", "Alice"],
250250
age: [30, 25, 30],
@@ -276,7 +276,7 @@ <h2>3 · Deduplication with hashes</h2>
276276
</div>
277277
<pre class="playground-editor" contenteditable="true" spellcheck="false">import { DataFrame, hashPandasObject } from "tsb";
278278

279-
const df = new DataFrame({
279+
const df = DataFrame.fromColumns({
280280
a: [1, 2, 1, 3],
281281
b: ["x", "y", "x", "z"],
282282
});

playground/merge_ordered.html

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,13 @@ <h2>Basic outer ordered merge</h2>
176176
});
177177

178178
const result = mergeOrdered(left, right, { on: "date" });
179+
console.log(result.toString());
179180
// date | price | volume
180181
// 1 | 10 | null
181182
// 2 | null | 200
182183
// 3 | 30 | 300
183184
// 5 | 50 | null
184-
// 6 | null | 600
185-
console.log(result);</textarea>
185+
// 6 | null | 600</textarea>
186186
<div class="playground-output">Click ▶ Run to execute</div>
187187
<div class="playground-hint">Ctrl+Enter to run · Tab to indent</div>
188188
</div>
@@ -198,17 +198,28 @@ <h2>Forward-fill after merge</h2>
198198
<button class="playground-reset">↺ Reset</button>
199199
</div>
200200
</div>
201-
<textarea class="playground-editor" spellcheck="false">const result = mergeOrdered(left, right, {
201+
<textarea class="playground-editor" spellcheck="false">import { DataFrame, mergeOrdered } from "tsb";
202+
203+
const left = DataFrame.fromColumns({
204+
date: [1, 3, 5],
205+
price: [10, 30, 50],
206+
});
207+
const right = DataFrame.fromColumns({
208+
date: [2, 3, 6],
209+
volume: [200, 300, 600],
210+
});
211+
212+
const result = mergeOrdered(left, right, {
202213
on: "date",
203214
fill_method: "ffill",
204215
});
216+
console.log(result.toString());
205217
// date | price | volume
206218
// 1 | 10 | null ← no earlier price to fill
207219
// 2 | 10 | 200 ← price carried forward from date=1
208220
// 3 | 30 | 300
209221
// 5 | 50 | 300 ← volume carried forward from date=3
210-
// 6 | 50 | 600
211-
console.log(result);</textarea>
222+
// 6 | 50 | 600</textarea>
212223
<div class="playground-output">Click ▶ Run to execute</div>
213224
<div class="playground-hint">Ctrl+Enter to run · Tab to indent</div>
214225
</div>
@@ -224,8 +235,14 @@ <h2>Inner join variant</h2>
224235
<button class="playground-reset">↺ Reset</button>
225236
</div>
226237
</div>
227-
<textarea class="playground-editor" spellcheck="false">// Only rows where both DataFrames have a key
228-
console.log(mergeOrdered(left, right, { on: "date", how: "inner" }));
238+
<textarea class="playground-editor" spellcheck="false">import { DataFrame, mergeOrdered } from "tsb";
239+
240+
const left = DataFrame.fromColumns({ date: [1, 3, 5], price: [10, 30, 50] });
241+
const right = DataFrame.fromColumns({ date: [2, 3, 6], volume: [200, 300, 600] });
242+
243+
// Only rows where both DataFrames have a key
244+
const result = mergeOrdered(left, right, { on: "date", how: "inner" });
245+
console.log(result.toString());
229246
// date | price | volume
230247
// 3 | 30 | 300</textarea>
231248
<div class="playground-output">Click ▶ Run to execute</div>
@@ -243,10 +260,13 @@ <h2>Different key column names per side</h2>
243260
<button class="playground-reset">↺ Reset</button>
244261
</div>
245262
</div>
246-
<textarea class="playground-editor" spellcheck="false">const left2 = DataFrame.fromColumns({ t_left: [1, 3, 5], a: [10, 30, 50] });
263+
<textarea class="playground-editor" spellcheck="false">import { DataFrame, mergeOrdered } from "tsb";
264+
265+
const left2 = DataFrame.fromColumns({ t_left: [1, 3, 5], a: [10, 30, 50] });
247266
const right2 = DataFrame.fromColumns({ t_right: [2, 3, 6], b: [200, 300, 600] });
248267

249-
console.log(mergeOrdered(left2, right2, { left_on: "t_left", right_on: "t_right" }));
268+
const result = mergeOrdered(left2, right2, { left_on: "t_left", right_on: "t_right" });
269+
console.log(result.toString());
250270
// t_left | a | b
251271
// 1 | 10 | null
252272
// 2 | null | 200
@@ -268,7 +288,9 @@ <h2>Group-wise ordered merge (left_by / right_by)</h2>
268288
<button class="playground-reset">↺ Reset</button>
269289
</div>
270290
</div>
271-
<textarea class="playground-editor" spellcheck="false">// Perform the ordered merge independently for each group
291+
<textarea class="playground-editor" spellcheck="false">import { DataFrame, mergeOrdered } from "tsb";
292+
293+
// Perform the ordered merge independently for each group
272294
const left3 = DataFrame.fromColumns({
273295
grp: ["A", "A", "B", "B"],
274296
k: [1, 3, 1, 3],
@@ -280,19 +302,19 @@ <h2>Group-wise ordered merge (left_by / right_by)</h2>
280302
b: [20, 30, 200, 300],
281303
});
282304

283-
mergeOrdered(left3, right3, {
305+
const result = mergeOrdered(left3, right3, {
284306
on: "k",
285307
left_by: "grp",
286308
right_by: "grp",
287309
});
310+
console.log(result.toString());
288311
// grp | k | a | b
289312
// A | 1 | 10 | null
290313
// A | 2 | null | 20
291314
// A | 3 | 30 | 30
292315
// B | 1 | 100 | null
293316
// B | 2 | null | 200
294-
// B | 3 | 300 | 300
295-
console.log(right3);</textarea>
317+
// B | 3 | 300 | 300</textarea>
296318
<div class="playground-output">Click ▶ Run to execute</div>
297319
<div class="playground-hint">Ctrl+Enter to run · Tab to indent</div>
298320
</div>
@@ -308,10 +330,13 @@ <h2>Overlapping non-key columns — suffixes</h2>
308330
<button class="playground-reset">↺ Reset</button>
309331
</div>
310332
</div>
311-
<textarea class="playground-editor" spellcheck="false">const left4 = DataFrame.fromColumns({ k: [1, 2, 3], val: [10, 20, 30] });
333+
<textarea class="playground-editor" spellcheck="false">import { DataFrame, mergeOrdered } from "tsb";
334+
335+
const left4 = DataFrame.fromColumns({ k: [1, 2, 3], val: [10, 20, 30] });
312336
const right4 = DataFrame.fromColumns({ k: [2, 3, 4], val: [200, 300, 400] });
313337

314-
console.log(mergeOrdered(left4, right4, { on: "k", suffixes: ["_L", "_R"] }));
338+
const result = mergeOrdered(left4, right4, { on: "k", suffixes: ["_L", "_R"] });
339+
console.log(result.toString());
315340
// k | val_L | val_R
316341
// 1 | 10 | null
317342
// 2 | 20 | 200

tests-e2e/known-failures.json

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{
2+
"align.html": [2, 3, 5, 6, 7],
3+
"api_types.html": [2, 3, 4, 5, 6, 7],
4+
"assign.html": [2, 3, 4, 5],
5+
"at_iat.html": [1, 3, 4, 5, 7, 8, 9],
6+
"attrs.html": [2, 3, 4, 5, 6],
7+
"between.html": [1, 3, 5, 7],
8+
"clip_advanced.html": [1, 2, 3, 4, 5, 6],
9+
"corrwith.html": [1, 3, 5, 7],
10+
"crosstab.html": [2, 4, 6, 8, 10, 12],
11+
"cut.html": [2, 3, 4, 5, 6, 8],
12+
"cut_bins_to_frame.html": [1],
13+
"cut_qcut.html": [1, 2, 4, 5, 6, 7],
14+
"datetime_tz.html": [2, 4, 6, 8, 10],
15+
"dot_matmul.html": [1, 2],
16+
"eval_query.html": [1, 2, 3],
17+
"excel.html": [1, 2, 3, 4],
18+
"factorize.html": [2, 4, 6, 8],
19+
"filter.html": [1, 3, 5, 7, 9],
20+
"get_dummies.html": [2, 4, 6, 8],
21+
"infer_dtype.html": [5],
22+
"insert_pop.html": [6],
23+
"interpolate.html": [2, 6],
24+
"join.html": [2, 3, 4, 5, 6],
25+
"json_normalize.html": [2, 3],
26+
"memory_usage.html": [1, 2, 3, 4, 5],
27+
"merge_asof.html": [2, 3, 4, 5, 6, 7],
28+
"mode.html": [1, 2, 3, 4, 5, 6],
29+
"named_agg.html": [2, 3, 5, 6, 7],
30+
"natsort.html": [2, 6],
31+
"nunique.html": [2, 3, 5],
32+
"pipe_apply.html": [1, 3],
33+
"pivot_table.html": [2, 3, 4],
34+
"pow_mod.html": [5],
35+
"quantile.html": [1, 2, 3, 4, 5, 6, 7],
36+
"reduce_ops.html": [1, 3, 4, 6],
37+
"reindex.html": [6],
38+
"rolling_apply.html": [1, 2, 3, 4, 5, 6],
39+
"scalar_extract.html": [1, 3, 5, 7, 9, 11],
40+
"searchsorted.html": [6],
41+
"select_dtypes.html": [2, 3, 4, 5],
42+
"sem_var.html": [2, 3, 5],
43+
"skew_kurt.html": [1, 2, 3, 4, 5, 6],
44+
"sort_ops.html": [1, 3, 4, 5, 6, 7, 9, 10, 11],
45+
"str_findall_and_json_denormalize.html": [2, 3, 4, 5, 6, 8, 9, 10, 11, 12],
46+
"str_get_dummies.html": [2, 4, 6, 8],
47+
"style.html": [
48+
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25
49+
],
50+
"swaplevel.html": [1, 2, 3, 4, 5, 6, 7, 8],
51+
"testing.html": [1, 3, 4, 6, 8],
52+
"to_datetime.html": [2],
53+
"to_from_dict.html": [1, 2],
54+
"to_numeric.html": [1, 2, 3, 4, 5],
55+
"to_timedelta.html": [2],
56+
"transform_agg.html": [1, 2],
57+
"truncate.html": [1, 3, 5, 7],
58+
"update.html": [1, 3, 5],
59+
"value_counts_full.html": [1, 3, 4, 5, 6],
60+
"wide_to_long.html": [1, 2, 3, 4],
61+
"window_extended.html": [1, 3, 5],
62+
"xs.html": [1]
63+
}

0 commit comments

Comments
 (0)