feat(web): add responsive workbench navigation
This commit is contained in:
@@ -22,6 +22,56 @@ page.on('console', (message) => {
|
||||
if (message.type() === 'error') errors.push(message.text())
|
||||
})
|
||||
|
||||
const visualCheckWorkspace = {
|
||||
project: {
|
||||
id: 1,
|
||||
name: '森林项目',
|
||||
identifier: 'forest',
|
||||
icon: '森',
|
||||
background: '#165DFF',
|
||||
description: '移动导航视觉检查项目',
|
||||
initials: '森林',
|
||||
unreadCount: 2,
|
||||
},
|
||||
channels: [
|
||||
{ id: 'overview', projectID: 1, type: 'overview', title: '概况', icon: 'home', sortOrder: 0 },
|
||||
{ id: 'tasks', projectID: 1, type: 'tasks', title: '工作计划', icon: 'list', count: 1, sortOrder: 1 },
|
||||
{ id: 'ai', projectID: 1, type: 'ai_sessions', title: 'AI 助手', icon: 'robot', sortOrder: 2 },
|
||||
{ id: 'notes', projectID: 1, type: 'notes_sources', title: '笔记资料', icon: 'file', sortOrder: 3 },
|
||||
{ id: 'cron', projectID: 1, type: 'cron', title: '计划任务', icon: 'clock', sortOrder: 4 },
|
||||
],
|
||||
tags: ['all', '产品'],
|
||||
recentSessions: [
|
||||
{ id: 'session-1', title: '移动导航会话', summary: '检查抽屉关闭行为', updatedAt: '今天 10:00', references: [] },
|
||||
],
|
||||
inbox: [],
|
||||
tasks: [],
|
||||
aiSessions: [],
|
||||
notesSources: [],
|
||||
cronPlans: [],
|
||||
}
|
||||
|
||||
await page.route('http://localhost:9150/api/**', async (route) => {
|
||||
const url = new URL(route.request().url())
|
||||
if (url.pathname === '/api/status') {
|
||||
await route.fulfill({ json: { timestamp_ms: Date.now() } })
|
||||
return
|
||||
}
|
||||
if (url.pathname === '/api/auth/login') {
|
||||
await route.fulfill({ json: { token: 'visual-check-token' } })
|
||||
return
|
||||
}
|
||||
if (url.pathname === '/api/projects') {
|
||||
await route.fulfill({ json: [{ ID: 1, Name: visualCheckWorkspace.project.name }] })
|
||||
return
|
||||
}
|
||||
if (url.pathname === '/api/projects/1/workspace') {
|
||||
await route.fulfill({ json: visualCheckWorkspace })
|
||||
return
|
||||
}
|
||||
await route.fulfill({ status: 404, json: { error: `unmocked visual check route: ${url.pathname}` } })
|
||||
})
|
||||
|
||||
const collectLoginMetrics = async () => page.evaluate(() => {
|
||||
const loginCard = document.querySelector('.login-card')
|
||||
|
||||
@@ -37,8 +87,14 @@ const getColumnCount = (columns) => columns === 'none'
|
||||
? 0
|
||||
: columns.split(' ').filter(Boolean).length
|
||||
|
||||
const waitForDrawerOpen = async (selector) => page.waitForFunction((drawerSelector) => {
|
||||
const drawer = document.querySelector(drawerSelector)
|
||||
return drawer?.classList.contains('open') && Math.abs(drawer.getBoundingClientRect().left) < 1
|
||||
}, selector)
|
||||
|
||||
const collectMetrics = async () => page.evaluate(() => {
|
||||
const statusbar = document.querySelector('.statusbar')
|
||||
const statusUser = document.querySelector('.status-user')
|
||||
const statusName = document.querySelector('.status-name')
|
||||
const statusOnlineDot = document.querySelector('.status-online-dot')
|
||||
const search = document.querySelector('.global-search')
|
||||
@@ -86,6 +142,8 @@ const collectMetrics = async () => page.evaluate(() => {
|
||||
|
||||
return {
|
||||
statusbarHeight: statusbar?.getBoundingClientRect().height,
|
||||
statusUserTag: statusUser?.tagName,
|
||||
nestedButtonCount: document.querySelectorAll('button button').length,
|
||||
statusName: rect(statusName),
|
||||
statusOnlineDot: rect(statusOnlineDot),
|
||||
searchHeight: search?.getBoundingClientRect().height,
|
||||
@@ -154,6 +212,12 @@ await page.waitForTimeout(700)
|
||||
await page.screenshot({ path: 'test-results/workbench-react-acro-light.png', fullPage: true })
|
||||
const workspaceMetrics = await collectMetrics()
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
if (await page.getByRole('button', { name: '打开频道导航', exact: true }).count() !== 0) {
|
||||
failures.push('workspace mobile view must not offer channel navigation without a channel aside')
|
||||
}
|
||||
await page.setViewportSize({ width: 1440, height: 1024 })
|
||||
|
||||
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 })
|
||||
@@ -164,6 +228,39 @@ await page.waitForTimeout(500)
|
||||
await page.screenshot({ path: 'test-results/project-react-acro-light.png', fullPage: true })
|
||||
const projectMetrics = await collectMetrics()
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
const mobileProjectMetrics = await collectMetrics()
|
||||
const projectNavButton = page.getByRole('button', { name: '打开项目导航', exact: true })
|
||||
if (await projectNavButton.count() === 0) {
|
||||
failures.push('missing mobile button 打开项目导航')
|
||||
} else {
|
||||
await projectNavButton.click()
|
||||
await waitForDrawerOpen('aside.project-rail')
|
||||
await page.screenshot({ path: 'test-results/project-navigation-mobile.png', fullPage: true })
|
||||
if (!await page.locator('aside.project-rail.open').isVisible()) {
|
||||
failures.push('打开项目导航 must reveal the project rail aside')
|
||||
}
|
||||
await page.getByRole('button', { name: '关闭导航', exact: true }).click({ position: { x: 380, y: 20 } })
|
||||
}
|
||||
|
||||
const channelNavButton = page.getByRole('button', { name: '打开频道导航', exact: true })
|
||||
if (await channelNavButton.count() === 0) {
|
||||
failures.push('missing mobile button 打开频道导航')
|
||||
} else {
|
||||
await channelNavButton.click()
|
||||
await waitForDrawerOpen('aside.channel-sidebar')
|
||||
await page.screenshot({ path: 'test-results/channel-navigation-mobile.png', fullPage: true })
|
||||
if (!await page.locator('aside.channel-sidebar.open').isVisible()) {
|
||||
failures.push('打开频道导航 must reveal the channel sidebar aside')
|
||||
}
|
||||
await page.locator('.recent-sessions button').first().click()
|
||||
if (await page.locator('aside.channel-sidebar.open').count() !== 0) {
|
||||
failures.push('selecting a recent session must close the channel drawer')
|
||||
await page.getByRole('button', { name: '关闭导航', exact: true }).click({ position: { x: 380, y: 20 } })
|
||||
}
|
||||
}
|
||||
await page.setViewportSize({ width: 1440, height: 1024 })
|
||||
|
||||
await page.locator('.channel-sidebar').hover()
|
||||
const channelSidebarHoverMetrics = await collectMetrics()
|
||||
await page.locator('.stage').hover()
|
||||
@@ -174,7 +271,7 @@ const channelListHoverMetrics = await collectMetrics()
|
||||
const channelPageChecks = []
|
||||
for (const channel of [
|
||||
{ label: '工作计划', pageClass: 'project-tasks-page', expectedHeading: '工作计划' },
|
||||
{ label: 'AI智能体', pageClass: 'project-ai-page', expectedHeading: 'AI智能体' },
|
||||
{ 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: '新建频道' },
|
||||
@@ -183,7 +280,7 @@ for (const channel of [
|
||||
await page.waitForTimeout(250)
|
||||
channelPageChecks.push(await page.evaluate((expected) => {
|
||||
const pageNode = document.querySelector(`.${expected.pageClass}`)
|
||||
const heading = pageNode?.querySelector('h4, h5')?.textContent ?? ''
|
||||
const heading = pageNode?.querySelector('h2, h3, h4, h5')?.textContent ?? ''
|
||||
const activeChannel = document.querySelector('.channel-button.active')?.textContent ?? ''
|
||||
return {
|
||||
...expected,
|
||||
@@ -194,7 +291,7 @@ for (const channel of [
|
||||
}, channel))
|
||||
}
|
||||
|
||||
await page.locator('.topbar-actions .arco-btn').first().click()
|
||||
await page.getByRole('button', { name: '切换深色模式', exact: true }).click()
|
||||
await page.screenshot({ path: 'test-results/project-react-acro-dark.png', fullPage: true })
|
||||
|
||||
await browser.close()
|
||||
@@ -204,6 +301,7 @@ console.log(JSON.stringify({
|
||||
workspaceMetrics,
|
||||
workspaceExploreMetrics,
|
||||
projectMetrics,
|
||||
mobileProjectMetrics,
|
||||
channelSidebarHoverMetrics,
|
||||
stageHoverMetrics,
|
||||
channelListHoverMetrics,
|
||||
@@ -214,10 +312,8 @@ console.log(JSON.stringify({
|
||||
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.statusOnlineDot) failures.push('expected online status to render as an icon dot')
|
||||
if (metrics.statusName && metrics.statusOnlineDot && Math.abs(metrics.statusName.top - metrics.statusOnlineDot.top) > 6) {
|
||||
failures.push(`expected online dot to sit beside username, got name=${JSON.stringify(metrics.statusName)}, dot=${JSON.stringify(metrics.statusOnlineDot)}`)
|
||||
}
|
||||
if (metrics.statusUserTag !== 'DIV') failures.push(`status user must be a non-button DIV container, got ${metrics.statusUserTag}`)
|
||||
if (metrics.nestedButtonCount !== 0) failures.push(`expected no nested buttons, got ${metrics.nestedButtonCount}`)
|
||||
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')
|
||||
@@ -305,6 +401,7 @@ if (projectMetrics.dashboardButtonActive) failures.push('expected dashboard butt
|
||||
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 (mobileProjectMetrics.overflowX) failures.push('mobile project viewport must not overflow horizontally')
|
||||
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)}`,
|
||||
|
||||
Reference in New Issue
Block a user