← back to Commercialrealestate

5x/deals-assert.js

53 lines

// Strong deals.html gate (what /3x's --selector cannot do): assert the 6 charts actually drew DATA,
// no tile renders NaN/$NaN, and the EMPTY-DATA path degrades without crashing or showing NaN.
const pw = require('/Users/macstudio3/.claude/skills/browserbase/node_modules/playwright-core');
const EXEC = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
const CHARTS = ['cPipe','cType','cCap','cMed','cGeo','cBand'];

async function run(label, 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('PAGEERROR: '+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('**/api/segments', r=>r.fulfill({status:200,contentType:'application/json',body:'{"segments":[]}'}));
    await pg.route('**/api/crcp/stats', r=>r.fulfill({status:200,contentType:'application/json',body:'{}'}));
    await pg.route('**/data/ranked.json', r=>r.fulfill({status:200,contentType:'application/json',body:'{"ranked":[]}'}));
    await pg.route('**/data/map-points.json', r=>r.fulfill({status:200,contentType:'application/json',body:'{"points":[]}'}));
    await pg.route('**/data/demographics.json', r=>r.fulfill({status:200,contentType:'application/json',body:'{"byCity":{}}'}));
  }
  await pg.goto('http://127.0.0.1:9911/deals.html', { waitUntil:'networkidle' });
  await pg.waitForTimeout(2200);
  const r = await pg.evaluate((CHARTS)=>{
    const charts = CHARTS.map(id=>{ const c=Chart.getChart(id); return { id, exists:!!c, points: c?(c.data.datasets[0]?.data?.length||0):0 }; });
    const tiles = [...document.querySelectorAll('.tile .v')].map(v=>v.textContent);
    const nanTiles = tiles.filter(t=>/nan|undefined/i.test(t));
    // Top Deals by Unified Score: rows render, each score is a finite 0-100, in descending order, no NaN.
    const tdRows = [...document.querySelectorAll('#tdlist .tdrow')];
    const tdScores = tdRows.map(r=>{ const m=(r.querySelector('.sc')?.textContent||'').match(/(\d+)/); return m?+m[1]:null; }).filter(s=>s!=null);
    let tdDesc=true; for(let i=1;i<tdScores.length;i++) if(tdScores[i]>tdScores[i-1]) tdDesc=false;
    const td = { rows:tdRows.length, scores:tdScores.length, descending:tdDesc,
      allFinite:tdScores.every(s=>isFinite(s)&&s>=0&&s<=100),
      anyNaN:/NaN|\$NaN|undefined/.test(document.querySelector('#topdeals')?.textContent||''),
      panelPresent:!!document.querySelector('#topdeals') };
    return { charts, tiles, nanTiles, td };
  }, CHARTS);
  await b.close();
  const chartsWithData = r.charts.filter(c=>c.points>0).length;
  return { label, ...r, chartsWithData, jsErrors:errors };
}

(async()=>{
  const normal = await run('NORMAL', false);
  const empty  = await run('EMPTY-DATA', true);
  // Pass criteria:
  const passNormal = normal.charts.every(c=>c.exists) && normal.chartsWithData>=5 && normal.nanTiles.length===0
    && normal.td.panelPresent && normal.td.rows>0 && normal.td.scores>1 && normal.td.allFinite && normal.td.descending && !normal.td.anyNaN
    && normal.jsErrors.length===0;
  // empty: top-deals panel must degrade to 0 rows (or a "no scorable deals" note) with NO NaN, no crash.
  const passEmpty  = empty.nanTiles.length===0 && !empty.td.anyNaN && empty.td.panelPresent && 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); });