94 lines
6.5 KiB
JavaScript
94 lines
6.5 KiB
JavaScript
import {linkShared} from './helpers/shared-modules.mjs';
|
|
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(linkShared(replace(outputText)).replace(/(['"])\.\.\/routing\1/g,JSON.stringify(routingModuleUrl))+'\n//# sourceURL='+filename.pathname).toString('base64');
|
|
}
|
|
const routingUrl=await moduleUrl('../src/lib/routing.ts');
|
|
const {routeFromSegments,parseSitePath,paginationPath,internalPagePath,searchPath}=await import(routingUrl);
|
|
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 {pagination}=await import(renderUrl);
|
|
const {bindPaginationJumps}=await import(await moduleUrl('../src/lib/client/navigation.ts'));
|
|
|
|
test('every paginated page type resolves directly from the page suffix',()=>{
|
|
const keyword='都市 / page/7/?#%2F';
|
|
const bases=['/shorts/','/topics/','/topics/7/','/theme/8/','/tag/9/','/config/31/','/rank/','/rank/hot/','/rank/new/','/rank/follow/','/search/'+encodeURIComponent(keyword)+'/'];
|
|
for(const base of bases){
|
|
for(const page of [1,2,37]){
|
|
const path=base+'page/'+page+'/';
|
|
assert.deepEqual(parseSitePath(path),{...parseSitePath(base),page},path);
|
|
assert.deepEqual(parseSitePath(path.slice(0,-1)),parseSitePath(path),path);
|
|
assert.equal(internalPagePath(path,'https://drama.example/'),path,path);
|
|
assert.equal(paginationPath(path,1),base.startsWith('/search/')?searchPath(keyword):base,path);
|
|
}
|
|
}
|
|
assert.deepEqual(routeFromSegments(['search',keyword,'page','3']),{group:'search',key:keyword,page:3});
|
|
});
|
|
|
|
test('invalid page numbers, nested page suffixes, and non-paginated sections are rejected',()=>{
|
|
for(const value of ['0','-1','1.5','02','NaN','9007199254740992'])assert.equal(parseSitePath(`/shorts/page/${value}/`),null,value);
|
|
for(const base of ['/','/apps/','/home-road/','/navigation/','/info/about-us/','/detail/23456/','/play/23456/90001/'])assert.equal(parseSitePath(base+'page/2/'),null,base);
|
|
for(const base of ['/member/center/','/member/favorites/','/info/deposit/','/info/feedback/'])assert.equal(parseSitePath(base),null,base);
|
|
assert.equal(parseSitePath('/shorts/page/2/page/3/'),null);
|
|
assert.throws(()=>paginationPath('/shorts/',0));assert.throws(()=>paginationPath('/apps/',2));
|
|
});
|
|
|
|
test('page links retain filters and encoded search terms and sorting resets to the first page',()=>{
|
|
assert.equal(paginationPath('/shorts/page/4/?sort=3&source=menu',2),'/shorts/page/2/?sort=3&source=menu');
|
|
assert.equal(paginationPath('/shorts/page/4/?sort=3&source=menu',1,{sort:2}),'/shorts/?sort=2&source=menu');
|
|
assert.equal(paginationPath('/shorts/page/4/?sort=3',1,{sort:null}),'/shorts/');
|
|
const base='/search/'+encodeURIComponent('都市 / 爱情?#%')+'/';
|
|
assert.equal(paginationPath(base,2),searchPath('都市 / 爱情?#%',2));
|
|
const url=new URL(paginationPath(base,2),'https://drama.example');
|
|
assert.equal(parseSitePath(url.pathname,url.search).key,'都市 / 爱情?#%');
|
|
});
|
|
|
|
test('pagination renders real page links with correct boundaries, current page, and bounded page numbers',()=>{
|
|
const href=page=>paginationPath('/rank/new/page/5/?sort=2&from=menu',page);
|
|
const html=pagination(240,5,href);
|
|
assert.ok(html.includes('href="/rank/new/page/4/?sort=2&from=menu" rel="prev"'));
|
|
assert.ok(html.includes('href="/rank/new/page/6/?sort=2&from=menu" rel="next"'));
|
|
assert.ok(html.includes('href="/rank/new/page/5/?sort=2&from=menu" aria-current="page"'));
|
|
assert.equal((html.match(/<button/g)||[]).length,1);assert.ok(html.includes('<button type="submit">确定</button>'));assert.ok(!html.includes('data-page-number'));
|
|
assert.ok(html.includes('data-page-base="/rank/new/?sort=2&from=menu"'));
|
|
assert.ok(html.includes('name="page" min="1" max="10" step="1"'));
|
|
assert.ok((html.match(/<a /g)||[]).length<=7);
|
|
assert.ok(pagination(240,1,href).includes('aria-disabled="true" aria-label="上一页"'));
|
|
assert.ok(pagination(240,10,href).includes('aria-disabled="true" aria-label="下一页"'));
|
|
assert.equal(pagination(0,1,href),'');assert.equal(pagination(24,1,href),'');
|
|
assert.ok(pagination(0,9,href).includes('href="/rank/new/?sort=2&from=menu"'));
|
|
});
|
|
|
|
test('jump forms navigate to independent routes, retain canonical slugs and filters, and reject invalid pages',()=>{
|
|
const previousForm=globalThis.HTMLFormElement,previousInput=globalThis.HTMLInputElement;
|
|
class Input {valueAsNumber=3;max='10';}
|
|
class Form {dataset={pageBase:'/topics/sweet-love/?sort=2&source=menu'};input=new Input();valid=true;elements={namedItem:()=>this.input};matches=()=>true;reportValidity=()=>this.valid;}
|
|
globalThis.HTMLFormElement=Form;globalThis.HTMLInputElement=Input;
|
|
try{
|
|
let submit;const paths=[];
|
|
bindPaginationJumps({addEventListener:(type,handler)=>{assert.equal(type,'submit');submit=handler;}},path=>paths.push(path));
|
|
const form=new Form();let prevented=0;
|
|
const send=()=>submit({target:form,preventDefault(){prevented++;}});
|
|
send();assert.equal(paths.at(-1),'/topics/sweet-love/page/3/?sort=2&source=menu');
|
|
form.input.valueAsNumber=1;send();assert.equal(paths.at(-1),'/topics/sweet-love/?sort=2&source=menu');
|
|
form.dataset.pageBase=searchPath('都市 / 爱情');form.input.valueAsNumber=2;send();assert.equal(paths.at(-1),searchPath('都市 / 爱情',2));
|
|
const count=paths.length;
|
|
for(const value of [NaN,0,-1,1.5,11,Infinity,Number.MAX_SAFE_INTEGER+1]){form.input.valueAsNumber=value;send();}
|
|
form.input.valueAsNumber=2;form.valid=false;send();assert.equal(paths.length,count);
|
|
assert.equal(prevented,11);
|
|
// The delegated listener works for forms added after it was registered.
|
|
const added=new Form();submit({target:added,preventDefault(){}});assert.equal(paths.length,count+1);
|
|
submit({target:{},preventDefault(){assert.fail('unrelated form');}});
|
|
}finally{
|
|
if(previousForm===undefined)delete globalThis.HTMLFormElement;else globalThis.HTMLFormElement=previousForm;
|
|
if(previousInput===undefined)delete globalThis.HTMLInputElement;else globalThis.HTMLInputElement=previousInput;
|
|
}
|
|
});
|