UNPKG

anbani

Version:

Multifunctional javascript toolkit for Georgian Alphabet - Anbani

201 lines (181 loc) 8.24 kB
import assert from "assert"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import * as app from "../src/tui/app.mjs"; import { makeStyle } from "../src/tui/style.mjs"; import { width } from "../src/tui/ui.mjs"; import g from "../src/lib/georgianisation.mjs"; import c from "../src/lib/contractions.mjs"; // KEEP IN SYNC WITH anbani.py/tests/test_tui.py const styleOff = makeStyle({ colors: "none" }); const SIZE = { cols: 80, rows: 24 }; const K = (name) => ({ type: "key", name }); const CH = (ch) => ({ type: "char", char: ch }); const type = (s) => [...s].map(CH); function feed(state, events, ctx) { for (const e of events) state = app.update(state, e, ctx || {}); return state; } function frameOf(state, size) { return app.render(state, size || SIZE, styleOff); } describe("tui app: chrome invariants", function () { it("every frame is exactly rows lines of cols columns", function () { let base = app.initialState(); const states = []; // one state per tab for (const tab of ["converter", "alphabet", "lorem", "nlp", "toolkit"]) { states.push(feed(base, [CH(tab === "converter" ? "1" : tab === "alphabet" ? "2" : tab === "lorem" ? "3" : tab === "nlp" ? "4" : "5")])); } states.push(feed(base, [CH("?")])); // help overlay states.push(base); for (const st of states) { for (const size of [SIZE, { cols: 100, rows: 30 }, { cols: 62, rows: 18 }]) { const lines = frameOf(st, size); assert.strictEqual(lines.length, size.rows); for (const l of lines) assert.strictEqual(width(l), size.cols, `line width != ${size.cols}: ${JSON.stringify(l)}`); } } }); it("min-size guard shows a notice", function () { const lines = frameOf(app.initialState(), { cols: 50, rows: 10 }); assert.ok(lines.join("\n").includes("Terminal too small")); }); it("help toggles and any key closes", function () { let s = feed(app.initialState(), [CH("?")]); assert.strictEqual(s.help, true); s = feed(s, [CH("x")]); assert.strictEqual(s.help, false); }); it("tab wraps with shift+tab", function () { const s = feed(app.initialState(), [K("shift+tab")]); assert.strictEqual(s.tab, "toolkit"); }); it("ctrl+c quits even in insert mode; q types 'q' in insert mode", function () { let s = feed(app.initialState(), [CH("i")]); // focus converter input assert.strictEqual(s.screens.converter.focus, "input"); const typed = feed(s, [CH("q")]); assert.notStrictEqual(typed.quit, true); // q typed, not a quit assert.strictEqual(typed.screens.converter.input.text, "q"); const quit = feed(s, [K("ctrl+c")]); assert.strictEqual(quit.quit, true); }); }); describe("tui converter", function () { it("auto-converts typed qwerty and shows detected script", function () { let s = feed(app.initialState(), [CH("i"), ...type("gamarjoba"), K("escape")]); assert.strictEqual(s.screens.converter.output, "ᲒᲐᲛᲐᲠᲯᲝᲑᲐ"); assert.strictEqual(s.screens.converter.detected, "latin"); assert.ok(frameOf(s).join("\n").includes("ᲒᲐᲛᲐᲠᲯᲝᲑᲐ")); }); it("undetectable input surfaces an error, app survives", function () { let s = feed(app.initialState(), [CH("i"), ...type("!!!")]); assert.ok(s.screens.converter.error); s = feed(s, [K("escape"), CH("2")]); // keep operating assert.strictEqual(s.tab, "alphabet"); }); it("swap on a non-source target toasts and does not change state", function () { // manual mode, then cycle `to` to a non-source target (homoglyph) let s = feed(app.initialState(), [CH("m"), CH("o"), CH("o"), CH("o")]); assert.strictEqual(s.screens.converter.to, "homoglyph"); const before = { from: s.screens.converter.from, to: s.screens.converter.to }; s = feed(s, [CH("s")]); assert.ok(s.toast && /cannot swap/.test(s.toast.text)); assert.deepStrictEqual({ from: s.screens.converter.from, to: s.screens.converter.to }, before); }); it("swap on a valid source target succeeds", function () { // to=nuskhuri (a source) via two `o` cycles from mtavruli let s = feed(app.initialState(), [CH("m"), CH("o"), CH("o")]); assert.strictEqual(s.screens.converter.to, "nuskhuri"); s = feed(s, [CH("s")]); assert.strictEqual(s.screens.converter.from, "nuskhuri"); assert.strictEqual(s.screens.converter.to, "mkhedruli"); }); }); describe("tui alphabet", function () { it("navigation, detail card, archaic toggle, copy", function () { const copied = []; const ctx = { copy: (t) => copied.push(t) }; let s = feed(app.initialState(), [CH("2"), CH("l"), CH("l"), CH("l")], ctx); assert.strictEqual(s.screens.alphabet.cursor, 3); const f = frameOf(s).join("\n"); for (const token of ["დონ", "Ⴃ", "⠙", "4"]) assert.ok(f.includes(token), `detail missing ${token}`); s = feed(s, [CH("a")], ctx); assert.strictEqual(s.screens.alphabet.archaic, true); assert.strictEqual(s.screens.alphabet.cursor, 3); s = feed(s, [K("enter")], ctx); assert.deepStrictEqual(copied, ["დ"]); }); }); describe("tui lorem", function () { it("generates on demand, cycles kind, clamps bounds", function () { const fake = { sentences: (n) => "სიტყვა ".repeat(n).trim() + ".", paragraphs: (w, p) => (("სიტყვა ".repeat(w).trim() + ".\n\n").repeat(p)).trim(), names: (n) => Array.from({ length: n }, () => "სახელი გვარი"), }; const ctx = { lorem: fake }; let s = feed(app.initialState(), [CH("3"), CH("g")], ctx); assert.ok(s.screens.lorem.output.length > 0); s = feed(s, [CH("k")], ctx); assert.strictEqual(s.screens.lorem.kind, "paragraphs"); // clamp words down to 1 s = feed(app.initialState(), [CH("3")], ctx); s = feed(s, Array.from({ length: 30 }, () => CH("-")), ctx); assert.strictEqual(s.screens.lorem.words, 1); }); }); describe("tui nlp", function () { it("lazy-loads once, computes, and never exposes accurate on JS", function () { let loads = 0; const ctx = { requestNlpLoad: () => (loads += 1) }; let s = feed(app.initialState(), [CH("4")], ctx); assert.strictEqual(loads, 1); assert.strictEqual(s.screens.nlp.loading, true); // revisit does not reload s = feed(s, [CH("1"), CH("4")], ctx); assert.strictEqual(loads, 1); // simulate load complete ctx.nlp = { georgianisation: g, contractions: c }; s = app.update(s, { type: "nlp:loaded" }, ctx); assert.strictEqual(s.screens.nlp.loaded, true); s = feed(s, [CH("i"), ...type("gamarjoba")], ctx); assert.ok(/[ა-ჰ]/.test(s.screens.nlp.output)); // gmode cycling stays within fast/balanced s = feed(s, [K("escape")], ctx); for (let i = 0; i < 6; i++) { s = feed(s, [CH("b")], ctx); assert.notStrictEqual(s.screens.nlp.gmode, "accurate"); } }); }); describe("tui toolkit", function () { it("frequency bars sorted, friedman + counts", function () { let s = feed(app.initialState(), [CH("5"), CH("i"), ...type("აააბბგ")]); const st = s.screens.toolkit.stats; assert.strictEqual(st.total, 6); assert.strictEqual(st.unique, 3); assert.strictEqual(st.entries[0][1], 3); assert.strictEqual(st.entries[1][1], 2); assert.strictEqual(st.entries[2][1], 1); assert.ok(/\d\.\d{4}/.test(frameOf(s).join("\n"))); }); }); describe("tui splash", function () { it("black-background wordmark fills the screen", function () { for (const size of [SIZE, { cols: 100, rows: 30 }, { cols: 62, rows: 18 }]) { const lines = app.splashFrame(size, styleOff); assert.strictEqual(lines.length, size.rows); for (const l of lines) assert.strictEqual(width(l), size.cols); } assert.ok(app.splashFrame(SIZE, styleOff).join("\n").includes("loading…")); }); }); describe("tui packaging guard", function () { it("browser entry does not import the TUI", function () { const dir = path.dirname(fileURLToPath(import.meta.url)); const src = fs.readFileSync(path.join(dir, "..", "src", "anbani.mjs"), "utf8"); assert.ok(!src.includes("tui"), "src/anbani.mjs must not reference tui"); }); });