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') const root = path.resolve(__dirname, '../..') const origin = process.env.VERIFY_FRONTEND_ORIGIN || 'https://agent.pducv.cn' const reportName = process.env.VERIFY_REPORT_NAME || 'public-first-entry-loading' assert.match(reportName, /^[a-zA-Z0-9_-]+$/) const started_at = new Date().toISOString() const checks = [], failures = [], timings = {}, requests = [] let browser function check(name, value) { checks.push({ name, passed: !!value }) assert.ok(value, name) } async function main() { const credentials = JSON.parse(fs.readFileSync(path.join(root, 'acceptance/super-admin-login.json'), 'utf8')) browser = await chromium.launch({ headless: true, channel: 'chrome' }) const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } }) await page.addInitScript(() => { localStorage.setItem('locale', 'zh-CN') for (const key of ['weknora:new-user-guide-done:v1', 'weknora:contextual-guide-kb-list:v2', 'weknora:contextual-guide-kb-create:v3', 'weknora:contextual-guide-tenant-models:v1']) localStorage.setItem(key, '1') }) page.on('pageerror', error => failures.push(error.message)) page.on('request', request => requests.push({ path: new URL(request.url()).pathname, method: request.method() })) await page.goto(`${origin}/login`) await page.locator('input[autocomplete="username"]').fill(credentials.username) await page.locator('input[type="password"]').first().fill(credentials.password) const loginStarted = Date.now() await page.locator('button[type="submit"]').click() await page.waitForURL('**/platform/knowledge-bases', { timeout: 30000 }) timings.login_to_route_ms = Date.now() - loginStarted await page.locator('[data-guide="kb-list-create"]').waitFor({ state: 'visible' }) await page.waitForLoadState('networkidle', { timeout: 30000 }) check('cold_form_login_completes_within_30_seconds', timings.login_to_route_ms < 30000) check('hidden_settings_module_not_requested_on_entry', !requests.some(item => /\/Settings-[^/]+\.js$/.test(item.path))) check('hidden_knowledge_editor_not_requested_on_entry', !requests.some(item => /\/KnowledgeBaseEditorModal-[^/]+\.js$/.test(item.path))) check('chat_agent_list_not_prefetched_on_knowledge_entry', !requests.some(item => item.path === '/api/v1/agents')) check('chat_search_providers_not_prefetched_on_knowledge_entry', !requests.some(item => item.path === '/api/v1/web-search-providers')) const editorStarted = Date.now() await page.locator('[data-guide="kb-list-create"]').click() const editor = page.locator('.settings-overlay').filter({ has: page.locator('[data-guide="kb-editor-sidebar"]') }) await editor.waitFor({ state: 'visible' }) await editor.locator('.editor-initializing').waitFor({ state: 'hidden' }) const name = editor.locator('[data-guide="kb-create-name"] input') await name.fill('Unsaved loading verification') timings.editor_ready_ms = Date.now() - editorStarted check('first_open_editor_initializes_and_accepts_input', await name.inputValue() === 'Unsaved loading verification') check('editor_module_requested_only_when_opened', requests.some(item => /\/KnowledgeBaseEditorModal-[^/]+\.js$/.test(item.path))) await page.locator('.t-message').first().waitFor({ state: 'hidden' }) await page.locator('.t-notification').first().waitFor({ state: 'hidden' }) await page.screenshot({ path: path.join(root, `acceptance/${reportName}-editor-desktop.png`) }) await editor.locator('.close-btn').click() await editor.waitFor({ state: 'hidden' }) await page.locator('[data-guide="kb-list-create"]').click() await editor.waitFor({ state: 'visible' }) await editor.locator('.editor-initializing').waitFor({ state: 'hidden' }) check('reopened_editor_does_not_reuse_discarded_draft', await name.inputValue() === '') for (const width of [390, 320]) { await page.setViewportSize({ width, height: 844 }) await page.screenshot({ path: path.join(root, `acceptance/${reportName}-editor-mobile-${width}.png`) }) check(`editor_mobile_${width}_no_page_overflow`, await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 2)) check(`editor_mobile_${width}_form_uses_available_width_without_clipping`, await editor.evaluate(element => { const content = element.querySelector('.content-wrapper') const input = element.querySelector('[data-guide="kb-create-name"]') if (!content || !input) return false const bounds = content.getBoundingClientRect(), field = input.getBoundingClientRect() return bounds.width >= innerWidth - 40 && content.scrollWidth <= content.clientWidth + 2 && field.width >= 250 && field.left >= bounds.left && field.right <= bounds.right })) check(`editor_mobile_${width}_footer_buttons_visible`, await editor.locator('.settings-footer-actions').evaluate(element => { const bounds = element.getBoundingClientRect() return bounds.left >= 0 && bounds.right <= innerWidth && bounds.bottom <= innerHeight && bounds.top > 0 })) } await editor.locator('.close-btn').click() await editor.waitFor({ state: 'hidden' }) await page.setViewportSize({ width: 1440, height: 1000 }) await page.locator('[data-guide="user-menu"]').click() await page.locator('.menu-item').filter({ hasText: '全部设置' }).click() await page.waitForURL('**/platform/settings**') await page.locator('.settings-overlay').waitFor({ state: 'visible' }) check('settings_menu_opens_one_settings_dialog', await page.locator('.settings-overlay').count() === 1) check('settings_module_requested_when_opened', requests.some(item => /\/Settings-[^/]+\.js$/.test(item.path))) const chatRequestStart = requests.length await page.goto(`${origin}/platform/creatChat`) await page.waitForLoadState('networkidle', { timeout: 30000 }) check('chat_route_still_loads_its_agents_and_models', ['/api/v1/agents', '/api/v1/models'].every(path => requests.slice(chatRequestStart).some(item => item.path === path))) check('chat_route_loads_without_redirect', new URL(page.url()).pathname === '/platform/creatChat') check('no_business_resource_writes', !requests.some(item => ['POST', 'PUT', 'PATCH', 'DELETE'].includes(item.method) && item.path.startsWith('/api/') && !item.path.startsWith('/api/v1/auth/'))) check('no_page_errors', failures.length === 0) } main().catch(error => { failures.push(error.message) }).finally(async () => { if (browser) await browser.close() const report = { started_at, checked_at: new Date().toISOString(), origin, checks, timings, failures, passed: checks.length === 20 && checks.every(item => item.passed) && failures.length === 0 } fs.writeFileSync(path.join(root, `acceptance/${reportName}.json`), JSON.stringify(report, null, 2)) console.log(JSON.stringify(report)) if (!report.passed) process.exitCode = 1 })