[object Object]

← back to Commercialrealestate

TK-10703 cycle7: commit the direct-listings + linkedin verification harnesses (consistent with existing 5x/*-assert.js)

0cff5ace8633795b0ab6ee87931d09b9ba2cac41 · 2026-08-21 07:07:46 -0700 · Steve Abrams

Files touched

Diff

commit 0cff5ace8633795b0ab6ee87931d09b9ba2cac41
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 21 07:07:46 2026 -0700

    TK-10703 cycle7: commit the direct-listings + linkedin verification harnesses (consistent with existing 5x/*-assert.js)
---
 5x/tk10703-direct-listings-assert.js | 79 ++++++++++++++++++++++++++++++++++++
 5x/tk10703-linkedin-assert.js        | 75 ++++++++++++++++++++++++++++++++++
 2 files changed, 154 insertions(+)

diff --git a/5x/tk10703-direct-listings-assert.js b/5x/tk10703-direct-listings-assert.js
new file mode 100644
index 0000000..57face1
--- /dev/null
+++ b/5x/tk10703-direct-listings-assert.js
@@ -0,0 +1,79 @@
+// 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);});
diff --git a/5x/tk10703-linkedin-assert.js b/5x/tk10703-linkedin-assert.js
new file mode 100644
index 0000000..3f1cb29
--- /dev/null
+++ b/5x/tk10703-linkedin-assert.js
@@ -0,0 +1,75 @@
+// TK-10703 gate for linkedin.html:
+//  HTTP 200 · 0 swallowed JS errors · view toggle (List↔Grid) switches layout · sort reorders rows ·
+//  tel:/mailto: counts > 0 (firm-confirmed contacts via /api/brokers/all id/name+firm join) ·
+//  BOTH light + dark theme flip surfaces.
+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/linkedin.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());});
+  await pg.addInitScript(t=>{try{localStorage.setItem('crcp-theme',t);localStorage.setItem('liView','list');}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 rowEl=document.querySelector('.row');
+    const rowBg=rowEl?getComputedStyle(rowEl).backgroundColor:'';
+    return {
+      theme:document.documentElement.getAttribute('data-theme'),
+      bodyBg, rowBg,
+      tel:document.querySelectorAll('a[href^="tel:"]').length,
+      mailto:document.querySelectorAll('a[href^="mailto:"]').length,
+      rows:document.querySelectorAll('#list .row').length,
+      contactStrips:document.querySelectorAll('#list .row .contact').length,
+      sortOpts:document.querySelectorAll('#sortsel option').length,
+      viewBtns:document.querySelectorAll('#viewseg button').length,
+      anyNaN:/NaN|undefined/.test((document.querySelector('#list')?.textContent||'')+(document.querySelector('#count')?.textContent||'')),
+    };
+  });
+  // view toggle: switch to grid, confirm #list gets .grid class (layout changes), back to list
+  const viewSwitch=await pg.evaluate(async()=>{
+    const list=document.querySelector('#list');
+    const gridBtn=[...document.querySelectorAll('#viewseg button')].find(b=>/grid/i.test(b.textContent));
+    const listBtn=[...document.querySelectorAll('#viewseg button')].find(b=>/list/i.test(b.textContent));
+    if(gridBtn){gridBtn.click();await new Promise(r=>setTimeout(r,250));}
+    const isGrid=list.classList.contains('grid') && getComputedStyle(list).display==='grid';
+    if(listBtn){listBtn.click();await new Promise(r=>setTimeout(r,250));}
+    const backList=!list.classList.contains('grid') && getComputedStyle(list).display==='flex';
+    return {isGrid,backList};
+  });
+  // sort reorder: capture first name, change sort select to a different option, confirm reorder
+  const sortReorder=await pg.evaluate(async()=>{
+    const firstName=()=>document.querySelector('#list .row .nm')?.textContent||'';
+    const before=firstName();
+    const sel=document.querySelector('#sortsel'); if(!sel||sel.options.length<2) return {changed:false,before,after:before,note:'no sort select'};
+    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,350));
+    const after=firstName();
+    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');
+  const themeFlips = dark.bodyBg!==light.bodyBg && dark.rowBg!==light.rowBg;
+  const pass=r=>r.http===200 && r.jsErrors.length===0 && !r.anyNaN && r.tel>0 && r.mailto>0
+    && r.rows>0 && r.contactStrips>0 && r.sortOpts>1 && r.viewBtns>=2
+    && r.viewSwitch.isGrid && r.viewSwitch.backList && 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,contactStrips:dark.contactStrips,view:dark.viewSwitch,sort:dark.sortReorder,bodyBg:dark.bodyBg,rowBg:dark.rowBg},
+    lightSummary:{http:light.http,tel:light.tel,mailto:light.mailto,jsErr:light.jsErrors.length,bodyBg:light.bodyBg,rowBg:light.rowBg},
+    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);});

← 1d2233c TK-10703 cycle7: contrarian-gate fixes — robust address norm  ·  back to Commercialrealestate  ·  auto-data-snapshot: 2026-08-21T07:10:15 (1 data files) — dat 8bef3f6 →