104 lines
7.0 KiB
JavaScript
104 lines
7.0 KiB
JavaScript
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 })
|