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)
140 lines (128 loc) • 4.73 kB
text/typescript
import { test } from "node:test";
import { strictEqual, ok, match } from "node:assert";
import { parseYarn, compile } from "../index.js";
import { YarnRunner } from "../runtime/runner.js";
test("custom functions", () => {
const yarnText = `
title: CustomFuncs
---
<<declare $doubled = multiply(2, 3)>>
<<declare $concatenated = concat("Hello", " World")>>
<<declare $power = pow(2, 3)>>
<<declare $conditionalValue = ifThen(true, "yes", "no")>>
Result: {$doubled}, {$concatenated}, {$power}, {$conditionalValue}
===
`;
const ast = parseYarn(yarnText);
const program = compile(ast);
const runner = new YarnRunner(program, {
startAt: "CustomFuncs",
functions: {
multiply: (a: unknown, b: unknown) => Number(a) * Number(b),
concat: (a: unknown, b: unknown) => String(a) + String(b),
pow: (base: unknown, exp: unknown) => Math.pow(Number(base), Number(exp)),
ifThen: (cond: unknown, yes: unknown, no: unknown) => Boolean(cond) ? yes : no,
},
});
// Need to advance past declare commands and get to the text line
for (let i = 0; i < 4; i++) {
runner.advance();
}
strictEqual(runner.currentResult?.type, "text");
if (runner.currentResult?.type === "text") {
const fullText = (runner.currentResult.speaker ? `${runner.currentResult.speaker}: ` : "") + runner.currentResult.text;
strictEqual(fullText, "Result: 6, Hello World, 8, yes");
}
});
test("custom functions with type coercion", () => {
const yarnText = `
title: TypeCoercion
---
<<declare $numFromStr = multiply("2", "3")>>
<<declare $concatNums = concat(123, 456)>>
<<declare $boolStr = ifThen("true", 1, 0)>>
Result: {$numFromStr}, {$concatNums}, {$boolStr}
===
`;
const ast = parseYarn(yarnText);
const program = compile(ast);
const runner = new YarnRunner(program, {
startAt: "TypeCoercion",
functions: {
multiply: (a: unknown, b: unknown) => Number(a) * Number(b),
concat: (a: unknown, b: unknown) => String(a) + String(b),
ifThen: (cond: unknown, yes: unknown, no: unknown) => Boolean(cond) ? yes : no,
},
});
// Need to advance past declare commands and get to the text line
for (let i = 0; i < 3; i++) {
runner.advance();
}
strictEqual(runner.currentResult?.type, "text");
if (runner.currentResult?.type === "text") {
const fullText = (runner.currentResult.speaker ? `${runner.currentResult.speaker}: ` : "") + runner.currentResult.text;
strictEqual(fullText, "Result: 6, 123456, 1");
}
});
test("custom functions error handling", () => {
const yarnText = `
title: ErrorHandling
---
<<declare $result = safeDivide(10, 0)>>
Result: {$result}
===
`;
const ast = parseYarn(yarnText);
const program = compile(ast);
const runner = new YarnRunner(program, {
startAt: "ErrorHandling",
functions: {
safeDivide: (a: unknown, b: unknown) => {
const numerator = Number(a);
const denominator = Number(b);
return denominator === 0 ? "Cannot divide by zero" : numerator / denominator;
},
},
});
// Advance until we reach a text result (some commands emit immediately)
for (let i = 0; i < 10 && runner.currentResult?.type !== "text"; i++) {
runner.advance();
}
strictEqual(runner.currentResult?.type, "text");
if (runner.currentResult?.type === "text") {
const fullText = (runner.currentResult.speaker ? `${runner.currentResult.speaker}: ` : "") + runner.currentResult.text;
strictEqual(fullText, "Result: Cannot divide by zero");
}
});
test("custom functions alongside built-in functions", () => {
const yarnText = `
title: MixedFunctions
---
<<declare $random = random()>>
<<declare $doubled = multiply($random, 2)>>
<<declare $formatted = format_number($doubled)>>
Result: {$formatted}
===
`;
const ast = parseYarn(yarnText);
const program = compile(ast);
const runner = new YarnRunner(program, {
startAt: "MixedFunctions",
functions: {
multiply: (a: unknown, b: unknown) => Number(a) * Number(b),
format_number: (n: unknown) => Number(n).toFixed(2),
},
});
// Need to advance past declare commands and get to the text line
for (let i = 0; i < 3; i++) {
runner.advance();
}
strictEqual(runner.currentResult?.type, "text");
if (runner.currentResult?.type === "text") {
const fullText = (runner.currentResult.speaker ? `${runner.currentResult.speaker}: ` : "") + runner.currentResult.text;
const resultNumber = parseFloat(fullText.replace("Result: ", ""));
ok(resultNumber >= 0);
ok(resultNumber <= 2);
match(fullText, /Result: \d+\.\d{2}/);
}
});