@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.
- package/README.md +2 -2
- package/dist/{assessment-By2GGTJC.mjs → assessment-Dy359O1O.mjs} +1 -1
- package/dist/{blocks-BmRrD7UY.mjs → blocks-CxtCPAa5.mjs} +2 -2
- package/dist/blocks.mjs +2 -2
- package/dist/{check-BkEl-m-F.mjs → check-CteIFN96.mjs} +1 -1
- package/dist/{content-cYBPYg_J.mjs → content-DO6oEJxj.mjs} +1 -1
- package/dist/{e2e-BiMI-pCL.mjs → e2e-CU0n_unV.mjs} +1 -1
- package/dist/{engine-BqF2Vffn.mjs → engine-CArxqizW.mjs} +1 -1
- package/dist/explainer-e2e.mjs +54 -19
- package/dist/explainer-plugin.mjs +6 -5
- package/dist/explainer.mjs +4 -4
- package/dist/interactive-e2e.mjs +1 -1
- package/dist/interactive-plugin.mjs +2 -2
- package/dist/{load-C-0hUaSC.mjs → load-RosvaPZf.mjs} +37 -8
- package/dist/questions-e2e.mjs +1 -1
- package/dist/questions-engine.mjs +2 -2
- package/dist/questions.mjs +2 -2
- package/dist/{quiz-BKc8JYtC.mjs → quiz-DnW-xtCh.mjs} +1 -1
- package/dist/quiz-e2e.mjs +52 -28
- package/dist/quiz-plugin.mjs +4 -4
- package/dist/quiz.mjs +3 -3
- package/dist/{render-BL6fV_NI.mjs → render-D5kwkNt1.mjs} +2 -2
- package/dist/{render-DpRO_RKm.mjs → render-DqHOJyHK.mjs} +3 -3
- package/dist/{render-BJey0bwl.mjs → render-GS1Htlu8.mjs} +9 -4
- package/dist/{result-_eQcamoz.mjs → result-D_mmCVwN.mjs} +2 -2
- package/dist/{validate-COaZso7u.mjs → validate-BDwL7Su3.mjs} +6 -2
- package/dist/{validate-CxtdeQ5S.mjs → validate-Bxy9NfB8.mjs} +1 -1
- package/docs/arc42/09-decisions.arc42.md +24 -0
- package/explainer/docs/arc42/05-explainer-building-blocks.arc42.md +7 -3
- package/explainer/e2e/runtime.e2e.ts +6 -0
- package/explainer/package.json +2 -1
- package/interactive/package.json +2 -1
- package/package.json +38 -2
- package/quiz/docs/arc42/05-quiz-building-blocks.arc42.md +7 -3
- package/quiz/e2e/runtime.e2e.ts +5 -0
- package/quiz/package.json +2 -1
- package/runtime/explainer/assessment-page.ts +2 -0
- package/runtime/explainer/e2e.ts +204 -149
- package/runtime/explainer/exercises.ts +2 -0
- package/runtime/explainer/outline.ts +8 -1
- package/runtime/quiz/e2e.ts +172 -135
- package/runtime/shared/questions/question.ts +3 -0
- package/runtime/shared/questions/question.v1.schema.json +4 -0
- package/runtime/shared/questions/render.ts +14 -6
- package/runtime/shared/text/markdown.ts +1 -1
- package/runtime/shared/text/speech.ts +39 -0
- package/scaffold/explainer/src/app.yaml +5 -0
- package/scaffold/explainer/src/thema/chapter.yaml +4 -0
- package/scaffold/explainer/src/thema/einstieg.yaml +28 -0
- package/scaffold/interactive/src/app.yaml +8 -0
- package/scaffold/interactive/src/core/index.ts +23 -0
- package/scaffold/interactive/src/messages/de.json +6 -0
- package/scaffold/interactive/src/playthroughs/zaehlen.yaml +12 -0
- package/scaffold/interactive/src/ui/App.vue +27 -0
- 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)
|
|
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,
|
|
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-
|
|
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 {
|
|
4
|
-
import "./validate-
|
|
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-
|
|
3
|
-
import { n as checkMaterial, t as MATERIAL_DEFINITIONS } from "./validate-
|
|
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 {
|
|
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,5 +1,5 @@
|
|
|
1
1
|
import { l as textsIn } from "./html-mffkHGHh.mjs";
|
|
2
|
-
import { g as questionId, t as QUESTION_MESSAGES } from "./render-
|
|
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-
|
|
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
|
package/dist/explainer-e2e.mjs
CHANGED
|
@@ -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-
|
|
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 {
|
|
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-
|
|
5
|
-
import { a as giveAnswer, i as expectMaterial, r as expectFeedbackInView, t as answerQuestion } from "./e2e-
|
|
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-
|
|
7
|
-
import { n as answerShown } from "./result-
|
|
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`).
|
|
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
|
-
/**
|
|
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
|
|
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-
|
|
5
|
-
import {
|
|
6
|
-
import { _ as questionIn, a as renderQuestion, i as questionMessagesIn, n as answerText } from "./render-
|
|
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-
|
|
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
|
}
|
package/dist/explainer.mjs
CHANGED
|
@@ -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-
|
|
3
|
-
import { n as startQuestion, t as readAnswer } from "./engine-
|
|
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-
|
|
5
|
-
import { i as sessionLines, n as answerShown, r as resultLine, t as Progress } from "./result-
|
|
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";
|
package/dist/interactive-e2e.mjs
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
3
|
-
import { a as
|
|
4
|
-
import { n as checkMaterial } from "./validate-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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 {
|
|
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 };
|
package/dist/questions-e2e.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as answerQuestion, n as expectBackground, r as expectFeedbackInView, i as expectMaterial, a as giveAnswer, o as question } from "./e2e-
|
|
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-
|
|
2
|
-
import { n as startQuestion, t as readAnswer } from "./engine-
|
|
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 };
|
package/dist/questions.mjs
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
2
|
-
import { i as expectMaterial, n as expectBackground, o as question, r as expectFeedbackInView, t as answerQuestion } from "./e2e-
|
|
3
|
-
import { n as quizLanguages, t as quizIn } from "./quiz-
|
|
4
|
-
import { n as messagesIn, t as QUIZ_MESSAGES } from "./render-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 };
|
package/dist/quiz-plugin.mjs
CHANGED
|
@@ -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-
|
|
4
|
-
import { a as duplicates, d as readable, f as schemaMessages, o as inQuestion, p as validateQuestion, u as questionSchemas } from "./validate-
|
|
5
|
-
import { n as quizLanguages, t as quizIn } from "./quiz-
|
|
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-
|
|
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";
|