Spaces:
Running
Running
| // @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(); | |
| }); | |
| }); | |