mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-21 21:47:00 +08:00
feat: 统一界面视觉规范并优化导出交互
This commit is contained in:
@@ -16,6 +16,32 @@ const baseProps = {
|
||||
}
|
||||
|
||||
describe('export progress panel', () => {
|
||||
it('uses the semantic accent surface for outgoing preview messages', () => {
|
||||
render(
|
||||
<ExportPreviewPanel
|
||||
{...baseProps}
|
||||
status="idle"
|
||||
previewItems={[
|
||||
{
|
||||
id: 'outgoing-preview',
|
||||
from: 'self',
|
||||
type: '文字',
|
||||
datetime: '',
|
||||
content: '已发送的消息',
|
||||
isSender: true
|
||||
}
|
||||
]}
|
||||
progress={null}
|
||||
includeVoiceTranscripts={false}
|
||||
zip={false}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByText('已发送的消息').closest('.export-preview-bubble')).toHaveClass(
|
||||
'bg-accent'
|
||||
)
|
||||
})
|
||||
|
||||
it('shows an indeterminate bar while the first message scan is still at zero', () => {
|
||||
render(
|
||||
<ExportPreviewPanel
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Switch
|
||||
} from '../../src/renderer/src/components/ui'
|
||||
|
||||
describe('TraceMemo UI control states', () => {
|
||||
it('uses the compact, standard, and form control height contract', () => {
|
||||
render(
|
||||
<>
|
||||
<Button size="sm">紧凑</Button>
|
||||
<Button>标准</Button>
|
||||
<Button size="lg">表单</Button>
|
||||
<Input aria-label="表单输入" />
|
||||
</>
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: '紧凑' })).toHaveClass('h-control-compact')
|
||||
expect(screen.getByRole('button', { name: '标准' })).toHaveClass('h-control-standard')
|
||||
expect(screen.getByRole('button', { name: '表单' })).toHaveClass('h-control-form')
|
||||
expect(screen.getByRole('textbox', { name: '表单输入' })).toHaveClass('h-control-form')
|
||||
})
|
||||
|
||||
it('keeps disabled controls readable without the old opacity and surface shadow', () => {
|
||||
render(
|
||||
<>
|
||||
<Button disabled>不可用操作</Button>
|
||||
<Input aria-label="不可用输入" value="仍可读取" disabled readOnly />
|
||||
<Checkbox aria-label="不可用多选" disabled />
|
||||
<RadioGroup aria-label="不可用单选组">
|
||||
<RadioGroupItem value="disabled" aria-label="不可用单选" disabled />
|
||||
</RadioGroup>
|
||||
<Switch aria-label="不可用开关" disabled />
|
||||
</>
|
||||
)
|
||||
|
||||
for (const control of [
|
||||
screen.getByRole('button', { name: '不可用操作' }),
|
||||
screen.getByRole('textbox', { name: '不可用输入' }),
|
||||
screen.getByRole('checkbox', { name: '不可用多选' }),
|
||||
screen.getByRole('radio', { name: '不可用单选' }),
|
||||
screen.getByRole('switch', { name: '不可用开关' })
|
||||
]) {
|
||||
expect(control).toHaveClass('disabled:opacity-100')
|
||||
expect(control).not.toHaveClass('shadow-surface')
|
||||
}
|
||||
|
||||
expect(screen.getByRole('button', { name: '不可用操作' })).toHaveClass(
|
||||
'disabled:bg-disabled-surface',
|
||||
'disabled:text-disabled-foreground',
|
||||
'disabled:!text-disabled-foreground'
|
||||
)
|
||||
expect(screen.getByRole('checkbox', { name: '不可用多选' })).toHaveClass(
|
||||
'border-muted-foreground/60'
|
||||
)
|
||||
expect(screen.getByRole('radio', { name: '不可用单选' })).toHaveClass(
|
||||
'border-muted-foreground/60'
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps selection semantics while removing text glyph indicators', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<>
|
||||
<Select defaultValue="recent">
|
||||
<SelectTrigger aria-label="时间范围">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="recent">最近 30 天</SelectItem>
|
||||
<SelectItem value="all">全部时间</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Checkbox aria-label="包含图片" />
|
||||
<RadioGroup aria-label="密度" defaultValue="compact">
|
||||
<RadioGroupItem value="compact" aria-label="紧凑密度" />
|
||||
<RadioGroupItem value="comfortable" aria-label="舒适密度" />
|
||||
</RadioGroup>
|
||||
<Switch aria-label="显示头像" />
|
||||
</>
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('combobox', { name: '时间范围' })
|
||||
expect(trigger).not.toHaveTextContent('⌄')
|
||||
await user.click(trigger)
|
||||
expect(await screen.findByRole('option', { name: '最近 30 天' })).not.toHaveTextContent('✓')
|
||||
await user.keyboard('{Escape}')
|
||||
|
||||
const checkbox = screen.getByRole('checkbox', { name: '包含图片' })
|
||||
await user.click(checkbox)
|
||||
expect(checkbox).toBeChecked()
|
||||
expect(checkbox).not.toHaveTextContent('✓')
|
||||
|
||||
await user.click(screen.getByRole('radio', { name: '舒适密度' }))
|
||||
expect(screen.getByRole('radio', { name: '舒适密度' })).toBeChecked()
|
||||
|
||||
const toggle = screen.getByRole('switch', { name: '显示头像' })
|
||||
await user.click(toggle)
|
||||
expect(toggle).toBeChecked()
|
||||
})
|
||||
})
|
||||
@@ -649,6 +649,62 @@ test('EXPORT-01 multi-chat selection stays local to export and forces HTML', asy
|
||||
}
|
||||
})
|
||||
|
||||
test('EXPORT-02 large contact list stays bounded and searchable', async () => {
|
||||
const fixture = await launchTestApp({ largeContacts: 1500 })
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize({ width: 1400, height: 772 })
|
||||
await fixture.page.getByRole('button', { name: '导出' }).click()
|
||||
await expect(fixture.page.getByText('共 1,503 个')).toBeVisible()
|
||||
|
||||
const contactList = fixture.page.locator('.export-contact-list')
|
||||
expect(await contactList.getByRole('button').count()).toBeLessThan(50)
|
||||
|
||||
const compositeButtons = [
|
||||
fixture.page.getByRole('button', { name: /^全部导出/ }),
|
||||
fixture.page.getByRole('button', { name: /HTML/ }),
|
||||
fixture.page.locator('.export-workspace > aside:first-child > button:last-child')
|
||||
]
|
||||
for (const button of compositeButtons) {
|
||||
await expect(button).toBeVisible()
|
||||
expect(
|
||||
await button.evaluate(
|
||||
(element) => element.scrollHeight <= element.clientHeight && element.clientHeight > 32
|
||||
)
|
||||
).toBe(true)
|
||||
}
|
||||
|
||||
const previewGeometryIsStable = await fixture.page
|
||||
.locator('.export-workspace > aside:last-child')
|
||||
.evaluate((previewPanel) => {
|
||||
const scrollRegion = previewPanel.children.item(1)
|
||||
const statistics = previewPanel.children.item(2)
|
||||
if (!(scrollRegion instanceof HTMLElement) || !(statistics instanceof HTMLElement)) {
|
||||
return false
|
||||
}
|
||||
const panelRect = previewPanel.getBoundingClientRect()
|
||||
const scrollRect = scrollRegion.getBoundingClientRect()
|
||||
const statisticsRect = statistics.getBoundingClientRect()
|
||||
return (
|
||||
scrollRect.bottom <= statisticsRect.top &&
|
||||
statisticsRect.bottom <= panelRect.bottom &&
|
||||
statistics.scrollHeight <= statistics.clientHeight
|
||||
)
|
||||
})
|
||||
expect(previewGeometryIsStable).toBe(true)
|
||||
|
||||
await fixture.page.getByRole('textbox', { name: '搜索聊天' }).fill('性能样本 1499')
|
||||
const target = contactList.getByRole('button', { name: /性能样本 1499/ })
|
||||
await expect(target).toBeVisible()
|
||||
await target.click()
|
||||
await expect(target).toHaveAttribute('aria-pressed', 'true')
|
||||
expect(pageErrors).toEqual([])
|
||||
} finally {
|
||||
await fixture.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('LAYOUT-01 core workspaces fit a narrow desktop viewport without page errors', async () => {
|
||||
const fixture = await launchTestApp()
|
||||
const pageErrors: Error[] = []
|
||||
|
||||
@@ -23,10 +23,16 @@ export async function launchTestApp(
|
||||
aiFailure?: string
|
||||
now?: number
|
||||
appearanceTheme?: 'light' | 'dark'
|
||||
stableUserData?: string
|
||||
} = {}
|
||||
): Promise<TestApplication> {
|
||||
const ownsDirectory = !options.userData
|
||||
const userData = options.userData || mkdtempSync(resolve(tmpdir(), 'wxe-e2e-'))
|
||||
const ownsDirectory = !options.userData || Boolean(options.stableUserData)
|
||||
const userData =
|
||||
options.userData ||
|
||||
(options.stableUserData
|
||||
? resolve(options.stableUserData)
|
||||
: mkdtempSync(resolve(tmpdir(), 'wxe-e2e-')))
|
||||
if (options.stableUserData) rmSync(userData, { recursive: true, force: true })
|
||||
const localTestEnv = loadEnv('test', process.cwd(), 'WXE_E2E_')
|
||||
const configuredCloseDelay = Number(
|
||||
process.env.WXE_E2E_CLOSE_DELAY_MS ?? localTestEnv.WXE_E2E_CLOSE_DELAY_MS
|
||||
@@ -50,15 +56,21 @@ export async function launchTestApp(
|
||||
const page = await app.firstWindow()
|
||||
await page.waitForLoadState('domcontentloaded')
|
||||
const setWindowContentSize = async (size: { width: number; height: number }): Promise<void> => {
|
||||
await app.evaluate(({ BrowserWindow }, nextSize) => {
|
||||
await app.evaluate(({ BrowserWindow, screen }, nextSize) => {
|
||||
const [window] = BrowserWindow.getAllWindows()
|
||||
if (!window) throw new Error('E2E BrowserWindow is unavailable')
|
||||
const { workArea } = screen.getPrimaryDisplay()
|
||||
window.setPosition(workArea.x + 40, workArea.y + 40)
|
||||
window.setContentSize(nextSize.width, nextSize.height)
|
||||
}, size)
|
||||
await page.waitForFunction(
|
||||
(nextSize) => window.innerWidth === nextSize.width && window.innerHeight === nextSize.height,
|
||||
size
|
||||
)
|
||||
await page.evaluate(
|
||||
() =>
|
||||
new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
||||
)
|
||||
}
|
||||
return {
|
||||
app,
|
||||
|
||||
+118
-68
@@ -4,7 +4,7 @@ import { resolve } from 'path'
|
||||
import { launchTestApp } from './support/electron'
|
||||
|
||||
const baselineDirectory = resolve(`tests/e2e/__screenshots__/${process.platform}/visual.spec.ts`)
|
||||
const visualViewport = { width: 1000, height: 650 }
|
||||
const visualViewport = { width: 1400, height: 772 }
|
||||
const visualNow = Date.parse('2026-08-19T14:46:40+08:00')
|
||||
|
||||
async function clearScreenshotFocus(page: import('@playwright/test').Page): Promise<void> {
|
||||
@@ -200,6 +200,30 @@ test('API-00 Reader Skill page visual @visual', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('API-00 Reader Skill page dark visual @visual', async () => {
|
||||
const fixture = await launchTestApp({ now: visualNow, appearanceTheme: 'dark' })
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize(visualViewport)
|
||||
await fixture.page.getByRole('button', { name: 'API' }).click()
|
||||
await expect(fixture.page.getByRole('heading', { name: 'TraceMemo Reader' })).toBeVisible()
|
||||
await expect(fixture.page.getByText('API Token', { exact: true })).toBeVisible()
|
||||
await expect(fixture.page.locator('html')).toHaveAttribute('data-theme', 'dark')
|
||||
expect(
|
||||
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
|
||||
).toBe(true)
|
||||
expect(pageErrors).toEqual([])
|
||||
await clearScreenshotFocus(fixture.page)
|
||||
await expect(fixture.page).toHaveScreenshot('api-center-page-dark.png', {
|
||||
animations: 'disabled',
|
||||
caret: 'hide'
|
||||
})
|
||||
} finally {
|
||||
await fixture.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('API-01 Reader Skill preview visual @visual', async () => {
|
||||
const fixture = await launchTestApp({ now: visualNow })
|
||||
const pageErrors: Error[] = []
|
||||
@@ -259,77 +283,99 @@ test('API-02 Agent target segmented control visual @visual', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('REPORT-00 report configuration controls visual @visual', async () => {
|
||||
const fixture = await launchTestApp({ now: visualNow })
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize(visualViewport)
|
||||
await fixture.page.getByRole('button', { name: '日报' }).click()
|
||||
await fixture.page.getByRole('button', { name: '开始生成日报' }).click()
|
||||
await fixture.page.locator('.report-source-item').filter({ hasText: '产品测试群' }).click()
|
||||
await expect(fixture.page.getByRole('heading', { name: '生成群聊日报' })).toBeVisible()
|
||||
await expect(fixture.page.getByRole('radiogroup', { name: '总结范围' })).toBeVisible()
|
||||
expect(
|
||||
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
|
||||
).toBe(true)
|
||||
expect(pageErrors).toEqual([])
|
||||
await clearScreenshotFocus(fixture.page)
|
||||
await expect(fixture.page).toHaveScreenshot('report-config-page.png', {
|
||||
animations: 'disabled',
|
||||
caret: 'hide'
|
||||
})
|
||||
for (const appearanceTheme of ['light', 'dark'] as const) {
|
||||
test(`REPORT-00 report configuration controls ${appearanceTheme} visual @visual`, async () => {
|
||||
const fixture = await launchTestApp({ now: visualNow, appearanceTheme })
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize(visualViewport)
|
||||
await fixture.page.getByRole('button', { name: '日报' }).click()
|
||||
await fixture.page.getByRole('button', { name: '开始生成日报' }).click()
|
||||
await fixture.page.locator('.report-source-item').filter({ hasText: '产品测试群' }).click()
|
||||
await expect(fixture.page.getByRole('heading', { name: '生成群聊日报' })).toBeVisible()
|
||||
await expect(fixture.page.getByRole('radiogroup', { name: '总结范围' })).toBeVisible()
|
||||
expect(
|
||||
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
|
||||
).toBe(true)
|
||||
expect(pageErrors).toEqual([])
|
||||
await clearScreenshotFocus(fixture.page)
|
||||
await expect(fixture.page).toHaveScreenshot(
|
||||
`${appearanceTheme === 'light' ? 'report-config-page' : 'report-config-page-dark'}.png`,
|
||||
{
|
||||
animations: 'disabled',
|
||||
caret: 'hide'
|
||||
}
|
||||
)
|
||||
|
||||
const modelHeading = fixture.page.getByRole('heading', { name: '模型配置' })
|
||||
await modelHeading.scrollIntoViewIfNeeded()
|
||||
await expect(modelHeading).toBeVisible()
|
||||
await clearScreenshotFocus(fixture.page)
|
||||
await expect(fixture.page).toHaveScreenshot('report-model-controls.png', {
|
||||
animations: 'disabled',
|
||||
caret: 'hide'
|
||||
})
|
||||
const modelHeading = fixture.page.getByRole('heading', { name: '模型配置' })
|
||||
await modelHeading.scrollIntoViewIfNeeded()
|
||||
await expect(modelHeading).toBeVisible()
|
||||
await clearScreenshotFocus(fixture.page)
|
||||
await expect(fixture.page).toHaveScreenshot(
|
||||
`${appearanceTheme === 'light' ? 'report-model-controls' : 'report-model-controls-dark'}.png`,
|
||||
{
|
||||
animations: 'disabled',
|
||||
caret: 'hide'
|
||||
}
|
||||
)
|
||||
|
||||
const templateSection = fixture.page.getByRole('heading', { name: '日报模板' }).locator('..')
|
||||
await templateSection.getByRole('button', { name: '查看版式' }).first().click()
|
||||
const templateDialog = fixture.page.getByRole('dialog', { name: '经典日报' })
|
||||
await expect(templateDialog).toBeVisible()
|
||||
await clearScreenshotFocus(fixture.page)
|
||||
await expect(fixture.page).toHaveScreenshot('report-template-dialog.png', {
|
||||
animations: 'disabled',
|
||||
caret: 'hide'
|
||||
})
|
||||
} finally {
|
||||
await fixture.close()
|
||||
}
|
||||
})
|
||||
if (appearanceTheme === 'light') {
|
||||
const templateSection = fixture.page
|
||||
.getByRole('heading', { name: '日报模板' })
|
||||
.locator('..')
|
||||
await templateSection.getByRole('button', { name: '查看版式' }).first().click()
|
||||
const templateDialog = fixture.page.getByRole('dialog', { name: '经典日报' })
|
||||
await expect(templateDialog).toBeVisible()
|
||||
await clearScreenshotFocus(fixture.page)
|
||||
await expect(fixture.page).toHaveScreenshot('report-template-dialog.png', {
|
||||
animations: 'disabled',
|
||||
caret: 'hide'
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
await fixture.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
test('REPORT-01 report retry controls visual @visual', async () => {
|
||||
const fixture = await launchTestApp({ now: visualNow, aiFailure: '401' })
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize(visualViewport)
|
||||
await fixture.page.getByRole('button', { name: '日报' }).click()
|
||||
await fixture.page.getByRole('button', { name: '开始生成日报' }).click()
|
||||
await fixture.page.locator('.report-source-item').filter({ hasText: '产品测试群' }).click()
|
||||
await fixture.page.getByRole('radio', { name: '近 7 天' }).click()
|
||||
await fixture.page.getByRole('button', { name: '开始生成日报' }).click()
|
||||
await expect(fixture.page.getByText(/本地假服务错误 401/).first()).toBeVisible()
|
||||
await expect(fixture.page.getByRole('combobox', { name: '切换模型' })).toBeVisible()
|
||||
await expect(fixture.page.getByRole('button', { name: '使用所选模型重新生成' })).toBeEnabled()
|
||||
expect(
|
||||
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
|
||||
).toBe(true)
|
||||
expect(pageErrors).toEqual([])
|
||||
await clearScreenshotFocus(fixture.page)
|
||||
await expect(fixture.page).toHaveScreenshot('report-retry-controls.png', {
|
||||
animations: 'disabled',
|
||||
caret: 'hide'
|
||||
for (const appearanceTheme of ['light', 'dark'] as const) {
|
||||
test(`REPORT-01 report retry controls ${appearanceTheme} visual @visual`, async () => {
|
||||
const fixture = await launchTestApp({
|
||||
now: visualNow,
|
||||
aiFailure: '401',
|
||||
appearanceTheme,
|
||||
stableUserData: '/tmp/tracememo-e2e-report-user-data'
|
||||
})
|
||||
} finally {
|
||||
await fixture.close()
|
||||
}
|
||||
})
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize(visualViewport)
|
||||
await fixture.page.getByRole('button', { name: '日报' }).click()
|
||||
await fixture.page.getByRole('button', { name: '开始生成日报' }).click()
|
||||
await fixture.page.locator('.report-source-item').filter({ hasText: '产品测试群' }).click()
|
||||
await fixture.page.getByRole('radio', { name: '近 7 天' }).click()
|
||||
await fixture.page.getByRole('button', { name: '开始生成日报' }).click()
|
||||
await expect(fixture.page.getByText(/本地假服务错误 401/).first()).toBeVisible()
|
||||
await expect(fixture.page.getByRole('combobox', { name: '切换模型' })).toBeVisible()
|
||||
await expect(fixture.page.getByRole('button', { name: '使用所选模型重新生成' })).toBeEnabled()
|
||||
expect(
|
||||
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
|
||||
).toBe(true)
|
||||
expect(pageErrors).toEqual([])
|
||||
await clearScreenshotFocus(fixture.page)
|
||||
await expect(fixture.page).toHaveScreenshot(
|
||||
`${appearanceTheme === 'light' ? 'report-retry-controls' : 'report-retry-controls-dark'}.png`,
|
||||
{
|
||||
animations: 'disabled',
|
||||
caret: 'hide'
|
||||
}
|
||||
)
|
||||
} finally {
|
||||
await fixture.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for (const appearanceTheme of ['light', 'dark'] as const) {
|
||||
test(`CHAT-02 personal WeChat send dialog ${appearanceTheme} visual @visual`, async () => {
|
||||
@@ -568,6 +614,10 @@ for (const appearanceTheme of ['light', 'dark'] as const) {
|
||||
await expect(fixture.page.getByRole('heading', { name: '新增供应商' })).toBeVisible()
|
||||
await fixture.page.getByLabel('供应商 ID').fill('fixture-new-provider')
|
||||
await expect(fixture.page.getByRole('combobox', { name: '快速模板' })).toBeVisible()
|
||||
// Pin the screenshot after fill() auto-scroll to avoid half-pixel capture drift.
|
||||
await fixture.page
|
||||
.locator('.settings-page-scroll')
|
||||
.evaluate((element) => (element.scrollTop = 396))
|
||||
expect(
|
||||
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
|
||||
).toBe(true)
|
||||
|
||||
Reference in New Issue
Block a user