import { test, expect, type Page } from '@playwright/test'; // ── Tasks page sub-tabs (タスク / ファイル / 設定) ───────────────────────────── // // The Tasks page gained a sub-tab strip that surfaces the user's PERSONAL // workspace files + settings without leaving the page. The default sub-tab // (タスク) keeps the existing list+detail behavior; ファイル reuses the same // the Spaces page uses (testid `space-files`); 設定 reuses // (nav testid `space-settings-nav-agents`). // // With auth disabled the /api/auth/me probe 404s on purpose and the UI falls // back to the synthetic local user; the personal workspace is auto-created on // the first /api/local/spaces call. Personal-workspace file listing may 404 // when the tree is empty — that is benign here. const TASKS_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 TASKS_BENIGN_ASSET = /\.(ico|png|svg|map|webmanifest|json)$/i; // Personal-workspace files endpoints may legitimately 404 with an empty tree // (the SpaceFiles source-library group probes for source/index.jsonl content). const TASKS_BENIGN_PATH_RE = /\/api\/local\/spaces\/[^/]+\/files(\/content)?($|\?|\/)/; 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 ( !TASKS_BENIGN_ASSET.test(pathname) && !TASKS_BENIGN_PATHS.has(pathname) && !TASKS_BENIGN_PATH_RE.test(pathname) ) { fatalErrors.push(`HTTP ${res.status()} ${res.url()}`); } } }); return fatalErrors; } test('tasks page: タスク / ファイル / 設定 sub-tabs switch the content area', async ({ page }) => { const fatalErrors = trackFatalErrors(page); // Tasks is the default page. await page.goto('/ui'); // 1. The sub-tab strip and all three sub-tabs render. const strip = page.getByTestId('tasks-subtabs'); await expect(strip).toBeVisible(); await expect(page.getByTestId('tasks-subtab-tasks')).toBeVisible(); await expect(page.getByTestId('tasks-subtab-files')).toBeVisible(); await expect(page.getByTestId('tasks-subtab-settings')).toBeVisible(); // 2. ファイル shows the personal-workspace file view (reused SpaceFiles). await page.getByTestId('tasks-subtab-files').click(); await expect(page.getByTestId('space-files')).toBeVisible(); // URL state persists the active sub-tab for shareable/back-button behavior. await expect(page).toHaveURL(/[?&]tasksTab=files(&|$)/); // 3. 設定 shows the personal-workspace settings (reused SpaceSettings nav). await page.getByTestId('tasks-subtab-settings').click(); await expect(page.getByTestId('space-settings-nav-agents')).toBeVisible(); await expect(page).toHaveURL(/[?&]tasksTab=settings(&|$)/); // 4. タスク returns to the task list (default state, no tasksTab param). await page.getByTestId('tasks-subtab-tasks').click(); await expect(page.getByTestId('space-files')).toHaveCount(0); await expect(page.getByTestId('space-settings-nav-agents')).toHaveCount(0); await expect(page).not.toHaveURL(/[?&]tasksTab=/); expect(fatalErrors).toEqual([]); }); test('tasks page: tasksTab=files deep-link lands on the files sub-tab', async ({ page }) => { const fatalErrors = trackFatalErrors(page); await page.goto('/ui?tasksTab=files'); await expect(page.getByTestId('tasks-subtabs')).toBeVisible(); await expect(page.getByTestId('space-files')).toBeVisible(); expect(fatalErrors).toEqual([]); });