This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
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([]);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,88 @@
|
||||
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
|
||||
// <SpaceFiles> the Spaces page uses (testid `space-files`); 設定 reuses
|
||||
// <SpaceSettings> (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([]);
|
||||
});
|
||||
Reference in New Issue
Block a user