feat(desktop): refine repository-aware project workspaces (#6003)

## Summary
- Makes Projects repository-aware across browsing, branch and tag
selection, local and remote source management, work items, commits, and
contextual repository actions.
- Refines project details into consistent single-column reading surfaces
with a resizable, section-aware context pod, while keeping project
conversations available in the attached chat panel.
- Adds persistent sidebar project navigation, direct entity links,
repository discussion channels, contributor identity matching, and
consistent loading and activity presentation.
- Keeps branch-specific controls on code-oriented sections while Tasks,
Reviews, and Channels remain repository-scoped, reducing misleading
context and actions.

Replacement for #5981 with an identical final tree flattened into one
signed-off commit because the required DCO check suite remained stalled.

### Related issue
N/A

### Testing
- [x] Desktop pre-push checks, TypeScript typecheck, and unit tests
- [x] E2E production build with `pnpm build:e2e`
- [x] Focused Playwright smoke coverage for project workspace, task,
review, commit, sidebar, and contextual-panel behavior
- [ ] Attach final before/after screenshots after design review

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Wintermute <165f0c871dd2586bb18b6aa109eeaf57bb2132ff4d27b10120f4368a0f627022@buzz.block.builderlab.xyz>
Co-authored-by: Wintermute <165f0c871dd2586bb18b6aa109eeaf57bb2132ff4d27b10120f4368a0f627022@buzz.block.builderlab.xyz>
This commit is contained in:
thomaspblock
2026-08-19 07:50:00 -04:00
committed by GitHub
parent 93114c9c65
commit 87f8ff82ae
188 changed files with 13049 additions and 3931 deletions

View File

@@ -1869,7 +1869,11 @@ test("channel with messages shows content", async ({ page }) => {
await expect(page.getByTestId("welcome-composer-guide-banner")).toHaveCount(
0,
);
await expect(page.getByTestId("message-timeline-day-divider")).toBeVisible();
// `.first()`: backdated seeds can straddle midnight UTC and render two
// dividers (Yesterday + Today); a bare locator fails Playwright strict mode.
await expect(
page.getByTestId("message-timeline-day-divider").first(),
).toBeVisible();
await expect(page.getByTestId("message-timeline")).toContainText(
"Welcome to general",
);
@@ -2441,7 +2445,11 @@ test("sidebar clears unread indicator after opening a DM", async ({ page }) => {
await expect(page.getByTestId("message-dm-intro")).toContainText(
"This is the beginning of your direct message with",
);
await expect(page.getByTestId("message-timeline-day-divider")).toBeVisible();
// `.first()`: backdated seeds can straddle midnight UTC and render two
// dividers (Yesterday + Today); a bare locator fails Playwright strict mode.
await expect(
page.getByTestId("message-timeline-day-divider").first(),
).toBeVisible();
await expect(page.getByTestId("message-timeline")).toContainText(
"Unread update for the DM",
);

View File

@@ -236,7 +236,7 @@ test("reopening the same entity link reapplies its workspace state", async ({
await emitEntityLink(repoLink);
const pullRequestsTab = page.getByRole("tab", {
name: "Pull Request",
name: "Review",
exact: true,
});
await expect(pullRequestsTab).toHaveAttribute("aria-selected", "true");
@@ -263,9 +263,7 @@ test("reopening the same entity link reapplies its workspace state", async ({
await emitEntityLink(prLink);
const prHeading = page.getByRole("heading", { name: PR_SUBJECT });
await expect(prHeading).toBeVisible();
await breadcrumb
.getByRole("button", { name: "Pull Request", exact: true })
.click();
await breadcrumb.getByRole("button", { name: "Review", exact: true }).click();
await expect(prHeading).toHaveCount(0);
await emitEntityLink(prLink);
await expect(prHeading).toBeVisible();
@@ -273,7 +271,7 @@ test("reopening the same entity link reapplies its workspace state", async ({
await emitEntityLink(issueLink);
const issueHeading = page.getByRole("heading", { name: ISSUE_SUBJECT });
await expect(issueHeading).toBeVisible();
await breadcrumb.getByRole("button", { name: "Issues", exact: true }).click();
await breadcrumb.getByRole("button", { name: "Tasks", exact: true }).click();
await expect(issueHeading).toHaveCount(0);
await emitEntityLink(issueLink);
await expect(issueHeading).toBeVisible();
@@ -290,7 +288,7 @@ test("cold-start entity links drain after the React listener mounts", async ({
await page.goto("/", { waitUntil: "domcontentloaded" });
await expect(
page.getByRole("tab", { name: "Pull Request", exact: true }),
page.getByRole("tab", { name: "Review", exact: true }),
).toHaveAttribute("aria-selected", "true");
await expect
.poll(() =>

View File

@@ -1996,7 +1996,13 @@ test("day divider appears in timeline", async ({ page }) => {
await expect(page.getByTestId("message-timeline")).toContainText(
"Welcome to general",
);
await expect(page.getByTestId("message-timeline-day-divider")).toBeVisible();
// `.first()`: the seeds are backdated by up to 120s, so a run that straddles
// midnight UTC legitimately renders two dividers (Yesterday + Today) and a
// bare locator fails Playwright strict mode. The intent is "a divider
// appears", which the first divider proves on both sides of midnight.
await expect(
page.getByTestId("message-timeline-day-divider").first(),
).toBeVisible();
});
test("send message to DM channel p-tags the recipient", async ({ page }) => {

View File

@@ -2,7 +2,11 @@ import { hexToBytes } from "@noble/hashes/utils.js";
import { expect, test, type Page } from "@playwright/test";
import { nsecEncode } from "nostr-tools/nip19";
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
import {
installMockBridge,
openChannelBrowser,
TEST_IDENTITIES,
} from "../helpers/bridge";
import { expectEmojiMartStylesInstalled } from "../helpers/css";
import { installFakeCamera } from "../helpers/fakeCamera";
import {
@@ -3628,6 +3632,192 @@ test("membership denial can import a different invited key", async ({
await expectHomeView(page);
});
// Guards the signer-aware community boundary: replacing identity A with
// identity B on the SAME relay (in-app key import) must rebuild the community
// query client and re-bucket the draft store, so B never renders A's cached
// data, never enumerates A's drafts, and never persists drafts into A's
// bucket. Mutation-proof: removing the signer (pubkey/epoch) from
// `communityKey` in App.tsx retains A's query client and draft bucket and
// fails every leg below.
test("same-relay identity replacement rebuilds the community boundary (A→B→A cache and draft isolation)", async ({
page,
}) => {
// Two full boots (denied-A → imported-B, then relaunch-as-A) plus draft
// flush polling exceed the default budget.
test.slow();
const GENERAL_CHANNEL_ID = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
const DRAFT_STORE_KEY_PREFIX = "buzz-drafts.v2";
// Mirrors the bridge's DEFAULT_RELAY_WS_URL and useDrafts'
// canonicalizeRelayScope (scheme://host, no trailing slash).
const relayScope = (process.env.BUZZ_E2E_RELAY_URL ?? "http://localhost:3000")
.replace(/^http/, "ws")
.replace(/\/+$/, "");
const tylerDraftStoreKey = `${DRAFT_STORE_KEY_PREFIX}:${relayScope}:${TEST_IDENTITIES.tyler.pubkey}`;
const aliceDraftStoreKey = `${DRAFT_STORE_KEY_PREFIX}:${relayScope}:${TEST_IDENTITIES.alice.pubkey}`;
await seedActiveIdentity(page, BLANK_TYLER_IDENTITY);
await installMockBridge(
page,
{
relayRequiresMembership: true,
relayRole: null,
},
{ skipOnboardingSeed: true },
);
await page.goto("/");
// Seed a draft in tyler's identity-scoped bucket after boot, as if typed in
// an earlier session. Written directly to localStorage (not through the
// live store) so it exercises the persisted bucket, not the memory cache.
await page.evaluate(
([draftStoreKey, channelId]) => {
const now = new Date().toISOString();
window.localStorage.setItem(
draftStoreKey,
JSON.stringify({
[channelId]: {
content: "tyler private draft",
selectionStart: 0,
selectionEnd: 0,
channelId,
createdAt: now,
updatedAt: now,
pendingImeta: [],
spoileredAttachmentUrls: [],
status: "active",
},
}),
);
},
[tylerDraftStoreKey, GENERAL_CHANNEL_ID] as const,
);
await page.getByTestId("onboarding-display-name").fill("Morty QA");
await page.getByTestId("onboarding-next").click();
await expect(page.getByTestId("membership-denied")).toBeVisible();
// Seed identity-unkeyed data into tyler's live community query client and
// capture the client instance so its replacement is provable.
await page.evaluate(() => {
const testWindow = window as Window & {
__BUZZ_E2E_QUERY_CLIENT__?: {
setQueryData: (key: unknown[], data: unknown) => void;
};
__BUZZ_E2E_PREVIOUS_QUERY_CLIENT__?: unknown;
};
const client = testWindow.__BUZZ_E2E_QUERY_CLIENT__;
if (!client) {
throw new Error("community query client seam is not installed");
}
// A key no production query uses: real keys (projects/channels) would be
// refetched by B's session and mask whether the CACHE was dropped.
client.setQueryData(
["e2e-identity-boundary-probe"],
[{ id: "tyler-cached-project" }],
);
testWindow.__BUZZ_E2E_PREVIOUS_QUERY_CLIENT__ = client;
});
// Import alice (B) through the in-app denied-membership key swap.
await page.getByTestId("membership-denied-change-key").click();
const importedNsec = nsecEncode(hexToBytes(TEST_IDENTITIES.alice.privateKey));
await page.getByTestId("membership-denied-nsec-input").fill(importedNsec);
await expect(
page.getByTestId("membership-denied-npub-preview"),
).toBeVisible();
await page.getByTestId("membership-denied-import-key").click();
await expect(page.getByTestId("onboarding-gate")).toHaveCount(0);
await expectHomeView(page);
// Leg 1 — the community boundary was rebuilt for B: a fresh query client
// replaced tyler's, and the seeded cache entry did not survive into
// alice's session.
await expect
.poll(() =>
page.evaluate(() => {
const testWindow = window as Window & {
__BUZZ_E2E_QUERY_CLIENT__?: {
getQueryData: (key: unknown[]) => unknown;
};
__BUZZ_E2E_PREVIOUS_QUERY_CLIENT__?: unknown;
};
const client = testWindow.__BUZZ_E2E_QUERY_CLIENT__;
if (!client) return "client-missing";
if (client === testWindow.__BUZZ_E2E_PREVIOUS_QUERY_CLIENT__) {
return "client-retained";
}
return client.getQueryData(["e2e-identity-boundary-probe"]) ===
undefined
? "rebuilt"
: "cache-retained";
}),
)
.toBe("rebuilt");
// Leg 2 — alice opens the channel tyler's seeded draft targets: the
// composer must not restore another identity's draft.
await page.getByTestId("channel-general").click();
const composerInput = page.getByTestId("message-input");
await expect(composerInput).toBeVisible();
await expect(composerInput).not.toContainText("tyler private draft");
// A draft typed by alice persists in alice's bucket, never tyler's. Draft
// persistence flushes on composer teardown, so switch channels after typing.
await composerInput.fill("alice draft after import");
await page.getByTestId("channel-random").click();
await expect
.poll(() =>
page.evaluate(
([aliceKey, tylerKey]) => {
const aliceStore = window.localStorage.getItem(aliceKey) ?? "";
const tylerStore = window.localStorage.getItem(tylerKey) ?? "";
return {
aliceHasDraft: aliceStore.includes("alice draft after import"),
tylerBucketLeaked: tylerStore.includes("alice draft after import"),
tylerDraftIntact: tylerStore.includes("tyler private draft"),
};
},
[aliceDraftStoreKey, tylerDraftStoreKey] as const,
),
)
.toEqual({
aliceHasDraft: true,
tylerBucketLeaked: false,
tylerDraftIntact: true,
});
// Leg 3 — alice's own draft restores from alice's bucket, and her first
// send succeeds. The mock's send handler asserts the frontend-captured
// signer against the active identity, so a send that still carried tyler's
// cached identity would be rejected and never render.
await page.getByTestId("channel-general").click();
await expect(composerInput).toContainText("alice draft after import");
await composerInput.fill("hello from alice");
await composerInput.press("Enter");
await expect(page.getByText("hello from alice").first()).toBeVisible();
await expect(composerInput).not.toContainText("hello from alice");
// Leg 4 — A restoration: relaunch as tyler (fully onboarded this time,
// open relay) on the same relay. Tyler's bucket must restore tyler's
// draft untouched by alice's session, and alice's draft must stay in
// alice's bucket. Tyler is not a member of the mock starter channels, so
// reach general through the channel browser.
await seedActiveIdentity(page, TEST_IDENTITIES.tyler);
await installMockBridge(page);
await page.goto("/");
await openChannelBrowser(page);
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
await page
.getByTestId("browse-channel-general")
.getByRole("button", { name: "Join" })
.click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await expect(composerInput).toContainText("tyler private draft");
await expect(composerInput).not.toContainText("alice draft after import");
});
test("onboarding relay reconnect — click shows Connected then auto-dismisses", async ({
page,
}) => {

View File

@@ -5,6 +5,7 @@ import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
const SHOTS = "test-results/project-commit-detail";
const ALIGNMENT_TOLERANCE_PX = 2;
const LATEST_COMMIT_HASH = "0123456789abcdef0123456789abcdef01234567";
// The projects surface is a preview feature — opt in before the app mounts.
// Must run before installMockBridge so React reads the override on mount.
@@ -17,6 +18,34 @@ async function enableProjectsFeature(page: import("@playwright/test").Page) {
});
}
async function addProjectToSidebar(
page: import("@playwright/test").Page,
dtag: string,
) {
await page.getByTestId("sidebar-projects-section-label").hover();
await page.getByTestId("sidebar-projects-create").click();
const browser = page.getByTestId("project-browser-dialog");
await browser.getByRole("searchbox", { name: "Search projects" }).fill(dtag);
await browser.getByTestId(`project-browser-result-${dtag}`).click();
}
async function waitForMockLiveSubscription(
page: import("@playwright/test").Page,
channelName: string,
) {
await expect
.poll(() =>
page.evaluate(
(name) =>
window.__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?.({
channelName: name,
}) ?? false,
channelName,
),
)
.toBe(true);
}
test("top-level project lists align dates and overflow actions", async ({
page,
}) => {
@@ -134,27 +163,23 @@ test("top-level project lists align dates and overflow actions", async ({
).toBeVisible();
await page.keyboard.press("Escape");
await page
.getByRole("button", { name: "Pull Requests", exact: true })
.click();
await page.getByRole("button", { name: "Filter pull requests" }).click();
await page.getByRole("button", { name: "Reviews", exact: true }).click();
await page.getByRole("button", { name: "Filter reviews" }).click();
await expect(
page.getByRole("menuitem", { name: "My Pull Requests" }),
page.getByRole("menuitem", { name: "My Reviews" }),
).toBeVisible();
await page.keyboard.press("Escape");
await page.getByTestId("projects-create-menu").hover();
await expect(page.getByRole("menuitem", { name: "Project" })).toBeVisible();
await expect(page.getByRole("menuitem", { name: "Issue" })).toBeVisible();
await page
.getByRole("menuitem", { name: "Pull Request", exact: true })
.click();
await expect(page.getByRole("menuitem", { name: "Task" })).toBeVisible();
await page.getByRole("menuitem", { name: "Review", exact: true }).click();
await expect(page.getByTestId("create-pull-request-dialog")).toBeVisible();
await expect(
page.getByTestId("create-pull-request-repository"),
).toBeVisible();
await page.keyboard.press("Escape");
await page.getByTestId("projects-create-menu").hover();
await page.getByRole("menuitem", { name: "Issue" }).click();
await page.getByRole("menuitem", { name: "Task" }).click();
await expect(page.getByTestId("create-issue-repository")).toBeVisible();
await page.keyboard.press("Escape");
const pullRequestRow = page
@@ -165,30 +190,29 @@ test("top-level project lists align dates and overflow actions", async ({
.getByRole("button", { name: /More options for/ })
.click();
await expect(
page.getByRole("menuitem", { name: /Review PR|View (draft|merge|closed)/ }),
page.getByRole("menuitem", {
name: /Open review|View (draft|merge|closed)/,
}),
).toBeVisible();
await page.keyboard.press("Escape");
await page.getByRole("button", { name: "Issues", exact: true }).click();
await page.getByRole("button", { name: "Filter issues" }).click();
await expect(page.getByRole("menuitem", { name: "My Issues" })).toBeVisible();
await page.getByRole("button", { name: "Tasks", exact: true }).click();
await page.getByRole("button", { name: "Filter tasks" }).click();
await expect(page.getByRole("menuitem", { name: "My Tasks" })).toBeVisible();
await page.keyboard.press("Escape");
const issueRow = page.locator('[data-testid^="projects-issue-row-"]').first();
await expect(issueRow).toBeVisible();
const issuePositions = await trailingPositions(issueRow);
expect(
Math.abs(pullRequestPositions.dateX - projectPositions.dateX),
Math.abs(pullRequestPositions.dateX - issuePositions.dateX),
).toBeLessThanOrEqual(ALIGNMENT_TOLERANCE_PX);
expect(
Math.abs(pullRequestPositions.menuX - projectPositions.menuX),
Math.abs(pullRequestPositions.menuX - issuePositions.menuX),
).toBeLessThanOrEqual(ALIGNMENT_TOLERANCE_PX);
expect(
Math.abs(issuePositions.dateX - projectPositions.dateX),
Math.abs(pullRequestPositions.rowHeight - issuePositions.rowHeight),
).toBeLessThanOrEqual(ALIGNMENT_TOLERANCE_PX);
expect(
Math.abs(issuePositions.menuX - projectPositions.menuX),
).toBeLessThanOrEqual(ALIGNMENT_TOLERANCE_PX);
await page.setViewportSize({ height: 720, width: 900 });
await page.getByTestId("projects-section-projects").click();
const responsiveRepositoryRow = page
@@ -307,9 +331,10 @@ test("unsupported relays keep the initial repository accessible", async ({
await projectEntry
.getByRole("button", { name: "View legacy-fallback" })
.click();
const repositoryPicker = page.getByTestId("project-repository-picker");
await expect(repositoryPicker).toBeVisible();
await expect(repositoryPicker).toContainText("legacy-fallback");
const repositoryRow = page.getByTestId(
"sidebar-project-repository-legacy-fallback",
);
await expect(repositoryRow).toBeVisible();
await waitForAnimations(page);
await page.screenshot({
path: `${SHOTS}/06-single-repository-add.png`,
@@ -406,41 +431,98 @@ test("multi-repository projects switch the active repository", async ({
await enableProjectsFeature(page);
await installMockBridge(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-projects-view").click();
await page.getByTestId("projects-section-projects").click();
await page
.locator(
'[data-testid="project-card-buzz"], [data-testid="project-row-buzz"]',
)
.first()
.click();
await addProjectToSidebar(page, "buzz");
const picker = page.getByTestId("project-repository-picker");
await expect(picker).toContainText("buzz");
await picker.click();
await expect(
page.getByTestId("project-repository-relay-tools"),
).toBeVisible();
const primaryRepository = page.getByTestId("sidebar-project-repository-buzz");
const relayToolsRepository = page.getByTestId(
"sidebar-project-repository-relay-tools",
);
const projectRow = page.getByTestId("sidebar-project-buzz");
await expect(projectRow).toHaveAttribute("aria-expanded", "true");
await expect(primaryRepository).toHaveAttribute("data-active", "true");
await expect(relayToolsRepository).toBeVisible();
await projectRow.click();
await expect(projectRow).toHaveAttribute("aria-expanded", "false");
await expect(relayToolsRepository).toBeHidden();
await page.reload({ waitUntil: "domcontentloaded" });
await addProjectToSidebar(page, "buzz");
await expect(projectRow).toHaveAttribute("aria-expanded", "false");
await expect(relayToolsRepository).toBeHidden();
await projectRow.click();
await expect(projectRow).toHaveAttribute("aria-expanded", "true");
await expect(relayToolsRepository).toBeVisible();
await page.getByTestId("channel-general").click();
await expect(projectRow).toHaveAttribute("aria-expanded", "true");
await expect(relayToolsRepository).toBeVisible();
const sidebarScrollContent = page.getByTestId("sidebar-scroll-content");
const channelSidebarMetrics = await sidebarScrollContent.evaluate(
(element) => {
const bounds = element.getBoundingClientRect();
return {
clientWidth: element.clientWidth,
left: bounds.left,
width: bounds.width,
};
},
);
await projectRow.click();
await expect(page).toHaveURL(/\/projects\//);
await expect(relayToolsRepository).toBeVisible();
const projectSidebarMetrics = await sidebarScrollContent.evaluate(
(element) => {
const bounds = element.getBoundingClientRect();
return {
clientWidth: element.clientWidth,
left: bounds.left,
width: bounds.width,
};
},
);
expect(projectSidebarMetrics).toEqual(channelSidebarMetrics);
await projectRow.click();
await expect(projectRow).toHaveAttribute("aria-expanded", "false");
await page.getByTestId("channel-general").click();
const sidebarScroller = page.locator('[data-sidebar="content"]');
const anchoredScrollTop = await sidebarScroller.evaluate((element) => {
element.scrollTop = Math.min(
20,
Math.max(0, element.scrollHeight - element.clientHeight),
);
return element.scrollTop;
});
await projectRow.click();
await expect(page).toHaveURL(/\/projects\//);
await expect(projectRow).toHaveAttribute("aria-expanded", "true");
await expect
.poll(() =>
sidebarScroller.evaluate((element) => Math.round(element.scrollTop)),
)
.toBe(Math.round(anchoredScrollTop));
await waitForAnimations(page);
await page.screenshot({
path: `${SHOTS}/04-multi-repository-picker.png`,
});
await page.getByTestId("project-repository-relay-tools").click();
await expect(picker).toContainText("relay-tools");
await relayToolsRepository.click();
await expect(relayToolsRepository).toHaveAttribute("data-active", "true");
await expect(page).toHaveURL(
new RegExp(`repositoryId=${TEST_IDENTITIES.alice.pubkey}%3Arelay-tools`),
);
await picker.click();
await page.getByTestId("add-project-repository").click();
await expect(page.getByTestId("create-project-repository")).toBeVisible();
await expect(page.getByTestId("attach-project-repository")).toBeVisible();
await page.getByTestId("create-project-repository").click();
await page.getByTestId("add-project-repository-name").fill("mobile-app");
await page.getByTestId("add-project-repository-submit").click();
await expect(page.getByTestId("add-project-repository-dialog")).toBeHidden();
await expect(picker).toContainText("mobile-app");
await expect(
page.getByTestId("sidebar-project-repository-mobile-app"),
).toBeVisible();
const addedEvents = await page.evaluate(
() =>
window.__BUZZ_E2E_ACCEPTED_PROJECT_EVENTS__?.filter(
@@ -459,7 +541,6 @@ test("multi-repository projects switch the active repository", async ({
addedEvents.find((event) => event.kind === 30617)?.tags,
).toContainEqual(["buzz-channel", "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50"]);
await picker.click();
await page.getByTestId("add-project-repository").click();
await page.getByTestId("attach-project-repository").click();
await expect(
@@ -469,7 +550,9 @@ test("multi-repository projects switch the active repository", async ({
await expect(
page.getByTestId("attach-project-repository-dialog"),
).toBeHidden();
await expect(picker).toContainText("design-system");
await expect(
page.getByTestId("sidebar-project-repository-design-system"),
).toBeVisible();
await expect
.poll(() =>
page.evaluate(
@@ -513,10 +596,22 @@ test("commit detail opens from the commits feed with a diff", async ({
const commitRows = page.getByTestId("project-activity-feed-item");
await expect(commitRows.first()).toBeVisible({ timeout: 10_000 });
// Commits share the rounded list structure used by issues and pull requests.
// Commits use the same compact, aligned row structure as work items.
await expect(
page.getByRole("heading", { name: "Commits", exact: true }),
).toBeVisible();
const firstCommitRow = commitRows.first();
expect((await firstCommitRow.boundingBox())?.height).toBeLessThanOrEqual(40);
await expect(firstCommitRow.getByTitle(/^View commit /)).toBeVisible();
await expect(
firstCommitRow.getByTestId("project-commit-author"),
).toHaveAttribute("title", /^Committed by .+ · \d+ commits?$/);
await expect(
firstCommitRow.getByRole("button", { name: "Copy commit hash" }),
).toBeVisible();
await expect(
firstCommitRow.getByTestId("project-commit-row-date"),
).toHaveClass(/text-muted-foreground\/70/);
await waitForAnimations(page);
await page.screenshot({
fullPage: false,
@@ -529,14 +624,24 @@ test("commit detail opens from the commits feed with a diff", async ({
.getByRole("button", { name: /Add Trello board workflow details/ })
.click();
// Detail header: author line, subject, and hash.
await expect(page.getByText("Commit from")).toBeVisible();
// Detail header: resolved author, subject, and hash.
await expect(page.getByText("Brain", { exact: true })).toBeVisible();
await expect(
page.getByRole("heading", { name: "Add Trello board workflow details" }),
).toBeVisible();
const commitDetail = page.getByTestId("project-commit-detail");
await expect(commitDetail).toHaveCSS("max-width", "768px");
await expect(
commitDetail.getByRole("heading", {
name: "Add Trello board workflow details",
}),
).toHaveCSS("font-size", "18px");
await expect(
page.getByRole("button", { name: "Copy commit hash" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Copy commit link" }),
).toBeVisible();
await expect(page.getByTestId("project-workspace-tab-menu")).toHaveCount(0);
await expect(
page.getByRole("link", { name: "project guide", exact: true }),
@@ -581,10 +686,10 @@ test("commit detail opens from the commits feed with a diff", async ({
.first()
.getByRole("button", { name: /Add Trello board workflow details/ })
.click();
await expect(page.getByText("Commit from")).toBeVisible();
await expect(page.getByText("Brain", { exact: true })).toBeVisible();
await page
.getByRole("navigation", { name: "Project breadcrumb" })
.getByRole("button", { name: "buzz", exact: true })
.getByTestId("project-breadcrumb-repository")
.click();
await expect(page.getByRole("tab", { name: "Overview" })).toHaveAttribute(
"aria-selected",
@@ -599,7 +704,69 @@ test("commit detail opens from the commits feed with a diff", async ({
await expect(projectEntry).toBeVisible();
});
test("pull request and issue feeds share the commit row structure", async ({
test("project discussion row opens its channel thread in context", async ({
page,
}) => {
await enableProjectsFeature(page);
await installMockBridge(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("channel-general").click();
await waitForMockLiveSubscription(page, "general");
await page.evaluate(
({ author, commitHash }) => {
const now = Math.floor(Date.now() / 1_000);
const root = window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "general",
content: `Context leading to ${commitHash} OR ${commitHash.slice(0, 7)}`,
createdAt: now - 1,
kind: 9,
pubkey: author,
});
if (!root) throw new Error("mock message emitter is not installed");
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "general",
content: `Follow-up about ${commitHash} OR ${commitHash.slice(0, 7)}`,
createdAt: now,
kind: 9,
parentEventId: root.id,
pubkey: author,
});
},
{
author: TEST_IDENTITIES.alice.pubkey,
commitHash: LATEST_COMMIT_HASH,
},
);
await page.getByTestId("open-projects-view").click();
await page.getByTestId("projects-section-projects").click();
await page
.locator(
'[data-testid="project-card-buzz"], [data-testid="project-row-buzz"]',
)
.first()
.click();
await page.getByRole("tab", { name: "Commits" }).click();
const commitRow = page.getByTestId("project-activity-feed-item").first();
await commitRow
.getByRole("button", { name: /Add Trello board workflow details/ })
.click();
await page
.getByRole("button", { name: "Open conversation in #general" })
.click();
const panel = page.getByTestId("project-conversation-panel");
await expect(panel).toBeVisible();
await expect(panel).toContainText(`Context leading to ${LATEST_COMMIT_HASH}`);
await expect(panel).toContainText(`Follow-up about ${LATEST_COMMIT_HASH}`);
await expect(
page.getByRole("heading", { name: "Add Trello board workflow details" }),
).toBeVisible();
await panel.getByRole("button", { name: "Close panel" }).click();
await expect(panel).toBeHidden();
});
test("pull request and issue feeds use compact work item rows", async ({
page,
}) => {
await enableProjectsFeature(page);
@@ -619,36 +786,83 @@ test("pull request and issue feeds share the commit row structure", async ({
await expect(projectEntry).toBeVisible({ timeout: 10_000 });
await projectEntry.click();
// PR rows use the shared feed row: title button + #id cluster cell.
await page.getByRole("tab", { name: "Pull Request" }).click();
// Reviews use the compact single-line work-item row.
await page.getByRole("tab", { name: "Review" }).click();
const prRows = page.getByTestId("project-pull-request-row");
await expect(prRows.first()).toBeVisible({ timeout: 10_000 });
await expect(
prRows.first().getByRole("button", { name: /^#/ }),
).toBeVisible();
expect((await prRows.first().boundingBox())?.height).toBeLessThanOrEqual(40);
await expect(
prRows.first().getByTestId("project-pull-request-comments"),
).toHaveText("0");
await expect(
prRows.first().getByTestId("project-pull-request-row-date"),
).toHaveClass(/text-muted-foreground\/70/);
await expect(
page.getByTestId("project-work-item-group-header").first(),
).toBeVisible();
await expect(
prRows.first().locator("button").filter({ hasText: /.+/ }).nth(1),
).toHaveCSS("font-weight", "400");
await waitForAnimations(page);
await page.screenshot({ fullPage: false, path: `${SHOTS}/03-prs-feed.png` });
// The #id cell opens the PR detail, same as clicking the title.
// The inline #id opens the review detail, same as clicking the title.
await prRows.first().getByRole("button", { name: /^#/ }).click();
await expect(
page.getByRole("navigation", { name: "Project breadcrumb" }),
).toContainText("Pull Request");
).toContainText("Review");
// Step back to the feed so the community tabs are available again.
await page
.getByRole("navigation", { name: "Project breadcrumb" })
.getByRole("button", { name: "Pull Request", exact: true })
.getByRole("button", { name: "Review", exact: true })
.click();
await expect(prRows.first()).toBeVisible();
// Issue rows share the same structure.
await page.getByRole("tab", { name: "Issues" }).click();
// Tasks share the same compact structure.
await page.getByRole("tab", { name: "Tasks" }).click();
const issueRows = page.getByTestId("project-issue-row");
await expect(issueRows.first()).toBeVisible({ timeout: 10_000 });
await expect(
issueRows.first().getByRole("button", { name: /^#/ }),
).toBeVisible();
expect((await issueRows.first().boundingBox())?.height).toBeLessThanOrEqual(
40,
);
const taskCategoryCells = issueRows.getByTestId("project-issue-row-category");
await expect(taskCategoryCells.first()).toHaveText(
/^(Issue|Change request|Improvement)$/,
);
const taskCreator = issueRows.first().getByTestId("project-issue-creator");
const emptyAssignee = issueRows
.first()
.getByTestId("project-issue-assignee-placeholder");
await expect(taskCreator).toHaveAttribute("title", /^Created by /);
await expect(emptyAssignee).toHaveAttribute("title", "Unassigned");
const [taskCreatorBox, emptyAssigneeBox] = await Promise.all([
taskCreator.boundingBox(),
emptyAssignee.boundingBox(),
]);
expect(
(emptyAssigneeBox?.x ?? 0) -
((taskCreatorBox?.x ?? 0) + (taskCreatorBox?.width ?? 0)),
).toBeLessThanOrEqual(4);
await expect(
issueRows.first().getByTestId("project-issue-comments"),
).toHaveText("0");
await expect(
issueRows.first().getByTestId("project-issue-row-date"),
).toHaveClass(/text-muted-foreground\/70/);
const taskCategoryBoxes = await taskCategoryCells.evaluateAll((cells) =>
cells.slice(0, 5).map((cell) => {
const box = cell.getBoundingClientRect();
return box.x + box.width;
}),
);
expect(new Set(taskCategoryBoxes).size).toBe(1);
await waitForAnimations(page);
await page.screenshot({
fullPage: false,
@@ -668,17 +882,8 @@ test("adding a repository retries and reports an error when the 30617 publicatio
});
await installMockBridge(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-projects-view").click();
await page.getByTestId("projects-section-projects").click();
await page
.locator(
'[data-testid="project-card-buzz"], [data-testid="project-row-buzz"]',
)
.first()
.click();
await addProjectToSidebar(page, "buzz");
const picker = page.getByTestId("project-repository-picker");
await picker.click();
await page.getByTestId("add-project-repository").click();
await page.getByTestId("create-project-repository").click();
await page.getByTestId("add-project-repository-name").fill("rejected-repo");
@@ -735,18 +940,8 @@ test("adding a repository treats a lost 30617 acknowledgement as success", async
});
await installMockBridge(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-projects-view").click();
await page.getByTestId("projects-section-projects").click();
await page
.locator(
'[data-testid="project-card-buzz"], [data-testid="project-row-buzz"]',
)
.first()
.click();
await addProjectToSidebar(page, "buzz");
const picker = page.getByTestId("project-repository-picker");
await picker.click();
await page.getByTestId("add-project-repository").click();
await page.getByTestId("create-project-repository").click();
await page.getByTestId("add-project-repository-name").fill("lost-ack-repo");
@@ -754,8 +949,10 @@ test("adding a repository treats a lost 30617 acknowledgement as success", async
// The dialog should close — the operation recovered from the lost ACK.
await expect(page.getByTestId("add-project-repository-dialog")).toBeHidden();
// The repository picker must reflect the newly added repository.
await expect(picker).toContainText("lost-ack-repo");
// The sidebar must reflect the newly added repository.
await expect(
page.getByTestId("sidebar-project-repository-lost-ack-repo"),
).toBeVisible();
// Both events must have been accepted: the 30621 (project update) and the
// 30617 (repository — accepted by relay even though ACK was lost).
@@ -812,8 +1009,6 @@ test("adding a repository blocks when a standalone 30617 already exists at that
.first()
.click();
const picker = page.getByTestId("project-repository-picker");
await picker.click();
await page.getByTestId("add-project-repository").click();
await page.getByTestId("create-project-repository").click();
// Use the same name — the dtag will match the seeded standalone 30617.
@@ -900,10 +1095,10 @@ test("navigating via a 30617 entity-link route opens the correct non-primary rep
{ waitUntil: "domcontentloaded" },
);
// Repository controls intentionally render only on README and Files. The
// seeded PR and branch prove that this detail route resolved relay-tools
// rather than falling back to the project's primary repository.
await expect(page.getByTestId("project-repository-picker")).toHaveCount(0);
// Direct navigation must not implicitly add the project to the sidebar.
await expect(page.getByTestId("sidebar-project-buzz")).toHaveCount(0);
// The seeded PR proves that this detail route resolved relay-tools rather
// than falling back to the project's primary repository.
// Use `first()` to avoid Playwright strict-mode violations: the text appears
// in both the breadcrumb and the PR title heading once the detail panel opens.
await expect(

View File

@@ -27,11 +27,13 @@ test("Buzz Git pull request renders and stays actionable in Inbox", async ({
)
.first()
.click();
await page.getByRole("tab", { name: "Pull Request" }).click();
await page.getByRole("tab", { name: "Review" }).click();
const alicePullRequest = page
.getByTestId("project-pull-request-row")
.filter({ hasText: "alice" })
.filter({
has: page.getByRole("button", { name: "alice", exact: true }),
})
.first();
await expect(alicePullRequest).toBeVisible({ timeout: 10_000 });
const pullRequestId = await alicePullRequest.getAttribute(
@@ -110,7 +112,7 @@ test("Buzz Git pull request renders and stays actionable in Inbox", async ({
).toBeVisible();
await expect(
detail.getByRole("heading", {
name: "alice sent you a pull request",
name: "alice sent you a review",
exact: true,
}),
).toBeVisible();
@@ -144,8 +146,8 @@ test("Buzz Git pull request renders and stays actionable in Inbox", async ({
path: "test-results/project-inbox/01-pull-request-detail.png",
});
// At the wide two-column breakpoint, metadata may wrap between phrases but
// must never compress individual phrases into word-wide columns.
// Metadata phrases may wrap between items but must never compress
// individual phrases into word-wide columns.
await page.setViewportSize({ width: 1440, height: 900 });
await expect
.poll(() =>
@@ -156,7 +158,7 @@ test("Buzz Git pull request renders and stays actionable in Inbox", async ({
.filter(Boolean).length,
),
)
.toBe(2);
.toBe(1);
await expect(
detail.getByRole("button", {
name: "Open author-claimed origin channel #general",

View File

@@ -26,7 +26,7 @@ test("issue comments use the project activity timeline", async ({ page }) => {
await installMockBridge(page);
await openBuzzProject(page);
await page.getByRole("tab", { name: "Issues", exact: true }).click();
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();
@@ -75,7 +75,7 @@ test("issue assignees can be assigned and unassigned", async ({ page }) => {
await installMockBridge(page);
await openBuzzProject(page);
await page.getByRole("tab", { name: "Issues", exact: true }).click();
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();
@@ -94,6 +94,6 @@ test("issue assignees can be assigned and unassigned", async ({ page }) => {
const unassign = page.getByTestId(`project-issue-unassign-${assignee}`);
await expect(unassign).toBeVisible({ timeout: 10_000 });
await unassign.click();
await expect(page.getByText("Issue unassigned.")).toBeVisible();
await expect(page.getByText("Task unassigned.")).toBeVisible();
await expect(unassign).toHaveCount(0, { timeout: 10_000 });
});

View File

@@ -32,6 +32,39 @@ async function openBuzzProject(page: import("@playwright/test").Page) {
await projectEntry.click();
}
async function addProjectToSidebar(
page: import("@playwright/test").Page,
dtag: string,
) {
await page.getByTestId("sidebar-projects-section-label").hover();
await page.getByTestId("sidebar-projects-create").click();
const browser = page.getByTestId("project-browser-dialog");
await browser.getByRole("searchbox", { name: "Search projects" }).fill(dtag);
await browser.getByTestId(`project-browser-result-${dtag}`).click();
}
function pullRequestRowByAuthor(
page: import("@playwright/test").Page,
author: string,
) {
return page.getByTestId("project-pull-request-row").filter({
has: page.getByRole("button", { name: author, exact: true }),
});
}
async function expectLocalRepositoryOpenAction(
page: import("@playwright/test").Page,
) {
const openButton = page.getByRole("button", { name: "Open", exact: true });
await expect(openButton).toHaveAttribute(
"title",
"Open local repository folder",
);
await expect(
page.getByRole("link", { name: "Open", exact: true }),
).toHaveCount(0);
}
test("same-second request changes supersedes approval", async ({ page }) => {
await enableProjectsFeature(page);
await page.addInitScript(() => {
@@ -40,17 +73,14 @@ test("same-second request changes supersedes approval", async ({ page }) => {
await installMockBridge(page);
await openBuzzProject(page);
await page.getByRole("tab", { name: "Pull Request" }).click();
const aliceRow = page
.getByTestId("project-pull-request-row")
.filter({ hasText: "alice" })
.first();
await page.getByRole("tab", { name: "Review" }).click();
const aliceRow = pullRequestRowByAuthor(page, "alice").first();
await expect(aliceRow).toBeVisible({ timeout: 10_000 });
await aliceRow.getByRole("button", { name: /^#/ }).click();
await page.getByRole("button", { name: "Approve", exact: true }).click();
const approveDialog = page.getByRole("dialog", {
name: "Approve pull request",
name: "Approve review",
});
await approveDialog
.getByRole("textbox", { name: "Approval summary" })
@@ -58,7 +88,7 @@ test("same-second request changes supersedes approval", async ({ page }) => {
await approveDialog
.getByRole("button", { name: "Approve", exact: true })
.click();
await expect(page.getByText("Pull request approved.")).toBeVisible();
await expect(page.getByText("Review approved.")).toBeVisible();
await expect(
page.getByRole("button", { name: "Approve", exact: true }),
).toHaveCount(0);
@@ -106,18 +136,23 @@ test("PR creator/owner can toggle draft, request reviews, and approve", async ({
await installMockBridge(page);
await openBuzzProject(page);
await page.getByRole("tab", { name: "Pull Request" }).click();
await page.getByRole("tab", { name: "Review" }).click();
const prRows = page.getByTestId("project-pull-request-row");
await expect(prRows.first()).toBeVisible({ timeout: 10_000 });
// Pick a PR authored by alice: the viewer is not the author, so the
// Approve button must be available alongside the owner status controls.
const aliceRow = prRows.filter({ hasText: "alice" }).first();
const aliceRow = prRows
.filter({
has: page.getByRole("button", { name: "alice", exact: true }),
})
.first();
await expect(aliceRow).toBeVisible();
await aliceRow.getByRole("button", { name: /^#/ }).click();
const header = page.getByRole("heading", { level: 3 });
await expect(header.first()).toBeVisible();
await expect(page.getByTestId("project-review-summary")).toBeVisible();
const sourceChannelLink = page.getByRole("button", {
name: "Open author-claimed origin channel #general",
exact: true,
@@ -126,7 +161,7 @@ test("PR creator/owner can toggle draft, request reviews, and approve", async ({
// Owner viewing an open PR: draft toggle and both review decisions are offered.
const morePullRequestActions = page.getByRole("button", {
name: "More pull request actions",
name: "More review actions",
});
const approve = page.getByRole("button", { name: "Approve", exact: true });
const commentComposer = page.getByTestId(
@@ -206,9 +241,9 @@ test("PR creator/owner can toggle draft, request reviews, and approve", async ({
.getByTestId("project-pull-request-timeline-row")
.filter({ hasText: "requested changes" }),
).toBeVisible({ timeout: 10_000 });
await expect(
page.getByText("Changes requested", { exact: true }),
).toHaveCount(0);
await expect(page.getByTestId("project-review-summary")).toHaveText(
"Changes requested",
);
const changeRequestEvent = await page.evaluate(() =>
window.__BUZZ_E2E_SIGNED_EVENTS__
?.filter(
@@ -273,7 +308,7 @@ test("PR creator/owner can toggle draft, request reviews, and approve", async ({
// remain tied to the current commit and their timestamps preserve order.
await approve.click();
const approveDialog = page.getByRole("dialog", {
name: "Approve pull request",
name: "Approve review",
});
await approveDialog
.getByRole("textbox", { name: "Approval summary" })
@@ -281,11 +316,14 @@ test("PR creator/owner can toggle draft, request reviews, and approve", async ({
await approveDialog
.getByRole("button", { name: "Approve", exact: true })
.click();
await expect(page.getByText("Pull request approved.")).toBeVisible();
await expect(page.getByText("Review approved.")).toBeVisible();
await expect(
page.getByRole("button", { name: "Approve", exact: true }),
).toHaveCount(0);
await expect(page.getByText("Approved", { exact: true })).toHaveCount(0);
await expect(page.getByTestId("project-review-summary")).toHaveText(
"Awaiting review",
);
await expect(page.getByText("Approved", { exact: true })).toBeVisible();
const approvalEvent = await page.evaluate(() =>
window.__BUZZ_E2E_SIGNED_EVENTS__
?.filter(
@@ -366,18 +404,18 @@ test("PR creator/owner can toggle draft, request reviews, and approve", async ({
// Closing is reversible, unlike merging: a closed PR can be reopened.
await morePullRequestActions.click();
const closePullRequest = page.getByRole("menuitem", {
name: "Close pull request",
name: "Close review",
});
await closePullRequest.click();
await expect(page.getByText("Pull request closed.")).toBeVisible();
await expect(page.getByText("Review closed.")).toBeVisible();
const reopenPullRequest = page.getByRole("button", {
name: "Reopen pull request",
name: "Reopen review",
});
await expect(reopenPullRequest).toBeVisible({ timeout: 10_000 });
await expect(closePullRequest).toHaveCount(0);
await reopenPullRequest.click();
await expect(page.getByText("Pull request reopened.")).toBeVisible();
await expect(page.getByText("Review reopened.")).toBeVisible();
await expect(morePullRequestActions).toBeVisible({ timeout: 10_000 });
await page.getByRole("button", { name: "Merge", exact: true }).click();
@@ -406,9 +444,7 @@ test("PR creator/owner can toggle draft, request reviews, and approve", async ({
exact: true,
})
.click();
await expect(
page.getByText("Published merged pull request status."),
).toBeVisible();
await expect(page.getByText("Published merged review status.")).toBeVisible();
await expect
.poll(() =>
page.evaluate(
@@ -470,11 +506,8 @@ test("merge conflicts offer persistent terminal recovery", async ({ page }) => {
};
});
await page.getByRole("tab", { name: "Pull Request" }).click();
const aliceRow = page
.getByTestId("project-pull-request-row")
.filter({ hasText: "alice" })
.first();
await page.getByRole("tab", { name: "Review" }).click();
const aliceRow = pullRequestRowByAuthor(page, "alice").first();
await aliceRow.getByRole("button", { name: /^#/ }).click();
await page.getByRole("button", { name: "Merge", exact: true }).click();
await page.getByTestId("merge-pull-request-confirm-button").click();
@@ -533,13 +566,10 @@ test("reviewer can leave a commit-scoped inline diff comment", async ({
await installMockBridge(page);
await openBuzzProject(page);
await page.getByRole("tab", { name: "Pull Request" }).click();
const aliceRow = page
.getByTestId("project-pull-request-row")
.filter({ hasText: "alice" })
.first();
await page.getByRole("tab", { name: "Review" }).click();
const aliceRow = pullRequestRowByAuthor(page, "alice").first();
await aliceRow.getByRole("button", { name: /^#/ }).click();
await page.getByRole("tab", { name: /Files changed/ }).click();
await page.getByRole("button", { name: /^Files changed/ }).click();
const diffLine = page
.getByTestId("project-diff-line")
@@ -584,16 +614,12 @@ test("reviewer can leave a commit-scoped inline diff comment", async ({
"Please add a type for this parameter.",
);
await page.getByRole("tab", { name: "Conversation" }).click();
await expect(
page.getByTestId("project-pull-request-review-history-toggle"),
).toHaveAttribute("aria-expanded", "true");
await expect(
page.getByText("Please add a type for this parameter."),
).toBeVisible();
await expect(
page.getByText("desktop/src/features/projects/ui/ProjectDetailScreen.tsx"),
).toBeVisible();
const timelineComment = page
.getByTestId("project-pull-request-timeline-row")
.filter({ hasText: "Please add a type for this parameter." });
await expect(timelineComment).toContainText(
"desktop/src/features/projects/ui/ProjectDetailScreen.tsx",
);
await waitForAnimations(page);
await page.screenshot({
fullPage: false,
@@ -606,8 +632,8 @@ test("reviewer can leave a commit-scoped inline diff comment", async ({
})
.click();
await expect(
page.getByRole("tab", { name: /Files changed/ }),
).toHaveAttribute("data-state", "active");
page.getByRole("button", { name: /^Files changed/ }),
).toHaveAttribute("aria-expanded", "true");
const focusedLine = page.getByTestId("project-diff-focused-line");
await expect(focusedLine).toBeVisible();
await expect(focusedLine).toHaveAttribute(
@@ -639,11 +665,8 @@ test("managed agent repository owner can merge", async ({ page }) => {
});
await openBuzzProject(page);
await page.getByRole("tab", { name: "Pull Request" }).click();
const agentRow = page
.getByTestId("project-pull-request-row")
.filter({ hasText: "Brain" })
.first();
await page.getByRole("tab", { name: "Review" }).click();
const agentRow = pullRequestRowByAuthor(page, "Brain").first();
await expect(agentRow).toBeVisible({ timeout: 10_000 });
await agentRow.getByRole("button", { name: /^#/ }).click();
await page.getByRole("button", { name: "Add Reviewer", exact: true }).click();
@@ -663,15 +686,15 @@ test("managed agent repository owner can merge", async ({ page }) => {
targetOwner: TEST_IDENTITIES.alice.pubkey,
},
});
await page.getByRole("button", { name: "More pull request actions" }).click();
await page.getByRole("button", { name: "More review actions" }).click();
const closePullRequest = page.getByRole("menuitem", {
name: "Close pull request",
name: "Close review",
});
await expect(closePullRequest).toBeVisible();
await closePullRequest.click();
await expect(page.getByText("Pull request closed.")).toBeVisible();
await page.getByRole("button", { name: "Reopen pull request" }).click();
await expect(page.getByText("Pull request reopened.")).toBeVisible();
await expect(page.getByText("Review closed.")).toBeVisible();
await page.getByRole("button", { name: "Reopen review" }).click();
await expect(page.getByText("Review reopened.")).toBeVisible();
const statusPayloads = await page.evaluate(() =>
window.__BUZZ_E2E_COMMAND_PAYLOADS__?.filter(
(entry) => entry.command === "sign_project_pull_request_status",
@@ -726,7 +749,7 @@ test("viewer without repository ownership cannot merge", async ({ page }) => {
});
await openBuzzProject(page);
await page.getByRole("tab", { name: "Pull Request" }).click();
await page.getByRole("tab", { name: "Review" }).click();
const prRow = page.getByTestId("project-pull-request-row").first();
await expect(prRow).toBeVisible({ timeout: 10_000 });
await prRow.getByRole("button", { name: /^#/ }).click();
@@ -779,15 +802,13 @@ test("project pull requests preserve partial results from batched queries", asyn
await installMockBridge(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-projects-view").click();
await page
.getByRole("button", { name: "Pull Requests", exact: true })
.click();
await page.getByRole("button", { name: "Reviews", exact: true }).click();
await expect(
page.getByRole("button", { name: /^View / }).first(),
).toBeVisible();
await expect(
page.getByText(/Some pull request details could not be loaded/),
page.getByText(/Some review details could not be loaded/),
).toBeVisible();
await expect(page.getByRole("button", { name: "Retry" })).toBeVisible();
@@ -823,7 +844,7 @@ test("project pull requests preserve partial results from batched queries", asyn
});
await page.getByRole("button", { name: "Retry" }).click();
await expect(
page.getByText(/Some pull request details could not be loaded/),
page.getByText(/Some review details could not be loaded/),
).toHaveCount(0);
});
@@ -834,9 +855,7 @@ test("project pull request author rollover stays identity-only", async ({
await installMockBridge(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-projects-view").click();
await page
.getByRole("button", { name: "Pull Requests", exact: true })
.click();
await page.getByRole("button", { name: "Reviews", exact: true }).click();
await page.getByRole("button", { name: "List layout" }).click();
const row = page.locator('[data-testid^="projects-pr-row-"]').first();
@@ -867,7 +886,7 @@ test("project issue author rollover matches pull requests", async ({
await installMockBridge(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-projects-view").click();
await page.getByRole("button", { name: "Issues", exact: true }).click();
await page.getByRole("button", { name: "Tasks", exact: true }).click();
await page.getByRole("button", { name: "List layout" }).click();
const row = page.locator('[data-testid^="projects-issue-row-"]').first();
@@ -901,19 +920,17 @@ test("project pull requests report aggregate root query failures", async ({
await installMockBridge(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-projects-view").click();
await page
.getByRole("button", { name: "Pull Requests", exact: true })
.click();
await page.getByRole("button", { name: "Reviews", exact: true }).click();
await expect(page.getByText("Could not load pull requests.")).toBeVisible();
await expect(page.getByText("Could not load reviews.")).toBeVisible();
await expect(page.getByRole("button", { name: "Retry" })).toBeVisible();
await expect(page.getByText("No pull requests yet.")).toHaveCount(0);
await expect(page.getByText("No reviews yet.")).toHaveCount(0);
await page.evaluate(() => {
window.__BUZZ_E2E_REJECT_PROJECT_QUERY_KINDS__ = [];
});
await page.getByRole("button", { name: "Retry" }).click();
await expect(page.getByText("Could not load pull requests.")).toHaveCount(0);
await expect(page.getByText("Could not load reviews.")).toHaveCount(0);
await expect(
page.getByRole("button", { name: /^View / }).first(),
).toBeVisible();
@@ -929,13 +946,13 @@ test("project issues preserve partial results from aggregate queries", async ({
await installMockBridge(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-projects-view").click();
await page.getByRole("button", { name: "Issues", exact: true }).click();
await page.getByRole("button", { name: "Tasks", exact: true }).click();
await expect(
page.getByRole("button", { name: /^View / }).first(),
).toBeVisible();
await expect(
page.getByText("Some issue details could not be loaded."),
page.getByText("Some task details could not be loaded."),
).toBeVisible();
// Rejecting kind 1 fails both the comment window and the exhaustive
// assignment-operation query, so both sections are reported missing.
@@ -947,7 +964,7 @@ test("project issues preserve partial results from aggregate queries", async ({
});
await page.getByRole("button", { name: "Retry" }).click();
await expect(
page.getByText("Some issue details could not be loaded."),
page.getByText("Some task details could not be loaded."),
).toHaveCount(0);
});
@@ -1046,14 +1063,14 @@ test("project subsections do not paint backgrounds behind list or grid items", a
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-projects-view").click();
for (const section of ["Repositories", "Pull Requests", "Issues"] as const) {
for (const section of ["Repositories", "Reviews", "Tasks"] as const) {
await page.getByRole("button", { name: section, exact: true }).click();
await page.getByRole("button", { name: "List layout" }).click();
const listItems = page.locator(
section === "Repositories"
? '[data-testid^="repository-row-"]'
: section === "Pull Requests"
: section === "Reviews"
? '[data-testid^="projects-pr-row-"]'
: '[data-testid^="projects-issue-row-"]',
);
@@ -1092,16 +1109,26 @@ test("project detail content areas do not paint background fills", async ({
await installMockBridge(page);
await openBuzzProject(page);
const expectVisiblePanelsToBeTransparent = async () => {
const expectVisiblePanelsToBeTransparent = async ({
bordered = true,
required = false,
}: {
bordered?: boolean;
required?: boolean;
} = {}) => {
const panels = page.locator("[data-project-detail-panel]:visible");
await expect(panels.first()).toBeVisible();
const panelCount = await panels.count();
if (required) await expect(panels.first()).toBeVisible();
for (let index = 0; index < panelCount; index += 1) {
await expect(panels.nth(index)).toHaveCSS(
"background-color",
"rgba(0, 0, 0, 0)",
);
await expect(panels.nth(index)).toHaveCSS("border-style", "solid");
if (bordered) {
await expect(panels.nth(index)).toHaveCSS("border-style", "solid");
} else {
await expect(panels.nth(index)).toHaveCSS("border-width", "0px");
}
}
};
@@ -1109,22 +1136,25 @@ test("project detail content areas do not paint background fills", async ({
"Overview",
"Files",
"Commits",
"Issues",
"Pull Request",
"Tasks",
"Review",
"Contributors",
]) {
await page.getByRole("tab", { name: tab, exact: true }).click();
await expectVisiblePanelsToBeTransparent();
}
await page.getByRole("tab", { name: "Pull Request", exact: true }).click();
await page.getByRole("tab", { name: "Review", exact: true }).click();
const pullRequest = page.getByTestId("project-pull-request-row").first();
await expect(pullRequest).toBeVisible();
await pullRequest.getByRole("button", { name: /^#/ }).click();
await expectVisiblePanelsToBeTransparent();
await expectVisiblePanelsToBeTransparent({
bordered: false,
required: true,
});
});
test("project without a checkout offers fetch feedback and dropdown cloning", async ({
test("project without a checkout offers fetch feedback and cloning", async ({
page,
}) => {
await enableProjectsFeature(page);
@@ -1132,21 +1162,21 @@ test("project without a checkout offers fetch feedback and dropdown cloning", as
await openBuzzProject(page);
await expect(
page.getByRole("button", { name: "Buzz", exact: true }),
page.getByRole("button", { name: "Remote", exact: true }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Buzz", exact: true }),
page.getByRole("button", { name: "Remote", exact: true }),
).toHaveClass(/\bborder-input\/40\b/);
await expect(page.getByRole("button", { name: /main/ })).toHaveClass(
/\bborder-input\/40\b/,
);
await expect(
page.getByRole("button", { name: "Clone", exact: true }),
).toHaveCount(0);
).toBeVisible();
await page.getByRole("button", { name: "Fetch", exact: true }).click();
await expect(page.getByText("Remote state refreshed.")).toBeVisible();
await page.getByRole("button", { name: "Buzz", exact: true }).click();
await page.getByRole("button", { name: "Remote", exact: true }).click();
const cloneItem = page.getByRole("menuitem", {
name: "Local missing Clone",
});
@@ -1228,7 +1258,7 @@ test("repository tags can be browsed as immutable remote snapshots", async ({
await expect(page.getByRole("button", { name: /v1\.0\.0/ })).toBeVisible();
await expect(
page.getByRole("button", { name: "Buzz", exact: true }),
page.getByRole("button", { name: "Remote", exact: true }),
).toBeVisible();
await expect
.poll(() =>
@@ -1281,6 +1311,114 @@ test("project branches can be deleted but the default branch cannot", async ({
expect(commands).toContain("delete_project_remote_branch");
});
test("external repositories stay on local source after a branch round trip", async ({
page,
}) => {
await enableProjectsFeature(page);
await page.addInitScript(() => {
const commit = "0123456789abcdef0123456789abcdef01234567";
const localBranch =
"wintermute/entity-link-recipient-cards-with-a-long-branch-name";
window.sessionStorage.setItem(
"buzz-e2e-project-branches",
JSON.stringify({ "relay-tools": { [localBranch]: commit } }),
);
window.__BUZZ_E2E_PROJECT_REPO_SYNC_STATUS__ = {
local_path: "/tmp/buzz/REPOS/relay-tools",
local_branch: localBranch,
local_branches: ["main", localBranch],
local_head: commit,
local_short_head: commit.slice(0, 7),
remote_branch: localBranch,
remote_head: commit,
remote_short_head: commit.slice(0, 7),
merge_base: commit,
ahead_count: 0,
behind_count: 0,
has_uncommitted_changes: false,
has_untracked_files: false,
can_push: false,
push_block_reason: "Local branch is already pushed.",
can_pull: false,
pull_block_reason: "Local branch is up to date.",
};
window.__BUZZ_E2E_PROJECT_LOCAL_REPO_SNAPSHOT__ = {
path: "/tmp/buzz/REPOS/relay-tools",
snapshot: {
latest_commit: null,
commits: [],
contributors: [],
files: [
{
path: "README.md",
kind: "text",
size: 21,
preview_content: "# Local branch README",
last_changed_at: null,
latest_commit: null,
},
],
},
};
});
await installMockBridge(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await addProjectToSidebar(page, "buzz");
await page.getByTestId("sidebar-project-repository-relay-tools").click();
await expect(
page.getByRole("heading", { name: "Local branch README" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Local", exact: true }),
).toBeVisible();
await expectLocalRepositoryOpenAction(page);
await page.getByRole("button", { name: /main/ }).click();
await page
.getByRole("menuitemradio", {
name: "wintermute/entity-link-recipient-cards-with-a-long-branch-name",
})
.click();
const branchTrigger = page.getByTestId("project-repository-branch-trigger");
await expect(
page.getByRole("button", {
name: /wintermute\/entity-link-recipient-cards-with-a-long-branch-name/,
}),
).toBeVisible();
await expect
.poll(() =>
branchTrigger.evaluate(
(element) => element.scrollWidth <= element.clientWidth,
),
)
.toBe(true);
await expect
.poll(() =>
branchTrigger
.locator("span")
.evaluate((element) => element.scrollWidth > element.clientWidth),
)
.toBe(true);
await expect(
page.getByRole("button", { name: "Local", exact: true }),
).toBeVisible();
await expectLocalRepositoryOpenAction(page);
await branchTrigger.click();
await page.getByRole("menuitemradio", { name: "main" }).click();
await expect(page.getByRole("button", { name: /main/ })).toBeVisible();
await expect(
page.getByRole("button", { name: "Local", exact: true }),
).toBeVisible();
await expect(
page.getByRole("heading", { name: "Local branch README" }),
).toBeVisible();
await expectLocalRepositoryOpenAction(page);
await expect(page.getByText("Code hosted on github.com")).toHaveCount(0);
});
test("pushed local branch can open a pull request", async ({ page }) => {
await enableProjectsFeature(page);
await page.addInitScript(() => {
@@ -1315,8 +1453,8 @@ test("pushed local branch can open a pull request", async ({ page }) => {
await page
.getByRole("menuitemradio", { name: "feature/projects-workflow" })
.click();
await page.getByRole("tab", { name: "Pull Request", exact: true }).click();
await page.getByRole("button", { name: "New pull request" }).click();
await page.getByRole("tab", { name: "Review", exact: true }).click();
await page.getByRole("button", { name: "Create review" }).click();
await expect(page.getByTestId("create-pull-request-repository")).toHaveValue(
/:buzz$/,
);
@@ -1336,7 +1474,7 @@ test("pushed local branch can open a pull request", async ({ page }) => {
button.click();
button.click();
});
await expect(page.getByText("Pull request created.")).toBeVisible();
await expect(page.getByText("Review created.")).toBeVisible();
const createdEvents = await page.evaluate(
() =>
@@ -1357,15 +1495,21 @@ test("pushed local branch can open a pull request", async ({ page }) => {
]);
});
test("project issue can be created from the issues header", async ({
test("project task can be created with a category from the tasks header", async ({
page,
}) => {
await enableProjectsFeature(page);
await installMockBridge(page);
await openBuzzProject(page);
await page.getByRole("tab", { name: "Issues", exact: true }).click();
await page.getByRole("button", { name: "New issue" }).click();
await page.getByRole("tab", { name: "Tasks", exact: true }).click();
await page
.getByTestId("project-section-header")
.getByRole("button", { name: "Create task" })
.click();
await page
.getByTestId("create-issue-category")
.selectOption("change-request");
await page
.getByTestId("create-issue-title")
.fill("Document the broken workflow");
@@ -1373,7 +1517,7 @@ test("project issue can be created from the issues header", async ({
.getByTestId("create-issue-body")
.fill("The project workflow needs a clear repair path.");
await page.getByTestId("create-issue-submit").click();
await expect(page.getByText("Issue created.")).toBeVisible();
await expect(page.getByText("Task created.")).toBeVisible();
const createdEvent = await page.evaluate(() =>
window.__BUZZ_E2E_SIGNED_EVENTS__?.find((event) => event.kind === 1621),
@@ -1382,4 +1526,5 @@ test("project issue can be created from the issues header", async ({
"subject",
"Document the broken workflow",
]);
expect(createdEvent?.tags).toContainEqual(["t", "change-request"]);
});

File diff suppressed because it is too large Load Diff

View File

@@ -229,8 +229,7 @@ test("failed initial relay dial retries automatically", async ({ page }) => {
__BUZZ_E2E_GET_RELAY_CONNECTION_STATE__?: () => string;
}
).__BUZZ_E2E_GET_RELAY_CONNECTION_STATE__;
if (!getState) throw new Error("Relay state seam is not installed.");
return getState();
return getState?.() ?? null;
}),
{ timeout: 10_000 },
)

View File

@@ -169,6 +169,33 @@ async function reveal(page: Page) {
.toBeGreaterThanOrEqual(180);
}
test("project terminal button opens Buzz Term for the repository", async ({
page,
}) => {
await page.setViewportSize({ width: 1280, height: 800 });
await installTerminalBackend(page);
await installMockBridge(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();
const terminalButton = page.getByTestId("project-terminal-toggle");
await expect(terminalButton).toBeEnabled();
await terminalButton.click();
await expect(page.locator(TERM)).toHaveAttribute(
"data-terminal-mode",
"docked",
);
await expect(page.locator(TERM)).toBeVisible();
});
test("scrollback: wheel over Buzz Term reaches terminal_scroll", async ({
page,
}) => {

View File

@@ -32,15 +32,69 @@ async function seedChannelSections(page: Page) {
// the sequence dnd-kit needs to fire onDragEnd and commit the reorder.
async function dragOver(page: Page, source: Locator, target: Locator) {
const from = await source.boundingBox();
const to = await target.boundingBox();
if (!from || !to) throw new Error("drag handles not laid out");
await page.mouse.move(from.x + from.width / 2, from.y + from.height / 2);
await page.mouse.down();
await page.mouse.move(from.x + from.width / 2, from.y + from.height / 2 + 10);
await page.mouse.move(to.x + to.width / 2, to.y + to.height / 2, {
steps: 10,
if (!from) throw new Error("drag source not laid out");
const pointer = {
x: from.x + from.width / 2,
y: from.y + from.height / 2,
};
await source.dispatchEvent("pointerdown", {
button: 0,
buttons: 1,
clientX: pointer.x,
clientY: pointer.y,
isPrimary: true,
pointerId: 1,
pointerType: "mouse",
});
await page.mouse.up();
await page.evaluate(({ x, y }) => {
document.dispatchEvent(
new PointerEvent("pointermove", {
bubbles: true,
buttons: 1,
clientX: x,
clientY: y + 8,
isPrimary: true,
pointerId: 1,
pointerType: "mouse",
}),
);
}, pointer);
await expect(page.getByTestId("sidebar-section-drag-overlay")).toBeVisible();
const to = await target.boundingBox();
if (!to) throw new Error("drag target not laid out");
const destination = {
x: to.x + to.width / 2,
y: to.y + to.height - 2,
};
await page.evaluate(async ({ x, y }) => {
document.dispatchEvent(
new PointerEvent("pointermove", {
bubbles: true,
buttons: 1,
clientX: x,
clientY: y,
isPrimary: true,
pointerId: 1,
pointerType: "mouse",
}),
);
await new Promise<void>((resolve) => {
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
});
document.dispatchEvent(
new PointerEvent("pointerup", {
bubbles: true,
button: 0,
buttons: 0,
clientX: x,
clientY: y,
isPrimary: true,
pointerId: 1,
pointerType: "mouse",
}),
);
}, destination);
}
test.describe("list virtualization", () => {
@@ -160,6 +214,10 @@ test.describe("list virtualization", () => {
const headers = page.locator('[aria-roledescription="sortable"]');
const topHeader = headers.filter({ hasText: "Priority" });
const bottomHeader = headers.filter({ hasText: "Archive" });
const topHeaderButton = topHeader.getByRole("button", {
name: "Priority",
exact: true,
});
await expect(topHeader).toBeVisible();
await expect(bottomHeader).toBeVisible();
await expect(headers).toHaveCount(2);
@@ -176,7 +234,7 @@ test.describe("list virtualization", () => {
// Drag "Priority" past "Archive" — onDragEnd commits arrayMove and persists
// the new order. The drop must land for the order to flip.
await dragOver(page, topHeader, bottomHeader);
await dragOver(page, topHeaderButton, bottomHeader);
// The drop landed: order flipped. A no-op drag would leave it unchanged.
await expect.poll(sectionOrder).toEqual(["Archive", "Priority"]);

View File

@@ -285,6 +285,9 @@ type MockBridgeOptions = {
clearPendingNavigationDeepLinksError?: string;
openDmDelayMs?: number;
sendMessageDelayMs?: number;
/** Delay (ms) for `start_managed_agent` so e2e tests can switch the
* community mid-startup and observe the fail-closed scope check. */
startManagedAgentDelayMs?: number;
/** Hold the media proxy at port 0 until the E2E release seam is invoked. */
mediaProxyInitiallyUnavailable?: boolean;
/** Hold mock send live echoes until the E2E release seam is invoked. */