149 lines
7.0 KiB
TypeScript
149 lines
7.0 KiB
TypeScript
import { expect, test } from '@playwright/test'
|
|
import { mockNexusApi } from './support/nexusApi'
|
|
|
|
test.describe('task board query lifecycle', () => {
|
|
test('shows authoritative data after refresh and appends Done pagination', async ({ page }) => {
|
|
const api = await mockNexusApi(page, { role: 'owner' })
|
|
await page.goto('/tasks')
|
|
const board = page.locator('.board-columns')
|
|
|
|
await expect(board.getByText('E2E Backlog task', { exact: true })).toBeVisible()
|
|
await expect(board.getByText('E2E Done page one', { exact: true })).toBeVisible()
|
|
await expect(board.getByText('E2E Done page two', { exact: true })).toHaveCount(0)
|
|
await expect(page.getByText('Revision board-r1', { exact: true })).toBeVisible()
|
|
|
|
await page.getByRole('button', { name: 'Weitere erledigte laden' }).click()
|
|
await expect(board.getByText('E2E Done page two', { exact: true })).toBeVisible()
|
|
await expect(page.getByRole('button', { name: 'Weitere erledigte laden' })).toHaveCount(0)
|
|
expect(api.boardRequestUrls.some(url => url.includes('doneCursor=done-page-2'))).toBe(true)
|
|
|
|
await page.reload()
|
|
await expect(board.getByText('E2E Backlog task', { exact: true })).toBeVisible()
|
|
await expect(board.getByText('E2E Done page one', { exact: true })).toBeVisible()
|
|
await expect(page.getByRole('button', { name: 'Weitere erledigte laden' })).toBeVisible()
|
|
expect(api.boardRequestUrls.filter(url => !url.includes('doneCursor=')).length).toBeGreaterThanOrEqual(2)
|
|
})
|
|
|
|
test('advances the SSE cursor and performs exactly one visible-data resync', async ({ page }) => {
|
|
const api = await mockNexusApi(page, {
|
|
role: 'owner',
|
|
domainResyncSequence: 25,
|
|
boardRefreshDelayMs: 700,
|
|
boardRefreshTitle: 'E2E Backlog task refreshed',
|
|
})
|
|
await page.goto('/tasks')
|
|
const board = page.locator('.board-columns')
|
|
|
|
await expect(board.getByText('E2E Backlog task', { exact: true })).toBeVisible()
|
|
await expect.poll(
|
|
() => api.boardRequestUrls.filter(url => !url.includes('doneCursor=')).length,
|
|
).toBe(2)
|
|
|
|
// A background refresh must retain the authoritative snapshot instead of
|
|
// replacing it with a loading state.
|
|
await expect(board.getByText('E2E Backlog task', { exact: true })).toBeVisible()
|
|
await expect(board.getByText('E2E Backlog task refreshed', { exact: true })).toBeVisible()
|
|
await expect(page.getByText('Revision board-r2', { exact: true })).toBeVisible()
|
|
|
|
await expect.poll(() => api.domainEventRequests.length).toBeGreaterThanOrEqual(2)
|
|
expect(api.domainEventRequests[0]?.lastEventId).toBeNull()
|
|
expect(api.domainEventRequests.slice(1).every(item => item.lastEventId === '25')).toBe(true)
|
|
expect(api.resyncResponseCount).toBe(1)
|
|
|
|
// Give another reconnect enough time to prove the stale cursor does not
|
|
// trigger a second resync or a second REST refresh.
|
|
await page.waitForTimeout(1_200)
|
|
expect(api.resyncResponseCount).toBe(1)
|
|
expect(api.boardRequestUrls.filter(url => !url.includes('doneCursor='))).toHaveLength(2)
|
|
})
|
|
|
|
test('persists a drag-and-drop move and keeps the card in its new column', async ({ page }) => {
|
|
const api = await mockNexusApi(page, { role: 'owner' })
|
|
await page.goto('/tasks')
|
|
|
|
const source = page.locator('.card').filter({ hasText: 'E2E Backlog task' }).first()
|
|
const reviewColumn = page.locator('.col').filter({
|
|
has: page.locator('.col-name', { hasText: 'Review' }),
|
|
})
|
|
await expect(source).toBeVisible()
|
|
const dataTransfer = await page.evaluateHandle(() => new DataTransfer())
|
|
await source.dispatchEvent('dragstart', { dataTransfer })
|
|
await reviewColumn.dispatchEvent('dragover', { dataTransfer })
|
|
await reviewColumn.dispatchEvent('drop', { dataTransfer })
|
|
await source.dispatchEvent('dragend', { dataTransfer })
|
|
|
|
await expect.poll(() => api.taskMutationRequests.length).toBe(1)
|
|
expect(api.taskMutationRequests[0]?.path).toBe('/api/dashboard/tasks/task-open-1/move')
|
|
expect(api.taskMutationRequests[0]?.body).toEqual({ state: 'review' })
|
|
await expect(reviewColumn.getByText('E2E Backlog task', { exact: true })).toBeVisible()
|
|
await expect(
|
|
page.locator('.col').filter({
|
|
has: page.locator('.col-name', { hasText: 'Offen' }),
|
|
}).getByText('E2E Backlog task', { exact: true }),
|
|
).toHaveCount(0)
|
|
|
|
const operationTray = page.getByRole('complementary', {
|
|
name: 'Letztes Operationsergebnis',
|
|
})
|
|
await expect(operationTray.getByText('Task verschoben', { exact: true })).toBeVisible()
|
|
const taskResultLink = operationTray.getByRole('link', { name: /E2E Backlog task/ })
|
|
await expect(taskResultLink).toHaveAttribute('href', '/tasks/task-open-1')
|
|
await taskResultLink.click()
|
|
await expect(page).toHaveURL('/tasks/task-open-1')
|
|
})
|
|
})
|
|
|
|
const viewports = [
|
|
{ width: 375, height: 812 },
|
|
{ width: 768, height: 900 },
|
|
{ width: 1024, height: 900 },
|
|
{ width: 1440, height: 900 },
|
|
{ width: 1920, height: 1080 },
|
|
]
|
|
|
|
for (const viewport of viewports) {
|
|
test(`contains horizontal board scrolling at ${viewport.width}px without page overflow`, async ({ page }) => {
|
|
await page.setViewportSize(viewport)
|
|
await mockNexusApi(page, { role: 'owner' })
|
|
await page.goto('/tasks')
|
|
await expect(page.locator('.board-columns').getByText('E2E Backlog task', { exact: true })).toBeVisible()
|
|
|
|
const geometry = await page.evaluate(() => {
|
|
const root = document.documentElement
|
|
const body = document.body
|
|
const main = document.querySelector<HTMLElement>('.legacy-main')
|
|
const scroller = document.querySelector<HTMLElement>('.board-columns')
|
|
if (!main || !scroller) throw new Error('Expected Task Board geometry was not rendered.')
|
|
const mainRect = main.getBoundingClientRect()
|
|
const scrollerRect = scroller.getBoundingClientRect()
|
|
scroller.scrollLeft = 100
|
|
return {
|
|
viewportWidth: root.clientWidth,
|
|
rootOverflow: root.scrollWidth - root.clientWidth,
|
|
bodyOverflow: body.scrollWidth - root.clientWidth,
|
|
mainLeft: mainRect.left,
|
|
mainRight: mainRect.right,
|
|
scrollerLeft: scrollerRect.left,
|
|
scrollerRight: scrollerRect.right,
|
|
scrollerClientWidth: scroller.clientWidth,
|
|
scrollerScrollWidth: scroller.scrollWidth,
|
|
scrollerScrollLeft: scroller.scrollLeft,
|
|
scrollerOverflowX: getComputedStyle(scroller).overflowX,
|
|
}
|
|
})
|
|
|
|
expect(geometry.rootOverflow).toBeLessThanOrEqual(1)
|
|
expect(geometry.bodyOverflow).toBeLessThanOrEqual(1)
|
|
expect(geometry.mainLeft).toBeGreaterThanOrEqual(-1)
|
|
expect(geometry.mainRight).toBeLessThanOrEqual(geometry.viewportWidth + 1)
|
|
expect(geometry.scrollerLeft).toBeGreaterThanOrEqual(-1)
|
|
expect(geometry.scrollerRight).toBeLessThanOrEqual(geometry.viewportWidth + 1)
|
|
expect(geometry.scrollerScrollWidth).toBeGreaterThanOrEqual(geometry.scrollerClientWidth)
|
|
expect(geometry.scrollerOverflowX).toBe('auto')
|
|
if (viewport.width <= 1024) {
|
|
expect(geometry.scrollerScrollWidth - geometry.scrollerClientWidth).toBeGreaterThan(1)
|
|
expect(geometry.scrollerScrollLeft).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
}
|