import {test} from 'node:test'; import assert from 'node:assert/strict'; import {readFile} from 'node:fs/promises'; import ts from 'typescript'; async function moduleUrl(file,transform=code=>code){ const source=await readFile(new URL(file,import.meta.url),'utf8'); const code=ts.transpileModule(source,{fileName:new URL(file,import.meta.url).pathname,compilerOptions:{module:ts.ModuleKind.ESNext,target:ts.ScriptTarget.ES2022,jsx:ts.JsxEmit.ReactJSX}}).outputText; return 'data:text/javascript;base64,'+Buffer.from(transform(code)).toString('base64'); } const protocol=await moduleUrl('../src/lib/api/protocol.ts'),auth=await moduleUrl('../src/lib/client/auth.ts'); const apiUrl=await moduleUrl('../src/lib/client/api.ts',code=>code.replace('"../api/protocol"',JSON.stringify(protocol)).replace('"./auth"',JSON.stringify(auth))); const {ApiClient}=await import(apiUrl); const startupUrl='data:text/javascript,export const calls=[];export function startTracker(){calls.push("tracker")}export function startApplication(...args){calls.push(args)}'; const startup=await import(startupUrl); const scriptUrl='data:text/javascript,export default function Script(){}'; const runtimeUrl=await moduleUrl('../src/components/site/SiteRuntime.tsx',code=>{ const imports={'react/jsx-runtime':import.meta.resolve('react/jsx-runtime'),'next/script':scriptUrl,'@/lib/client/application':startupUrl,'@/lib/client/analytics':startupUrl}; for(const [path,url] of Object.entries(imports))for(const quote of ["'",'"'])code=code.replace(quote+path+quote,JSON.stringify(url)); return code; }); const {SiteRuntime}=await import(runtimeUrl); async function browser(work){ const previous={window:globalThis.window,fetch:globalThis.fetch},logs=[]; globalThis.window={console:Object.fromEntries(['log','info','debug','warn','error','group','groupCollapsed','groupEnd','table','trace'].map(method=>[method,(...args)=>logs.push({method,args})]))}; try{await work(logs);assert.deepEqual(logs,[]);} finally{globalThis.fetch=previous.fetch;if(previous.window===undefined)delete globalThis.window;else globalThis.window=previous.window;} } test('browser requests retain query, credentials, decoded responses and multipart bodies without console output',async()=>{ await browser(async()=>{ const body={account:'player',password:'unchanged-password'},query={pageNum:2,token:'query-token',empty:''}; const payload={code:200,data:JSON.stringify({id:31,moreUrl:'/rank/new/',access_token:'response-token'})}; let received; globalThis.fetch=async(url,options)=>{received={url,options};return new Response(JSON.stringify(payload),{status:200});}; const result=await new ApiClient().raw('auth/login',query,body); assert.equal(received.url,'/api/app/auth/login?pageNum=2&token=query-token'); assert.equal(received.options.credentials,'include');assert.equal(received.options.headers.Accept,'application/json');assert.equal(JSON.parse(received.options.body).password,body.password); assert.equal(result.access_token,'response-token');assert.equal(result.moreUrl,'/rank/new/'); const form=new FormData();form.append('image',new Blob(['image bytes'],{type:'image/png'}),'cover.png'); await new ApiClient().raw('upload',{},form);assert.equal(received.url,'/api/app/upload');assert.equal(received.options.body,form);assert.equal(received.options.headers['Content-Type'],undefined); }); }); test('API business failures, invalid JSON and network failures remain rejected without console output',async()=>{ await browser(async()=>{ const api=new ApiClient(); globalThis.fetch=async()=>new Response(JSON.stringify({code:5100,msg:'搜索服务暂不可用',data:null}),{status:200}); await assert.rejects(api.raw('playlet/search',{keyword:'都市'}),error=>error.code===5100&&error.message==='搜索服务暂不可用'); globalThis.fetch=async()=>new Response('invalid json',{status:502}); await assert.rejects(api.raw('playlet/home'),SyntaxError); globalThis.fetch=async()=>{throw new TypeError('Network offline');}; await assert.rejects(api.raw('playlet/home'),/Network offline/); }); }); test('home, player, navigation and wallet hydration starts normally without logging server data or refetching',async()=>{ await browser(async()=>{ globalThis.fetch=()=>assert.fail('hydration must use existing server data'); const wallet={user:{userId:77,recharge:90,token:'session-secret'},packages:{coinList:[{id:31,name:'金币套餐',price:10000}],vipList:[]},transactions:{list:[{id:12,amount:30}],totalCount:18}}; const home={homeConfigData:[{id:31,moreUrl:'/topics/'}]},navigation=[{name:'短剧',url:'/shorts/'}]; for(const page of [{path:'/',panel:'home'},{path:'/detail/7/',player:{title:'短剧'}},{path:'/info/deposit/page/2/',panel:'info',info:{key:'deposit',wallet}}]){ const start=startup.calls.length; SiteRuntime({home,homeLoaded:true,page,navigation}).props.onReady(); assert.equal(startup.calls[start],'tracker');assert.equal(startup.calls[start+1][0],home);assert.equal(startup.calls[start+1][3],page); } assert.equal(wallet.user.token,'session-secret'); }); });