Files
kandev/apps/web/e2e/tests/pr/pr-watcher-dockview-layout.spec.ts
T

220 lines
8.5 KiB
TypeScript

import { execSync } from "node:child_process";
import path from "node:path";
import { test, expect } from "../../fixtures/test-base";
import { makeGitEnv } from "../../helpers/git-helper";
import { KanbanPage } from "../../pages/kanban-page";
import { SessionPage } from "../../pages/session-page";
test.describe("PR watcher dockview layout stability", () => {
/**
* Verifies the dockview layout stays correct when switching between
* PR tasks auto-created by the review watcher.
*
* Flow:
* 1. PR watcher detects 3 PRs and auto-creates tasks in the background
* 2. Open PR task 1 from the kanban homepage
* 3. Toggle plan mode (adds plan panel)
* 4. Switch to PR task 2 via sidebar → should have default layout
* 5. Switch to PR task 3 via sidebar → should have default layout
*
* Setup:
* Review step with auto_start_agent on_enter — all 3 tasks get primary
* sessions immediately, so sidebar navigation takes the fast (synchronous) path.
*/
test("layout remains correct when switching between PR watcher tasks with plan mode", async ({
testPage,
apiClient,
seedData,
backend,
}) => {
test.setTimeout(120_000);
// --- Register the GitHub repo so the PR watcher can resolve it to a real
// local path (the seed repo created in test-base.ts has no provider info,
// and clone-from-real-GitHub fails for the mocked testorg/testrepo). ---
const repoDir = path.join(backend.tmpDir, "repos", "e2e-repo");
await apiClient.createRepository(seedData.workspaceId, repoDir, "main", {
name: "GitHub Test Repo",
provider: "github",
provider_owner: "testorg",
provider_name: "testrepo",
});
// Create the PR head branches in the local seed repo so the executor's
// git checkout for auto-started review tasks succeeds (in production these
// branches would have been fetched during clone).
const gitEnv = makeGitEnv(backend.tmpDir);
for (const branch of ["fix/auth", "feat/dashboard", "docs/update"]) {
execSync(`git branch -f ${branch} main`, { cwd: repoDir, env: gitEnv });
}
// --- Seed workflow ---
const workflow = await apiClient.createWorkflow(
seedData.workspaceId,
"PR Watcher Layout Workflow",
);
const reviewStep = await apiClient.createWorkflowStep(workflow.id, "Review", 0);
// Configure auto-start so the review watcher immediately launches mock agents
// for all 3 PR tasks. By the time the test reaches the sidebar navigation
// steps, tasks 2 and 3 already have a primarySessionId in kanbanMulti.snapshots
// → handleSelectTask takes the fast (synchronous) path instead of the slow
// HTTP + WS round-trip that times out in CI.
await apiClient.updateWorkflowStep(reviewStep.id, {
events: { on_enter: [{ type: "auto_start_agent" }] },
});
await apiClient.saveUserSettings({
workspace_id: seedData.workspaceId,
workflow_filter_id: workflow.id,
enable_preview_on_click: false,
});
// --- Seed mock GitHub data ---
await apiClient.mockGitHubReset();
await apiClient.mockGitHubSetUser("test-user");
// Add 3 PRs with requested_reviewers so the review watcher picks them up
await apiClient.mockGitHubAddPRs([
{
number: 101,
title: "Fix auth bug",
state: "open",
head_branch: "fix/auth",
base_branch: "main",
author_login: "alice",
repo_owner: "testorg",
repo_name: "testrepo",
additions: 10,
deletions: 2,
requested_reviewers: [{ login: "test-user", type: "User" }],
},
{
number: 202,
title: "Add dashboard",
state: "open",
head_branch: "feat/dashboard",
base_branch: "main",
author_login: "bob",
repo_owner: "testorg",
repo_name: "testrepo",
additions: 80,
deletions: 5,
requested_reviewers: [{ login: "test-user", type: "User" }],
},
{
number: 303,
title: "Update docs",
state: "open",
head_branch: "docs/update",
base_branch: "main",
author_login: "charlie",
repo_owner: "testorg",
repo_name: "testrepo",
additions: 30,
deletions: 10,
requested_reviewers: [{ login: "test-user", type: "User" }],
},
]);
// Navigate to kanban BEFORE creating the review watch so the WS is
// subscribed when task.created/task.updated events fire.
const kanban = new KanbanPage(testPage);
await kanban.goto();
// --- Create review watch (triggers initial poll → auto-creates 3 tasks) ---
await apiClient.createReviewWatch(
seedData.workspaceId,
workflow.id,
reviewStep.id,
seedData.agentProfileId,
{
repos: [{ owner: "testorg", name: "testrepo" }],
prompt: "Review {{pr.link}}",
},
);
// --- Wait for all 3 PR tasks to appear in the Review column ---
const prTask1Title = "PR #101: Fix auth bug";
const prTask2Title = "PR #202: Add dashboard";
const prTask3Title = "PR #303: Update docs";
await expect(kanban.taskCardInColumn(prTask1Title, reviewStep.id)).toBeVisible({
timeout: 60_000,
});
await expect(kanban.taskCardInColumn(prTask2Title, reviewStep.id)).toBeVisible({
timeout: 60_000,
});
await expect(kanban.taskCardInColumn(prTask3Title, reviewStep.id)).toBeVisible({
timeout: 60_000,
});
// --- Click PR task 1 to enter session view ---
await kanban.taskCardInColumn(prTask1Title, reviewStep.id).click();
await expect(testPage).toHaveURL(/\/t\//, { timeout: 15_000 });
const session = new SessionPage(testPage);
await session.waitForLoad();
// Verify initial default layout for task 1
await expect(session.chat).toBeVisible({ timeout: 10_000 });
await expect(session.sidebar).toBeVisible();
// Wait for the mock agent to complete and the layout to be stable before toggling
// plan mode. Without this, an in-flight layout restore could swallow the panel add.
// Use `waitForChatIdle` (vs. raw `idleInput().waitFor`) so the helper's
// reload-and-retry recovery covers the rare case where the WS-driven idle
// signal misses its window under shard pressure.
await session.waitForChatIdle({ timeout: 30_000 });
// --- Toggle plan mode on task 1 ---
await session.togglePlanMode();
await expect(session.planPanel).toBeVisible({ timeout: 10_000 });
await expect(session.chat).toBeVisible();
await expect(session.sidebar).toBeVisible();
// --- Switch to PR task 2 via sidebar ---
await session.clickTaskInSidebar(prTask2Title);
// With auto_start_agent configured, task 2 already has a primarySessionId in
// kanbanMulti.snapshots → handleSelectTask takes the synchronous fast path
// and setActiveSession is called immediately.
await expect(
testPage
.getByRole("navigation", { name: "breadcrumb" })
.getByText(prTask2Title, { exact: false }),
).toBeVisible({ timeout: 30_000 });
// Task 2: full layout intact — sidebar, chat accessible, changes panel visible,
// plan panel must NOT leak from task 1, and layout fills the viewport
await expect(session.sidebar).toBeVisible({ timeout: 15_000 });
await session.expectNoLayoutGap();
await expect(session.planPanel).not.toBeVisible({ timeout: 10_000 });
// Changes panel verifies the right column rendered
await session.clickTab("Changes");
await expect(session.changes).toBeVisible({ timeout: 10_000 });
// Chat tab may be renamed (e.g. "#1 Mock • Mock Default") — use stable testid
await session.clickSessionChatTab();
await expect(session.chat).toBeVisible({ timeout: 10_000 });
// --- Switch to PR task 3 via sidebar ---
await session.clickTaskInSidebar(prTask3Title);
// Same fast-path navigation as task 2
await expect(
testPage
.getByRole("navigation", { name: "breadcrumb" })
.getByText(prTask3Title, { exact: false }),
).toBeVisible({ timeout: 30_000 });
// Task 3: same layout checks
await expect(session.sidebar).toBeVisible({ timeout: 15_000 });
await session.expectNoLayoutGap();
await expect(session.planPanel).not.toBeVisible({ timeout: 10_000 });
await session.clickTab("Changes");
await expect(session.changes).toBeVisible({ timeout: 10_000 });
// Chat tab may be renamed (e.g. "#1 Mock • Mock Default") — use stable testid
await session.clickSessionChatTab();
await expect(session.chat).toBeVisible({ timeout: 10_000 });
});
});