// @vitest-environment jsdom import { describe, it, expect, vi } from "vitest"; import { makeTiming, SPEED_PRESETS } from "../../lib/timing.js"; import { createCompose } from "../../views/compose.js"; import { createLearn } from "../../views/learn.js"; import { createSettings } from "../../views/settings.js"; import { createListen } from "../../views/listen.js"; import { createDemo } from "../../views/demo.js"; import { DEFAULT_TAP_PROFILE } from "../../lib/robot-tapper.js"; import { DEFAULT_RULES } from "../../lib/rules.js"; function makeCtx(overrides = {}) { const state = { mode: "app", speed: "normal", unitMs: SPEED_PRESETS.normal, emitter: "device", beepFreq: 2200, detector: { highpassHz: 2000, thresholdFactor: 4.0 }, tap: { ...DEFAULT_TAP_PROFILE }, demo: { rules: DEFAULT_RULES.map((r) => ({ ...r })) }, }; const synth = { play: vi.fn(), stop: vi.fn(), freq: 2200 }; return { reachy: null, state, timing: () => makeTiming(state.unitMs), setSpeed: (p) => { state.speed = p; if (SPEED_PRESETS[p]) state.unitMs = SPEED_PRESETS[p]; }, synth: () => synth, tapper: () => null, makeMic: () => ({ start: vi.fn(), stop: vi.fn(), now: () => 0 }), selfEchoActive: () => false, isTransmitting: () => false, transmit: vi.fn(() => Promise.resolve()), toast: vi.fn(), _synth: synth, ...overrides, }; } describe("Compose view", () => { it("renders Morse chips for the default message", () => { const ctx = makeCtx(); const { node } = createCompose(ctx); const input = node.querySelector(".compose-input"); expect(input.value).toBe("SOS"); // SOS = 3 + 3 + 3 = 9 symbol chips expect(node.querySelectorAll(".morse-out .pattern").length).toBe(3); expect(node.querySelectorAll(".morse-out .dot, .morse-out .dash").length).toBe(9); }); it("updates preview on input and warns on non-encodable chars", () => { const ctx = makeCtx(); const { node } = createCompose(ctx); const input = node.querySelector(".compose-input"); input.value = "Aé"; input.dispatchEvent(new window.Event("input")); expect(node.querySelector(".warn").hidden).toBe(false); // 'A' -> .- = 2 chips expect(node.querySelectorAll(".morse-out .dot, .morse-out .dash").length).toBe(2); }); it("plays via synth when emitter is device", () => { const ctx = makeCtx(); const { node } = createCompose(ctx); node.querySelector(".btn.primary").click(); expect(ctx._synth.play).toHaveBeenCalled(); }); }); describe("Learn view", () => { it("renders a tile per letter and digit (no punctuation), and plays on tap", () => { const ctx = makeCtx(); const { node } = createLearn(ctx); const cells = node.querySelectorAll(".chart-cell"); expect(cells.length).toBe(26 + 10); cells[0].click(); // 'A' expect(ctx._synth.play).toHaveBeenCalled(); }); }); describe("Settings view", () => { it("renders calibration sliders and mutates config", () => { const ctx = makeCtx(); const { node } = createSettings(ctx); const ranges = node.querySelectorAll("input.range"); expect(ranges.length).toBeGreaterThanOrEqual(8); // first range is the speed unit ranges[0].value = "200"; ranges[0].dispatchEvent(new window.Event("input")); expect(ctx.state.unitMs).toBe(200); }); }); describe("Listen view", () => { it("builds without a mic and shows placeholder decode", () => { const ctx = makeCtx(); const { node } = createListen(ctx); expect(node.querySelector(".scope")).toBeTruthy(); expect(node.querySelector(".decoded-text").textContent).toBe("…"); expect(node.querySelector(".btn.primary").textContent).toMatch(/Start listening/); }); }); describe("Demo view", () => { it("renders the big live readout and embeds the alphabet chart", () => { const ctx = makeCtx(); const { node } = createDemo(ctx); expect(node.querySelector(".demo-decoded")).toBeTruthy(); expect(node.querySelector(".scope-big")).toBeTruthy(); // alphabet chart embedded below expect(node.querySelectorAll(".chart-cell").length).toBe(26 + 10); }); }); describe("Settings — demo mode", () => { it("shows editable conversation rules with the defaults", () => { const ctx = makeCtx({ state: { ...makeCtx().state, mode: "demo" } }); const { node } = createSettings(ctx); const rows = node.querySelectorAll(".rule-row"); expect(rows.length).toBe(DEFAULT_RULES.length); // first rule input reflects the default "U OK?" expect(node.querySelector(".rule-input").value).toBe(DEFAULT_RULES[0].input); }); it("omits the rules editor in app mode", () => { const ctx = makeCtx(); const { node } = createSettings(ctx); expect(node.querySelector(".rule-row")).toBeNull(); }); });