// Tests for Eleven's guided tour (client-side flow). node test/tutorial.test.mjs import assert from "node:assert/strict"; import { startTour } from "../web/static/tutorial.js"; import { runCommand, commandCatalog, setDefaultBot } from "../web/static/apps.js"; let failures = 0; async function test(name, fn) { try { await fn(); console.log(`PASS ${name}`); } catch (e) { failures++; console.log(`FAIL ${name} — ${e && e.message}`); } } const tick = () => new Promise((r) => setTimeout(r, 0)); const lastChip = (spec) => spec.actions[spec.actions.length - 1]; // the advancing chip const textOf = (steps) => steps.map((s) => s.text).join("\n"); // Walk the tour, tapping one chip per step (chooser picks which). Stops at the // first step with no chips (the ending). Awaits a tick after each tap since an // advance can be async (e.g. "Show me around" navigates to the Eleven DM first). async function walk(chooser = lastChip) { const steps = []; startTour((spec) => steps.push(spec)); await tick(); // spaceInfo().finally → first step for (let guard = 0; guard < 20; guard++) { const cur = steps[steps.length - 1]; if (!cur.actions || !cur.actions.length) break; // the ending const before = steps.length; chooser(cur).run(); await tick(); if (steps.length === before) break; // tapped a non-advancing chip; stop } return steps; } await test("every step is attributed to Eleven and the tour ends with a 🎉", async () => { setDefaultBot(() => null); // no bot → speaks as "Eleven", AI section off const steps = await walk(); assert.ok(steps.length >= 5, `expected a multi-step tour, got ${steps.length}`); for (const s of steps) assert.equal(s.sender.name, "Eleven"); assert.match(steps[0].text, /I'm Eleven/); // intro introduces itself assert.match(steps[steps.length - 1].text, /🎉/); // celebratory ending }); await test("skipIntro jumps past the 'fancy a tour?' greeting straight into the tour", async () => { setDefaultBot(() => null); // no Eleven to DM → the tour renders in place const steps = []; startTour((spec) => steps.push(spec), null, { skipIntro: true }); await tick(); // spaceInfo().finally → toElevenDm(elevenTour resolves null) await tick(); // → the first real step assert.ok(steps.length >= 1, "a step rendered"); assert.doesNotMatch(steps[0].text, /Fancy a quick tour/); // the greeting is skipped assert.doesNotMatch(steps[0].text, /your guide to this app/); // ...and its intro line assert.match(steps[0].text, /You're in /); // it opens on the space step instead }); await test("the default path skips the security deep-dive; picking it shows it", async () => { setDefaultBot(() => null); const plain = textOf(await walk()); // taps "No thanks" at the privacy step assert.doesNotMatch(plain, /never leaves your device/); // Now tap the FIRST chip at the privacy step ("Tell me about security"). const security = textOf( await walk((spec) => (/technical version/.test(spec.text) ? spec.actions[0] : lastChip(spec))), ); assert.match(security, /never leaves your device/); }); await test("the AI section appears only where a conversational helper is present", async () => { setDefaultBot(() => null); assert.doesNotMatch(textOf(await walk()), /AI helpers switched on/); setDefaultBot(() => ({ name: "Eleven", kind: "ai" })); const withAi = textOf(await walk()); assert.match(withAi, /AI helpers switched on/); assert.match(withAi, /asking me anything/i); // invites a free-form question setDefaultBot(() => null); }); await test("command examples are wrapped in backticks so they render as code", async () => { setDefaultBot(() => null); const all = textOf(await walk()); assert.match(all, /`\/todo buy milk`/); assert.match(all, /`\/help`/); }); await test("tapping an advancing chip twice can't double-advance", async () => { setDefaultBot(() => null); const steps = []; startTour((spec) => steps.push(spec)); await tick(); const chip = lastChip(steps[0]); // "Show me around" — fires once even if tapped twice chip.run(); chip.run(); // stale tap await tick(); assert.equal(steps.length, 2, "a second tap on the same chip appended another step"); }); await test("the Todo/Reminder steps invite trying the command and reassure you can come back", async () => { setDefaultBot(() => null); const all = textOf(await walk()); assert.match(all, /\/todo /); // shows how to try it yourself assert.match(all, /come back to this conversation/i); // reassurance that you won't lose your place }); await test("/tutorial is a client-answered command, listed everywhere in the typeahead", async () => { const entry = commandCatalog().find((c) => c.name === "/tutorial"); assert.ok(entry, "/tutorial missing from the command catalog"); assert.equal(entry.bot, false); // client-answered → shown in every thread setDefaultBot(() => null); const steps = []; const handled = runCommand("/tutorial", "thread-1", (spec) => steps.push(spec)); assert.equal(handled, true); // caught, not broadcast await tick(); assert.match(steps[0].text, /I'm Eleven/); }); console.log(failures === 0 ? "\nALL PASSED" : `\n${failures} FAILED`); process.exit(failures === 0 ? 0 : 1);