import { createElement, Fragment } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vitest"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { Button } from "~/components/ui/button.tsx"; import { ClampText, type MediaFacts, MediaHero } from "./media-detail.tsx"; // 详情页头图(方案 A):背景图渐隐到页面底色、无背景图退化为普通头部;状态徽标并进事实行; // 操作按钮并进头图区且只有一个实心主操作;简介折叠只留一个「展开」入口。 const facts = (over: Partial = {}): MediaFacts => ({ title: "人生切割术", originalTitle: "Severance", year: 2022, kind: "series", kindLabel: "剧集", genres: ["剧情"], voteAverage: 8.4, voteCount: 1200, airStatus: "Returning Series", runtime: 55, seasonCount: 2, episodeCount: 19, releaseDate: "2022-02-17", lastAirDate: "2025-03-20", platform: "Apple TV", originCountry: ["US"], overview: "故事发生在一家名为卢蒙的超级大公司内。", tagline: null, posterPath: "/sev-poster.jpg", backdropPath: "/severance-bg.jpg", externalLinks: [{ label: "TMDB", href: "https://www.themoviedb.org/tv/95396" }], ...over, }); const render = (props: Parameters[0]) => renderToStaticMarkup(createElement(MediaHero, props)); /** 取 data-slot 标记的元素起始处之后的一段 HTML(足够断言其中的内容) */ const slotHtml = (html: string, slot: string) => { const i = html.indexOf(`data-slot="${slot}"`); return i < 0 ? null : html.slice(i, html.indexOf("", i) + 6); }; describe("MediaHero 头图", () => { it("有背景图:剧照层经图片代理、带渐隐材质,不再是深色圆角卡片;文字用常规前景色", () => { const html = render({ facts: facts() }); expect(html).toContain('data-backdrop="image"'); expect(html).toContain("media-hero-backdrop"); expect(html).toContain('src="/api/v1/img/tmdb/w1280/severance-bg.jpg"'); expect(html).not.toMatch(/bg-scrim|text-on-media|rounded-2xl/); expect(html).toContain("text-foreground"); }); it("没有背景图:退化为普通头部,不渲染剧照层和空的深色块", () => { const html = render({ facts: facts({ backdropPath: null }) }); expect(html).toContain('data-backdrop="none"'); expect(html).not.toContain("media-hero-backdrop"); expect(html).not.toContain("w1280"); expect(html).not.toMatch(/bg-scrim/); expect(html).toContain("人生切割术"); }); it("状态徽标(正常 / Emby 等)与播出状态在同一行", () => { const html = render({ facts: facts(), badges: StatusBadge({ tone: "success", children: "正常" }), }); const row = slotHtml(html, "media-hero-badges"); expect(row).toContain("连载中"); expect(row).toContain("正常"); }); it("操作按钮并进头图区、在外链之后;只有订阅是实心主操作,其余是描边次操作,没有棱镜描边", () => { const html = render({ facts: facts(), actions: createElement( Fragment, null, createElement(Button, { variant: "solid", size: "sm" }, "订阅"), createElement(Button, { variant: "outline", size: "sm" }, "搜索资源"), createElement(Button, { variant: "outline", size: "sm" }, "查看订阅"), ), }); const actions = slotHtml(html, "media-hero-actions"); expect(actions).not.toBeNull(); expect(html.indexOf('data-slot="media-hero-actions"')).toBeGreaterThan(html.indexOf("TMDB")); expect(actions!.match(/pill-solid/g)).toHaveLength(1); expect(actions!.match(/pill-secondary/g)).toHaveLength(2); expect(actions).not.toContain("pill-primary"); expect(actions!.indexOf("pill-solid")).toBeLessThan(actions!.indexOf("订阅")); }); it("没有操作时不渲染操作行;标语有值才显示", () => { const plain = render({ facts: facts() }); expect(plain).not.toContain("media-hero-actions"); expect(plain).not.toContain("media-hero-tagline"); const tagged = render({ facts: facts({ tagline: "工作与生活,一刀两断" }) }); expect(slotHtml(tagged, "media-hero-tagline")).toContain("工作与生活,一刀两断"); }); }); describe("ClampText 折叠", () => { const long = "很长的简介。".repeat(60); it("超出行数:折叠成行尾渐隐 + 一个「展开」入口,不再另加省略号", () => { const html = renderToStaticMarkup(createElement(ClampText, { text: long, lines: 4 })); expect(html).toContain("clamp-fade"); expect(html).toContain("--clamp-lines:4"); expect(html).not.toContain("line-clamp"); expect(html).not.toMatch(/…|\.\.\./); expect(html.match(/展开/g)).toHaveLength(1); expect(html).toContain('aria-expanded="false"'); }); it("放得下时不折叠、不给「展开」", () => { const html = renderToStaticMarkup(createElement(ClampText, { text: "短简介", lines: 4 })); expect(html).not.toContain("clamp-fade"); expect(html).not.toContain("展开"); }); });