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