mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
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:
@@ -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",
|
||||
);
|
||||
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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,
|
||||
}) => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
@@ -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
@@ -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 },
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}) => {
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user