Files
buzz/desktop/tests/e2e/project-issue-comments.spec.ts
thomaspblock 9b32e055fe polish(desktop): finish Projects navigation and context chrome (#6429)
## Summary

After #6396, Projects still split chrome across the workspace header, a
copy-link control, and a labeled Actions group that mixed people,
create, and metadata. This PR finishes that surface: the right-hand
context box is unlabeled actions plus a Details group, people stacks and
contribution heatmaps are gone from that box, Create review sits with
Create task, and the top chrome is terminal / chat / info with no
copy-link. Sent project context collapses to a pill, and review file
diffs keep the last good git view instead of flashing empty while
queries refetch.

This also lands the remaining navigation polish that followed Part 3:
overview and list presentation, readme and commit layout, and opening
the latest matching conversation from the Channels tab without leaving
the project.

### Related issue
N/A. Related: #6396

## Testing
- Walked Files, Tasks, Reviews, task/review detail, overview tabs, and
chrome chat vs info in the running desktop app
- Pre-push: desktop typecheck, unit tests, Tauri checks, and file-size
gate passed
- Updated Projects smoke specs for the new context groups, Create
review, chrome order, and removed copy-link control
- Merged current `origin/main`; one conflict in discussion-channel rows
kept conversation-panel navigation and took main's bounded channel-name
lookup

## Post-Deploy Monitoring & Validation
- validate Projects workspace chrome, context box, and review file diffs
in the first staging Desktop session
- healthy signals: context box shows unlabeled actions then Details,
chat toggle sits between terminal and info, review diffs stay populated
across selection changes
- failure signals: missing Create review, restored heatmap/people in the
context box, or empty Files Changed while the review is still selected;
mitigate by reverting this PR

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
2026-08-21 05:45:55 -07:00

250 lines
9.1 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
const ISSUE_COMMENTS = [
"First issue comment",
"Second issue comment",
"Third issue comment",
"Fourth issue comment",
];
const DEFAULT_MOCK_PUBKEY = "deadbeef".repeat(8);
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();
}
test("issue detail can open agent chat or seed a channel question", async ({
page,
}) => {
await installMockBridge(page);
await openBuzzProject(page);
await page.getByRole("tab", { name: "Tasks", exact: true }).click();
const issueRow = page.getByTestId("project-issue-row").first();
await expect(issueRow).toBeVisible({ timeout: 10_000 });
await issueRow.getByRole("button", { name: /^#/ }).click();
const communication = page.getByTestId(
"project-context-communication-actions",
);
await expect(communication).toBeVisible();
const contextPanel = page.getByTestId("project-repository-actions-panel");
await expect(
contextPanel.getByRole("heading", { name: "Actions", exact: true }),
).toHaveCount(0);
await expect(
contextPanel.getByRole("heading", { name: "Details", exact: true }),
).toBeVisible();
await expect(
contextPanel.getByRole("heading", { name: "Assignment", exact: true }),
).toHaveCount(0);
await expect(
contextPanel.getByRole("heading", { name: "Discussion", exact: true }),
).toHaveCount(0);
await expect(
contextPanel.getByTestId("project-repository-people"),
).toHaveCount(0);
await page.getByTestId("project-context-chat-agent").click();
await expect(page.getByTestId("project-agent-chat-panel")).toBeVisible();
await expect(page.getByTestId("projects-agent-selection-item")).toHaveCount(
1,
);
await page.getByRole("button", { name: "Close agent chat" }).click();
await expect(
page.getByTestId("project-right-panel-repository-tab"),
).toHaveAttribute("aria-pressed", "false");
await page.getByTestId("project-right-panel-repository-tab").click();
await page.getByTestId("project-context-discuss").click();
await expect(
page.getByTestId("project-context-channel-choices"),
).toBeVisible();
await page.getByTestId("project-context-related-channel").first().click();
await expect(page.getByTestId("message-input")).toContainText(
"Let's talk about this task:",
);
});
test("issue discussion ignores an author-claimed origin channel", async ({
page,
}) => {
const forgedIssueId = "f".repeat(64);
await page.addInitScript(
({ issueId, owner }) => {
window.__BUZZ_E2E_EXTRA_PROJECT_EVENTS__ = [
{
id: issueId,
kind: 1621,
pubkey: owner,
created_at: Math.floor(Date.now() / 1000) + 10,
content: "This task claims an unrelated visible channel.",
tags: [
["a", `30617:${owner}:buzz`],
["subject", "Forged origin task"],
["h", "9dae0116-799b-5071-a0a8-fdd30a91a35d"],
],
},
];
},
{ issueId: forgedIssueId, owner: DEFAULT_MOCK_PUBKEY },
);
await installMockBridge(page);
await openBuzzProject(page);
await page.getByRole("tab", { name: "Tasks", exact: true }).click();
const issueRow = page
.getByTestId("project-issue-row")
.filter({ hasText: "Forged origin task" });
await expect(issueRow).toBeVisible();
await issueRow.getByRole("button", { name: /^#/ }).click();
await page.getByTestId("project-context-discuss").click();
const channelChoices = page.getByTestId("project-context-channel-choices");
const relatedChannel = channelChoices.getByTestId(
"project-context-related-channel",
);
await expect(relatedChannel).toHaveCount(1);
await expect(relatedChannel).toContainText("#general");
await expect(channelChoices).not.toContainText("#random");
await relatedChannel.click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const issueDraftChip = page
.getByTestId("message-input")
.locator('[data-composer-buzz-link=""]', {
hasText: "buzz",
});
await expect(issueDraftChip).toHaveAttribute(
"data-href",
new RegExp(`id=${forgedIssueId}`),
);
await page.getByTestId("channel-random").click();
await expect(
page.getByTestId("message-input").locator('[data-composer-buzz-link=""]'),
).toHaveCount(0);
});
test("issue comments use the project activity timeline", async ({ page }) => {
await installMockBridge(page);
await openBuzzProject(page);
await page.getByRole("tab", { name: "Tasks", exact: true }).click();
const issueRow = page.getByTestId("project-issue-row").first();
await expect(issueRow).toBeVisible({ timeout: 10_000 });
await issueRow.getByRole("button", { name: /^#/ }).click();
const composer = page.getByTestId("project-issue-comment-composer");
await expect(composer).toBeVisible();
for (const comment of ISSUE_COMMENTS) {
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,
});
}
const timelineRows = page.getByTestId("project-issue-comment-timeline-row");
const earlierComments = page.getByTestId("project-issue-earlier-comments");
const historyToggle = page.getByTestId(
"project-issue-comment-history-toggle",
);
await expect(timelineRows).toHaveCount(3);
await expect(earlierComments).toContainText("Show 1 earlier comment");
await expect(
timelineRows.filter({ hasText: "First issue comment" }),
).toHaveCount(0);
await expect(
timelineRows.filter({ hasText: "Fourth issue comment" }),
).toHaveCount(1);
await earlierComments.click();
await expect(timelineRows).toHaveCount(4);
for (const comment of ISSUE_COMMENTS) {
await expect(timelineRows.filter({ hasText: comment })).toHaveCount(1);
}
await historyToggle.click();
await expect(timelineRows).toHaveCount(0);
await expect(historyToggle).toContainText("Show 4 earlier comments");
await historyToggle.click();
await expect(timelineRows).toHaveCount(4);
});
test("issue assignees can be assigned and unassigned", async ({ page }) => {
await installMockBridge(page);
await openBuzzProject(page);
await page.getByRole("tab", { name: "Tasks", exact: true }).click();
const issueRow = page.getByTestId("project-issue-row").first();
await expect(issueRow).toBeVisible({ timeout: 10_000 });
await issueRow.getByRole("button", { name: /^#/ }).click();
const issueHeader = page
.getByTestId("project-issue-detail")
.locator("header")
.first();
await expect(issueHeader).toContainText("Task created");
await expect(issueHeader).not.toContainText("alice");
await expect(issueHeader.locator("img")).toHaveCount(0);
const contextAssignment = page.getByTestId("project-context-task-assignment");
await expect(contextAssignment).toBeVisible();
await expect(contextAssignment.locator("img")).toHaveCount(0);
const selfAssign = page.getByTestId("project-context-issue-self-assign");
await expect(selfAssign).toBeVisible();
await expect(page.getByTestId("project-context-issue-assign")).toBeVisible();
const createTask = page
.getByTestId("project-repository-actions-panel")
.getByRole("button", { name: "Create task", exact: true });
await expect(createTask).toBeVisible();
await expect(selfAssign.locator("svg")).toHaveCount(1);
const actionGeometry = await Promise.all(
[selfAssign, createTask].map((action) =>
action.evaluate((element) => {
const bounds = element.getBoundingClientRect();
return {
height: bounds.height,
left: bounds.left,
width: bounds.width,
};
}),
),
);
expect(actionGeometry[0]).toEqual(actionGeometry[1]);
await selfAssign.click();
await expect(contextAssignment).toContainText("Assigned to me");
await expect(page.getByTestId("project-detail-section").first()).toHaveCSS(
"border-top-width",
"0px",
);
await page.getByTestId("project-issue-assign").click();
const candidate = page
.locator('[data-testid^="project-assignee-result-"]')
.first();
await expect(candidate).toBeVisible();
const candidateTestId = await candidate.getAttribute("data-testid");
const assignee = candidateTestId?.replace("project-assignee-result-", "");
if (!assignee) throw new Error("Assignee result is missing its pubkey.");
expect(assignee).toMatch(/^[0-9a-f]{64}$/);
await candidate.click();
const unassign = page.getByTestId(`project-issue-unassign-${assignee}`);
await expect(unassign).toBeVisible({ timeout: 10_000 });
await unassign.click();
await expect(page.getByText("Task unassigned.")).toBeVisible();
await expect(unassign).toHaveCount(0, { timeout: 10_000 });
});