← 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);
})();