220 lines
11 KiB
JavaScript
220 lines
11 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 = []
|
|
page.on('console', (message) => {
|
|
if (message.type() === 'error') errors.push(message.text())
|
|
})
|
|
|
|
const collectMetrics = async () => page.evaluate(() => {
|
|
const statusbar = document.querySelector('.statusbar')
|
|
const statusName = document.querySelector('.status-name')
|
|
const statusOnlineDot = document.querySelector('.status-online-dot')
|
|
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 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 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,
|
|
}
|
|
: null
|
|
|
|
return {
|
|
statusbarHeight: statusbar?.getBoundingClientRect().height,
|
|
statusName: rect(statusName),
|
|
statusOnlineDot: rect(statusOnlineDot),
|
|
searchHeight: search?.getBoundingClientRect().height,
|
|
workbenchMain: rect(main),
|
|
hasWorkspacePage: Boolean(workspacePage),
|
|
hasProjectOverview: Boolean(projectOverview),
|
|
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,
|
|
projectButton: rect(project),
|
|
projectButtonActive: project?.classList.contains('active') ?? false,
|
|
createProjectButton: rect(create),
|
|
firstProjectBadge: rect(firstBadge),
|
|
horizontalInsets,
|
|
verticalGaps,
|
|
projectRailOverflow: overflowState(rail),
|
|
projectRailChildrenOverflow: overflowState(railChildren),
|
|
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('.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('.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, projectMetrics, 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.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.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.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.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 (!projectMetrics.hasProjectOverview) failures.push('expected first project click to show project overview page')
|
|
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.projectRail || !projectMetrics.channelSidebar || !projectMetrics.stage || !projectMetrics.inspector) {
|
|
failures.push(
|
|
`missing project layout regions: rail=${JSON.stringify(projectMetrics.projectRail)}, sidebar=${JSON.stringify(projectMetrics.channelSidebar)}, stage=${JSON.stringify(projectMetrics.stage)}, inspector=${JSON.stringify(projectMetrics.inspector)}`,
|
|
)
|
|
} else {
|
|
const sameTop = [projectMetrics.projectRail, projectMetrics.channelSidebar, projectMetrics.stage, projectMetrics.inspector]
|
|
.every((region) => Math.abs(region.top - projectMetrics.projectRail.top) <= 1)
|
|
if (!sameTop) {
|
|
failures.push(
|
|
`expected project rail, channel sidebar, stage, and inspector to share the same top edge, got rail=${projectMetrics.projectRail.top}, sidebar=${projectMetrics.channelSidebar.top}, stage=${projectMetrics.stage.top}, inspector=${projectMetrics.inspector.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.inspector.left - projectMetrics.stage.right) > 1) {
|
|
failures.push(`expected inspector to sit after stage, got inspector.left=${projectMetrics.inspector.left}, stage.right=${projectMetrics.stage.right}`)
|
|
}
|
|
if (Math.abs(projectMetrics.inspector.right - projectMetrics.viewportWidth) > 1) {
|
|
failures.push(`expected inspector to end at viewport right edge, got inspector.right=${projectMetrics.inspector.right}, viewport=${projectMetrics.viewportWidth}`)
|
|
}
|
|
}
|
|
|
|
if (failures.length) {
|
|
throw new Error(failures.join('\n'))
|
|
}
|