@lernapps/app-templates 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +2 -2
  2. package/dist/{assessment-By2GGTJC.mjs → assessment-Dy359O1O.mjs} +1 -1
  3. package/dist/{blocks-BmRrD7UY.mjs → blocks-CxtCPAa5.mjs} +2 -2
  4. package/dist/blocks.mjs +2 -2
  5. package/dist/{check-BkEl-m-F.mjs → check-CteIFN96.mjs} +1 -1
  6. package/dist/{content-cYBPYg_J.mjs → content-DO6oEJxj.mjs} +1 -1
  7. package/dist/{e2e-BiMI-pCL.mjs → e2e-CU0n_unV.mjs} +1 -1
  8. package/dist/{engine-BqF2Vffn.mjs → engine-CArxqizW.mjs} +1 -1
  9. package/dist/explainer-e2e.mjs +54 -19
  10. package/dist/explainer-plugin.mjs +6 -5
  11. package/dist/explainer.mjs +4 -4
  12. package/dist/interactive-e2e.mjs +1 -1
  13. package/dist/interactive-plugin.mjs +2 -2
  14. package/dist/{load-C-0hUaSC.mjs → load-RosvaPZf.mjs} +37 -8
  15. package/dist/questions-e2e.mjs +1 -1
  16. package/dist/questions-engine.mjs +2 -2
  17. package/dist/questions.mjs +2 -2
  18. package/dist/{quiz-BKc8JYtC.mjs → quiz-DnW-xtCh.mjs} +1 -1
  19. package/dist/quiz-e2e.mjs +52 -28
  20. package/dist/quiz-plugin.mjs +4 -4
  21. package/dist/quiz.mjs +3 -3
  22. package/dist/{render-BL6fV_NI.mjs → render-D5kwkNt1.mjs} +2 -2
  23. package/dist/{render-DpRO_RKm.mjs → render-DqHOJyHK.mjs} +3 -3
  24. package/dist/{render-BJey0bwl.mjs → render-GS1Htlu8.mjs} +9 -4
  25. package/dist/{result-_eQcamoz.mjs → result-D_mmCVwN.mjs} +2 -2
  26. package/dist/{validate-COaZso7u.mjs → validate-BDwL7Su3.mjs} +6 -2
  27. package/dist/{validate-CxtdeQ5S.mjs → validate-Bxy9NfB8.mjs} +1 -1
  28. package/docs/arc42/09-decisions.arc42.md +24 -0
  29. package/explainer/docs/arc42/05-explainer-building-blocks.arc42.md +7 -3
  30. package/explainer/e2e/runtime.e2e.ts +6 -0
  31. package/explainer/package.json +2 -1
  32. package/interactive/package.json +2 -1
  33. package/package.json +38 -2
  34. package/quiz/docs/arc42/05-quiz-building-blocks.arc42.md +7 -3
  35. package/quiz/e2e/runtime.e2e.ts +5 -0
  36. package/quiz/package.json +2 -1
  37. package/runtime/explainer/assessment-page.ts +2 -0
  38. package/runtime/explainer/e2e.ts +204 -149
  39. package/runtime/explainer/exercises.ts +2 -0
  40. package/runtime/explainer/outline.ts +8 -1
  41. package/runtime/quiz/e2e.ts +172 -135
  42. package/runtime/shared/questions/question.ts +3 -0
  43. package/runtime/shared/questions/question.v1.schema.json +4 -0
  44. package/runtime/shared/questions/render.ts +14 -6
  45. package/runtime/shared/text/markdown.ts +1 -1
  46. package/runtime/shared/text/speech.ts +39 -0
  47. package/scaffold/explainer/src/app.yaml +5 -0
  48. package/scaffold/explainer/src/thema/chapter.yaml +4 -0
  49. package/scaffold/explainer/src/thema/einstieg.yaml +28 -0
  50. package/scaffold/interactive/src/app.yaml +8 -0
  51. package/scaffold/interactive/src/core/index.ts +23 -0
  52. package/scaffold/interactive/src/messages/de.json +6 -0
  53. package/scaffold/interactive/src/playthroughs/zaehlen.yaml +12 -0
  54. package/scaffold/interactive/src/ui/App.vue +27 -0
  55. package/scaffold/quiz/src/quiz.json +31 -0
package/README.md CHANGED
@@ -7,7 +7,7 @@ with which a creator and their AI assistant build a learning app.
7
7
  ## Who it is for
8
8
 
9
9
  - **Creators and their assistants** never open this repo: the generator of
