71 lines
4.9 KiB
JavaScript
71 lines
4.9 KiB
JavaScript
import {linkShared} from './helpers/shared-modules.mjs';
|
|
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(linkShared(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};
|
|
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 and info hydration starts the application only, without analytics, logging or refetching',async()=>{
|
|
await browser(async()=>{
|
|
globalThis.fetch=()=>assert.fail('hydration must use existing server data');
|
|
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/about-us/',panel:'info',info:{key:'about-us'}}]){
|
|
const start=startup.calls.length;
|
|
SiteRuntime({home,homeLoaded:true,page,navigation}).props.onReady();
|
|
assert.equal(startup.calls.length,start+1);assert.equal(startup.calls[start][0],home);assert.equal(startup.calls[start][3],page);
|
|
}
|
|
assert.ok(!startup.calls.includes('tracker'));
|
|
});
|
|
});
|