mirror of
https://github.com/block/buzz.git
synced 2026-10-30 05:51:46 +00:00
## Summary - inject bounded project-home identity and repository context into managed agent sessions - add project-aware CLI flows for creating projects, repositories, issues, and related channels - match project homes through the existing relay query surface, then filter channel metadata client-side This is Part 1 of the channel-first Projects stack. Part 2 contains project creation and model foundations. ## Testing - `cargo fmt --all -- --check` - `cargo clippy -p buzz-cli -p buzz-acp --all-targets -- -D warnings` - `cargo test -p buzz-cli -p buzz-acp` — 1,184 tests passed, 1 doc test ignored - full pre-push gate passed ## Post-Deploy Monitoring & Validation - validate project-home agent context and project-aware CLI commands against a staging relay - healthy signals: project context matches the active channel, explicit repo coordinates remain stable, and normal channels receive no project block - failure signals: cross-channel project context, duplicate project creation, or commands targeting an unrelated repository; mitigate by reverting this PR --------- Signed-off-by: Thomas Petersen <thomasp@squareup.com> Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz> Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
1281 lines
48 KiB
TypeScript
1281 lines
48 KiB
TypeScript
import { expect, type Locator, test } from "@playwright/test";
|
|
|
|
import { waitForAnimations } from "../helpers/animations";
|
|
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
|
|
|
|
const SHOTS = "test-results/projects-v3-screenshots";
|
|
|
|
async function expectSinglePrimaryTextColumn(row: Locator) {
|
|
const primary = row.locator('[data-projects-text-priority="primary"]');
|
|
const secondary = row.locator('[data-projects-text-priority="secondary"]');
|
|
await expect(primary).toHaveCount(1);
|
|
expect(await secondary.count()).toBeGreaterThan(0);
|
|
const primaryColor = await primary.evaluate(
|
|
(element) => getComputedStyle(element).color,
|
|
);
|
|
const secondaryColors = await secondary.evaluateAll((elements) =>
|
|
elements.map((element) => getComputedStyle(element).color),
|
|
);
|
|
expect(secondaryColors.every((color) => color !== primaryColor)).toBe(true);
|
|
}
|
|
|
|
async function expectProjectContextGroups(
|
|
panel: Locator,
|
|
{ hasActions }: { hasActions: boolean },
|
|
) {
|
|
const detailsHeading = panel.getByRole("heading", {
|
|
name: "Details",
|
|
exact: true,
|
|
});
|
|
await expect(detailsHeading).toBeVisible();
|
|
if (hasActions) {
|
|
await expect(panel.getByTestId("project-context-actions")).toBeVisible();
|
|
} else {
|
|
await expect(panel.getByTestId("project-context-actions")).toHaveCount(0);
|
|
}
|
|
for (const name of [
|
|
"Actions",
|
|
"Assignment",
|
|
"Discussion",
|
|
"People",
|
|
"Task details",
|
|
"Review details",
|
|
"Review activity",
|
|
"Repository activity",
|
|
]) {
|
|
await expect(panel.getByRole("heading", { name, exact: true })).toHaveCount(
|
|
0,
|
|
);
|
|
}
|
|
await expect(panel.getByTestId("project-repository-people")).toHaveCount(0);
|
|
}
|
|
|
|
async function openBuzzProject(page: import("@playwright/test").Page) {
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByTestId("open-projects-view").click();
|
|
await page.getByTestId("projects-section-projects").click();
|
|
const projectEntry = page
|
|
.locator(
|
|
'[data-testid="project-card-buzz"], [data-testid="project-row-buzz"]',
|
|
)
|
|
.first();
|
|
await expect(projectEntry).toBeVisible({ timeout: 10_000 });
|
|
await projectEntry.click();
|
|
await page.getByTestId("project-home-context-repo-buzz").click();
|
|
}
|
|
|
|
test("repository-only relays keep the Repositories section available", async ({
|
|
page,
|
|
}) => {
|
|
await page.addInitScript(() => {
|
|
window.__BUZZ_E2E_REPOSITORY_ONLY_PROJECTS__ = true;
|
|
});
|
|
await installMockBridge(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByTestId("open-projects-view").click();
|
|
|
|
await expect(page.getByTestId("projects-page-tabs")).toBeVisible();
|
|
await page.getByTestId("projects-section-repositories").click();
|
|
await expect(
|
|
page.locator(
|
|
'[data-testid="repository-card-buzz"], [data-testid="repository-row-buzz"]',
|
|
),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("projects activity overview screenshot", async ({ page }) => {
|
|
await page.addInitScript(() => {
|
|
window.localStorage.setItem("buzz-theme", "light");
|
|
});
|
|
await installMockBridge(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByTestId("open-projects-view").click();
|
|
await expect(page.getByTestId("projects-page-tabs")).toBeVisible();
|
|
const activityHeader = page.getByTestId("projects-page-header");
|
|
await expect(activityHeader).toBeVisible();
|
|
await expect(page.getByTestId("projects-activity-relay-icon")).toHaveCount(0);
|
|
await expect(page.getByTestId("projects-activity-intro")).toHaveCSS(
|
|
"text-align",
|
|
"left",
|
|
);
|
|
await expect(page.getByTestId("projects-activity-search")).toBeVisible();
|
|
await expect(page.getByTestId("projects-activity-intro")).toContainText(
|
|
"Projects Activity",
|
|
);
|
|
await expect(
|
|
page.getByTestId("projects-overview-context-panel"),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("projects-overview-create-project"),
|
|
).toHaveCount(0);
|
|
await expect(
|
|
page.getByTestId("projects-activity-group").first(),
|
|
).toBeVisible();
|
|
await waitForAnimations(page);
|
|
await page.screenshot({ path: `${SHOTS}/00-projects-pulse.png` });
|
|
});
|
|
|
|
test("submitted project context stays compact and expandable", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByTestId("open-projects-view").click();
|
|
await page.getByTestId("projects-section-prs").click();
|
|
await page.getByRole("button", { name: "List layout" }).click();
|
|
await page.getByTestId("projects-overview-chat-toggle").click();
|
|
|
|
const panel = page.getByTestId("project-agent-chat-panel");
|
|
await panel.getByTestId("message-input").fill("Summarize these reviews");
|
|
await panel.getByTestId("message-input").press("Enter");
|
|
const context = panel.getByTestId("project-agent-sent-context");
|
|
await expect(context).toBeVisible();
|
|
await expect(
|
|
panel.getByRole("button", { name: "Preview message context" }),
|
|
).toBeVisible();
|
|
|
|
await waitForAnimations(page);
|
|
await panel.screenshot({
|
|
path: `${SHOTS}/08-agent-context-collapsed.png`,
|
|
});
|
|
|
|
await context.getByRole("button", { name: "Show sent context" }).click();
|
|
await expect(
|
|
context.getByTestId("project-agent-sent-context-payload"),
|
|
).toBeVisible();
|
|
await waitForAnimations(page);
|
|
await panel.screenshot({
|
|
path: `${SHOTS}/09-agent-context-expanded.png`,
|
|
});
|
|
});
|
|
|
|
test("sidebar project add flow browses before creating", async ({ page }) => {
|
|
await installMockBridge(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await expect(page.getByTestId("sidebar-project-buzz")).toHaveCount(0);
|
|
await page.getByTestId("sidebar-projects-section-label").hover();
|
|
await page.getByTestId("sidebar-projects-create").click();
|
|
|
|
const browser = page.getByTestId("project-browser-dialog");
|
|
await expect(browser).toBeVisible();
|
|
const search = browser.getByRole("searchbox", { name: "Search projects" });
|
|
await search.fill("new workspace");
|
|
await browser.getByTestId("project-browser-create").click();
|
|
|
|
await expect(page.getByTestId("create-project-dialog")).toBeVisible();
|
|
await expect(page.getByTestId("create-project-name")).toHaveValue(
|
|
"new workspace",
|
|
);
|
|
await expect(
|
|
page.getByTestId("create-project-channel-permissions"),
|
|
).toBeVisible();
|
|
await expect(page.getByTestId("create-project-listing")).toHaveText("Listed");
|
|
await expect(page.getByTestId("create-project-template")).toHaveText(
|
|
"Project home",
|
|
);
|
|
await expect(page.getByTestId("create-project-team")).toHaveText("None");
|
|
await expect(page.getByTestId("create-project-agent")).toHaveText("None");
|
|
await page.getByRole("button", { name: "Back to projects" }).click();
|
|
await expect(browser).toBeVisible();
|
|
|
|
await search.fill("buzz");
|
|
await browser.getByTestId("project-browser-result-buzz").click();
|
|
await expect(browser).toBeHidden();
|
|
await expect(
|
|
page.getByRole("navigation", { name: "Project breadcrumb" }),
|
|
).toContainText("buzz");
|
|
const addedProject = page.getByTestId("sidebar-project-buzz");
|
|
await expect(addedProject).toBeVisible();
|
|
await addedProject.click({ button: "right" });
|
|
await page.getByRole("menuitem", { name: "Remove from sidebar" }).click();
|
|
await expect(addedProject).toHaveCount(0);
|
|
});
|
|
|
|
test("restricted repositories keep event work visible and offer access help", async ({
|
|
page,
|
|
}) => {
|
|
await page.addInitScript((owner) => {
|
|
window.__BUZZ_E2E_PROJECT_OWNER_OVERRIDE__ = owner;
|
|
}, TEST_IDENTITIES.alice.pubkey);
|
|
await installMockBridge(page, {
|
|
projectAccessChannelId: "11111111-1111-4111-8111-111111111111",
|
|
projectRepoSnapshotError: "remote: repository not found",
|
|
});
|
|
await openBuzzProject(page);
|
|
|
|
const unavailableState = page
|
|
.getByTestId("project-repository-unavailable")
|
|
.first();
|
|
await expect(unavailableState).toContainText("Repository access restricted");
|
|
await expect(
|
|
unavailableState.getByTestId("repository-owner-name"),
|
|
).toHaveText("alice");
|
|
await expect(unavailableState).toContainText(/the repository owner/i);
|
|
await expect(
|
|
unavailableState
|
|
.locator(
|
|
'[data-testid="repository-owner-avatar-image"], [data-testid="repository-owner-avatar-fallback"]',
|
|
)
|
|
.first(),
|
|
).toBeVisible();
|
|
await expect(page.getByTestId("project-section-header")).toHaveCount(0);
|
|
await expect(
|
|
page.getByRole("button", { name: "Ask for access" }),
|
|
).toHaveCount(2);
|
|
await expect(
|
|
page.getByRole("button", { name: "Clone", exact: true }),
|
|
).toHaveCount(0);
|
|
await expect(page.getByTestId("project-repository-file-count")).toHaveText(
|
|
"—",
|
|
);
|
|
|
|
await page.getByRole("tab", { name: "Tasks", exact: true }).click();
|
|
await expect(page.getByTestId("project-issue-row").first()).toBeVisible();
|
|
|
|
await page.getByRole("tab", { name: "Review", exact: true }).click();
|
|
await expect(
|
|
page.getByTestId("project-pull-request-row").first(),
|
|
).toBeVisible();
|
|
|
|
await page.getByRole("tab", { name: "Files", exact: true }).click();
|
|
await expect(page.getByText("Repository access restricted")).toBeVisible();
|
|
await expect(page.getByTestId("project-section-header")).toHaveCount(0);
|
|
|
|
await page.getByRole("tab", { name: "Commits", exact: true }).click();
|
|
await expect(page.getByText("Repository access restricted")).toBeVisible();
|
|
await page.getByRole("button", { name: "Ask for access" }).first().click();
|
|
|
|
const chatPanel = page.getByTestId("project-agent-chat-panel");
|
|
await expect(chatPanel).toBeVisible();
|
|
await expect(chatPanel.getByTestId("project-agent-context")).toContainText(
|
|
"Commits",
|
|
);
|
|
await expect(chatPanel.getByTestId("message-composer")).toBeVisible();
|
|
});
|
|
|
|
test("repository pages show a centered Buzz loader while fetching", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page, { projectRepoSnapshotDelayMs: 750 });
|
|
await openBuzzProject(page);
|
|
|
|
const loader = page.getByTestId("buzz-loading-state");
|
|
await expect(loader).toBeVisible();
|
|
await expect(
|
|
loader.getByRole("img", { name: "Loading repository" }),
|
|
).toBeVisible();
|
|
const animatedMark = loader.locator(".buzz-logo__mark");
|
|
await expect(animatedMark).toHaveCSS(
|
|
"animation-name",
|
|
"buzz-logo-scale-pulse",
|
|
);
|
|
await expect(animatedMark).toHaveCSS("opacity", "1");
|
|
await expect(loader).toHaveCSS("justify-content", "center");
|
|
await expect(loader).toBeHidden({ timeout: 5_000 });
|
|
});
|
|
|
|
// Walks the Projects v3 workspace through its headline states so PR
|
|
// screenshots capture distinct pixels per feature (overview box, tab-strip
|
|
// plus, issue detail with inline copy link + avatar timeline, PR detail).
|
|
test("projects v3 workspace screenshot states", async ({ page }) => {
|
|
await installMockBridge(page);
|
|
await openBuzzProject(page);
|
|
const initialProjectBreadcrumb = page.getByRole("navigation", {
|
|
name: "Project breadcrumb",
|
|
});
|
|
await expect(
|
|
initialProjectBreadcrumb.getByTestId("project-breadcrumb-project"),
|
|
).toBeVisible();
|
|
await expect(
|
|
initialProjectBreadcrumb.getByTestId("project-breadcrumb-repository"),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByRole("button", { name: "Open Discussion" }),
|
|
).toHaveCount(0);
|
|
|
|
// Borderless workspace: repository controls live in the persistent,
|
|
// resizable auxiliary panel instead of a header row.
|
|
const backButton = page.getByTestId("project-workspace-back");
|
|
const overviewTab = page.getByRole("tab", { name: "Overview" });
|
|
const filesTab = page.getByRole("tab", { name: "Files" });
|
|
const channelsTab = page.getByRole("tab", { name: "Channels" });
|
|
const contributorsTab = page.getByRole("tab", { name: "Contributors" });
|
|
const tabMenu = page.getByTestId("project-workspace-tab-menu");
|
|
const workspacePanel = page.getByTestId("project-workspace-panel");
|
|
const projectDetailScroll = page.getByTestId("project-detail-scroll");
|
|
const repositoryActionsPanel = page.getByTestId(
|
|
"project-repository-actions-panel",
|
|
);
|
|
const expectInsetSection = async () => {
|
|
const sectionHeader = workspacePanel.getByTestId("project-section-header");
|
|
const [workspaceBox, headerBox, menuBox] = await Promise.all([
|
|
workspacePanel.boundingBox(),
|
|
sectionHeader.boundingBox(),
|
|
tabMenu.boundingBox(),
|
|
]);
|
|
expect(workspaceBox).not.toBeNull();
|
|
expect(headerBox).not.toBeNull();
|
|
expect(menuBox).not.toBeNull();
|
|
expect(Math.round((headerBox?.x ?? 0) - (workspaceBox?.x ?? 0))).toBe(16);
|
|
expect(
|
|
Math.round(
|
|
(workspaceBox?.x ?? 0) +
|
|
(workspaceBox?.width ?? 0) -
|
|
((headerBox?.x ?? 0) + (headerBox?.width ?? 0)),
|
|
),
|
|
).toBe(16);
|
|
expect(workspaceBox?.x).toBe(menuBox?.x);
|
|
expect((workspaceBox?.x ?? 0) + (workspaceBox?.width ?? 0)).toBe(
|
|
(menuBox?.x ?? 0) + (menuBox?.width ?? 0),
|
|
);
|
|
};
|
|
await expect(overviewTab).toBeVisible();
|
|
await expect(filesTab).toBeVisible();
|
|
expect((await overviewTab.boundingBox())?.x).toBeLessThan(
|
|
(await filesTab.boundingBox())?.x ?? 0,
|
|
);
|
|
await expect(backButton).toBeVisible();
|
|
await expect(page.getByTestId("app-sidebar")).toBeVisible();
|
|
await expect(projectDetailScroll).toHaveCSS("overscroll-behavior-y", "none");
|
|
await filesTab.click();
|
|
await expect(filesTab).toHaveAttribute("data-state", "active");
|
|
const [projectDetailScrollBounds, initialTabMenuBounds] = await Promise.all([
|
|
projectDetailScroll.boundingBox(),
|
|
tabMenu.boundingBox(),
|
|
]);
|
|
expect(projectDetailScrollBounds).not.toBeNull();
|
|
expect(initialTabMenuBounds?.y).toBe(projectDetailScrollBounds?.y);
|
|
expect(
|
|
await filesTab.evaluate((element) =>
|
|
Number.parseFloat(getComputedStyle(element).borderRadius),
|
|
),
|
|
).toBeGreaterThan(13);
|
|
expect((await filesTab.boundingBox())?.height).toBe(28);
|
|
await expect(repositoryActionsPanel).toBeVisible();
|
|
const projectContextCard = repositoryActionsPanel.getByTestId(
|
|
"project-context-card",
|
|
);
|
|
await expect(projectContextCard).toBeVisible();
|
|
await expect(projectContextCard).toHaveCSS("border-radius", "16px");
|
|
const repositoryHeading = repositoryActionsPanel.getByRole("heading", {
|
|
name: "buzz",
|
|
exact: true,
|
|
});
|
|
await expect(repositoryHeading).toBeVisible();
|
|
await expect(repositoryHeading).toHaveCSS("font-size", "14px");
|
|
await expectProjectContextGroups(repositoryActionsPanel, {
|
|
hasActions: true,
|
|
});
|
|
await expect(
|
|
repositoryActionsPanel.getByRole("heading", {
|
|
name: "Languages",
|
|
exact: true,
|
|
}),
|
|
).toHaveCount(0);
|
|
const detailsHeading = repositoryActionsPanel.getByRole("heading", {
|
|
name: "Details",
|
|
exact: true,
|
|
});
|
|
const fetchButton = repositoryActionsPanel.getByRole("button", {
|
|
name: "Fetch",
|
|
exact: true,
|
|
});
|
|
await expect(detailsHeading.locator("..")).toHaveCSS(
|
|
"border-top-width",
|
|
"0px",
|
|
);
|
|
expect(
|
|
await repositoryHeading.evaluate(
|
|
(element) => getComputedStyle(element).color,
|
|
),
|
|
).toBe(
|
|
await detailsHeading.evaluate((element) => getComputedStyle(element).color),
|
|
);
|
|
const actionsSection = repositoryActionsPanel.getByTestId(
|
|
"project-context-actions",
|
|
);
|
|
const detailsSection = repositoryActionsPanel.getByTestId(
|
|
"project-context-details",
|
|
);
|
|
const [actionsSectionBounds, detailsSectionBounds, detailsHeadingBounds] =
|
|
await Promise.all([
|
|
actionsSection.boundingBox(),
|
|
detailsSection.boundingBox(),
|
|
detailsHeading.boundingBox(),
|
|
]);
|
|
expect(
|
|
(detailsSectionBounds?.y ?? 0) -
|
|
((actionsSectionBounds?.y ?? 0) + (actionsSectionBounds?.height ?? 0)),
|
|
).toBe(8);
|
|
expect((detailsHeadingBounds?.y ?? 0) - (detailsSectionBounds?.y ?? 0)).toBe(
|
|
8,
|
|
);
|
|
await expect(fetchButton).toHaveCSS("text-align", "left");
|
|
const sourceButton = repositoryActionsPanel.getByRole("button", {
|
|
name: "Remote",
|
|
exact: true,
|
|
});
|
|
const latestDetailRow = repositoryActionsPanel
|
|
.getByText("Latest", { exact: true })
|
|
.locator("..");
|
|
const [
|
|
alignedDetailsHeadingBounds,
|
|
sourceButtonBounds,
|
|
sourceSectionBounds,
|
|
repositoryActionListBounds,
|
|
sourceTrailingIconBounds,
|
|
fetchButtonBounds,
|
|
fetchIconBounds,
|
|
branchTrailingIconBounds,
|
|
latestDetailRowBounds,
|
|
latestDetailValueBounds,
|
|
managementActionsBounds,
|
|
] = await Promise.all([
|
|
detailsHeading.boundingBox(),
|
|
sourceButton.boundingBox(),
|
|
repositoryActionsPanel
|
|
.getByTestId("project-context-source-controls")
|
|
.boundingBox(),
|
|
repositoryActionsPanel
|
|
.getByTestId("project-context-repository-actions")
|
|
.boundingBox(),
|
|
sourceButton.locator("svg").last().boundingBox(),
|
|
fetchButton.boundingBox(),
|
|
fetchButton.locator("svg").first().boundingBox(),
|
|
repositoryActionsPanel
|
|
.getByTestId("project-repository-branch-trigger")
|
|
.locator("svg")
|
|
.last()
|
|
.boundingBox(),
|
|
latestDetailRow.boundingBox(),
|
|
latestDetailRow.locator("dd").boundingBox(),
|
|
repositoryActionsPanel
|
|
.getByTestId("project-repository-management-actions")
|
|
.boundingBox(),
|
|
]);
|
|
expect(alignedDetailsHeadingBounds?.height).toBe(28);
|
|
expect(sourceButtonBounds?.height).toBe(28);
|
|
expect(fetchButtonBounds?.height).toBe(28);
|
|
expect(latestDetailRowBounds?.height).toBe(28);
|
|
expect(
|
|
(repositoryActionListBounds?.y ?? 0) -
|
|
((sourceSectionBounds?.y ?? 0) + (sourceSectionBounds?.height ?? 0)),
|
|
).toBe(2);
|
|
expect(fetchButtonBounds?.x ?? 0).toBeLessThan(
|
|
alignedDetailsHeadingBounds?.x ?? 0,
|
|
);
|
|
expect(fetchIconBounds?.x).toBe(alignedDetailsHeadingBounds?.x);
|
|
const detailRight =
|
|
(latestDetailValueBounds?.x ?? 0) + (latestDetailValueBounds?.width ?? 0);
|
|
expect(
|
|
(sourceTrailingIconBounds?.x ?? 0) + (sourceTrailingIconBounds?.width ?? 0),
|
|
).toBe(detailRight);
|
|
expect(
|
|
(branchTrailingIconBounds?.x ?? 0) + (branchTrailingIconBounds?.width ?? 0),
|
|
).toBe(detailRight);
|
|
expect(
|
|
(managementActionsBounds?.x ?? 0) + (managementActionsBounds?.width ?? 0),
|
|
).toBe(detailRight);
|
|
await expect(
|
|
repositoryActionsPanel.getByText("Working copy", { exact: true }),
|
|
).toHaveCount(0);
|
|
await expect(
|
|
repositoryActionsPanel.getByTestId("project-right-panel-scope"),
|
|
).toHaveCount(0);
|
|
const sharedHeaderBackdrop = page.getByTestId(
|
|
"project-shared-header-backdrop",
|
|
);
|
|
await expect(sharedHeaderBackdrop).toBeVisible();
|
|
await expect
|
|
.poll(() =>
|
|
sharedHeaderBackdrop.evaluate(
|
|
(element) => getComputedStyle(element).backdropFilter,
|
|
),
|
|
)
|
|
.not.toBe("none");
|
|
const projectPanelLayout = page.getByTestId("project-panel-layout");
|
|
const projectContentPod = page.getByTestId("project-content-pod");
|
|
const appContentSurface = page
|
|
.locator("[data-buzz-content-surface]")
|
|
.filter({ has: projectPanelLayout })
|
|
.first();
|
|
await expect(projectPanelLayout).toHaveAttribute("data-detached", "true");
|
|
await expect(appContentSurface).toHaveCSS(
|
|
"background-color",
|
|
"rgba(0, 0, 0, 0)",
|
|
);
|
|
await expect(appContentSurface).toHaveCSS("border-radius", "0px");
|
|
await expect(projectContentPod).toHaveCSS("border-radius", "16px");
|
|
await expect(projectContentPod).toHaveCSS("box-shadow", "none");
|
|
await expect(repositoryActionsPanel).toHaveCSS("border-radius", "0px");
|
|
await expect(repositoryActionsPanel).toHaveCSS(
|
|
"background-color",
|
|
"rgba(0, 0, 0, 0)",
|
|
);
|
|
await page.evaluate(() => document.documentElement.classList.add("dark"));
|
|
await expect(appContentSurface).toHaveCSS(
|
|
"background-color",
|
|
"rgba(0, 0, 0, 0)",
|
|
);
|
|
await expect(projectContentPod).toHaveCSS("border-radius", "16px");
|
|
await expect(projectContextCard).toHaveCSS("border-radius", "16px");
|
|
await page.evaluate(() => document.documentElement.classList.remove("dark"));
|
|
await expect(projectContextCard).toHaveCSS("border-radius", "16px");
|
|
await expect(projectContextCard).toHaveCSS("border-width", "0px");
|
|
await expect(projectContextCard).toHaveCSS("box-shadow", "none");
|
|
const [
|
|
projectContextCardBounds,
|
|
sharedHeaderBackdropBounds,
|
|
projectContentPodBounds,
|
|
repositoryActionsPanelBounds,
|
|
] = await Promise.all([
|
|
projectContextCard.boundingBox(),
|
|
sharedHeaderBackdrop.boundingBox(),
|
|
projectContentPod.boundingBox(),
|
|
repositoryActionsPanel.boundingBox(),
|
|
]);
|
|
expect(projectContextCardBounds).not.toBeNull();
|
|
expect(sharedHeaderBackdropBounds).not.toBeNull();
|
|
expect(projectContentPodBounds).not.toBeNull();
|
|
expect(repositoryActionsPanelBounds).not.toBeNull();
|
|
expect(Math.round(repositoryActionsPanelBounds?.width ?? 0)).toBe(280);
|
|
expect(
|
|
await projectContextCard.evaluate(
|
|
(element) => getComputedStyle(element).backgroundColor,
|
|
),
|
|
).toBe(
|
|
await projectContentPod.evaluate(
|
|
(element) => getComputedStyle(element).backgroundColor,
|
|
),
|
|
);
|
|
expect(projectContextCardBounds?.height ?? 0).toBeLessThan(
|
|
repositoryActionsPanelBounds?.height ?? 0,
|
|
);
|
|
expect(projectContextCardBounds?.y).toBe(repositoryActionsPanelBounds?.y);
|
|
expect(
|
|
(sharedHeaderBackdropBounds?.x ?? 0) +
|
|
(sharedHeaderBackdropBounds?.width ?? 0),
|
|
).toBeLessThanOrEqual(
|
|
(projectContentPodBounds?.x ?? 0) + (projectContentPodBounds?.width ?? 0),
|
|
);
|
|
expect(
|
|
(repositoryActionsPanelBounds?.x ?? 0) -
|
|
((projectContentPodBounds?.x ?? 0) +
|
|
(projectContentPodBounds?.width ?? 0)),
|
|
).toBe(8);
|
|
expect(
|
|
(sharedHeaderBackdropBounds?.x ?? 0) +
|
|
(sharedHeaderBackdropBounds?.width ?? 0),
|
|
).toBeLessThan(repositoryActionsPanelBounds?.x ?? 0);
|
|
const contextRail = page.getByTestId("project-context-rail");
|
|
const resizeHandle = repositoryActionsPanel.getByTestId(
|
|
"right-auxiliary-pane-resize-handle",
|
|
);
|
|
const resizeHandleBounds = await resizeHandle.boundingBox();
|
|
expect(resizeHandleBounds).not.toBeNull();
|
|
const resizeStartX =
|
|
(resizeHandleBounds?.x ?? 0) + (resizeHandleBounds?.width ?? 0) / 2;
|
|
const resizeStartY =
|
|
(resizeHandleBounds?.y ?? 0) + (resizeHandleBounds?.height ?? 0) / 2;
|
|
await resizeHandle.dispatchEvent("pointerdown", {
|
|
button: 0,
|
|
clientX: resizeStartX,
|
|
clientY: resizeStartY,
|
|
pointerId: 1,
|
|
pointerType: "mouse",
|
|
});
|
|
await expect(contextRail).toHaveAttribute("data-resizing", "true");
|
|
await expect(contextRail).toHaveCSS("transition-duration", "0s");
|
|
await page.mouse.move(resizeStartX - 40, resizeStartY);
|
|
await expect
|
|
.poll(async () =>
|
|
repositoryActionsPanel.evaluate((element) =>
|
|
Math.round(element.getBoundingClientRect().width),
|
|
),
|
|
)
|
|
.toBe(320);
|
|
await page.mouse.up();
|
|
await expect(contextRail).toHaveAttribute("data-resizing", "false");
|
|
await expect(contextRail).toHaveCSS("transition-duration", "0.2s");
|
|
await resizeHandle.dblclick();
|
|
await expect
|
|
.poll(async () =>
|
|
repositoryActionsPanel.evaluate((element) =>
|
|
Math.round(element.getBoundingClientRect().width),
|
|
),
|
|
)
|
|
.toBe(280);
|
|
const repositoryPanelTab = page.getByTestId(
|
|
"project-right-panel-repository-tab",
|
|
);
|
|
const chatPanelTab = page.getByTestId("project-right-panel-chat-tab");
|
|
const terminalButton = page.getByTestId("project-terminal-toggle");
|
|
const terminalIcon = page.getByTestId("project-terminal-icon");
|
|
const repositoryContextIcon = page.getByTestId(
|
|
"project-right-panel-repository-icon",
|
|
);
|
|
await expect(repositoryPanelTab).toHaveAttribute("aria-pressed", "true");
|
|
await expect(repositoryPanelTab).toHaveAttribute(
|
|
"aria-label",
|
|
"Hide project context",
|
|
);
|
|
await expect(terminalIcon).toBeVisible();
|
|
await expect(repositoryPanelTab).toHaveCSS(
|
|
"background-color",
|
|
"rgba(0, 0, 0, 0)",
|
|
);
|
|
await expect(repositoryContextIcon).toHaveCSS("opacity", "1");
|
|
await expect(contextRail).toHaveCSS("width", "288px");
|
|
await expect(contextRail).toHaveCSS("transition-duration", "0.2s");
|
|
// The icon is an inline SVG drawn with currentColor, so it must inherit
|
|
// the toggle button's text color to stay tinted with button state.
|
|
expect(
|
|
await terminalIcon.evaluate((element) => ({
|
|
color: getComputedStyle(element).color,
|
|
strokesCurrentColor: Array.from(element.querySelectorAll("rect")).some(
|
|
(rect) =>
|
|
rect.getAttribute("stroke") === "currentColor" ||
|
|
rect.getAttribute("fill") === "currentColor",
|
|
),
|
|
tagName: element.tagName.toLowerCase(),
|
|
})),
|
|
).toMatchObject({
|
|
color: await terminalButton.evaluate(
|
|
(element) => getComputedStyle(element).color,
|
|
),
|
|
strokesCurrentColor: true,
|
|
tagName: "svg",
|
|
});
|
|
const [repositoryTabBounds, chatTabBounds, terminalTabBounds] =
|
|
await Promise.all([
|
|
repositoryPanelTab.boundingBox(),
|
|
chatPanelTab.boundingBox(),
|
|
terminalButton.boundingBox(),
|
|
]);
|
|
expect(repositoryTabBounds?.width).toBe(chatTabBounds?.width);
|
|
expect(terminalTabBounds?.x).toBeLessThan(chatTabBounds?.x ?? 0);
|
|
expect(chatTabBounds?.x).toBeLessThan(repositoryTabBounds?.x ?? 0);
|
|
await chatPanelTab.click();
|
|
const agentChatPanel = page.getByTestId("project-agent-chat-panel");
|
|
await expect(agentChatPanel).toBeVisible();
|
|
await expect(projectPanelLayout).toHaveAttribute("data-detached", "false");
|
|
await expect(projectContentPod).toHaveCount(0);
|
|
await expect(appContentSurface).toHaveCSS("border-radius", "16px");
|
|
await expect
|
|
.poll(() =>
|
|
appContentSurface.evaluate(
|
|
(element) => getComputedStyle(element).backgroundColor,
|
|
),
|
|
)
|
|
.not.toBe("rgba(0, 0, 0, 0)");
|
|
await expect(agentChatPanel.getByTestId("message-composer")).toBeVisible();
|
|
const agentContext = agentChatPanel.getByTestId("project-agent-context");
|
|
await expect(agentContext).toBeVisible();
|
|
await expect(agentContext).toContainText("Files");
|
|
await expect(agentContext).not.toContainText("Buzz /");
|
|
// The context rail reveals the chat panel with a width transition; measure
|
|
// only after it settles or the panel's unclipped box overhangs the rail.
|
|
await waitForAnimations(page);
|
|
const [
|
|
attachedSharedHeaderBackdropBounds,
|
|
tabMenuHeaderBounds,
|
|
agentContextBounds,
|
|
] = await Promise.all([
|
|
sharedHeaderBackdrop.boundingBox(),
|
|
tabMenu.boundingBox(),
|
|
agentContext.boundingBox(),
|
|
]);
|
|
expect(attachedSharedHeaderBackdropBounds).not.toBeNull();
|
|
expect(tabMenuHeaderBounds).not.toBeNull();
|
|
expect(agentContextBounds).not.toBeNull();
|
|
expect(tabMenuHeaderBounds?.height).toBe(52);
|
|
expect(
|
|
Math.abs((tabMenuHeaderBounds?.y ?? 0) - (agentContextBounds?.y ?? 0)),
|
|
).toBeLessThanOrEqual(1);
|
|
expect(
|
|
Math.abs(
|
|
(tabMenuHeaderBounds?.height ?? 0) - (agentContextBounds?.height ?? 0),
|
|
),
|
|
).toBeLessThanOrEqual(1);
|
|
expect(attachedSharedHeaderBackdropBounds?.x).toBeLessThanOrEqual(
|
|
tabMenuHeaderBounds?.x ?? 0,
|
|
);
|
|
expect(
|
|
(attachedSharedHeaderBackdropBounds?.x ?? 0) +
|
|
(attachedSharedHeaderBackdropBounds?.width ?? 0),
|
|
).toBeGreaterThanOrEqual(
|
|
(agentContextBounds?.x ?? 0) + (agentContextBounds?.width ?? 0),
|
|
);
|
|
await expect
|
|
.poll(() =>
|
|
agentContext.evaluate((element) => getComputedStyle(element).position),
|
|
)
|
|
.toBe("absolute");
|
|
const agentConversationScroll = agentChatPanel.getByTestId(
|
|
"project-agent-conversation-scroll",
|
|
);
|
|
await expect
|
|
.poll(() =>
|
|
agentConversationScroll.evaluate((element) =>
|
|
Number.parseFloat(getComputedStyle(element).paddingTop),
|
|
),
|
|
)
|
|
.toBeGreaterThanOrEqual(52);
|
|
await agentChatPanel
|
|
.getByTestId("message-input")
|
|
.fill("Summarize this repository.");
|
|
await agentChatPanel.getByTestId("send-message").click();
|
|
const projectAgentMessage = agentChatPanel
|
|
.locator("[data-message-id]")
|
|
.filter({ hasText: "Summarize this repository." });
|
|
await expect(projectAgentMessage).toBeVisible();
|
|
const projectAgentMessageId =
|
|
await projectAgentMessage.getAttribute("data-message-id");
|
|
expect(projectAgentMessageId).not.toBeNull();
|
|
const projectAgentChannelId = await agentChatPanel
|
|
.locator("[data-project-agent-channel-id]")
|
|
.getAttribute("data-project-agent-channel-id");
|
|
expect(projectAgentChannelId).not.toBeNull();
|
|
await page.evaluate(
|
|
async ({ channelId, parentEventId }) => {
|
|
if (!channelId)
|
|
throw new Error("Project agent DM channel was not recorded.");
|
|
await window.__BUZZ_E2E_INVOKE_MOCK_COMMAND__?.("send_channel_message", {
|
|
channelId,
|
|
content: "A persisted threaded agent response.",
|
|
parentEventId: parentEventId ?? undefined,
|
|
});
|
|
},
|
|
{ channelId: projectAgentChannelId, parentEventId: projectAgentMessageId },
|
|
);
|
|
await expect(
|
|
agentChatPanel.getByText("A persisted threaded agent response."),
|
|
).toBeVisible();
|
|
await chatPanelTab.click();
|
|
// The rail collapses but retains the panel so conversation state survives
|
|
// toggling; assert the collapsed rail instead of a full unmount.
|
|
await expect(contextRail).toHaveCSS("width", "0px");
|
|
await expect(contextRail).toHaveAttribute("aria-hidden", "true");
|
|
await expect(chatPanelTab).toHaveAttribute("aria-label", "Show project chat");
|
|
await chatPanelTab.click();
|
|
await expect(
|
|
agentChatPanel.getByText("A persisted threaded agent response."),
|
|
).toBeVisible();
|
|
await expect(chatPanelTab).toHaveAttribute("aria-label", "Hide project chat");
|
|
await expect(repositoryActionsPanel).toHaveCount(0);
|
|
await page.getByTestId("project-right-panel-repository-tab").click();
|
|
await expect(repositoryActionsPanel).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("project-repository-selection-row"),
|
|
).toHaveCount(0);
|
|
await repositoryPanelTab.click();
|
|
await expect(contextRail).toHaveCSS("width", "0px");
|
|
await expect(repositoryPanelTab).toHaveAttribute("aria-pressed", "false");
|
|
await expect(repositoryPanelTab).toHaveAttribute(
|
|
"aria-label",
|
|
"Show project context",
|
|
);
|
|
const [attachedContentSurfaceBounds, collapsedMainPaneBounds] =
|
|
await Promise.all([
|
|
appContentSurface.boundingBox(),
|
|
workspacePanel.boundingBox(),
|
|
]);
|
|
expect(attachedContentSurfaceBounds).not.toBeNull();
|
|
await expect(appContentSurface).toHaveCSS("box-shadow", "none");
|
|
// The detached pod fills the surface minus its hairline top/left inset and
|
|
// the 8px bottom gutter (ml-px mt-px mb-2 on the pod wrapper).
|
|
expect(
|
|
(projectContentPodBounds?.x ?? 0) - (attachedContentSurfaceBounds?.x ?? 0),
|
|
).toBe(1);
|
|
expect(
|
|
(projectContentPodBounds?.y ?? 0) - (attachedContentSurfaceBounds?.y ?? 0),
|
|
).toBe(1);
|
|
expect(
|
|
(attachedContentSurfaceBounds?.height ?? 0) -
|
|
(projectContentPodBounds?.height ?? 0),
|
|
).toBe(9);
|
|
const viewportSize = page.viewportSize();
|
|
expect(collapsedMainPaneBounds).not.toBeNull();
|
|
expect(viewportSize).not.toBeNull();
|
|
expect(
|
|
Math.abs(
|
|
(collapsedMainPaneBounds?.x ?? 0) +
|
|
(collapsedMainPaneBounds?.width ?? 0) -
|
|
(viewportSize?.width ?? 0),
|
|
),
|
|
).toBeLessThanOrEqual(8);
|
|
await repositoryPanelTab.click();
|
|
await expect(contextRail).toHaveCSS("width", "288px");
|
|
await expect(repositoryPanelTab).toHaveAttribute("aria-pressed", "true");
|
|
await expect(repositoryActionsPanel).toBeVisible();
|
|
await expect(repositoryPanelTab).toHaveAttribute(
|
|
"aria-label",
|
|
"Hide project context",
|
|
);
|
|
await expect(page.getByTestId("project-detail-copy-link")).toHaveCount(0);
|
|
await expect(
|
|
workspacePanel.getByTestId("project-workspace-tab-menu"),
|
|
).toHaveCount(0);
|
|
const backBounds = await backButton.boundingBox();
|
|
const channelsBounds = await channelsTab.boundingBox();
|
|
const contributorsBounds = await contributorsTab.boundingBox();
|
|
const tabMenuBounds = await tabMenu.boundingBox();
|
|
const workspaceBounds = await workspacePanel.boundingBox();
|
|
const repositoryActionsBounds = await repositoryActionsPanel.boundingBox();
|
|
expect(backBounds).not.toBeNull();
|
|
expect(channelsBounds).not.toBeNull();
|
|
expect(contributorsBounds).not.toBeNull();
|
|
expect(tabMenuBounds).not.toBeNull();
|
|
expect(workspaceBounds).not.toBeNull();
|
|
expect(repositoryActionsBounds).not.toBeNull();
|
|
expect(channelsBounds?.x).toBeLessThan(contributorsBounds?.x ?? 0);
|
|
expect(backBounds?.x).toBeGreaterThan(workspaceBounds?.x ?? 0);
|
|
expect(tabMenuBounds?.y).toBeLessThan(workspaceBounds?.y ?? 0);
|
|
expect(repositoryActionsBounds?.x).toBeGreaterThan(workspaceBounds?.x ?? 0);
|
|
await expect(
|
|
workspacePanel.getByRole("heading", { name: "README", exact: true }),
|
|
).toHaveCount(0);
|
|
await expect(
|
|
workspacePanel.getByTestId("project-section-header-icon"),
|
|
).toHaveCount(1);
|
|
await waitForAnimations(page);
|
|
await page.screenshot({ path: `${SHOTS}/01-workspace-overview.png` });
|
|
|
|
await repositoryActionsPanel
|
|
.getByRole("button", { name: "Clone", exact: true })
|
|
.click();
|
|
const localSourceTrigger = repositoryActionsPanel.getByRole("button", {
|
|
name: /^Local /,
|
|
});
|
|
await expect(localSourceTrigger).toBeVisible();
|
|
await expect(
|
|
repositoryActionsPanel.getByTestId("project-repository-local-path"),
|
|
).toHaveText("…/buzz/REPOS/buzz");
|
|
await expect(
|
|
repositoryActionsPanel.getByRole("button", {
|
|
name: "Open",
|
|
exact: true,
|
|
}),
|
|
).toHaveAttribute("title", "Open local repository folder");
|
|
await localSourceTrigger.click();
|
|
await page
|
|
.getByRole("menuitemradio", { name: "Remote", exact: true })
|
|
.click();
|
|
await expect(
|
|
repositoryActionsPanel.getByRole("button", {
|
|
name: "Remote",
|
|
exact: true,
|
|
}),
|
|
).toBeVisible();
|
|
|
|
await filesTab.click();
|
|
await expect(repositoryActionsPanel).toBeVisible();
|
|
await expect(
|
|
workspacePanel.getByRole("heading", { name: "Files", exact: true }),
|
|
).toBeVisible();
|
|
await expect(
|
|
workspacePanel.getByTestId("project-repository-entry-icon").first(),
|
|
).toHaveCSS("border-radius", "8px");
|
|
const repositoryEntryCell = workspacePanel
|
|
.getByTestId("project-repository-entry-row")
|
|
.first()
|
|
.locator("td")
|
|
.first();
|
|
await expect(repositoryEntryCell).toHaveCSS("border-radius", "0px");
|
|
await expect(repositoryEntryCell).toHaveCSS("border-bottom-width", "0px");
|
|
await chatPanelTab.click();
|
|
await expect(agentContext).toContainText("Files");
|
|
await expect(
|
|
agentChatPanel.getByText("A persisted threaded agent response."),
|
|
).toBeVisible();
|
|
await repositoryPanelTab.click();
|
|
await expect(repositoryActionsPanel).toBeVisible();
|
|
await expectInsetSection();
|
|
await page.getByRole("tab", { name: "Commits" }).click();
|
|
await expect(repositoryActionsPanel).toBeVisible();
|
|
await expect(
|
|
workspacePanel.getByRole("heading", { name: "Commits", exact: true }),
|
|
).toBeVisible();
|
|
await expectInsetSection();
|
|
const commitRow = page.getByTestId("project-activity-feed-item").first();
|
|
const commitDate = commitRow.getByTestId("project-commit-row-date");
|
|
const commitHash = commitRow.getByTitle(/^View commit /);
|
|
await expect(commitDate).toBeVisible();
|
|
const commitDateBounds = await commitDate.boundingBox();
|
|
const commitHashBounds = await commitHash.boundingBox();
|
|
expect(commitDateBounds).not.toBeNull();
|
|
expect(commitHashBounds).not.toBeNull();
|
|
expect(commitDateBounds?.x).toBeGreaterThan(commitHashBounds?.x ?? 0);
|
|
await contributorsTab.click();
|
|
await expectInsetSection();
|
|
const contributorRow = page.getByTestId("project-contributor-row").first();
|
|
await expect(
|
|
contributorRow.getByTestId("project-contributor-commit-count"),
|
|
).toBeVisible();
|
|
await expect(
|
|
contributorRow.getByTestId("project-contributor-review-count"),
|
|
).toBeVisible();
|
|
await expect(
|
|
contributorRow.getByTestId("project-contributor-task-count"),
|
|
).toBeVisible();
|
|
const contributorBounds = await contributorRow.boundingBox();
|
|
const contributorIdentityBounds = await contributorRow
|
|
.getByTestId("project-contributor-identity")
|
|
.boundingBox();
|
|
const contributorCommitCountBounds = await contributorRow
|
|
.getByTestId("project-contributor-commit-count")
|
|
.boundingBox();
|
|
expect(contributorBounds).not.toBeNull();
|
|
expect(contributorBounds?.height).toBeLessThanOrEqual(40);
|
|
expect(contributorIdentityBounds).not.toBeNull();
|
|
expect(contributorCommitCountBounds?.x).toBeGreaterThan(
|
|
contributorIdentityBounds?.x ?? 0,
|
|
);
|
|
const contributorCommitCounts = await page
|
|
.getByTestId("project-contributor-commit-count")
|
|
.evaluateAll((nodes) =>
|
|
nodes.map((node) => {
|
|
const value = Number.parseInt(node.textContent?.trim() ?? "", 10);
|
|
return Number.isNaN(value) ? -1 : value;
|
|
}),
|
|
);
|
|
expect(contributorCommitCounts).toEqual(
|
|
[...contributorCommitCounts].sort((left, right) => right - left),
|
|
);
|
|
|
|
await channelsTab.click();
|
|
await expect(
|
|
workspacePanel.getByRole("heading", { name: "Channels", exact: true }),
|
|
).toBeVisible();
|
|
await expect(
|
|
repositoryActionsPanel.getByTestId("project-right-panel-scope"),
|
|
).toHaveCount(0);
|
|
await expect(
|
|
repositoryActionsPanel.getByText("Working copy", { exact: true }),
|
|
).toHaveCount(0);
|
|
await expectProjectContextGroups(repositoryActionsPanel, {
|
|
hasActions: false,
|
|
});
|
|
for (const action of ["Clone", "Fetch", "Terminal"]) {
|
|
await expect(
|
|
repositoryActionsPanel.getByRole("button", {
|
|
name: action,
|
|
exact: true,
|
|
}),
|
|
).toHaveCount(0);
|
|
}
|
|
await expectInsetSection();
|
|
|
|
// Issues tab: the create action lives in the section header.
|
|
await page.getByRole("tab", { name: "Tasks", exact: true }).click();
|
|
await expect(repositoryActionsPanel).toBeVisible();
|
|
await expectProjectContextGroups(repositoryActionsPanel, {
|
|
hasActions: true,
|
|
});
|
|
await expect(
|
|
repositoryActionsPanel.getByRole("heading", {
|
|
name: "Task activity",
|
|
exact: true,
|
|
}),
|
|
).toHaveCount(0);
|
|
const taskActionsSection = repositoryActionsPanel.getByTestId(
|
|
"project-context-actions",
|
|
);
|
|
const taskDetailsSection = repositoryActionsPanel.getByTestId(
|
|
"project-context-details",
|
|
);
|
|
const [taskActionsBounds, taskDetailsBounds] = await Promise.all([
|
|
taskActionsSection.boundingBox(),
|
|
taskDetailsSection.boundingBox(),
|
|
]);
|
|
expect(taskDetailsBounds?.y ?? 0).toBeGreaterThan(
|
|
(taskActionsBounds?.y ?? 0) + (taskActionsBounds?.height ?? 0),
|
|
);
|
|
await expectInsetSection();
|
|
const newIssueButton = workspacePanel.getByRole("button", {
|
|
name: "Create task",
|
|
});
|
|
await expect(newIssueButton).toBeVisible();
|
|
const contextCreateTaskButton = repositoryActionsPanel.getByRole("button", {
|
|
name: "Create task",
|
|
exact: true,
|
|
});
|
|
await expect(contextCreateTaskButton).toBeVisible();
|
|
await contextCreateTaskButton.click();
|
|
await expect(page.getByTestId("create-issue-dialog")).toBeVisible();
|
|
await page.keyboard.press("Escape");
|
|
await expect(
|
|
tabMenu.getByRole("button", { name: "Create task" }),
|
|
).toHaveCount(0);
|
|
const issuesHeading = workspacePanel.getByRole("heading", {
|
|
name: "Tasks",
|
|
exact: true,
|
|
});
|
|
const issuesHeadingBounds = await issuesHeading.boundingBox();
|
|
const newIssueBounds = await newIssueButton.boundingBox();
|
|
expect(issuesHeadingBounds).not.toBeNull();
|
|
expect(newIssueBounds).not.toBeNull();
|
|
expect(newIssueBounds?.x).toBeGreaterThan(issuesHeadingBounds?.x ?? 0);
|
|
const issueRow = page.getByTestId("project-issue-row").first();
|
|
await expect(issueRow).toBeVisible({ timeout: 10_000 });
|
|
await expect(issueRow).toHaveCSS("border-radius", "8px");
|
|
const [issueHeaderBounds, firstIssueBounds, secondIssueBounds] =
|
|
await Promise.all([
|
|
page.getByTestId("project-work-item-group-header").first().boundingBox(),
|
|
page.getByTestId("project-issue-row").nth(0).boundingBox(),
|
|
page.getByTestId("project-issue-row").nth(1).boundingBox(),
|
|
]);
|
|
expect(
|
|
(firstIssueBounds?.y ?? 0) -
|
|
((issueHeaderBounds?.y ?? 0) + (issueHeaderBounds?.height ?? 0)),
|
|
).toBe(4);
|
|
expect(
|
|
Math.round((firstIssueBounds?.x ?? 0) - (issueHeaderBounds?.x ?? 0)),
|
|
).toBe(8);
|
|
expect(
|
|
Math.round(
|
|
(issueHeaderBounds?.width ?? 0) - (firstIssueBounds?.width ?? 0),
|
|
),
|
|
).toBe(16);
|
|
expect(
|
|
(secondIssueBounds?.y ?? 0) -
|
|
((firstIssueBounds?.y ?? 0) + (firstIssueBounds?.height ?? 0)),
|
|
).toBe(2);
|
|
const issueDate = issueRow.getByTestId("project-issue-row-date");
|
|
const issueId = issueRow.getByTitle("View task");
|
|
await expect(issueDate).toBeVisible();
|
|
const issueDateBounds = await issueDate.boundingBox();
|
|
const issueIdBounds = await issueId.boundingBox();
|
|
const issueRowBounds = await issueRow.boundingBox();
|
|
expect(issueDateBounds).not.toBeNull();
|
|
expect(issueIdBounds).not.toBeNull();
|
|
expect(issueRowBounds).not.toBeNull();
|
|
expect(issueRowBounds?.height).toBeLessThanOrEqual(40);
|
|
expect(issueDateBounds?.x).toBeGreaterThan(issueIdBounds?.x ?? 0);
|
|
await waitForAnimations(page);
|
|
await page.screenshot({ path: `${SHOTS}/02-issues-tab.png` });
|
|
|
|
// Issue detail: inline copy link after the title, assignees rail, and the
|
|
// avatar comment timeline (seed comments so the timeline renders).
|
|
await issueRow.getByRole("button", { name: /^#/ }).click();
|
|
const composer = page.getByTestId("project-issue-comment-composer");
|
|
await expect(composer).toBeVisible();
|
|
await expect(tabMenu).toHaveCount(0);
|
|
for (const comment of [
|
|
"The palette needs the most work — starting there.",
|
|
"Agreed. Typography pass can land separately.",
|
|
]) {
|
|
await composer.locator('[contenteditable="true"]').fill(comment);
|
|
await composer.getByRole("button", { name: "Send message" }).click();
|
|
await expect(page.getByText(comment, { exact: true })).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
}
|
|
await expect(page.getByTestId("project-issue-copy-link")).toBeVisible();
|
|
await expectProjectContextGroups(repositoryActionsPanel, {
|
|
hasActions: true,
|
|
});
|
|
const issueDetail = page.getByTestId("project-issue-detail");
|
|
await expect(issueDetail).toHaveCSS("max-width", "768px");
|
|
await expect(
|
|
issueDetail.getByRole("heading", { level: 3 }).first(),
|
|
).toHaveCSS("font-size", "18px");
|
|
await expect(
|
|
page.getByTestId("project-issue-comment-timeline-row").first(),
|
|
).toBeVisible();
|
|
const issueActivity = workspacePanel.getByRole("button", {
|
|
name: "Activity",
|
|
exact: true,
|
|
});
|
|
await issueActivity.click();
|
|
await expect(composer).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("project-issue-comment-timeline-row"),
|
|
).toHaveCount(0);
|
|
await issueActivity.click();
|
|
// Let the "Comment posted." toast dismiss so it doesn't photobomb.
|
|
await expect(page.getByText("Comment posted.")).toHaveCount(0, {
|
|
timeout: 10_000,
|
|
});
|
|
await waitForAnimations(page);
|
|
await page.screenshot({ path: `${SHOTS}/03-issue-detail.png` });
|
|
|
|
// PR list: the create action lives in both the section header and context.
|
|
await page
|
|
.getByRole("navigation", { name: "Project breadcrumb" })
|
|
.getByRole("button", { name: "Tasks", exact: true })
|
|
.click();
|
|
await expect(tabMenu).toBeVisible();
|
|
await page.getByRole("tab", { name: "Review", exact: true }).click();
|
|
await expect(
|
|
repositoryActionsPanel.getByTestId("project-right-panel-scope"),
|
|
).toHaveCount(0);
|
|
await expectProjectContextGroups(repositoryActionsPanel, {
|
|
hasActions: true,
|
|
});
|
|
await expect(
|
|
workspacePanel.getByRole("heading", {
|
|
name: "Reviews",
|
|
exact: true,
|
|
}),
|
|
).toBeVisible();
|
|
await expect(
|
|
workspacePanel.getByRole("button", { name: "Create review" }),
|
|
).toBeVisible();
|
|
const contextCreateReviewButton = repositoryActionsPanel.getByRole("button", {
|
|
name: "Create review",
|
|
exact: true,
|
|
});
|
|
await expect(contextCreateReviewButton).toBeVisible();
|
|
await contextCreateReviewButton.click();
|
|
await expect(page.getByTestId("create-pull-request-dialog")).toBeVisible();
|
|
await page.keyboard.press("Escape");
|
|
await expectInsetSection();
|
|
await expect(
|
|
tabMenu.getByRole("button", { name: "Create review" }),
|
|
).toHaveCount(0);
|
|
const prRow = page.getByTestId("project-pull-request-row").first();
|
|
await expect(prRow).toBeVisible({ timeout: 10_000 });
|
|
const [reviewHeaderBounds, firstReviewBounds, secondReviewBounds] =
|
|
await Promise.all([
|
|
page.getByTestId("project-work-item-group-header").first().boundingBox(),
|
|
page.getByTestId("project-pull-request-row").nth(0).boundingBox(),
|
|
page.getByTestId("project-pull-request-row").nth(1).boundingBox(),
|
|
]);
|
|
expect(
|
|
(firstReviewBounds?.y ?? 0) -
|
|
((reviewHeaderBounds?.y ?? 0) + (reviewHeaderBounds?.height ?? 0)),
|
|
).toBe(4);
|
|
expect(
|
|
Math.round((firstReviewBounds?.x ?? 0) - (reviewHeaderBounds?.x ?? 0)),
|
|
).toBe(8);
|
|
expect(
|
|
Math.round(
|
|
(reviewHeaderBounds?.width ?? 0) - (firstReviewBounds?.width ?? 0),
|
|
),
|
|
).toBe(16);
|
|
expect(
|
|
(secondReviewBounds?.y ?? 0) -
|
|
((firstReviewBounds?.y ?? 0) + (firstReviewBounds?.height ?? 0)),
|
|
).toBe(2);
|
|
const prDate = prRow.getByTestId("project-pull-request-row-date");
|
|
const prId = prRow.getByTitle("View review");
|
|
await expect(prDate).toBeVisible();
|
|
const prDateBounds = await prDate.boundingBox();
|
|
const prIdBounds = await prId.boundingBox();
|
|
expect(prDateBounds).not.toBeNull();
|
|
expect(prIdBounds).not.toBeNull();
|
|
expect(prDateBounds?.x).toBeGreaterThan(prIdBounds?.x ?? 0);
|
|
await prRow.getByRole("button", { name: /^#/ }).click();
|
|
await expect(
|
|
page.getByTestId("project-pull-request-copy-link"),
|
|
).toBeVisible();
|
|
await expectProjectContextGroups(repositoryActionsPanel, {
|
|
hasActions: true,
|
|
});
|
|
const pullRequestDetail = page.getByTestId("project-pull-request-detail");
|
|
await expect(pullRequestDetail).toHaveCSS("max-width", "768px");
|
|
await expect(
|
|
pullRequestDetail.getByRole("heading", { level: 3 }).first(),
|
|
).toHaveCSS("font-size", "18px");
|
|
const reviewCommits = workspacePanel.getByRole("button", {
|
|
name: "Commits",
|
|
exact: true,
|
|
});
|
|
await expect(reviewCommits).toHaveAttribute("aria-expanded", "false");
|
|
await expect(reviewCommits).toHaveCSS("font-size", "14px");
|
|
await expect(reviewCommits).toHaveCSS("font-weight", "500");
|
|
await reviewCommits.click();
|
|
const openedReviewCommits = workspacePanel.getByRole("button", {
|
|
name: /^Commits \d+$/,
|
|
});
|
|
await expect(openedReviewCommits).toHaveAttribute("aria-expanded", "true");
|
|
await openedReviewCommits.click();
|
|
const reviewComposer = page.getByTestId(
|
|
"project-pull-request-comment-composer",
|
|
);
|
|
await expect(reviewComposer).toBeVisible();
|
|
const reviewActivity = workspacePanel.getByRole("button", {
|
|
name: "Activity",
|
|
exact: true,
|
|
});
|
|
await reviewActivity.click();
|
|
await expect(reviewComposer).toBeVisible();
|
|
await expect(tabMenu).toHaveCount(0);
|
|
await waitForAnimations(page);
|
|
await page.screenshot({ path: `${SHOTS}/04-pr-detail.png` });
|
|
});
|
|
|
|
test("projects v3 work-item list metadata", async ({ page }) => {
|
|
await page.addInitScript(() => {
|
|
window.localStorage.setItem("buzz.projects.viewMode", "list");
|
|
});
|
|
await installMockBridge(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByTestId("open-projects-view").click();
|
|
|
|
await page.getByTestId("projects-section-all").click();
|
|
await expectSinglePrimaryTextColumn(
|
|
page.getByTestId("projects-activity-card").first(),
|
|
);
|
|
|
|
await page.getByTestId("projects-section-projects").click();
|
|
await expect(page.getByTestId("projects-list-header")).toHaveCSS(
|
|
"border-left-width",
|
|
"0px",
|
|
);
|
|
const projectRow = page.getByTestId(/^project-row-/).first();
|
|
await expectSinglePrimaryTextColumn(projectRow);
|
|
const [projectTitleBox, repositoryCountBox] = await Promise.all([
|
|
projectRow.locator('[data-projects-text-priority="primary"]').boundingBox(),
|
|
projectRow.getByTestId("projects-row-context").boundingBox(),
|
|
]);
|
|
expect(projectTitleBox).not.toBeNull();
|
|
expect(repositoryCountBox).not.toBeNull();
|
|
expect(
|
|
Math.round(
|
|
(repositoryCountBox?.x ?? 0) -
|
|
((projectTitleBox?.x ?? 0) + (projectTitleBox?.width ?? 0)),
|
|
),
|
|
).toBe(12);
|
|
|
|
await page.getByTestId("projects-section-repositories").click();
|
|
await expectSinglePrimaryTextColumn(
|
|
page.getByTestId(/^repository-row-/).first(),
|
|
);
|
|
|
|
await page.getByTestId("projects-section-prs").click();
|
|
const reviewList = page.getByTestId("projects-list-container");
|
|
await expect(reviewList).toBeVisible();
|
|
const pullRequestRow = page.getByTestId(/^projects-pr-row-/).first();
|
|
await expect(pullRequestRow).toBeVisible();
|
|
await expectSinglePrimaryTextColumn(pullRequestRow);
|
|
await expect(pullRequestRow).toContainText(/relay-tools|buzz|design-system/);
|
|
await waitForAnimations(page);
|
|
await page.screenshot({ path: `${SHOTS}/05-pr-list-metadata.png` });
|
|
|
|
await page.getByTestId("projects-section-issues").click();
|
|
const taskList = page.getByTestId("projects-list-container");
|
|
await expect(taskList).toBeVisible();
|
|
const issueRow = page.getByTestId(/^projects-issue-row-/).first();
|
|
await expect(issueRow).toBeVisible();
|
|
await expectSinglePrimaryTextColumn(issueRow);
|
|
await expect(issueRow).toContainText(/relay-tools|buzz|design-system/);
|
|
await waitForAnimations(page);
|
|
await page.screenshot({ path: `${SHOTS}/06-issue-list-metadata.png` });
|
|
|
|
await page.getByTestId("projects-section-channels").click();
|
|
const channelRow = page.getByTestId("project-channel-row").first();
|
|
await expect(channelRow).toBeVisible();
|
|
await expectSinglePrimaryTextColumn(channelRow);
|
|
await expect(channelRow).toContainText("#buzz");
|
|
await expect(
|
|
page.getByTestId("project-channel-project").first(),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("project-channel-repository").first(),
|
|
).toBeVisible();
|
|
await waitForAnimations(page);
|
|
await page.screenshot({ path: `${SHOTS}/07-channels-list.png` });
|
|
});
|