Files
web_hgdjc_h5/tests/api-contract.test.mjs
2026-11-06 00:13:27 +07:00

119 lines
9.0 KiB
JavaScript

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {readFile} from 'node:fs/promises';
import vm from 'node:vm';
import ts from 'typescript';
const contract=JSON.parse(await readFile(new URL('./fixtures/sgdj-app-contract.json',import.meta.url),'utf8')).operations;
const compile=async file=>ts.transpileModule(await readFile(new URL(file,import.meta.url),'utf8'),{compilerOptions:{target:ts.ScriptTarget.ES2022,module:ts.ModuleKind.CommonJS}}).outputText;
const protocol={},routing={},rendering={},channel={},server={};
vm.runInNewContext(await compile('../src/lib/api/protocol.ts'),{exports:protocol,URL});
vm.runInNewContext(await compile('../src/lib/routing.ts'),{exports:routing,URL,URLSearchParams});
vm.runInNewContext(await compile('../src/lib/client/render.ts'),{exports:rendering,URL,require:key=>key==='../routing'?routing:protocol});
vm.runInNewContext(await compile('../src/lib/client/channel.ts'),{exports:channel,URL,URLSearchParams,require:key=>key==='../routing'?routing:key==='../api/protocol'?protocol:key==='./render'?rendering:{}});
vm.runInNewContext(await compile('../src/lib/api/server.ts'),{exports:server,require:key=>key==='react'?{cache:fn=>fn}:protocol});
const plain=value=>JSON.parse(JSON.stringify(value));
test('proxy methods and public endpoints are backed by the reviewed API contract',async()=>{
for(const path of server.PUBLIC_PATHS)assert.ok(contract['GET '+path],path);
const code=ts.createSourceFile('server.ts',await readFile(new URL('../src/lib/api/server.ts',import.meta.url),'utf8'),ts.ScriptTarget.Latest,true);
const walk=node=>{
if(ts.isStringLiteral(node)&&node.text.includes('/')&&server.allowedRequest(node.text,'POST'))assert.ok(contract['POST '+node.text],node.text);
ts.forEachChild(node,walk);
};walk(code);
for(const method of ['GET','POST'])for(const path of ['video/detail','video/all/list','playlet/hot/list','playlet/home/config/detail/list','statcenter/sync'])assert.equal(server.allowedRequest(path,method),false,method+' '+path);
});
test('member list, coin, recharge, feedback and account endpoints are no longer proxied',()=>{
for(const path of ['playlet/like/list','playlet/collect/list','playlet/history/list','coin/list','recharge/list','feedback/add','user/upload','auth/login','auth/register'])
for(const method of ['GET','POST'])assert.equal(server.allowedRequest(path,method),false,method+' '+path);
assert.equal(server.PUBLIC_PATHS.has('playlet/like/list'),false);
assert.equal(server.allowedRequest('auth/guest','POST'),true);
});
test('all public list routes send only supported query fields with their native page numbers',()=>{
for(const path of ['/shorts/page/2/','/rank/hot/page/2/','/rank/new/page/2/','/rank/follow/page/2/','/topics/page/2/','/topics/10404/page/2/','/theme/10404/page/2/','/tag/6/page/2/','/config/10081/']){
const route=routing.parseSitePath(path),request=channel.channelRequest(route,'?sort=2'),operation=contract['GET '+request.endpoint];
assert.ok(operation,path);
for(const key of Object.keys(request.query))assert.ok(operation.query.includes(key),path+' unsupported '+key);
for(const key of operation.requiredQuery)assert.notEqual(request.query[key],undefined,path+' missing '+key);
if(route.key!=='config')assert.equal(request.query.pageNum,2,path);
}
assert.deepEqual(plain(channel.channelRequest(routing.parseSitePath('/shorts/'),'?sort=2')),{endpoint:'playlet/operate/list',query:{pageNum:1,pageSize:24,type:2}});
});
test('shorts random recommendations send operate type 4 and preserve selection and pagination filters',async()=>{
const route=routing.parseSitePath('/shorts/page/2/'),calls=[];
const data=await channel.loadChannel({get:async(endpoint,query)=>{calls.push({endpoint,query});return {list:[{id:8,title:'随机短剧'}],totalCount:72};}},route,'?sort=4');
assert.deepEqual(plain(calls),[{endpoint:'playlet/operate/list',query:{pageNum:2,pageSize:24,type:4}}]);
const view=channel.channelView(route,data,'/shorts/page/2/?sort=4');
assert.ok(view.html.includes('data-sort="4" aria-pressed="true">随机推荐</button>'));
assert.equal((view.html.match(/aria-pressed="true"/g)||[]).length,1);
assert.ok(view.html.includes('href="/shorts/page/3/?sort=4"'));
assert.equal(routing.paginationPath('/shorts/page/2/?sort=2',1,{sort:4}),'/shorts/?sort=4');
for(const sort of [1,2,3,4])assert.equal(channel.channelRequest(route,'?sort='+sort).query.type,sort);
for(const sort of ['0','5','NaN','1.5'])assert.equal(channel.channelRequest(route,'?sort='+sort).query.type,1);
assert.equal(channel.channelRequest(routing.parseSitePath('/tag/6/'),'?sort=4').query.sortType,1);
});
test('home more uses the documented paginated config endpoint and actual totals',async()=>{
const calls=[];
const api={get:async(endpoint,query)=>{calls.push({endpoint,query});return {config:{id:1,slug:'editors',title:'正确栏目',desc:'栏目介绍',moduleType:1},list:[{id:8,title:'当前页短剧'}],totalCount:72,pageNum:query.pageNum,pageSize:24};}};
const data=await channel.loadChannel(api,routing.parseSitePath('/config/editors/page/2/'));
assert.equal(data.name,'正确栏目');assert.equal(data.totalCount,72);assert.equal(data.list[0].id,8);
assert.deepEqual(plain(calls),[{endpoint:'playlet/home-config/list',query:{pageNum:2,pageSize:24,slug:'editors'}}]);
const view=channel.channelView(routing.parseSitePath('/config/editors/page/2/'),data,'/config/editors/page/2/');
assert.ok(view.html.includes('href="/config/editors/page/3/"'));
assert.equal((await channel.loadChannel({get:async()=>({})},routing.parseSitePath('/config/2/'))).missing,true);
assert.equal(routing.parseSitePath('/series/'),null);assert.equal(routing.parseSitePath('/video/7/'),null);
});
test('topic groups keep their names, card identities and per-group pagination boundaries',async()=>{
const groups=[{id:1,name:'热门剧单',desc:'区块介绍',totalCount:49,list:[{id:7,name:'都市精选',slug:'city',playletCount:12}]},{id:2,name:'本周推荐',totalCount:25,list:[{id:8,name:'古装精选'}]}];
const route=routing.parseSitePath('/topics/page/2/');
const data=await channel.loadChannel({get:async()=>({list:groups,pageNum:2,pageSize:24})},route);
assert.equal(data.totalCount,49);assert.deepEqual(plain(data.list.map(item=>item.id)),[7,8]);
const view=channel.channelView(route,data,'/topics/page/2/');
assert.ok(view.html.includes('热门剧单'));assert.ok(view.html.includes('本周推荐'));assert.ok(view.html.includes('/topics/city/'));assert.ok(!view.html.includes('/topics/1/'));
assert.ok(view.html.includes('/topics/page/3/'));assert.ok(!view.html.includes('/topics/page/4/'));
assert.ok(view.html.includes('channel-topic-group'));
assert.ok(!view.html.includes('channel-summary-count'));
assert.equal(view.subtitle,'围绕一个主题,发现一组好故事。');
});
test('topic detail shows playlet count subtitle, drama list summary and moreTopics',()=>{
const route=routing.parseSitePath('/topics/city/');
const view=channel.channelView(route,{
id:7,slug:'city',name:'都市精选',desc:'都市逆袭合集',playletCount:9,totalCount:9,
list:[{id:101,title:'归家'}],
moreTopics:[{id:8,name:'古装精选',slug:'costume',playletCount:6}],
},'/topics/city/');
assert.equal(view.title,'都市精选');
assert.equal(view.subtitle,'都市逆袭合集 · 9 部精选');
assert.ok(view.html.includes('共 9 部作品'));
assert.ok(view.html.includes('更多专题'));
assert.ok(view.html.includes('/topics/costume/'));
assert.ok(view.html.includes('6部短剧'));
});
test('topic pages omit empty list, summary, moreTopics and zero playlet badges',()=>{
const list=channel.channelView(routing.parseSitePath('/topics/'),{list:[],topicGroups:[{id:1,name:'空分组',totalCount:0,list:[]}],totalCount:0},'/topics/');
assert.equal(list.html,'');
assert.equal(list.subtitle,'');
const detail=channel.channelView(routing.parseSitePath('/topics/city/'),{id:7,slug:'city',name:'都市精选',list:[],totalCount:0,moreTopics:[]},'/topics/city/');
assert.ok(!detail.html.includes('channel-summary'));
assert.ok(!detail.html.includes('class="cards"'));
assert.ok(!detail.html.includes('更多专题'));
assert.ok(!detail.html.includes('ui-empty-state'));
const card=channel.channelView(routing.parseSitePath('/topics/'),{topicGroups:[{id:1,name:'有专题',totalCount:1,list:[{id:8,name:'无数量专题',slug:'empty'}]}]},'/topics/');
assert.ok(card.html.includes('无数量专题'));
assert.ok(!card.html.includes('0部短剧'));
});
test('theme filters use the actual theme catalog and all/list rather than topic detail',async()=>{
const calls=[];
const data=await channel.loadChannel({get:async(endpoint,query)=>{calls.push({endpoint,query});return endpoint==='theme/list'?{list:[{id:9,slug:'urban',name:'都市题材'}]}:{list:[{id:7}],totalCount:30};}},routing.parseSitePath('/theme/urban/page/2/'),'?sort=3&updateStatus=2');
assert.deepEqual(plain(calls),[{endpoint:'theme/list',query:{}},{endpoint:'playlet/all/list',query:{pageNum:2,pageSize:24,themeSlug:'urban',sortType:3,updateStatus:2}}]);
assert.equal(data.id,9);assert.equal(data.name,'都市题材');
});