Files
web_huakui_web/tests/catalog-react.test.tsx
T
谢宇宁andClaude Opus 5.5 d2ddb1c321 [Ai] feat: 花魁短剧 Next.js 站点初始提交
- 原生 Next.js App Router + 严格 TypeScript 实现花魁短剧 PC/H5 页面,按 src/features/{catalog,content,playback} 组织,保留完整 SEO(metadata、JSON-LD、canonical、robots、llms、sitemap、IndexNow)。
- 主域名通过 SITE_URL=https://huakuidj.com 强制覆盖后端 foreverUrlAddr(config/*.env、src/server/config.ts、src/server/seo/shared.ts、src/server/services/shared.ts),支持邮箱改为 support@huakuidj.com。
- 移除登录与账户体系:删除 /account 全部页面、登录/注册/充值弹窗、收藏点赞、评论、金币解锁及对应 BFF 接口;旧账户地址 GET/HEAD 统一 301 到首页(src/lib/routes.ts REMOVED_ACCOUNT_PATHS),保留游客会话、反馈、图片上传、观看上报与搜索联想。
- 播放页:评论位改为同类推荐 + 热播榜单,锁定集改为 App 观看遮罩与底部弹层,新增简介标签、分集图例与独立视觉皮肤(src/features/playback、src/styles/pages/playback.css)。
- 卡片:短剧/视频卡状态与时长移至信息行胶囊标签,封面角标移至左上角(src/components/catalog/Cards.tsx、src/styles/foundations/base.css)。
- 同步更新单元测试、浏览器冒烟脚本、路由清单与 README。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 11:33:31 +07:00

