- 原生 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>
186 lines
5.8 KiB
TypeScript
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, /暂无剧集/);
|
|
});
|