morse-code / tests /unit /views.test.js
RemiFabre
feat: Demo mode for filming two robots conversing
a33a583
Raw
History Blame Contribute Delete
5.17 kB
// @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();
});
});