This commit is contained in:
@@ -114,6 +114,11 @@ test('cross calendar: top-bar tab opens, grid renders, month nav works', async (
|
||||
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横断カレンダー');
|
||||
|
||||
// The cross-cal dot reflects a space's activity for the day — task or
|
||||
// event (see issue #804: it used to be task-only, leaving single-day
|
||||
// events with no cross-calendar indicator). This space has an event and
|
||||
// no task, so the dot must come from the event alone.
|
||||
const eventTitle = `横断予定-${Date.now()}`;
|
||||
await addTodayEvent(page, eventTitle);
|
||||
|
||||
@@ -123,10 +128,12 @@ test('cross calendar: a space with activity shows its dot + day panel groups by
|
||||
|
||||
const today = e2eLocalToday();
|
||||
const todayCell = page.getByTestId(`cross-cal-day-${today}`);
|
||||
// Today's cell carries the activity dot for our space.
|
||||
// Today's cell carries the activity dot for our space (event-gated only).
|
||||
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.
|
||||
// Click the day → panel grouped by space, with our space heading and its
|
||||
// event (a space qualifies for the panel with either task or event
|
||||
// activity — see CrossSpaceCalendar.tsx's activeSpaces filter).
|
||||
await todayCell.click();
|
||||
const panel = page.getByTestId('cross-cal-day-panel');
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
@@ -69,8 +69,10 @@ test('create a custom piece from the sidebar, then edit + save it via the editor
|
||||
await nameInput.fill(pieceName);
|
||||
await nameInput.press('Enter');
|
||||
|
||||
// The new custom piece appears in the sidebar list.
|
||||
await expect(page.getByText(pieceName, { exact: false })).toBeVisible({ timeout: 15_000 });
|
||||
// The new custom piece appears in the sidebar list AND is auto-selected, so
|
||||
// its name also shows in the editor header (PieceEditor.tsx's <h2>) —
|
||||
// scope to the sidebar row button to avoid a strict-mode double match.
|
||||
await expect(page.getByRole('button', { name: pieceName })).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// It was persisted as a custom piece (the endpoint the page reads returns
|
||||
// { pieces: [...] } with custom:true on user pieces).
|
||||
@@ -88,6 +90,10 @@ test('create a custom piece from the sidebar, then edit + save it via the editor
|
||||
await page.getByRole('button', { name: 'YAML' }).click();
|
||||
const yaml = page.locator('textarea').first();
|
||||
await expect(yaml).toBeVisible({ timeout: 15_000 });
|
||||
// Terminal moves go through the `complete` tool, not rules[].next (which
|
||||
// only accepts other movement names / WAIT_SUBTASKS — see
|
||||
// docs/movement-control-flow-tools.md). Tool/edit access is a workspace tool
|
||||
// policy setting now, not a piece field (allowed_tools/edit are removed).
|
||||
const edited = [
|
||||
`name: ${pieceName}`,
|
||||
'description: edited by e2e',
|
||||
@@ -95,14 +101,10 @@ test('create a custom piece from the sidebar, then edit + save it via the editor
|
||||
'initial_movement: execute',
|
||||
'movements:',
|
||||
' - name: execute',
|
||||
' edit: true',
|
||||
' persona: worker',
|
||||
" instruction: 'do the thing'",
|
||||
' allowed_tools: [Read, Write, Edit]',
|
||||
' default_next: COMPLETE',
|
||||
' rules:',
|
||||
" - condition: '完了'",
|
||||
' next: COMPLETE',
|
||||
' rules: []',
|
||||
'',
|
||||
].join('\n');
|
||||
await yaml.fill(edited);
|
||||
@@ -125,10 +127,15 @@ test('create a custom piece from the sidebar, then edit + save it via the editor
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
});
|
||||
|
||||
// Negative/visibility: built-in pieces are not deletable and the editor renders
|
||||
// them read-only (no Save/Delete footer). 'chat' is a built-in piece always
|
||||
// present. Selecting it shows the read-only badge instead of the editable footer.
|
||||
test('a built-in piece opens read-only (no Save, no Delete)', async ({ page }) => {
|
||||
// Negative/visibility: built-in pieces are never deletable, by anyone
|
||||
// (PieceEditor.tsx: Delete is gated on effectiveSource !== 'builtin', with no
|
||||
// admin exception). The readonly badge/footer gate, by contrast, is
|
||||
// `!isAdmin && (builtin || global-custom)` — and with auth disabled the E2E
|
||||
// synthetic user is always admin (App.tsx: isAdmin = auth.mode ===
|
||||
// 'authenticated' ? user.role === 'admin' : true), so admins CAN edit
|
||||
// (Save) built-in pieces here; only non-admins get the true read-only view.
|
||||
// 'chat' is a built-in piece always present.
|
||||
test('a built-in piece opens editable for admin (no Delete, Save appears once dirty)', async ({ page }) => {
|
||||
const fatalErrors = trackFatalErrors(page);
|
||||
|
||||
await page.goto('/ui?page=pieces');
|
||||
@@ -137,11 +144,13 @@ test('a built-in piece opens read-only (no Save, no Delete)', async ({ page }) =
|
||||
// Open the built-in 'chat' piece from the Default Pieces section.
|
||||
await page.getByRole('button', { name: /chat/i }).first().click();
|
||||
|
||||
// The editor marks it read-only (PieceEditor renders the readonly badge and
|
||||
// omits the editable footer's Save button for built-ins).
|
||||
await expect(page.getByText(/read-?only/i).first()).toBeVisible({ timeout: 15_000 });
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toHaveCount(0);
|
||||
// No readonly badge (this session is admin) and no Delete (built-ins are
|
||||
// never deletable), but the editable footer with Save is present.
|
||||
const saveBtn = page.getByRole('button', { name: 'Save' });
|
||||
await expect(saveBtn).toBeVisible({ timeout: 15_000 });
|
||||
await expect(page.getByText(/read-?only/i)).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: 'Delete' })).toHaveCount(0);
|
||||
await expect(saveBtn).toBeDisabled();
|
||||
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
});
|
||||
|
||||
+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