import assert from "node:assert/strict"; import { fileURLToPath } from "node:url"; import { test } from "node:test"; import { build } from "esbuild"; async function load(name) { const result = await build({ entryPoints: [fileURLToPath(new URL(`../src/player/${name}.ts`, import.meta.url))], bundle: true, write: false, format: "esm", platform: "node", logLevel: "silent" }); return import(`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString("base64")}`); } const { evaluateNani, naniText } = await load("naninovel"); const c = (value) => ["const", value]; const n = (name) => ["name", name]; const bin = (op, a, b) => ["bin", op, a, b]; test("Naninovel uses its own truth, concatenation and short-circuit semantics", () => { const evaluate = (ast) => evaluateNani(ast, (key) => ({ points: 3, flag: "false" })[key]); assert.equal(evaluate(bin("+", c("Score: "), n("points"))), "Score: 3"); assert.equal(evaluate(["if", n("flag"), c("yes"), c("no")]), "no"); assert.equal(evaluate(bin("&&", c(false), ["call", "Missing", []])), false); assert.equal(evaluate(bin("||", c(true), ["call", "Missing", []])), true); assert.throws(() => evaluate(["call", "Missing", []]), /функция/); assert.throws(() => evaluate(bin("/", c(1), c(0))), /ноль/); }); test("Naninovel translates complete templates before substituting expressions", () => { const expressions = { points: n("points") }; assert.equal(naniText("Очки: {points}
Продолжить", expressions, () => 7), "{b}Очки: 7{/b}\nПродолжить"); assert.throws(() => naniText("{unexpected}", expressions, () => 1), /неизвестное выражение/); assert.equal(naniText("{points}", expressions, () => "{b}"), "{{b}"); }); test("Naninovel AST runs through the shared store and survives JSON saves", async () => { const { Python } = await load("python"); const store = { score: 2 }; const py = new Python({ get: (key) => Object.hasOwn(store, key) ? store[key] : undefined, has: (key) => Object.hasOwn(store, key), set: (key, value) => { store[key] = value; }, del: (key) => { delete store[key]; } }); py.exec([{ op: "set", t: { k: "name", id: "score" }, v: { k: "nani", ast: bin("+", n("score"), c(4)) } }]); assert.equal(JSON.parse(JSON.stringify(store)).score, 6); }); test("the player presents translated Naninovel choices and jumps after selection", async () => { const { Engine } = await load("engine"); class Element { children = []; style = {}; classList = { add() {}, remove() {} }; append(child) { this.children.push(child); } set textContent(value) { this.children = value ? [value] : []; } } const previousDocument = globalThis.document; globalThis.document = { createElement: () => new Element() }; try { const engine = Object.create(Engine.prototype); Object.assign(engine, { store: {}, game: { engine: "naninovel", naniExpressions: {} }, tr: { line: (_tid, text) => text === "Again" ? "Ещё раз" : text }, menuEl: new Element(), baseSize: 28, interactionStart() {}, setSkip() {}, advance() {}, jump(target) { this.chosenTarget = target; }, wait(setup) { return new Promise((resolve) => { setup(resolve); this.menuEl.children[0].onclick({ stopPropagation() {} }); }); } }); await engine.exec({ op: "nani_add_choice", what: "Again", tid: "choice-id", target: "nani:Opening", handler: "" }); assert.equal(JSON.parse(JSON.stringify(engine.store)).__nani_choices[0].tid, "choice-id"); await engine.exec({ op: "nani_menu" }); assert.equal(engine.menuEl.children[0].children[0], "Ещё раз"); assert.equal(engine.chosenTarget, "nani:Opening"); assert.deepEqual(engine.store.__nani_choices, []); } finally { globalThis.document = previousDocument; } });