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/91av-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/operate/list','playlet/home/config/detail/list','statcenter/sync'])assert.equal(server.allowedRequest(path,method),false,method+' '+path); }); test('the user-confirmed likes list is an authenticated GET endpoint',()=>{ assert.equal(server.allowedRequest('playlet/like/list','GET'),true); assert.equal(server.allowedRequest('playlet/like/list','POST'),false); assert.equal(server.PUBLIC_PATHS.has('playlet/like/list'),false); assert.deepEqual(contract['GET playlet/like/list'].query,['pageNum','pageSize']); }); 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/all/list',query:{pageNum:1,pageSize:24,sortType:2}}); }); test('shorts random recommendations send sortType 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/all/list',query:{pageNum:2,pageSize:24,sortType:4}}]); const view=channel.channelView(route,data,'/shorts/page/2/?sort=4'); assert.ok(view.html.includes('data-sort="4" aria-pressed="true">随机推荐')); 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.sortType,sort); for(const sort of ['0','5','NaN','1.5'])assert.equal(channel.channelRequest(route,'?sort='+sort).query.sortType,1); assert.equal(channel.channelRequest(routing.parseSitePath('/tag/6/'),'?sort=4').query.sortType,1); }); test('home more fallback uses the exact enabled block without inventing a paginated catalog',async()=>{ const calls=[]; const api={get:async(endpoint,query)=>{calls.push({endpoint,query});return {homeConfigData:[{id:1,status:true,name:'正确栏目',type:1,list:[{id:8,title:'唯一短剧'}]},{id:2,status:false,list:[{id:9}]}]};}}; const first=await channel.loadChannel(api,routing.parseSitePath('/config/1/')); assert.equal(first.name,'正确栏目');assert.deepEqual(plain(first.list),[{id:8,title:'唯一短剧'}]);assert.equal(first.totalCount,1); const second=await channel.loadChannel(api,routing.parseSitePath('/config/1/page/2/')); assert.deepEqual(plain(second.list),[]);assert.equal(second.totalCount,1); assert.equal((await channel.loadChannel(api,routing.parseSitePath('/config/2/'))).missing,true); assert.ok(calls.every(call=>call.endpoint==='playlet/home'&&Object.keys(call.query).length===0)); assert.equal(routing.parseSitePath('/series/'),null);assert.equal(routing.parseSitePath('/video/7/'),null); });