veritas-kanban/e2e/task-popout-stack.spec.ts

224 lines
9.9 KiB
TypeScript

import { expect, test } from '@playwright/test';
import { bypassAuth, cleanupRoutes, deleteTask, seedTestTask, unwrapApiData } from './helpers/auth';
for (const theme of ['light', 'dark']) {
test(`nested task utilities retain fixed controls and Escape ownership in ${theme}`, async ({
page,
}) => {
await bypassAuth(page);
await page.addInitScript((theme) => localStorage.setItem('veritas-kanban-theme', theme), theme);
const title = `Task utility fixture ${theme}`;
const task = await seedTestTask(page, {
title,
type: 'code',
git: {
repo: 'Fixture/repo',
branch: 'fixture',
baseBranch: 'main',
worktreePath: '/tmp/task-overlay-fixture',
worktreeManifestId: 'fixture-manifest',
},
});
const writes: string[] = [];
// The public update API deliberately cannot manufacture a managed worktree.
// Add ownership only to the browser's synthetic read fixture, never storage.
await page.route(/\/api\/tasks(?:\/[^/?]+)?(?:\?.*)?$/, async (route) => {
if (route.request().method() !== 'GET') return route.fallback();
const response = await route.fetch();
const json = JSON.parse(JSON.stringify(await response.json()), (_key, value) =>
value && typeof value === 'object' && value.id === task.id
? { ...value, git: { ...value.git, worktreeManifestId: 'fixture-manifest' } }
: value
);
await route.fulfill({ response, json });
});
await page.route('**/api/config', async (route) => {
if (route.request().method() !== 'GET') return route.fallback();
const config = unwrapApiData<Record<string, unknown>>(await (await route.fetch()).json());
await route.fulfill({
json: {
...config,
repos: [
{ name: 'Fixture/repo', path: '/tmp/task-overlay-fixture', defaultBranch: 'main' },
],
},
});
});
await page.route(new RegExp(`/api/(?:preview|conflicts)/${task.id}/`), (route) => {
if (route.request().method() === 'GET') return route.fallback();
writes.push(route.request().url());
return route.fulfill({ status: 409, json: { error: 'Read-only fixture' } });
});
const fixture = async (path: string, json: unknown) =>
page.route(path, (route) => {
if (route.request().method() !== 'GET') {
writes.push(route.request().url());
return route.fulfill({ status: 409, json: { error: 'Read-only fixture' } });
}
return route.fulfill({ json });
});
await fixture(`**/api/preview/${task.id}`, { status: 'stopped' });
await fixture(`**/api/tasks/${task.id}/worktree`, {
aheadBehind: { ahead: 1, behind: 0 },
hasChanges: false,
changedFiles: 0,
remoteState: { stale: false },
});
await fixture(`**/api/conflicts/${task.id}`, {
hasConflicts: true,
conflictingFiles: ['fixture.txt'],
rebaseInProgress: true,
mergeInProgress: false,
});
await fixture(`**/api/conflicts/${task.id}/file?*`, {
filePath: 'fixture.txt',
content: 'Draft resolution',
oursContent: 'Ours\n'.repeat(100),
theirsContent: 'Theirs\n'.repeat(100),
markers: [],
});
try {
await page.setViewportSize({ width: 1700, height: 900 });
await page.goto('/');
await page.getByRole('article', { name: `Task: ${title}` }).click();
const workspace = page.getByTestId('task-detail-panel');
await workspace.getByRole('button', { name: 'Run', exact: true }).click();
await workspace.getByRole('tab', { name: /Git/ }).click();
const previewOpener = workspace.getByRole('button', { name: 'Preview', exact: true });
await previewOpener.click();
const preview = page.getByRole('dialog', { name: 'Preview', exact: true });
await expect(preview).toBeVisible();
await expect(preview).toHaveCSS('opacity', '1');
await expect(workspace).toHaveAttribute('inert', '');
await page.setViewportSize({ width: 900, height: 480 });
await page.evaluate(() => {
document.documentElement.style.fontSize = '20px';
});
const start = preview.getByRole('button', { name: 'Start Preview', exact: true });
await expect(start).toBeInViewport({ ratio: 1 });
const startBox = await start.boundingBox();
await preview.locator('.vk-overlay-scroll').evaluate((element) => {
element.scrollTop = element.scrollHeight;
});
expect(await start.boundingBox()).toEqual(startBox);
await page.screenshot({ path: test.info().outputPath(`preview-${theme}.png`) });
await page.keyboard.press('Escape');
await expect(preview).toHaveCount(0);
await expect(previewOpener).toBeFocused();
const conflictOpener = workspace.getByRole('button', {
name: 'Resolve Conflicts',
exact: true,
});
await conflictOpener.click();
const conflict = page.getByRole('dialog', { name: 'Merge Conflicts', exact: true });
await expect(conflict).toBeVisible();
await expect(conflict).toHaveCSS('opacity', '1');
const abortOpener = conflict.getByRole('button', { name: 'Abort', exact: true });
await expect(abortOpener).toBeInViewport({ ratio: 1 });
const abortBox = await abortOpener.boundingBox();
await conflict.locator('.vk-overlay-scroll').evaluate((element) => {
element.scrollTop = element.scrollHeight;
});
expect(await abortOpener.boundingBox()).toEqual(abortBox);
expect(await conflict.evaluate((element) => element.scrollWidth - element.clientWidth)).toBe(
0
);
await page.screenshot({ path: test.info().outputPath(`conflicts-${theme}.png`) });
await abortOpener.click();
const confirmation = page.getByRole('dialog', { name: 'Abort Rebase?', exact: true });
await expect(confirmation).toBeVisible();
await expect(confirmation).toHaveCSS('opacity', '1');
await page.screenshot({ path: test.info().outputPath(`abort-${theme}.png`) });
expect(
await conflict.evaluate((element) =>
element.closest('[data-overlay-variant]')?.hasAttribute('inert')
)
).toBe(true);
await confirmation.getByRole('button', { name: 'Cancel', exact: true }).focus();
await page.keyboard.press('Escape');
await expect(confirmation).toHaveCount(0);
await expect(abortOpener).toBeFocused();
await expect(workspace).toHaveAttribute('inert', '');
await page.keyboard.press('Escape');
await expect(conflict).toHaveCount(0);
await expect(conflictOpener).toBeFocused();
expect(writes).toEqual([]);
} finally {
await deleteTask(page, String(task.id)).catch(() => {});
await cleanupRoutes(page).catch(() => {});
}
});
test(`task template popout keeps task state and bounded actions in ${theme}`, async ({
page,
}) => {
await bypassAuth(page);
await page.addInitScript((theme) => {
localStorage.setItem('veritas-kanban-theme', theme);
Object.defineProperty(window, 'veritasDesktop', {
configurable: true,
value: { onMenuCommand: () => () => undefined },
});
}, theme);
const title = `Task popout fixture ${theme}`;
const task = await seedTestTask(page, { title, type: 'code', status: 'todo' });
try {
await page.setViewportSize({ width: 1700, height: 900 });
await page.goto('/');
const card = page.getByRole('article', { name: `Task: ${title}` });
await card.focus();
await card.press('Enter');
const workspace = page.getByTestId('task-detail-panel');
await workspace.getByRole('button', { name: 'Plan', exact: true }).click();
const opener = workspace.getByRole('button', { name: 'Template', exact: true });
for (const geometry of [
{ width: 1700, height: 900, fontSize: '16px' },
{ width: 1180, height: 760, fontSize: '20px' },
{ width: 900, height: 480, fontSize: '20px' },
]) {
await page.setViewportSize(geometry);
await page.evaluate((fontSize) => {
document.documentElement.style.fontSize = fontSize;
}, geometry.fontSize);
await opener.click();
const dialog = page.getByRole('dialog', { name: 'Apply Template to Task' });
await expect(dialog).toBeVisible();
await expect(workspace).toHaveAttribute('inert', '');
const scroll = dialog.locator('.vk-overlay-scroll');
const footer = dialog.locator('.vk-overlay-footer');
await dialog.getByRole('button', { name: 'Help', exact: true }).click();
for (const action of ['Cancel', 'Apply Template']) {
await expect(footer.getByRole('button', { name: action, exact: true })).toBeInViewport({
ratio: 1,
});
}
const before = await footer.boundingBox();
await scroll.evaluate((element) => {
element.scrollTop = element.scrollHeight;
});
expect(await footer.boundingBox()).toEqual(before);
const bounds = await dialog.evaluate((element) => ({
top: element.getBoundingClientRect().top,
bottom: element.getBoundingClientRect().bottom,
overflow: element.scrollWidth - element.clientWidth,
}));
expect(bounds.top).toBeGreaterThanOrEqual(0);
expect(bounds.bottom).toBeLessThanOrEqual(geometry.height);
expect(bounds.overflow).toBe(0);
await footer.getByRole('button', { name: 'Cancel', exact: true }).focus();
await page.keyboard.press('Escape');
await expect(dialog).toHaveCount(0);
await expect(workspace).not.toHaveAttribute('inert');
await expect(opener).toBeFocused();
await expect(
workspace.getByRole('textbox', { name: 'Task title', exact: true })
).toHaveValue(title);
}
await page.keyboard.press('Escape');
await expect(workspace).toHaveCount(0);
await expect(card).toBeFocused();
} finally {
await deleteTask(page, String(task.id));
await cleanupRoutes(page);
}
});
}