Files
lms-sb/src/lib/__tests__/comment-reply-email.test.ts
T
adminsandClaude Opus 5.5 d8777a7a23 Кликабельные ссылки: история фидбека у куратора и цитаты в письмах
Куратор видел адрес в своём фидбеке мёртвым текстом — linkify там не было.
В письмах (фидбек, ответы на вопросы и комментарии) quote() только экранировал:
кликабельность зависела от почтовика. Теперь ссылки оформлены <a>.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 14:48:59 +05:00

103 lines
3.6 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
const sent: Array<Record<string, string>> = [];
vi.mock("resend", () => ({
Resend: class {
emails = {
send: async (args: Record<string, string>) => {
sent.push(args);
return { data: { id: "test" }, error: null };
},
};
},
}));
vi.mock("../settings", () => ({
getSetting: async () => "Second Brain",
}));
const { sendCommentReplyEmail } = await import("../email");
const LESSON_URL = "https://school.second-brain.ru/courses/obsidian-full/lessons/lesson-1";
describe("sendCommentReplyEmail", () => {
beforeEach(() => {
sent.length = 0;
});
it("кладёт текст ответа в письмо и ведёт кнопкой на урок", async () => {
await sendCommentReplyEmail(
"student@example.com",
"Владимир",
"6.3. Холст",
"Прямого импорта нет, но есть два пути.",
LESSON_URL,
"Дмитрий"
);
expect(sent).toHaveLength(1);
expect(sent[0].to).toBe("student@example.com");
expect(sent[0].subject).toBe("Ответ на ваш комментарий к уроку «6.3. Холст»");
expect(sent[0].html).toContain("Прямого импорта нет, но есть два пути.");
expect(sent[0].html).toContain("Привет, Владимир!");
expect(sent[0].html).toContain("Отвечает <strong>Дмитрий</strong>:");
expect(sent[0].html).toContain(`href="${LESSON_URL}"`);
});
it("экранирует разметку в тексте ответа и в названии урока", async () => {
await sendCommentReplyEmail(
"student@example.com",
"Владимир",
"Урок <b>про холст</b>",
'Смотрите тег <script>alert("x")</script>',
LESSON_URL
);
const html = sent[0].html;
expect(html).not.toContain("<script>");
expect(html).toContain("&lt;script&gt;");
expect(html).toContain("Урок &lt;b&gt;про холст&lt;/b&gt;");
});
it("обрезает длинный ответ и говорит, что целиком он под уроком", async () => {
await sendCommentReplyEmail(
"student@example.com",
"Владимир",
"6.3. Холст",
"я".repeat(4500),
LESSON_URL
);
const html = sent[0].html;
expect(html).toContain("Ответ длинный — целиком он под уроком.");
expect(html).toContain("…");
expect(html).not.toContain("я".repeat(4100));
});
it("без имени отвечающего пишет просто «Школа ответила»", async () => {
await sendCommentReplyEmail("student@example.com", "", "6.3. Холст", "Коротко", LESSON_URL);
const html = sent[0].html;
expect(html).toContain("Школа ответила на ваш комментарий");
expect(html).not.toContain("Отвечает <strong>");
expect(html).toContain("Привет!");
});
it("делает ссылки в тексте ответа кликабельными", async () => {
await sendCommentReplyEmail(
"student@example.com",
"Екатерина",
"6.3. Холст",
"Подарок тут: https://obsidian.second-brain.ru/secret-bonus?a=1&b=2. Пишите!",
LESSON_URL
);
const html = sent[0].html;
expect(html).toContain(
'<a href="https://obsidian.second-brain.ru/secret-bonus?a=1&amp;b=2" target="_blank"'
);
expect(html).toContain("secret-bonus?a=1&amp;b=2</a>. Пишите!");
});
});