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