import type { APIRequestContext, 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 } }) } async function openSidebarView(page: Page, name: string) { const sidebar = page.locator('.sidebar') const target = sidebar.getByRole('button', { name, exact: true }) if (await page.locator('.shell').evaluate(element => element.classList.contains('sidebar-collapsed'))) { await page.getByRole('button', { name: '展开菜单' }).click() } else if (await page.evaluate(() => window.innerWidth <= 930)) { await page.locator('main .topbar > button').first().click() } else if (!await target.isVisible()) { await page.getByRole('button', { name: /展开菜单|收起菜单/ }).click() } await target.click() } function futureDueAt(days: number) { const date = new Date() date.setDate(date.getDate() + days) date.setHours(12, 0, 0, 0) return date.toISOString() } test('Upcoming shares the task-list hierarchy while keeping its date scope and no manual reorder', async ({ page, request, baseURL }) => { 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() for (const [title, days] of [['未来一天任务', 1], ['未来三天任务', 3]] as const) { const response = await mutate(request, baseURL!, '/api/v1/tasks', { method: 'POST', data: { title, list_id: inbox.id, due_at: futureDueAt(days), due_has_time: false }, }) expect(response.ok(), await response.text()).toBeTruthy() } await page.goto('/') await openSidebarView(page, '最近 7 天') await expect(page.locator('main')).toHaveClass(/list-main/) await expect(page.locator('.topbar-title')).toHaveCount(0) const header = page.locator('.list-page-context') await expect(header.getByRole('heading', { name: '最近 7 天' })).toBeVisible() await expect(header).toContainText(/还有 \d+ 项待完成/) await expect(header.getByRole('switch', { name: '显示已完成' })).toBeVisible() await expect(page.locator('.list-section-heading')).toContainText('任务') await expect(page.getByRole('button', { name: '调整顺序' })).toHaveCount(0) await expect(page.locator('.task-row')).toHaveCount(2) await expect(page.locator('.task-row')).toContainText(['未来一天任务', '未来三天任务']) const geometry = await page.evaluate(() => { const rect = (selector: string) => { const value = document.querySelector(selector)!.getBoundingClientRect() return { left: value.left, right: value.right, width: value.width, top: value.top, bottom: value.bottom } } return { viewportWidth: innerWidth, overflow: document.documentElement.scrollWidth - document.documentElement.clientWidth, header: rect('.list-page-context'), search: rect('.list-search-reveal'), section: rect('.list-section-heading'), list: rect('.task-list'), } }) expect(geometry.overflow).toBe(0) expect(geometry.header.left).toBeCloseTo(geometry.search.left, 0) expect(geometry.search.left).toBeCloseTo(geometry.section.left, 0) expect(geometry.section.left).toBeCloseTo(geometry.list.left, 0) expect(geometry.header.width).toBeCloseTo(geometry.list.width, 0) if (geometry.viewportWidth <= 720) { expect(geometry.header.left).toBeCloseTo(29, 0) expect(geometry.list.right).toBeCloseTo(geometry.viewportWidth - 29, 0) } const fab = page.getByRole('button', { name: '添加任务' }) await expect(fab).toBeVisible() await fab.click() await expect(page.getByRole('dialog', { name: '添加任务' })).toBeVisible() })