@lernapps/app-templates 0.1.1 → 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/explainer-e2e.mjs +27 -6
- package/dist/quiz-e2e.mjs +48 -24
- 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 +1 -0
- package/interactive/package.json +1 -0
- package/package.json +37 -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 +1 -0
- package/runtime/explainer/e2e.ts +165 -124
- package/runtime/quiz/e2e.ts +172 -135
- 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/runtime/quiz/e2e.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
// The end-to-end tests of
|
|
2
|
-
// app with
|
|
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
|
-
/**
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
await page.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
186
|
-
test.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
page,
|
|
190
|
-
|
|
191
|
-
|
|
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(
|
|
206
|
-
await expect(page.locator("
|
|
207
|
-
await expect(page.locator("
|
|
208
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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,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,23 @@
|
|
|
1
|
+
// The app's logic, a placeholder: a count from 0 to 3, one step for each "weiter" (the button of the component).
|
|
2
|
+
// Pure TypeScript without DOM; the runtime keeps the state and calls start and step
|
|
3
|
+
// (@lernapps/app-templates/interactive). Replace it with the planned core.
|
|
4
|
+
import type { Input } from "@lernapps/app-templates/interactive";
|
|
5
|
+
|
|
6
|
+
export interface State {
|
|
7
|
+
seed: number;
|
|
8
|
+
count: number;
|
|
9
|
+
done: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** How far the count goes. */
|
|
13
|
+
export const GOAL = 3;
|
|
14
|
+
|
|
15
|
+
export function start(seed: number): State {
|
|
16
|
+
return { seed, count: 0, done: false };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function step(state: State, input: Input): State {
|
|
20
|
+
if (state.done || input.kind !== "action" || input.name !== "weiter") return state;
|
|
21
|
+
const count = state.count + 1;
|
|
22
|
+
return { ...state, count, done: count >= GOAL };
|
|
23
|
+
}
|
|
@@ -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
|
+
}
|