| 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()) |