186 lines
5.8 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import { renderToStaticMarkup } from "react-dom/server";
import {
DramaCard,
PlayRecommendationCard,
SeriesCard,
} from "../src/components/catalog/Cards";
import { Pagination } from "../src/components/catalog/Pagination";
import { PromoGrid } from "../src/components/catalog/Promotions";
import {
CatalogGrid,
ListPage,
SeriesPage,
} from "../src/features/catalog/BrowsePages";
import {
allListURL,
array,
cardTags,
moreListURL,
moreSeriesListURL,
searchResultsURL,
seriesListURL,
tagPageURL,
topicDetailURL,
} from "../src/features/catalog/model";
const item = {
id: 7,
slug: "都市 剧",
playName: "作品名",
title: "备用标题",
cover: "/static/brand/header.svg",
totalEpisode: 8,
updateStatus: 1,
isFeatured: true,
tagIds: [2],
tagNames: ["爱情"],
tagSlugs: ["romance"],
themeIds: [3],
themeNames: ["爱情", "都市"],
themeSlugs: ["love", "city"],
};
const ads = [
{
id: 1,
images: "/static/brand/header.svg",
link: "https://promo.test/",
title: "推荐",
},
];
test("React card variants preserve watch URLs, episode gates, badges and linked deduplicated tags", () => {
const card = renderToStaticMarkup(<DramaCard item={item} />);
assert.match(card, /class="hk-drama-card hk-drama-card--list"/);
assert.match(
card,
/href="\/drama\/都市%20剧\/"|href="\/drama\/%E9%83%BD%E5%B8%82%20%E5%89%A7\/"/,
);
assert.match(
card,
/<p class="hk-drama-card__meta hk-drama-card__meta--ep"><span class="hk-drama-card__meta-ep" data-status="serial">连载 · 8 集<\/span><span class="hk-drama-card__meta-tags">/,
);
assert.doesNotMatch(card, /hk-drama-card__ep"/);
assert.match(card, /hk-badge hk-badge--hot/);
assert.match(card, /href="\/tags\/romance\/"/);
assert.deepEqual(
cardTags(item).map((tag) => tag.Name),
["爱情", "都市"],
);
const recommendation = renderToStaticMarkup(
<PlayRecommendationCard item={item} />,
);
assert.match(recommendation, /hk-drama-card hk-drama-card--play/);
assert.match(recommendation, /hk-drama-card__meta-ep" data-status="serial">连载 · 8 集</);
assert.doesNotMatch(recommendation, /hk-badge--hot/);
const video = renderToStaticMarkup(
<SeriesCard
item={{
id: 9,
slug: "movie",
title: "剧集",
mediaType: 2,
videoCount: 12,
topicNames: ["动画"],
coverImg: "/static/brand/header.svg",
}}
/>,
);
assert.match(video, /href="\/series\/watch\/movie\/"/);
assert.match(video, /全 12 集/);
assert.match(video, /动画/);
});
test("React list advertising keeps independent desktop and mobile row positions and clone attribution", () => {
const items = array(
Array.from({ length: 25 }, (_, index) => ({ ...item, id: index + 1 })),
);
const grid = renderToStaticMarkup(<CatalogGrid items={items} ads={ads} />);
assert.equal((grid.match(/class="hk-drama-card hk-drama-card--list"/g) || []).length, 25);
assert.equal((grid.match(/class="hk-drama-browse-mid-promo hk-desktop-only"/g) || []).length, 2);
assert.equal((grid.match(/class="hk-drama-browse-mid-promo hk-mobile-only"/g) || []).length, 4);
const videoGrid = renderToStaticMarkup(
<CatalogGrid items={items.slice(0, 16)} ads={ads} series />,
);
assert.equal(
(videoGrid.match(/class="hk-series-list-promo hk-desktop-only"/g) || []).length,
1,
);
assert.equal(
(videoGrid.match(/class="hk-series-list-promo hk-mobile-only"/g) || []).length,
3,
);
const promo = renderToStaticMarkup(<PromoGrid items={ads} />);
assert.equal((promo.match(/data-track-ad="external"/g) || []).length, 2);
assert.equal((promo.match(/data-ad-clone="1"/g) || []).length, 1);
assert.match(promo, /rel="sponsored nofollow noopener"/);
});
test("React filter and pagination links preserve the source path-only canonical route contracts", () => {
assert.equal(allListURL("", 0, 0, 1, 1), "/dramas/");
assert.equal(
allListURL("都市", 3, 2, 3, 2),
"/dramas/%E9%83%BD%E5%B8%82/2/3/page/2/",
);
assert.equal(seriesListURL(2, 4, 3, 2), "/series/anime/4/3/page/2/");
assert.equal(moreListURL("featured", 1, 0, "", 0, 1), "/curated/dramas/featured/");
assert.equal(
moreSeriesListURL("", 3, 1, "city", 0, 2),
"/curated/series/3/1/city/page/2/",
);
assert.equal(tagPageURL("romance", 1, 2, 1), "/tags/romance/sort/2/");
assert.equal(
topicDetailURL("city", 1, 3, 2),
"/collection/city/sort/3/page/2/",
);
assert.equal(searchResultsURL("a/b"), "/search/a%2Fb/");
const html = renderToStaticMarkup(
<Pagination
pagination={{
Visible: true,
HasPrev: false,
HasNext: true,
NextURL: "/dramas/all/0/1/page/2/",
Items: [
{ Label: "1", URL: "/dramas/", Active: true },
{ Label: "…", Dots: true },
],
TotalText: "共 3 页",
}}
/>,
);
assert.match(html, /hk-pagination__btn--prev hk-is-disabled/);
assert.match(html, /aria-current="page"/);
assert.match(html, /href="\/dramas\/all\/0\/1\/page\/2\/"/);
assert.match(html, /hk-pagination__ellipsis/);
assert.match(html, /共 3 页/);
const list = renderToStaticMarkup(
<ListPage
data={{
list: [],
themeList: [{ id: 3, slug: "city", name: "都市" }],
themeId: 3,
themeSlug: "city",
updateStatus: 2,
sortType: 3,
}}
/>,
);
assert.match(list, /href="\/dramas\/city\/2\/3\/"/);
assert.match(list, /暂无短剧/);
const series = renderToStaticMarkup(
<SeriesPage
data={{
list: [],
topicList: [{ id: 4, name: "爱情" }],
topicId: 4,
mediaType: 2,
sortType: 1,
}}
/>,
);
assert.match(series, /href="\/series\/anime\/4\/1\/"/);
assert.match(series, /暂无剧集/);
});