- 材质统一边光:slab / float / pill / pill-selected / seg-thumb / chrome-side 改为左上亮右下暗的 1px 边光 + 外侧暗线;新增 lens / field / chip 参数与 pg-float-modal - 主按钮:转动光谱环改为静态左上光谱高光,悬停沿边扫过一小段;次按钮同为 lens 玻璃 - Input / Textarea / SelectTrigger 挂 .field-glass,原生下拉与实体选择器同步;对话框按钮行 gap-3 - 提示条改 pg-float(状态色只在图标、展开不叠放);新增 GlassTooltip 接管 title 提示 - 触屏判断 input-modality.ts、touch / hover 自定义变体;animate-pg-pop 改 backwards;scroll-fade-end - 控制台外壳:侧栏无硬分隔线、折叠开关进标志行;顶栏去磨砂,右侧控件收进玻璃胶囊 - 登录页居中单卡 auth-card;详情头图「订阅」改回普通主按钮,删除 pill-solid;发现页海报角标改 glass-chip-media - 引入 simple-liquid-glass(liquid-surface.tsx);更新 docs/design 三份规范与 CHANGELOG Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
121 lines
5.0 KiB
TypeScript
121 lines
5.0 KiB
TypeScript
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> = {}): 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<typeof MediaHero>[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("</div>", 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, { 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-primary/g)).toHaveLength(1);
|
|
expect(actions!.match(/pill-secondary/g)).toHaveLength(2);
|
|
expect(actions).not.toContain("pill-solid");
|
|
expect(actions!.indexOf("pill-primary")).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("展开");
|
|
});
|
|
});
|