import {routingModuleUrl} from './helpers/routing.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,replace=code=>code){ const filename=new URL(file,import.meta.url); const {outputText}=ts.transpileModule(await readFile(filename,'utf8'),{compilerOptions:{target:ts.ScriptTarget.ES2022,module:ts.ModuleKind.ESNext}}); return 'data:text/javascript;base64,'+Buffer.from(replace(outputText).replace(/(['"])\.\.\/routing\1/g,JSON.stringify(routingModuleUrl))+'\n//# sourceURL='+filename.pathname).toString('base64'); } const protocolUrl=await moduleUrl('../src/lib/api/protocol.ts'); const renderUrl=await moduleUrl('../src/lib/client/render.ts',code=>code.replace('"../api/protocol"',JSON.stringify(protocolUrl))); const searchUrl=await moduleUrl('../src/lib/client/search.ts',code=>code.replace("'../api/protocol'",JSON.stringify(protocolUrl)).replace("'./render'",JSON.stringify(renderUrl))); const routingUrl=await moduleUrl('../src/lib/routing.ts'); const {loadSearchResults,searchResultsMarkup,searchErrorMarkup,searchSuggestionsMarkup,searchPicksMarkup,SEARCH_PAGE_SIZE}=await import(searchUrl); const {highlightSearchTitle}=await import(renderUrl); const {paginationPath,searchPath,parseSitePath}=await import(routingUrl); const signal=()=>new AbortController().signal; const drama={id:7,title:'SEO 标题',playName:'都市 <短剧>',cover:'https://cdn.example/encrypted.webp',totalEpisode:12}; test('search uses reference API parameters, authoritative totals and actual playName without local filtering',async()=>{ const calls=[]; const result=await loadSearchResults({get:async(path,query,requestSignal)=>{calls.push({path,query,requestSignal});return {keyword:'都市剧',list:[null,drama,{id:8,title:'服务端判定的相关作品'}],totalCount:102,pageSize:25};}},' 都市 ',3,signal()); assert.equal(SEARCH_PAGE_SIZE,25);assert.equal(calls.length,1);assert.equal(calls[0].path,'playlet/search');assert.deepEqual(calls[0].query,{keyword:'都市',pageNum:3,pageSize:25}); assert.equal(result.keyword,'都市剧');assert.equal(result.total,102);assert.equal(result.page,3);assert.equal(result.pageSize,25); assert.equal(result.list[0].title,'都市 <短剧>');assert.equal(result.list.length,2);assert.deepEqual(result.recommended,[]); }); test('independent search page links preserve encoded keywords and use the response page size',async()=>{ const keyword='都市 / 爱情?#%'; const result=await loadSearchResults({get:async()=>({list:[drama],totalCount:61,pageSize:10})},keyword,2,signal()); const base=searchPath(keyword,2); const html=searchResultsMarkup(result,page=>paginationPath(base,page)); for(const page of [1,3,7])assert.ok(html.includes('href="'+searchPath(keyword,page)+'"')); assert.ok(html.includes('name="page" min="1" max="7"'));assert.ok(html.includes('共 61 个结果'));assert.ok(html.includes('data-src="https://cdn.example/encrypted.webp"')); assert.ok(!html.includes('SEO 标题'));assert.ok(html.includes('都市 <短剧>'));assert.ok(!html.includes('#search/')); }); test('empty results request six hottest works and keep them separate from the search count',async()=>{ const calls=[]; const result=await loadSearchResults({get:async(path,query)=>{ calls.push({path,query}); if(path==='playlet/search')return {list:null,totalCount:0}; return {list:[null,{id:0,title:'无效'},drama,drama,...Array.from({length:7},(_,i)=>({...drama,id:i+10}))]}; }},'没有匹配的关键词',1,signal()); assert.deepEqual(calls[1],{path:'playlet/operate/list',query:{pageNum:1,pageSize:6,type:2}}); assert.equal(result.recommended.length,6);assert.equal(new Set(result.recommended.map(item=>item.id)).size,6);assert.equal(result.total,0);assert.deepEqual(result.list,[]); const html=searchResultsMarkup(result,()=>assert.fail('empty first page has no pagination')); assert.ok(html.includes('共 0 个结果'));assert.ok(html.includes('搜索无结果'));assert.ok(html.includes('f2ef01f956fdcd80.png')); assert.ok(html.includes('大家都在看'));assert.ok(html.includes('aria-label="热门推荐"'));assert.ok(html.includes('href="/shorts/"')); }); test('empty entry skips the search endpoint; unavailable recommendations never replace an empty result with an error',async()=>{ const calls=[]; const result=await loadSearchResults({get:async(path,query)=>{calls.push({path,query});throw new Error('recommendation unavailable');}},' ',1,signal()); assert.deepEqual(calls.map(call=>call.path),['playlet/operate/list']);assert.equal(result.keyword,'');assert.deepEqual(result.recommended,[]); const html=searchResultsMarkup(result,()=>assert.fail('empty entry cannot paginate')); assert.ok(html.includes('搜索你想看的短剧'));assert.ok(html.includes('输入剧名、演员或标签'));assert.ok(!html.includes('大家都在看'));assert.ok(!html.includes('recommendation unavailable')); }); test('search failures have their own retry state and never masquerade as zero matches',async()=>{ const calls=[]; await assert.rejects(loadSearchResults({get:async path=>{calls.push(path);throw new Error('搜索服务超时');}},'原名',1,signal()),/搜索服务超时/); assert.deepEqual(calls,['playlet/search']); const html=searchErrorMarkup('