← back to Dw Domain Fleet
scripts/tk11470-sort-proof/proof2.cjs
213 lines
const PW='/Users/macstudio3/Projects/astek-landing/node_modules/playwright';
const { chromium, webkit } = require(PW);
const BASE = process.env.BASE;
const MODES = JSON.parse(process.env.MODES);
const DENSITY_KEY = process.env.DENSITY_KEY || 'grassclothwallcovering_density';
const results = [];
const rec=(n,p,d)=>{results.push({name:n,pass:p,detail:d});console.log((p?'PASS ':'FAIL ')+n+' '+JSON.stringify(d));};
const step=async(n,fn)=>{try{await fn();}catch(e){rec(n,false,{threw:String(e.message||e).split('\n')[0]});}};
async function fresh(browser){ // fresh context = empty localStorage, no cross-test bleed
const c=await browser.newContext({ignoreHTTPSErrors:true});
const p=await c.newPage();
await c.route('**/*',r=>{const t=r.request().resourceType();return (t==='image'||t==='font'||t==='media')?r.abort():r.continue();});
p.on('console',m=>{if(m.type()==='error')(p._errs=p._errs||[]).push(m.text());});
p.on('pageerror',e=>(p._errs=p._errs||[]).push('PAGEERROR '+e.message));
return {c,p};
}
async function go(p,url){ // load + settle, retry on non-200
for(let i=0;i<3;i++){
try{ const r=await p.goto(url,{waitUntil:'domcontentloaded',timeout:25000});
if(r&&r.status()===200){await p.waitForSelector('#sortSel',{state:'attached',timeout:20000});return true;} }
catch(e){}
await p.waitForTimeout(500);
}
throw new Error('go() could not load '+url);
}
const firstSku=p=>p.$eval('.card[data-sku]', e=>e.dataset.sku);
async function run(engine,name){
const browser=await engine.launch();
// 1. each of the 9 sort modes: real navigation + real SERVER ordering + dropdown reflects it
for(const [mode,expect] of Object.entries(MODES)){
await step(`${name}/sort=${mode}`, async()=>{
const {c,p}=await fresh(browser);
await go(p,BASE+'/catalog');
await Promise.all([ p.waitForNavigation({waitUntil:'domcontentloaded',timeout:25000}),
p.selectOption('#sortSel',mode) ]);
await p.waitForSelector('.card[data-sku]',{timeout:20000});
const url=p.url(), sku=await firstSku(p), sel=await p.$eval('#sortSel',e=>e.value);
rec(`${name}/sort=${mode}`, url.includes('sort='+mode)&&sku===expect&&sel===mode,
{url:url.replace(BASE,''),sku,expect,sel});
await c.close();
});
}
// 2. a search query survives a sort change
await step(`${name}/query-retained`, async()=>{
const {c,p}=await fresh(browser);
await go(p,BASE+'/catalog?q=grasscloth');
await Promise.all([p.waitForNavigation({waitUntil:'domcontentloaded',timeout:25000}),p.selectOption('#sortSel','title')]);
rec(`${name}/query-retained`, /q=grasscloth/.test(p.url())&&/sort=title/.test(p.url()), {url:p.url().replace(BASE,'')});
await c.close();
});
// 3. sorting from page 3 returns to page 1
await step(`${name}/page-reset`, async()=>{
const {c,p}=await fresh(browser);
await go(p,BASE+'/catalog?sort=newest&page=3');
await Promise.all([p.waitForNavigation({waitUntil:'domcontentloaded',timeout:25000}),p.selectOption('#sortSel','sku')]);
rec(`${name}/page-reset`, /sort=sku/.test(p.url())&&!/page=[2-9]/.test(p.url()), {url:p.url().replace(BASE,'')});
await c.close();
});
// 4. sort is deliberately NOT remembered (Steve, TK-11470): a later bare /catalog visit
// must come back UNSORTED, with the dropdown honestly showing the default.
await step(`${name}/sort-not-remembered`, async()=>{
const {c,p}=await fresh(browser);
await go(p,BASE+'/catalog');
await Promise.all([p.waitForNavigation({waitUntil:'domcontentloaded',timeout:25000}),p.selectOption('#sortSel','sku')]);
await p.goto(BASE+'/catalog',{waitUntil:'domcontentloaded',timeout:25000});
await p.waitForSelector('.card[data-sku]',{timeout:20000});
await p.waitForTimeout(1500); // give any stray redirect time to fire
const sku=await firstSku(p), sel=await p.$eval('#sortSel',e=>e.value);
rec(`${name}/sort-not-remembered`,
p.url().endsWith('/catalog') && sku===MODES.newest && sel==='newest',
{url:p.url().replace(BASE,''), sku, expectDefault:MODES.newest, sel});
await c.close();
});
// 4b. the dropdown can never label an ordering the grid isn't in (the old cosmetic lie)
await step(`${name}/dropdown-never-lies`, async()=>{
const {c,p}=await fresh(browser);
await go(p,BASE+'/catalog');
await Promise.all([p.waitForNavigation({waitUntil:'domcontentloaded',timeout:25000}),p.selectOption('#sortSel','price-desc')]);
await p.goto(BASE+'/catalog',{waitUntil:'domcontentloaded',timeout:25000});
await p.waitForSelector('.card[data-sku]',{timeout:20000});
await p.waitForTimeout(1200);
const sel=await p.$eval('#sortSel',e=>e.value), sku=await firstSku(p);
const labelled=MODES[sel];
rec(`${name}/dropdown-never-lies`, labelled===sku,
{dropdownSays:sel, gridStartsWith:sku, thatSortWouldStartWith:labelled});
await c.close();
});
// 5. a bare /catalog visit must not redirect AT ALL (exactly one navigation)
await step(`${name}/no-redirect`, async()=>{
const {c,p}=await fresh(browser);
await go(p,BASE+'/catalog');
await Promise.all([p.waitForNavigation({waitUntil:'domcontentloaded',timeout:25000}),p.selectOption('#sortSel','sku')]);
let navs=0; p.on('framenavigated',f=>{if(f===p.mainFrame())navs++;});
await p.goto(BASE+'/catalog',{waitUntil:'domcontentloaded',timeout:25000});
await p.waitForTimeout(2500);
rec(`${name}/no-redirect`, navs===1, {navigations:navs, expect:1});
await c.close();
});
// 6. BACK must genuinely undo a sort — this is the regression that decided the design
await step(`${name}/back-undoes-sort`, async()=>{
const {c,p}=await fresh(browser);
await go(p,BASE+'/catalog');
const before=await firstSku(p);
await Promise.all([p.waitForNavigation({waitUntil:'domcontentloaded',timeout:25000}),p.selectOption('#sortSel','sku')]);
const sorted=await firstSku(p);
await p.goBack({waitUntil:'domcontentloaded',timeout:25000});
await p.waitForSelector('.card[data-sku]',{timeout:20000});
await p.waitForTimeout(1500);
const after=await firstSku(p), sel=await p.$eval('#sortSel',e=>e.value);
rec(`${name}/back-undoes-sort`,
sorted===MODES.sku && after===before && after===MODES.newest && sel==='newest' && p.url().endsWith('/catalog'),
{beforeSort:before, afterSort:sorted, afterBack:after, dropdownAfterBack:sel, url:p.url().replace(BASE,'')});
await c.close();
});
// 7. saved density: TWO-SIDED. A one-sided "99 was not applied" would also pass if the
// key were wrong and nothing was ever read at all — a green it did not earn. So first
// prove a VALID value IS applied (read path + key name are real), then prove out-of-range
// values are ignored.
await step(`${name}/density-clamped`, async()=>{
const probe=async(val)=>{
const {c,p}=await fresh(browser);
await go(p,BASE+'/catalog');
await p.evaluate(([k,v])=>{try{localStorage.setItem(k,v);}catch(e){}},[DENSITY_KEY,val]);
await go(p,BASE+'/catalog');
await p.waitForTimeout(600);
const cols=await p.$eval('#grid',e=>getComputedStyle(e).getPropertyValue('--cols').trim());
await c.close(); return cols;
};
const valid=await probe('7'), high=await probe('99'), low=await probe('1');
// baseline (no guard) lets the browser clamp 99 -> "8"; the guard rejects to default "5".
rec(`${name}/density-clamped`,
valid==='7' && high==='5' && low==='5',
{valid_7_applied:valid, invalid_99:high, invalid_1:low, note:'baseline yields 8 here'});
});
// 8. DOM clobbering: query params literally named submit / appendChild
await step(`${name}/dom-clobber-survived`, async()=>{
const {c,p}=await fresh(browser);
await go(p,BASE+'/catalog?submit=x&appendChild=y');
await Promise.all([p.waitForNavigation({waitUntil:'domcontentloaded',timeout:25000}),p.selectOption('#sortSel','title')]);
await p.waitForSelector('.card[data-sku]',{timeout:20000});
const sku=await firstSku(p);
rec(`${name}/dom-clobber-survived`, /sort=title/.test(p.url())&&sku===MODES.title, {url:p.url().replace(BASE,''),sku,expect:MODES.title});
await c.close();
});
// 9. denied localStorage (private mode / blocked storage) must not break sorting
await step(`${name}/denied-storage-still-sorts`, async()=>{
const c=await browser.newContext({ignoreHTTPSErrors:true});
await c.route('**/*',r=>{const t=r.request().resourceType();return (t==='image'||t==='font'||t==='media')?r.abort():r.continue();});
await c.addInitScript(()=>{Object.defineProperty(window,'localStorage',{configurable:true,get(){throw new Error('denied');}});});
const p=await c.newPage();
await go(p,BASE+'/catalog');
await Promise.all([p.waitForNavigation({waitUntil:'domcontentloaded',timeout:25000}),p.selectOption('#sortSel','sku')]);
await p.waitForSelector('.card[data-sku]',{timeout:20000});
const sku=await firstSku(p);
rec(`${name}/denied-storage-still-sorts`, /sort=sku/.test(p.url())&&sku===MODES.sku, {url:p.url().replace(BASE,''),sku,expect:MODES.sku});
await c.close();
});
// 10. zero CSP violations on the catalog page
await step(`${name}/no-csp-violations`, async()=>{
const {c,p}=await fresh(browser);
await go(p,BASE+'/catalog');
await Promise.all([p.waitForNavigation({waitUntil:'domcontentloaded',timeout:25000}),p.selectOption('#sortSel','sku')]);
await p.waitForTimeout(1200);
const csp=(p._errs||[]).filter(e=>/Content Security Policy|script-src-attr/i.test(e));
rec(`${name}/no-csp-violations`, csp.length===0, {csp:csp.slice(0,2), totalErrs:(p._errs||[]).length});
await c.close();
});
// 11. OLD-SAFARI SIMULATION: requestSubmit is Safari 16+. Remove it and the sort must
// still work via the submit() fallback — otherwise the patch silently reintroduces
// the very dead-dropdown bug it fixes, on exactly the browsers we cannot test natively.
await step(`${name}/legacy-no-requestSubmit`, async()=>{
const c=await browser.newContext({ignoreHTTPSErrors:true});
await c.route('**/*',r=>{const t=r.request().resourceType();return (t==='image'||t==='font'||t==='media')?r.abort():r.continue();});
await c.addInitScript(()=>{ try{ delete HTMLFormElement.prototype.requestSubmit; }catch(e){} });
const p=await c.newPage();
const perr=[]; p.on('pageerror',e=>perr.push(e.message));
await go(p,BASE+'/catalog');
const gone=await p.evaluate(()=>typeof HTMLFormElement.prototype.requestSubmit);
await Promise.all([p.waitForNavigation({waitUntil:'domcontentloaded',timeout:25000}),p.selectOption('#sortSel','sku')]);
await p.waitForSelector('.card[data-sku]',{timeout:20000});
const sku=await firstSku(p);
rec(`${name}/legacy-no-requestSubmit`,
gone==='undefined' && /sort=sku/.test(p.url()) && sku===MODES.sku && perr.length===0,
{requestSubmitTypeof:gone, url:p.url().replace(BASE,''), sku, expect:MODES.sku, pageerrors:perr.slice(0,2)});
await c.close();
});
await browser.close();
}
(async()=>{
await run(chromium,'chromium');
await run(webkit,'webkit');
const fail=results.filter(r=>!r.pass);
console.log('\n==== '+(results.length-fail.length)+' PASS / '+fail.length+' FAIL of '+results.length+' ====');
require('fs').writeFileSync(process.env.OUT||'/tmp/tk11470-proof/result.json',
JSON.stringify({base:BASE,pass:results.length-fail.length,fail:fail.length,results},null,2));
process.exit(fail.length?1:0);
})();