← back to Commercialrealestate
5x/index-assert.js
69 lines
// Strong index.html gate (primary property viewer). Tests the REAL DOM (verified by probe):
// • 1407 ranked cards under .grid render, NO NaN/$NaN, count text present, LoopNet + rent lines present
// • changing #sort reorders the cards (first card changes)
// • clicking a filter .chip narrows the result set
// • empty data (mocked ranked.json/listings.json) degrades: 0 cards, no NaN, 0 swallowed JS errors
const pw=require('/Users/macstudio3/.claude/skills/browserbase/node_modules/playwright-core');
const EXEC='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
async function run(mockEmpty){
const errors=[]; const b=await pw.chromium.launch({executablePath:EXEC,headless:true});
const pg=await b.newPage({viewport:{width:1320,height:1000}});
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());});
if(mockEmpty){
await pg.route('**/ranked.json',r=>r.fulfill({status:200,contentType:'application/json',body:'{"ranked":[]}'}));
await pg.route('**/listings.json',r=>r.fulfill({status:200,contentType:'application/json',body:'{"listings":[]}'}));
}
await pg.goto('http://127.0.0.1:9911/index.html',{waitUntil:'networkidle'}); await pg.waitForTimeout(2000);
const base=await pg.evaluate(()=>({
cards:document.querySelectorAll('.grid .card').length,
anyNaN:/NaN|\$NaN|undefined/.test([...document.querySelectorAll('.grid .card')].slice(0,40).map(c=>c.textContent).join(' ')),
loopnet:document.querySelectorAll('.src a[href*="loopnet"], .src a[href*="site:loopnet"]').length,
rentLines:[...document.querySelectorAll('.card')].filter(c=>/Est\. rent/.test(c.textContent)).length,
// Deal Score: badges render, every shown score is a finite 0-100, scores compute on the row,
// and the breakdown tooltip explains the sub-scores (transparent, not a black box).
dealBadges:document.querySelectorAll('.deal-badge').length,
dealScoresOk:(()=>{ const ds=(DATA&&DATA.ranked||[]).map(p=>p.deal).filter(d=>d&&d.score!=null);
return { scored:ds.length, allFinite:ds.every(d=>isFinite(d.score)&&d.score>=0&&d.score<=100),
hasParts:ds.every(d=>Array.isArray(d.parts)&&d.parts.length>0), anyNaN:ds.some(d=>!isFinite(d.score)) }; })(),
dealBadgeNaN:[...document.querySelectorAll('.deal-badge')].some(b=>/NaN|undefined/.test(b.textContent)),
dealTipOk:/Deal Score \d+\/100/.test(document.querySelector('.deal-badge')?.getAttribute('title')||''),
countTxt:(document.querySelector('.bar .count')?.textContent||'').replace(/\s+/g,' ').trim() }));
let sortOk=null, filterOk=null, dealSortOk=null;
if(!mockEmpty){
sortOk=await pg.evaluate(async()=>{ const sel=document.querySelector('#sort'); if(!sel||sel.options.length<2) return null;
const first=()=>document.querySelector('.grid .card .addr')?.textContent||document.querySelector('.grid .card')?.textContent.slice(0,40);
const before=first();
// pick a sort guaranteed to reorder (Price ↑) rather than the next index — robust to option order.
sel.value=[...sel.options].some(o=>o.value==='priceAsc')?'priceAsc':sel.options[(sel.selectedIndex+1)%sel.options.length].value;
sel.dispatchEvent(new Event('change'));
await new Promise(r=>setTimeout(r,500)); return { before, after:first(), changed:before!==first() }; });
filterOk=await pg.evaluate(async()=>{ const chip=document.querySelector('.chip'); if(!chip) return null;
const n=()=>document.querySelectorAll('.grid .card').length; const before=n();
chip.click(); await new Promise(r=>setTimeout(r,500)); const after=n();
return { before, after, narrowed: after<before && after>=0 }; });
// "Deal Score ↓" sort: selecting it orders the visible cards by descending score (first >= second).
dealSortOk=await pg.evaluate(async()=>{ const sel=document.querySelector('#sort'); if(!sel) return null;
sel.value='deal'; sel.dispatchEvent(new Event('change')); await new Promise(r=>setTimeout(r,500));
const score=c=>{ const m=(c.querySelector('.deal-badge')?.textContent||'').match(/Deal (\d+)/); return m?+m[1]:null; };
const cards=[...document.querySelectorAll('.grid .card')].slice(0,30);
const scs=cards.map(score).filter(s=>s!=null);
let desc=true; for(let i=1;i<scs.length;i++) if(scs[i]>scs[i-1]) desc=false;
return { sampled:scs.length, first:scs[0], descending:desc, optionExists:[...sel.options].some(o=>o.value==='deal') }; });
}
await b.close();
return { mockEmpty, ...base, sortOk, filterOk, dealSortOk, jsErrors:errors };
}
(async()=>{
const normal=await run(false); const empty=await run(true);
const passNormal = normal.cards>0 && !normal.anyNaN && normal.loopnet>0 && normal.rentLines>0
&& (!normal.sortOk||normal.sortOk.changed) && (!normal.filterOk||normal.filterOk.narrowed)
&& normal.dealBadges>0 && !normal.dealBadgeNaN && normal.dealTipOk
&& normal.dealScoresOk.scored>0 && normal.dealScoresOk.allFinite && normal.dealScoresOk.hasParts && !normal.dealScoresOk.anyNaN
&& normal.dealSortOk && normal.dealSortOk.optionExists && normal.dealSortOk.descending && normal.dealSortOk.sampled>1
&& normal.jsErrors.length===0;
const passEmpty = empty.cards===0 && !empty.anyNaN && empty.jsErrors.length===0;
console.log(JSON.stringify({normal,empty,passNormal,passEmpty,VERDICT:(passNormal&&passEmpty)?'PASS':'FAIL'},null,2));
process.exit((passNormal&&passEmpty)?0:1);
})().catch(e=>{console.error('FAIL',e.message);process.exit(1);});