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
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ jobs:
# condition comments, readable types and argument checks, the doctest
# runner, and which errors get a hint.
- run: npm run test:runtime
# The console in Chrome (on the runner) with Pyodide from its CDN,
# driving the embed built above: Tests → edit → Tests shows the new
# report (P20).
- run: npm run test:console
- name: Not checked here
run: |
echo "::notice title=Not run in CI::Needs Pyodide in a browser, so it isn't checked here: the worker (run, REPL, Tests in the browser, input() bridge, gturtle) and the UI. Check these in the embed preview (npm run build:embed && npm run preview:embed)."
echo "::notice title=Not run in CI::Needs Pyodide in a browser, so apart from test:console it isn't checked here: the worker (run, REPL, input() bridge, gturtle) and the UI. Check these in the embed preview (npm run build:embed && npm run preview:embed)."
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ npm run lint # oxlint
npm run test:protocol # builds the library, then runs scripts/check-embed-protocol.mjs against dist/
npm run test:runtime # the runtime under CPython: scripts/check-runtime-{columns,values,doctests,calls,hints,fidelity,replay}.py
npm run test:hints # the error hints' wording (de/en) against the built dist/ (run a build first)
npm run test:console # the console in Chrome with Pyodide, against dist-embed/ (run build:embed first)
npm run build:embed # builds the iframe app + demo page into dist-embed/
npm run preview:embed # serves dist-embed/ with COOP/COEP headers; open /demo.html
```

`npm run dev:embed` currently 404s: `embed/index.html` loads `../src/embed/main.tsx`, which lies outside the Vite root, so only the build works. Use `build:embed` + `preview:embed`.

There is no unit test framework. The automated checks are `test:protocol` (plain `node:assert` against the built `dist/index.js`; add protocol validation cases there) and `test:runtime` (plain Python scripts that exec `src/runtime/` under CPython 3.12, Pyodide's version; add runtime regression cases there). `.github/workflows/ci.yml` runs lint, both builds and the checks on every pull request and on pushes to `main`. Nothing that needs Pyodide in a browser (the worker, the UI) is checked automatically. `npm run lint` has one known warning in `GlPythonWorkspace.tsx` (`react(immutability)` on the `cursor` variable in `Frame`).
There is no unit test framework. The automated checks are `test:protocol` (plain `node:assert` against the built `dist/index.js`; add protocol validation cases there) and `test:runtime` (plain Python scripts that exec `src/runtime/` under CPython 3.12, Pyodide's version; add runtime regression cases there). `.github/workflows/ci.yml` runs lint, both builds and the checks on every pull request and on pushes to `main`. The one browser check is `test:console` (`scripts/check-console-browser.mjs`): `playwright-core` drives the installed Chrome (GitHub's runner has one) against `vite preview` of `dist-embed/`, through the embed protocol, with Pyodide from its CDN; add browser regression cases there. Everything else that needs Pyodide in a browser (the worker, most of the UI) is not checked automatically. `npm run lint` has one known warning in `GlPythonWorkspace.tsx` (`react(immutability)` on the `cursor` variable in `Frame`).

**Releases.**
- The package is consumed from git: the glpython IDE (`../glpython`) pins `glpython-editor` by tag in its `package.json`. The `prepare` script builds `dist/` on install.
Expand Down
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"preview:embed": "vite preview --config vite.embed.config.ts",
"test:protocol": "npm run build && node scripts/check-embed-protocol.mjs",
"test:hints": "node scripts/check-error-hints.mjs",
"test:console": "node scripts/check-console-browser.mjs",
"test:runtime": "python3 -B scripts/check-runtime-columns.py && python3 -B scripts/check-runtime-values.py && python3 -B scripts/check-runtime-doctests.py && python3 -B scripts/check-runtime-calls.py && python3 -B scripts/check-runtime-hints.py && python3 -B scripts/check-runtime-fidelity.py && python3 -B scripts/check-runtime-replay.py && python3 -B scripts/check-runtime-imports.py",
"prepare": "npm run build"
},
Expand All @@ -43,6 +44,7 @@
"@vitejs/plugin-react": "^6.1.1",
"lucide-react": "^1.47.0",
"oxlint": "^1.81.0",
"playwright-core": "^1.48.2",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"typescript": "~6.0.2",
Expand Down
85 changes: 85 additions & 0 deletions scripts/check-console-browser.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Browser check of the console (P20): Tests, then an edit, then Tests again
// - without ▶ Run in between - runs the edited code, and its report is in
// view, also when the learner scrolled up to read the first report. Before
// the fix the second report was appended out of view below the first one,
// so «Tests» looked as if it didn't react.
//
// Drives the built embed (dist-embed/, from npm run build:embed) in Chrome
// through its protocol, as the LiaScript macro does; Pyodide comes from its
// CDN. Needs Google Chrome (GitHub's ubuntu-latest has it).
//
// npm run build:embed && npm run test:console
import assert from 'node:assert/strict'
import { spawn } from 'node:child_process'
import { chromium } from 'playwright-core'

const port = 4185
const origin = `http://localhost:${port}`
const server = spawn('npx', ['vite', 'preview', '--config', 'vite.embed.config.ts', '--port', String(port), '--strictPort'], { stdio: 'ignore' })
const stopServer = () => server.kill()
process.on('exit', stopServer)

