Andrey Epifancev fbc14f6366 fix(web): вложения в ленте именем (I-1), один шаг при двойном «Назад» (M-1), разбор таблиц и {code} (T1.1, T1.2)
wikiPlain включает WikiOptions.anyAttachment: !имя! и [^имя] дают вложение без списка; карточка флаг не передаёт.
onBack защищён closingKey; в таблице [ парна, только если ближайшая скобка — ]; {code} из пробелов в одной строке не даёт pre.
Тесты: events, plain, parse, IssueModal (двойной «Назад», «Назад» при ошибке загрузки — T3.2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 01:32:16 +04:00

111 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, expect, it } from "vitest";
import type { Inline } from "./inline.ts";
import { type Block, type ListBlock, type ListItem, parseWiki } from "./parse.ts";
const P = { projectKey: "AURORA" };
const t = (text: string): Inline => ({ t: "text", text });
const br: Inline = { t: "br" };
const p = (...children: Inline[]): Block => ({ t: "p", children });
const item = (text: string, ...lists: ListBlock[]): ListItem => ({ children: text ? [t(text)] : [], lists });
const ul = (...items: ListItem[]): ListBlock => ({ t: "list", ordered: false, items });
const ol = (...items: ListItem[]): ListBlock => ({ t: "list", ordered: true, items });
const h = (level: 1 | 2 | 3 | 4 | 5 | 6, text: string): Block => ({ t: "heading", level, children: [t(text)] });
const STRUCTURE = "h3. Контекст\nШаги:\n# Загрузить ОСВ\n# Открыть документ\n\n*Итог:* ошибка в {{data_type}}";
describe("parseWiki: абзацы и заголовки", () => {
it("сценарий «Описание со структурой»", () =>
expect(parseWiki(STRUCTURE, P)).toEqual([
h(3, "Контекст"),
p(t("Шаги:")),
ol(item("Загрузить ОСВ"), item("Открыть документ")),
p({ t: "strong", children: [t("Итог:")] }, t(" ошибка в "), { t: "code", text: "data_type" }),
]));
it("CRLF даёт то же дерево", () => {
const tree = parseWiki(STRUCTURE.replace(/\n/g, "\r\n"), P);
expect(tree).toEqual(parseWiki(STRUCTURE, P));
expect(JSON.stringify(tree)).not.toContain("\\r");
});
it("абзац из двух строк — через br", () => expect(parseWiki("a\nb")).toEqual([p(t("a"), br, t("b"))]));
it("пустые строки разделяют абзацы, пустых абзацев нет", () => expect(parseWiki("a\n\n\n\nb")).toEqual([p(t("a")), p(t("b"))]));
it.each(["", " \n ", "\r\n"])("пустой текст %j → []", (src) => expect(parseWiki(src)).toEqual([]));
it.each([1, 2, 3, 4, 5, 6] as const)("h%i.", (n) => expect(parseWiki(`h${n}. Заголовок`)).toEqual([h(n, "Заголовок")]));
it.each(["h7. x", "h3.без пробела"])("%s — абзац", (src) => expect(parseWiki(src)).toEqual([p(t(src))]));
it("----", () => expect(parseWiki("a\n----\nb")).toEqual([p(t("a")), { t: "hr" }, p(t("b"))]));
});
describe("parseWiki: списки", () => {
it("сценарий «Списки с отступом и пустыми строками» (R-4)", () =>
expect(parseWiki("Шаги воспроизведения:\n * Загрузить ОСВ.\n\n * Дождаться обработки.")).toEqual([
p(t("Шаги воспроизведения:")), ul(item("Загрузить ОСВ."), item("Дождаться обработки.")),
]));
it("вложенность и смешанные маркеры", () =>
expect(parseWiki("* a\n** b\n*# c\n* d")).toEqual([ul(item("a", ul(item("b")), ol(item("c"))), item("d"))]));
it("- пункты", () => expect(parseWiki("- x\n- y")).toEqual([ul(item("x"), item("y"))]));
it.each(["-- x", "*- x", "-# x"])("%s — не список", (src) => expect(parseWiki(src)).toEqual([p(t(src))]));
it("## Заголовок — второй уровень нумерованного", () =>
expect(parseWiki("## Заголовок")).toEqual([ol(item("", ol(item("Заголовок"))))]));
it("строка без маркера после списка — абзац", () => expect(parseWiki("* a\nтекст")).toEqual([ul(item("a")), p(t("текст"))]));
it("смена вида на первом уровне — новый список", () => expect(parseWiki("* a\n# b")).toEqual([ul(item("a")), ol(item("b"))]));
it("*жирный* в начале строки — не список", () =>
expect(parseWiki("*Уточнение.* текст")).toEqual([p({ t: "strong", children: [t("Уточнение.")] }, t(" текст"))]));
});
describe("parseWiki: таблицы", () => {
it("заголовки, ячейки, | внутри ссылки", () =>
expect(parseWiki("||Поле||Значение||\n|data_type|null|\n|ссылка|[a|https://x.ru]|", P)).toEqual([{
t: "table",
rows: [
[{ header: true, children: [t("Поле")] }, { header: true, children: [t("Значение")] }],
[{ header: false, children: [t("data_type")] }, { header: false, children: [t("null")] }],
[{ header: false, children: [t("ссылка")] }, { header: false, children: [{ t: "link", href: "https://x.ru", children: [t("a")] }] }],
],
}]));
it("смешанная строка ||h|c|", () =>
expect(parseWiki("||h|c|")).toEqual([{ t: "table", rows: [[{ header: true, children: [t("h")] }, { header: false, children: [t("c")] }]] }]));
});
describe("parseWiki: блоки-макросы", () => {
it("{code:sql}", () => expect(parseWiki("{code:sql}\nselect *\nfrom t\n{code}")).toEqual([{ t: "pre", text: "select *\nfrom t" }]));
it("{noformat} в одной строке", () => expect(parseWiki("{noformat}*x*{noformat}")).toEqual([{ t: "pre", text: "*x*" }]));
it("незакрытый {code} — текст", () => expect(parseWiki("{code}\nx")).toEqual([p(t("{code}"), br, t("x"))]));
it("{quote} и хвост после закрывающего тега", () =>
expect(parseWiki("{quote}\nh3. Внутри\nтекст\n{quote}хвост")).toEqual([
{ t: "quote", children: [h(3, "Внутри"), p(t("текст"))] }, p(t("хвост")),
]));
it("bq.", () => expect(parseWiki("bq. цитата")).toEqual([{ t: "quote", children: [p(t("цитата"))] }]));
it("{quote} не в начале строки — текст (R-8)", () => expect(parseWiki("см. {quote}x{quote}")).toEqual([p(t("см. {quote}x{quote}"))]));
it.each(["{color:red}важно{color}", "!схема.png!", "{panel}x{panel}"])("неизвестное %s — текст как есть", (src) =>
expect(parseWiki(src)).toEqual([p(t(src))]));
it("\\# в начале строки — абзац, не список", () => {
expect(parseWiki("\\# не пункт")).toEqual([{ t: "p", children: [{ t: "text", text: "# не пункт" }] }]);
});
it("quote и noformat с параметрами (M-5)", () => {
expect(parseWiki("{quote:title=Итог}\nсогласовано\n{quote}")).toEqual([{ t: "quote", children: [{ t: "p", children: [{ t: "text", text: "согласовано" }] }] }]);
expect(parseWiki("{noformat:borderStyle=solid}\n*x*\n{noformat}")).toEqual([{ t: "pre", text: "*x*" }]);
});
it("пробелы после открывающего {code} не дают пустой строки (M-6)", () => {
expect(parseWiki("{code} \nx = 1\n{code}")).toEqual([{ t: "pre", text: "x = 1" }]);
});
it("непарная [ в ячейке не съедает разделители (M-6)", () => {
const [table] = parseWiki("|[draft|итог|готово|");
expect(table).toMatchObject({ t: "table" });
expect((table as { rows: { children: { text: string }[] }[][] }).rows[0].map((c) => c.children[0].text)).toEqual(["[draft", "итог", "готово"]);
});
it("непарная [ не берёт чужую ] из ссылки в следующей ячейке (T1.1)", () => {
const [table] = parseWiki("|[draft|итог|[док|https://x.ru]|");
const cells = (table as { rows: { children: Inline[] }[][] }).rows[0];
expect(cells).toHaveLength(3);
expect(cells[0].children[0]).toEqual({ t: "text", text: "[draft" });
expect(cells[1].children[0]).toEqual({ t: "text", text: "итог" });
expect(cells[2].children[0]).toMatchObject({ t: "link", href: "https://x.ru" });
});
it("{code} {code} в одной строке — пустой блок, не pre из пробелов (T1.2)", () => {
expect(parseWiki("{code} {code}")).toEqual([{ t: "pre", text: "" }]);
});
it("| внутри [текст|адрес] ячейку по-прежнему не делит", () => {
const [table] = parseWiki("|[док|https://x.ru]|b|");
expect((table as { rows: unknown[][] }).rows[0]).toHaveLength(2);
});
});