Files
deepseek-harness/apps/web/tests/schedule-pickers.e2e.ts
T

205 lines
9.8 KiB
TypeScript

/** Zoomed automation pickers truncate wide labels and keep vertical choices reachable. */
import type { Browser, CDPSession, Locator, Page } from 'playwright'
import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import type {} from '@deepseek-ai/dsh-schedule'
import { launchWebScaffold, watchConsole, type WebScaffold } from './scaffold.ts'
import { revealNavigationDrawer, saveFailureShot } from './support.ts'
const TITLE = 'Inspect zoomed automation pickers'
let scaffold: WebScaffold
let browser: Browser
beforeAll(async () => {
scaffold = await launchWebScaffold()
// Long zone labels use the same font on every host.
await scaffold.ctx.settings.update('ui-theme', { textFontFamily: 'monospace' })
await scaffold.ctx.schedule.create({
title: TITLE, prompt: 'Inspect the deployment log',
timing: { kind: 'rrule', rrule: 'DTSTART;TZID=Asia/Shanghai:20990825T200800\nRRULE:FREQ=DAILY;COUNT=1' },
target: { kind: 'new_once' },
})
browser = await chromium.launch()
})
afterAll(async () => {
try {
await browser?.close()
} finally {
await scaffold?.close()
}
})
/** Pan the visual viewport to a row, then touch it without locator auto-scroll. */
async function touchRow(cdp: CDPSession, row: Locator): Promise<void> {
const target = await row.evaluate((node) => {
const rect = node.getBoundingClientRect()
const view = window.visualViewport!
return {
left: Math.max(0, Math.min(window.innerWidth - view.width, rect.left + rect.width / 2 - view.width / 2)),
top: Math.max(0, Math.min(window.innerHeight - view.height, rect.top + rect.height / 2 - view.height / 2)),
offsetLeft: view.offsetLeft, offsetTop: view.offsetTop,
}
})
await cdp.send('Input.synthesizeScrollGesture', {
x: 1, y: 1, xDistance: target.offsetLeft - target.left, yDistance: target.offsetTop - target.top,
gestureSourceType: 'mouse', speed: 800,
})
await expect.poll(() => row.evaluate((node) => {
const rect = node.getBoundingClientRect()
const view = window.visualViewport!
const x = rect.left + rect.width / 2 - view.offsetLeft
const y = rect.top + rect.height / 2 - view.offsetTop
return x >= 0 && x < view.width && y >= 0 && y < view.height
})).toBe(true)
const point = await row.evaluate((node) => {
const rect = node.getBoundingClientRect()
return { x: rect.left + rect.width / 2 - window.visualViewport!.offsetLeft,
y: rect.top + rect.height / 2 - window.visualViewport!.offsetTop }
})
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [point] })
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] })
}
/** Labels retain ellipsis styling without increasing the list's width. */
async function expectEllipsisStyle(labels: Locator): Promise<void> {
expect(await labels.evaluateAll(nodes => nodes.every((node) => {
const style = getComputedStyle(node)
return style.textOverflow === 'ellipsis' && style.overflowX === 'hidden'
}))).toBe(true)
}
describe.each([
{ locale: 'en-US', tasks: 'Automation tasks', time: 'Time', second: 'Second', zone: 'Time zone', repeat: 'Repeat' },
{ locale: 'zh-CN', tasks: '自动化任务', time: '时间', second: '秒', zone: '时区', repeat: '重复' },
])('automation pickers ($locale)', (copy) => {
it('keeps the repeat menu inside the short layout viewport before and after zoom', async () => {
const context = await browser.newContext({
viewport: { width: 360, height: 586 }, locale: copy.locale, timezoneId: 'Asia/Shanghai',
isMobile: true, hasTouch: true,
})
const page = await context.newPage()
try {
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
await revealNavigationDrawer(page, copy.locale === 'zh-CN' ? 'zh' : 'en')
await page.getByRole('button', { name: copy.tasks, exact: true }).click()
const detail = page.getByTestId('task-manager-page')
await detail.getByRole('button', { name: TITLE, exact: true }).click()
await page.evaluate(() => document.fonts.ready)
await detail.getByRole('button', { name: new RegExp(`^${copy.repeat}`) }).click()
const menu = page.getByRole('menu')
await expect.poll(() => menu.evaluate((node) => {
const rect = node.getBoundingClientRect()
return Math.min(rect.left, rect.top, innerWidth - rect.right, innerHeight - rect.bottom)
})).toBeGreaterThanOrEqual(11.5)
const unzoomed = await menu.boundingBox()
expect(unzoomed).not.toBeNull()
const cdp = await context.newCDPSession(page)
try {
await cdp.send('Emulation.setPageScaleFactor', { pageScaleFactor: 5 })
await expect.poll(() => page.evaluate(() => visualViewport!.scale)).toBe(5)
await expect.poll(() => menu.boundingBox()).toEqual(unzoomed)
} finally {
await cdp.detach()
}
} catch (error) {
await saveFailureShot(page, `schedule-repeat-bounds-${copy.locale}`)
throw error
} finally {
await context.close()
}
})
it.each(['clock', 'zone'] as const)('keeps %s layout size, clipping and vertical choices reachable at 500% zoom', async (kind) => {
const context = await browser.newContext({
viewport: { width: 390, height: 844 }, locale: copy.locale, timezoneId: 'Asia/Shanghai',
isMobile: true, hasTouch: true,
})
const page: Page = await context.newPage()
try {
const consoleWatch = watchConsole(page)
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
await revealNavigationDrawer(page, copy.locale === 'zh-CN' ? 'zh' : 'en')
await page.getByRole('button', { name: copy.tasks, exact: true }).click()
const detail = page.getByTestId('task-manager-page')
await detail.getByRole('button', { name: TITLE, exact: true }).click()
await page.evaluate(() => document.fonts.ready)
const trigger = kind === 'clock'
? detail.getByRole('button', { name: copy.time, exact: true })
: detail.getByRole('button', { name: new RegExp(`^${copy.zone}`) })
await trigger.click()
const panel = kind === 'clock'
? page.getByRole('dialog', { name: copy.time, exact: true })
: page.getByRole('menu')
const before = await panel.evaluate((node) => {
const rect = node.getBoundingClientRect()
return { width: rect.width, height: rect.height, left: rect.left, top: rect.top }
})
const cdp = await context.newCDPSession(page)
await cdp.send('Emulation.setPageScaleFactor', { pageScaleFactor: 5 })
await expect.poll(() => page.evaluate(() => window.visualViewport!.scale)).toBe(5)
await expect.poll(() => panel.evaluate((node, original) => {
const rect = node.getBoundingClientRect()
return Math.max(Math.abs(rect.width - original.width), Math.abs(rect.height - original.height),
Math.abs(rect.left - original.left), Math.abs(rect.top - original.top))
}, before)).toBeLessThan(1)
expect(await panel.evaluate(node => node.getBoundingClientRect().width > window.visualViewport!.width)).toBe(true)
expect(await panel.evaluate(node => node.scrollWidth <= node.clientWidth)).toBe(true)
if (kind === 'clock') {
await expectEllipsisStyle(panel.locator('[role="option"] > span'))
const columns = panel.getByRole('listbox')
expect(await columns.evaluateAll(nodes => nodes.every((node) => {
const style = getComputedStyle(node)
return style.overscrollBehaviorX === 'auto' && style.overscrollBehaviorY === 'contain'
}))).toBe(true)
expect(await columns.evaluateAll(nodes => nodes.every(node =>
node.scrollWidth <= node.clientWidth))).toBe(true)
const seconds = panel.getByRole('listbox', { name: copy.second, exact: true })
expect(await seconds.evaluate((node) => {
node.scrollTop = node.scrollHeight
return node.scrollTop > 0
})).toBe(true)
const last = seconds.getByRole('option', { name: '59', exact: true })
await expect.poll(() => last.evaluate((node) => {
const label = node.firstElementChild!.getBoundingClientRect()
const column = node.parentElement!.getBoundingClientRect()
return label.left >= column.left && label.right <= column.right
&& label.top >= column.top && label.bottom <= column.bottom
})).toBe(true)
await touchRow(cdp, last)
await expect.poll(() => trigger.textContent()).toMatch(/:59$/)
} else {
const labels = panel.locator('[role="menuitem"] > span')
await expectEllipsisStyle(labels)
// Long English names exercise overflow; translated labels may fit the layout-sized menu.
if (copy.locale === 'en-US') {
expect(await labels.evaluateAll(nodes => nodes.some(node => node.scrollWidth > node.clientWidth))).toBe(true)
}
const viewport = panel.locator('[role="presentation"]').last()
expect(await viewport.evaluate((node) => {
node.scrollTop = node.scrollHeight
return node.scrollTop > 0 && node.scrollWidth <= node.clientWidth
})).toBe(true)
const last = panel.getByRole('menuitem').last()
await expect.poll(() => last.evaluate((node) => {
const row = node.getBoundingClientRect()
const viewport = node.parentElement!.getBoundingClientRect()
return row.left >= viewport.left && row.right <= viewport.right
&& row.top >= viewport.top && row.bottom <= viewport.bottom
})).toBe(true)
await touchRow(cdp, last)
await panel.waitFor({ state: 'hidden' })
}
expect(consoleWatch.pageErrors).toEqual([])
expect(consoleWatch.warnings).toEqual([])
await cdp.detach()
} catch (error) {
await saveFailureShot(page, `schedule-picker-${copy.locale}-${kind}`)
throw error
} finally {
await context.close()
}
})
})