main
py 85 lines 6.33 KB
Raw
1 """Browser regression with explicit API fixtures. Run against a local Next server; requires Python playwright and Edge."""
2 import asyncio,json
3 from pathlib import Path
4 from urllib.parse import urlparse,parse_qs
5 from playwright.async_api import async_playwright
6 ROOT=Path(__file__).resolve().parents[2]
7 OUT=ROOT/'artifacts/research-discovery';OUT.mkdir(parents=True,exist_ok=True)
8 ID='11111111-1111-4111-8111-111111111111'
9 match=dict(globalInstrumentId=ID,companyName='Chennai Petroleum Corporation Ltd.',canonicalSymbol='CHENNPETRO',symbol='CHENNPETRO',exchange='NSE',isin='INE178A01016',country='IN',region='INDIA',assetType='EQUITY')
10 company=dict(instrumentId=ID,companyName=match['companyName'],ticker='CHENNPETRO',exchange='NSE',isin=match['isin'],assetType='EQUITY',status='RESEARCH_PENDING',valuation=dict(state='UNAVAILABLE',reason='No persisted valuation'),sourceDiversity={},sourceCount=0,documentCount=0,ownershipIncreases=[],shareholdingChanges=[],currentQuarterCatalysts=[])
11 watchlist=dict(watchlistId='list-india',region='INDIA',name='WATCHLIST-IND',systemDefault=True,instrumentCount=0)
12 async def main():
13 calls=[]; saved=[]; browser_requests=[]
14 async with async_playwright() as pw:
15 browser=await pw.chromium.launch(channel='msedge',headless=True)
16 page=await browser.new_page(viewport=dict(width=1440,height=1050))
17 await page.add_init_script("localStorage.setItem('aip.accessToken','fixture-token'); localStorage.setItem('aip.user',JSON.stringify({userId:'fixture',displayName:'Research validation',email:'fixture@example.test'}));")
18 page.on('request', lambda request: browser_requests.append(dict(method=request.method,url=request.url)))
19 page.on('pageerror',lambda error: print('PAGEERROR',str(error).splitlines()[0]))
20 async def route(r):
21 u=urlparse(r.request.url); path=u.path; calls.append(dict(method=r.request.method,url=r.request.url))
22 data=[];status=200
23 if path.endswith('/dashboard'): data=dict(currencyTotals={},portfolios=[],incompleteValuationPortfolioIds=[])
24 elif path.endswith('/instruments/search'):
25 q=parse_qs(u.query).get('q',[''])[0]
26 if q=='slow': await asyncio.sleep(.8); data=[dict(match,companyName='Stale result')]
27 elif q=='nothing': data=[]
28 else: data=[match]
29 elif path.endswith('/watchlists/default/ensure'): data=watchlist
30 elif path.endswith('/watchlists/list-india/instruments'):
31 saved.append(r.request.post_data_json['globalInstrumentId']);data={'globalInstrumentId':ID}
32 elif path.endswith('/watchlists'): data=[watchlist]
33 elif path.endswith('/watchlists/list-india/research'): data={'watchlist':watchlist,'instruments':[dict(globalInstrumentId=i,company=company,held=False) for i in saved]}
34 elif '/readiness/' in path: data=dict(globalInstrumentId=ID,overallStatus='MISSING',overallCompletenessPct=0,criticalCompletenessPct=0,confidence='LOW',confidencePct=0,requirements=[],generatedAt='2026-09-12T00:00:00Z')
35 elif path.endswith('/presentation'): data=company
36 elif path.endswith('/summary'): status=404;data={'message':'No persisted summary'}
37 elif 'ensure' in path: data={}
38 await r.fulfill(status=status,json=data)
39 await page.route('**/api/v1/**',route)
40 async def research():
41 await page.goto('http://localhost:3000'); await page.get_by_role('button',name='Research',exact=True).click();await page.wait_for_timeout(700)
42 await research()
43 await page.screenshot(path=str(OUT/'after.png'),full_page=True)
44 search=page.locator('#research-stock-query')
45 searches=lambda:[c for c in calls if '/instruments/search?' in c['url']]
46 await search.fill('c');await page.wait_for_timeout(400)
47 await search.fill('ch');await page.wait_for_timeout(400)
48 assert len(searches())==0,searches()
49 await search.fill('che');await page.wait_for_timeout(500)
50 assert len(searches())==1,searches()
51 await page.locator('#research-search-listbox [role=option]').first.wait_for()
52 await page.screenshot(path=str(OUT/'after-results.png'),full_page=True)
53 await search.fill(' che ');await page.wait_for_timeout(400);assert len(searches())==1
54 await search.fill('slow');await page.wait_for_timeout(400);await search.fill('nothing');await page.wait_for_timeout(1100)
55 assert await page.get_by_text('No stocks found.',exact=True).is_visible()
56 assert not await page.get_by_text('Stale result',exact=True).is_visible()
57 await search.fill('che');await page.wait_for_timeout(400);assert len(searches())==3
58 before_selection=len(calls)
59 await search.press('ArrowDown');await search.press('Enter');await page.wait_for_timeout(600)
60 assert await page.get_by_role('dialog').count() == 1
61 assert all(c['method'] == 'GET' for c in calls[before_selection:]), calls[before_selection:]
62 assert any('/companies/'+ID+'/presentation?' in c['url'] for c in calls[before_selection:])
63 close=page.get_by_role('button',name='Close research readiness')
64 if await close.count():await close.click()
65 else: await page.keyboard.press('Escape')
66 await page.get_by_role('button',name='Add to WATCHLIST-IND',exact=True).click()
67 await page.get_by_role('button',name='Saved to WATCHLIST-IND',exact=True).wait_for()
68 await research();await page.locator('#research-stock-query').fill('che');await page.wait_for_timeout(450);await page.locator('#research-stock-query').press('Enter');await page.wait_for_timeout(500)
69 if await close.count():await close.click()
70 else:await page.keyboard.press('Escape')
71 await page.get_by_role('button',name='Saved to WATCHLIST-IND',exact=True).wait_for()
72 assert await page.get_by_text('Public company research · Not held',exact=True).is_visible()
73 assert not await page.get_by_text('Sector Performance',exact=True).is_visible()
74 assert not any('nseindia.com' in c['url'] or 'yahoo' in c['url'] or 'mcp' in c['url'] for c in browser_requests)
75 await page.screenshot(path=str(OUT/'after-saved-reload.png'),full_page=True)
76 await page.get_by_role('button',name='Open company research',exact=True).click()
77 drawer=page.get_by_role('dialog')
78 await drawer.wait_for()
79 for label in ['Quantity', 'Average Cost', 'Cost Basis', 'Market Value', 'Unrealized P/L']:
80 assert not await drawer.get_by_text(label,exact=True).count()
81 await page.screenshot(path=str(OUT/'after-public-company.png'),full_page=True)
82 (OUT/'browser-network.json').write_text(json.dumps(browser_requests,indent=2))
83 print('PASS browser debounce, cache, stale response, keyboard, canonical selection, watchlist save/reload; fixture APIs')
84 await browser.close()
85 asyncio.run(main())