// E16 S3: the draft a session suggests, and the markdown an issue carries. Pure, so the composer's // preview and what it saves are provably one computation. import { describe, expect, it } from "vitest"; import type { ServerLogRow } from "./api"; import { draftSteps, osName, renderIssue, type DraftForm, type IssueText } from "./issueDraft"; let nextId = 1; const row = (over: Partial): ServerLogRow => ({ id: nextId++, ts: "2026-09-19T14:05:31.000Z", level: "INFO", source: "web", logger: "web", event: "x.y", msg: "", attrs: null, exc: null, request_id: null, user_id: 1, job_id: null, debug_session_id: "0123456789abcdef", version: null, ...over, }); const nav = (page: string) => row({ event: "nav.view", attrs: { via: "push", page } }); const step = (label?: string) => row({ event: "debug.step", attrs: label ? { n: 1, label } : { n: 1 } }); const pageName = (p: string) => p.toUpperCase(); const clock = () => "14:05:31"; describe("the steps a session suggests", () => { it("are its markers and the page changes between them, in order", () => { const steps = draftSteps( [nav("plex"), nav("plex"), step("opened the player"), nav("settings")], pageName, clock, ); expect(steps.map((s) => [s.kind, s.text])).toEqual([ ["nav", "PLEX"], // the same page twice in a row is one step ["marker", "opened the player"], ["nav", "SETTINGS"], ]); }); it("name a marker set without a label by its time, so the issue keeps it", () => { expect(draftSteps([step()], pageName, clock)[0]?.text).toBe("14:05:31"); }); it("hang a screenshot on the step before it, and leave server rows out", () => { const steps = draftSteps( [ step("spinner"), row({ event: "debug.screenshot" }), row({ source: "api", event: "nav.view" }), ], pageName, clock, ); expect(steps).toHaveLength(1); expect(steps[0]?.shot).toBe(true); }); }); const TEXT: IssueText = { steps: "Steps", environment: "Environment", envKeys: { version: "Version" }, screenshot: "screenshot", attachments: "Attachments", clientLog: (n) => `${n} browser lines`, serverLog: (n) => `${n} server lines`, screenshots: (n) => `${n} screenshots`, }; const form = (over: Partial = {}): DraftForm => ({ title: "t", description: "It spins.", steps: [ { id: "a", kind: "nav", text: "Plex" }, { id: "b", kind: "marker", text: " ", shot: false }, { id: "c", kind: "marker", text: "Resume", shot: true }, ], excluded: [], ...over, }); describe("the issue text", () => { it("carries the description, the non-empty steps, the environment and the attachments", () => { const md = renderIssue( form(), [["version", "0.93.0 | beta"]], { client: 3, server: 0, screenshots: 1 }, TEXT, ); expect(md).toBe( [ "It spins.", "### Steps\n\n1. Plex\n2. Resume _(screenshot)_", "### Environment\n\n| | |\n|---|---|\n| Version | 0.93.0 \\| beta |", "### Attachments\n\n- `client.log` — 3 browser lines\n- 1 screenshots", ].join("\n\n"), ); }); it("says when the logs were cut", () => { const md = renderIssue( form(), [], { client: 1, server: 1, screenshots: 0 }, { ...TEXT, truncated: "cut at 5000", }, ); expect(md.endsWith("_cut at 5000_")).toBe(true); }); }); describe("the system a browser runs on", () => { it.each([ ["Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 Chrome/152.0.0.0 Mobile", "Android 10"], ["Mozilla/5.0 (iPad; CPU OS 26_6_1 like Mac OS X) AppleWebKit/605.1.15", "iPad 26.6.1"], ["Mozilla/5.0 (iPhone; CPU iPhone OS 18_2 like Mac OS X)", "iPhone 18.2"], ["Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/152.0.0.0", "Windows NT 10.0"], ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Safari/605.1.15", "macOS 10.15.7"], ["Mozilla/5.0 (X11; Linux x86_64) Firefox/140.0", "Linux"], ])("%s → %s", (ua, os) => expect(osName(ua)).toBe(os)); });