Files
buzz/desktop/tests/e2e/project-issue-comments.spec.ts
thomaspblock 3c228b1082 feat(desktop): refine context-aware Projects collaboration (#6396)
## Summary
- give inline project agents bounded visible-page and selection context
while reusing the shared message-thread presentation
- add contextual collaboration actions for discussing project entities
in related channels
- align project list metadata, context rails, and work-item
communication actions with the active workspace

This is Part 3 of the Projects v6 stack, following #6368. Part 4
contains the remaining navigation and detail-page polish.

## Testing
- Desktop unit suite: 5,125/5,125 passed
- Projects smoke specs: 62/62 passed
- TypeScript, Biome, typography, pubkey, and differential file-size
checks passed
- full pre-push gate passed

## Post-Deploy Monitoring & Validation
- validate Projects overview/detail agent chat and discuss-in-channel
journeys in the first staging Desktop session
- healthy signals: context matches the active project/repository/work
item, messages remain in the chosen channel, and restored conversations
exclude unrelated DM history
- failure signals: stale or cross-project context, duplicate/missing
thread rows, or collaboration actions targeting the wrong channel;
mitigate by reverting this PR

Related: #6335

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
2026-08-20 12:59:09 -04:00

224 lines
8.3 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();
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");
await expect(page.getByTestId("message-input")).toContainText("ffffffff");
await page.getByTestId("channel-random").click();
await expect(page.getByTestId("message-input")).not.toContainText("ffffffff");
});
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 });
});