wand-kit 0.2.0 → 0.3.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.
@@ -0,0 +1,108 @@
1
+ /**
2
+ * lib/runtime_check.js — the one-file HTML, driven the way a presenter drives it.
3
+ *
4
+ * The gates in engine.js read the static render, which is all the PDF, the
5
+ * PNGs and the .pptx ever see. A talk or a board also has a runtime, and a
6
+ * runtime can break where no still frame shows it. This opens the exported
7
+ * file in Chromium and uses it: every slide and step by keyboard, the
8
+ * overview, the notes, the presenter view kept in step both ways, every demo
9
+ * mounted; or, for a board, play, scrub and the frames on their cues.
10
+ * Ported from the BofA talk's qa.mjs feature checks.
11
+ */
12
+ import { pathToFileURL } from "node:url";
13
+ import { browser } from "./engine.js";
14
+
15
+ const watch = (page, errors) => {
16
+ page.on("pageerror", (e) => errors.push(String(e).split("\n")[0]));
17
+ page.on("console", (m) => { if (m.type() === "error") errors.push(m.text()); });
18
+ };
19
+
20
+ export async function checkTalk(file, { total }) {
21
+ const b = await browser();
22
+ const ctx = await b.newContext({ viewport: { width: 1440, height: 810 } });
23
+ const page = await ctx.newPage();
24
+ const errors = [], fail = [];
25
+ watch(page, errors);
26
+ try {
27
+ await page.goto(pathToFileURL(file).href, { waitUntil: "load" });
28
+ await page.waitForFunction(() => window.wandTalk, null, { timeout: 8000 });
29
+ const st = (p = page) => p.evaluate(() => ({ cur: wandTalk.cur, shown: wandTalk.shown, total: wandTalk.total, on: document.querySelectorAll(".stage > .page.is-current").length }));
30
+ const s0 = await st();
31
+ if (s0.total !== total) fail.push(`the runtime sees ${s0.total} slides, the build ${total}`);
32
+ if (s0.on !== 1) fail.push(`${s0.on} slides on screen at once`);
33
+
34
+ // every slide and every step, forward, by keyboard
35
+ let presses = 0, steps = 0;
36
+ for (;;) {
37
+ const a = await st(); await page.keyboard.press("ArrowRight"); const z = await st();
38
+ if (z.cur === a.cur && z.shown === a.shown) break;
39
+ presses++; if (z.cur === a.cur) steps++;
40
+ if (presses > 600) { fail.push("ArrowRight never reaches the end"); break; }
41
+ }
42
+ if ((await st()).cur !== total - 1) fail.push("the keyboard does not reach the last slide");
43
+ await page.keyboard.press("Home");
44
+
45
+ await page.keyboard.press("g");
46
+ const thumbs = await page.$$eval(".overview:not([hidden]) .thumb", (t) => t.length);
47
+ if (thumbs !== total) fail.push(`the overview shows ${thumbs} of ${total}`);
48
+ await page.keyboard.press("Escape");
49
+ await page.keyboard.press("n");
50
+ if (!(await page.$eval(".notes-panel", (e) => !e.hidden))) fail.push("N does not open the notes");
51
+ await page.keyboard.press("Escape");
52
+
53
+ // the presenter's window, kept in step from either side
54
+ const [pop] = await Promise.all([ctx.waitForEvent("page", { timeout: 8000 }), page.keyboard.press("p")]);
55
+ watch(pop, errors);
56
+ await pop.waitForLoadState("load");
57
+ await pop.waitForFunction(() => window.wandTalk, null, { timeout: 8000 });
58
+ await page.keyboard.press("ArrowRight"); await page.waitForTimeout(250);
59
+ let r = await st(), q = await st(pop);
60
+ if (r.cur !== q.cur || r.shown !== q.shown) fail.push(`room ${r.cur + 1}/${r.shown} and presenter ${q.cur + 1}/${q.shown} disagree`);
61
+ await pop.keyboard.press("ArrowRight"); await page.waitForTimeout(250);
62
+ r = await st(); q = await st(pop);
63
+ if (r.cur !== q.cur || r.shown !== q.shown) fail.push("the presenter's keys do not move the room");
64
+ await pop.close();
65
+
66
+ // every demo mounts and loads
67
+ const demoSlides = await page.$$eval(".stage > .page", (ps) => ps.map((p, i) => (p.querySelector(".media.demo") ? i : -1)).filter((i) => i >= 0));
68
+ for (const i of demoSlides) {
69
+ await page.evaluate((n) => wandTalk.go(n), i);
70
+ try { await page.waitForSelector(".stage > .page.is-current .media.demo.is-ready", { timeout: 10000 }); }
71
+ catch { fail.push(`the demo on slide ${i + 1} never loaded`); }
72
+ }
73
+ const facts = `${total} slides${steps ? ` and ${steps} steps` : ""} by keyboard, overview, notes, presenter in step both ways${demoSlides.length ? `, ${demoSlides.length} demo${demoSlides.length === 1 ? "" : "s"} mounted` : ""}`;
74
+ const all = [...fail, ...errors.map((e) => `console: ${e}`)];
75
+ return { ok: !all.length, msg: all.length ? all.slice(0, 3).join("; ") : facts };
76
+ } catch (e) {
77
+ return { ok: false, msg: `${String(e.message || e).split("\n")[0]}` };
78
+ } finally { await ctx.close(); }
79
+ }
80
+
81
+ export async function checkBoard(file, { shots, total }) {
82
+ const b = await browser();
83
+ const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } });
84
+ const page = await ctx.newPage();
85
+ const errors = [], fail = [];
86
+ watch(page, errors);
87
+ try {
88
+ await page.goto(pathToFileURL(file).href, { waitUntil: "load" });
89
+ await page.waitForFunction(() => window.wandBoard, null, { timeout: 8000 });
90
+ await page.click('[data-ui="play-board"]');
91
+ await page.waitForTimeout(1200);
92
+ const a = await page.evaluate(() => ({ t: wandBoard.t, playing: wandBoard.playing, on: document.querySelectorAll(".pl-frames .pf.on").length, art: !!document.querySelector(".pl-frames .pf.on .frame-art") }));
93
+ if (!a.playing || a.t < 0.6) fail.push(`the player did not run (t=${a.t.toFixed(1)})`);
94
+ if (a.on !== 1 || !a.art) fail.push("no frame on the player's screen");
95
+ // scrub to the last shot: its frame and its words come up
96
+ const last = shots[shots.length - 1];
97
+ await page.evaluate((t) => { wandBoard.pause(); wandBoard.seek(t); }, last.in + 0.05);
98
+ const z = await page.evaluate(() => ({ id: document.querySelector(".pl-frames .pf.on")?.dataset.id, vo: document.querySelector(".pl-vo").dataset.n }));
99
+ if (z.vo !== String(last.n)) fail.push(`seeking to shot ${last.n} shows shot ${z.vo}`);
100
+ if (!z.id || !z.id.startsWith(String(last.n).padStart(2, "0"))) fail.push(`seeking to shot ${last.n} shows frame ${z.id}`);
101
+ await page.keyboard.press("Escape");
102
+ const all = [...fail, ...errors.map((e) => `console: ${e}`)];
103
+ const frames = shots.reduce((s, x) => s + x.frames.length, 0);
104
+ return { ok: !all.length, msg: all.length ? all.slice(0, 3).join("; ") : `the player runs ${frames} frames over ${Math.floor(total / 60)}:${String(Math.round(total % 60)).padStart(2, "0")}, scrubs to the last shot, captions follow` };
105
+ } catch (e) {
106
+ return { ok: false, msg: `${String(e.message || e).split("\n")[0]}` };
107
+ } finally { await ctx.close(); }
108
+ }