import { expect, test } from '@playwright/test' import { addTask } from './helpers' test.beforeEach(async ({ page }) => { await page.goto('/') }) /** 点卡片左侧的勾选按钮标记完成 */ async function completeTask(page: Parameters[0], title: string): Promise { await page.locator('article').filter({ hasText: title }).locator('button').first().click() } /** 本地日期 YYYY-MM-DD,与热力图的口径一致 */ async function todayIso(page: Parameters[0]): Promise { return page.evaluate(() => { const now = new Date() const pad = (value: number) => String(value).padStart(2, '0') return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}` }) } test('统计页可从导航进入,汇总数字与热力图同步更新', async ({ page }) => { await addTask(page, '统计任务 A') await completeTask(page, '统计任务 A') await addTask(page, '统计任务 B') await completeTask(page, '统计任务 B') await page.getByRole('link', { name: '统计' }).click() await expect(page).toHaveURL(/\/stats$/) await expect(page.getByRole('heading', { name: '统计' })).toBeVisible() // 累计与「今天完成」都是 2 await expect( page.locator('dl > div').filter({ hasText: '累计完成' }).locator('dd'), ).toContainText('2') await expect( page.locator('dl > div').filter({ hasText: '今天完成' }).locator('dd'), ).toContainText('2') // 一年 53 周 × 7 天 await expect(page.locator('[data-role="heatmap-cell"]')).toHaveCount(53 * 7) const today = await todayIso(page) await expect(page.locator(`[data-role="heatmap-cell"][data-date="${today}"]`)).toHaveAttribute( 'data-count', '2', ) }) test('深链 /stats 可以直接打开,空数据时给出引导', async ({ page }) => { await page.goto('/stats') await expect(page.getByRole('heading', { name: '统计' })).toBeVisible() await expect(page.getByText('还没有完成记录')).toBeVisible() // 空数据也渲染完整网格,只是全部为最浅档 await expect(page.locator('[data-role="heatmap-cell"]')).toHaveCount(53 * 7) }) test('回收站里的任务不计入统计', async ({ page }) => { await addTask(page, '要删掉的任务') await completeTask(page, '要删掉的任务') // 删除入口在详情抽屉底部(卡片上只有完成勾选) await page.locator('article').filter({ hasText: '要删掉的任务' }).click() await page.getByRole('dialog').getByRole('button', { name: '删除' }).click() await page.goto('/stats') await expect( page.locator('dl > div').filter({ hasText: '累计完成' }).locator('dd'), ).toContainText('0') }) test('热力图左右留白对称,不产生横向滚动条也不裁切描边', async ({ page }) => { await page.goto('/stats') await page.waitForSelector('[data-role="heatmap-cell"]') const metrics = await page.evaluate(() => { const scroller = document.querySelector('[role="img"]') as HTMLElement const cells = [...document.querySelectorAll('[data-role="heatmap-cell"]')] const box = scroller.getBoundingClientRect() const style = getComputedStyle(scroller) const innerLeft = box.left + parseFloat(style.paddingLeft) const innerRight = box.right - parseFloat(style.paddingRight) const first = cells[0]!.getBoundingClientRect() const last = cells[cells.length - 1]!.getBoundingClientRect() // 今天那格带 ring-1:描边画在元素外侧 1px,贴边就会被 overflow 容器裁掉 const today = document.querySelector('[data-role="heatmap-cell"].ring-1') return { clientWidth: scroller.clientWidth, scrollWidth: scroller.scrollWidth, leftGap: Math.round(first.left - innerLeft), rightGap: Math.round(innerRight - last.right), ringOverflow: today ? Math.round(today.getBoundingClientRect().right + 1 - innerRight) : null, } }) // 宽屏(e2e 默认 1280)下内容不溢出,也就没有滚动条 expect(metrics.scrollWidth).toBe(metrics.clientWidth) // 左右留白相等:左侧是星期标签列(一 / 三 / 五)的宽度,右侧补了同样的宽度。 // 少了它右侧会一直贴到卡片边缘,既不对称也会把 ring 描边切掉一条。 expect(metrics.leftGap).toBeGreaterThan(0) expect(metrics.rightGap).toBe(metrics.leftGap) // ring 必须留有余量,不能顶到容器的裁剪边界 expect(metrics.ringOverflow).toBeLessThan(0) }) test('标签分布与细节指标随完成情况更新', async ({ page }) => { // 建一个带标签的任务,再从卡片上勾完成 await addTask(page, '带标签的活') await page.locator('article').filter({ hasText: '带标签的活' }).click() const tagInput = page.getByLabel('添加标签') await tagInput.fill('设计') await tagInput.press('Enter') await page.getByRole('button', { name: '保存' }).click() await completeTask(page, '带标签的活') await page.getByRole('link', { name: '统计' }).click() // 标签分布:一条记录算在该标签名下 const row = page.locator('[data-role="tag-bar"][data-tag="设计"]') await expect(row).toHaveAttribute('data-count', '1') await expect(row).toContainText('#设计') // 细节卡片:只有今天这一天有记录,连续 1 天,平均每天 1.0 个 await expect(page.locator('dl > div').filter({ hasText: '有记录' }).locator('dd')).toHaveText( '1 天', ) await expect(page.locator('dl > div').filter({ hasText: '最长连续' }).locator('dd')).toHaveText( '1 天', ) await expect(page.locator('dl > div').filter({ hasText: '平均每天' }).locator('dd')).toHaveText( '1.0 个', ) // 刚建完就完成 → 周期不足 1 天,显示「当天」 await expect( page.locator('dl > div').filter({ hasText: '平均完成周期' }).locator('dd'), ).toHaveText('当天') })