| 1 | // Tests for Eleven's guided tour (client-side flow). node test/tutorial.test.mjs |
| 2 | import assert from "node:assert/strict"; |
| 3 | import { startTour } from "../web/static/tutorial.js"; |
| 4 | import { runCommand, commandCatalog, setDefaultBot } from "../web/static/apps.js"; |
| 5 | |
| 6 | let failures = 0; |
| 7 | async function test(name, fn) { |
| 8 | try { |
| 9 | await fn(); |
| 10 | console.log(`PASS ${name}`); |
| 11 | } catch (e) { |
| 12 | failures++; |
| 13 | console.log(`FAIL ${name} β ${e && e.message}`); |
| 14 | } |
| 15 | } |
| 16 | |
| 17 | const tick = () => new Promise((r) => setTimeout(r, 0)); |
| 18 | const lastChip = (spec) => spec.actions[spec.actions.length - 1]; // the advancing chip |
| 19 | const textOf = (steps) => steps.map((s) => s.text).join("\n"); |
| 20 | |
| 21 | // Walk the tour, tapping one chip per step (chooser picks which). Stops at the |
| 22 | // first step with no chips (the ending). Awaits a tick after each tap since an |
| 23 | // advance can be async (e.g. "Show me around" navigates to the Eleven DM first). |
| 24 | async function walk(chooser = lastChip) { |
| 25 | const steps = []; |
| 26 | startTour((spec) => steps.push(spec)); |
| 27 | await tick(); // spaceInfo().finally β first step |
| 28 | for (let guard = 0; guard < 20; guard++) { |
| 29 | const cur = steps[steps.length - 1]; |
| 30 | if (!cur.actions || !cur.actions.length) break; // the ending |
| 31 | const before = steps.length; |
| 32 | chooser(cur).run(); |
| 33 | await tick(); |
| 34 | if (steps.length === before) break; // tapped a non-advancing chip; stop |
| 35 | } |
| 36 | return steps; |
| 37 | } |
| 38 | |
| 39 | await test("every step is attributed to Eleven and the tour ends with a π", async () => { |
| 40 | setDefaultBot(() => null); // no bot β speaks as "Eleven", AI section off |
| 41 | const steps = await walk(); |
| 42 | assert.ok(steps.length >= 5, `expected a multi-step tour, got ${steps.length}`); |
| 43 | for (const s of steps) assert.equal(s.sender.name, "Eleven"); |
| 44 | assert.match(steps[0].text, /I'm Eleven/); // intro introduces itself |
| 45 | assert.match(steps[steps.length - 1].text, /π/); // celebratory ending |
| 46 | }); |
| 47 | |
| 48 | await test("skipIntro jumps past the 'fancy a tour?' greeting straight into the tour", async () => { |
| 49 | setDefaultBot(() => null); // no Eleven to DM β the tour renders in place |
| 50 | const steps = []; |
| 51 | startTour((spec) => steps.push(spec), null, { skipIntro: true }); |
| 52 | await tick(); // spaceInfo().finally β toElevenDm(elevenTour resolves null) |
| 53 | await tick(); // β the first real step |
| 54 | assert.ok(steps.length >= 1, "a step rendered"); |
| 55 | assert.doesNotMatch(steps[0].text, /Fancy a quick tour/); // the greeting is skipped |
| 56 | assert.doesNotMatch(steps[0].text, /your guide to this app/); // ...and its intro line |
| 57 | assert.match(steps[0].text, /You're in /); // it opens on the space step instead |
| 58 | }); |
| 59 | |
| 60 | await test("the default path skips the security deep-dive; picking it shows it", async () => { |
| 61 | setDefaultBot(() => null); |
| 62 | const plain = textOf(await walk()); // taps "No thanks" at the privacy step |
| 63 | assert.doesNotMatch(plain, /never leaves your device/); |
| 64 | |
| 65 | // Now tap the FIRST chip at the privacy step ("Tell me about security"). |
| 66 | const security = textOf( |
| 67 | await walk((spec) => (/technical version/.test(spec.text) ? spec.actions[0] : lastChip(spec))), |
| 68 | ); |
| 69 | assert.match(security, /never leaves your device/); |
| 70 | }); |
| 71 | |
| 72 | await test("the AI section appears only where a conversational helper is present", async () => { |
| 73 | setDefaultBot(() => null); |
| 74 | assert.doesNotMatch(textOf(await walk()), /AI helpers switched on/); |
| 75 | |
| 76 | setDefaultBot(() => ({ name: "Eleven", kind: "ai" })); |
| 77 | const withAi = textOf(await walk()); |
| 78 | assert.match(withAi, /AI helpers switched on/); |
| 79 | assert.match(withAi, /asking me anything/i); // invites a free-form question |
| 80 | setDefaultBot(() => null); |
| 81 | }); |
| 82 | |
| 83 | await test("command examples are wrapped in backticks so they render as code", async () => { |
| 84 | setDefaultBot(() => null); |
| 85 | const all = textOf(await walk()); |
| 86 | assert.match(all, /`\/todo buy milk`/); |
| 87 | assert.match(all, /`\/help`/); |
| 88 | }); |
| 89 | |
| 90 | await test("tapping an advancing chip twice can't double-advance", async () => { |
| 91 | setDefaultBot(() => null); |
| 92 | const steps = []; |
| 93 | startTour((spec) => steps.push(spec)); |
| 94 | await tick(); |
| 95 | const chip = lastChip(steps[0]); // "Show me around" β fires once even if tapped twice |
| 96 | chip.run(); |
| 97 | chip.run(); // stale tap |
| 98 | await tick(); |
| 99 | assert.equal(steps.length, 2, "a second tap on the same chip appended another step"); |
| 100 | }); |
| 101 | |
| 102 | await test("the Todo/Reminder steps invite trying the command and reassure you can come back", async () => { |
| 103 | setDefaultBot(() => null); |
| 104 | const all = textOf(await walk()); |
| 105 | assert.match(all, /\/todo /); // shows how to try it yourself |
| 106 | assert.match(all, /come back to this conversation/i); // reassurance that you won't lose your place |
| 107 | }); |
| 108 | |
| 109 | await test("/tutorial is a client-answered command, listed everywhere in the typeahead", async () => { |
| 110 | const entry = commandCatalog().find((c) => c.name === "/tutorial"); |
| 111 | assert.ok(entry, "/tutorial missing from the command catalog"); |
| 112 | assert.equal(entry.bot, false); // client-answered β shown in every thread |
| 113 | |
| 114 | setDefaultBot(() => null); |
| 115 | const steps = []; |
| 116 | const handled = runCommand("/tutorial", "thread-1", (spec) => steps.push(spec)); |
| 117 | assert.equal(handled, true); // caught, not broadcast |
| 118 | await tick(); |
| 119 | assert.match(steps[0].text, /I'm Eleven/); |
| 120 | }); |
| 121 | |
| 122 | console.log(failures === 0 ? "\nALL PASSED" : `\n${failures} FAILED`); |
| 123 | process.exit(failures === 0 ? 0 : 1); |
| 124 | |