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('.legacy-main') const scroller = document.querySelector('.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) } }) }