样式修改

This commit is contained in:
谢宇宁
2026-10-16 14:10:10 +07:00
parent d2ddb1c321
commit 4afd5f9143
42 changed files with 89 additions and 1765 deletions
+2 -2
View File
@@ -37,14 +37,14 @@ test('page injection and cached upstream configuration cannot restore the former
process.env.UPSTREAM_API_URL='http://brand-contract.test';const calls=new Map<string,number>();
globalThis.fetch=async(input)=>{
const path=new URL(String(input)).pathname;calls.set(path,(calls.get(path)||0)+1);let data:unknown={list:[],totalCount:0};
if(path==='/api/app/config/info')data={config:{foreverUrlAddr:'https://canonical.drama.test',gmailAddr:'support@huangguo.com',brandName:'黄果短剧'},seoInfo:{title:'黄果短剧首页',description:'黄果短剧介绍',webSiteName:'黄果短剧',keywords:['黄果短剧']},seoNavInfo:{dhTitle:'黄果短剧导航',dhDescription:'黄果短剧最新线路',dhSiteName:'黄果短剧'},reportSdkConfig:'黄果短剧-signed-sdk'};
if(path==='/api/app/config/info')data={config:{foreverUrlAddr:'https://canonical.drama.test',gmailAddr:'support@huangguo.com',brandName:'黄果短剧'},seoInfo:{title:'黄果短剧首页',description:'黄果短剧介绍',webSiteName:'黄果短剧',keywords:['黄果短剧']},seoNavInfo:{dhTitle:'黄果短剧导航',dhDescription:'黄果短剧最新线路',dhSiteName:'黄果短剧'}};
if(path==='/api/app/translation/category/list')data={categoryList:[{name:'黄果短剧首页',urlValue:'/',categoryType:1,seoInfo:{title:'黄果短剧首页',description:'黄果短剧介绍'}}]};
if(path==='/api/app/playlet/home')data={seoInfo:{title:'黄果短剧首页'},announcements:[{content:'欢迎来到黄果短剧'}],homeConfigData:[{name:'黄果短剧精选',type:1,status:true}],homeBannerData:[{id:1,playName:'黄果恋人'}]};
return Response.json({code:200,data});
};
try{
const page=await buildPage(context('/'));const config=page.data.appConfig;
assert.equal(page.data.title,'花魁短剧首页');assert.equal(config.config.brandName,BRAND_NAME);assert.equal(config.seoInfo.webSiteName,BRAND_NAME);assert.equal(config.seoNavInfo.dhTitle,'花魁短剧导航');assert.equal(page.data.categoryList[0].name,'花魁短剧首页');assert.equal(page.data.announcements[0].content,'欢迎来到花魁短剧');assert.equal(page.data.homeConfigPlaylets[0].name,'花魁短剧精选');assert.equal(page.data.homeBannerData[0].playName,'黄果恋人');assert.equal(config.gmailAddr,'support@huangguo.com');assert.equal(config.reportSdkConfig,'黄果短剧-signed-sdk');
assert.equal(page.data.title,'花魁短剧首页');assert.equal(config.config.brandName,BRAND_NAME);assert.equal(config.seoInfo.webSiteName,BRAND_NAME);assert.equal(config.seoNavInfo.dhTitle,'花魁短剧导航');assert.equal(page.data.categoryList[0].name,'花魁短剧首页');assert.equal(page.data.announcements[0].content,'欢迎来到花魁短剧');assert.equal(page.data.homeConfigPlaylets[0].name,'花魁短剧精选');assert.equal(page.data.homeBannerData[0].playName,'黄果恋人');assert.equal(config.gmailAddr,'support@huangguo.com');
const cached=await apiData(context('/'),'/api/app/config/info',{}, {public:true,ttl:60000});assert.equal(cached.seoInfo.webSiteName,BRAND_NAME);assert.equal(cached.config.brandName,BRAND_NAME);assert.equal(calls.get('/api/app/config/info'),1);
const rendered=enrichSEO(context('/'),page.data);assert.equal(rendered.siteName,BRAND_NAME);assert.equal(rendered.canonicalURL,'https://canonical.drama.test/');assert.equal(rendered.metaTitle,'花魁短剧首页');
const help=await buildPage(context('/help/'));assert.equal(help.data.faqItems[0].Question,'花魁短剧网页版可以看全集吗?');
+2 -3
View File
@@ -112,9 +112,8 @@ test("React list advertising keeps independent desktop and mobile row positions
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"/);
assert.equal((promo.match(/rel="sponsored nofollow noopener"/g) || []).length, 2);
assert.equal((promo.match(/tabindex="-1"/g) || []).length, 1);
});
test("React filter and pagination links preserve the source path-only canonical route contracts", () => {
+2 -2
View File
@@ -63,7 +63,7 @@ test('missing upstream fields retain legacy Go zero values rather than blank att
assert.equal(detail.CountLike,0);assert.equal(detail.IsLike,0);assert.equal(detail.Preview,'');assert.equal(detail.ThemeNames,null);assert.equal(detail.PlayletChapterInfo[0].MediaTime,0);assert.equal(detail.PlayletChapterInfo[0].NeedUnlock,false);assert.equal(detail.PlayletChapterInfo[0].AllowWatch,true);
});
test('list and playback page loaders preserve filters, canonical paths, locked media and tracking',async()=>{
test('list and playback page loaders preserve filters, canonical paths and locked media',async()=>{
const originalFetch=globalThis.fetch,originalUpstream=process.env.UPSTREAM_API_URL;process.env.UPSTREAM_API_URL='http://page-contract-test';const calls:{url:URL;headers:Headers}[]=[];
globalThis.fetch=async(input,init)=>{
const url=new URL(String(input));calls.push({url,headers:new Headers(init?.headers)});let data:any={list:[],totalCount:0};
@@ -78,7 +78,7 @@ test('list and playback page loaders preserve filters, canonical paths, locked m
const listed=await list(ctx('/list/12/2/3/page/2/'),{theme:'12',updateStatus:'2',sortType:'3',page:'2'});
assert.equal(listed.themeId,12);assert.equal(listed.themeSlug,'romance');assert.equal(listed.canonicalPath,'/list/romance/2/3/page/2/');assert.equal(listed.pagination.NextURL,'/list/romance/2/3/page/3/');assert.equal(listed.pageHeading,'爱情短剧');
const catalogue=calls.find(call=>call.url.pathname==='/api/app/playlet/all/list')!;assert.equal(catalogue.url.searchParams.get('themeSlug'),'romance');assert.equal(catalogue.headers.has('authorization'),false);
const played=await play(ctx('/play/drama/102/'),{slug:'drama',ep:'102'});assert.ok(played);assert.equal(played.chapterID,102);assert.equal(played.episode,2);assert.equal(played.canWatch,false);assert.equal(played.mainURL,'');assert.equal(played.previewURL,'preview.m3u8');assert.equal(played.balanceText,'12,345');assert.equal(played.seoInfo.title,'剧名 - 第二集');assert.equal(played.canonicalPath,'/drama/drama/');assert.equal(played.trackerDrama.VideoID,102);assert.equal(played.trackerDrama.DramaTypeID,'12');assert.equal(played.comments,undefined);assert.ok(!calls.some(call=>call.url.pathname==='/api/app/comment/list'));
const played=await play(ctx('/play/drama/102/'),{slug:'drama',ep:'102'});assert.ok(played);assert.equal(played.chapterID,102);assert.equal(played.episode,2);assert.equal(played.canWatch,false);assert.equal(played.mainURL,'');assert.equal(played.previewURL,'preview.m3u8');assert.equal(played.balanceText,'12,345');assert.equal(played.seoInfo.title,'剧名 - 第二集');assert.equal(played.canonicalPath,'/drama/drama/');assert.equal(played.comments,undefined);assert.ok(!calls.some(call=>call.url.pathname==='/api/app/comment/list'));
const chapter=calls.find(call=>call.url.pathname==='/api/app/playlet/chapter/detail')!;assert.equal(chapter.url.searchParams.get('chapterId'),'102');assert.equal(chapter.headers.get('authorization'),'test-session');
assert.equal(await play(ctx('/play/drama/999/'),{slug:'drama',ep:'999'}),null);
for(const path of ['/search/?q=legacy','/search/?page=2','/play/drama/?guessPage=2','/favorites/?filter=all','/favorites/invalid/','/history/page/10001/','/settings//'])assert.equal((await buildPage(ctx(path))).status,404,path);
-26
View File
@@ -6,11 +6,6 @@ import {
shouldDecryptImage,
} from "../src/lib/media/images";
import { isHLSURL, nativeHLS, resolvePlayURL } from "../src/lib/media/playback";
import { trackingContext } from "../src/features/playback/model";
import {
normalizeChannel,
normalizeUID,
} from "../src/components/analytics/tracker";
test("legacy image XOR is immutable, symmetric and changes only the first 100 bytes", () => {
const plain = Uint8Array.from({ length: 150 }, (_, index) => index),
@@ -75,24 +70,3 @@ test("native HLS only uses Safari and shell browsers keep the inline MSE player"
])
assert(!nativeHLS(video, "Mozilla iPhone Safari/604.1 " + browser));
});
test("drama tracking and timestamps preserve upstream field meaning", () => {
assert.equal(trackingContext({ DramaID: 0 }), null);
const context = trackingContext({
DramaID: 42,
VideoID: 101,
Episode: 2,
TotalEpisode: 8,
DramaTypeID: 3,
DramaTypeName: "都市",
DramaTagKey: "1,2",
DramaTagName: "剧情,原创",
});
assert.equal(context?.drama_id, "42");
assert.equal(context?.episode, 2);
assert.equal(context?.drama_content_type, "short_drama");
assert.equal(context?.drama_tag_key, "1,2");
assert.equal(normalizeChannel("official"), "organic");
assert.equal(normalizeChannel("channel_A-2"), "channel_A-2");
assert.equal(normalizeUID("stress_user"), "");
assert.equal(normalizeUID("real_1001"), "real_1001");
});
+3 -11
View File
@@ -44,13 +44,13 @@ after(async () => { await new Promise<void>(resolve => server.close(() => resolv
const request = (path: string, init: RequestInit = {}) => new Request('http://local.example' + path, init);
const post = (path: string, body: unknown, headers: HeadersInit = {}) => request(path, { method: 'POST', headers: { 'Content-Type': 'application/json', ...Object.fromEntries(new Headers(headers)) }, body: JSON.stringify(body) });
test('SSR/upstream preserve client cookies, cookie-token priority, IP, custom UA and SDK headers', async () => {
test('SSR/upstream preserve client cookies, cookie-token priority, IP, and custom UA headers', async () => {
scenario = ''; received.length = 0;
const ctx = createContext(request('/list/', { headers: { Cookie: 'token=cookie-token; extra=keep%20me', Authorization: 'header-token', 'User-Agent': 'browser-agent', 'X-User-Agent': 'client-UA', 'X-Real-IP': '203.0.113.12', 'X-SDK-Common': 'sdk-snapshot' } }));
const ctx = createContext(request('/list/', { headers: { Cookie: 'token=cookie-token; extra=keep%20me', Authorization: 'header-token', 'User-Agent': 'browser-agent', 'X-User-Agent': 'client-UA', 'X-Real-IP': '203.0.113.12'} }));
await upstreamGet(ctx, '/api/app/config/info');
const headers = received[0].headers;
assert.equal(headers.authorization, 'cookie-token'); assert.equal(headers.cookie, 'token=cookie-token; extra=keep%20me');
assert.equal(headers['user-agent'], 'browser-agent'); assert.equal(headers['x-user-agent'], 'client-UA'); assert.equal(headers['x-real-ip'], '203.0.113.12'); assert.equal(headers['x-sdk-common'], 'sdk-snapshot');
assert.equal(headers['user-agent'], 'browser-agent'); assert.equal(headers['x-user-agent'], 'client-UA'); assert.equal(headers['x-real-ip'], '203.0.113.12');
});
test('public data requests isolate both stale Authorization and auth cookies', async () => {
scenario = ''; received.length = 0;
@@ -67,14 +67,6 @@ test('hop-by-hop, Connection-nominated and internal headers never cross the BFF'
const headers = buildUpstreamHeaders(ctx);
for (const name of ['connection', 'x-internal', 'x-origin-head', 'x-tracker-request-id', 'x-dc-request-context']) assert.equal(headers.has(name), false);
});
test('canonical attribution snapshot forwards only on the original allowed action', () => {
const id = 'a'.repeat(32);
const envelope = { v: 1, request_id: id, event_name: 'drama_like', common: { app_id: 'JHA-4001', uid: '12345', channel: 'campaign', sid: 'session', client_ts: 1791550000, device: 'PC', device_id: 'device', device_brand: '', device_model: '', device_fingerprint: '', fp_version: '', user_agent: 'browser', sdk_version: '1.2.0', app_version: '1.0.0' }, drama: { media_id: '', drama_id: '123', episode: 1, video_id: '', recommend_trace_id: '' } };
const headers = { 'X-Tracker-Request-Id': id, 'X-Tracker-Event-Name': 'drama_like', 'X-DC-Request-Context': Buffer.from(JSON.stringify(envelope)).toString('base64url') };
const allowed = buildUpstreamHeaders(createContext(post('/api/playlet/like/set', { id: 123, isLike: true }, headers)));
assert.deepEqual(JSON.parse(Buffer.from(allowed.get('X-DC-Request-Context')!, 'base64url').toString()), envelope);
assert.equal(buildUpstreamHeaders(createContext(post('/api/video/like/set', {}, headers))).has('X-DC-Request-Context'), false);
});
test('Set-Cookie values remain separate and updated same-name cookies are written once', async () => {
scenario = 'cookies'; received.length = 0;
const ctx = createContext(request('/api/auth/login'));
+2 -2
View File
@@ -107,8 +107,8 @@ test("React tags and list pages preserve the original Go catalog content and nav
name,
);
assert.equal(
(actual.match(/data-track-ad="external"/g) || []).length,
(expected.match(/data-track-ad="external"/g) || []).length,
(actual.match(/rel="sponsored nofollow noopener"/g) || []).length,
(expected.match(/rel="sponsored nofollow noopener"/g) || []).length,
name + " ad placement",
);
assert.match(actual, /花魁 &amp; 第一部/);
+5 -5
View File
@@ -6,7 +6,7 @@ import { canonicalRedirect } from "../src/server/redirects";
import { matchPageRoute } from "../src/server/pages";
import { pageRoutes, publicPageRoutes } from "../src/server/pages/routes";
import { preparePage } from "../src/server/react/page-data";
import { trackerPage } from "../src/server/services/shared";
import { pageIdentity } from "../src/server/services/shared";
test("Huakui route prefixes preserve encoded slugs, raw queries and fragments", () => {
const suffix = "/%E7%9F%AD%E5%89%A7%2F%E9%9B%86%252F1/?x=a+b&x=%2b&token=a%2FB#episode%202";
@@ -262,7 +262,7 @@ test("navigation-only home road links use a configured main-site origin, then th
});
test("semantic page paths retain their original analytics classification", () => {
test("semantic page paths retain their original page classification", () => {
const paths: [string, string, string][] = [
["/list/romance/", "/dramas/romance/", "list"],
["/play/a%2Fb/123/", "/drama/a%2Fb/123/", "play"],
@@ -279,8 +279,8 @@ test("semantic page paths retain their original analytics classification", () =>
["/home-road/", "/return-home/", "home_road"],
];
for (const [legacy, current, key] of paths) {
assert.deepEqual(trackerPage(current), trackerPage(legacy), current);
assert.equal(trackerPage(current)[0], key, current);
assert.deepEqual(pageIdentity(current), pageIdentity(legacy), current);
assert.equal(pageIdentity(current)[0], key, current);
}
assert.equal(trackerPage("/unknown/")[0], "internal_other");
assert.equal(pageIdentity("/unknown/")[0], "internal_other");
});
-81
View File
@@ -1,81 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { runInNewContext } from "node:vm";
const sdkSource = readFileSync(new URL("../public/static/common/web-sdk-v1.2.0.js", import.meta.url), "utf8");
interface SDKContext {
navigator?: { userAgent: string; platform: string; maxTouchPoints: number };
WebSDK?: { getCommonFields(fields?: string[]): Promise<Record<string, unknown>> };
}
function loadSDK(userAgent?: string, platform = "", maxTouchPoints = 0) {
const context: SDKContext = {};
if (userAgent !== undefined) context.navigator = { userAgent, platform, maxTouchPoints };
runInNewContext(sdkSource, context, { filename: "web-sdk-v1.2.0.js" });
assert.ok(context.WebSDK);
return context.WebSDK;
}
const userAgents = [
{
name: "iPhone",
ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 Version/18.0 Mobile/15E148 Safari/604.1",
platform: "iPhone", touch: 5, system: "iOS", version: "18.0",
},
{
name: "iPad mobile mode",
ua: "Mozilla/5.0 (iPad; CPU OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 Version/17.6 Mobile/15E148 Safari/604.1",
platform: "iPad", touch: 5, system: "iOS", version: "17.6.1",
},
{
name: "Android",
ua: "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 Chrome/129.0.0.0 Mobile Safari/537.36",
platform: "Linux armv8l", touch: 5, system: "Android", version: "14",
},
{
name: "Windows NT",
ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/129.0.0.0 Safari/537.36",
platform: "Win32", touch: 0, system: "Windows", version: "10.0",
},
{
name: "macOS",
ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6_1) AppleWebKit/605.1.15 Version/17.6 Safari/605.1.15",
platform: "MacIntel", touch: 0, system: "macOS", version: "14.6.1",
},
{
name: "iPad desktop mode without an exposed iOS version",
ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Version/18.0 Safari/605.1.15",
platform: "MacIntel", touch: 5, system: "iOS", version: "",
},
{
name: "Linux without an exposed kernel version",
ua: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/129.0.0.0 Safari/537.36",
platform: "Linux x86_64", touch: 0, system: "Linux", version: "",
},
{
name: "unknown operating system",
ua: "ExampleBrowser/1.0", platform: "", touch: 0, system: "Other", version: "",
},
];
for (const device of userAgents) {
test(`bundled SDK common fields report the operating system for ${device.name}`, async () => {
const sdk = loadSDK(device.ua, device.platform, device.touch);
const common = await sdk.getCommonFields();
assert.equal(common.system_name, device.system);
assert.equal(common.system_version, device.version);
assert.equal(common.user_agent, device.ua);
const selected = await sdk.getCommonFields(["system_name", "system_version"]);
assert.deepEqual(JSON.parse(JSON.stringify(selected)), {
system_name: device.system, system_version: device.version,
});
});
}
test("bundled SDK OS fields tolerate a missing navigator without inventing a version", async () => {
const common = await loadSDK().getCommonFields();
assert.equal(common.system_name, "Other");
assert.equal(common.system_version, "");
});