fix: match Today approved prototype
This commit is contained in:
@@ -1,16 +1,65 @@
|
||||
import type { APIRequestContext } from '@playwright/test'
|
||||
import { expect, test } from './fixtures'
|
||||
|
||||
const widths = [721, 720, 390, 375]
|
||||
|
||||
test('Today plain-list layout matches the approved responsive geometry', async ({ page }) => {
|
||||
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, data: Record<string, unknown>) {
|
||||
return request.post(path, { data, headers: { 'x-csrf-token': await csrf(request), origin: baseURL } })
|
||||
}
|
||||
|
||||
test('Today plain-list layout matches the approved responsive geometry', async ({ page, request, baseURL }, testInfo) => {
|
||||
const runId = `${testInfo.project.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||
const taskTitle = `原型对齐今日任务-${runId}`
|
||||
const habitName = `原型对齐习惯-${runId}`
|
||||
const bootstrap = await request.get('/api/v1/bootstrap')
|
||||
expect(bootstrap.ok(), await bootstrap.text()).toBeTruthy()
|
||||
const inbox = (await bootstrap.json()).lists.find((item: { is_inbox: boolean }) => item.is_inbox)
|
||||
expect(inbox).toBeTruthy()
|
||||
const today = new Intl.DateTimeFormat('sv-SE', {
|
||||
timeZone: 'Asia/Shanghai',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).format(new Date())
|
||||
const taskResponse = await mutate(request, baseURL!, '/api/v1/tasks', { title: taskTitle, list_id: inbox.id, due_at: `${today}T23:59:00+08:00`, due_has_time: false })
|
||||
expect(taskResponse.ok(), await taskResponse.text()).toBeTruthy()
|
||||
const taskId = (await taskResponse.json()).id as string
|
||||
const habitResponse = await mutate(request, baseURL!, '/api/v1/habits', { name: habitName, kind: 'numeric', target: 8, max_value: 8, schedule_type: 'daily' })
|
||||
expect(habitResponse.ok(), await habitResponse.text()).toBeTruthy()
|
||||
const habitId = (await habitResponse.json()).id as string
|
||||
await page.goto('/')
|
||||
const taskRowLocator = page.locator('#today-tasks .task-row').filter({ hasText: taskTitle })
|
||||
const habitRowLocator = page.locator('.today-habit-row').filter({ hasText: habitName })
|
||||
await expect(taskRowLocator).toHaveCount(1)
|
||||
await expect(habitRowLocator).toHaveCount(1)
|
||||
for (const width of widths) {
|
||||
await page.setViewportSize({ width, height: width <= 390 ? 844 : 900 })
|
||||
await expect.poll(async () => page.evaluate(() => {
|
||||
const content = document.querySelector<HTMLElement>('.today-context')?.getBoundingClientRect()
|
||||
const environment = document.querySelector<HTMLElement>('.today-environment')?.getBoundingClientRect()
|
||||
if (!content || !environment) return null
|
||||
return {
|
||||
media720: matchMedia('(max-width: 720.98px)').matches,
|
||||
contentWidth: Math.round(content.width),
|
||||
environmentHeight: Math.round(environment.height),
|
||||
}
|
||||
})).toEqual({
|
||||
media720: width <= 720,
|
||||
contentWidth: width === 721 ? 630 : width - 58,
|
||||
environmentHeight: width === 721 ? 55 : width === 375 ? 90 : 93,
|
||||
})
|
||||
await expect(taskRowLocator).toHaveCount(1)
|
||||
await expect(habitRowLocator).toHaveCount(1)
|
||||
await expect(page.locator('.today-environment')).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: '刷新当前页面' })).toBeVisible()
|
||||
await expect(page.getByRole('switch', { name: '显示已完成' })).toHaveCount(1)
|
||||
await expect(page.locator('.today-environment__gold')).toContainText('Au99.99')
|
||||
const metrics = await page.evaluate(() => {
|
||||
const metrics = await page.evaluate(({ taskId, habitId }) => {
|
||||
const rect = (selector: string) => document.querySelector<HTMLElement>(selector)!.getBoundingClientRect()
|
||||
const environment = document.querySelector<HTMLElement>('.today-environment')!
|
||||
const calendar = document.querySelector<HTMLElement>('.today-environment__calendar')!
|
||||
@@ -22,6 +71,20 @@ test('Today plain-list layout matches the approved responsive geometry', async (
|
||||
const firstSection = document.querySelector<HTMLElement>('.today-section-toggle')!
|
||||
const main = document.querySelector<HTMLElement>('main.today-main')!
|
||||
const content = rect('.today-context')
|
||||
const sectionTitle = firstSection.querySelector<HTMLElement>('.today-section-title')!
|
||||
const sectionSummary = firstSection.querySelector<HTMLElement>('.today-section-summary')!
|
||||
const sectionIcon = firstSection.querySelector<HTMLElement>('.today-section-chevron')!
|
||||
const summaries = [...document.querySelectorAll<HTMLElement>('.today-section-summary')].map(summary => ({
|
||||
text: summary.innerText.trim(),
|
||||
expected: summary.closest<HTMLButtonElement>('.today-section-toggle')?.getAttribute('aria-controls') === 'today-habits'
|
||||
? document.querySelectorAll('.today-habit-row').length
|
||||
: summary.closest<HTMLButtonElement>('.today-section-toggle')?.getAttribute('aria-controls') === 'today-overdue'
|
||||
? document.querySelectorAll('#today-overdue .task-row').length
|
||||
: document.querySelectorAll('#today-tasks .task-row').length,
|
||||
}))
|
||||
const taskRow = document.querySelector<HTMLElement>(`#today-tasks .task-row[data-task-id="${taskId}"]`)!
|
||||
const habitRow = document.querySelector<HTMLElement>(`.today-habit-row[data-habit-id="${habitId}"]`)!
|
||||
const fab = document.querySelector<HTMLElement>('.unified-fab')!
|
||||
const orderedElements = [environment, title, remaining, filter, firstSection]
|
||||
const ordered = orderedElements.map((element) => {
|
||||
const box = element.getBoundingClientRect()
|
||||
@@ -50,13 +113,16 @@ test('Today plain-list layout matches the approved responsive geometry', async (
|
||||
const secondary = [...environment.querySelectorAll<HTMLElement>('small')].map(node => ({ scrollWidth: node.scrollWidth, clientWidth: node.clientWidth, visible: visible(node) }))
|
||||
return {
|
||||
viewport: innerWidth,
|
||||
media720: matchMedia('(max-width: 720.98px)').matches,
|
||||
documentOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||||
mainOverflow: main.scrollWidth - main.clientWidth,
|
||||
main: { ...main.getBoundingClientRect().toJSON(), clientWidth: main.clientWidth, paddingLeft: getComputedStyle(main).paddingLeft, paddingRight: getComputedStyle(main).paddingRight },
|
||||
content: { left: content.left, right: content.right, width: content.width },
|
||||
environment: { ...environment.getBoundingClientRect().toJSON(), scrollWidth: environment.scrollWidth, clientWidth: environment.clientWidth },
|
||||
directions: { calendar: getComputedStyle(calendar).flexDirection, weather: getComputedStyle(weather).flexDirection, gold: getComputedStyle(gold).flexDirection },
|
||||
uniqueRows,
|
||||
collisions,
|
||||
collisionRects: [calendar, weather, gold].map(element => element.getBoundingClientRect().toJSON()),
|
||||
secondary,
|
||||
ordered,
|
||||
orderedPairs,
|
||||
@@ -65,8 +131,32 @@ test('Today plain-list layout matches the approved responsive geometry', async (
|
||||
weatherText: weather.innerText,
|
||||
goldText: gold.innerText,
|
||||
goldLabel: gold.querySelector('.today-environment__gold-date')?.getAttribute('aria-label'),
|
||||
summaries,
|
||||
styles: {
|
||||
mainPaddingLeft: getComputedStyle(main).paddingLeft,
|
||||
titleFontSize: getComputedStyle(title).fontSize,
|
||||
titleFontWeight: getComputedStyle(title).fontWeight,
|
||||
filterWidth: filter.getBoundingClientRect().width,
|
||||
filterHeight: filter.getBoundingClientRect().height,
|
||||
filterVisualHeight: filter.querySelector<HTMLElement>('.completed-filter-pill__track')!.getBoundingClientRect().height,
|
||||
filterBackground: getComputedStyle(filter).backgroundColor,
|
||||
filterBorderRadius: getComputedStyle(filter).borderRadius,
|
||||
sectionBorderBottom: getComputedStyle(firstSection).borderBottomWidth,
|
||||
sectionTitleSize: getComputedStyle(sectionTitle).fontSize,
|
||||
sectionTitleWeight: getComputedStyle(sectionTitle).fontWeight,
|
||||
sectionSummaryText: sectionSummary.innerText,
|
||||
sectionIconText: sectionIcon.textContent,
|
||||
taskHeight: taskRow.getBoundingClientRect().height,
|
||||
taskBackground: getComputedStyle(taskRow).backgroundColor,
|
||||
taskBorderBottom: getComputedStyle(taskRow).borderBottomWidth,
|
||||
habitHeight: habitRow.getBoundingClientRect().height,
|
||||
habitBackground: getComputedStyle(habitRow).backgroundColor,
|
||||
habitBorderBottom: getComputedStyle(habitRow).borderBottomWidth,
|
||||
fabShadow: getComputedStyle(fab).boxShadow,
|
||||
fabIconWidth: fab.querySelector('svg')?.getBoundingClientRect().width,
|
||||
},
|
||||
}
|
||||
})
|
||||
}, { taskId, habitId })
|
||||
console.log(`[today-geometry][${width}] ${JSON.stringify(metrics)}`)
|
||||
expect(metrics.documentOverflow).toBe(0)
|
||||
expect(metrics.mainOverflow).toBeLessThanOrEqual(0)
|
||||
@@ -78,14 +168,43 @@ test('Today plain-list layout matches the approved responsive geometry', async (
|
||||
expect(metrics.goldText).toContain('Au99.99')
|
||||
expect(metrics.goldLabel).toMatch(/市场日期 \d{4}-\d{2}-\d{2}/)
|
||||
expect(metrics.secondary.every(line => line.visible && line.scrollWidth <= line.clientWidth)).toBeTruthy()
|
||||
expect(metrics.styles.titleFontWeight).toBe('700')
|
||||
expect(metrics.styles.sectionTitleSize).toBe('13px')
|
||||
expect(metrics.styles.sectionTitleWeight).toBe('700')
|
||||
expect(metrics.styles.sectionSummaryText).not.toContain('项')
|
||||
expect(metrics.summaries.every(summary => /^\d+$/.test(summary.text) && Number(summary.text) === summary.expected)).toBeTruthy()
|
||||
expect(metrics.styles.sectionIconText).not.toContain('项')
|
||||
expect(metrics.styles.sectionBorderBottom).toBe('1px')
|
||||
expect(metrics.styles.taskHeight).toBeCloseTo(58, 0)
|
||||
expect(metrics.styles.habitHeight).toBeCloseTo(58, 0)
|
||||
expect(metrics.styles.taskBackground).toBe('rgba(0, 0, 0, 0)')
|
||||
expect(metrics.styles.habitBackground).toBe('rgba(0, 0, 0, 0)')
|
||||
expect(metrics.styles.taskBorderBottom).toBe('1px')
|
||||
expect(metrics.styles.habitBorderBottom).toBe('1px')
|
||||
expect(metrics.styles.filterWidth).toBeLessThanOrEqual(90)
|
||||
expect(metrics.styles.filterHeight).toBeGreaterThanOrEqual(width <= 720 ? 44 : 36)
|
||||
expect(metrics.styles.filterVisualHeight).toBeLessThanOrEqual(36)
|
||||
expect(metrics.styles.filterBackground).toBe('rgba(0, 0, 0, 0)')
|
||||
expect(metrics.styles.filterBorderRadius).toBe('0px')
|
||||
expect(metrics.styles.fabShadow).toContain('8px 18px')
|
||||
expect(metrics.styles.fabIconWidth).toBeCloseTo(30, 0)
|
||||
if (width === 721) {
|
||||
expect(metrics.uniqueRows).toHaveLength(1)
|
||||
expect(metrics.environment.height).toBeLessThan(80)
|
||||
expect(metrics.content.width).toBeCloseTo(630, 0)
|
||||
expect(metrics.environment.height).toBeLessThanOrEqual(72)
|
||||
expect(metrics.styles.titleFontSize).toBe('34px')
|
||||
} else {
|
||||
expect(metrics.uniqueRows).toHaveLength(2)
|
||||
expect(metrics.directions.calendar).toBe('row')
|
||||
expect(metrics.directions.weather).toBe('column')
|
||||
expect(metrics.directions.gold).toBe('column')
|
||||
if (width <= 720) {
|
||||
expect(metrics.content.width).toBeCloseTo(width - 58, 0)
|
||||
expect(metrics.content.left).toBeCloseTo(29, 0)
|
||||
expect(metrics.styles.mainPaddingLeft).toBe('29px')
|
||||
expect(metrics.styles.titleFontSize).toBe('24px')
|
||||
expect(metrics.environment.height).toBeCloseTo(width === 375 ? 90 : 93, 0)
|
||||
}
|
||||
}
|
||||
if (width >= 721) {
|
||||
expect(metrics.content.width).toBeLessThanOrEqual(721)
|
||||
|
||||
Reference in New Issue
Block a user