import {createServer} from 'vite'; import {chromium} from '@playwright/test'; import {fileURLToPath} from 'node:url'; import path from 'node:path'; import {mkdir,writeFile} from 'node:fs/promises'; const workdir=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'../../../..'); const evidence=path.join(workdir,'evidence'); await mkdir(evidence,{recursive:true}); const servers=[];let browser; const results=[]; const latest=(mode)=>({uuid:'latest',name:'pro',revision:mode==='same'?1:2,display_name:{en_US:'Pro'},description:{en_US:'Workspace plan'},price_cny:'70',price_usd:'10',price_jpy:'1500',features:[],limits: mode==='equal'||mode==='same'?{'bots.max':2,'members.max':3}:{'bots.max':8,'members.max':5},entitlement_features:{mcp_stdio:!['same','equal'].includes(mode)},disabled_adapters:['same','equal'].includes(mode)?['telegram']:[],chat_history_retention_days:['same','equal'].includes(mode)?30:90,monthly_bonus_credits:['same','equal'].includes(mode)?10:20,is_active:true,subscription_disabled:mode==='paused',sort_order:1,highlight:false}); try { for(const [repo,port] of [['space',4185],['core',4186]]){ const server=await createServer({configFile:path.join(workdir,repo,'web/tests/revisions/vite.config.mjs')});await server.listen();servers.push(server); } browser=await chromium.launch({headless:true}); for(const repo of ['space','core']){ const modes=repo==='space'?['changed','same','equal','paused','removed','member','stripe','cancel-failure']:['changed','same','equal','paused','removed','member','unknown','error']; for(const mode of modes){ const page=await browser.newPage({viewport:{width:1360,height:1100}}); page.setDefaultTimeout(120000); const errors=[];page.on('pageerror',error=>{errors.push(error.message);console.error(error.message);}); await page.route('**/*',async route=>{ const url=new URL(route.request().url()); if(url.hostname==='127.0.0.1')return route.continue(); if(url.pathname==='/api/v1/cloud/v2/plans')return route.fulfill({status:mode==='error'?500:200,contentType:'application/json',body:JSON.stringify({code:0,data:mode==='removed'?[]:[latest(mode)]})}); return route.abort(); }); const port=repo==='space'?4185:4186; await page.goto(`http://127.0.0.1:${port}/tests/revisions/index.html?mode=${mode}${mode==='stripe'?'&intent=1':''}`,{waitUntil:'domcontentloaded',timeout:120000}); if(repo==='core'&&mode==='error')await page.getByRole('alert').waitFor(); else if(mode==='unknown')await page.getByText(/Current subscription details are unavailable/).waitFor(); else await page.locator(repo==='space'?'[data-plan-comparison]':'[data-cloud-plans] article').first().waitFor(); if(errors.length)throw new Error(`${repo}/${mode}: ${errors.join(';')}`); if(await page.locator('[data-revision-comparison]').count())throw new Error('Standalone comparison remains'); if(['same','equal'].includes(mode) && await page.locator('[data-owned-plan]').count())throw new Error('Identical benefits duplicated'); const area=page.locator(repo==='space'?'[data-plan-comparison]':'[data-cloud-plans]'); if(!['unknown','error'].includes(mode)) { const copy=await area.innerText(); if(/&#|revision|mcp stdio|workspace multi user|platform telegram/.test(copy))throw new Error('Internal metadata leaked into plan presentation'); if(repo==='core'&&await area.locator('[data-changed], .bg-amber-500\\/10').count())throw new Error('Diff highlighting remains'); if(['same','equal'].includes(mode)&&/Subscribed on|Latest quotas for this plan/.test(copy))throw new Error('Unnecessary comparison labels'); } if(mode==='changed') { await area.getByText(/Subscribed on/).waitFor(); await area.getByText('Latest quotas for this plan',{exact:true}).waitFor(); const owned=page.locator('[data-owned-plan]'); if(await owned.count()!==1)throw new Error('Missing integrated owned version'); const horizontal=await owned.evaluate(el=>{const next=el.nextElementSibling;return !!next && Math.abs(next.getBoundingClientRect().y-el.getBoundingClientRect().y)<5 && next.getBoundingClientRect().x>el.getBoundingClientRect().x;}); if(!horizontal)throw new Error('Owned and latest are not adjacent horizontally'); } if(repo==='space'&&mode==='stripe'){ if(await page.getByRole('dialog').count())throw new Error('Deep link bypassed renewal gate'); await page.getByRole('button',{name:'Manage subscription'}).first().click(); await page.getByText('Server refresh confirmed cancellation').waitFor(); if (!(await page.getByRole('dialog').count())) await page.getByRole('button',{name:/Upgrade to|Subscribe to|Choose/}).last().click(); await page.getByRole('dialog').waitFor(); } if(repo==='space'&&mode==='cancel-failure'){ await page.getByRole('button',{name:'Manage subscription'}).first().click(); await page.getByText('Cancellation failed').waitFor(); if(await page.getByRole('dialog').count())throw new Error('Failed cancellation opened checkout'); } if(mode==='member'&&repo==='core'&&await page.getByRole('link').count())throw new Error('Member purchase link'); if(mode==='changed'||mode==='equal'||mode==='member'||mode==='paused')await page.screenshot({path:path.join(evidence,`${repo}-${mode}.png`),fullPage:true}); results.push(`${repo}/${mode}: PASS`);console.log(results.at(-1));await page.close(); } } } finally { await writeFile(path.join(evidence,'browser-results.txt'),results.join('\n')+'\n'); if(browser)await browser.close();for(const server of servers)await server.close(); }