Files
nexus/frontend/e2e/task-board.e2e.ts
AzuTear f5552218bc
CI - Build & Test / Backend (.NET) (push) Successful in 42s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 2m46s
CI - Build & Test / Security Check (push) Successful in 3s
CI - Build & Test / Deploy Nexus (push) Successful in 56s
feat: ship agent-first mission control v0.2.57
2026-07-31 22:39:47 +02:00

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)
}
})
}