← back to Commercialrealestate

5x/tk10703-direct-listings-assert.js

80 lines

// TK-10703 gate for direct-listings.html:
//  HTTP 200 · 0 swallowed JS errors · view toggle switches views · sort reorders rows ·
//  tel:/mailto: counts BOTH > 0 (real contacts via /api/listing-brokers address-join) ·
//  BOTH light + dark theme flip surfaces (no hardcoded dark literals).
const pw=require('/Users/macstudio3/.claude/skills/browserbase/node_modules/playwright-core');
const EXEC='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
const SID=process.argv[2];
const URL='http://127.0.0.1:9911/direct-listings.html';
async function run(theme){
  const errors=[]; const b=await pw.chromium.launch({executablePath:EXEC,headless:true});
  const ctx=await b.newContext({viewport:{width:1360,height:1000}});
  await ctx.addCookies([{name:'crcp_sid',value:SID,domain:'127.0.0.1',path:'/'}]);
  const pg=await ctx.newPage();
  pg.on('pageerror',e=>errors.push('PAGEERR: '+e.message));
  pg.on('console',m=>{if(m.type()==='error'&&!/Failed to load resource/.test(m.text()))errors.push('CONSOLE: '+m.text());});
  // set theme BEFORE load
  await pg.addInitScript(t=>{try{localStorage.setItem('crcp-theme',t);}catch(e){}},theme);
  const resp=await pg.goto(URL,{waitUntil:'networkidle'});
  const http=resp.status();
  await pg.waitForTimeout(1800);
  const base=await pg.evaluate(()=>{
    const bodyBg=getComputedStyle(document.body).backgroundColor;
    const cardEl=document.querySelector('.gcard,.tablewrap');
    const cardBg=cardEl?getComputedStyle(cardEl).backgroundColor:'';
    return {
      theme:document.documentElement.getAttribute('data-theme'),
      bodyBg, cardBg,
      tel:document.querySelectorAll('a[href^="tel:"]').length,
      mailto:document.querySelectorAll('a[href^="mailto:"]').length,
      tableRows:document.querySelectorAll('#tbody tr').length,
      gridCards:document.querySelectorAll('#gridView .gcard').length,
      seg:document.querySelectorAll('[data-tvseg] button, #viewseg button').length,
      sortOpts:document.querySelectorAll('#tvSort option').length,
      stats:(document.querySelector('#stats')?.textContent||'').length,
      anyNaN:/NaN|undefined/.test((document.querySelector('#tbody')?.textContent||'')+(document.querySelector('#stats')?.textContent||'')),
    };
  });
  // view toggle: click through the segmented buttons, confirm each view becomes visible
  const viewSwitch=await pg.evaluate(async()=>{
    const btns=[...document.querySelectorAll('#viewseg button,[data-tvseg] button')];
    const vis=el=>el&&getComputedStyle(el).display!=='none'&&el.offsetHeight>0;
    const seen={};
    for(const btn of btns){
      btn.click(); await new Promise(r=>setTimeout(r,350));
      const label=(btn.textContent||'').trim().toLowerCase();
      seen[label]={grid:vis(document.querySelector('#gridView')),list:vis(document.querySelector('#tvList')),table:vis(document.querySelector('#tableView'))};
    }
    return seen;
  });
  // sort reorder: switch to table, capture first address, change sort select, confirm order changed
  const sortReorder=await pg.evaluate(async()=>{
    const seg=[...document.querySelectorAll('#viewseg button,[data-tvseg] button')].find(b=>/table/i.test(b.textContent));
    if(seg){seg.click();await new Promise(r=>setTimeout(r,300));}
    const firstAddr=()=>document.querySelector('#tbody tr td')?.textContent||'';
    const before=firstAddr();
    const sel=document.querySelector('#tvSort'); if(!sel||sel.options.length<2) return {changed:false,before,after:before,note:'no sort select'};
    // pick a different option than current
    const cur=sel.value; let target=null;
    for(const o of sel.options){ if(o.value!==cur){target=o.value;break;} }
    sel.value=target; sel.dispatchEvent(new Event('change',{bubbles:true}));
    await new Promise(r=>setTimeout(r,400));
    const after=firstAddr();
    return {changed:before!==after,before,after};
  });
  await b.close();
  return {theme,http,...base,viewSwitch,sortReorder,jsErrors:errors};
}
(async()=>{
  const dark=await run('dark'); const light=await run('light');
  // theme flip: body bg must differ between themes AND card bg must differ (surfaces actually flip)
  const themeFlips = dark.bodyBg!==light.bodyBg && dark.cardBg!==light.cardBg;
  const viewOk=r=>{const s=r.viewSwitch;const g=Object.entries(s).find(([k])=>/grid/.test(k)),l=Object.entries(s).find(([k])=>/list/.test(k)),t=Object.entries(s).find(([k])=>/table/.test(k));return !!(g&&g[1].grid && l&&l[1].list && t&&t[1].table);};
  const pass=r=>r.http===200 && r.jsErrors.length===0 && !r.anyNaN && r.tel>0 && r.mailto>0
    && (r.tableRows>0||r.gridCards>0) && r.seg>=3 && r.sortOpts>1 && viewOk(r) && r.sortReorder.changed;
  const passDark=pass(dark), passLight=pass(light);
  const VERDICT=(passDark&&passLight&&themeFlips)?'PASS':'FAIL';
  console.log(JSON.stringify({darkSummary:{http:dark.http,tel:dark.tel,mailto:dark.mailto,jsErr:dark.jsErrors.length,bodyBg:dark.bodyBg,cardBg:dark.cardBg},lightSummary:{http:light.http,tel:light.tel,mailto:light.mailto,jsErr:light.jsErrors.length,bodyBg:light.bodyBg,cardBg:light.cardBg},dark,light,themeFlips,passDark,passLight,VERDICT},null,2));
  process.exit(VERDICT==='PASS'?0:1);
})().catch(e=>{console.error('FAIL',e.message,e.stack);process.exit(1);});