After Stop (or at the cap, or from Settings → Diagnosztika's past sessions) one dialog drafts the issue: title, description, the steps drafted from the session's markers and page changes (a marker without a label is named by its time), the environment (version, browser, system, window, language). Beside it, what goes with it: the browser and server logs — each reviewable line by line, an unticked line is not sent — and the screenshots, removable, with paste / drop / pick to add more. - it saves as it goes (debug_sessions.draft, migration 0090): the rendered markdown in the user's language, the excluded rows, and its own fields to reopen as it was; a sent session is read-only - the zip export (issue.md, client.log, server.log, screenshots) is built from the saved draft by app/issues/bundle.py — the one definition the Gitea delivery (S4) will share; .log, not .jsonl, which Gitea refuses - the log line's level tags and clock move to components/logLineParts so the review list draws a level the way both log viewers do
129 lines
4.0 KiB
TypeScript
129 lines
4.0 KiB
TypeScript
// 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>): 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> = {}): 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));
|
|
});
|