This commit is contained in:
+109
-85
@@ -217,10 +217,13 @@ test('spaces foundation: rail, create case space, open detail tabs', async ({ pa
|
||||
});
|
||||
|
||||
// Mobile (<md) layout: the rail and detail must NOT sit side-by-side on a
|
||||
// phone. With no space selected the rail is a full-width list; selecting a
|
||||
// space replaces the rail with a full-width detail plus a "← ワークスペース一覧"
|
||||
// back control that returns to the rail. At >=md both are visible together
|
||||
// (unchanged), so this invariant only holds at narrow widths.
|
||||
// phone. Startup auto-opens the personal workspace (App.tsx's one-shot
|
||||
// personalSpaceAutoOpened guard), so detail is the initial full-width view;
|
||||
// backing out returns to the full-width rail (the guard does not re-fire) and
|
||||
// selecting another space replaces the rail with detail plus a
|
||||
// "← ワークスペース一覧" back control that returns to the rail. At >=md both
|
||||
// are visible together (unchanged), so this invariant only holds at narrow
|
||||
// widths.
|
||||
test('spaces mobile: rail <-> detail switch with back control at 390px', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
|
||||
@@ -229,9 +232,18 @@ test('spaces mobile: rail <-> detail switch with back control at 390px', async (
|
||||
// URL state (page=spaces) instead — it's the same selection, sans drawer.
|
||||
await page.goto('/ui?page=spaces');
|
||||
|
||||
// No space selected: the rail is the visible full-width list; detail hidden.
|
||||
// Startup auto-opens the personal workspace: detail is the visible
|
||||
// full-width view, rail is hidden, and the back control is already present.
|
||||
const rail = page.getByTestId('space-rail');
|
||||
const detail = page.getByTestId('space-detail');
|
||||
const back = page.getByTestId('space-back-to-list');
|
||||
await expect(detail).toBeVisible();
|
||||
await expect(rail).toBeHidden();
|
||||
await expect(back).toBeVisible();
|
||||
|
||||
// Tapping back clears the selection and returns to the full-width rail. The
|
||||
// auto-open guard is one-shot, so it does not immediately re-select.
|
||||
await back.click();
|
||||
await expect(rail).toBeVisible();
|
||||
await expect(detail).toBeHidden();
|
||||
await expect(page.getByTestId('space-back-to-list')).toHaveCount(0);
|
||||
@@ -250,7 +262,6 @@ test('spaces mobile: rail <-> detail switch with back control at 390px', async (
|
||||
await expect(detail).toBeVisible();
|
||||
await expect(detail.getByText(caseTitle)).toBeVisible();
|
||||
await expect(rail).toBeHidden();
|
||||
const back = page.getByTestId('space-back-to-list');
|
||||
await expect(back).toBeVisible();
|
||||
|
||||
// Tapping back clears the selection and returns to the full-width rail.
|
||||
@@ -331,10 +342,10 @@ test('space inline chat: create + open conversation without leaving for Tasks',
|
||||
// (urlState serializes page=spaces) and sets the chat= param for the
|
||||
// selected inline chat. It must NOT switch to page=tasks (which would
|
||||
// drop page= entirely AND have no chat=).
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
await expect(page).toHaveURL(/[?&]chat=\d+/);
|
||||
expect(page.url()).toContain('chat=');
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
// The Spaces detail (not the Tasks board) is still mounted.
|
||||
await expect(detail).toBeVisible();
|
||||
|
||||
@@ -353,9 +364,8 @@ test('space inline chat: create + open conversation without leaving for Tasks',
|
||||
// No overlay close affordance — the redesign dropped the overlay entirely.
|
||||
await expect(page.getByTestId('detail-panel-close')).toHaveCount(0);
|
||||
// Invariant: still inline on Spaces, same chat, never bounced to Tasks.
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
await expect(page).toHaveURL(/[?&]chat=\d+/);
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
// Return to the conversation via the 会話 tab so the rest of the flow
|
||||
// (creating a second chat) starts from chat again.
|
||||
await conversation.getByTestId('space-chat-tab-chat').click();
|
||||
@@ -390,8 +400,7 @@ test('space inline chat: create + open conversation without leaving for Tasks',
|
||||
expect(secondChatId).not.toBe(firstChatId);
|
||||
await expect(conversation).toBeVisible();
|
||||
// Still on Spaces, still has a chat — never bounced to Tasks.
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
});
|
||||
@@ -497,8 +506,7 @@ test('space files: icon grid, upload appears as tile, click opens preview modal'
|
||||
await expect(dialog).toHaveCount(0);
|
||||
|
||||
// 6. No-nav invariant: file management never leaves Spaces for the Tasks board.
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
await expect(detail).toBeVisible();
|
||||
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
@@ -679,9 +687,8 @@ test('space output link: chat output-path link opens preview modal', async ({ pa
|
||||
await expect(dialog).toHaveCount(0);
|
||||
|
||||
// 7. No-nav invariant: clicking the link previewed in place, never bounced to Tasks.
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
await expect(page).toHaveURL(/[?&]chat=\d+/);
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
await expect(detail).toBeVisible();
|
||||
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
@@ -689,12 +696,17 @@ test('space output link: chat output-path link opens preview modal', async ({ pa
|
||||
|
||||
// Redesigned space-chat UX (feat/space-chat-ux). The inline conversation now
|
||||
// carries a SINGLE in-place tab bar (data-testid="space-chat-tabs", role=tablist)
|
||||
// with 会話 (chat) + 概要 (overview) + 進捗 (activity) + トレース (trace) and NO
|
||||
// files tab (files live at the space level). Clicking a non-chat tab swaps the
|
||||
// content IN PLACE — no overlay, no ✕ close button — and 会話 returns to the
|
||||
// conversation. Throughout, we never leave for the Tasks page (page=spaces +
|
||||
// chat= stay put). No LLM is configured in E2E, so we assert UI/structure only,
|
||||
// never agent output.
|
||||
// with 会話 (chat) + 概要 (overview) + 進捗 (activity) + ファイル (files) +
|
||||
// トレース (trace). This chat-level ファイル tab is distinct from the
|
||||
// space-level files (SpaceDetail.tsx comment: it shows the chat's own
|
||||
// input/output/logs under runs/{taskId}, not the shared space deliverables).
|
||||
// On desktop (md+) SpaceDetail renders a ChatDetailSplit: 会話 (with its
|
||||
// composer) is ALWAYS mounted on the left and never unmounts; a non-chat tab
|
||||
// opens the selected detail panel in a right pane alongside it (no overlay,
|
||||
// no ✕ close button) — clicking 会話 just collapses the right pane back to
|
||||
// full-width chat, it doesn't remount the conversation. Throughout, we never
|
||||
// leave for the Tasks page (space= + chat= stay put; no page= param). No LLM
|
||||
// is configured in E2E, so we assert UI/structure only, never agent output.
|
||||
test('space chat tabs: single in-place tab bar, overview in place, back to chat', async ({ page }) => {
|
||||
const fatalErrors: string[] = [];
|
||||
const benignAsset = /\.(ico|png|svg|map|webmanifest|json)$/i;
|
||||
@@ -755,15 +767,16 @@ test('space chat tabs: single in-place tab bar, overview in place, back to chat'
|
||||
const overviewTab = page.getByTestId('space-chat-tab-overview');
|
||||
await expect(chatTab).toBeVisible();
|
||||
await expect(overviewTab).toBeVisible();
|
||||
// No files tab inside this in-place bar (files are a space-level concern).
|
||||
await expect(tabs.getByTestId('space-chat-tab-files')).toHaveCount(0);
|
||||
// The chat-level ファイル tab is present too (per-chat I/O/logs, distinct
|
||||
// from the space-level files tab).
|
||||
await expect(tabs.getByTestId('space-chat-tab-files')).toBeVisible();
|
||||
// 会話 is the default-selected tab; the message composer (textarea) is present.
|
||||
await expect(chatTab).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(conversation.locator('textarea')).toBeVisible();
|
||||
|
||||
// 3. Click 概要 → its content renders IN PLACE. There is NO overlay and NO ✕
|
||||
// close button (headerless DetailPanel ⇒ no detail-panel-close testid), and
|
||||
// the overview tab becomes aria-selected. The conversation composer is gone.
|
||||
// 3. Click 概要 → it opens as a right-pane detail panel. There is NO overlay
|
||||
// and NO ✕ close button (headerless DetailPanel ⇒ no detail-panel-close
|
||||
// testid), and the overview tab becomes aria-selected.
|
||||
await overviewTab.click();
|
||||
await expect(overviewTab).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(chatTab).toHaveAttribute('aria-selected', 'false');
|
||||
@@ -772,20 +785,22 @@ test('space chat tabs: single in-place tab bar, overview in place, back to chat'
|
||||
await expect(conversation.getByRole('tabpanel')).toBeVisible();
|
||||
// No overlay close affordance anywhere on the page.
|
||||
await expect(page.getByTestId('detail-panel-close')).toHaveCount(0);
|
||||
// Composer is swapped out (overview content replaced the conversation in place).
|
||||
await expect(conversation.locator('textarea')).toHaveCount(0);
|
||||
// The chat composer stays mounted (ChatDetailSplit's left pane never
|
||||
// unmounts on desktop — see the top-of-file comment) — exactly one
|
||||
// textarea, not the zero a full replacement would produce.
|
||||
await expect(conversation.locator('textarea')).toHaveCount(1);
|
||||
|
||||
// 4. Click 会話 → back to the conversation; composer returns, chat selected.
|
||||
// 4. Click 会話 → the right pane collapses back to full-width chat.
|
||||
await chatTab.click();
|
||||
await expect(chatTab).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(overviewTab).toHaveAttribute('aria-selected', 'false');
|
||||
await expect(conversation.locator('textarea')).toBeVisible();
|
||||
|
||||
// 5. No navigation to Tasks throughout: page=spaces + chat= held the entire time.
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
await expect(page).toHaveURL(/[?&]chat=\d+/);
|
||||
expect(page.url()).toContain('chat=');
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
await expect(detail).toBeVisible();
|
||||
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
@@ -932,8 +947,7 @@ test('space settings tab: AGENTS.md per-space persistence + MCP/SSH panels rende
|
||||
await expect(page.getByTestId('space-ssh-panel')).toBeVisible();
|
||||
|
||||
// 6. No-nav invariant: settings is in-place, never leaves Spaces for Tasks.
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
await expect(detail).toBeVisible();
|
||||
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
@@ -1094,8 +1108,7 @@ test('space MCP isolation: server registered in A is visible in A, absent in B',
|
||||
await expect(page.getByTestId('space-mcp-panel')).not.toContainText(mcpName);
|
||||
|
||||
// No-nav invariant + no fatal client errors.
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -1153,8 +1166,7 @@ test('space chat DELETE: toolbar delete removes the chat and returns to the list
|
||||
await expect.poll(() => new URL(page.url()).searchParams.get('chat')).toBeFalsy();
|
||||
|
||||
// No-nav invariant: deletion stayed on Spaces, never bounced to Tasks.
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -1197,8 +1209,7 @@ test('space chat CANCEL: ChatPane stop button cancels a running task', async ({
|
||||
const status = await waitForTerminalJob(page, taskId, 20_000);
|
||||
expect(status).toBe('cancelled');
|
||||
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -1242,8 +1253,7 @@ test('space chat SHARE: toolbar share publishes a share token', async ({ page, c
|
||||
timeout: 15_000,
|
||||
});
|
||||
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -1287,8 +1297,7 @@ test('space chat CONTINUE: button gated on terminal job; opens ContinueWithPiece
|
||||
await expect(page.locator('#continue-instruction')).toBeVisible();
|
||||
await expect.poll(async () => pieceSelect.locator('option').count()).toBeGreaterThan(0);
|
||||
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -1325,7 +1334,7 @@ test('space chat VISIBILITY: no visibility control (fixed member-only scope)', a
|
||||
// tests in this file still assert it and are stale — see issue #782 follow-up).
|
||||
await expect(page).toHaveURL(/[?&]space=[^&]+/);
|
||||
await expect(page).toHaveURL(/[?&]chat=\d+/);
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -1345,15 +1354,17 @@ test('space chat FEEDBACK: 概要 tab renders OverviewTab; rating round-trip sav
|
||||
'E2E: フィードバックテスト用チャット。最初のメッセージ。',
|
||||
);
|
||||
|
||||
// Always-verifiable: switch to 概要 → OverviewTab renders in place. The
|
||||
// composer (textarea) is swapped out and the shared tabpanel is mounted.
|
||||
// Always-verifiable: switch to 概要 → OverviewTab opens as a right-pane
|
||||
// detail panel and the shared tabpanel is mounted.
|
||||
const overviewTab = conversation.getByTestId('space-chat-tab-overview');
|
||||
await expect(overviewTab).toBeVisible();
|
||||
await overviewTab.click();
|
||||
await expect(overviewTab).toHaveAttribute('aria-selected', 'true');
|
||||
const panel = conversation.getByRole('tabpanel');
|
||||
await expect(panel).toBeVisible();
|
||||
await expect(conversation.locator('textarea')).toHaveCount(0);
|
||||
// The chat composer stays mounted (ChatDetailSplit's left pane never
|
||||
// unmounts on desktop) — exactly one textarea.
|
||||
await expect(conversation.locator('textarea')).toHaveCount(1);
|
||||
|
||||
// Active path: the FeedbackPanel only renders once the job is succeeded/failed.
|
||||
const status = await waitForTerminalJob(page, taskId);
|
||||
@@ -1397,8 +1408,7 @@ test('space chat FEEDBACK: 概要 tab renders OverviewTab; rating round-trip sav
|
||||
conversationAfter.getByRole('tabpanel').getByRole('button', { name: /^(Change|変更)$/ }),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -1421,14 +1431,14 @@ test('space settings sub-tabs: agents/memory/pieces/skills/mcp/ssh all render th
|
||||
// memory → the MemoryEntriesPanel header + new-entry button render. The header
|
||||
// text is i18n (English in the E2E browser locale, Japanese otherwise).
|
||||
await page.getByTestId('space-settings-nav-memory').click();
|
||||
await expect(page.getByText(/Memory entries|メモリエントリ/)).toBeVisible({ timeout: 15_000 });
|
||||
await expect(page.getByRole('heading', { name: /Memory entries|メモリエントリ/ })).toBeVisible({ timeout: 15_000 });
|
||||
await expect(page.getByRole('button', { name: /New entry|新しいエントリ/ })).toBeVisible();
|
||||
|
||||
// pieces → the SpacePiecesPanel list (Default / Custom sections) renders. These
|
||||
// section labels are literal (non-i18n) strings; match the built-in piece list.
|
||||
// pieces → the SpacePiecesPanel list (Default / Custom sections) renders.
|
||||
// These section labels are i18n (spaces.json settings.pieces.default/custom).
|
||||
await page.getByTestId('space-settings-nav-pieces').click();
|
||||
await expect(page.getByText('Default', { exact: true })).toBeVisible({ timeout: 15_000 });
|
||||
await expect(page.getByText('Custom', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText(/^(Default|標準)$/)).toBeVisible({ timeout: 15_000 });
|
||||
await expect(page.getByText(/^(Custom|カスタム)$/)).toBeVisible();
|
||||
|
||||
// skills → the SkillsForm heading renders (the install-from-URL input is
|
||||
// intentionally hidden in space mode — the Git install path isn't space-scoped
|
||||
@@ -1442,8 +1452,7 @@ test('space settings sub-tabs: agents/memory/pieces/skills/mcp/ssh all render th
|
||||
await page.getByTestId('space-settings-nav-ssh').click();
|
||||
await expect(page.getByTestId('space-ssh-panel')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -1493,11 +1502,10 @@ test('space settings MEMORY: entry saved in space A persists + is absent in spac
|
||||
await expect(detail).toBeVisible();
|
||||
await page.getByTestId('space-tab-settings').click();
|
||||
await page.getByTestId('space-settings-nav-memory').click();
|
||||
await expect(page.getByText(/Memory entries|メモリエントリ/)).toBeVisible({ timeout: 15_000 });
|
||||
await expect(page.getByRole('heading', { name: /Memory entries|メモリエントリ/ })).toBeVisible({ timeout: 15_000 });
|
||||
await expect(page.getByText(memName, { exact: true })).toHaveCount(0);
|
||||
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -1542,8 +1550,7 @@ test('space calendar: tab renders month grid + month nav', async ({ page }) => {
|
||||
await page.getByTestId('space-cal-prev').click();
|
||||
await expect(title).toHaveText(initial!);
|
||||
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -1556,7 +1563,7 @@ test('space calendar: add event → list + day badge + persists across reload',
|
||||
|
||||
const today = e2eLocalToday();
|
||||
const todayCell = page.getByTestId(`space-cal-day-${today}`);
|
||||
// Badge baseline: today's event count badge (📌N) is absent before adding.
|
||||
// Baseline: today's event count badge (📌N) is absent before adding.
|
||||
await expect(todayCell.getByText(/^📌/)).toHaveCount(0);
|
||||
|
||||
// Open today's day panel and add an event.
|
||||
@@ -1577,13 +1584,26 @@ test('space calendar: add event → list + day badge + persists across reload',
|
||||
await expect(panel.getByText(eventTitle, { exact: true })).toBeVisible({ timeout: 10_000 });
|
||||
await expect(panel.getByText('14:30')).toBeVisible();
|
||||
|
||||
// The day cell event badge increments to 📌1 after the month query invalidates.
|
||||
await expect(todayCell.getByText('📌1')).toBeVisible({ timeout: 10_000 });
|
||||
// Grab the event id from its panel row so we can assert both month-grid
|
||||
// indicators by id (a single-day event drives TWO separate render paths).
|
||||
const row = panel.locator('[data-testid^="space-cal-event-"]').filter({ hasText: eventTitle }).first();
|
||||
const eventId = (await row.getAttribute('data-testid'))!.replace('space-cal-event-', '');
|
||||
expect(eventId).toMatch(/^\d+$/);
|
||||
|
||||
// Persists across a full reload (server-backed, not just client state).
|
||||
// Month grid shows BOTH indicators for the single-day event:
|
||||
// - the 📌1 day-cell badge (counts.eventCount path, added in #808 / #804).
|
||||
// exact:true so it pins count===1, not a substring of 📌1x.
|
||||
// - a colSpan-1 week bar (layoutWeekBars path) — a separate code path, so
|
||||
// assert it explicitly (a badge-only check would miss a phantom-bar bug).
|
||||
await expect(todayCell.getByText('📌1', { exact: true })).toBeVisible({ timeout: 10_000 });
|
||||
await expect(page.getByTestId(`space-cal-bar-${eventId}`)).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Persists across a full reload (server-backed, not just client state):
|
||||
// day-cell badge, week bar, and the day-panel event all come back.
|
||||
await page.reload();
|
||||
await page.getByTestId('space-tab-calendar').click();
|
||||
await expect(page.getByTestId(`space-cal-day-${today}`).getByText('📌1')).toBeVisible({ timeout: 10_000 });
|
||||
await expect(page.getByTestId(`space-cal-day-${today}`).getByText('📌1', { exact: true })).toBeVisible({ timeout: 10_000 });
|
||||
await expect(page.getByTestId(`space-cal-bar-${eventId}`)).toBeVisible({ timeout: 10_000 });
|
||||
await page.getByTestId(`space-cal-day-${today}`).click();
|
||||
await expect(page.getByTestId('space-cal-day-panel').getByText(eventTitle, { exact: true })).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
@@ -1621,12 +1641,20 @@ test('space calendar: edit + delete an event', async ({ page }) => {
|
||||
await page.getByTestId('space-cal-event-save').click();
|
||||
await expect(panel.getByText(newTitle, { exact: true })).toBeVisible({ timeout: 10_000 });
|
||||
await expect(panel.getByText(origTitle, { exact: true })).toHaveCount(0);
|
||||
// While the event exists the day cell carries both month-grid indicators:
|
||||
// the 📌1 badge (eventCount path) and the colSpan-1 week bar (layoutWeekBars
|
||||
// path). Assert both so a regression in either path is caught.
|
||||
await expect(todayCell.getByText('📌1', { exact: true })).toBeVisible({ timeout: 10_000 });
|
||||
await expect(page.getByTestId(`space-cal-bar-${eventId}`)).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Delete: confirm the window.confirm() then assert the row disappears + badge clears.
|
||||
// Delete: confirm the window.confirm() then assert the row disappears and
|
||||
// BOTH month-grid indicators clear (badge tracks eventCount, bar comes from
|
||||
// the events array — separate paths, so a delete could orphan one).
|
||||
page.once('dialog', (d) => d.accept());
|
||||
await panel.getByTestId(`space-cal-event-delete-${eventId}`).click();
|
||||
await expect(panel.getByTestId(`space-cal-event-${eventId}`)).toHaveCount(0, { timeout: 10_000 });
|
||||
await expect(todayCell.getByText(/^📌/)).toHaveCount(0, { timeout: 10_000 });
|
||||
await expect(page.getByTestId(`space-cal-bar-${eventId}`)).toHaveCount(0, { timeout: 10_000 });
|
||||
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
});
|
||||
@@ -1658,8 +1686,7 @@ test('space calendar: day panel lists tasks created in the space', async ({ page
|
||||
await expect(page.getByTestId('space-conversation')).toBeVisible();
|
||||
await expect(page).toHaveURL(/[?&]chat=\d+/);
|
||||
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -1706,8 +1733,7 @@ test('space settings MEMBERS: panel renders + invite shows no-auth hint', async
|
||||
await expect(picker).toBeVisible({ timeout: 15_000 });
|
||||
await expect(picker.getByText('認証を有効化するとワークスペースを共有できます。')).toBeVisible();
|
||||
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -1743,8 +1769,7 @@ test('space sources: curated ソース list view (incl. empty-state) is absent o
|
||||
await expect(page.getByTestId('space-sources')).toHaveCount(0);
|
||||
await expect(page.getByTestId('space-sources-empty')).toHaveCount(0);
|
||||
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -1799,8 +1824,7 @@ test('space sources: source/ folder still accumulates and is browsable; curated
|
||||
page.locator('[data-testid="space-file-tile"][data-name="index.jsonl"]'),
|
||||
).toHaveCount(0);
|
||||
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -1857,8 +1881,7 @@ test('space settings BROWSER: panel + three groups render with empty states', as
|
||||
await expect(recordings.getByRole('heading', { name: '録画' })).toBeVisible();
|
||||
await expect(recordings).toContainText('このワークスペースにはまだ録画がありません');
|
||||
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -1904,7 +1927,7 @@ test('space files DELETE: select a file and delete it (gone); ソース list vie
|
||||
await expect(tile).toHaveCount(0, { timeout: 15000 });
|
||||
|
||||
// No-nav invariant.
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
await expect(detail).toBeVisible();
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
});
|
||||
@@ -1978,13 +2001,15 @@ test('space RENAME: header pencil renames the workspace and the title updates',
|
||||
const fatalErrors = trackFatalErrors(page);
|
||||
const { detail, caseTitle, rail } = await createAndOpenCaseSpace(page, 'E2E改名');
|
||||
|
||||
// Open the inline rename editor, change the title, save.
|
||||
await page.getByTestId('space-rename').click();
|
||||
const input = page.getByTestId('space-rename-input');
|
||||
// Open the header pencil, which reuses the create/edit SpaceFormDialog
|
||||
// (space-title-input pre-filled with the current title, same submit button
|
||||
// as create). There is no dedicated inline rename input/save pair.
|
||||
await page.getByTestId('space-edit').click();
|
||||
const input = page.getByTestId('space-title-input');
|
||||
await expect(input).toBeVisible();
|
||||
const renamed = `${caseTitle}-改`;
|
||||
await input.fill(renamed);
|
||||
await page.getByTestId('space-rename-save').click();
|
||||
await page.getByTestId('space-form-submit').click();
|
||||
|
||||
// The header reflects the new title, and the rail row updates too.
|
||||
await expect(detail).toContainText(renamed);
|
||||
@@ -2013,8 +2038,7 @@ test('space DELETE: case workspace delete returns to the list (selection cleared
|
||||
await expect.poll(() => new URL(page.url()).searchParams.get('space')).toBeFalsy();
|
||||
|
||||
// Still on Spaces, never bounced to Tasks.
|
||||
await expect(page).toHaveURL(/[?&]page=spaces(&|$)/);
|
||||
expect(page.url()).not.toContain('page=tasks');
|
||||
expect(new URL(page.url()).searchParams.get('page')).toBeFalsy();
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user