import { test, expect, type Page } from '@playwright/test'; // E2E smoke for the V2 cross-space calendar (top-bar "カレンダー" tab). // // Drives the real orchestrator with auth off → synthetic 'local' admin, who can // see every space and edit events. We create a case space, add an event for // today via the per-space calendar (reusing its tested flow), then open the // top-bar Calendar tab and assert the cross view shows that space's dot on // today and groups the day panel by space. // // Benign-path allowlist mirrors spaces.spec.ts: with auth disabled the UI's // /api/auth/me probe 404s on purpose; org/MCP/ssh probes 401/404. All benign. const BENIGN_PATHS = new Set([ '/api/auth/me', '/api/users/me/orgs', '/api/mcp/connections', '/api/mcp/servers', '/api/mcp/user-servers', '/api/ssh/connections', ]); const BENIGN_ASSET = /\.(ico|png|svg|map|webmanifest|json)$/i; const BENIGN_PATH_RE = /\/api\/local\/tasks\/\d+\/console\/status$|\/api\/local\/browser\/sessions\/task-session\/\d+$/; function trackFatalErrors(page: Page): string[] { const fatalErrors: string[] = []; page.on('pageerror', (err) => fatalErrors.push(`pageerror: ${err.message}`)); page.on('response', (res) => { if (res.status() >= 400) { const { pathname } = new URL(res.url()); if (!BENIGN_ASSET.test(pathname) && !BENIGN_PATHS.has(pathname) && !BENIGN_PATH_RE.test(pathname)) { fatalErrors.push(`HTTP ${res.status()} ${res.url()}`); } } }); return fatalErrors; } /** Viewer-local 'YYYY-MM-DD' today, matching the UI's localToday(). */ function e2eLocalToday(): string { const d = new Date(); const mm = String(d.getMonth() + 1).padStart(2, '0'); const dd = String(d.getDate()).padStart(2, '0'); return `${d.getFullYear()}-${mm}-${dd}`; } async function createAndOpenCaseSpace(page: Page, label: string) { await page.goto('/ui'); await page.getByTestId('nav-spaces').click(); const rail = page.getByTestId('space-rail'); await expect(rail).toBeVisible(); await page.getByTestId('create-space-btn').click(); const titleInput = page.getByTestId('space-title-input'); await expect(titleInput).toBeVisible(); const caseTitle = `${label}-${Date.now()}`; await titleInput.fill(caseTitle); await page.getByTestId('create-space-submit').click(); await expect(titleInput).toHaveCount(0); const caseRow = rail .locator('[data-testid="space-row"][data-space-kind="case"]') .filter({ hasText: caseTitle }); await expect(caseRow).toBeVisible(); const spaceId = await caseRow.getAttribute('data-space-id'); expect(spaceId).toBeTruthy(); await caseRow.click(); await expect(page.getByTestId('space-detail')).toBeVisible(); return { caseTitle, spaceId: spaceId as string }; } /** Add an event for today in the currently-open space's calendar tab. */ async function addTodayEvent(page: Page, title: string) { await page.getByTestId('space-tab-calendar').click(); await expect(page.getByTestId('space-cal-grid')).toBeVisible(); const today = e2eLocalToday(); await page.getByTestId(`space-cal-day-${today}`).click(); const panel = page.getByTestId('space-cal-day-panel'); await expect(panel).toBeVisible(); await panel.getByTestId('space-cal-add-event-btn').click(); const form = page.getByTestId('space-cal-add-event'); await expect(form).toBeVisible(); await form.getByTestId('space-cal-event-title').fill(title); await form.getByTestId('space-cal-event-time').fill('11:00'); await form.getByTestId('space-cal-event-save').click(); await expect(panel.getByText(title, { exact: true })).toBeVisible({ timeout: 10_000 }); } test('cross calendar: top-bar tab opens, grid renders, month nav works', async ({ page }) => { const fatalErrors = trackFatalErrors(page); await page.goto('/ui'); await page.getByTestId('nav-calendar').click(); await expect(page.getByTestId('cross-calendar')).toBeVisible(); await expect(page.getByTestId('cross-cal-grid')).toBeVisible(); await expect(page).toHaveURL(/[?&]page=calendar(&|$)/); // today's cell exists await expect(page.getByTestId(`cross-cal-day-${e2eLocalToday()}`)).toBeVisible(); // month nav flips the YYYY年M月 header and returns const title = page.getByTestId('cross-cal-title'); const initial = (await title.textContent())?.trim(); expect(initial).toMatch(/\d{4}年\d{1,2}月/); await page.getByTestId('cross-cal-next').click(); await expect(title).not.toHaveText(initial!); await page.getByTestId('cross-cal-prev').click(); await expect(title).toHaveText(initial!); expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]); }); test('cross calendar: a space with activity shows its dot + day panel groups by space', async ({ page }) => { const fatalErrors = trackFatalErrors(page); const { caseTitle, spaceId } = await createAndOpenCaseSpace(page, 'E2E横断カレンダー'); const eventTitle = `横断予定-${Date.now()}`; await addTodayEvent(page, eventTitle); // Open the top-bar cross calendar. await page.getByTestId('nav-calendar').click(); await expect(page.getByTestId('cross-cal-grid')).toBeVisible(); const today = e2eLocalToday(); const todayCell = page.getByTestId(`cross-cal-day-${today}`); // Today's cell carries the activity dot for our space. await expect(todayCell.getByTestId(`cross-cal-dot-${spaceId}`)).toBeVisible({ timeout: 10_000 }); // Click the day → panel grouped by space, with our space heading + its event. await todayCell.click(); const panel = page.getByTestId('cross-cal-day-panel'); await expect(panel).toBeVisible(); const spaceGroup = panel.getByTestId(`cross-cal-space-${spaceId}`); await expect(spaceGroup).toBeVisible(); await expect(spaceGroup.getByText(caseTitle, { exact: true })).toBeVisible(); await expect(spaceGroup.getByText(eventTitle, { exact: true })).toBeVisible({ timeout: 10_000 }); expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]); });