I-1 I-2 M-1 M-2 M-3 M-5 M-6 M-7 T3.4 T4.3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
120 lines
7.1 KiB
TypeScript
120 lines
7.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import { type Inline, parseInline } from "./inline.ts";
|
||
|
||
const P = { projectKey: "AURORA" };
|
||
const t = (text: string): Inline => ({ t: "text", text });
|
||
const wrap = (kind: "strong" | "em" | "del" | "ins" | "sup" | "sub", ...children: Inline[]): Inline => ({ t: kind, children });
|
||
const issue = (key: string, ...children: Inline[]): Inline => ({ t: "issue", key, children });
|
||
const link = (href: string, ...children: Inline[]): Inline => ({ t: "link", href, children });
|
||
|
||
describe("parseInline: выделение", () => {
|
||
it.each<[string, Inline[]]>([
|
||
["*Итог:* ошибка", [wrap("strong", t("Итог:")), t(" ошибка")]],
|
||
["_к_", [wrap("em", t("к"))]],
|
||
["-з-", [wrap("del", t("з"))]],
|
||
["+п+", [wrap("ins", t("п"))]],
|
||
["^в^", [wrap("sup", t("в"))]],
|
||
["~н~", [wrap("sub", t("н"))]],
|
||
["*_x_*", [wrap("strong", wrap("em", t("x")))]],
|
||
["(*x*)", [t("("), wrap("strong", t("x")), t(")")]],
|
||
["a *b c* d", [t("a "), wrap("strong", t("b c")), t(" d")]],
|
||
["{{data_type}}", [{ t: "code", text: "data_type" }]],
|
||
["{{*x*}} и", [{ t: "code", text: "*x*" }, t(" и")]],
|
||
])("%s", (src, tree) => expect(parseInline(src, P)).toEqual(tree));
|
||
});
|
||
|
||
describe("parseInline: обычный текст не портится", () => {
|
||
it.each([
|
||
"data_type = null", "2026-10-15", "счёт-фактура", "a*b*c", "18 803 903,65 - 9 351 102,89", "**важно**",
|
||
"* одинокая", "*незакрытый", "[без конца", "*x*y", "{{без конца", "UTF-8", "SHA-256", "DOC-221280", "XAURORA-1",
|
||
"AURORA-12x", "",
|
||
])("%s", (src) => expect(parseInline(src, P)).toEqual(src ? [t(src)] : []));
|
||
});
|
||
|
||
describe("parseInline: экранирование и перенос", () => {
|
||
it("\\* — сам символ", () => expect(parseInline("\\*не жирный\\*", P)).toEqual([t("*не жирный*")]));
|
||
it("\\\\ — перенос", () => expect(parseInline("a\\\\b", P)).toEqual([t("a"), { t: "br" }, t("b")]));
|
||
});
|
||
|
||
describe("parseInline: ссылки", () => {
|
||
it.each<[string, Inline[]]>([
|
||
["[Jira|https://x.ru/a?b=1]", [link("https://x.ru/a?b=1", t("Jira"))]],
|
||
["[https://x.ru]", [link("https://x.ru", t("https://x.ru"))]],
|
||
["[mail|MAILTO:a@b.ru]", [link("MAILTO:a@b.ru", t("mail"))]],
|
||
["[*Жирная* ссылка|http://x.ru]", [link("http://x.ru", wrap("strong", t("Жирная")), t(" ссылка"))]],
|
||
["см. https://x.ru/p, далее", [t("см. "), link("https://x.ru/p", t("https://x.ru/p")), t(", далее")]],
|
||
["(https://x.ru/p).", [t("("), link("https://x.ru/p", t("https://x.ru/p")), t(").")]],
|
||
["[~ivanov]", [{ t: "mention", login: "ivanov" }]],
|
||
])("%s", (src, tree) => expect(parseInline(src, P)).toEqual(tree));
|
||
|
||
it("в подписи ссылки ключи и адреса — текст (I-2: без вложенных ссылок)", () => {
|
||
expect(parseInline("[AURORA-1445|https://j.x/browse/AURORA-1445]", P)).toEqual([link("https://j.x/browse/AURORA-1445", t("AURORA-1445"))]);
|
||
expect(parseInline("[см. https://a.ru|https://b.ru]", P)).toEqual([link("https://b.ru", t("см. https://a.ru"))]);
|
||
expect(parseInline("[*AURORA-1*|AURORA-2]", P)).toEqual([issue("AURORA-2", wrap("strong", t("AURORA-1")))]);
|
||
});
|
||
|
||
it.each(["[нажми|javascript:alert(1)]", "[x|ftp://h]", "[javascript:alert(1)]", "[просто текст]"])(
|
||
"опасная или неизвестная цель — текст как есть: %s",
|
||
(src) => expect(parseInline(src, P)).toEqual([t(src)]),
|
||
);
|
||
});
|
||
|
||
describe("parseInline: ключи задач", () => {
|
||
it("ключи проекта в тексте", () =>
|
||
expect(parseInline("ведётся в AURORA-1445; AURORA-1645 снята", P)).toEqual([
|
||
t("ведётся в "), issue("AURORA-1445"), t("; "), issue("AURORA-1645"), t(" снята"),
|
||
]));
|
||
it("[ключ] и [текст|ключ]", () => {
|
||
expect(parseInline("[AURORA-1]", P)).toEqual([issue("AURORA-1")]);
|
||
expect(parseInline("[см.|AURORA-2]", P)).toEqual([issue("AURORA-2", t("см."))]);
|
||
});
|
||
it("не внутри {{…}} и адреса", () => {
|
||
expect(parseInline("{{AURORA-1}}", P)).toEqual([{ t: "code", text: "AURORA-1" }]);
|
||
expect(parseInline("https://j/browse/AURORA-1", P)).toEqual([link("https://j/browse/AURORA-1", t("https://j/browse/AURORA-1"))]);
|
||
});
|
||
it("без projectKey — текст", () => expect(parseInline("AURORA-1445")).toEqual([t("AURORA-1445")]));
|
||
});
|
||
|
||
describe("parseInline: ссылки на вложения", () => {
|
||
const atts = [
|
||
{ id: "31337", filename: "скрин 1.png", created: "2026-10-15T12:00:00.000+0300" },
|
||
{ id: "31338", filename: "a.pdf", created: null },
|
||
{ id: "10", filename: "дубль.png", created: "2026-10-14T12:00:00.000+0300" },
|
||
{ id: "20", filename: "дубль.png", created: "2026-10-20T12:00:00.000+0300" },
|
||
];
|
||
const A = { attachments: atts };
|
||
const att = (id: string, ...children: Inline[]): Inline => ({ t: "attachment", id, children });
|
||
|
||
it("!имя|параметры! и !имя!", () => {
|
||
expect(parseInline("см. !скрин 1.png|thumbnail!", A)).toEqual([t("см. "), att("31337", t("скрин 1.png"))]);
|
||
expect(parseInline("!скрин 1.png!", A)).toEqual([att("31337", t("скрин 1.png"))]);
|
||
});
|
||
it("[^имя] и [текст|^имя]", () => {
|
||
expect(parseInline("[^a.pdf]", A)).toEqual([att("31338", t("a.pdf"))]);
|
||
expect(parseInline("[отчёт|^a.pdf]", A)).toEqual([att("31338", t("отчёт"))]);
|
||
});
|
||
it("имя точно (с регистром); несколько с одним именем — самое новое", () => {
|
||
expect(parseInline("!Скрин 1.png!", A)).toEqual([t("!Скрин 1.png!")]);
|
||
expect(parseInline("!дубль.png!", A)).toEqual([att("20", t("дубль.png"))]);
|
||
});
|
||
it("имени нет среди вложений — исходник как есть", () => {
|
||
expect(parseInline("!нет.png!", A)).toEqual([t("!нет.png!")]);
|
||
expect(parseInline("[^нет.pdf] и [x|^нет.pdf]", A)).toEqual([t("[^нет.pdf] и [x|^нет.pdf]")]);
|
||
});
|
||
it("одиночный «!» перед ссылкой не мешает ей (I-1)", () => {
|
||
expect(parseInline("Внимание! см. !a.pdf!", A)).toContainEqual(att("31338", t("a.pdf")));
|
||
const r = parseInline("Важно! [^a.pdf] и !a.pdf!", A);
|
||
expect(r.filter((n) => n.t === "attachment")).toEqual([att("31338", t("a.pdf")), att("31338", t("a.pdf"))]);
|
||
});
|
||
it("внешняя картинка не вложение", () => {
|
||
expect(JSON.stringify(parseInline("!http://x/скрин 1.png!", { attachments: [{ id: "1", filename: "http://x/скрин 1.png", created: null }] }))).not.toContain("attachment");
|
||
});
|
||
it("без опции attachments — как раньше", () => {
|
||
expect(parseInline("!скрин 1.png!")).toEqual([t("!скрин 1.png!")]);
|
||
expect(parseInline("[^a.pdf]")).toEqual([t("[^a.pdf]")]);
|
||
});
|
||
it("внутри подписи ссылки вложения нет (кнопка в ссылке недопустима)", () => {
|
||
expect(parseInline("[!a.pdf!|https://x.y]", A)).toEqual([link("https://x.y", t("!a.pdf!"))]);
|
||
});
|
||
});
|