Files

104 lines
7.0 KiB
JavaScript
Raw Permalink Normal View History

const assert = require('node:assert/strict')
const fs = require('node:fs')
const { chromium } = require('/Users/macclaw/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright')
const directory = '/Volumes/Extreme SSD/yawan-storage/weknora/acceptance'
const origin = process.env.VERIFY_FRONTEND_ORIGIN || 'http://127.0.0.1:15173'
const apiOrigin = process.env.VERIFY_API_ORIGIN || 'http://127.0.0.1:18080'
async function main() {
const credentials = JSON.parse(fs.readFileSync(`${directory}/super-admin-login.json`, 'utf8'))
const response = await fetch(`${apiOrigin}/api/v1/auth/login`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: credentials.email, password: credentials.password }),
})
assert.equal(response.status, 200)
const auth = await response.json()
const browser = await chromium.launch({ headless: true, channel: 'chrome' })
const checks = [], errors = [], blockedWrites = [], writes = []
const check = (name, passed) => { checks.push({ name, passed: !!passed }); assert.ok(passed, name) }
try {
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } })
await context.addInitScript(a => {
const entries = {
weknora_user: JSON.stringify({ ...a.user, tenant_id: String(a.user.tenant_id) }),
weknora_tenant: JSON.stringify({ ...a.active_tenant, id: '10059' }),
weknora_token: a.token, weknora_refresh_token: a.refresh_token,
weknora_memberships: JSON.stringify(a.memberships), 'weknora:new-user-guide-done:v1': '1',
}
for (const [key, value] of Object.entries(entries)) localStorage.setItem(key, value)
}, auth)
const fixture = (id, kind, name, data) => ({ id, kind, name, data, tenant_id: 10059, owner_id: auth.user.id, owner_name: 'UI verification', status: kind === 'training-maps' ? 'confirmed' : 'draft', version: 1, created_at: new Date().toISOString(), updated_at: new Date().toISOString() })
const map = fixture('duration-map', 'training-maps', '时长验证导图', { days: [{ day: 1, title: '基础与实践' }], agent_id: 'duration-agent' })
const flow = fixture('duration-flow', 'flows', '时长验证画布', { nodes: [{ id: 'node', label: '内容作者', agent_id: 'duration-agent' }], edges: [] })
let task = null
// All business writes below are in-memory fixtures; no task or AI job reaches the API.
await context.route('**/api/v1/**', async route => {
const request = route.request(), path = new URL(request.url()).pathname, method = request.method()
const ok = body => route.fulfill({ status: 200, json: { success: true, ...body } })
if (path === '/api/v1/access/me') return ok({ data: { administrator: false, restricted: true, permissions: ['content.read', 'content.write', 'flows.read'] } })
const match = path.match(/^\/api\/v1\/workbench\/resources\/(content-tasks|training-maps|flows)(?:\/([^/]+))?(?:\/(jobs|actions\/.*))?$/)
if (match) {
const [, kind, id, action] = match
if (method === 'GET' && action === 'jobs') return ok({ data: [], total: 0 })
if (method === 'GET') {
const items = kind === 'training-maps' ? [map] : kind === 'flows' ? [flow] : task ? [task] : []
return ok(id ? { data: items.find(item => item.id === id) } : { data: items, total: items.length })
}
if (kind === 'content-tasks' && !action && ['POST', 'PUT'].includes(method)) {
const body = request.postDataJSON()
writes.push({ method, data: body.data })
task = { ...fixture('duration-task', kind, body.name, body.data), version: (task?.version || 0) + 1 }
return ok({ data: task })
}
}
if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) {
blockedWrites.push({ path, method })
return route.fulfill({ status: 409, json: { error: 'Verification prevents live writes' } })
}
return route.continue()
})
const page = await context.newPage()
page.on('pageerror', error => errors.push(error.message))
await page.goto(`${origin}/platform/workbench/content?tab=tasks`)
await page.getByRole('button', { name: '创建内容任务', exact: true }).click()
const drawer = page.locator('.t-drawer--open')
const duration = drawer.locator('[aria-label="直播时长(分钟)"] input')
await duration.waitFor()
check('new_task_defaults_to_60_minutes', await duration.inputValue() === '60')
await drawer.locator('.t-form__item').filter({ has: page.locator('.t-form__label').filter({ hasText: /^名称$/ }) }).locator('input').fill('直播时长验证任务')
await duration.fill('135')
await duration.press('Tab')
await drawer.locator('.t-form__item').filter({ hasText: '已确认的训练营导图' }).locator('.t-select').click()
await page.getByText('时长验证导图', { exact: true }).last().click()
await drawer.locator('.t-form__item').filter({ hasText: '画布模板' }).locator('.t-select').click()
await page.getByText('时长验证画布', { exact: true }).last().click()
await page.screenshot({ path: `${directory}/content-duration-desktop.png`, animations: 'disabled' })
await drawer.getByRole('button', { name: '保存草稿', exact: true }).click()
await page.getByText('直播时长:135 分钟', { exact: true }).waitFor()
check('draft_payload_and_detail_keep_duration', writes[0]?.data.duration_minutes === 135)
await page.getByRole('button', { name: '编辑配置', exact: true }).click()
await duration.waitFor()
check('editing_reopens_saved_duration', await duration.inputValue() === '135')
await page.setViewportSize({ width: 390, height: 844 })
await duration.scrollIntoViewIfNeeded()
await page.screenshot({ path: `${directory}/content-duration-mobile.png`, animations: 'disabled' })
check('duration_control_fits_mobile', await duration.evaluate(element => { const r = element.getBoundingClientRect(); return r.left >= 0 && r.right <= innerWidth }))
check('drawer_fits_mobile', await drawer.locator('.t-drawer__content-wrapper').evaluate(element => { const r = element.getBoundingClientRect(); return r.left >= -1 && r.right <= innerWidth + 1 }))
await duration.fill('225')
await duration.press('Tab')
await drawer.getByRole('button', { name: '保存配置', exact: true }).click()
await page.getByText('直播时长:225 分钟', { exact: true }).waitFor()
check('edited_duration_saved', writes.at(-1)?.data.duration_minutes === 225)
check('no_live_business_or_ai_writes', blockedWrites.length === 0 && writes.length === 2)
check('no_runtime_errors', errors.length === 0)
fs.writeFileSync(`${directory}/content-duration-ui.json`, JSON.stringify({ origin, checks, errors, businessWrites: 'mocked in browser only' }, null, 2))
console.log(JSON.stringify({ checks, errors }, null, 2))
} catch (error) {
console.error(error.stack)
process.exitCode = 1
} finally {
await browser.close()
}
}
main().catch(error => { console.error(error.stack); process.exitCode = 1 })