diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index ad494b0..0ee656c 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -20,3 +20,6 @@ jobs: with: node-version: 22 - run: node scripts/check-macro.mjs + - run: npm ci + # Chrome is installed on GitHub's runners; playwright-core drives it. + - run: npm run check:ready diff --git a/.gitignore b/.gitignore index e43b0f9..646ac51 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .DS_Store +node_modules/ diff --git a/CLAUDE.md b/CLAUDE.md index b471a62..5aa3f2e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,9 +20,11 @@ The macro renders a button row below the block's LiaScript code editor and an if ```bash node scripts/check-macro.mjs # node --check on every + +` + +// The embed: "ready" with every capability the macro asks about, and a log +// of what arrives. +const stub = `` + +const browser = await chromium.launch({ channel: 'chrome', headless: true }) +const failures = [] +try { + const context = await browser.newContext({ viewport: { width: 1000, height: 4000 } }) + await context.route('**/*', (route) => { + const url = new URL(route.request().url()) + if (url.origin === courseOrigin) return route.fulfill({ contentType: 'text/html', body: page }) + if (url.origin === embedOrigin) return route.fulfill({ contentType: 'text/html', body: stub }) + return route.abort() + }) + const tab = await context.newPage() + tab.on('pageerror', (error) => failures.push(`page error: ${error.message}`)) + await tab.goto(`${courseOrigin}/`) + await tab.waitForFunction((count) => Object.keys(window.readyAt).length === count, blocks.length, { timeout: 10000 }) + const embed = (id) => tab.frameLocator(`#glpython-frame-${id}`).locator('html') + const received = async (id) => (await embed(id).evaluate(() => window.received)).filter((message) => message.type !== 'glpython:reset') + const readyAt = (id) => tab.evaluate((block) => window.readyAt[block], id) + + // Nothing waits for the text but the first program: right after "ready", + // ▶ and «Tests» work, and a program sent meanwhile replaces the first one. + const runButton = tab.locator('#glpython-run-click') + await runButton.waitFor({ state: 'visible' }) + const check = async (label, test) => { + try { + await test() + console.log(`${label}: ok`) + } catch (error) { + failures.push(`${label}: ${error.message}`) + } + } + await check('▶ and «Tests» usable while the first program waits', async () => { + assert.equal(await runButton.isEnabled(), true, '▶ is disabled') + const clickedAt = Date.now() + await runButton.click() + await tab.locator('#glpython-tests-click').click() + await tab.waitForTimeout(300) + const messages = await received('click') + assert.deepEqual(messages.map((message) => message.type), ['glpython:run', 'glpython:test']) + assert.ok(messages[0].at - clickedAt < 250, `the run came ${messages[0].at - clickedAt} ms after the click`) + }) + + // Past the longest wait: every first program has gone out (or not). + await tab.waitForTimeout(textTimeout + 500) + + await check('text after "ready": sent as soon as it arrives', async () => { + const messages = await received('late') + assert.deepEqual(messages.map((message) => [message.type, message.source]), [['glpython:visualize', 'x = 1\nprint(x)']]) + const after = messages[0].at - await readyAt('late') + assert.ok(after >= 300 && after < 800, `sent ${after} ms after "ready"`) + }) + await check('text before "ready": sent right away', async () => { + const messages = await received('early') + assert.deepEqual(messages.map((message) => [message.type, message.source]), [['glpython:visualize', 'y = 2']]) + const after = messages[0].at - await readyAt('early') + assert.ok(after < 250, `sent ${after} ms after "ready"`) + }) + await check('a block empty on purpose: sent after the wait', async () => { + const messages = await received('empty') + assert.deepEqual(messages.map((message) => [message.type, message.source]), [['glpython:visualize', '']]) + const after = messages[0].at - await readyAt('empty') + assert.ok(after >= textTimeout - 50 && after < textTimeout + 400, `sent ${after} ms after "ready"`) + }) + await check('several files, one of them empty: no wait', async () => { + const messages = await received('files') + assert.deepEqual(messages.map((message) => [message.type, message.files?.map((file) => file.source)]), [['glpython:load', ['', 'import datei1\nz = 3']]]) + const after = messages[0].at - await readyAt('files') + assert.ok(after < 250, `sent ${after} ms after "ready"`) + }) + await check('a program sent meanwhile: the first one is dropped', async () => { + const messages = await received('click') + assert.deepEqual(messages.map((message) => message.type), ['glpython:run', 'glpython:test']) + }) +} finally { + await browser.close() +} +if (failures.length > 0) { + console.error(failures.join('\n')) + process.exit(1) +} +console.log('Ready check passed.') diff --git a/scripts/player/course.md b/scripts/player/course.md new file mode 100644 index 0000000..949d8f5 --- /dev/null +++ b/scripts/player/course.md @@ -0,0 +1,39 @@ + + +# glpython: Blöcke in Fragmenten + +Kurs für `npm run check:player`: ein Visualisieren-Block in einem Fragment, das +beim Öffnen der Folie verborgen ist. + +## Davor + +Eine Folie ohne glpython, von der aus die nächste geöffnet wird. + +## Im Fragment + +Erst nach einem Klick erscheint der Block: + + {{1}} +
+ +```python +def doppelt(zahl): + ergebnis = 2 * zahl + return ergebnis + +doppelt(21) +``` +@GLPython.visual + +
+ +## Danach + +Ende.