File size: 7,624 Bytes
b9cdea1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
"""Browser smoke checks. Run with `python test_browser.py` and installed Chromium + playwright.
Loads the current source files directly into Chromium; no HTTP server is needed.
Uses system Chromium or an installed Playwright Chromium.
"""
from playwright.sync_api import sync_playwright
import os,json,re,shutil
from pathlib import Path
CHROME=os.getenv('CHROMIUM_BINARY') or shutil.which('chromium') or shutil.which('google-chrome')
checks=[]
def assertion(cond):
 assert cond,'assertion failed'
def check(name, fn):
    try:fn();checks.append((name,True))
    except Exception as e:checks.append((name,False,str(e)));print('FAIL',name, str(e)[:220]);raise

with sync_playwright() as p:
 browser=p.chromium.launch(**({'executable_path':CHROME} if CHROME else {}),headless=True,args=['--no-sandbox','--disable-dev-shm-usage'])
 context=browser.new_context(viewport={'width':1480,'height':1100},accept_downloads=True)
 page=context.new_page();errors=[]
 page.on('pageerror',lambda err:errors.append(str(err)))
 base=Path(__file__).resolve().parent
 html=(base/'index.html').read_text()
 html=re.sub(r'<link rel="stylesheet" href="styles.css"/>', '<style>'+ (base/'styles.css').read_text() + '</style>',html)
 html=re.sub(r'<script src="[^"]+"></script>', '', html)
 page.set_content(html,wait_until='load')
 for script in ('engine.js','demo-data.js','app.js'):page.add_script_tag(content=(base/script).read_text())
 check('Title and layout',lambda: (assertion(page.title().startswith('ImpactLab')),assertion(page.locator('#indicator option').count()==8)))
 check('Synthetic source visible',lambda:assertion('SYNTHETIC DEMO' in page.locator('#sourceBanner').inner_text()))
 check('Overview metrics rendered',lambda:assertion('DiD contrast' in page.locator('#view').inner_text()))
 check('Default DiD is calculated',lambda:assertion(page.evaluate('window.__impactState.did.ok') is True))
 check('Synthetic control has convex weights',lambda:assertion(page.evaluate('window.__impactState.synth.ok') is True))
 check('Demo 3000 rows',lambda:assertion(page.evaluate('window.__impactState.rows.length')==3000))
 page.locator('[data-tab="trends"]').click()
 check('Trends tab renders chart',lambda:assertion(page.locator('#view svg').count()>=1))
 page.locator('[data-tab="did"]').click()
 check('DiD tab renders interval',lambda:assertion('Year-resampling range' in page.locator('#view').inner_text()))
 page.locator('[data-tab="synthetic"]').click()
 check('Synthetic tab visualizes donors',lambda:assertion('Donor weights' in page.locator('#view').inner_text()))
 page.locator('[data-tab="placebos"]').click()
 check('Placebo analyses show diagnostics',lambda:assertion('unit placebos' in page.locator('#view').inner_text()))
 page.locator('[data-tab="quality"]').click()
 check('Coverage audit reports countries',lambda:assertion('Panel completeness' in page.locator('#view').inner_text()))
 page.locator('[data-tab="protocol"]').click()
 page.locator('#policyName').fill('Hypothetical education reform (not verified)')
 page.locator('#policyUrl').fill('https://example.org/report')
 page.locator('#researchNotes').fill('This is an exploratory research note. No causal claim.')
 page.locator('[data-tab="export"]').click()
 check('Research report contains notes and provenance',lambda:assertion('This is an exploratory research note.' in page.locator('#reportPreview').inner_text() and 'SYNTHETIC DEMO' in page.locator('#reportPreview').inner_text()))
 with page.expect_download() as dl:page.locator('#exportCsv').click()
 check('CSV downloads',lambda:assertion(dl.value.suggested_filename.endswith('.csv')))
 with page.expect_download() as dl2:page.locator('#exportMd').click()
 check('Markdown downloads',lambda:assertion(dl2.value.suggested_filename.endswith('.md')))
 with page.expect_download() as dl3:page.locator('#exportJson').click()
 sess=json.loads(dl3.value.path().read_text())
 check('JSON session saves exact provenance',lambda:assertion(sess['source']=='demo' and sess['notes'].startswith('This is an exploratory') and len(sess['rows'])==3000))
 page.locator('#startYear').fill('2018');page.locator('#runBtn').click()
 check('Invalid short design yields visible warning',lambda:assertion(not page.locator('#errorPanel').is_hidden()))
 page.locator('#startYear').fill('2003');page.locator('#runBtn').click()
 check('Warning clears after valid design',lambda:assertion(page.locator('#errorPanel').is_hidden()))
 # Fake network errors must preserve earlier results, not mislabel as live.
 page.route('https://api.worldbank.org/**',lambda route:route.fulfill(status=503,body='unavailable'))
 page.locator('#liveBtn').click();page.wait_for_timeout(500)
 check('Live fetch failure preserves synthetic data',lambda:assertion(page.evaluate('window.__impactState.source')=='demo' and not page.locator('#errorPanel').is_hidden()))
 page.unroute('https://api.worldbank.org/**')
 def fake_response(route):
  u=route.request.url
  data=[{'countryiso3code':code,'date':str(y),'value':70+(i%4)+.1*(y-2000)} for i,code in enumerate(['CHL','ARG','PER','URY']) for y in range(2000,2025)]
  route.fulfill(status=200,headers={'Access-Control-Allow-Origin':'*','Content-Type':'application/json'},body=json.dumps([{'page':1,'pages':1,'total':len(data)},data]))
 page.route('https://api.worldbank.org/**',fake_response)
 page.locator('#liveBtn').click();page.wait_for_timeout(750)
 check('World Bank response adapter switches source to LIVE',lambda:assertion(page.evaluate('window.__impactState.source')=='worldbank'))
 check('Loaded record count matches mock response',lambda:assertion(page.evaluate('window.__impactState.rows.length')==100))
 check('UI labels live data',lambda:assertion('WORLD BANK LIVE' in page.locator('#sourceBanner').inner_text()))
 page.locator('#demoBtn').click()
 check('Demo restores previous defaults',lambda:assertion(page.evaluate('window.__impactState.source')=='demo' and page.evaluate('window.__impactState.rows.length')==3000))
 # Import JSON must be flagged imported; do not automatically trust source.
 f=page.locator('[data-tab="export"]');f.click();page.locator('#importJson').set_input_files({'name':'session.json','mimeType':'application/json','buffer':json.dumps(sess).encode()});page.wait_for_timeout(150)
 check('Session restores as UNVERIFIED IMPORTED',lambda:assertion(page.evaluate('window.__impactState.source')=='imported'))
 # malicious URL should never become link
 page.locator('#policyUrl').fill('javascript:alert(1)');page.locator('[data-tab="protocol"]').click()
 check('Unsafe URL excluded',lambda:assertion('javascript:alert' not in page.locator('#view').inner_html()))
 page.locator('#demoBtn').click()
 # Desktop and mobile screenshots
 page.screenshot(path=str(base/'ImpactLab_LATAM_DESKTOP_PREVIEW.png'),full_page=True)
 mobile=browser.new_context(viewport={'width':390,'height':844},device_scale_factor=1,accept_downloads=True)
 mob=mobile.new_page();mob.set_content(html,wait_until='load');[mob.add_script_tag(content=(base/script).read_text()) for script in ('engine.js','demo-data.js','app.js')]
 check('Mobile interface renders',lambda:assertion(mob.locator('#view .metric').count()>=4))
 check('Mobile layout has no horizontal overflow',lambda:assertion(mob.evaluate('document.documentElement.scrollWidth <= innerWidth+2')))
 mob.screenshot(path=str(base/'ImpactLab_LATAM_MOBILE_PREVIEW.png'),full_page=True)
 check('Desktop runtime errors zero',lambda:assertion(len(errors)==0))
 browser.close()

for item in checks:print('PASS' if item[1] else 'FAIL',item[0])
print('TOTAL',len(checks),'PASS',sum(1 for x in checks if x[1]),'FAIL',sum(1 for x in checks if not x[1]))