test.web/presentation/widgets/surface.widget.test.ts

test.web/presentation/widgets/surface.widget.test.ts is a file in Codebase Testing. 79 lines of code and 0 definitions.

import { PAUSE_LABEL, PLAY_LABEL } from "@banes-lab/web/configuration/strings/surface.strings.ts";
import { afterEach, describe, expect, it, vi } from "vitest";
import { SURFACE_LINE_CLASS } from "@banes-lab/web/configuration/constants/surface.constants.ts";
import { SurfacePlayer } from "@banes-lab/web/presentation/widgets/surface.widget.ts";
import type { SurfaceRecording } from "@banes-lab/web/types/surface.types.ts";

type Intersection = (entries: readonly { readonly isIntersecting: boolean }[]) => void;

class FakeObserver {
    private readonly callback: Intersection;

    public constructor(callback: Intersection) {
        this.callback = callback;
    }

    public disconnect(): void {
        this.callback([]);
    }

    public observe(): void {
        this.callback([{ isIntersecting: true }]);
    }
}

const RECORDING: SurfaceRecording = {
    figure: "venue",
    frames: [
        {
            command: "npm run await -- --post",
            lines: [
                { changed: false, text: "roster" },
                { changed: true, text: "  a position" },
            ],
            output: "POSTED\n",
            surface: "venue.md",
        },
    ],
    inputs: "digest",
    opening: ["roster"],
};

const parts = function parts(): { controls: HTMLElement; figure: HTMLElement; stage: HTMLElement } {
    return {
        controls: document.createElement("div"),
        figure: document.createElement("figure"),
        stage: document.createElement("div"),
    };
};

const venueTexts = function venueTexts(stage: HTMLElement): readonly (string | null)[] {
    return [...(stage.lastElementChild?.querySelectorAll(`.${SURFACE_LINE_CLASS}`) ?? [])].map(
        (line) => line.textContent,
    );
};

afterEach(() => {
    vi.unstubAllGlobals();
});

describe("SurfacePlayer", () => {
    it("builds the command and venue panes, plays while visible, and pauses and resumes on its button", () => {
        const requested = vi.fn(() => 1);
        vi.stubGlobal("IntersectionObserver", FakeObserver);
        vi.stubGlobal("requestAnimationFrame", requested);
        vi.stubGlobal("cancelAnimationFrame", vi.fn());
        const held = parts();
        const dispose = new SurfacePlayer(held, RECORDING).start();
        expect(held.stage.children).toHaveLength(2);
        expect(venueTexts(held.stage)).toStrictEqual(["roster"]);
        expect(requested).toHaveBeenCalledTimes(1);
        const button = held.controls.querySelector("button");
        expect(button?.getAttribute("aria-label")).toBe(PAUSE_LABEL);
        button?.click();
        expect(held.controls.querySelector("button")?.getAttribute("aria-label")).toBe(PLAY_LABEL);
        dispose();
    });

    it("shows the end state and waits for Play when the reader asked for reduced motion", () => {
        const requested = vi.fn(() => 1);
        vi.stubGlobal("IntersectionObserver", FakeObserver);
        vi.stubGlobal("requestAnimationFrame", requested);
        vi.stubGlobal("cancelAnimationFrame", vi.fn());
        vi.stubGlobal("matchMedia", () => ({ matches: true }));
        const held = parts();
        new SurfacePlayer(held, RECORDING).start();
        expect(venueTexts(held.stage)).toStrictEqual(["roster", "  a position"]);
        expect(requested).not.toHaveBeenCalled();
        expect(held.controls.querySelector("button")?.getAttribute("aria-label")).toBe(PLAY_LABEL);
    });
});