@lernapps/app-templates 0.1.1 → 0.2.1

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.
@@ -1,9 +1,14 @@
1
- // The end-to-end tests of every quiz (@lernapps/app-templates/quiz/e2e), run by `lernapps check --pre-push` on the built
2
- // app with the app's own question bank. An app's e2e/quiz.e2e.ts is
1
+ // The end-to-end tests of the quiz (@lernapps/app-templates/quiz/e2e), run by `lernapps check --pre-push` on the built
2
+ // app with its own question bank, in two parts. quizTests() tests the app's content, and every app runs it: its
3
+ // e2e/quiz.e2e.ts, part of the scaffold, is
3
4
  //
4
5
  // import { quizTests } from "@lernapps/app-templates/quiz/e2e";
5
6
  // quizTests();
6
7
  //
8
+ // Each question's link opens it with its text and what it shows, its images loaded; without JavaScript every question
9
+ // and its solution can be read; each other language has its page, its switch, and what a question shows there.
10
+ // quizRuntimeTests() tests the runtime, the same in every app; only this repo runs it, on the template and its
11
+ // fixtures (quiz/e2e/runtime.e2e.ts, not in the scaffold). What follows is what the two parts check together.
7
12
  // For each question: its deep link opens it, the right answer counts and a wrong one does not; after either, the
8
13
  // background of every option, the explanation and the links for further reading show, the learner's choice marked.
9
14
  // The feedback then has the focus and its top is in view, and the button to the next question comes after it.
@@ -37,8 +42,29 @@ const de = QUIZ_MESSAGES.de;
37
42
  /** The quiz's own texts in `lang`. */
38
43
  const messagesOf = messagesIn;
39
44
 
