Add interactive playground with WASM discovery and Pyodide test execution#126
Add interactive playground with WASM discovery and Pyodide test execution#126thejchap wants to merge 28 commits into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0d0d8d7876
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Adds a browser playground for tryke that combines client-side Rust/WASM discovery/report formatting with a React/Vite UI and Pyodide-based Python test execution.
Changes:
- Adds a new
tryke_wasmcrate and feature-gates native-only discovery/reporter dependencies. - Adds a React playground with Monaco editing, discovery panels, graph visualization, terminal output, examples, and a Pyodide worker.
- Moves
DiscoveredFileintotryke_typesand updates workspace/package configuration for WASM support.
Reviewed changes
Copilot reviewed 30 out of 34 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
.claude/launch.json |
Adds playground launch configuration. |
.gitignore |
Ignores Node/build/generated playground artifacts. |
Cargo.lock |
Updates dependency lockfile for WASM-related changes. |
Cargo.toml |
Adjusts workspace dependencies and adds WASM bindings dependencies. |
crates/tryke/Cargo.toml |
Enables native features for CLI dependencies. |
crates/tryke_discovery/Cargo.toml |
Adds native feature gating for discovery dependencies. |
crates/tryke_discovery/src/db.rs |
Re-exports moved DiscoveredFile. |
crates/tryke_discovery/src/lib.rs |
Gates native-only APIs and exposes source discovery/import resolution for WASM. |
crates/tryke_reporter/Cargo.toml |
Adds native feature gating for reporter dependencies. |
crates/tryke_reporter/src/clear.rs |
Makes terminal clearing native-only. |
crates/tryke_reporter/src/lib.rs |
Gates native-only reporter modules/exports. |
crates/tryke_server/Cargo.toml |
Enables native features for server dependencies. |
crates/tryke_types/src/lib.rs |
Moves DiscoveredFile into shared types. |
crates/tryke_wasm/Cargo.toml |
Adds the WASM crate manifest. |
crates/tryke_wasm/src/lib.rs |
Exposes discovery and reporter formatting functions through wasm-bindgen. |
playground/bun.lock |
Adds frontend dependency lockfile. |
playground/index.html |
Adds Vite app HTML entrypoint. |
playground/package.json |
Defines playground scripts and frontend dependencies. |
playground/tsconfig.json |
Adds TypeScript configuration. |
playground/vite.config.ts |
Configures Vite plugins and WASM handling. |
playground/src/Editor/Chrome.tsx |
Implements playground shell, file tabs, examples, WASM/Pyodide wiring, and run controls. |
playground/src/Editor/DiscoveryPanel.tsx |
Displays discovered tests, hooks, errors, and dynamic import status. |
playground/src/Editor/Editor.tsx |
Connects editor, discovery, graph, and output panels. |
playground/src/Editor/GraphView.tsx |
Renders import graph visualization. |
playground/src/Editor/SecondarySideBar.tsx |
Adds secondary panel tabs. |
playground/src/Editor/SourceEditor.tsx |
Adds Monaco Python editor wrapper. |
playground/src/Editor/TerminalOutput.tsx |
Adds xterm.js output rendering. |
playground/src/Editor/constants.ts |
Adds default files and curated examples. |
playground/src/Editor/index.tsx |
Exports playground chrome component. |
playground/src/Editor/types.ts |
Defines playground TypeScript data types. |
playground/src/main.tsx |
Mounts the React app. |
playground/src/styles/global.css |
Adds Tailwind imports and playground theme tokens. |
playground/src/vite-env.d.ts |
Adds Vite/raw Python module typings. |
playground/src/workers/pyodide.worker.ts |
Adds Pyodide initialization and browser-side Python test runner. |
Comments suppressed due to low confidence (1)
playground/src/workers/pyodide.worker.ts:81
- This error outcome uses the same invalid wire format as the import-error path:
TestOutcome::Errorexpectsdetailto contain amessagefield, not a string. As written, a missing discovered function makes reporter formatting fail and shows raw JSON in the terminal.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2456afda90
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 34 out of 39 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (3)
playground/src/Editor/Chrome.tsx:181
handleAddFileaccepts a name that already exists, which creates duplicate React keys for the file tabs and causes later test execution to write multiple playground files to the same Pyodide path. Reject duplicate normalized filenames (or switch to stable unique ids while still preventing duplicate on-disk paths) before appending the new file.
const handleAddFile = useCallback(() => {
if (!newFileName) return;
const name = newFileName.endsWith(".py")
? newFileName
: `${newFileName}.py`;
setFiles((prev) => [...prev, { name, source: "" }]);
playground/src/workers/pyodide.worker.ts:81
- Writing each playground file with
open()assumes every parent directory already exists. If a user adds a file such aspkg/helpers.pyto test package-style imports, the worker raisesFileNotFoundErrorbefore any tests run. Create the parent directories in the Pyodide FS before writing each file (and similarly for the single-file path).
playground/src/Editor/Chrome.tsx:278 - The file tabs are implemented as clickable
divs, so they are not focusable or activatable from the keyboard. Usebuttonelements (or add the appropriate tab roles,tabIndex, and keyboard handlers) so users who do not use a mouse can switch files.
<div
key={file.name}
className={`flex items-center gap-1 px-3 py-1.5 text-xs cursor-pointer border-r border-border ${
i === activeFileIndex
? "bg-bg text-text"
: "text-text-dim hover:text-text hover:bg-surface-hover"
}`}
onClick={() => setActiveFileIndex(i)}
>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 22716697ca
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dccc1a338a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e48ae11109
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 56 out of 61 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (1)
python/tryke/playground.py:37
- The single-file path has the same nested-path failure:
open('/home/pyodide/{filename}', 'w')raises iffilenameincludes a directory. Ensure parent directories exist before writing the active file.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e6b1c6ea3b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 152f152df3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 56 out of 61 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
python/tryke/playground.py:38
- The single-file path has the same directory-creation issue: a filename containing a subdirectory will raise
FileNotFoundErrorbefore the test module can be imported. Ensure the parent directory exists before writing the source file.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 27f8eef15a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if case_index < len(cases): | ||
| case_label = cases[case_index].label | ||
|
|
||
| result = run_test(fn, xfail=t.get("xfail"), case_label=case_label) |
A ⊞ toggle in the tab bar switches between single-tab and stacked mode, where all four panels (Discovery, Import Graph, Fixture Graph, Output) render vertically in a scrollable container. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tooltips on: run button, example/reporter pickers, Pyodide status badge, docs link, file add/remove buttons, and skip/todo/xfail badges (showing the reason text) and hook scope labels. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Running tests no longer overrides the active tab — the output panel updates in place regardless of which view is selected. Fixed test.cases examples that incorrectly mixed positional specs with a name= kwarg. Added a Kitchen Sink example with multi-file imports, fixtures, describe blocks, parametrized cases, and markers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Custom Monaco theme matching the Catppuccin Mocha UI colors - Stacked layout: Output on top (50%), Discovery/Import Graph/Fixture Graph evenly split below with overflow-y scrolling - Use Annotated[T, Depends(fixture)] form in all examples; update Pyodide runner to resolve Depends from type annotations - Remove name= kwargs from @test decorators (use function names) - Rename "Hooks" to "Fixtures" in Discovery panel and tooltips - Kitchen Sink: test file listed first, source file second Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…spinner - Extract test execution logic from worker.py into runner.py, reducing worker.py by ~270 lines of duplicated code - Create playground.py as a thin harness that delegates to runner.run_test - Move RunTestResultWire and AssertionWire to tryke_types so the WASM crate can accept the flat runner format directly, eliminating the Python-side _to_wire conversion entirely - Extract inline example Python code from constants.ts into standalone .py files under playground/examples/ so ruff can lint/format them - Add playground/examples to ruff src config with appropriate per-file ignores for example code - Show a loading spinner in the output panel while Pyodide loads - Pass pyodideReady through to Editor for conditional rendering Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The playground/examples/ directory contains intentionally-failing tests for the browser demo. Exclude them from the project test suite so CI does not report spurious failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- runner: run async tests on resolver's shared loop and reject case kwargs that collide with fixture-injected parameters, matching HookExecutor.run_test semantics for the no-executor playground path - playground: route discovered doctest items through the doctest runner, create parent directories for nested filenames, purge files removed between runs (and their cached modules), and invalidate import caches before re-importing - playground frontend: focus the existing tab when a user adds a file whose name is already open instead of creating a duplicate tab - playground build: add `wasm` / `predev` / `prebuild` scripts so a fresh checkout can `bun run dev` or `bun run build` without first invoking wasm-pack manually - tests: cover the no-executor collision and shared-loop behaviors and the playground multi-file/cleanup/doctest paths
Build the WASM crate and Vite frontend in CI, then deploy to Cloudflare Pages via wrangler. Triggers on pushes to main that touch playground/, WASM crate, discovery, reporter, types, or the Python tryke package. Also supports manual dispatch. Requires CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID secrets. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rollup converts static imports from CDN URLs into global variable references (output.globals), causing ReferenceError in production. Switch to dynamic import() with @vite-ignore so the CDN module is loaded at runtime instead of being bundled. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Set tryke_guard.__TRYKE_TESTING__ = True in the Pyodide worker init so tests inside guard blocks execute at runtime (matching the native worker behavior). - Normalize file paths in discover_multi by joining filenames with root before passing to discover_file_from_source. Fixes relative import resolution for nested files (e.g. pkg/test_api.py) where candidate paths need the root prefix for starts_with checks. - Share a HookExecutor across the test loop in playground.py so per-scope fixtures are resolved once and reused, matching the native worker's fixture lifecycle. The frontend now sends WASM- discovered hooks alongside tests; playground.py registers them with a HookExecutor and passes it to run_test. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Trim to the essential patterns: scope fixture, Annotated Depends, describe blocks, to_raise, and parametrized cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Rust HookItem uses skip_serializing_if = "Vec::is_empty", so the groups field is absent when empty. Use .get() with a default instead of direct key access. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use stable root "." for single-file discover() so nested files like pkg/test_api.py keep their full module_path. - Validate user filenames: reject paths with ".." or that shadow the bundled tryke package under /home/pyodide. - Add run request IDs so stale Pyodide results from a previous run are discarded instead of overwriting the current output. - Add python/tryke_guard.py, Cargo.toml, and Cargo.lock to the playground workflow trigger paths. - Use button with role="tab" and aria-selected for file tabs; add aria-label to the panel layout toggle. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add tryke.py and tryke_guard.py to the reserved names so a user tab cannot shadow the bundled framework module. - Use div[role=tab] with tabIndex and keyboard handler instead of a button to avoid nesting interactive elements (the close button was inside the tab button). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Native and WASM/Pyodide were diverging into two parallel implementations of the same plumbing. Push every duplicated piece down to its natural common location so each surface keeps only the thin glue it actually needs. - `tryke_types` now owns `convert_wire_result`, the full assertion enrichment pipeline (`convert_assertion`, span computation, expected matching, executed-line mapping, etc.) and a `RunSummary::from_results` helper. Both `tryke_runner` and `tryke_wasm` call into them instead of carrying private copies. - `tryke_wasm` drops its hand-rolled `convert_wire_result_simple` and `build_summary`, and stops needing `TestOutcome` / `Duration` imports. - `python/tryke/runner.py` grows `HookInfo`, `build_executor_from_hooks`, and `run_doctest` — all derived directly from the worker/playground duplicates. `worker.py` keeps its JSON-RPC validation step and then delegates; `playground.py` becomes a thin harness around `run_doctest` / `build_executor_from_hooks` / `run_test`. - Drop the now-unused `span_offset` / `span_length` fields from `AssertionWire`: they were always `0` from Python and overwritten by the Rust-side `compute_subject_span`, so the wire roundtrip was carrying dead weight. - `playground.py` reads `case_label` straight off the `TestItem` (discovery already emits it) instead of looking it up through `__tryke_cases__` by `case_index`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
b3f1c66 to
23f9f26
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 23f9f26713
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| else: | ||
| if not _is_safe_filename(filename): | ||
| msg = f"unsafe filename: {filename!r}" | ||
| raise ValueError(msg) | ||
| file_path = _PYODIDE_ROOT / filename | ||
| file_path.parent.mkdir(parents=True, exist_ok=True) | ||
| file_path.write_text(source) |
| test.case("empty", value=""), | ||
| test.case("hello", value="hello"), | ||
| test.case("spaces", value=" hi "), | ||
| ) | ||
| def string_strip(value): | ||
| expect(value.strip(), name="stripped value").to_equal(value.strip()) |
| pub struct AssertionWire { | ||
| pub expression: String, | ||
| pub expected: String, | ||
| pub received: String, | ||
| pub line: u32, | ||
| #[serde(default)] | ||
| pub column: Option<u32>, | ||
| #[serde(default)] | ||
| pub file: Option<String>, | ||
| } |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d794caf5b5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| for stale in _WRITTEN_FILES - current_names: | ||
| stale_path = _PYODIDE_ROOT / stale | ||
| with contextlib.suppress(FileNotFoundError): | ||
| stale_path.unlink() |
There was a problem hiding this comment.
Remove stale package directories after unlinking files
When the playground run set shrinks from a package file such as pkg/__init__.py to no files under pkg/, the current cleanup only unlinks the stale file and leaves the now-empty parent directory behind. Because Python treats that leftover directory as a namespace package after _purge_module("pkg"), a later test containing import pkg can still pass even though the user removed the package from the open-file set; the fresh evidence is that this cleanup deletes files but never prunes empty parent directories.
Useful? React with 👍 / 👎.
Context
Adds a browser-based playground (similar to Ruff's playground) that lets users write Python tests, see discovered tests/hooks in real-time, visualize import graphs, and execute tests — all entirely client-side with no backend.
Changes
tryke_discoveryandtryke_reporterbehind anativefeature so parsing and reporting compile towasm32-unknown-unknowntryke_wasmcrate exposingdiscover(),format_results(),format_collect(), anddiscover_multi()viawasm-bindgenDiscoveredFiletotryke_typesso the WASM crate can use it without pulling in salsa/rayonuseDeferredValue)Test plan
cargo clippy --workspace --all-targets --all-features -- -D warnings— cleancargo nextest run --workspace --all-features— 651 passeduvx prek run -a— all hooks passwasm-pack build crates/tryke_wasm --target web— builds cleanlycd playground && bun run build— production build succeeds🤖 Generated with Claude Code