// The parent page, served on the embed's own origin: it loads the embed and
// passes messages on, version 4.
const harness = `<!doctype html><meta charset="utf-8"><body style="margin:0">
<iframe id="embed" style="width:760px;height:900px;border:0"></iframe>
<script>
const iframe = document.querySelector('#embed')
window.ready = false
window.send = (type, source) => iframe.contentWindow.postMessage({ type: 'glpython:' + type, version: 4, source, mode: 'eval' }, location.origin)
addEventListener('message', (event) => { if (event.data?.type === 'glpython:ready') window.ready = true })
iframe.src = './index.html?parentOrigin=' + encodeURIComponent(location.origin) + '&mode=eval&lang=de'
</script>`

const program = (examples) => `def schnitt(a, b):\n """\n${examples.map(([call, want]) => ` >>> ${call}\n ${want}\n`).join('')} """\n return (a + b) / 2\n`
// Enough failing examples that the report is taller than the console.
const failing = program([['schnitt(4, 6)', '5'], ['schnitt(6, 4)', '4.5'], ['schnitt(2, 2)', '3'], ['schnitt(1, 1)', '7'], ['schnitt(0, 4)', '1']])
const edited = program([['schnitt(4, 6)', '5.0'], ['schnitt(6, 4)', '4.5'], ['schnitt(2, 2)', '3'], ['schnitt(1, 1)', '7'], ['schnitt(0, 4)', '1']])
const passing = program([['schnitt(4, 6)', '5.0']])
const passingEdited = program([['schnitt(4, 6)', '5.0'], ['schnitt(1, 2)', '1.5']])

let failed = 0
const browser = await chromium.launch({ channel: 'chrome', headless: true })
try {
for (let attempt = 0; ; attempt++) {
try { await fetch(`${origin}/index.html`); break } catch { if (attempt > 50) throw new Error('vite preview did not start') }
await new Promise((resolve) => setTimeout(resolve, 200))
}
for (const [label, first, second, firstSummary, secondSummary] of [
['Tests → fail → edit → Tests', failing, edited, '0 von 5 Beispielen bestanden', '1 von 5 Beispielen bestanden'],
['Tests → pass → edit → Tests', passing, passingEdited, '1 von 1 Beispielen bestanden', '2 von 2 Beispielen bestanden'],
]) {
const page = await browser.newPage({ viewport: { width: 800, height: 950 } })
await page.route(`${origin}/check-console.html`, (route) => route.fulfill({ contentType: 'text/html', body: harness }))
await page.goto(`${origin}/check-console.html`)
await page.waitForFunction(() => window.ready, null, { timeout: 120000 })
const frame = page.frameLocator('#embed')
const summaries = frame.locator('.test-summary')
await page.evaluate((source) => window.send('test', source), first)
await summaries.first().waitFor({ timeout: 60000 })
// The learner reads the report from its top.
await frame.locator('.console').evaluate((element) => { element.scrollTop = 0; element.dispatchEvent(new Event('scroll')) })
await page.evaluate((source) => window.send('test', source), second)
await summaries.nth(1).waitFor({ timeout: 60000 })
await page.waitForTimeout(300)
try {
assert.deepEqual(await summaries.allInnerTexts(), [firstSummary, secondSummary], `${label}: the second run tests the edited code`)
const inView = await frame.locator('.console').evaluate((element) => {
const view = element.getBoundingClientRect()
const summary = [...element.querySelectorAll('.test-summary')].at(-1).getBoundingClientRect()
return summary.top >= view.top && summary.bottom <= view.bottom
})
assert.ok(inView, `${label}: the new report's summary is in the console's view`)
console.log(`${label}: ok`)
} catch (error) {
failed += 1
console.error(error.message)
}
await page.close()
}
} finally {
await browser.close()
stopServer()
}
if (failed > 0) process.exit(1)
console.log('Console browser checks passed.')
Loading
Loading