40
- /** The quiz's tests, for the question bank in `bank` (relative to the app's root). */
45
+ /**
46
+ * The tests of a quiz's content, for the question bank in `bank` (relative to the app's root): what every app runs
47
+ * (e2e/quiz.e2e.ts). Each question's link opens it with what it shows; without JavaScript every question and its
48
+ * solution can be read; each other language has its page. What the runtime does the same in every app is
49
+ * quizRuntimeTests.
50
+ */
41
51
  export function quizTests(bank = "src/quiz.json"): void {
52
+ quizSuite(bank, "content");
53
+ }
54
+
55
+ /**
56
+ * The tests of the quiz's runtime on the question bank in `bank`: the right answer counts and a wrong one does not, with
57
+ * the feedback in view; the seed and the order; a run through every question to the full score; how many questions
58
+ * the learner chooses; what is stored. The template folder and the fixtures of lernapps/app-templates run them
59
+ * (e2e/runtime.e2e.ts); an app does not, its scaffold has no such file: they test the runtime, which is the same in
60
+ * every app, and their time grows with the bank (lernapps/app-templates#125).
61
+ */
62
+ export function quizRuntimeTests(bank = "src/quiz.json"): void {
63
+ quizSuite(bank, "runtime");
64
+ }
65
+
66
+ function quizSuite(bank: string, part: "content" | "runtime"): void {
67
+ const content = part === "content";
42
68
  const all = JSON.parse(readFileSync(bank, "utf8")) as Quiz<Text>;
43
69
  const languages = quizLanguages(all);
44
70
  const quiz = quizIn(all, "de");
@@ -48,94 +74,115 @@ export function quizTests(bank = "src/quiz.json"): void {
48
74
  // answer: with motion, every click would wait for a scroll to settle, the run through a quiz grow with its size.
49
75
  test.use({ reducedMotion: "reduce" });
50
76
 
51
- for (const item of quiz.questions) {
52
- test(`${item.id} (${item.type}): its link opens it and the right answer counts`, async ({ page }) => {
53
- await page.goto(`./#${questionId(item)}`);
54
- await expect(question(page, item)).toBeVisible();
55
- await expect(page.locator(".question:visible")).toHaveCount(1);
56
- await answerQuestion(page, item, rightAnswer(item));
57
- await expect(question(page, item).locator(".question-feedback")).toContainText(de.right);
58
- await expectFeedbackInView(page, item);
59
- // from the feedback, the keyboard reaches the next question (past the links of the background)
60
- const next = question(page, item).locator(".question-next");
61
- for (let tab = 0; tab < 30 && !(await next.evaluate((button) => button === document.activeElement)); tab++) {
62
- await page.keyboard.press("Tab");
63
- }
64
- await expect(next).toBeFocused();
65
- const reveal = question(page, item).locator(".question-reveal");
66
- await expectBackground(reveal, item);
67
- if (item.type === "single-choice" || item.type === "multiple-choice") {
68
- const right = item.options.filter((option) => option.correct).length;
69
- await expect(reveal.locator(".question-badge-right")).toHaveCount(right);
70
- await expect(reveal.locator(".question-badge-right").first()).toHaveText(de.rightOption);
71
- } else {
72
- await expect(reveal).toContainText(de.rightAnswer);
73
- }
77
+ if (content) {
78
+ for (const item of quiz.questions) {
79
+ test(`${item.id} (${item.type}): its link opens it, with what it shows`, async ({ page }) => {
80
+ await page.goto(`./#${questionId(item)}`);
81
+ await expect(question(page, item)).toBeVisible();
82
+ await expect(page.locator(".question:visible")).toHaveCount(1);
83
+ await expect(question(page, item).locator("legend")).toHaveText(item.text);
84
+ if (item.material !== undefined) await expectMaterial(page, item);
85
+ });
86
+ }
87
+
88
+ test.describe("without JavaScript", () => {
89
+ test.use({ javaScriptEnabled: false });
90
+
91
+ test("every question and its solution can be read", async ({ page }) => {
92
+ await page.goto("./");
93
+ for (const item of quiz.questions) {
94
+ await expect(question(page, item).locator("legend")).toHaveText(item.text);
95
+ await expectMaterial(page, item, { loaded: false });
96
+ await expectBackground(page.locator(`#loesung-${item.id}`), item);
97
+ }
98
+ });
74
99
  });
100
+ }
101
+
102
+ if (!content)
103
+ for (const item of quiz.questions) {
104
+ test(`${item.id} (${item.type}): its link opens it and the right answer counts`, async ({ page }) => {
105
+ await page.goto(`./#${questionId(item)}`);
106
+ await expect(question(page, item)).toBeVisible();
107
+ await expect(page.locator(".question:visible")).toHaveCount(1);
108
+ await answerQuestion(page, item, rightAnswer(item));
109
+ await expect(question(page, item).locator(".question-feedback")).toContainText(de.right);
110
+ await expectFeedbackInView(page, item);
111
+ // from the feedback, the keyboard reaches the next question (past the links of the background)
112
+ const next = question(page, item).locator(".question-next");
113
+ for (let tab = 0; tab < 30 && !(await next.evaluate((button) => button === document.activeElement)); tab++) {
114
+ await page.keyboard.press("Tab");
115
+ }
116
+ await expect(next).toBeFocused();
117
+ const reveal = question(page, item).locator(".question-reveal");
118
+ await expectBackground(reveal, item);
119
+ if (item.type === "single-choice" || item.type === "multiple-choice") {
120
+ const right = item.options.filter((option) => option.correct).length;
121
+ await expect(reveal.locator(".question-badge-right")).toHaveCount(right);
122
+ await expect(reveal.locator(".question-badge-right").first()).toHaveText(de.rightOption);
123
+ } else {
124
+ await expect(reveal).toContainText(de.rightAnswer);
125
+ }
126
+ });
75
127
 
76
- if (item.material !== undefined) {
77
- test(`${item.id} (${item.type}): what it shows is there, and its images load`, async ({ page }) => {
128
+ test(`${item.id} (${item.type}): a wrong answer does not count`, async ({ page }) => {
78
129
  await page.goto(`./#${questionId(item)}`);
79
- await expectMaterial(page, item);
130
+ await answerQuestion(page, item, wrongAnswer(item));
131
+ await expect(question(page, item).locator(".question-feedback")).toContainText(de.wrong);
132
+ await expectFeedbackInView(page, item);
133
+ await expectBackground(question(page, item).locator(".question-reveal"), item);
134
+ if (item.type === "single-choice" || item.type === "multiple-choice") {
135
+ await expect(question(page, item).locator(".question-badge-chosen").first()).toHaveText(de.yourChoice);
136
+ }
80
137
  });
81
138
  }
82
139
 
83
- test(`${item.id} (${item.type}): a wrong answer does not count`, async ({ page }) => {
84
- await page.goto(`./#${questionId(item)}`);
85
- await answerQuestion(page, item, wrongAnswer(item));
86
- await expect(question(page, item).locator(".question-feedback")).toContainText(de.wrong);
87
- await expectFeedbackInView(page, item);
88
- await expectBackground(question(page, item).locator(".question-reveal"), item);
89
- if (item.type === "single-choice" || item.type === "multiple-choice") {
90
- await expect(question(page, item).locator(".question-badge-chosen").first()).toHaveText(de.yourChoice);
140
+ if (!content)
141
+ test("the seed in the address gives the order; without one, the address gets one", async ({ page }) => {
142
+ const everything = `&n=${quiz.questions.length}`;
143
+ const order = async (address: string) => {
144
+ await page.goto(address);
145
+ return page.locator(".question").evaluateAll((items) => items.map((item) => item.id).join());
146
+ };
147
+ const first = await order(`./?seed=11${everything}`);
148
+ expect(await order(`./?seed=11${everything}`)).toBe(first);
149
+ if (quiz.questions.length > 2) {
150
+ const others = [];
151
+ for (const seed of [12, 13, 14, 15, 16]) others.push(await order(`./?seed=${seed}${everything}`));
152
+ expect(others.some((other) => other !== first)).toBe(true);
91
153
  }
154
+ await page.goto("./");
155
+ await expect.poll(() => new URL(page.url()).searchParams.get("seed")).toMatch(/^\d+$/);
92
156
  });
93
- }
94
157
 
95
- test("the seed in the address gives the order; without one, the address gets one", async ({ page }) => {
96
- const everything = `&n=${quiz.questions.length}`;
97
- const order = async (address: string) => {
98
- await page.goto(address);
99
- return page.locator(".question").evaluateAll((items) => items.map((item) => item.id).join());
100
- };
101
- const first = await order(`./?seed=11${everything}`);
102
- expect(await order(`./?seed=11${everything}`)).toBe(first);
103
- if (quiz.questions.length > 2) {
104
- const others = [];
105
- for (const seed of [12, 13, 14, 15, 16]) others.push(await order(`./?seed=${seed}${everything}`));
106
- expect(others.some((other) => other !== first)).toBe(true);
107
- }
108
- await page.goto("./");
109
- await expect.poll(() => new URL(page.url()).searchParams.get("seed")).toMatch(/^\d+$/);
110
- });
111
-
112
- test("all right answers give the full score, then the solutions show", async ({ page }) => {
113
- await page.goto("./");
114
- if (quiz.questions.length > 1) await page.locator(".quiz-start button").click();
115
- expect(await page.evaluate(() => Object.keys(localStorage))).toEqual([]);
116
- for (let step = 0; step < quiz.questions.length; step++) {
117
- const id = await page.locator(".question:visible").getAttribute("data-id");
118
- const item = quiz.questions.find((candidate) => candidate.id === id);
119
- if (item === undefined) throw new Error(`no visible question at step ${step + 1}`);
120
- await answerQuestion(page, item, rightAnswer(item));
121
- await question(page, item).locator(".question-next").click();
122
- }
123
- const total = String(quiz.questions.length);
124
- await expect(page.locator(".quiz-score")).toHaveText(
125
- de.score.replace("{score}", total).replace("{total}", total),
126
- );
127
- await expect(page.locator("#loesungen")).toBeVisible();
128
- // only the last result, and only when the bank asks for it, under the app's own prefix
129
- const stored = await page.evaluate(() => Object.keys(localStorage));
130
- if (quiz.rememberLastResult === true) {
131
- expect(stored).toHaveLength(1);
132
- expect(stored[0]).toMatch(/^lernapps:.+:last-result$/);
133
- } else {
134
- expect(stored).toEqual([]);
135
- }
136
- });
158
+ if (!content)
159
+ test("all right answers give the full score, then the solutions show", async ({ page }) => {
160
+ await page.goto("./");
161
+ if (quiz.questions.length > 1) await page.locator(".quiz-start button").click();
162
+ expect(await page.evaluate(() => Object.keys(localStorage))).toEqual([]);
163
+ for (let step = 0; step < quiz.questions.length; step++) {
164
+ const id = await page.locator(".question:visible").getAttribute("data-id");
165
+ const item = quiz.questions.find((candidate) => candidate.id === id);
166
+ if (item === undefined) throw new Error(`no visible question at step ${step + 1}`);
167
+ await answerQuestion(page, item, rightAnswer(item));
168
+ await question(page, item).locator(".question-next").click();
169
+ }
170
+ const total = String(quiz.questions.length);
171
+ await expect(page.locator(".quiz-score")).toHaveText(
172
+ de.score.replace("{score}", total).replace("{total}", total),
173
+ );
174
+ await expect(page.locator("#loesungen")).toBeVisible();
175
+ // only the last result, and only when the bank asks for it, under the app's own prefix
176
+ const stored = await page.evaluate(() => Object.keys(localStorage));
177
+ if (quiz.rememberLastResult === true) {
178
+ expect(stored).toHaveLength(1);
179
+ expect(stored[0]).toMatch(/^lernapps:.+:last-result$/);
180
+ } else {
181
+ expect(stored).toEqual([]);
182
+ }
183
+ });
137
184
 
138
- if (quiz.questions.length > 1) {
185
+ if (!content && quiz.questions.length > 1) {
139
186
  test("the learner chooses how many questions to answer; the address keeps the choice", async ({ page }) => {
140
187
  await page.goto("./?seed=11");
141
188
  await expect(page.locator(".quiz-start")).toBeVisible();
@@ -163,70 +210,60 @@ export function quizTests(bank = "src/quiz.json"): void {
163
210
  });
164
211
  }
165
212
 
166
- test.describe("without JavaScript", () => {
167
- test.use({ javaScriptEnabled: false });
168
-
169
- test("every question and its solution can be read", async ({ page }) => {
170
- await page.goto("./");
171
- for (const item of quiz.questions) {
172
- await expect(question(page, item).locator("legend")).toHaveText(item.text);
173
- await expectMaterial(page, item, { loaded: false });
174
- await expectBackground(page.locator(`#loesung-${item.id}`), item);
175
- }
176
- });
177
- });
178
-
179
213
  for (const lang of languages.slice(1)) {
180
214
  const local = quizIn(all, lang);
181
215
  const words = messagesOf(lang);
182
216
  const address = `./${lang}/`;
183
217
 
184
218
  test.describe(`in ${lang}`, () => {
185
- test.describe("without JavaScript", () => {
186
- test.use({ javaScriptEnabled: false });
187
-
188
- test(`the page is in ${lang}, every question and its solution can be read, the switch leads to every language`, async ({
189
- page,
190
- }) => {
191
- await page.goto(address);
192
- await expect(page.locator("html")).toHaveAttribute("lang", lang);
193
- await expect(page.locator("h1")).toHaveText(local.title);
194
- await expect(page.locator("#loesungen h2")).toHaveText(words.solutions);
195
- for (const item of local.questions) {
196
- await expect(question(page, item).locator("legend")).toHaveText(item.text);
197
- await expectMaterial(page, item, { loaded: false });
198
- await expectBackground(page.locator(`#loesung-${item.id}`), item);
199
- }
200
- for (const other of languages) {
201
- await expect(page.locator(`link[rel="alternate"][hreflang="${other}"]`)).toHaveCount(1);
202
- }
203
- for (const other of languages.filter((candidate) => candidate !== lang)) {
219
+ if (content)
220
+ test.describe("without JavaScript", () => {
221
+ test.use({ javaScriptEnabled: false });
222
+
223
+ test(`the page is in ${lang}, every question and its solution can be read, the switch leads to every language`, async ({
224
+ page,
225
+ }) => {
204
226
  await page.goto(address);
205
- await page.locator(`.page-languages a[hreflang="${other}"]`).click();
206
- await expect(page.locator("html")).toHaveAttribute("lang", other);
207
- await expect(page.locator("h1")).toHaveText(quizIn(all, other).title);
208
- await expect(page.locator('.page-languages a[aria-current="page"]')).toHaveAttribute("hreflang", other);
209
- }
227
+ await expect(page.locator("html")).toHaveAttribute("lang", lang);
228
+ await expect(page.locator("h1")).toHaveText(local.title);
229
+ await expect(page.locator("#loesungen h2")).toHaveText(words.solutions);
230
+ for (const item of local.questions) {
231
+ await expect(question(page, item).locator("legend")).toHaveText(item.text);
232
+ await expectMaterial(page, item, { loaded: false });
233
+ await expectBackground(page.locator(`#loesung-${item.id}`), item);
234
+ }
235
+ for (const other of languages) {
236
+ await expect(page.locator(`link[rel="alternate"][hreflang="${other}"]`)).toHaveCount(1);
237
+ }
238
+ for (const other of languages.filter((candidate) => candidate !== lang)) {
239
+ await page.goto(address);
240
+ await page.locator(`.page-languages a[hreflang="${other}"]`).click();
241
+ await expect(page.locator("html")).toHaveAttribute("lang", other);
242
+ await expect(page.locator("h1")).toHaveText(quizIn(all, other).title);
243
+ await expect(page.locator('.page-languages a[aria-current="page"]')).toHaveAttribute("hreflang", other);
244
+ }
245
+ });
210
246
  });
211
- });
212
247
 
213
- test(`what a question shows, in ${lang}`, async ({ page }) => {
214
- const item = local.questions.find((candidate) => candidate.material !== undefined);
215
- test.skip(item === undefined, "no question shows content");
216
- if (item === undefined) return;
217
- await page.goto(`${address}#${questionId(item)}`);
218
- await expectMaterial(page, item);
219
- });
248
+ if (content)
249
+ test(`what a question shows, in ${lang}`, async ({ page }) => {
250
+ const item = local.questions.find((candidate) => candidate.material !== undefined);
251
+ test.skip(item === undefined, "no question shows content");
252
+ if (item === undefined) return;
253
+ await page.goto(`${address}#${questionId(item)}`);
254
+ await expectMaterial(page, item);
255
+ });
220
256
 
221
- test(`one question, answered in ${lang}`, async ({ page }) => {
222
- const item = local.questions[0];
223
- if (item === undefined) throw new Error("the quiz has no questions");
224
- await page.goto(`${address}#${questionId(item)}`);
225
- await expect(question(page, item)).toBeVisible();
226
- await answerQuestion(page, item, rightAnswer(item), lang);
227
- await expect(question(page, item).locator(".question-feedback")).toContainText(words.right);
228
- await expectBackground(question(page, item).locator(".question-reveal"), item);
229
- });
257
+ if (!content)
258
+ test(`one question, answered in ${lang}`, async ({ page }) => {
259
+ const item = local.questions[0];
260
+ if (item === undefined) throw new Error("the quiz has no questions");
261
+ await page.goto(`${address}#${questionId(item)}`);
262
+ await expect(question(page, item)).toBeVisible();
263
+ await answerQuestion(page, item, rightAnswer(item), lang);
264
+ await expect(question(page, item).locator(".question-feedback")).toContainText(words.right);
265
+ await expectBackground(question(page, item).locator(".question-reveal"), item);
266
+ });
230
267
  });
231
268
  }
232
269
  });
@@ -0,0 +1,5 @@
1
+ # yaml-language-server: $schema=../node_modules/@lernapps/app-templates/runtime/explainer/explainer.v1.schema.json
2
+ title: Platzhalter
3
+ intro: Dieses Kapitel hält den Platz frei, bis der geplante Inhalt da ist.
4
+ languages: [de]
5
+ chapters: [thema]
@@ -0,0 +1,4 @@
1
+ # yaml-language-server: $schema=../../node_modules/@lernapps/app-templates/runtime/explainer/explainer.v1.schema.json
2
+ title: Thema
3
+ summary: Ersetze dieses Kapitel durch das erste geplante.
4
+ pages: [einstieg]
@@ -0,0 +1,28 @@
1
+ # yaml-language-server: $schema=../../node_modules/@lernapps/app-templates/runtime/explainer/explainer.v1.schema.json
2
+ title: Einstieg
3
+ description: Eine Seite, die den Platz frei hält.
4
+ blocks:
5
+ - type: section
6
+ id: worum-es-geht
7
+ title: Worum es geht
8
+ blocks:
9
+ - type: text
10
+ text: Ersetze diese Seite durch die erste geplante.
11
+ - type: section
12
+ id: uebung
13
+ title: Übung
14
+ blocks:
15
+ - type: single-choice
16
+ id: platzhalter
17
+ text: Welche Antwort ist richtig?
18
+ explanation: Diese Übung ist ein Platzhalter. Ersetze sie durch die erste geplante.
19
+ links:
20
+ - title: lernapps.net
21
+ url: https://lernapps.net/
22
+ options:
23
+ - text: Diese
24
+ correct: true
25
+ info: "Richtig: Diese Antwort ist als richtig markiert."
26
+ - text: Diese nicht
27
+ correct: false
28
+ info: "Falsch: Diese Antwort ist als falsch markiert."
@@ -0,0 +1,8 @@
1
+ # yaml-language-server: $schema=../node_modules/@lernapps/app-templates/runtime/interactive/interactive.v1.schema.json
2
+ title: Platzhalter
3
+ description: Ein Knopf, der bis drei zählt. Er hält den Platz frei, bis die geplante App da ist.
4
+ howto: |
5
+ - Drücke „Weiter“, bis du bei drei bist.
6
+ competencies:
7
+ - Ich kann bis drei zählen.
8
+ languages: [de]
@@ -0,0 +1,25 @@
1
+ // The app's logic, a placeholder: a count from 0 to the goal of src/data/count.json, one step for each "weiter" (the
2
+ // button of the component).
3
+ // Pure TypeScript without DOM; the runtime keeps the state and calls start and step
4
+ // (@lernapps/app-templates/interactive). Replace it with the planned core.
5
+ import type { Input } from "@lernapps/app-templates/interactive";
6
+ import count from "../data/count.json" with { type: "json" };
7
+
8
+ export interface State {
9
+ seed: number;
10
+ count: number;
11
+ done: boolean;
12
+ }
13
+
14
+ /** How far the count goes. */
15
+ export const GOAL: number = count.goal;
16
+
17
+ export function start(seed: number): State {
18
+ return { seed, count: 0, done: false };
19
+ }
20
+
21
+ export function step(state: State, input: Input): State {
22
+ if (state.done || input.kind !== "action" || input.name !== "weiter") return state;
23
+ const count = state.count + 1;
24
+ return { ...state, count, done: count >= GOAL };
25
+ }
@@ -0,0 +1 @@
1
+ { "goal": 3 }
@@ -0,0 +1,6 @@
1
+ {
2
+ "app": "Zähler",
3
+ "count": "{count} von {goal}",
4
+ "next": "Weiter",
5
+ "done": "Fertig: bis drei gezählt."
6
+ }
@@ -0,0 +1,12 @@
1
+ # yaml-language-server: $schema=../../node_modules/@lernapps/app-templates/runtime/interactive/playthrough.v1.schema.json
2
+ # Three times "weiter": the count reaches three and the app is done; a fourth press changes nothing.
3
+ seed: 1
4
+ steps:
5
+ - inputs: [{ kind: action, name: weiter }]
6
+ state: { count: 1, done: false }
7
+ announce: 1 von 3
8
+ - inputs: [{ kind: action, name: weiter }, { kind: action, name: weiter }]
9
+ state: { count: 3, done: true }
10
+ announce: "Fertig: bis drei gezählt."
11
+ - inputs: [{ kind: action, name: weiter }]
12
+ state: { count: 3, done: true }
@@ -0,0 +1,27 @@
1
+ <script setup lang="ts">
2
+ // A placeholder: the count and a button that emits the action "weiter". Replace it with the planned component.
3
+ import { watch } from "vue";
4
+ import { announce } from "@lernapps/tooling/a11y";
5
+ import { translator } from "@lernapps/tooling/i18n";
6
+ import type { Input } from "@lernapps/app-templates/interactive";
7
+ import { GOAL, type State } from "../core/index.ts";
8
+ import de from "../messages/de.json";
9
+
10
+ const props = defineProps<{ state: State; lang: string }>();
11
+ const emit = defineEmits<{ input: [input: Input] }>();
12
+ const t = translator(de);
13
+
14
+ watch(
15
+ () => props.state.count,
16
+ (count) => announce(props.state.done ? t("done") : t("count", { count, goal: GOAL })),
17
+ );
18
+ </script>
19
+
20
+ <template>
21
+ <section :aria-label="t('app')">
22
+ <p>{{ state.done ? t("done") : t("count", { count: state.count, goal: GOAL }) }}</p>
23
+ <button v-if="!state.done" type="button" @click="emit('input', { kind: 'action', name: 'weiter' })">
24
+ {{ t("next") }}
25
+ </button>
26
+ </section>
27
+ </template>
@@ -0,0 +1,31 @@
1
+ {
2
+ "$schema": "../node_modules/@lernapps/app-templates/runtime/quiz/quiz.v1.schema.json",
3
+ "title": "Platzhalter",
4
+ "intro": "Diese Frage hält den Platz frei, bis die geplanten Fragen da sind.",
5
+ "questions": [
6
+ {
7
+ "id": "platzhalter",
8
+ "type": "single-choice",
9
+ "text": "Welche Antwort ist richtig?",
10
+ "options": [
11
+ {
12
+ "text": "Diese",
13
+ "correct": true,
14
+ "info": "Richtig: Diese Antwort ist als richtig markiert."
15
+ },
16
+ {
17
+ "text": "Diese nicht",
18
+ "correct": false,
19
+ "info": "Falsch: Diese Antwort ist als falsch markiert."
20
+ }
21
+ ],
22
+ "explanation": "Diese Frage ist ein Platzhalter. Sie zeigt, wie eine Frage aufgebaut ist, und wird durch die erste geplante ersetzt.",
23
+ "links": [
24
+ {
25
+ "title": "lernapps.net",
26
+ "url": "https://lernapps.net/"
27
+ }
28
+ ]
29
+ }
30
+ ]
31
+ }