样式修改
This commit is contained in:
+2
-2
@@ -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,'花魁短剧网页版可以看全集吗?');
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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'));
|
||||
|
||||
@@ -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, /花魁 & 第一部/);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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, "");
|
||||
});
|
||||
Reference in New Issue
Block a user