Files
web_huakui_web/tests/public-navigation.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

212 lines
12 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import type { ReactNode } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { SiteProvider } from "../src/components/providers/SiteProvider";
import { SiteHeader } from "../src/components/layout/SiteHeader";
import { MobileTabbar } from "../src/components/layout/MobileTabbar";
import { HomePage } from "../src/features/catalog/HomePage";
import { HomeRoadPage } from "../src/features/content/HomeRoadPage";
import { SiteNavigationPage } from "../src/features/content/SiteNavigationPage";
import { brandAssets } from "../src/lib/brand-assets";
import { toPublicPath } from "../src/lib/routes";
import { allListURL, chapterPlayURL, categoryURL, listThemeSegment, listUpdateStatus, moreListURL, moreSeriesListURL, navHasMegaMenu, navHref, navIsActive, navIsDownload, pageURL, videoPlayURL } from "../src/lib/view-helpers";
const data = {
requestPath: "/dramas/", isLoggedIn: true, isRegistered: true, userID: 7,
userName: "观众", recharge: 100, appConfig: {}, config: {},
categoryList: [
{ name: "首页", urlValue: "/", categoryType: 1 },
{ name: "短剧", urlValue: "/list", categoryType: 1 },
{ name: "专题", urlValue: "/topic", categoryType: 1 },
{ name: "排行榜", urlValue: "/rank", categoryType: 1 },
{ name: "回家的路", urlValue: "/home-road/", categoryType: 1 },
],
themeList: [{ id: 1, slug: "romance", name: "爱情" }],
};
const render = (children: ReactNode) => renderToStaticMarkup(<SiteProvider data={data} dialogs={false}>{children}</SiteProvider>);
const hrefs = (html: string) => [...html.matchAll(/<a\b[^>]*\bhref="([^"]*)"/g)].map((match) => match[1].replaceAll("&amp;", "&"));
function assertPublicLinks(html: string): string[] {
const links = hrefs(html);
for (const href of links.filter((value) => value.startsWith("/") && !value.startsWith("//")))
assert.equal(href, toPublicPath(href), "internal link should reach its page directly: " + href);
return links;
}
test("shared view helpers produce business URLs and keep nested menu activation boundaries", () => {
assert.equal(chapterPlayURL("story/a", 7, 101), "/drama/story%2Fa/101/");
assert.equal(pageURL("/play/story%2fa?x=a+b&x=%2b#ep%202"), "/drama/story%2fa/?x=a+b&x=%2b#ep%202");
assert.equal(videoPlayURL("movie", 9), "/series/watch/movie/");
assert.equal(categoryURL("romance", 1), "/collection/romance/");
assert.equal(allListURL("romance", 1, 2, 1, 3), "/dramas/romance/2/1/page/3/");
assert.equal(moreListURL("picks", 1, 0, "", 0, 1), "/curated/dramas/picks/");
assert.equal(moreSeriesListURL("movies", 2, 0, "", 0, 1), "/curated/series/movies/");
assert.equal(navHref("/playlet"), "/dramas/");
assert.equal(navHref("/topic/romance/"), "/collection/romance/");
assert.equal(navHref("/download-app/"), "/app/download/");
assert.equal(navHref("/home-road/"), "/return-home/");
assert.equal(navIsDownload("/app/download/", "客户端下载"), true);
assert.equal(navHasMegaMenu("/dramas/"), true);
assert.equal(listThemeSegment("/dramas/romance/2/1/"), "romance");
assert.equal(listUpdateStatus("/dramas/romance/2/1/"), 2);
assert.equal(navIsActive("/series/", "/series/watch/movie/"), false);
assert.equal(navIsActive("/dramas/", "/dramas/romance/"), true);
});
test("navigation configuration resolves both legacy and normalized collection-detail aliases without decoding slugs", () => {
for (const root of ["/topic", "/collections"]) {
assert.equal(navHref(root + "/热门?q=a"), "/collection/%E7%83%AD%E9%97%A8/?q=a");
assert.equal(navHref(root + "/story%2fa%252F1?x=a+b&x=%2b#ep%202"), "/collection/story%2fa%252F1/?x=a+b&x=%2b#ep%202");
assert.equal(navHref(root + "/page/2/"), "/collections/page/2/");
assert.equal(navHref(root + "/sort/2/"), "/collections/sort/2/");
}
});
test("SSR header exposes direct canonical URLs and no account entries", () => {
const html = render(<SiteHeader data={data} />);
const links = assertPublicLinks(html);
for (const expected of ["/dramas/", "/collections/", "/rankings/", "/discover/search/", "/return-home/"])
assert.ok(links.includes(expected), expected);
assert.ok(!links.some((link) => link.startsWith("/account")), "account links removed");
assert.doesNotMatch(html, /data-login-open|hk-user-menu/);
});
test("configured header submenus expose canonical links, external link attributes and download actions", () => {
const html = render(<SiteHeader data={{
...data,
appConfig: { appAddr: "https://downloads.test/app" },
categoryList: [
{
name: "友情链接", urlValue: "#", categoryType: 2,
translationList: [
{ name: "黄果视频", urlValue: "https://huangguo.com", isFriendAds: false },
{ name: "外部站点", urlValue: "//partner.test/path", isFriendAds: true },
{ name: "爱情专题", urlValue: "/topic/romance/" },
{ name: "下载 App", urlValue: "/download-app/" },
],
},
{ name: "更多入口", urlValue: "#", translationList: [{ name: "排行榜", urlValue: "/rank/" }] },
],
}} />);
const links = assertPublicLinks(html);
assert.ok(links.includes("/collection/romance/"));
assert.ok(links.includes("/rankings/"));
for (const href of ["https://huangguo.com", "//partner.test/path"]) {
const anchor = [...html.matchAll(/<a\b[^>]*>/g)].map(([tag]) => tag).find((tag) => tag.includes(`href="${href}"`));
assert.ok(anchor, href);
assert.match(anchor, /target="_blank"/);
assert.match(anchor, /rel="sponsored nofollow noopener"/);
}
assert.match(html, /<a\b[^>]*href="https:\/\/downloads\.test\/app"[^>]*data-download-open=""/);
const toggles = [...html.matchAll(/<button\b[^>]*>/g)].map(([tag]) => tag).filter((tag) => tag.includes("hk-site-navigation__submenu-toggle"));
assert.equal(toggles.length, 2);
const controls = toggles.map((tag) => {
assert.match(tag, /aria-expanded="false"/);
const id = /aria-controls="([^"]+)"/.exec(tag)?.[1];
assert.ok(id);
assert.ok(html.includes(`id="${id}"`), "submenu control points to its rendered panel");
return id;
});
assert.equal(new Set(controls).size, controls.length);
});
test("configured lists take precedence over the short-drama mega menu while missing or empty lists retain normal navigation", () => {
const configured = render(<SiteHeader data={{
...data,
categoryList: [{ name: "短剧", urlValue: "/list", translationList: [{ name: "爱情专题", urlValue: "/topic/romance/" }] }],
}} />);
assert.match(configured, /data-navigation-submenu="desktop"/);
assert.doesNotMatch(configured, /aria-label="短剧快捷筛选"/);
for (const translationList of [undefined, [], null, {}, "invalid"]) {
const html = render(<SiteHeader data={{
...data,
categoryList: [
{ name: "短剧", urlValue: "/list", translationList },
{ name: "专题", urlValue: "/topic", translationList },
],
}} />);
assert.doesNotMatch(html, /data-navigation-submenu="desktop"/);
assert.match(html, /aria-label="短剧快捷筛选"/);
const links = assertPublicLinks(html);
assert.ok(links.includes("/dramas/"));
assert.ok(links.includes("/collections/"));
}
});
test("mobile TabBar shows only content tabs and stays hidden on nested series pages", () => {
const links = assertPublicLinks(renderToStaticMarkup(<MobileTabbar data={{ requestPath: "/" }} />));
assert.deepEqual(links, ["/", "/dramas/", "/series/"]);
for (const requestPath of ["/account/", "/series/watch/movie/"])
assert.equal(renderToStaticMarkup(<MobileTabbar data={{ requestPath }} />), "", requestPath);
assert.match(renderToStaticMarkup(<MobileTabbar data={{ requestPath: "/dramas/romance/" }} />), /aria-current="page"[^>]*data-navigation-key="mobile_short"/);
});
test("home Hero, cards and curated sections all navigate directly to new page addresses", () => {
const work = { id: 7, slug: "story", playName: "作品", totalEpisode: 8, lastChapterId: 101, lastEpisode: 2, cover: "/static/brand/header.svg", banner: "/static/brand/header.svg" };
const html = render(<HomePage data={{
...data, homeBannerData: [work],
homeConfigData: [
{ id: 1, slug: "picks", name: "精选", type: 1, list: [work] },
{ id: 2, slug: "movies", name: "剧集", type: 2, videoList: [{ id: 9, slug: "movie", title: "剧集", coverImg: "/static/brand/header.svg" }] },
{ id: 3, name: "专题", type: 3, themeList: [{ id: 4, slug: "romance", name: "爱情", covers: [work.cover] }] },
],
}} />);
const links = assertPublicLinks(html);
for (const expected of ["/drama/story/", "/dramas/", "/curated/dramas/picks/", "/curated/series/movies/", "/series/watch/movie/", "/collection/romance/", "/collections/", "/app/download/"])
assert.ok(links.includes(expected), expected);
});
test("home road renders current branding, configured access routes and official contact links", () => {
const html = render(<HomeRoadPage data={{
requestPath: "/return-home/", siteURL: "https://huakui.example",
appConfig: {
newURLAddr: "https://new.huakui.example/",
foreverURLAddr: "https://huakui.example/home-road/",
newURLAddr1: "/topic/",
navPageList: [{ url: "/list/" }, { url: "https://backup.example/path?x=a+b&x=%2b" }],
gmailAddr: "help@huakui.example", tgGroup: "https://t.me/huakui_official",
telegram: "https://t.me/huakui_support", twitterAddr: "https://x.com/huakui",
},
faqItems: [{ Question: "如何保存入口?", Answer: "收藏本页并保存备用地址。" }],
}} />);
const links = assertPublicLinks(html);
for (const expected of ["https://new.huakui.example/", "/dramas/", "https://backup.example/path?x=a+b&x=%2b", "mailto:help@huakui.example", "https://t.me/huakui_official", "https://t.me/huakui_support", "https://x.com/huakui"])
assert.ok(links.includes(expected), expected);
assert.ok(html.includes(`src="${brandAssets.navigationLogo}"`));
assert.match(html, /alt="花魁短剧"/);
assert.match(html, /花魁短剧 · 官方最新地址发布页/);
assert.match(html, /huakui\.example\/return-home/);
assert.match(html, /如何保存入口?/);
assert.doesNotMatch(html, /黄果短剧|huangguoaiai@gmail\.com|href="\/home-road\//);
});
test("home road keeps an accessible fallback when configured routes or contacts are absent or unsafe", () => {
for (const appConfig of [{}, {
newURLAddr: "javascript:alert(1)", foreverURLAddr: "//unknown.example/",
newURLAddr1: "https://unsafe.example/\\path", newURLAddr2: "data:text/html,unsafe",
navPageList: [{ url: "javascript:alert(2)" }],
gmailAddr: "invalid", tgGroup: "javascript:alert(3)",
}]) {
const html = render(<HomeRoadPage data={{ appConfig }} />);
assert.ok(hrefs(html).every((href) => href === "/"));
assert.match(html, /永久入口正在更新/);
assert.match(html, /官方联系方式正在更新/);
assert.match(html, /回家的路页面有什么作用?/);
assert.doesNotMatch(html, /javascript:|data:text|huangguoaiai@gmail\.com/);
}
});
test("site navigation uses the supplied home road destination and hides an unavailable entry", () => {
for (const homeRoadURL of ["/return-home/", "https://main.huakui.test/return-home/", "https://files.huakui.test/access.pdf", ""]) {
const html = render(<SiteNavigationPage data={{ appConfig: {}, homeRoadURL }} />);
const homeRoadLink = [...html.matchAll(/<a\b[^>]*>[\s\S]*?<\/a>/g)]
.map(([anchor]) => anchor).find((anchor) => anchor.includes(">回家的路</span>"));
if (!homeRoadURL) assert.equal(homeRoadLink, undefined);
else {
assert.ok(homeRoadLink);
assert.ok(homeRoadLink.includes(`href="${homeRoadURL}"`));
if (homeRoadURL.startsWith("https://")) assert.match(homeRoadLink, /target="_blank"/);
}
}
});