38 lines
5.0 KiB
HTML
38 lines
5.0 KiB
HTML
<!doctype html>
|
|
<meta charset="utf-8">
|
|
<title>ProspectOS smoke test</title>
|
|
<style>body{font:16px system-ui;padding:2rem;background:#f7f8fb;color:#172033}li{margin:.5rem 0}.pass{color:#16845b}.fail{color:#b84d55}</style>
|
|
<h1>ProspectOS static smoke test</h1><p id="summary">Running…</p><ul id="checks"></ul>
|
|
<iframe id="app" src="index.html" hidden></iframe>
|
|
<script>
|
|
const frame=document.querySelector('#app');
|
|
frame.onload=async()=>{const d=frame.contentDocument;const js=await fetch('app.js').then(r=>r.text());const checks=[
|
|
['Login screen',()=>!!d.querySelector('#loginScreen')],
|
|
['Email/password login fields',()=>!!d.querySelector('#loginEmail')&&!!d.querySelector('#loginPassword')],
|
|
['Dashboard starts protected',()=>d.querySelector('#dashboardShell').hidden],
|
|
['Logout and user display',()=>!!d.querySelector('#logoutBtn')&&!!d.querySelector('#userIdentity')],
|
|
['Explorer pagination and filters',()=>!!d.querySelector('#pageSize')&&!!d.querySelector('#nextPageBtn')&&!!d.querySelector('#websiteClassFilter')&&!!d.querySelector('#pipelineFilter')],
|
|
['Detail panel contract',()=>!!d.querySelector('#detailPanel')&&js.includes('/api/v1/businesses/${encodeURIComponent(id)}')&&js.includes('contacts')&&js.includes('evidence_timeline')&&js.includes('pipeline_stage')],
|
|
['Manual detail controls',()=>js.includes('contactForm')&&js.includes('noteForm')&&js.includes('pipelineForm')&&js.includes('verifyBtn')],
|
|
['All API requests include cookies',()=>[...js.matchAll(/fetch\([^;]+/g)].every(m=>m[0].includes("credentials:'include'"))],
|
|
['Detail writes use authenticated request helper',()=>['/contacts','/notes','/pipeline','/verify'].every(path=>js.includes(path)&&js.includes('jsonRequest'))],
|
|
['Validation and error states',()=>js.includes('Email is required.')&&js.includes('Note cannot be empty.')&&js.includes('detail-error')&&js.includes('role="alert"')],
|
|
['No hardcoded credentials or demo fallback',()=>!js.includes('demoProspects')&&!/password.{0,30}['\"][^'\"]+['\"]/.test(js)],
|
|
['Safety copy and blocked outreach preserved',()=>js.includes('disabled-action')&&js.includes('Suppressed records cannot be contacted.')&&js.includes('Review this prospect before outreach is available.')],
|
|
['No outreach/send controls',()=>!Array.from(d.querySelectorAll('button')).some(x=>/outreach|send/i.test(x.textContent)&&!x.disabled)]
|
|
,['Jobs navigation and monitor',()=>!!d.querySelector('[data-nav="jobs"]')&&!!d.querySelector('#jobs')&&!!d.querySelector('#jobsList')]
|
|
,['Job status counts and controls',()=>['jobCountQueued','jobCountRunning','jobCountSucceeded','jobCountFailed','jobCountCancelled','startDemoJobBtn','jobsRefreshBtn'].every(id=>!!d.querySelector('#'+id))]
|
|
,['Authenticated jobs API contract',()=>js.includes("/api/v1/jobs")&&js.includes('jobsRequest')&&js.includes('/cancel')&&js.includes('/retry')]
|
|
,['Job detail timeline and structured states',()=>!!d.querySelector('#jobDetailPanel')&&js.includes('event_timeline')&&js.includes('structured_error')&&js.includes('progress')]
|
|
,['Sources navigation and safety boundary',()=>!!d.querySelector('[data-nav="sources"]')&&!!d.querySelector('#sources')&&js.includes('No live source is enabled')]
|
|
,['Source registry status and governance fields',()=>!!d.querySelector('#sourcesList')&&['owner','terms','rate_limit','health'].every(x=>js.includes(x))&&js.includes('/api/v1/sources')]
|
|
,['Source configuration and controls use authenticated helper',()=>!!d.querySelector('#sourceForm')&&!!d.querySelector('#sourceType')&&js.includes('saveSource')&&js.includes('/test')&&js.includes('method:\'PATCH\'')&&js.includes('jsonRequest')]
|
|
,['Discovery dry-run and run controls',()=>!!d.querySelector('#discoveryForm')&&!!d.querySelector('#discoveryDryRunBtn')&&!!d.querySelector('#discoveryRunBtn')&&js.includes('/api/v1/sources/discovery')&&js.includes('dry_run')]
|
|
,['Recent source records and error/loading states',()=>!!d.querySelector('#sourceRecordsList')&&js.includes('source-record-table')&&js.includes('Loading source registry')&&js.includes('Unable to load sources')]
|
|
,['No live source enabled copy is explicit',()=>d.querySelector('#sources')?.textContent.includes('No live source is enabled')&&!js.includes('demoSources')]
|
|
,['Deduplication review UI contract',()=>!!d.querySelector('#mergeDialog')&&js.includes('/matches')&&js.includes('review-required')&&js.includes('confidence')&&js.includes('reasons')]
|
|
,['Merge actions are explicit and reversible',()=>js.includes('/merge-history')&&js.includes('/reverse')&&js.includes('This action is reversible')&&js.includes('Confirm merge')&&!js.includes('autoMerge')]
|
|
,['Deduplication loading and errors',()=>js.includes('Loading match suggestions')&&js.includes('Unable to load match suggestions')&&js.includes('merge-history')&&js.includes('Unable to load merge history')]
|
|
];let passed=0;document.querySelector('#checks').innerHTML=checks.map(([name,test])=>{const ok=test();if(ok)passed++;return `<li class="${ok?'pass':'fail'}">${ok?'PASS':'FAIL'} — ${name}</li>`}).join('');document.querySelector('#summary').textContent=`${passed}/${checks.length} checks passed`;};
|
|
</script>
|