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: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ npm run lint # oxlint; 7 known warnings, all in the vendored public/coi-servi
npm run test:browser # after a build: scripts/check-ide-browser.mjs, the IDE in headless Chrome
```

The one automated check is `test:browser` (`scripts/check-ide-browser.mjs`): `playwright-core` drives the installed Chrome (GitHub's runner has one) against `vite preview` of `dist/`, with Pyodide from its CDN and its own fixtures (it serves a library by URL itself). It covers tab switches with a read-only file, per-tab projects, share links (library by URL, 404, cut and long links), ▶ and Tests, and ⋮ «Info»; add regression cases there. The browser's «leave the page» question and the iPad stay manual. `.github/workflows/ci.yml` runs `npm ci`, the build (tsc catches type changes in a new editor tag), lint and `test:browser` on every pull request. Pushes to `main` deploy to GitHub Pages (`deploy-pages.yml`). To check a change in the browser, use `npm run dev`; `npm run build && npm run preview` serves the production layout. The embed is a separate project site under the same origin, https://glpython-edu.github.io/glpython-editor/. The IDE's `coi-serviceworker.js` registers for scope `/`, which includes it, so after a change to that worker check the embed too: its demo page opened directly, and a LiaScript course.
The one automated check is `test:browser` (`scripts/check-ide-browser.mjs`): `playwright-core` drives the installed Chrome (GitHub's runner has one) against `vite preview` of `dist/`, with Pyodide from its CDN and its own fixtures (it serves a library by URL itself). It covers tab switches with a read-only file, per-tab projects, share links (library by URL, 404, cut and long links), ▶ and Tests, ⋮ «Info», and `input()` in the console (a recursive REPL, Stop → `EOFError`); add regression cases there. The browser's «leave the page» question and the iPad stay manual. `.github/workflows/ci.yml` runs `npm ci`, the build (tsc catches type changes in a new editor tag), lint and `test:browser` on every pull request. Pushes to `main` deploy to GitHub Pages (`deploy-pages.yml`). To check a change in the browser, use `npm run dev`; `npm run build && npm run preview` serves the production layout. The embed is a separate project site under the same origin, https://glpython-edu.github.io/glpython-editor/. The IDE's `coi-serviceworker.js` registers for scope `/`, which includes it, so after a change to that worker check the embed too: its demo page opened directly, and a LiaScript course.

## Layout

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
},
"dependencies": {
"@azure/msal-browser": "^5.22.0",
"glpython-editor": "git+https://github.com/glpython-edu/glpython-editor.git#v0.1.21",
"glpython-editor": "git+https://github.com/glpython-edu/glpython-editor.git#v0.1.22",
"jszip": "^3.10.2",
"lucide-react": "^1.47.0",
"lz-string": "^1.5.0",
Expand Down
65 changes: 64 additions & 1 deletion scripts/check-ide-browser.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
// closed tab's project offered again;
// - share links: a library by URL (served here), a library that gives 404,
// a link cut at 2,083 characters, the note for a long link;
// - ▶ and «Tests» on a project of two files; ⋮ «Info» and its update check.
// - ▶ and «Tests» on a project of two files; ⋮ «Info» and its update check;
// - input() in the console's input line: ▶, a recursive REPL called in the
// REPL, Stop → EOFError.
// The browser's «leave the page» question and the iPad cases stay manual:
// headless Chrome can't confirm them.
//
Expand Down Expand Up @@ -51,6 +53,8 @@ const link = (files, mainFile, version = 1) => {
const helfer = 'def doppelt(x):\n return 2 * x\n'
const main = 'from helfer import doppelt\n\n\ndef viermal(x):\n """\n >>> viermal(2)\n 8\n """\n return doppelt(doppelt(x))\n\n\nprint(viermal(21))\n'
const twoFiles = link([file('helfer.py', helfer), file('main.py', main)], 'main.py')
const greeting = link([file('main.py', 'name = input("Name: ")\nprint("Hallo", name)\n')], 'main.py')
const language = link([file('main.py', 'def berechnen(text):\n return eval(text)\n\ndef repl():\n text = input("> ")\n if text == "ende":\n return\n print(berechnen(text))\n repl()\n')], 'main.py')
const withReadOnly = link([file('bib.py', library, { readOnly: true }), file('main.py', 'from bib import gross\nprint(gross("hallo"))\n')], 'main.py')
const byUrl = (url) => link([{ path: 'bib.py', url, readOnly: true }, file('main.py', 'from bib import gross\nprint(gross("hallo"))\n')], 'main.py', 2)
// A file that compresses badly, for a link longer than 2,083 characters.
Expand Down Expand Up @@ -228,6 +232,65 @@ try {
await context.close()
})

// input(): the console's >>> line becomes the program's input line, with
// the prompt in front; prompt and answer stay in the transcript.
const programLine = (page) => page.locator('.repl-line--program .repl-input')
const answer = async (page, text, prompt) => {
await programLine(page).waitFor({ timeout: 60000 })
assert.equal(await page.locator('.repl-line--program .repl-prompt').textContent(), prompt)
assert.ok(await programLine(page).evaluate((element) => element === document.activeElement), 'the input line has the focus')
await programLine(page).fill(text)
await programLine(page).press('Enter')
}
// A REPL input, once the REPL is free again (until then it keeps the text).
const enterRepl = async (page, text) => {
const line = page.locator('.repl-line .repl-input')
await page.getByRole('textbox', { name: 'Python-Eingabe' }).fill(text)
for (let attempt = 0; attempt < 100 && await line.inputValue() === text; attempt++) {
await line.press('Enter')
await page.waitForTimeout(100)
}
}
const replBack = (page) => page.locator('.repl-line:not(.repl-line--program)').waitFor({ timeout: 60000 })

await check('input(): ▶ asks in the console, no dialog', async () => {
const context = await newContext()
const page = await open(context, greeting)
await pythonReady(page)
await page.getByRole('button', { name: 'Ausführen' }).click()
await answer(page, 'Zoë', 'Name: ')
assert.equal(await page.locator('.input-dialog').count(), 0)
await page.waitForFunction(() => document.querySelector('.console')?.textContent.includes('Hallo Zoë'), null, { timeout: 60000 })
assert.match(await consoleText(page), /Name: Zoë\nHallo Zoë/)
await context.close()
})

await check('input(): a recursive REPL called in the REPL, then Stop → EOFError', async () => {
const context = await newContext()
const page = await open(context, language)
await pythonReady(page)
await page.getByRole('button', { name: 'Ausführen' }).click()
await page.locator('.console').getByText('(Keine Ausgabe)').waitFor({ timeout: 60000 })
await enterRepl(page, 'repl()')
await answer(page, '1 + 2', '> ')
await answer(page, '2 * (3 + 4)', '> ')
await answer(page, 'ende', '> ')
await replBack(page)
assert.match(await consoleText(page), />>> repl\(\)\n(Visualisieren\n)?> 1 \+ 2\n3\n> 2 \* \(3 \+ 4\)\n14\n> ende/)
// Stop while it waits: EOFError, and the session stays (no restart).
await enterRepl(page, 'repl()')
await answer(page, '6 * 7', '> ')
await programLine(page).waitFor({ timeout: 60000 })
await page.getByRole('button', { name: 'Stop' }).click()
await page.locator('.console').getByText('EOFError: EOF when reading a line').waitFor({ timeout: 60000 })
const text = await consoleText(page)
assert.match(text, /> 6 \* 7\n42\n> \n[\s\S]*EOFError: EOF when reading a line/)
assert.doesNotMatch(text, /neu gestartet/)
await enterRepl(page, 'berechnen("2 ** 10")')
await page.locator('.console').getByText('1024').waitFor({ timeout: 60000 })
await context.close()
})

await check('⋮ «Info»: the version from version.json, and the update check', async () => {
const { version } = JSON.parse(readFileSync(new URL('../dist/version.json', import.meta.url), 'utf8'))
const context = await newContext()
Expand Down
Loading