← 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
A 5x/tk10703-direct-listings-assert.jsA 5x/tk10703-linkedin-assert.js
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 →