611 lines
30 KiB
JavaScript
611 lines
30 KiB
JavaScript
import { createServer } from 'vite'
|
|
import { chromium } from 'playwright'
|
|
|
|
const port = Number(process.env.VISUAL_CHECK_PORT ?? 4175)
|
|
|
|
const server = await createServer({
|
|
root: process.cwd(),
|
|
server: {
|
|
host: '127.0.0.1',
|
|
port,
|
|
strictPort: true,
|
|
},
|
|
})
|
|
|
|
await server.listen()
|
|
|
|
const browser = await chromium.launch({ headless: true })
|
|
const page = await browser.newPage({ viewport: { width: 1440, height: 1024 }, deviceScaleFactor: 1 })
|
|
const errors = []
|
|
const projectId = '019b0000-0000-7000-8000-000000000001'
|
|
const visualExperts = [
|
|
{
|
|
id: '019b0000-0000-7000-8000-000000000020', slug: 'product-manager', category: 'product', categoryName: '产品',
|
|
name: '产品经理', description: '负责需求分析、路线规划和产品交付。', emoji: '🧭', color: '#165DFF',
|
|
},
|
|
{
|
|
id: '019b0000-0000-7000-8000-000000000021', slug: 'ui-designer', category: 'design', categoryName: '设计',
|
|
name: 'UI 设计师', description: '负责界面设计、组件规范和视觉一致性。', emoji: '🎨', color: '#722ED1',
|
|
},
|
|
{
|
|
id: '019b0000-0000-7000-8000-000000000022', slug: 'frontend-developer', category: 'engineering', categoryName: '工程',
|
|
name: '前端开发者', description: '负责现代 Web 应用实现与性能优化。', emoji: '💻', color: '#0FC6C2',
|
|
},
|
|
]
|
|
page.on('console', (message) => {
|
|
if (message.type() === 'error') errors.push(message.text())
|
|
})
|
|
|
|
const visualCheckWorkspace = {
|
|
project: {
|
|
id: projectId,
|
|
name: '森林项目',
|
|
identifier: 'forest',
|
|
icon: '森',
|
|
background: '#165DFF',
|
|
description: '前后端 API 对齐视觉检查项目',
|
|
initials: '森林',
|
|
unreadCount: 2,
|
|
},
|
|
channels: [
|
|
{ id: 'overview', projectId, type: 'overview', title: '概况', icon: 'home', count: 0, url: '', sortOrder: 0 },
|
|
{ id: 'inbox', projectId, type: 'inbox', title: 'Inbox 消息流', icon: 'email', count: 2, url: '', sortOrder: 1 },
|
|
{ id: 'tasks', projectId, type: 'tasks', title: '工作计划', icon: 'list', count: 0, url: '', sortOrder: 1 },
|
|
{ id: 'ai', projectId, type: 'ai_sessions', title: 'AI 会话', icon: 'robot', count: 0, url: '', sortOrder: 2 },
|
|
{ id: 'notes', projectId, type: 'notes_sources', title: '笔记资料', icon: 'file', count: 0, url: '', sortOrder: 3 },
|
|
{ id: 'cron', projectId, type: 'cron', title: '计划任务', icon: 'clock', count: 0, url: '', sortOrder: 4 },
|
|
],
|
|
tags: [
|
|
{ id: '019b0000-0000-7000-8000-000000000002', name: '产品' },
|
|
{ id: '019b0000-0000-7000-8000-000000000005', name: '设计' },
|
|
],
|
|
recentSessions: [],
|
|
inbox: [
|
|
{
|
|
id: '019b0000-0000-7000-8000-000000000003',
|
|
projectId,
|
|
source: '需求文档',
|
|
title: '森林项目体验优化需求',
|
|
summary: '整理工作台信息层级,并把关键需求沉淀为可跟进的项目计划。',
|
|
status: 'open',
|
|
tag: '需求',
|
|
time: '2026-07-22T08:00:00Z',
|
|
},
|
|
{
|
|
id: '019b0000-0000-7000-8000-000000000004',
|
|
projectId,
|
|
source: '架构讨论',
|
|
title: '项目资料组织方案',
|
|
summary: '围绕项目上下文统一组织任务、笔记、资料和 AI 会话。',
|
|
status: 'open',
|
|
tag: '架构',
|
|
time: '2026-07-22T07:00:00Z',
|
|
},
|
|
],
|
|
tasks: [
|
|
{
|
|
id: '019b0000-0000-7000-8000-000000000006',
|
|
projectId,
|
|
title: '梳理产品需求',
|
|
summary: '确认首版工作台的功能边界。',
|
|
completed: false,
|
|
owner: '森林AI',
|
|
createdAt: '2026-07-22',
|
|
completedAt: '',
|
|
duration: '',
|
|
progress: '40%',
|
|
tagId: '019b0000-0000-7000-8000-000000000002',
|
|
tag: '产品',
|
|
},
|
|
{
|
|
id: '019b0000-0000-7000-8000-000000000007',
|
|
projectId,
|
|
title: '优化会话界面',
|
|
summary: '统一 AI 会话的阅读与输入体验。',
|
|
completed: false,
|
|
owner: '森林AI',
|
|
createdAt: '2026-07-22',
|
|
completedAt: '',
|
|
duration: '',
|
|
progress: '60%',
|
|
tagId: '019b0000-0000-7000-8000-000000000005',
|
|
tag: '设计',
|
|
},
|
|
{
|
|
id: '019b0000-0000-7000-8000-000000000008',
|
|
projectId,
|
|
title: '确认项目定位',
|
|
summary: '完成项目功能与亮点说明。',
|
|
completed: true,
|
|
owner: '森林AI',
|
|
createdAt: '2026-07-21',
|
|
completedAt: '2026-07-22',
|
|
duration: '1 天',
|
|
progress: '100%',
|
|
tagId: '019b0000-0000-7000-8000-000000000002',
|
|
tag: '产品',
|
|
},
|
|
],
|
|
aiSessions: [],
|
|
notesSources: [],
|
|
cronPlans: [],
|
|
}
|
|
|
|
await page.route('http://localhost:9150/api/v1/**', async (route) => {
|
|
const url = new URL(route.request().url())
|
|
if (url.pathname === '/api/v1/status') {
|
|
await route.fulfill({ json: { timestamp: new Date().toISOString() } })
|
|
return
|
|
}
|
|
if (url.pathname === '/api/v1/auth/login') {
|
|
await route.fulfill({ json: { token: 'visual-check-token' } })
|
|
return
|
|
}
|
|
if (url.pathname === '/api/v1/projects') {
|
|
await route.fulfill({ json: [visualCheckWorkspace.project] })
|
|
return
|
|
}
|
|
if (url.pathname === `/api/v1/projects/${projectId}/workspace`) {
|
|
await route.fulfill({ json: visualCheckWorkspace })
|
|
return
|
|
}
|
|
if (url.pathname === `/api/v1/projects/${projectId}/ai-sessions`) {
|
|
if (route.request().method() === 'POST') {
|
|
const input = route.request().postDataJSON()
|
|
const expert = visualExperts.find((item) => item.id === input.expertId)
|
|
await route.fulfill({
|
|
json: {
|
|
id: '019b0000-0000-7000-8000-000000000010',
|
|
projectId,
|
|
title: input.title,
|
|
context: input.context,
|
|
status: 'ready',
|
|
expert,
|
|
createdAt: '2026-07-22T09:00:00Z',
|
|
updatedAt: '2026-07-22T09:00:00Z',
|
|
},
|
|
})
|
|
return
|
|
}
|
|
await route.fulfill({
|
|
json: [{
|
|
id: '019b0000-0000-7000-8000-000000000009',
|
|
projectId,
|
|
title: '报价分析',
|
|
context: '分析当前方案的报价结构。',
|
|
status: 'ready',
|
|
expert: visualExperts[1],
|
|
createdAt: '2026-07-22T08:30:00Z',
|
|
updatedAt: '2026-07-22T08:30:00Z',
|
|
}],
|
|
})
|
|
return
|
|
}
|
|
if (url.pathname === '/api/v1/ai-experts') {
|
|
await route.fulfill({ json: visualExperts })
|
|
return
|
|
}
|
|
await route.fulfill({ status: 404, json: { error: { code: 'not_found', message: '视觉检查未配置该接口' } } })
|
|
})
|
|
|
|
const collectMetrics = async () => page.evaluate(() => {
|
|
const statusbar = document.querySelector('.statusbar')
|
|
const statusName = document.querySelector('.status-name')
|
|
const statusOnlineDot = document.querySelector('.status-online-dot')
|
|
const statusUpgrade = document.querySelector('.status-upgrade')
|
|
const statusSystem = document.querySelector('.status-system')
|
|
const search = document.querySelector('.global-search')
|
|
const main = document.querySelector('.workbench-main')
|
|
const workspacePage = document.querySelector('.workspace-page')
|
|
const projectOverview = document.querySelector('.overview-page:not(.workspace-page)')
|
|
const rail = document.querySelector('.project-rail')
|
|
const railChildren = rail?.querySelector('.arco-layout-sider-children')
|
|
const sidebar = document.querySelector('.channel-sidebar')
|
|
const channelList = document.querySelector('.channel-list')
|
|
const stage = document.querySelector('.stage')
|
|
const inspector = document.querySelector('.inspector')
|
|
const workspaceActions = [...document.querySelectorAll('.workspace-page .overview-head .arco-btn')]
|
|
.map((button) => button.textContent?.trim())
|
|
const dashboard = document.querySelector('.dashboard-button')
|
|
const workspaceExplore = document.querySelector('.dashboard-button[title="探索"]')
|
|
const project = document.querySelector('.project-button')
|
|
const create = document.querySelector('.create-project')
|
|
const firstBadge = document.querySelector('.project-badge .arco-badge-number')
|
|
const railItems = [...document.querySelectorAll('.dashboard-button, .project-button, .create-project')]
|
|
const rect = (node) => {
|
|
const box = node?.getBoundingClientRect()
|
|
return box ? { left: box.left, right: box.right, top: box.top, width: box.width, height: box.height } : null
|
|
}
|
|
const railBox = rail?.getBoundingClientRect()
|
|
const itemRects = railItems.map(rect).filter(Boolean)
|
|
const horizontalInsets = railBox && project
|
|
? {
|
|
left: project.getBoundingClientRect().left - railBox.left,
|
|
right: railBox.right - project.getBoundingClientRect().right,
|
|
}
|
|
: null
|
|
const verticalGaps = itemRects.slice(1).map((item, index) => item.top - (itemRects[index].top + itemRects[index].height))
|
|
const overflowState = (node) => node
|
|
? {
|
|
clientWidth: node.clientWidth,
|
|
scrollWidth: node.scrollWidth,
|
|
clientHeight: node.clientHeight,
|
|
scrollHeight: node.scrollHeight,
|
|
overflowX: getComputedStyle(node).overflowX,
|
|
overflowY: getComputedStyle(node).overflowY,
|
|
scrollbarWidth: getComputedStyle(node).scrollbarWidth,
|
|
}
|
|
: null
|
|
|
|
return {
|
|
statusbarHeight: statusbar?.getBoundingClientRect().height,
|
|
statusName: rect(statusName),
|
|
statusOnlineDot: rect(statusOnlineDot),
|
|
statusUpgrade: rect(statusUpgrade),
|
|
statusSystemText: statusSystem?.textContent?.replace(/\s+/g, ' ').trim() ?? '',
|
|
searchHeight: search?.getBoundingClientRect().height,
|
|
workbenchMain: rect(main),
|
|
hasWorkspacePage: Boolean(workspacePage),
|
|
hasProjectOverview: Boolean(projectOverview),
|
|
exploreSourceCardCount: document.querySelectorAll('.explore-source-card').length,
|
|
hasExploreReader: Boolean(document.querySelector('.explore-reader-layout')),
|
|
hasInboxChannel: [...document.querySelectorAll('.channel-button')].some((button) => button.textContent?.includes('Inbox')),
|
|
projectRailWidth: rail?.getBoundingClientRect().width,
|
|
projectRail: rect(rail),
|
|
channelSidebar: rect(sidebar),
|
|
stage: rect(stage),
|
|
inspector: rect(inspector),
|
|
workspaceActions,
|
|
dashboardButton: rect(dashboard),
|
|
dashboardButtonActive: dashboard?.classList.contains('active') ?? false,
|
|
dashboardUsesPanelIcon: Boolean(dashboard?.querySelector('.arco-icon-dashboard')),
|
|
dashboardUsesBrandImage: Boolean(dashboard?.querySelector('img')),
|
|
workspaceExploreButton: rect(workspaceExplore),
|
|
workspaceExploreButtonActive: workspaceExplore?.classList.contains('active') ?? false,
|
|
projectButton: rect(project),
|
|
projectButtonActive: project?.classList.contains('active') ?? false,
|
|
createProjectButton: rect(create),
|
|
firstProjectBadge: rect(firstBadge),
|
|
horizontalInsets,
|
|
verticalGaps,
|
|
projectRailOverflow: overflowState(rail),
|
|
projectRailChildrenOverflow: overflowState(railChildren),
|
|
channelSidebarOverflow: overflowState(sidebar),
|
|
channelListOverflow: overflowState(channelList),
|
|
stageOverflow: overflowState(stage),
|
|
inspectorOverflow: overflowState(inspector),
|
|
overflowX: document.documentElement.scrollWidth > document.documentElement.clientWidth,
|
|
viewportWidth: window.innerWidth,
|
|
}
|
|
})
|
|
|
|
await page.goto(`http://127.0.0.1:${port}/`, { waitUntil: 'networkidle' })
|
|
await page.screenshot({ path: 'test-results/login-react-acro.png', fullPage: true })
|
|
await page.locator('.login-form-panel .arco-btn-primary').click()
|
|
await page.waitForSelector('.workbench-shell')
|
|
await page.waitForTimeout(700)
|
|
await page.screenshot({ path: 'test-results/workbench-react-acro-light.png', fullPage: true })
|
|
const workspaceMetrics = await collectMetrics()
|
|
|
|
await page.locator('.dashboard-button[title="探索"]').click()
|
|
await page.waitForTimeout(500)
|
|
await page.screenshot({ path: 'test-results/workspace-explore-react-acro-light.png', fullPage: true })
|
|
const workspaceExploreMetrics = await collectMetrics()
|
|
|
|
await page.getByRole('button', { name: '添加数据源' }).click()
|
|
await page.waitForSelector('.explore-source-modal')
|
|
await page.waitForTimeout(250)
|
|
await page.screenshot({ path: 'test-results/explore-add-source-modal.png', fullPage: true })
|
|
const addSourceModalCheck = await page.evaluate(() => ({
|
|
title: document.querySelector('.explore-source-modal .arco-modal-title')?.textContent ?? '',
|
|
inputCount: document.querySelectorAll('.explore-source-modal input, .explore-source-modal textarea').length,
|
|
}))
|
|
await page.locator('.explore-source-modal input').first().fill('行业资讯')
|
|
await page.locator('.explore-source-modal .arco-modal-footer .arco-btn-primary').click()
|
|
const addedSourceVisible = await page.locator('.explore-source-card', { hasText: '行业资讯' }).count() === 1
|
|
|
|
await page.locator('.explore-source-card').nth(1).hover()
|
|
await page.getByRole('button', { name: '编辑手动收集' }).click()
|
|
await page.waitForSelector('.explore-source-modal')
|
|
await page.waitForTimeout(250)
|
|
await page.screenshot({ path: 'test-results/explore-edit-source-modal.png', fullPage: true })
|
|
const editSourceModalCheck = await page.evaluate(() => ({
|
|
title: document.querySelector('.explore-source-modal .arco-modal-title')?.textContent ?? '',
|
|
name: document.querySelector('.explore-source-modal input')?.value ?? '',
|
|
}))
|
|
await page.locator('.explore-source-modal input').first().fill('手动整理')
|
|
await page.locator('.explore-source-modal .arco-modal-footer .arco-btn-primary').click()
|
|
const editedSourceVisible = await page.locator('.explore-source-card', { hasText: '手动整理' }).count() === 1
|
|
|
|
await page.locator('.project-button').first().click()
|
|
await page.waitForTimeout(500)
|
|
await page.screenshot({ path: 'test-results/project-react-acro-light.png', fullPage: true })
|
|
const projectMetrics = await collectMetrics()
|
|
|
|
await page.locator('.channel-sidebar').hover()
|
|
const channelSidebarHoverMetrics = await collectMetrics()
|
|
await page.locator('.stage').hover()
|
|
const stageHoverMetrics = await collectMetrics()
|
|
await page.locator('.channel-list').hover()
|
|
const channelListHoverMetrics = await collectMetrics()
|
|
|
|
const channelPageChecks = []
|
|
let taskTagFilterCheck = null
|
|
let aiSessionCheck = null
|
|
for (const channel of [
|
|
{ label: '工作计划', pageClass: 'project-tasks-page', expectedHeading: '工作计划' },
|
|
{ label: 'AI 会话', pageClass: 'project-ai-page', expectedHeading: '' },
|
|
{ label: '笔记资料', pageClass: 'project-notes-page', expectedHeading: '笔记资料' },
|
|
{ label: '计划任务', pageClass: 'project-cron-page', expectedHeading: '计划任务' },
|
|
{ label: '新建频道', pageClass: 'project-new-channel-page', expectedHeading: '新建频道' },
|
|
]) {
|
|
await page.locator('.channel-button', { hasText: channel.label }).click()
|
|
await page.waitForTimeout(250)
|
|
const pageCheck = await page.evaluate((expected) => {
|
|
const pageNode = document.querySelector(`.${expected.pageClass}`)
|
|
const heading = pageNode?.querySelector('h4, h5')?.textContent ?? ''
|
|
const activeChannel = document.querySelector('.channel-button.active')?.textContent ?? ''
|
|
return {
|
|
...expected,
|
|
foundPage: Boolean(pageNode),
|
|
heading,
|
|
activeChannel,
|
|
inputCount: pageNode?.querySelectorAll('input, textarea').length ?? 0,
|
|
hasSaveChannel: [...(pageNode?.querySelectorAll('button') ?? [])].some((button) => button.textContent?.includes('保存频道')),
|
|
hasOverviewHead: Boolean(pageNode?.querySelector('.overview-head')),
|
|
hasSessionList: Boolean(pageNode?.querySelector('.agent-session-list')),
|
|
hasComposer: Boolean(pageNode?.querySelector('.agent-composer')),
|
|
hasExpertPicker: Boolean(pageNode?.querySelector('.agent-expert-picker')),
|
|
expertHeading: pageNode?.querySelector('.agent-expert-heading')?.textContent?.replace(/\s+/g, ' ').trim() ?? '',
|
|
}
|
|
}, channel)
|
|
|
|
if (channel.label === '工作计划') {
|
|
await page.getByRole('button', { name: '设计', exact: true }).click()
|
|
await page.waitForTimeout(150)
|
|
taskTagFilterCheck = await page.evaluate(() => ({
|
|
activeTag: document.querySelector('.task-tag-filter.active')?.textContent?.trim() ?? '',
|
|
pendingTitles: [...document.querySelectorAll('.task-plan-title')].map((node) => node.textContent?.trim()),
|
|
completedTitles: [...document.querySelectorAll('.completed-plan-title')].map((node) => node.textContent?.trim()),
|
|
pressed: document.querySelector('.task-tag-filter[aria-pressed="true"]')?.textContent?.trim() ?? '',
|
|
}))
|
|
await page.screenshot({ path: 'test-results/project-task-tag-filter-light.png', fullPage: true })
|
|
}
|
|
|
|
if (channel.label === 'AI 会话') {
|
|
await page.screenshot({ path: 'test-results/project-ai-expert-picker-light.png', fullPage: true })
|
|
await page.getByRole('button', { name: /产品经理/ }).click()
|
|
await page.locator('.agent-composer textarea').fill('请分析这个项目的下一步计划')
|
|
await page.getByRole('button', { name: '发送消息' }).click()
|
|
await page.waitForSelector('.agent-user-message')
|
|
aiSessionCheck = await page.evaluate(() => ({
|
|
userMessage: document.querySelector('.agent-user-message')?.textContent?.trim() ?? '',
|
|
activeSession: document.querySelector('.agent-session.active')?.textContent?.trim() ?? '',
|
|
composerValue: document.querySelector('.agent-composer textarea')?.value ?? '',
|
|
expertName: document.querySelector('.agent-assistant-name')?.textContent?.trim() ?? '',
|
|
}))
|
|
await page.screenshot({ path: 'test-results/project-ai-session-light.png', fullPage: true })
|
|
}
|
|
|
|
channelPageChecks.push(pageCheck)
|
|
}
|
|
|
|
await page.locator('.topbar-actions .arco-btn').first().click()
|
|
await page.screenshot({ path: 'test-results/project-react-acro-dark.png', fullPage: true })
|
|
|
|
await browser.close()
|
|
await server.close()
|
|
|
|
console.log(JSON.stringify({
|
|
workspaceMetrics,
|
|
workspaceExploreMetrics,
|
|
addSourceModalCheck,
|
|
addedSourceVisible,
|
|
editSourceModalCheck,
|
|
editedSourceVisible,
|
|
projectMetrics,
|
|
channelSidebarHoverMetrics,
|
|
stageHoverMetrics,
|
|
channelListHoverMetrics,
|
|
channelPageChecks,
|
|
taskTagFilterCheck,
|
|
aiSessionCheck,
|
|
errors,
|
|
}, null, 2))
|
|
|
|
const failures = []
|
|
const metrics = workspaceMetrics
|
|
if (errors.length) failures.push(`console errors: ${errors.join('; ')}`)
|
|
if (metrics.statusbarHeight !== 32) failures.push(`expected statusbar height 32, got ${metrics.statusbarHeight}`)
|
|
if (!metrics.statusName) failures.push('expected authenticated status label')
|
|
if (!metrics.statusOnlineDot) failures.push('expected legacy online status dot')
|
|
if (!metrics.statusUpgrade) failures.push('expected legacy upgrade action')
|
|
if (!metrics.statusSystemText.includes('3 个计划进行中') || !metrics.statusSystemText.includes('AI 空闲') || !metrics.statusSystemText.includes('152GB 可用')) {
|
|
failures.push(`expected legacy system status summary, got ${JSON.stringify(metrics.statusSystemText)}`)
|
|
}
|
|
if (metrics.searchHeight !== 42) failures.push(`expected search height 42, got ${metrics.searchHeight}`)
|
|
if (metrics.overflowX) failures.push('expected no horizontal overflow')
|
|
if (!metrics.workbenchMain) failures.push('missing workbench main')
|
|
if (!metrics.hasWorkspacePage) failures.push('expected login to land on workspace page')
|
|
if (!metrics.dashboardButtonActive) failures.push('expected dashboard button to be active after login')
|
|
if (!metrics.dashboardUsesPanelIcon || metrics.dashboardUsesBrandImage) failures.push('expected workspace entry to use the panel icon instead of the brand logo')
|
|
if (!metrics.workspaceExploreButton) failures.push('expected fixed workspace explore button to render in project rail')
|
|
if (metrics.workspaceExploreButtonActive) failures.push('expected fixed workspace explore button not to be active after login')
|
|
if (metrics.projectButtonActive) failures.push('expected first project button not to be active on workspace landing page')
|
|
if (metrics.channelSidebar) failures.push(`expected workspace to hide channel sidebar, got ${JSON.stringify(metrics.channelSidebar)}`)
|
|
if (metrics.inspector) failures.push(`expected workspace to hide inspector, got ${JSON.stringify(metrics.inspector)}`)
|
|
if (metrics.workspaceActions.some((text) => text?.includes('团队视图') || text?.includes('进入重点项目'))) {
|
|
failures.push(`expected workspace header actions to remove team view and focused project buttons, got ${JSON.stringify(metrics.workspaceActions)}`)
|
|
}
|
|
if (!metrics.projectRail || !metrics.stage) {
|
|
failures.push(`missing workspace layout regions: rail=${JSON.stringify(metrics.projectRail)}, stage=${JSON.stringify(metrics.stage)}`)
|
|
} else {
|
|
if (Math.abs(metrics.stage.left - metrics.projectRail.right) > 1) {
|
|
failures.push(`expected workspace stage to start after project rail, got stage.left=${metrics.stage.left}, rail.right=${metrics.projectRail.right}`)
|
|
}
|
|
if (Math.abs(metrics.stage.right - metrics.viewportWidth) > 1) {
|
|
failures.push(`expected workspace stage to end at viewport right edge, got stage.right=${metrics.stage.right}, viewport=${metrics.viewportWidth}`)
|
|
}
|
|
}
|
|
if (!metrics.projectRailWidth || metrics.projectRailWidth < 88) {
|
|
failures.push(`expected project rail at least 88px wide, got ${metrics.projectRailWidth}`)
|
|
}
|
|
if (!metrics.projectButton) failures.push('missing project button')
|
|
if (!metrics.horizontalInsets || Math.abs(metrics.horizontalInsets.left - metrics.horizontalInsets.right) > 1) {
|
|
failures.push(`expected equal rail horizontal insets, got ${JSON.stringify(metrics.horizontalInsets)}`)
|
|
}
|
|
if (metrics.verticalGaps.some((gap) => gap !== 12)) {
|
|
failures.push(`expected all project rail vertical gaps to be 12px, got ${JSON.stringify(metrics.verticalGaps)}`)
|
|
}
|
|
for (const [name, overflow] of [
|
|
['project rail', metrics.projectRailOverflow],
|
|
['project rail children', metrics.projectRailChildrenOverflow],
|
|
]) {
|
|
if (!overflow || overflow.overflowX !== 'hidden' || overflow.overflowY !== 'hidden') {
|
|
failures.push(`expected ${name} scrollbars to be hidden with no overflow, got ${JSON.stringify(overflow)}`)
|
|
}
|
|
}
|
|
if (
|
|
!metrics.firstProjectBadge ||
|
|
metrics.firstProjectBadge.right > metrics.projectRailWidth ||
|
|
metrics.firstProjectBadge.left < 0
|
|
) {
|
|
failures.push(`expected first project badge to stay inside the project rail, got ${JSON.stringify(metrics.firstProjectBadge)}`)
|
|
}
|
|
if (
|
|
!metrics.dashboardButton ||
|
|
metrics.dashboardButton.width !== metrics.projectButton?.width ||
|
|
metrics.dashboardButton.height !== metrics.projectButton?.height
|
|
) {
|
|
failures.push(`dashboard button size ${JSON.stringify(metrics.dashboardButton)} does not match project button ${JSON.stringify(metrics.projectButton)}`)
|
|
}
|
|
if (
|
|
!metrics.workspaceExploreButton ||
|
|
metrics.workspaceExploreButton.width !== metrics.projectButton?.width ||
|
|
metrics.workspaceExploreButton.height !== metrics.projectButton?.height
|
|
) {
|
|
failures.push(`workspace explore button size ${JSON.stringify(metrics.workspaceExploreButton)} does not match project button ${JSON.stringify(metrics.projectButton)}`)
|
|
}
|
|
if (
|
|
!metrics.createProjectButton ||
|
|
metrics.createProjectButton.width !== metrics.projectButton?.width ||
|
|
metrics.createProjectButton.height !== metrics.projectButton?.height
|
|
) {
|
|
failures.push(`create project button size ${JSON.stringify(metrics.createProjectButton)} does not match project button ${JSON.stringify(metrics.projectButton)}`)
|
|
}
|
|
|
|
if (!workspaceExploreMetrics.workspaceExploreButtonActive) failures.push('expected fixed workspace explore button to become active when selected')
|
|
if (workspaceExploreMetrics.dashboardButtonActive) failures.push('expected dashboard button not to be active on workspace explore page')
|
|
if (workspaceExploreMetrics.channelSidebar) failures.push(`expected workspace explore to hide channel sidebar, got ${JSON.stringify(workspaceExploreMetrics.channelSidebar)}`)
|
|
if (workspaceExploreMetrics.inspector) failures.push(`expected workspace explore to hide inspector, got ${JSON.stringify(workspaceExploreMetrics.inspector)}`)
|
|
if (workspaceExploreMetrics.exploreSourceCardCount !== 4) failures.push(`expected four legacy explore source cards, got ${workspaceExploreMetrics.exploreSourceCardCount}`)
|
|
if (!workspaceExploreMetrics.hasExploreReader) failures.push('expected legacy explore article reader to render')
|
|
if (!addSourceModalCheck.title.includes('添加数据源') || addSourceModalCheck.inputCount < 3) {
|
|
failures.push(`expected add data source modal with fields, got ${JSON.stringify(addSourceModalCheck)}`)
|
|
}
|
|
if (!addedSourceVisible) failures.push('expected saved data source to appear in the source cards')
|
|
if (!editSourceModalCheck.title.includes('编辑数据源') || editSourceModalCheck.name !== '手动收集') {
|
|
failures.push(`expected prefilled edit data source modal, got ${JSON.stringify(editSourceModalCheck)}`)
|
|
}
|
|
if (!editedSourceVisible) failures.push('expected edited data source name to update its card')
|
|
if (!workspaceExploreMetrics.stage || !workspaceExploreMetrics.projectRail) {
|
|
failures.push(`missing workspace explore layout regions: rail=${JSON.stringify(workspaceExploreMetrics.projectRail)}, stage=${JSON.stringify(workspaceExploreMetrics.stage)}`)
|
|
} else if (Math.abs(workspaceExploreMetrics.stage.left - workspaceExploreMetrics.projectRail.right) > 1) {
|
|
failures.push(`expected workspace explore stage to start after project rail, got stage.left=${workspaceExploreMetrics.stage.left}, rail.right=${workspaceExploreMetrics.projectRail.right}`)
|
|
}
|
|
|
|
if (!projectMetrics.hasProjectOverview) failures.push('expected first project click to show project overview page')
|
|
if (projectMetrics.hasInboxChannel) failures.push('expected removed Inbox message stream to stay hidden from project channels')
|
|
if (projectMetrics.hasWorkspacePage) failures.push('expected project overview mode to leave workspace page')
|
|
if (!projectMetrics.projectButtonActive) failures.push('expected first project button to be active in project mode')
|
|
if (projectMetrics.dashboardButtonActive) failures.push('expected dashboard button not to be active in project mode')
|
|
if (projectMetrics.inspector) {
|
|
failures.push(`expected project page to remove right inspector until it becomes an on-demand panel, got ${JSON.stringify(projectMetrics.inspector)}`)
|
|
}
|
|
if (!projectMetrics.projectRail || !projectMetrics.channelSidebar || !projectMetrics.stage) {
|
|
failures.push(
|
|
`missing project layout regions: rail=${JSON.stringify(projectMetrics.projectRail)}, sidebar=${JSON.stringify(projectMetrics.channelSidebar)}, stage=${JSON.stringify(projectMetrics.stage)}`,
|
|
)
|
|
} else {
|
|
const sameTop = [projectMetrics.projectRail, projectMetrics.channelSidebar, projectMetrics.stage]
|
|
.every((region) => Math.abs(region.top - projectMetrics.projectRail.top) <= 1)
|
|
if (!sameTop) {
|
|
failures.push(
|
|
`expected project rail, channel sidebar, and stage to share the same top edge, got rail=${projectMetrics.projectRail.top}, sidebar=${projectMetrics.channelSidebar.top}, stage=${projectMetrics.stage.top}`,
|
|
)
|
|
}
|
|
if (Math.abs(projectMetrics.channelSidebar.left - projectMetrics.projectRail.right) > 1) {
|
|
failures.push(`expected channel sidebar to sit after project rail, got sidebar.left=${projectMetrics.channelSidebar.left}, rail.right=${projectMetrics.projectRail.right}`)
|
|
}
|
|
if (Math.abs(projectMetrics.stage.left - projectMetrics.channelSidebar.right) > 1) {
|
|
failures.push(`expected stage to sit after channel sidebar, got stage.left=${projectMetrics.stage.left}, sidebar.right=${projectMetrics.channelSidebar.right}`)
|
|
}
|
|
if (Math.abs(projectMetrics.stage.right - projectMetrics.viewportWidth) > 1) {
|
|
failures.push(`expected project stage to end at viewport right edge, got stage.right=${projectMetrics.stage.right}, viewport=${projectMetrics.viewportWidth}`)
|
|
}
|
|
}
|
|
for (const [name, overflow] of [
|
|
['channel sidebar', projectMetrics.channelSidebarOverflow],
|
|
['channel list', projectMetrics.channelListOverflow],
|
|
['stage', projectMetrics.stageOverflow],
|
|
]) {
|
|
if (!overflow || overflow.overflowX !== 'hidden' || overflow.overflowY !== 'auto') {
|
|
failures.push(`expected ${name} to hide horizontal scrollbars and show vertical scrollbars only when needed, got ${JSON.stringify(overflow)}`)
|
|
}
|
|
if (overflow?.scrollbarWidth !== 'none') {
|
|
failures.push(`expected ${name} scrollbar to be hidden until hover, got ${JSON.stringify(overflow)}`)
|
|
}
|
|
}
|
|
for (const [name, overflow] of [
|
|
['channel sidebar hover', channelSidebarHoverMetrics.channelSidebarOverflow],
|
|
['channel list hover', channelListHoverMetrics.channelListOverflow],
|
|
['stage hover', stageHoverMetrics.stageOverflow],
|
|
]) {
|
|
if (overflow?.scrollbarWidth !== 'thin') {
|
|
failures.push(`expected ${name} scrollbar to appear on hover, got ${JSON.stringify(overflow)}`)
|
|
}
|
|
}
|
|
for (const check of channelPageChecks) {
|
|
if (!check.foundPage) {
|
|
failures.push(`expected ${check.label} to render .${check.pageClass}`)
|
|
}
|
|
if (check.expectedHeading && !check.heading.includes(check.expectedHeading)) {
|
|
failures.push(`expected ${check.label} heading to include ${check.expectedHeading}, got ${JSON.stringify(check.heading)}`)
|
|
}
|
|
if (!check.activeChannel.includes(check.label)) {
|
|
failures.push(`expected ${check.label} sidebar button to stay active, got ${JSON.stringify(check.activeChannel)}`)
|
|
}
|
|
if (check.label === '新建频道' && (check.inputCount < 4 || !check.hasSaveChannel)) {
|
|
failures.push(`expected legacy new channel editor, got inputs=${check.inputCount}, save=${check.hasSaveChannel}`)
|
|
}
|
|
if (check.label === 'AI 会话' && (
|
|
check.hasOverviewHead || !check.hasSessionList || !check.hasComposer || !check.hasExpertPicker ||
|
|
!check.expertHeading.includes('选择专家进行会话')
|
|
)) {
|
|
failures.push(`expected headerless AI session chat layout, got ${JSON.stringify(check)}`)
|
|
}
|
|
}
|
|
|
|
if (
|
|
!taskTagFilterCheck ||
|
|
taskTagFilterCheck.activeTag !== '设计' ||
|
|
taskTagFilterCheck.pressed !== '设计' ||
|
|
taskTagFilterCheck.pendingTitles.join('|') !== '优化会话界面' ||
|
|
taskTagFilterCheck.completedTitles.length !== 0
|
|
) {
|
|
failures.push(`expected clickable task tags to filter pending and completed plans, got ${JSON.stringify(taskTagFilterCheck)}`)
|
|
}
|
|
if (
|
|
!aiSessionCheck ||
|
|
aiSessionCheck.userMessage !== '请分析这个项目的下一步计划' ||
|
|
!aiSessionCheck.activeSession.includes('请分析这个项目的下一步计划') ||
|
|
aiSessionCheck.composerValue !== '' ||
|
|
aiSessionCheck.expertName !== '产品经理'
|
|
) {
|
|
failures.push(`expected AI composer to create and select a session, got ${JSON.stringify(aiSessionCheck)}`)
|
|
}
|
|
|
|
if (failures.length) {
|
|
throw new Error(failures.join('\n'))
|
|
}
|