sync: update from private repo (91d8d79c)
CI / build-and-test (push) Failing after 7m0s

This commit is contained in:
oss-sync
2026-07-09 23:57:26 +00:00
parent 63d34d7cf6
commit 2044f0a2c4
81 changed files with 3456 additions and 372 deletions
+109 -85
View File
@@ -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([]);
});