10
- [lernapps/tooling](https://github.com/lernapps/tooling) copies a template folder into a new app. Start at
10
+ [lernapps/tooling](https://github.com/lernapps/tooling) writes a template's scaffold into a new app. Start at
11
11
  [lernapps.net/apps/eintragen/](https://lernapps.net/apps/eintragen/), or, as an assistant, at
12
12
  [lernapps.net/tooling/llms.txt](https://lernapps.net/tooling/llms.txt).
13
13
  - **Maintainers of an archetype** change its template and its runtime here.
@@ -24,7 +24,7 @@ feature: assistants read it before they choose an archetype.
24
24
  | [`explainer/`](explainer/) | an explainer: chapters of pages with sections, formulas, figures, tables, timelines, definitions with a glossary, quotes, code and YouTube videos behind two clicks, and an exercise on every page (the quiz's question types, and `generated` exercises drawn from a seed and property-tested at build), readable without JavaScript, in one or more languages; an optional self-assessment of the app or a chapter, checked as a whole; Vue components for what no block can express | the content as YAML in `src/`: `app.yaml`, a folder per chapter with `chapter.yaml` and its pages, `data/` and `images/`, and `components/` ([schema](runtime/explainer/explainer.v1.schema.json)) |
25
25
  | [`interactive/`](interactive/) | an interactive app whose doing is the content (a game, a trainer): a static start page, readable without JavaScript, and an app page with the app's own Vue components, its routes as `app/#<id>`; the build plays its playthroughs against its core | `src/app.yaml` ([schema](runtime/interactive/interactive.v1.schema.json)), the core `src/core/index.ts` (`start(seed, route)`, `step(state, input)` with a key or an action of its root component, optional `tick(state, dt)`), its components in `src/ui/`, data in `src/data/`, and `src/playthroughs/*.yaml` ([schema](runtime/interactive/playthrough.v1.schema.json)) |
26
26
 
27
- - [`runtime/`](runtime/): the runtime of each archetype, published to npm as `@lernapps/app-templates` (with the template folders, which `lernapps create` of `@lernapps/tooling` copies out of the installed package); its exports
27
+ - [`runtime/`](runtime/): the runtime of each archetype, published to npm as `@lernapps/app-templates` (with the template folders, complete example apps, and the scaffold of each archetype: `lernapps create` of `@lernapps/tooling` copies the paths `lernapps.scaffold.<archetype>` in [`package.json`](package.json) lists and the placeholder in [`scaffold/<archetype>/`](scaffold/), and leaves the example in the installed package; [`scripts/scaffold.ts`](scripts/scaffold.ts) writes the same for the CI job `scaffold`); its exports
28
28
  are in [`package.json`](package.json), what each does in the comments of its source. The quiz's questions also work
29
29
  one by one, for other archetypes that embed them as exercises: `@lernapps/app-templates/quiz/question` (schema,
30
30
  validation, rendering, checker, messages; a question's texts may be maps by language), `startQuestion` of `@lernapps/app-templates/quiz` and the helpers of
@@ -1,5 +1,5 @@
1
1
  import { i as inLanguage, l as textsIn, s as localeOf, t as escape } from "./html-mffkHGHh.mjs";
2
- import { S as shuffled, c as hash, m as parseNumber, u as isCorrect, y as random } from "./render-BJey0bwl.mjs";
2
+ import { S as shuffled, c as hash, m as parseNumber, u as isCorrect, y as random } from "./render-GS1Htlu8.mjs";
3
3
  import { createStorage } from "@lernapps/tooling/storage";
4
4
  // runtime/explainer/fraction.ts
5
5
  const abs = (n) => n < 0n ? -n : n;
@@ -1,7 +1,7 @@
1
1
  import { i as inLanguage, l as textsIn, n as escapeText, s as localeOf, t as escape } from "./html-mffkHGHh.mjs";
2
2
  import { n as markerOf } from "./block-BK_ohsH1.mjs";
3
- import { l as renderFormula, n as isUrl, o as renderInline, s as renderMarkdown } from "./check-BkEl-m-F.mjs";
4
- import "./validate-CxtdeQ5S.mjs";
3
+ import { c as renderMarkdown, n as isUrl, s as renderInline, u as renderFormula } from "./check-CteIFN96.mjs";
4
+ import "./validate-Bxy9NfB8.mjs";
5
5
  import hljs from "highlight.js/lib/core";
6
6
  import bash from "highlight.js/lib/languages/bash";
7
7
  import c from "highlight.js/lib/languages/c";
package/dist/blocks.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  import { i as materialIn, n as markerOf, r as materialImages, t as MATERIAL_TYPES } from "./block-BK_ohsH1.mjs";
2
- import { a as highlight, i as hundredGrid, n as renderMaterial, r as bar, t as renderCredit } from "./blocks-BmRrD7UY.mjs";
3
- import { n as checkMaterial, t as MATERIAL_DEFINITIONS } from "./validate-CxtdeQ5S.mjs";
2
+ import { a as highlight, i as hundredGrid, n as renderMaterial, r as bar, t as renderCredit } from "./blocks-CxtCPAa5.mjs";
3
+ import { n as checkMaterial, t as MATERIAL_DEFINITIONS } from "./validate-Bxy9NfB8.mjs";
4
4
  export { MATERIAL_DEFINITIONS, MATERIAL_TYPES, bar, checkMaterial, highlight, hundredGrid, markerOf, materialImages, materialIn, renderCredit, renderMaterial };
@@ -251,4 +251,4 @@ function checkText(value, kind, languages, declaredIn) {
251
251
  links
252
252
  };
253
253
  }
254
- export { plain as a, renderWithFormulas as c, inspect as i, renderFormula as l, isUrl as n, renderInline as o, formulaProblems as r, renderMarkdown as s, checkText as t };
254
+ export { inspect as a, renderMarkdown as c, formulaProblems as i, renderWithFormulas as l, isUrl as n, plain as o, FORMULA as r, renderInline as s, checkText as t, renderFormula as u };
@@ -1,4 +1,4 @@
1
- import { t as checkText } from "./check-BkEl-m-F.mjs";
1
+ import { t as checkText } from "./check-CteIFN96.mjs";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { LineCounter, isNode, parseDocument } from "yaml";
@@ -1,5 +1,5 @@
1
1
  import { l as textsIn } from "./html-mffkHGHh.mjs";
2
- import { g as questionId, t as QUESTION_MESSAGES } from "./render-BJey0bwl.mjs";
2
+ import { g as questionId, t as QUESTION_MESSAGES } from "./render-GS1Htlu8.mjs";
3
3
  import { expect } from "playwright/test";
4
4
  // runtime/shared/questions/e2e.ts
5
5
  /** The question's own texts in `lang`. */
@@ -1,5 +1,5 @@
1
1
  import { l as textsIn } from "./html-mffkHGHh.mjs";
2
- import { S as shuffled, c as hash, f as missesRight, l as isComplete, t as QUESTION_MESSAGES, u as isCorrect, w as wrongReasons } from "./render-BJey0bwl.mjs";
2
+ import { S as shuffled, c as hash, f as missesRight, l as isComplete, t as QUESTION_MESSAGES, u as isCorrect, w as wrongReasons } from "./render-GS1Htlu8.mjs";
3
3
  import { translator } from "@lernapps/tooling/i18n";
4
4
  import { moveFocus } from "@lernapps/tooling/a11y";
5
5
  // runtime/shared/questions/engine.ts
@@ -1,13 +1,18 @@
1
- import { a as routes, d as templatesOf, f as workedTasks, l as renderExercise, n as loadSite, s as dataOf, u as taskSeeds } from "./load-C-0hUaSC.mjs";
1
+ import { a as routes, d as templatesOf, f as workedTasks, l as renderExercise, n as loadSite, s as dataOf, u as taskSeeds } from "./load-RosvaPZf.mjs";
2
2
  import { i as inLanguage } from "./html-mffkHGHh.mjs";
3
- import { i as inspect, s as renderMarkdown } from "./check-BkEl-m-F.mjs";
4
- import { C as wrongAnswer, _ as questionIn, b as rightAnswer, f as missesRight, g as questionId, i as questionMessagesIn, n as answerText, w as wrongReasons } from "./render-BJey0bwl.mjs";
5
- import { a as giveAnswer, i as expectMaterial, r as expectFeedbackInView, t as answerQuestion } from "./e2e-BiMI-pCL.mjs";
6
- import { A as answerFor, C as watchAddress, D as generate, F as questionFor, G as taskText, I as taskKey, R as fill, U as plainText, X as kindOf, a as assessments, j as diagnose, m as titleOf, v as timelineEvents, w as message, y as embedAddress } from "./assessment-By2GGTJC.mjs";
7
- import { n as answerShown } from "./result-_eQcamoz.mjs";
3
+ import { a as inspect, c as renderMarkdown, l as renderWithFormulas } from "./check-CteIFN96.mjs";
4
+ import { C as wrongAnswer, _ as questionIn, b as rightAnswer, f as missesRight, g as questionId, i as questionMessagesIn, n as answerText, w as wrongReasons } from "./render-GS1Htlu8.mjs";
5
+ import { a as giveAnswer, i as expectMaterial, r as expectFeedbackInView, t as answerQuestion } from "./e2e-CU0n_unV.mjs";
6
+ import { A as answerFor, C as watchAddress, D as generate, F as questionFor, G as taskText, I as taskKey, R as fill, U as plainText, X as kindOf, a as assessments, j as diagnose, m as titleOf, v as timelineEvents, w as message, y as embedAddress } from "./assessment-Dy359O1O.mjs";
7
+ import { n as answerShown } from "./result-D_mmCVwN.mjs";
8
8
  import { n as ASSESSMENT_TASK_EVENT, p as taskNumber, u as nextTaskNumber } from "./tasks-DADUYy52.mjs";
9
9
  import { expect, test } from "playwright/test";
10
10
  // runtime/explainer/outline.ts
11
+ /**
12
+ * A learner text of a question as the page shows it, as text: its formulas $…$ rendered (a formula has no space the
13
+ * tests could rely on in a browser's text, see squash in e2e.ts), the rest as written.
14
+ */
15
+ const shownText = (text) => plainText(renderWithFormulas(text));
11
16
  /** The headings, sections, formulas and figures of `blocks` in `lang`, under a heading of level `level`. */
12
17
  function walk(site, blocks, lang, level, anchor, into) {
13
18
  const text = (value) => inLanguage(value, lang);
@@ -142,7 +147,7 @@ function walk(site, blocks, lang, level, anchor, into) {
142
147
  id: block.id,
143
148
  type: block.type,
144
149
  question,
145
- answers: question.type === "single-choice" || question.type === "multiple-choice" ? question.options.filter((option) => option.correct).map((option) => option.text) : [plainText(answerText(question, lang))]
150
+ answers: question.type === "single-choice" || question.type === "multiple-choice" ? question.options.filter((option) => option.correct).map((option) => shownText(option.text)) : [plainText(answerText(question, lang))]
146
151
  });
147
152
  }
148
153
  }
@@ -279,6 +284,11 @@ const BAND_WORDS = {
279
284
  read: "assessmentRead",
280
285
  unasked: "assessmentUnasked"
281
286
  };
287
+ /**
288
+ * `text` without space. A browser's text of a formula has space between its parts ("2 7" for 2⁷) where the content's
289
+ * has none, so texts with formulas are compared without it (in the order of their characters, which is what counts).
290
+ */
291
+ const squash = (text) => text.replace(/\s+/g, "");
282
292
  /** The task number of the deep link the tests open a generated exercise with. */
283
293
  const SEED = 7;
284
294
  /**
@@ -290,7 +300,7 @@ async function expectExercises(page, item) {
290
300
  const main = page.locator("main");
291
301
  const expected = item.exercises.map((exercise) => exercise.kind === "question" ? {
292
302
  id: exercise.id,
293
- texts: [exercise.question.text],
303
+ texts: [shownText(exercise.question.text)],
294
304
  answers: [exercise.answers]
295
305
  } : exercise.kind === "open" ? {
296
306
  id: exercise.id,
@@ -301,7 +311,7 @@ async function expectExercises(page, item) {
301
311
  texts: exercise.worked.map((worked) => worked.task),
302
312
  answers: exercise.worked.map((worked) => [worked.answer])
303
313
  });
304
- const read = () => main.locator(".explainer-exercise").evaluateAll((elements) => elements.map((element) => {
314
+ const read = async () => (await main.locator(".explainer-exercise").evaluateAll((elements) => elements.map((element) => {
305
315
  const fold = (text) => (text ?? "").replace(/\s+/g, " ").trim();
306
316
  const own = element.classList.contains("explainer-question") ? [...element.querySelectorAll("legend")] : element.classList.contains("explainer-open-task") ? [...element.querySelectorAll(":scope > .explainer-open-task-text")] : [...element.querySelectorAll(".explainer-worked-task > .explainer-task")];
307
317
  const solutions = [...element.querySelectorAll("details.explainer-solution")];
@@ -310,6 +320,10 @@ async function expectExercises(page, item) {
310
320
  texts: own.map((text) => fold(text.textContent)),
311
321
  shown: solutions.map((details) => details.open ? fold(details.innerText) : "")
312
322
  };
323
+ }))).map((item) => ({
324
+ ...item,
325
+ texts: item.texts.map(squash),
326
+ shown: item.shown.map(squash)
313
327
  }));
314
328
  const before = await read();
315
329
  expect(before.map(({ id, texts }) => ({
@@ -317,12 +331,12 @@ async function expectExercises(page, item) {
317
331
  texts
318
332
  })), `${item.url}: each exercise with its text`).toEqual(expected.map(({ id, texts }) => ({
319
333
  id,
320
- texts
334
+ texts: texts.map(squash)
321
335
  })));
322
336
  await main.locator(".explainer-exercise details.explainer-solution > summary").evaluateAll((summaries) => summaries.forEach((summary) => summary.click()));
323
337
  const after = await read();
324
338
  for (const [index, exercise] of expected.entries()) exercise.answers.forEach((answers, at) => {
325
- for (const answer of answers) expect(after[index]?.shown[at] ?? "", `${item.url}: ${exercise.id}, Lösung ${at + 1} shows ${answer}`).toContain(answer);
339
+ for (const answer of answers) expect(after[index]?.shown[at] ?? "", `${item.url}: ${exercise.id}, Lösung ${at + 1} shows ${answer}`).toContain(squash(answer));
326
340
  });
327
341
  }
328
342
  /** Without JavaScript: each component's nojs text is visible, not empty and as in the content; its island is hidden. */
@@ -535,7 +549,7 @@ const feedback = (page, id) => page.locator(`[id="${id}"] .question-feedback`);
535
549
  function reasonFor(question, answer) {
536
550
  const quiz = questionMessagesIn("de");
537
551
  const [first] = wrongReasons(question, answer);
538
- if (first !== void 0) return first.option === void 0 ? first.text : `${first.option}: ${first.text}`;
552
+ if (first !== void 0) return first.option === void 0 ? shownText(first.text) : `${shownText(first.option)}: ${shownText(first.text)}`;
539
553
  return missesRight(question, answer) ? quiz.missingRight : quiz.lookAgain;
540
554
  }
541
555
  /**
@@ -545,7 +559,7 @@ function reasonFor(question, answer) {
545
559
  async function wrongThenSolution(page, exercise, question, reason) {
546
560
  const item = page.locator(`[id="${questionId(question)}"]`);
547
561
  await expect(feedback(page, questionId(question))).toContainText(questionMessagesIn("de").wrong);
548
- await expect(item.locator(".question-diagnosis").first(), `${exercise}: the reason of the wrong answer`).toHaveText(reason);
562
+ await expect.poll(async () => squash(await item.locator(".question-diagnosis").first().textContent() ?? ""), `${exercise}: the reason of the wrong answer`).toBe(squash(reason));
549
563
  await expectFeedbackInView(page, question);
550
564
  await expect(item.locator(".question-reveal"), `${exercise}: no solution after a wrong answer`).toBeHidden();
551
565
  await expect(item.locator("input, select").first(), `${exercise}: another try`).toBeEnabled();
@@ -791,13 +805,32 @@ async function expectResultLine(request, copied, here, first, id, n) {
791
805
  expect(response.status(), `${address} answers`).toBe(200);
792
806
  expect(await response.text(), "the tutor page has the exercise").toContain(id);
793
807
  }
794
- /** The explainer's tests, for the content in `root`/src. */
808
+ /**
809
+ * The tests of an explainer's content, for the content in `root`/src: what every app runs (e2e/explainer.e2e.ts).
810
+ * Without JavaScript every page as its content says, previous and next, the app's own components and the checks of
811
+ * its exercise components. What the runtime does the same in every app is explainerRuntimeTests.
812
+ */
795
813
  function explainerTests(root = ".") {
814
+ explainerSuite(root, "content");
815
+ }
816
+ /**
817
+ * The tests of the explainer's runtime on the content in `root`/src: the exercises of each type, the videos, the tutor
818
+ * ways, the start page, the self-assessments, what is stored. The template folder and the fixtures of
819
+ * lernapps/app-templates run them (e2e/runtime.e2e.ts); an app does not, its scaffold has no such file: they test the
820
+ * runtime, which is the same in every app, not the app's content.
821
+ */
822
+ function explainerRuntimeTests(root = ".") {
823
+ explainerSuite(root, "runtime");
824
+ }
825
+ /** Only at desktop width, in a test of what does not depend on the width; what a page shows is tested at 360 px too. */
826
+ const desktopOnly = (page) => test.skip((page.viewportSize()?.width ?? 0) <= 400, "only at desktop width");
827
+ function explainerSuite(root, part) {
796
828
  const app = outline(root);
829
+ const content = part === "content";
797
830
  test.describe("explainer", () => {
798
831
  test.describe.configure({ mode: "parallel" });
799
832
  test.use({ reducedMotion: "reduce" });
800
- test.describe("without JavaScript", () => {
833
+ if (content) test.describe("without JavaScript", () => {
801
834
  test.use({ javaScriptEnabled: false });
802
835
  for (const lang of app.languages) {
803
836
  for (const item of app.pages[lang] ?? []) test(`${item.url || "./"}: headings, sections, formulas, figures and anchors as in the content`, async ({ page }) => {
@@ -834,6 +867,7 @@ function explainerTests(root = ".") {
834
867
  if (item.kind === "start") await expect(page.locator(".explainer-tutor-about h2")).toHaveText(message(item.lang, "tutorTitle"));
835
868
  });
836
869
  test(`${lang}: previous and next form one chain over all pages`, async ({ page }) => {
870
+ desktopOnly(page);
837
871
  const order = (app.pages[lang] ?? []).map((item) => item.url);
838
872
  const first = order[0];
839
873
  if (first === void 0) throw new Error(`no page in ${lang}`);
@@ -867,7 +901,7 @@ function explainerTests(root = ".") {
867
901
  });
868
902
  test.describe("with JavaScript", () => {
869
903
  test.skip(({ viewport }) => (viewport?.width ?? 0) > 400, "the exercises run at 360 px");
870
- for (const item of app.pages["de"] ?? []) {
904
+ if (!content) for (const item of app.pages["de"] ?? []) {
871
905
  const types = /* @__PURE__ */ new Map();
872
906
  for (const exercise of item.exercises) if (!types.has(exercise.type)) types.set(exercise.type, exercise);
873
907
  if (types.size === 0) continue;
@@ -881,13 +915,13 @@ function explainerTests(root = ".") {
881
915
  expect(kept.length, "an answer per open task").toBe(types.has("open-task") ? 1 : 0);
882
916
  });
883
917
  }
884
- for (const item of app.pages["de"] ?? []) {
918
+ if (!content) for (const item of app.pages["de"] ?? []) {
885
919
  if (item.videos.length === 0) continue;
886
920
  test(`${item.url}: the videos load nothing before a click, the player after it, directly once chosen`, async ({ page, baseURL }) => {
887
921
  await checkVideos(page, item, new URL(baseURL ?? page.url()).origin);
888
922
  });
889
923
  }
890
- for (const item of app.pages["de"] ?? []) {
924
+ if (content) for (const item of app.pages["de"] ?? []) {
891
925
  if (item.components.length === 0) continue;
892
926
  test(`${item.url}: each component hydrates without a console error (${item.components.map((component) => component.name).join(", ")})`, async ({ page }) => {
893
927
  await checkComponents(page, item);
@@ -895,6 +929,7 @@ function explainerTests(root = ".") {
895
929
  });
896
930
  }
897
931
  });
932
+ if (content) return;
898
933
  for (const item of (app.pages["de"] ?? []).filter((candidate) => candidate.exercises.some((exercise) => exercise.kind === "generated"))) test(`${item.url}: the address holds the task of each exercise, and the tutor button gives them to the prompt`, async ({ page }) => {
899
934
  await page.addInitScript(() => {
900
935
  const copied = [];
@@ -1294,4 +1329,4 @@ function explainerTests(root = ".") {
1294
1329
  });
1295
1330
  });
1296
1331
  }
1297
- export { explainerTests };
1332
+ export { explainerRuntimeTests, explainerTests };
@@ -1,12 +1,12 @@
1
- import { _ as propertyProblems, a as routes, c as exerciseData, d as templatesOf, g as exampleProblems, h as describedProblems, i as hiddenChapter, l as renderExercise, m as TABLE_CONTRACT, n as loadSite, o as terms, p as SIZE_BUDGET, r as contentPages, s as dataOf, t as ContentError, u as taskSeeds, v as propsIn } from "./load-C-0hUaSC.mjs";
1
+ import { _ as exampleProblems, a as routes, c as exerciseData, d as templatesOf, g as describedProblems, h as TABLE_CONTRACT, i as hiddenChapter, l as renderExercise, m as SIZE_BUDGET, n as loadSite, o as terms, p as formulaAlternative, r as contentPages, s as dataOf, t as ContentError, u as taskSeeds, v as propertyProblems, y as propsIn } from "./load-RosvaPZf.mjs";
2
2
  import { i as inLanguage, s as localeOf, t as escape } from "./html-mffkHGHh.mjs";
3
3
  import { n as markerOf } from "./block-BK_ohsH1.mjs";
4
- import { n as renderMaterial } from "./blocks-BmRrD7UY.mjs";
5
- import { a as plain, c as renderWithFormulas, o as renderInline, s as renderMarkdown } from "./check-BkEl-m-F.mjs";
6
- import { _ as questionIn, a as renderQuestion, i as questionMessagesIn, n as answerText } from "./render-BJey0bwl.mjs";
4
+ import { n as renderMaterial } from "./blocks-CxtCPAa5.mjs";
5
+ import { c as renderMarkdown, l as renderWithFormulas, o as plain, s as renderInline } from "./check-CteIFN96.mjs";
6
+ import { _ as questionIn, a as renderQuestion, i as questionMessagesIn, n as answerText } from "./render-GS1Htlu8.mjs";
7
7
  import { n as isSizeless, t as imageType } from "./image-CspJVfyQ.mjs";
8
8
  import { a as pageFooter, c as withLanguage, i as notFoundPage, n as fromDepth, o as pageHeader, r as languageRoot, s as relative, t as alternateLinks } from "./pages-9RaaomoO.mjs";
9
- import { B as label, D as generate, F as questionFor, H as placeholdersIn, S as videoLine, U as plainText, V as placeholderValue, X as kindOf, _ as termId, a as assessments, b as renderVideo, h as allBlocks, m as titleOf, o as drawsTasks, p as stepHint, r as assessmentOf, v as timelineEvents, w as message } from "./assessment-By2GGTJC.mjs";
9
+ import { B as label, D as generate, F as questionFor, H as placeholdersIn, S as videoLine, U as plainText, V as placeholderValue, X as kindOf, _ as termId, a as assessments, b as renderVideo, h as allBlocks, m as titleOf, o as drawsTasks, p as stepHint, r as assessmentOf, v as timelineEvents, w as message } from "./assessment-Dy359O1O.mjs";
10
10
  import { i as NEW_TASK_ICON } from "./tasks-DADUYy52.mjs";
11
11
  import { n as NEEDS, o as languageName, t as LANGUAGES } from "./needs-BzEahJ6X.mjs";
12
12
  import { n as vueAlias, r as withServer, t as VUE_OPTIONS } from "./vue-DO1m6q1R.mjs";
@@ -909,6 +909,7 @@ function itemHtml(context, data, n, component = "") {
909
909
  return `${label}${renderQuestion(question, {
910
910
  lang,
911
911
  format: (text) => rendered[text] ?? renderWithFormulas(text),
912
+ describe: (text) => formulaAlternative(text, lang),
912
913
  ...question.material === void 0 ? {} : { materialHtml: renderMaterial(question.material, context.material) }
913
914
  })}`;
914
915
  }
@@ -1,8 +1,8 @@
1
1
  import { t as escape } from "./html-mffkHGHh.mjs";
2
- import { S as shuffled, a as renderQuestion, g as questionId, l as isComplete, u as isCorrect } from "./render-BJey0bwl.mjs";
3
- import { n as startQuestion, t as readAnswer } from "./engine-BqF2Vffn.mjs";
4
- import { D as generate, F as questionFor, G as taskText, N as mistakeOf, W as stepsOf, c as isStoredResult, f as resultQuery, i as assessmentPrompt, j as diagnose, l as linkLines, n as BAND_MESSAGES, u as pageResults, w as message, x as startVideos, z as hintOf } from "./assessment-By2GGTJC.mjs";
5
- import { i as sessionLines, n as answerShown, r as resultLine, t as Progress } from "./result-_eQcamoz.mjs";
2
+ import { S as shuffled, a as renderQuestion, g as questionId, l as isComplete, u as isCorrect } from "./render-GS1Htlu8.mjs";
3
+ import { n as startQuestion, t as readAnswer } from "./engine-CArxqizW.mjs";
4
+ import { D as generate, F as questionFor, G as taskText, N as mistakeOf, W as stepsOf, c as isStoredResult, f as resultQuery, i as assessmentPrompt, j as diagnose, l as linkLines, n as BAND_MESSAGES, u as pageResults, w as message, x as startVideos, z as hintOf } from "./assessment-Dy359O1O.mjs";
5
+ import { i as sessionLines, n as answerShown, r as resultLine, t as Progress } from "./result-D_mmCVwN.mjs";
6
6
  import { c as addressOf, d as numberFromAddress, f as randomTaskNumber, i as NEW_TASK_ICON, n as ASSESSMENT_TASK_EVENT, p as taskNumber, r as ASSESSMENT_UNPACKED_EVENT, s as SHOWN_EVENT, t as ASSESSMENT_ANSWER_EVENT, u as nextTaskNumber } from "./tasks-DADUYy52.mjs";
7
7
  import { a as isNeedsChoice, c as withNeeds, r as NEEDS_KEY, s as withLanguage } from "./needs-BzEahJ6X.mjs";
8
8
  import { moveFocus } from "@lernapps/tooling/a11y";
@@ -1,7 +1,7 @@
1
1
  import { i as inLanguage } from "./html-mffkHGHh.mjs";
2
2
  import { r as languageRoot } from "./pages-9RaaomoO.mjs";
3
3
  import { i as doingText, n as STATE_PROPERTY, o as inputsOf, s as mismatch, t as CLOCK_PROPERTY } from "./contract-PONpxJtd.mjs";
4
- import { i as message, o as loadInteractive, t as APP_PAGE } from "./render-BL6fV_NI.mjs";
4
+ import { i as message, o as loadInteractive, t as APP_PAGE } from "./render-D5kwkNt1.mjs";
5
5
  import { expect, test } from "playwright/test";
6
6
  // runtime/interactive/e2e.ts
7
7
  /** The page's language, its alternates and a switch to each other language, the archetype marker. */
@@ -1,9 +1,9 @@
1
1
  import { c as withLanguage, i as notFoundPage, n as fromDepth } from "./pages-9RaaomoO.mjs";
2
2
  import { c as sourceScope, n as codeProblems } from "./vue-code-BkJjW_I_.mjs";
3
- import { o as pathText, t as Problems } from "./content-cYBPYg_J.mjs";
3
+ import { o as pathText, t as Problems } from "./content-DO6oEJxj.mjs";
4
4
  import { n as vueAlias, r as withServer, t as VUE_OPTIONS } from "./vue-DO1m6q1R.mjs";
5
5
  import { a as inputText, i as doingText, o as inputsOf, s as mismatch } from "./contract-PONpxJtd.mjs";
6
- import { a as InteractiveError, n as pages, o as loadInteractive, r as renderPage } from "./render-BL6fV_NI.mjs";
6
+ import { a as InteractiveError, n as pages, o as loadInteractive, r as renderPage } from "./render-D5kwkNt1.mjs";
7
7
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
  import vue from "@vitejs/plugin-vue";
@@ -1,19 +1,20 @@
1
1
  import { a as isLanguage, c as textLanguages, i as inLanguage, o as languageListProblems, r as LANGUAGES } from "./html-mffkHGHh.mjs";
2
- import { n as renderMaterial } from "./blocks-BmRrD7UY.mjs";
3
- import { a as plain, c as renderWithFormulas, i as inspect, o as renderInline, r as formulaProblems, s as renderMarkdown } from "./check-BkEl-m-F.mjs";
4
- import { n as checkMaterial } from "./validate-CxtdeQ5S.mjs";
5
- import { _ as questionIn, a as renderQuestion, b as rightAnswer, i as questionMessagesIn, n as answerText, o as renderSolution, u as isCorrect, v as questionTexts } from "./render-BJey0bwl.mjs";
6
- import { _ as verdict, g as schemasById, h as knownProperties, i as addressProblem, m as question_v1_schema_default, s as questionDetails, t as QUESTION_BRANCH, u as questionSchemas } from "./validate-COaZso7u.mjs";
2
+ import { n as renderMaterial } from "./blocks-CxtCPAa5.mjs";
3
+ import { a as inspect, c as renderMarkdown, i as formulaProblems, l as renderWithFormulas, o as plain, r as FORMULA, s as renderInline, u as renderFormula } from "./check-CteIFN96.mjs";
4
+ import { n as checkMaterial } from "./validate-Bxy9NfB8.mjs";
5
+ import { _ as questionIn, a as renderQuestion, b as rightAnswer, i as questionMessagesIn, n as answerText, o as renderSolution, u as isCorrect, v as questionTexts } from "./render-GS1Htlu8.mjs";
6
+ import { _ as verdict, g as schemasById, h as knownProperties, i as addressProblem, m as question_v1_schema_default, s as questionDetails, t as QUESTION_BRANCH, u as questionSchemas } from "./validate-BDwL7Su3.mjs";
7
7
  import { t as appName } from "./appname-Bh23cUuz.mjs";
8
8
  import { r as languageRoot } from "./pages-9RaaomoO.mjs";
9
- import { $ as parse, A as answerFor, B as label, D as generate, E as compiled, F as questionFor, G as taskText, I as taskKey, J as tokenize, K as templateProblems, L as answerLabel, M as exampleGiving, O as holds, P as pairValues, Q as evaluate, R as fill, T as StarvedError, W as stepsOf, X as kindOf, Y as unusedVariables, Z as ExpressionError, _ as termId, a as assessments, d as parseRef, et as same, g as isExercise, h as allBlocks, k as values, nt as Fraction, o as drawsTasks, q as templatesIn, s as exerciseOf, t as ASSESSMENT, tt as variables, w as message$1, z as hintOf } from "./assessment-By2GGTJC.mjs";
9
+ import { $ as parse, A as answerFor, B as label, D as generate, E as compiled, F as questionFor, G as taskText, I as taskKey, J as tokenize, K as templateProblems, L as answerLabel, M as exampleGiving, O as holds, P as pairValues, Q as evaluate, R as fill, T as StarvedError, W as stepsOf, X as kindOf, Y as unusedVariables, Z as ExpressionError, _ as termId, a as assessments, d as parseRef, et as same, g as isExercise, h as allBlocks, k as values, nt as Fraction, o as drawsTasks, q as templatesIn, s as exerciseOf, t as ASSESSMENT, tt as variables, w as message$1, z as hintOf } from "./assessment-Dy359O1O.mjs";
10
10
  import { a as RESERVED_PARAMS, l as distinctSeeds, o as SEED_TRIES } from "./tasks-DADUYy52.mjs";
11
11
  import { a as parse$1, i as nodes, o as readSfc, r as line, s as sfc, t as codeChecker } from "./vue-code-BkJjW_I_.mjs";
12
- import { a as lineOf, c as schemaProblems, i as isObject, n as Texts, o as pathText, r as get, s as readYaml, t as Problems } from "./content-cYBPYg_J.mjs";
12
+ import { a as lineOf, c as schemaProblems, i as isObject, n as Texts, o as pathText, r as get, s as readYaml, t as Problems } from "./content-DO6oEJxj.mjs";
13
13
  import "node:module";
14
14
  import { Ajv2020 } from "ajv/dist/2020.js";
15
15
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
16
16
  import { join } from "node:path";
17
+ import untyped from "speech-rule-engine";
17
18
  // \0rolldown/runtime.js
18
19
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod, mod.exports), cb = null), mod.exports);
19
20
  // runtime/explainer/components/props.ts
@@ -14709,6 +14710,33 @@ function readComponents(root) {
14709
14710
  };
14710
14711
  }
14711
14712
  /** A problem with a block's props, at a path below props. */
14713
+ // runtime/shared/text/speech.ts
14714
+ const SRE = untyped;
14715
+ const SPEECH = {
14716
+ domain: "clearspeak",
14717
+ modality: "speech"
14718
+ };
14719
+ for (const locale of LANGUAGES) await SRE.setupEngine({
14720
+ locale,
14721
+ ...SPEECH
14722
+ });
14723
+ /** The formula `tex` as words in `lang` (German for a language the runtime has no texts for), e.g. "2 hoch 7". */
14724
+ function speakFormula(tex, lang) {
14725
+ SRE.setupEngine({
14726
+ locale: isLanguage(lang) ? lang : "de",
14727
+ ...SPEECH
14728
+ });
14729
+ return SRE.toSpeech(renderFormula(tex, false)).replace(/\s+/g, " ").trim();
14730
+ }
14731
+ /**
14732
+ * The words of a text that is only formulas, $…$ (and space); undefined for any other text, which has words of its own
14733
+ * for a name to be made of.
14734
+ */
14735
+ function formulaAlternative(text, lang) {
14736
+ const formulas = [...text.matchAll(FORMULA)];
14737
+ if (formulas.length === 0 || text.replace(FORMULA, "").trim() !== "") return void 0;
14738
+ return formulas.map((formula) => speakFormula(formula[1] ?? "", lang)).join(" ");
14739
+ }
14712
14740
  // runtime/explainer/exercises.ts
14713
14741
  /** The templates of a generated exercise in each of `languages`; links inside them through `resolve`. */
14714
14742
  function templatesOf(spec, languages, resolve = (href) => href) {
@@ -14795,6 +14823,7 @@ function questionExercise(block, lang, material) {
14795
14823
  renderQuestion(question, {
14796
14824
  lang,
14797
14825
  format: renderWithFormulas,
14826
+ describe: (text) => formulaAlternative(text, lang),
14798
14827
  ...question.material === void 0 ? {} : { materialHtml: renderMaterial(question.material, material) }
14799
14828
  }),
14800
14829
  `<details class="explainer-solution"><summary>${plain(questionMessagesIn(lang).solution)}</summary>`,
@@ -16944,4 +16973,4 @@ function loadSite(root, seeds = 200) {
16944
16973
  if (site === void 0) throw new ContentError(problems);
16945
16974
  return site;
16946
16975
  }
16947
- export { propertyProblems as _, routes as a, exerciseData as c, templatesOf as d, workedTasks as f, exampleProblems$1 as g, describedProblems as h, hiddenChapter as i, renderExercise as l, TABLE_CONTRACT as m, loadSite as n, terms as o, SIZE_BUDGET as p, contentPages as r, dataOf as s, ContentError as t, taskSeeds as u, propsIn as v };
16976
+ export { exampleProblems$1 as _, routes as a, exerciseData as c, templatesOf as d, workedTasks as f, describedProblems as g, TABLE_CONTRACT as h, hiddenChapter as i, renderExercise as l, SIZE_BUDGET as m, loadSite as n, terms as o, formulaAlternative as p, contentPages as r, dataOf as s, ContentError as t, taskSeeds as u, propertyProblems as v, propsIn as y };
@@ -1 +1 @@
1
- export { t as answerQuestion, n as expectBackground, r as expectFeedbackInView, i as expectMaterial, a as giveAnswer, o as question } from "./e2e-BiMI-pCL.mjs";
1
+ export { t as answerQuestion, n as expectBackground, r as expectFeedbackInView, i as expectMaterial, a as giveAnswer, o as question } from "./e2e-CU0n_unV.mjs";
@@ -1,3 +1,3 @@
1
- import { g as questionId } from "./render-BJey0bwl.mjs";
2
- import { n as startQuestion, t as readAnswer } from "./engine-BqF2Vffn.mjs";
1
+ import { g as questionId } from "./render-GS1Htlu8.mjs";
2
+ import { n as startQuestion, t as readAnswer } from "./engine-CArxqizW.mjs";
3
3
  export { questionId, readAnswer, startQuestion };
@@ -1,5 +1,5 @@
1
- import { C as wrongAnswer, S as shuffled, T as de_default, _ as questionIn, a as renderQuestion, b as rightAnswer, c as hash, d as materialPairs, f as missesRight, g as questionId, h as questionAddresses, i as questionMessagesIn, l as isComplete, m as parseNumber, n as answerText, o as renderSolution, p as mixed, s as QUESTION_TYPES, t as QUESTION_MESSAGES, u as isCorrect, v as questionTexts, w as wrongReasons, x as rightEntries, y as random } from "./render-BJey0bwl.mjs";
2
- import { c as questionSchema, l as questionSchemaProblems, n as QUESTION_REF, p as validateQuestion, r as QUESTION_SCHEMA_ID, s as questionDetails, u as questionSchemas } from "./validate-COaZso7u.mjs";
1
+ import { C as wrongAnswer, S as shuffled, T as de_default, _ as questionIn, a as renderQuestion, b as rightAnswer, c as hash, d as materialPairs, f as missesRight, g as questionId, h as questionAddresses, i as questionMessagesIn, l as isComplete, m as parseNumber, n as answerText, o as renderSolution, p as mixed, s as QUESTION_TYPES, t as QUESTION_MESSAGES, u as isCorrect, v as questionTexts, w as wrongReasons, x as rightEntries, y as random } from "./render-GS1Htlu8.mjs";
2
+ import { c as questionSchema, l as questionSchemaProblems, n as QUESTION_REF, p as validateQuestion, r as QUESTION_SCHEMA_ID, s as questionDetails, u as questionSchemas } from "./validate-BDwL7Su3.mjs";
3
3
  // runtime/shared/questions/index.ts
4
4
  /** The texts of a question for learners in German, by their key (messages/de.json); questionMessagesIn(lang) for another. */
5
5
  const messages = de_default;
@@ -1,5 +1,5 @@
1
1
  import { i as inLanguage } from "./html-mffkHGHh.mjs";
2
- import { _ as questionIn } from "./render-BJey0bwl.mjs";
2
+ import { _ as questionIn } from "./render-GS1Htlu8.mjs";
3
3
  // runtime/quiz/messages/de.json
4
4
  var de_default = {
5
5
  howto: "Beantworte die Fragen der Reihe nach. Nach jeder Antwort siehst du, ob sie stimmt, und erfährst mehr über jede Antwort. Am Ende siehst du dein Ergebnis.",
package/dist/quiz-e2e.mjs CHANGED
@@ -1,21 +1,60 @@
1
- import { C as wrongAnswer, b as rightAnswer, g as questionId } from "./render-BJey0bwl.mjs";
2
- import { i as expectMaterial, n as expectBackground, o as question, r as expectFeedbackInView, t as answerQuestion } from "./e2e-BiMI-pCL.mjs";
3
- import { n as quizLanguages, t as quizIn } from "./quiz-BKc8JYtC.mjs";
4
- import { n as messagesIn, t as QUIZ_MESSAGES } from "./render-DpRO_RKm.mjs";
1
+ import { C as wrongAnswer, b as rightAnswer, g as questionId } from "./render-GS1Htlu8.mjs";
2
+ import { i as expectMaterial, n as expectBackground, o as question, r as expectFeedbackInView, t as answerQuestion } from "./e2e-CU0n_unV.mjs";
3
+ import { n as quizLanguages, t as quizIn } from "./quiz-DnW-xtCh.mjs";
4
+ import { n as messagesIn, t as QUIZ_MESSAGES } from "./render-DqHOJyHK.mjs";
5
5
  import { expect, test } from "playwright/test";
6
6
  import { readFileSync } from "node:fs";
7
7
  // runtime/quiz/e2e.ts
8
8
  const de = QUIZ_MESSAGES.de;
9
9
  /** The quiz's own texts in `lang`. */
10
10
  const messagesOf = messagesIn;
11
- /** The quiz's tests, for the question bank in `bank` (relative to the app's root). */
11
+ /**
12
+ * The tests of a quiz's content, for the question bank in `bank` (relative to the app's root): what every app runs
13
+ * (e2e/quiz.e2e.ts). Each question's link opens it with what it shows; without JavaScript every question and its
14
+ * solution can be read; each other language has its page. What the runtime does the same in every app is
15
+ * quizRuntimeTests.
16
+ */
12
17
  function quizTests(bank = "src/quiz.json") {
18
+ quizSuite(bank, "content");
19
+ }
20
+ /**
21
+ * The tests of the quiz's runtime on the question bank in `bank`: the right answer counts and a wrong one does not, with
22
+ * the feedback in view; the seed and the order; a run through every question to the full score; how many questions
23
+ * the learner chooses; what is stored. The template folder and the fixtures of lernapps/app-templates run them
24
+ * (e2e/runtime.e2e.ts); an app does not, its scaffold has no such file: they test the runtime, which is the same in
25
+ * every app, and their time grows with the bank (lernapps/app-templates#125).
26
+ */
27
+ function quizRuntimeTests(bank = "src/quiz.json") {
28
+ quizSuite(bank, "runtime");
29
+ }
30
+ function quizSuite(bank, part) {
31
+ const content = part === "content";
13
32
  const all = JSON.parse(readFileSync(bank, "utf8"));
14
33
  const languages = quizLanguages(all);
15
34
  const quiz = quizIn(all, "de");
16
35
  test.describe("quiz", () => {
17
36
  test.use({ reducedMotion: "reduce" });
18
- for (const item of quiz.questions) {
37
+ if (content) {
38
+ for (const item of quiz.questions) test(`${item.id} (${item.type}): its link opens it, with what it shows`, async ({ page }) => {
39
+ await page.goto(`./#${questionId(item)}`);
40
+ await expect(question(page, item)).toBeVisible();
41
+ await expect(page.locator(".question:visible")).toHaveCount(1);
42
+ await expect(question(page, item).locator("legend")).toHaveText(item.text);
43
+ if (item.material !== void 0) await expectMaterial(page, item);
44
+ });
45
+ test.describe("without JavaScript", () => {
46
+ test.use({ javaScriptEnabled: false });
47
+ test("every question and its solution can be read", async ({ page }) => {
48
+ await page.goto("./");
49
+ for (const item of quiz.questions) {
50
+ await expect(question(page, item).locator("legend")).toHaveText(item.text);
51
+ await expectMaterial(page, item, { loaded: false });
52
+ await expectBackground(page.locator(`#loesung-${item.id}`), item);
53
+ }
54
+ });
55
+ });
56
+ }
57
+ if (!content) for (const item of quiz.questions) {
19
58
  test(`${item.id} (${item.type}): its link opens it and the right answer counts`, async ({ page }) => {
20
59
  await page.goto(`./#${questionId(item)}`);
21
60
  await expect(question(page, item)).toBeVisible();
@@ -34,10 +73,6 @@ function quizTests(bank = "src/quiz.json") {
34
73
  await expect(reveal.locator(".question-badge-right").first()).toHaveText(de.rightOption);
35
74
  } else await expect(reveal).toContainText(de.rightAnswer);
36
75
  });
37
- if (item.material !== void 0) test(`${item.id} (${item.type}): what it shows is there, and its images load`, async ({ page }) => {
38
- await page.goto(`./#${questionId(item)}`);
39
- await expectMaterial(page, item);
40
- });
41
76
  test(`${item.id} (${item.type}): a wrong answer does not count`, async ({ page }) => {
42
77
  await page.goto(`./#${questionId(item)}`);
43
78
  await answerQuestion(page, item, wrongAnswer(item));
@@ -47,7 +82,7 @@ function quizTests(bank = "src/quiz.json") {
47
82
  if (item.type === "single-choice" || item.type === "multiple-choice") await expect(question(page, item).locator(".question-badge-chosen").first()).toHaveText(de.yourChoice);
48
83
  });
49
84
  }
50
- test("the seed in the address gives the order; without one, the address gets one", async ({ page }) => {
85
+ if (!content) test("the seed in the address gives the order; without one, the address gets one", async ({ page }) => {
51
86
  const everything = `&n=${quiz.questions.length}`;
52
87
  const order = async (address) => {
53
88
  await page.goto(address);
@@ -69,7 +104,7 @@ function quizTests(bank = "src/quiz.json") {
69
104
  await page.goto("./");
70
105
  await expect.poll(() => new URL(page.url()).searchParams.get("seed")).toMatch(/^\d+$/);
71
106
  });
72
- test("all right answers give the full score, then the solutions show", async ({ page }) => {
107
+ if (!content) test("all right answers give the full score, then the solutions show", async ({ page }) => {
73
108
  await page.goto("./");
74
109
  if (quiz.questions.length > 1) await page.locator(".quiz-start button").click();
75
110
  expect(await page.evaluate(() => Object.keys(localStorage))).toEqual([]);
@@ -89,7 +124,7 @@ function quizTests(bank = "src/quiz.json") {
89
124
  expect(stored[0]).toMatch(/^lernapps:.+:last-result$/);
90
125
  } else expect(stored).toEqual([]);
91
126
  });
92
- if (quiz.questions.length > 1) test("the learner chooses how many questions to answer; the address keeps the choice", async ({ page }) => {
127
+ if (!content && quiz.questions.length > 1) test("the learner chooses how many questions to answer; the address keeps the choice", async ({ page }) => {
93
128
  await page.goto("./?seed=11");
94
129
  await expect(page.locator(".quiz-start")).toBeVisible();
95
130
  await expect(page.locator(".question:visible")).toHaveCount(0);
@@ -113,23 +148,12 @@ function quizTests(bank = "src/quiz.json") {
113
148
  await expect(page.locator(".question")).toHaveCount(1);
114
149
  await expect(page.locator(".question")).toHaveAttribute("data-id", item.id);
115
150
  });
116
- test.describe("without JavaScript", () => {
117
- test.use({ javaScriptEnabled: false });
118
- test("every question and its solution can be read", async ({ page }) => {
119
- await page.goto("./");
120
- for (const item of quiz.questions) {
121
- await expect(question(page, item).locator("legend")).toHaveText(item.text);
122
- await expectMaterial(page, item, { loaded: false });
123
- await expectBackground(page.locator(`#loesung-${item.id}`), item);
124
- }
125
- });
126
- });
127
151
  for (const lang of languages.slice(1)) {
128
152
  const local = quizIn(all, lang);
129
153
  const words = messagesOf(lang);
130
154
  const address = `./${lang}/`;
131
155
  test.describe(`in ${lang}`, () => {
132
- test.describe("without JavaScript", () => {
156
+ if (content) test.describe("without JavaScript", () => {
133
157
  test.use({ javaScriptEnabled: false });
134
158
  test(`the page is in ${lang}, every question and its solution can be read, the switch leads to every language`, async ({ page }) => {
135
159
  await page.goto(address);
@@ -151,14 +175,14 @@ function quizTests(bank = "src/quiz.json") {
151
175
  }
152
176
  });
153
177
  });
154
- test(`what a question shows, in ${lang}`, async ({ page }) => {
178
+ if (content) test(`what a question shows, in ${lang}`, async ({ page }) => {
155
179
  const item = local.questions.find((candidate) => candidate.material !== void 0);
156
180
  test.skip(item === void 0, "no question shows content");
157
181
  if (item === void 0) return;
158
182
  await page.goto(`${address}#${questionId(item)}`);
159
183
  await expectMaterial(page, item);
160
184
  });
161
- test(`one question, answered in ${lang}`, async ({ page }) => {
185
+ if (!content) test(`one question, answered in ${lang}`, async ({ page }) => {
162
186
  const item = local.questions[0];
163
187
  if (item === void 0) throw new Error("the quiz has no questions");
164
188
  await page.goto(`${address}#${questionId(item)}`);
@@ -171,4 +195,4 @@ function quizTests(bank = "src/quiz.json") {
171
195
  }
172
196
  });
173
197
  }
174
- export { answerQuestion, expectBackground, expectFeedbackInView, expectMaterial, quizTests };
198
+ export { answerQuestion, expectBackground, expectFeedbackInView, expectMaterial, quizRuntimeTests, quizTests };
@@ -1,12 +1,12 @@
1
1
  import { c as textLanguages, o as languageListProblems, r as LANGUAGES, t as escape } from "./html-mffkHGHh.mjs";
2
2
  import { r as materialImages } from "./block-BK_ohsH1.mjs";
3
- import "./blocks-BmRrD7UY.mjs";
4
- import { a as duplicates, d as readable, f as schemaMessages, o as inQuestion, p as validateQuestion, u as questionSchemas } from "./validate-COaZso7u.mjs";
5
- import { n as quizLanguages, t as quizIn } from "./quiz-BKc8JYtC.mjs";
3
+ import "./blocks-CxtCPAa5.mjs";
4
+ import { a as duplicates, d as readable, f as schemaMessages, o as inQuestion, p as validateQuestion, u as questionSchemas } from "./validate-BDwL7Su3.mjs";
5
+ import { n as quizLanguages, t as quizIn } from "./quiz-DnW-xtCh.mjs";
6
6
  import { t as appName } from "./appname-Bh23cUuz.mjs";
7
7
  import { n as isSizeless, t as imageType } from "./image-CspJVfyQ.mjs";
8
8
  import { c as withLanguage, i as notFoundPage, n as fromDepth, r as languageRoot } from "./pages-9RaaomoO.mjs";
9
- import { r as renderQuiz } from "./render-DpRO_RKm.mjs";
9
+ import { r as renderQuiz } from "./render-DqHOJyHK.mjs";
10
10
  import { Ajv2020 } from "ajv/dist/2020.js";
11
11
  import { existsSync, readFileSync } from "node:fs";
12
12
  import { join } from "node:path";