import type { APIRequestContext, Locator, Page } from '@playwright/test' import { expect, test } from './fixtures' async function csrf(request: APIRequestContext) { const state = await request.storageState() return state.cookies.find(cookie => cookie.name === 'dodo_csrf')?.value ?? '' } async function mutate(request: APIRequestContext, baseURL: string, path: string, options: Parameters[1]) { return request.fetch(path, { ...options, headers: { ...options?.headers, 'x-csrf-token': await csrf(request), origin: baseURL } }) } function bottomTab(page: Page, name: string) { return page.getByRole('navigation', { name: '主要导航' }).getByRole('button', { name, exact: true }) } async function openSidebarView(page: Page, name: string) { await page.locator('.topbar').getByRole('button', { name: /展开菜单|收起菜单/ }).click() const target = page.locator('.sidebar').getByRole('button', { name, exact: true }) await expect(target).toBeInViewport() await target.click() } async function expectHeightInRange(locator: Locator, minimum: number, maximum: number) { const box = await locator.boundingBox() expect(box).not.toBeNull() expect(box!.height).toBeGreaterThanOrEqual(minimum) expect(box!.height).toBeLessThanOrEqual(maximum) } async function expectNotClipped(locator: Locator) { const metrics = await locator.evaluate((element: HTMLElement) => ({ clientWidth: element.clientWidth, scrollWidth: element.scrollWidth, clientHeight: element.clientHeight, scrollHeight: element.scrollHeight, })) expect(metrics.scrollWidth).toBeLessThanOrEqual(metrics.clientWidth) expect(metrics.scrollHeight).toBeLessThanOrEqual(metrics.clientHeight) } test('approved polish keeps search state, dense rows, title-only memos, and unique detail titles', async ({ page, request, baseURL }, testInfo) => { const suffix = testInfo.project.name const bootstrapResponse = await request.get('/api/v1/bootstrap') expect(bootstrapResponse.ok()).toBeTruthy() const inbox = (await bootstrapResponse.json()).lists.find((item: { is_inbox: boolean }) => item.is_inbox) expect(inbox).toBeTruthy() const taskTitle = `搜索保留-${suffix}-这是用于检查长标题和右侧控件不会裁切的任务标题` const taskResponse = await mutate(request, baseURL!, '/api/v1/tasks', { method: 'POST', data: { title: taskTitle, list_id: inbox.id }, }) expect(taskResponse.ok(), await taskResponse.text()).toBeTruthy() const memoTitle = `Markdown 摘要-${suffix}` const memoResponse = await mutate(request, baseURL!, '/api/v1/memos', { method: 'POST', data: { title: memoTitle, content: '# 标题\n\n**重点** 与 [链接文字](https://example.com)\n\n- 列表项' }, }) expect(memoResponse.ok(), await memoResponse.text()).toBeTruthy() const day = new Date().toLocaleDateString('sv-SE') const countdownTitle = `紧凑倒数-${suffix}` const countdownResponse = await mutate(request, baseURL!, '/api/v1/countdowns', { method: 'POST', data: { title: countdownTitle, event_date: day, kind: 'countdown', repeat_rule: 'none', calendar_mode: 'solar', ignore_year: false, pinned: true }, }) expect(countdownResponse.ok(), await countdownResponse.text()).toBeTruthy() await page.goto('/') await openSidebarView(page, '收集箱') const searchToggle = page.getByRole('button', { name: '展开搜索任务' }) await expect(searchToggle).toHaveAttribute('aria-expanded', 'false') await searchToggle.click() const searchInput = page.getByRole('textbox', { name: '搜索任务' }) await expect(searchInput).toBeFocused() await searchInput.fill(`搜索保留-${suffix}`) const taskRow = page.locator('.task-row').filter({ hasText: taskTitle }) await expect(taskRow).toHaveCount(1) await expectHeightInRange(taskRow, 64, 68) await expectNotClipped(taskRow) await searchInput.press('Escape') const collapsedToggle = page.getByRole('button', { name: '展开搜索任务' }) await expect(collapsedToggle).toBeFocused() await expect(collapsedToggle).toHaveAttribute('aria-expanded', 'false') await collapsedToggle.click() await expect(searchInput).toHaveValue(`搜索保留-${suffix}`) await bottomTab(page, '习惯').click() const habitName = `长标题习惯-${suffix}-检查进度与按钮不被裁切` await page.getByRole('button', { name: '添加习惯' }).click() await page.getByLabel('新习惯名称').fill(habitName) await page.getByLabel('习惯类型').selectOption('numeric') await page.getByLabel('目标值').fill('8') await page.getByRole('button', { name: '添加习惯', exact: true }).click() const habitRow = page.locator('.habit-row').filter({ hasText: habitName }) await expect(habitRow).toHaveCount(1) await expectHeightInRange(habitRow, 84, 88) await expectNotClipped(habitRow) await habitRow.getByRole('button', { name: `查看习惯详情:${habitName}` }).click() const habitDetail = page.getByRole('dialog', { name: habitName }) await expect(habitDetail.getByRole('heading', { name: habitName, exact: true })).toHaveCount(1) await expect(habitDetail).not.toContainText('习惯详情') await habitDetail.getByRole('button', { name: '关闭习惯详情' }).click() await bottomTab(page, '倒数日').click() const focus = page.locator('.countdown-focus').filter({ hasText: countdownTitle }) await expect(focus).toHaveCount(1) await expectHeightInRange(focus, 136, 148) await expect(focus).not.toContainText(/置顶的重要日子|下一个重要日子|还有|已经过去/) await expectNotClipped(focus) await focus.click() const countdownDetail = page.getByRole('dialog', { name: countdownTitle }) await expect(countdownDetail.getByRole('heading', { name: countdownTitle, exact: true })).toHaveCount(1) await expect(countdownDetail).not.toContainText('重要日子详情') await countdownDetail.getByRole('button', { name: '关闭详情' }).click() await openSidebarView(page, '备忘录') const memoRow = page.locator('.memo-row').filter({ hasText: memoTitle }) await expect(memoRow).toHaveCount(1) await expectHeightInRange(memoRow, 72, 76) await expect(memoRow.locator('.memo-row__excerpt')).toHaveCount(0) await expect(memoRow).not.toContainText('标题 重点 与 链接文字 列表项') await expect(memoRow.getByText(memoTitle, { exact: true })).toHaveCount(1) await expect(memoRow.locator('time')).toHaveCount(1) await expectNotClipped(memoRow) const shortMemoTitle = `短正文-${suffix}` const shortMemoResponse = await mutate(request, baseURL!, '/api/v1/memos', { method: 'POST', data: { title: shortMemoTitle, content: 'BODY_ONLY_SHORT' }, }) expect(shortMemoResponse.ok(), await shortMemoResponse.text()).toBeTruthy() await page.reload() const shortMemoRow = page.locator('.memo-row').filter({ hasText: shortMemoTitle }) await expect(shortMemoRow).toHaveCount(1) await expectHeightInRange(shortMemoRow, 72, 76) await expect(shortMemoRow.locator('.memo-row__excerpt')).toHaveCount(0) await expect(shortMemoRow).not.toContainText('BODY_ONLY_SHORT') await page.setViewportSize({ width: 1440, height: 900 }) const desktopInbox = page.locator('.sidebar').getByRole('button', { name: '收集箱', exact: true }) await expect(desktopInbox).toBeInViewport() await desktopInbox.click() await expect(page.getByRole('button', { name: /展开搜索任务|收起搜索任务/ })).toBeHidden() const desktopSearch = page.getByRole('textbox', { name: '搜索任务' }) await expect(desktopSearch).toBeVisible() const desktopPanel = page.locator('.search-reveal') const desktopBox = await desktopPanel.boundingBox() expect(desktopBox).not.toBeNull() expect(desktopBox!.width).toBeGreaterThanOrEqual(320) await desktopSearch.fill(`搜索保留-${suffix}`) const desktopTaskRow = page.locator('.task-row').filter({ hasText: taskTitle }) await expect(desktopTaskRow).toHaveCount(1) await expectNotClipped(desktopTaskRow) })