UNPKG

yarn-spinner-runner-ts

Version:

TypeScript parser, compiler, and runtime for Yarn Spinner 3.x with React adapter [NPM package](https://www.npmjs.com/package/yarn-spinner-runner-ts)

243 lines (212 loc) 6.71 kB
import { test } from "node:test"; import { strictEqual, ok } from "node:assert"; import { parseYarn, compile, YarnRunner } from "../index.js"; test("options selection", () => { const script = ` title: Start --- Narrator: Choose one -> A Narrator: Picked A -> B Narrator: Picked B === `; const doc = parseYarn(script); const ir = compile(doc); const runner = new YarnRunner(ir, { startAt: "Start" }); const a = runner.currentResult!; strictEqual(a.type, "text", "Expected intro text"); runner.advance(); const b = runner.currentResult!; strictEqual(b.type, "options", "Expected options after intro"); if (b.type === "options") strictEqual(b.options.length, 2, "Should have 2 options"); // choose B (index 1) runner.advance(1); const c = runner.currentResult!; strictEqual(c.type, "text", "Should be text after selection"); if (c.type === "text") strictEqual(c.text.includes("Picked B"), true, "Expected body of option B"); }); test("option markup is exposed", () => { const script = ` title: Start --- Narrator: Choose -> [b]Bold[/b] Narrator: Bold -> [wave intensity=5]Custom[/wave] Narrator: Custom === `; const doc = parseYarn(script); const ir = compile(doc); const runner = new YarnRunner(ir, { startAt: "Start" }); runner.advance(); // move to options const result = runner.currentResult; ok(result && result.type === "options", "Expected options result"); const options = result!.options; ok(options[0].markup, "Expected markup on first option"); ok(options[1].markup, "Expected markup on second option"); const boldMarkup = options[0].markup!; strictEqual(boldMarkup.text, "Bold"); ok( boldMarkup.segments.some((segment) => segment.wrappers.some((wrapper) => wrapper.name === "b" && wrapper.type === "default") ), "Expected bold wrapper" ); const customWrapper = options[1].markup!.segments .flatMap((segment) => segment.wrappers) .find((wrapper) => wrapper.name === "wave"); ok(customWrapper, "Expected custom wrapper on second option"); strictEqual(customWrapper!.properties.intensity, 5); }); test("option text interpolates variables", () => { const script = ` title: Start --- <<set $cost to 150>> <<set $bribe to 300>> Narrator: Decide -> Pay {$cost} Narrator: Paid -> Haggle {$bribe} Narrator: Haggle === `; const doc = parseYarn(script); const ir = compile(doc); const runner = new YarnRunner(ir, { startAt: "Start" }); // First result is command for set const initial = runner.currentResult; strictEqual(initial?.type, "command", "Expected first <<set>> to emit a command result"); runner.advance(); // second <<set>> command runner.advance(); // move to narration runner.advance(); // move to options const result = runner.currentResult; if (!result || result.type !== "options") { throw new Error("Expected to land on options"); } const [pay, haggle] = result.options; strictEqual(pay.text, "Pay 150", "Should replace placeholder with variable value"); strictEqual(haggle.text, "Haggle 300", "Should evaluate expressions inside placeholders"); }); test("conditional options respect once blocks and if statements", () => { const script = ` title: Start --- <<declare $secret = false>> Narrator: Boot <<once>> <<set $secret = true>> <<endonce>> Narrator: Menu <<if $secret>> -> Secret Option Narrator: Secret taken <<set $secret = false>> <<jump Start>> <<endif>> -> Regular Option Narrator: Regular taken <<jump Start>> === `; const doc = parseYarn(script); const ir = compile(doc); const runner = new YarnRunner(ir, { startAt: "Start" }); const nextOptions = () => { let guard = 25; while (guard-- > 0) { const result = runner.currentResult; if (!result) throw new Error("Expected runtime result"); if (result.type === "options") { return result; } runner.advance(); } throw new Error("Failed to reach options result"); }; const secretMenu = nextOptions(); strictEqual(secretMenu.options.length, 1, "First pass should expose the conditional secret option"); strictEqual(secretMenu.options[0].text, "Secret Option"); // Consume the secret option to flip the flag off runner.advance(0); const fallbackMenu = nextOptions(); strictEqual(fallbackMenu.options.length, 1, "After the secret path is used, only the regular option should remain"); strictEqual(fallbackMenu.options[0].text, "Regular Option"); }); test("options allow space-indented bodies", () => { const script = ` title: Start --- -> Pay <<jump Pay>> -> Run <<jump Run>> === title: Pay --- Narrator: Pay branch === title: Run --- Narrator: Run branch === `; const doc = parseYarn(script); const ir = compile(doc); const runner = new YarnRunner(ir, { startAt: "Start" }); const initial = runner.currentResult; if (initial?.type !== "options") { runner.advance(); } const optionsResult = runner.currentResult; strictEqual(optionsResult?.type, "options", "Expected to reach options"); if (optionsResult?.type !== "options") throw new Error("Options not emitted"); strictEqual(optionsResult.options.length, 2, "Space indents should still group options together"); strictEqual(optionsResult.options[0].text, "Pay"); strictEqual(optionsResult.options[1].text, "Run"); }); test("inline [if] option condition filters options", () => { const script = ` title: StartFalse --- <<declare $flag = false>> -> Hidden [if $flag] Narrator: Hidden -> Visible Narrator: Visible === title: StartTrue --- <<declare $flag = true>> -> Hidden [if $flag] Narrator: Hidden -> Visible Narrator: Visible === `; const doc = parseYarn(script); const ir = compile(doc); const getOptions = (startNode: string) => { const runner = new YarnRunner(ir, { startAt: startNode }); let guard = 25; while (guard-- > 0) { const result = runner.currentResult; if (!result) break; if (result.type === "options") { return { runner, options: result }; } runner.advance(); } throw new Error("Failed to reach options"); }; const { options: optionsFalse } = getOptions("StartFalse"); strictEqual(optionsFalse.options.length, 1, "Hidden option should be filtered out when condition is false"); strictEqual(optionsFalse.options[0].text, "Visible"); const { options: optionsTrue } = getOptions("StartTrue"); strictEqual(optionsTrue.options.length, 2, "Both options should appear when condition is true"); strictEqual(optionsTrue.options[0].text, "Hidden"); strictEqual(optionsTrue.options[1].text, "Visible"); });