const assert = require('node:assert/strict') const fs = require('node:fs') const path = require('node:path') const { chromium } = require('/Users/macclaw/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright') async function main() { const directory = '/Volumes/Extreme SSD/yawan-storage/weknora/acceptance' const manifest = JSON.parse(fs.readFileSync(path.join(directory, 'business-apply-20260916.manifest.json'), 'utf8')) const credentials = JSON.parse(fs.readFileSync(path.join(directory, 'super-admin-login.json'), 'utf8')) const login = await fetch('http://127.0.0.1:18080/api/v1/auth/login', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({email: credentials.email, password: credentials.password}), }) assert.equal(login.status, 200, 'Administrator login') const auth = await login.json() const headers = {'Content-Type': 'application/json', Authorization: `Bearer ${auth.token}`, 'X-Tenant-ID': '10059'} async function request(url, method = 'GET', data) { const response = await fetch(`http://127.0.0.1:18080/api/v1${url}`, {method, headers, body: data === undefined ? undefined : JSON.stringify(data)}) assert.ok(response.ok, `Acceptance HTTP ${response.status}`) return response.json() } const checks = [], errors = [] const browser = await chromium.launch({headless: true, channel: 'chrome'}) let fixture, page, stage = 'starting' const check = (name, passed) => {checks.push({check: name, passed: !!passed}); assert.ok(passed, name)} try { const context = await browser.newContext({viewport: {width: 1440, height: 1000}}) await context.addInitScript(a => { const tenant = {...a.active_tenant, id: '10059'} const data = {weknora_user: JSON.stringify({...a.user, tenant_id: String(a.user.tenant_id)}), weknora_tenant: JSON.stringify(tenant), weknora_token: a.token, weknora_refresh_token: a.refresh_token, weknora_memberships: JSON.stringify(a.memberships)} for (const [key, value] of Object.entries(data)) localStorage.setItem(key, value) localStorage.setItem('weknora:new-user-guide-done:v1', '1') }, auth) page = await context.newPage() page.setDefaultTimeout(15000) page.on('pageerror', () => errors.push('browser_error')) async function visit(url) { await page.goto('http://127.0.0.1:15173' + url) await page.locator('.workbench-resource').first().waitFor({timeout: 30000}) const guide = page.locator('.guide__close') if (await guide.isVisible()) await guide.click() } async function resize(width, height) { stage = 'resize_viewport' await page.setViewportSize({width, height}) stage = 'wait_sidebar_collapse' if (width <= 640) await page.locator('.aside_box--collapsed').waitFor() stage = 'wait_page_dimensions' await page.waitForFunction(() => { const bounds = document.querySelector('.workbench-page')?.getBoundingClientRect() return bounds && bounds.width >= Math.min(300, innerWidth - 80) && bounds.right <= innerWidth + 2 }, undefined, {polling: 100}) stage = 'wait_layout_transition' await page.evaluate(async () => { await Promise.race([ Promise.all(document.getAnimations() .filter(animation => Number.isFinite(animation.effect?.getComputedTiming().endTime)) .map(animation => animation.finished.catch(() => {}))), new Promise(resolve => setTimeout(resolve, 1000)), ]) }) } async function mobileCheck(kind) { await resize(390, 844) check(`${kind}_mobile_usable_width`, await page.locator('.workbench-page').evaluate(element => { const bounds = element.getBoundingClientRect() const detail = element.querySelector('.workbench-detail')?.getBoundingClientRect() return bounds.width >= 300 && bounds.right <= innerWidth + 2 && detail && detail.width >= 260 && detail.right <= innerWidth + 2 })) await page.locator('.workbench-page').evaluate(element => { element.scrollTop = 0 }) await page.screenshot({path: path.join(directory, `business-${kind}-mobile.png`), animations: 'disabled'}) check(`${kind}_mobile_no_overflow`, await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 2)) } for (const [kind, route, tab] of [ ['trainingmap', '/platform/workbench/content', 'maps'], ['campaign', '/platform/workbench/content', 'tasks'], ]) { const ids = Object.values(manifest.resource_map[kind]) await visit(`${route}?tab=${tab}&resource=${ids.at(-1)}`) check(`${kind}_list_count`, await page.locator('.workbench-resource').count() === ids.length) check(`${kind}_history_warning`, await page.locator('.workbench-detail .t-alert').isVisible()) check(`${kind}_delete_hidden`, await page.locator('.workbench-detail').getByRole('button', {name: '删除', exact: true}).count() === 0) const aiButtons = page.locator('.workbench-detail').getByRole('button', {name: kind === 'campaign' ? '重新拆解' : '重新生成方案', exact: true}) check(`${kind}_execution_disabled`, await aiButtons.isDisabled()) await page.screenshot({path: path.join(directory, `business-${kind}-desktop.png`), animations: 'disabled'}) await page.getByRole('button', {name: '全屏查看', exact: true}).click() await page.locator('.t-dialog:visible .workbench-preview').waitFor() check(`${kind}_fullscreen_readable`, await page.locator('.t-dialog:visible .workbench-preview').isVisible()) stage = 'close_desktop_preview' await page.locator('.t-dialog:visible .t-dialog__close').click() stage = 'wait_desktop_preview_closed' await page.locator('.t-dialog:visible').waitFor({state: 'hidden'}) await mobileCheck(kind) await page.getByRole('button', {name: '全屏查看', exact: true}).click() await page.locator('.t-dialog:visible .workbench-preview').waitFor() check(`${kind}_mobile_fullscreen_fits`, await page.locator('.t-dialog:visible').evaluate(element => { const bounds = element.getBoundingClientRect() return bounds.left >= 0 && bounds.right <= innerWidth + 2 })) await page.screenshot({path: path.join(directory, `business-${kind}-mobile-fullscreen.png`), animations: 'disabled'}) await page.locator('.t-dialog:visible .t-dialog__close').click() await page.locator('.t-dialog:visible').waitFor({state: 'hidden'}) await resize(1440, 1000) } await visit('/platform/workbench/flows') check('three_historical_flows_listed', await page.locator('.workbench-resource').count() === 3) await page.locator('.workbench-resource').first().click() check('flow_history_warning', await page.locator('.workbench-detail .t-alert').isVisible()) check('flow_delete_and_save_hidden', await page.locator('.workbench-detail').getByRole('button', {name: /^(保存|解绑并删除)$/}).count() === 0) await page.screenshot({path: path.join(directory, 'business-flow-desktop.png'), animations: 'disabled'}) await mobileCheck('flow') await resize(1440, 1000) const agents = (await request('/agents')).data const agent = agents.find(a => a.enabled && a.config.model_id && a.created_by === auth.user.id) assert.ok(agent, 'Runnable owned Agent available for branch editor fixture') const edges = [{source: 'a', target: 'b'}, {source: 'a', target: 'c'}] fixture = (await request('/workbench/resources/flows', 'POST', { name: `Migration branch check ${Date.now()}`, data: {description: 'Disposable branch preservation check', nodes: ['a', 'b', 'c'].map(id => ({id, label: `Branch ${id}`, agent_id: agent.id})), edges}, })).data await visit('/platform/workbench/flows') await page.locator('.workbench-resource').filter({hasText: fixture.name}).click() await page.locator('.flow-node').nth(2).getByRole('button').nth(0).click() const saved = page.waitForResponse(response => response.url().endsWith(fixture.id) && response.request().method() === 'PUT') await page.getByRole('button', {name: '保存', exact: true}).click() check('native_branch_save_success', (await saved).status() === 200) const current = (await request(`/workbench/resources/flows/${fixture.id}`)).data check('node_display_reordered', current.data.nodes.map(n => n.id).join(',') === 'a,c,b') check('branch_edges_preserved_after_reorder_and_save', JSON.stringify(current.data.edges) === JSON.stringify(edges)) check('no_browser_javascript_errors', errors.length === 0) } finally { if (checks.length < 25 && page && !page.isClosed()) { console.log(JSON.stringify({stage, layout: await page.evaluate(() => ({ dialogs: document.querySelectorAll('.t-dialog').length, collapsed: !!document.querySelector('.aside_box--collapsed'), width: innerWidth, page: document.querySelector('.workbench-page')?.getBoundingClientRect().toJSON(), }))})) await page.screenshot({path: path.join(directory, 'business-browser-debug.png'), animations: 'disabled'}) } if (fixture) await request(`/workbench/resources/flows/${fixture.id}?detach=true`, 'DELETE') await browser.close() fs.writeFileSync(path.join(directory, 'business-browser-20260916.json'), JSON.stringify({ passed: checks.length === 25 && checks.every(c => c.passed), checks, fixture_removed: !!fixture, }, null, 2) + '\n') } console.log(JSON.stringify({checks, passed: checks.every(c => c.passed)})) } main().catch(error => { console.error(error instanceof assert.AssertionError ? error.message : `Business browser verification failed (${error.name})`); process.exitCode = 1 })