feat: connect search and project settings

This commit is contained in:
2026-07-21 17:16:45 +08:00
parent bc1cd19c51
commit 110423406b
15 changed files with 825 additions and 139 deletions

View File

@@ -19,8 +19,23 @@ const page = await browser.newPage({ viewport: { width: 1440, height: 1024 }, de
const errors = []
const failures = []
const projectId = '019b0000-0000-7000-8000-000000000001'
const taskSearchResultId = '019b0000-0000-7000-8000-000000000003'
const unknownProjectId = '019b0000-0000-7000-8000-000000000099'
const searchRequests = []
const projectPatchRequests = []
let expectingProjectPatchError = false
let expectedProjectPatchConsoleErrorCount = 0
page.on('console', (message) => {
if (message.type() === 'error') errors.push(message.text())
if (message.type() !== 'error') return
if (
expectingProjectPatchError &&
expectedProjectPatchConsoleErrorCount === 0 &&
message.text() === 'Failed to load resource: the server responded with a status of 500 (Internal Server Error)'
) {
expectedProjectPatchConsoleErrorCount += 1
return
}
errors.push(message.text())
})
const visualCheckWorkspace = {
@@ -54,6 +69,7 @@ const visualCheckWorkspace = {
await page.route('http://localhost:9150/api/v1/**', async (route) => {
const url = new URL(route.request().url())
const method = route.request().method()
if (url.pathname === '/api/v1/status') {
await route.fulfill({ json: { timestamp: new Date().toISOString() } })
return
@@ -70,6 +86,39 @@ await page.route('http://localhost:9150/api/v1/**', async (route) => {
await route.fulfill({ json: visualCheckWorkspace })
return
}
if (url.pathname === '/api/v1/search') {
const query = url.searchParams.get('q') ?? ''
searchRequests.push(query)
if (query === '慢请求') await new Promise((resolve) => setTimeout(resolve, 300))
const projectIdForResult = query === '未知项目' ? unknownProjectId : projectId
await route.fulfill({
json: {
items: [{
id: taskSearchResultId,
type: 'task',
title: query === '未知项目' ? '未知项目任务' : query === '慢请求' ? '慢请求任务' : query === '快请求' ? '快请求任务' : '回调任务',
projectId: projectIdForResult,
snippet: '检查签名',
}],
},
})
return
}
if (url.pathname === `/api/v1/projects/${projectId}` && method === 'PATCH') {
const input = route.request().postDataJSON()
projectPatchRequests.push(input)
if (input.description === '触发失败') {
await new Promise((resolve) => setTimeout(resolve, 250))
await route.fulfill({
status: 500,
json: { error: { code: 'internal_error', message: '项目设置保存失败,请稍后重试' } },
})
return
}
Object.assign(visualCheckWorkspace.project, input)
await route.fulfill({ json: visualCheckWorkspace.project })
return
}
await route.fulfill({ status: 404, json: { error: { code: 'not_found', message: '视觉检查未配置该接口' } } })
})
@@ -282,6 +331,95 @@ if (await channelNavButton.count() === 0) {
}
await page.setViewportSize({ width: 1440, height: 1024 })
const searchInput = page.getByPlaceholder('搜索项目、任务和笔记')
const searchButton = page.getByRole('button', { name: '搜索', exact: true })
if (await searchInput.count() === 0 || await searchButton.count() === 0) {
failures.push('topbar must expose the connected project/task/note search controls')
} else {
await searchInput.fill('回调')
await searchInput.press('Enter')
await page.waitForTimeout(150)
if (!searchRequests.includes('回调')) failures.push(`search Enter must request q=回调, got ${JSON.stringify(searchRequests)}`)
const callbackResult = page.getByRole('button', { name: /回调任务/ })
if (await callbackResult.count() === 0) {
failures.push('search results must render in a selectable dropdown list')
} else {
await callbackResult.click()
const activeAfterSearch = await page.locator('.channel-button.active').textContent()
if (!activeAfterSearch?.includes('工作计划')) {
failures.push(`task search result must navigate to 工作计划, got ${JSON.stringify(activeAfterSearch)}`)
}
}
await searchInput.fill('未知项目')
await searchButton.click()
await page.waitForTimeout(150)
const unknownResult = page.getByRole('button', { name: /未知项目任务/ })
if (await unknownResult.count() === 0) {
failures.push('Search button must submit the current query and show results')
} else {
const projectBeforeUnknownResult = await page.locator('.project-title h5').textContent()
await unknownResult.click()
const projectAfterUnknownResult = await page.locator('.project-title h5').textContent()
if (projectAfterUnknownResult !== projectBeforeUnknownResult) {
failures.push('a result for an unavailable project must not fabricate project navigation')
}
}
await searchInput.fill('慢请求')
await searchInput.press('Enter')
await page.waitForTimeout(30)
await searchInput.fill('快请求')
await searchInput.press('Enter')
await page.waitForTimeout(400)
if (await page.getByRole('button', { name: /慢请求任务/ }).count() !== 0) {
failures.push('a stale search response must not replace newer results')
}
if (await page.getByRole('button', { name: /快请求任务/ }).count() === 0) {
failures.push('the newest search response must remain visible after requests resolve out of order')
}
await searchInput.fill('')
}
const settingsButton = page.getByRole('button', { name: '编辑项目设置', exact: true })
if (await settingsButton.count() === 0) {
failures.push('project sidebar must expose an accessible project settings button')
} else {
await settingsButton.click()
let settingsModal = page.getByRole('dialog', { name: '编辑项目' })
await settingsModal.getByLabel('名称').fill('森林项目已更新')
await settingsModal.getByRole('button', { name: '确定', exact: true }).click()
await settingsModal.waitFor({ state: 'hidden', timeout: 2000 }).catch(() => {})
if (projectPatchRequests[0]?.name !== '森林项目已更新') {
failures.push(`project settings must PATCH the edited identity, got ${JSON.stringify(projectPatchRequests)}`)
}
if (await page.locator('.project-title h5').textContent() !== '森林项目已更新') {
failures.push('successful project settings update must reload the same project identity')
}
if (await settingsModal.isVisible()) failures.push('successful project settings update must close the modal')
await settingsButton.click()
settingsModal = page.getByRole('dialog', { name: '编辑项目' })
await settingsModal.getByLabel('简介').fill('触发失败')
expectingProjectPatchError = true
await settingsModal.getByRole('button', { name: '确定', exact: true }).click()
await page.waitForTimeout(50)
const confirmButton = settingsModal.getByRole('button', { name: '确定', exact: true })
if (!await confirmButton.evaluate((button) => button.classList.contains('arco-btn-loading'))) {
failures.push('project settings confirmation must show loading while PATCH is pending')
}
await page.waitForTimeout(300)
if (!await settingsModal.isVisible()) failures.push('failed project settings update must keep the modal open')
if (!await settingsModal.getByText('项目设置保存失败,请稍后重试', { exact: true }).isVisible()) {
failures.push('failed project settings update must render the API Chinese error inside the modal')
}
expectingProjectPatchError = false
if (expectedProjectPatchConsoleErrorCount !== 1) {
failures.push(`expected one simulated PATCH console error, got ${expectedProjectPatchConsoleErrorCount}`)
}
await settingsModal.getByRole('button', { name: '取消', exact: true }).click()
}
await page.locator('.channel-sidebar').hover()
const channelSidebarHoverMetrics = await collectMetrics()
await page.locator('.stage').hover()