mirror of
https://github.com/block/buzz.git
synced 2026-10-31 06:21:34 +00:00
## Summary Test-side fixes for ten desktop E2E flake clusters and six long-tail rows from the 21–28 Oct 2026 CI audit. Each spec now waits for the state it assumes (a rendered row, a mounted listener, a finished animation, a held boot read), not a fixed delay or a one-shot probe. Two mock-bridge seams support this. The PR changes test code and mock-bridge seams only, apart from one product constant extracted with its value unchanged. It is one of seven PRs split out of the `flakey-tests` branch (siblings below). ### Related issue None found. - #8622, #8244 and #8386 touch some of these spec files, but not the tests fixed here. - §7 mode A (`navigation:472`) is left to open PR #6852, which already sets the width and measures in one polled evaluate. - Draft #8279 queues cold-launch nostr-bind links in the product. Its mock returns null, so the §12 listener seam is still needed. ### Mock-bridge seams - `__BUZZ_E2E_EVENT_LISTENER_COUNT__(event)` counts live **app** listeners per Tauri event. It wraps the mocked `plugin:event|listen`/`unlisten`. The spec-facing `__TAURI_INTERNALS__.listen` passes a marker target that the wrapper skips, so a spec's own subscription can't satisfy the wait. Tauri 2.11.1's mock ignores targets, so delivery is unchanged. - `deferFirstChannelsRead` arms the existing channels-read hold at boot. `__BUZZ_E2E_RELEASE_CHANNELS_READ__` releases it. - `CREATE_PROJECT_REFOCUS_DELAY_MS` moves to `createProjectFormFocus.ts` with its value unchanged (50 ms). Both `CreateProjectFormContent` and `project-commit-detail.spec` import it. ### Flaky tests fixed | Test | Failing runs | Cause | Change | |---|---|---|---| | §2 `overscroll-boundary.spec.ts:34` (and its sibling) | 39601763602, 40367495044, 40583830861, 40597484657, 40373404267 | The `message-timeline` test id first lands on the loading skeleton, which doesn't overflow, so the scroll lock rightly cancels the wheel. | Wait for a rendered row, and content taller than the viewport, before dispatching wheels. | | §4 `project-conversation-load-failure.spec.ts:73` | 40367495044 (all 3 attempts), 40608383847, 40583830861; PRs 39541263507, 40509460168 | The seeded root and reply were dated `now`. The panel's live REQ (`since = floor(now)`) still replayed the reply, so the error card never appeared. | Date both events a minute in the past, and correct the comment. | | §6 `workflows.spec.ts` `createWorkflow` cluster (:222, :365, :297, :525, :471, :406, :614, :997) | 14 of 50 main jobs; :222 ×4, e.g. 40353619362 | `AnimatePresence mode="wait"` keeps the trigger body mounted through its exit, so "Message text" could fill the trigger's field (:222 and the callers). For :297, `isVisible()` read "closed" one frame before the dialog opened its own channel list. | Wait for the step body's "Step details" control before filling. Create dialogs wait for the list; only the duplicate dialog clicks it. | | §12 `nostr-bind.spec.ts` (:156, :198, :228, :247, :260, :335, :417, :447) | 8 main jobs, each test once | The spec emitted `deep-link-nostr-bind` before the app subscribed, and the mock drops events that have no listener. | Wait for `__BUZZ_E2E_EVENT_LISTENER_COUNT__` to report the app listener before emitting. | | §15 `profile.spec.ts:1128` (integration) | 7 of 50 main jobs | The 320 ms motion-sampling window started before the click. | Start the window when `activeTab` reads "runtime". | | §17 `agent-availability.spec.ts:602`, both shutdown-failure variants (integration) | 6 jobs (4 disconnected-online, 2 failed-online) | The "shutdown refused" toast sits over the Delete row and pauses while the pointer is on it, so the retry click is intercepted. | Assert the toast, then retry with focus + Enter. The toaster has no close button. | | §19 `virtualization.spec.ts:247` "08 — cascading older pages…" | 4 of 50 main jobs; PR 40605736093 (all 3 attempts) | The 120 ms and 400 ms windows were timed from before the wheel bursts, and CDP can deliver wheels late. | Count the wheels the page receives. Each window opens at the first wheel and closes only after all have landed, then waits for still frames. **A second, product-side mode remains** (see below). | | §20 `message-copy-link.spec.ts:160` | 4 of 50 main jobs (5 failed attempts), e.g. 40595483966 | The huddle row reused `"d".repeat(64)`, which is a seeded ordinary `#general` message. | Use ids that no seed uses, and emit only after `waitForMockChannelHeadReady`. | | §21 `navigation.spec.ts:687` | 4 of 50 main jobs | The pending chip's `boundingBox()` was read once, and can be null while the row mounts. | Poll until the chip has a width. | | §23 `sidebar-snapshot.spec.ts:358`, `:388`, `:438` | PR 40427822003 (all 3 attempts); never on main | The 500 ms loading-state budget counted app boot, so a slow runner finished the channels read first. | Hold the first channels read with `deferFirstChannelsRead`, assert the loading state, then release it. | | `project-commit-detail.spec.ts:259` | 1–2 CI failures (audit long tail) | A 50 ms refocus timer can land mid-fill and type the description into the name field. | Once the name field is focused, wait out a page timer of `CREATE_PROJECT_REFOCUS_DELAY_MS`; equal-delay timers fire in order. | | `project-pr-review.spec.ts:3520` | 1–2 CI failures | The same timer pattern in `CreateProjectWorkItemDialog`. | Wait for `toBeFocused()` on the title, which only that timer can satisfy. | | `thread-load-failure.spec.ts:79` | 1–2 CI failures | `openWelcomeThread` branched on a one-shot `count()` and fell back to a hover-only Reply click. | Wait for the summary row, then click it. | | `virtualization.spec.ts:201` | 1–2 CI failures | The drop point sat 2 px above the target's bottom edge, after two fixed frames. | Drop at the centre, after dnd-kit's live region names the target. | | `channel-composer-overflow.spec.ts:217` | 1–2 CI failures | Same-second emits sort by id, and the clearance was checked once. | `createdAt: base + i` and `expect.poll`. | | `scroll-history.spec.ts:1289` | 1–2 CI failures | A fixed 250 ms wait before idle sampling. | Poll for coverage first. | Local before/after counts are from the original `flakey-tests` commits (e286a6ae0, 816c7c35e, 67185df6a). Each used a forcing that made `main`'s copy fail with CI's signature: | Test | Before | After | |---|---|---| | §2 | 3/15 | 15/15 | | §4 | 0/10 | 10/10 | | §6 :222 | 0/3 | 3/3 | | §6 :297 | 0/3 | 3/3 | | §12 | 0/3 | 3/3 | | §15 | 0/3 | 3/3 | | §17 | 1/4 | 4/4 | | §19 late-wheel modes | 0/3 | 3/3 and 2/3 | | §23 | 2/6 | 6/6 | | project-commit-detail | 0/3 | 3/3 | | project-pr-review | 0/3 | 3/3 | §20, §21, `thread-load-failure:79`, `virtualization:201`, `channel-composer-overflow:217` and `scroll-history:1289` were not reproduced; they get the fix that matches their CI signature. Not fixed (follow-up): `virtualization:247` also has a product-side mode. In one frame after a prepend, `scrollHeight` can drop 458–639 px while `scrollTop` moves only 50–114 px, so the anchor row jumps 382–457 px. That is lost anchor compensation, and this test can still fail on it. ### Verification This branch was pushed without local re-verification, by the user's choice: local test runs were overloading the machine and failing for that reason. **Hosted CI is the check.** It was pushed with `git push --no-verify`, because the pre-push hook runs typecheck and the unit suites. #8613's flake-check lane will also repeat the changed specs. - Every file except `virtualization.spec.ts` is byte-identical to its final version on `flakey-tests` (67185df6a). That file carries only this PR's hunks; see "Merge order". - Checked for this PR: `git diff --check`, biome on the partial spec (no changes, the same as on `main`), and a conflict-free merge with all six siblings onto `main`. ### Readiness Test and mock-bridge change; the one product edit is a constant extraction with its value unchanged. AGENTS.md's readiness steps (agent review, then human confirmation) are still outstanding, so `buzz-review-completed` is not set. ### Merge order No required order. `desktop/tests/e2e/virtualization.spec.ts` is also edited by **flakey-tests-desktop-races**, which adds one test after line 929; this PR's hunks are all above line 551. They don't touch, so the two merge cleanly in either order, as checked with `git merge-tree`. ### Sibling PRs Split from `flakey-tests` (one PR per area): #8676 (PostgreSQL lease + lane), #8677 (relay E2E helper + Windows shell), #8678 (mobile), #8679 (Tauri apt cache + Windows Rust cap), #8680 (desktop product races), #8682 (desktop E2E composer + row actions). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Jitter <2b2e6415e748c35180847a37aadae06a11e30dddc76b784e7fd9354c4eb42e7a@buzz.block.builderlab.xyz>
1648 lines
63 KiB
TypeScript
1648 lines
63 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
import { CREATE_PROJECT_REFOCUS_DELAY_MS } from "../../src/features/projects/ui/createProjectFormFocus";
|
|
import { waitForAnimations } from "../helpers/animations";
|
|
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.
|
|
async function enableProjectsFeature(page: import("@playwright/test").Page) {
|
|
await page.addInitScript(() => {
|
|
window.localStorage.setItem(
|
|
"buzz-feature-overrides-v1",
|
|
JSON.stringify({ projects: true }),
|
|
);
|
|
});
|
|
}
|
|
|
|
async function openCreateProjectDialog(page: import("@playwright/test").Page) {
|
|
await page.getByTestId("projects-section-projects").click();
|
|
await page.getByTestId("projects-overview-create-project").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();
|
|
await expect(browser).toBeHidden();
|
|
await expect(page.getByTestId(`sidebar-project-${dtag}`)).toBeVisible();
|
|
}
|
|
|
|
async function openProjectRepository(
|
|
page: import("@playwright/test").Page,
|
|
repositoryId: string,
|
|
) {
|
|
await expect(page).toHaveURL(/\/projects\//);
|
|
const target = await page.evaluate((id) => {
|
|
const url = new URL(window.location.href);
|
|
url.searchParams.set("repositoryId", id);
|
|
return `${url.pathname}${url.search}`;
|
|
}, repositoryId);
|
|
await page.goto(target, { waitUntil: "domcontentloaded" });
|
|
}
|
|
|
|
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 show metadata and overflow actions", async ({
|
|
page,
|
|
}) => {
|
|
await enableProjectsFeature(page);
|
|
await page.addInitScript(() => {
|
|
window.localStorage.setItem("buzz.projects.viewMode", "list");
|
|
});
|
|
await installMockBridge(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByTestId("open-projects-view").click();
|
|
await expect(
|
|
page.getByRole("heading", { level: 2, name: "Projects Activity" }),
|
|
).toBeVisible();
|
|
|
|
async function trailingPositions(
|
|
row: import("@playwright/test").Locator,
|
|
{
|
|
actionName = /More options for/,
|
|
dateTestId = "projects-row-date",
|
|
summaryTestId,
|
|
}: {
|
|
actionName?: RegExp;
|
|
dateTestId?: string;
|
|
summaryTestId?: string;
|
|
} = {},
|
|
) {
|
|
await waitForAnimations(page);
|
|
const date = row.getByTestId(dateTestId);
|
|
const menu = row.getByRole("button", { name: actionName });
|
|
await expect(date).toBeVisible();
|
|
await expect(menu).toBeVisible();
|
|
const dateBox = await date.boundingBox();
|
|
const menuBox = await menu.boundingBox();
|
|
const rowBox = await row.boundingBox();
|
|
const summaryBox = summaryTestId
|
|
? await row.getByTestId(summaryTestId).boundingBox()
|
|
: null;
|
|
expect(dateBox).not.toBeNull();
|
|
expect(menuBox).not.toBeNull();
|
|
expect(rowBox).not.toBeNull();
|
|
if (summaryTestId) expect(summaryBox).not.toBeNull();
|
|
return {
|
|
dateX: dateBox?.x ?? 0,
|
|
menuX: menuBox?.x ?? 0,
|
|
rowHeight: rowBox?.height ?? 0,
|
|
summaryX: summaryBox?.x ?? null,
|
|
};
|
|
}
|
|
|
|
await page.getByTestId("projects-section-projects").click();
|
|
await expect(
|
|
page.getByRole("button", { name: "Filter projects" }),
|
|
).toHaveCount(0);
|
|
const projectRow = page.locator('[data-testid^="project-row-"]').first();
|
|
const projectPositions = await trailingPositions(projectRow);
|
|
await expect(projectRow.getByTestId("projects-row-context")).toBeVisible();
|
|
await expect(projectRow.getByTestId("projects-row-people")).toBeVisible();
|
|
|
|
await page.getByTestId("projects-section-repositories").click();
|
|
await expect(
|
|
page.getByRole("button", { name: "Filter repositories" }),
|
|
).toHaveCount(0);
|
|
await expect(page.getByTestId("repository-row-buzz")).toBeVisible();
|
|
await expect(page.getByTestId("repository-row-relay-tools")).toBeVisible();
|
|
const repositoryRow = page.getByTestId("repository-row-buzz");
|
|
await expect(
|
|
repositoryRow.getByTestId("repositories-row-project"),
|
|
).toHaveCount(0);
|
|
const repositoryTitle = repositoryRow.getByTestId("project-entity-title");
|
|
const repositoryDescription = repositoryRow.getByTestId(
|
|
"repositories-row-description",
|
|
);
|
|
await expect(repositoryDescription).toContainText(
|
|
/Relay, desktop, and mobile|community platform/,
|
|
);
|
|
const [repositoryTitleBox, repositoryDescriptionBox] = await Promise.all([
|
|
repositoryTitle.boundingBox(),
|
|
repositoryDescription.boundingBox(),
|
|
]);
|
|
expect(repositoryTitleBox).not.toBeNull();
|
|
expect(repositoryDescriptionBox).not.toBeNull();
|
|
expect(repositoryDescriptionBox?.x ?? 0).toBeGreaterThanOrEqual(
|
|
(repositoryTitleBox?.x ?? 0) + (repositoryTitleBox?.width ?? 0),
|
|
);
|
|
await expect(repositoryDescription).toHaveCSS(
|
|
"font-size",
|
|
await repositoryTitle.evaluate(
|
|
(element) => getComputedStyle(element).fontSize,
|
|
),
|
|
);
|
|
await expect(repositoryDescription).toHaveCSS("text-align", "left");
|
|
const repositoryPositions = await trailingPositions(repositoryRow, {
|
|
actionName: /More options for/,
|
|
dateTestId: "repositories-row-date",
|
|
});
|
|
// Repository and project rows use different middle columns but retain the
|
|
// same compact row height.
|
|
expect(
|
|
Math.abs(repositoryPositions.rowHeight - projectPositions.rowHeight),
|
|
).toBeLessThanOrEqual(ALIGNMENT_TOLERANCE_PX);
|
|
await waitForAnimations(page);
|
|
await page.screenshot({
|
|
path: `${SHOTS}/05-project-repositories-list.png`,
|
|
});
|
|
await page
|
|
.getByTestId("repository-row-relay-tools")
|
|
.getByRole("button", { name: "More options for relay-tools" })
|
|
.click();
|
|
await expect(
|
|
page.getByRole("menuitem", { name: "Clone & open in Terminal" }),
|
|
).toBeVisible();
|
|
await page.keyboard.press("Escape");
|
|
|
|
await page.getByRole("button", { name: "Reviews", exact: true }).click();
|
|
await expect(
|
|
page.getByRole("button", { name: "Filter reviews" }),
|
|
).toHaveCount(0);
|
|
await page.getByTestId("projects-overview-create-pull-request").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.getByRole("button", { name: "Tasks", exact: true }).click();
|
|
await page.getByTestId("projects-overview-create-issue").click();
|
|
await expect(page.getByTestId("create-issue-repository")).toBeVisible();
|
|
await page.keyboard.press("Escape");
|
|
await page.getByRole("button", { name: "Reviews", exact: true }).click();
|
|
const pullRequestRow = page
|
|
.locator('[data-testid^="projects-pr-row-"]')
|
|
.first();
|
|
const pullRequestPositions = await trailingPositions(pullRequestRow);
|
|
await pullRequestRow
|
|
.getByRole("button", { name: /More options for/ })
|
|
.click();
|
|
await expect(
|
|
page.getByRole("menuitem", {
|
|
name: /Open review|View (draft|merge|closed)/,
|
|
}),
|
|
).toBeVisible();
|
|
await page.keyboard.press("Escape");
|
|
|
|
await page.getByRole("button", { name: "Tasks", exact: true }).click();
|
|
await expect(page.getByRole("button", { name: "Filter tasks" })).toHaveCount(
|
|
0,
|
|
);
|
|
const issueRow = page.locator('[data-testid^="projects-issue-row-"]').first();
|
|
await expect(issueRow).toBeVisible();
|
|
const issuePositions = await trailingPositions(issueRow);
|
|
|
|
expect(
|
|
Math.abs(pullRequestPositions.dateX - issuePositions.dateX),
|
|
).toBeLessThanOrEqual(ALIGNMENT_TOLERANCE_PX);
|
|
expect(
|
|
Math.abs(pullRequestPositions.menuX - issuePositions.menuX),
|
|
).toBeLessThanOrEqual(ALIGNMENT_TOLERANCE_PX);
|
|
expect(
|
|
Math.abs(pullRequestPositions.rowHeight - issuePositions.rowHeight),
|
|
).toBeLessThanOrEqual(ALIGNMENT_TOLERANCE_PX);
|
|
await page.setViewportSize({ height: 720, width: 900 });
|
|
await expect(
|
|
page.getByTestId("projects-overview-layout"),
|
|
).not.toHaveAttribute("data-project-context-detached", "true");
|
|
await expect(page.getByTestId("projects-overview-context-rail")).toHaveCSS(
|
|
"width",
|
|
"0px",
|
|
);
|
|
await page.getByTestId("projects-section-projects").click();
|
|
const responsiveRepositoryRow = page
|
|
.locator('[data-testid^="project-row-"]')
|
|
.first();
|
|
await expect(
|
|
responsiveRepositoryRow.getByTestId("projects-row-context"),
|
|
).toBeVisible();
|
|
await expect(
|
|
responsiveRepositoryRow.getByTestId("projects-row-people"),
|
|
).toBeVisible();
|
|
await expect(
|
|
responsiveRepositoryRow.getByTestId("projects-row-date"),
|
|
).toBeVisible();
|
|
await expect(
|
|
responsiveRepositoryRow.getByRole("button", { name: /More options for/ }),
|
|
).toBeVisible();
|
|
expect(
|
|
await responsiveRepositoryRow.evaluate(
|
|
(row) => row.scrollWidth <= row.clientWidth,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("creating a project opens its channel conversation", async ({ page }) => {
|
|
await enableProjectsFeature(page);
|
|
await installMockBridge(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByTestId("open-projects-view").click();
|
|
await openCreateProjectDialog(page);
|
|
// The dialog autofocuses the name field on open and focuses it again from a
|
|
// timer, which can land mid-fill and type the description into the name.
|
|
// Once the field is focused, that timer is set; a page timer of the same
|
|
// delay set now fires after it (equal-delay timers run in order).
|
|
await expect(page.getByTestId("create-project-name")).toBeFocused();
|
|
await page.evaluate(
|
|
(delayMs) => new Promise((resolve) => window.setTimeout(resolve, delayMs)),
|
|
CREATE_PROJECT_REFOCUS_DELAY_MS,
|
|
);
|
|
await page.getByTestId("create-project-name").fill("multi-repo-demo");
|
|
await page
|
|
.getByTestId("create-project-description")
|
|
.fill("A grouped project created through the desktop app.");
|
|
await expect(page.getByTestId("create-project-listing")).toHaveText("Listed");
|
|
await expect(page.getByTestId("create-project-template")).toHaveText(
|
|
"Project home",
|
|
);
|
|
await expect(page.getByTestId("create-project-team")).toHaveText("None");
|
|
await expect(page.getByTestId("create-project-agent")).toHaveText("None");
|
|
await page.getByTestId("create-project-submit").click();
|
|
|
|
await expect(page.getByTestId("create-project-dialog")).toBeHidden();
|
|
await expect(page.getByTestId("project-channel-home")).toBeVisible();
|
|
await expect(page.getByTestId("project-breadcrumb-project")).toHaveText(
|
|
"multi-repo-demo",
|
|
);
|
|
await expect(page.getByTestId("chat-title")).toHaveText("multi-repo-demo");
|
|
await expect(page.getByTestId("project-agent-chat-panel")).toHaveCount(0);
|
|
await expect(page.getByTestId("message-channel-intro")).toBeVisible();
|
|
await expect(page.getByTestId("message-channel-intro")).not.toContainText(
|
|
"This is the beginning",
|
|
);
|
|
await expect(
|
|
page
|
|
.getByTestId("message-channel-intro-icon")
|
|
.getByTestId("project-channel-icon"),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("chat-header").getByTestId("project-channel-icon"),
|
|
).toBeVisible();
|
|
await expect(
|
|
page
|
|
.getByTestId("channel-multi-repo-demo")
|
|
.getByTestId("project-channel-icon"),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("channel-intro-action-add-files"),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("channel-intro-action-add-files-title"),
|
|
).toHaveText("Add files");
|
|
await page.getByTestId("channel-intro-action-add-files").click();
|
|
await expect(page.getByTestId("add-project-repository-dialog")).toBeVisible();
|
|
await page.keyboard.press("Escape");
|
|
await expect(page.getByTestId("add-project-repository-dialog")).toBeHidden();
|
|
await expect(page.getByTestId("project-home-summary-column")).toBeVisible();
|
|
await expect(
|
|
page
|
|
.getByTestId("project-home-summary-column")
|
|
.getByRole("heading", { name: "Overview", exact: true }),
|
|
).toHaveCount(0);
|
|
await expect(
|
|
page
|
|
.getByTestId("project-home-summary-column")
|
|
.getByTestId("auxiliary-panel-close"),
|
|
).toHaveCount(0);
|
|
await expect(page.getByTestId("project-channel-home")).toHaveAttribute(
|
|
"data-project-context-detached",
|
|
"true",
|
|
);
|
|
await expect(page.getByTestId("project-home-summary-rail-panel")).toHaveCSS(
|
|
"border-radius",
|
|
"0px",
|
|
);
|
|
await expect(page.getByTestId("project-home-context-panel")).toBeVisible();
|
|
await expect(page.getByTestId("project-home-context-about")).toHaveCount(0);
|
|
await expect(
|
|
page.getByTestId("project-home-context-home-channel"),
|
|
).toContainText("multi-repo-demo");
|
|
await expect(
|
|
page
|
|
.getByTestId("project-home-context-home-channel")
|
|
.getByTestId("project-channel-icon"),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("project-home-context-home-channel"),
|
|
).not.toContainText("#multi-repo-demo");
|
|
await expect(
|
|
page.getByTestId("project-home-context-channel"),
|
|
).not.toContainText("people in this channel");
|
|
const channelSection = page.getByTestId("project-home-context-channel");
|
|
const channelSectionToggle = channelSection.getByRole("button", {
|
|
name: "Channels",
|
|
exact: true,
|
|
});
|
|
await channelSectionToggle.click();
|
|
await expect(
|
|
channelSection.getByTestId("project-home-context-home-channel"),
|
|
).toHaveCount(0);
|
|
await channelSectionToggle.click();
|
|
await expect(
|
|
channelSection.getByTestId("project-home-context-home-channel"),
|
|
).toBeVisible();
|
|
const codebaseSection = page.getByTestId("project-home-context-codebase");
|
|
const codebaseSectionToggle = codebaseSection.getByRole("button", {
|
|
name: "Codebase",
|
|
exact: true,
|
|
});
|
|
await codebaseSectionToggle.click();
|
|
await expect(
|
|
codebaseSection.getByTestId("project-home-context-repo-multi-repo-demo"),
|
|
).toHaveCount(0);
|
|
await codebaseSectionToggle.click();
|
|
const channelAction = page.getByTestId("add-project-channel").locator("..");
|
|
const repositoryAction = page
|
|
.getByTestId("add-project-repository")
|
|
.locator("..");
|
|
await page.evaluate(() => {
|
|
if (document.activeElement instanceof HTMLElement) {
|
|
document.activeElement.blur();
|
|
}
|
|
});
|
|
await page.mouse.move(1, 1);
|
|
await expect(channelAction).toHaveCSS("opacity", "0");
|
|
await expect(repositoryAction).toHaveCSS("opacity", "0");
|
|
await page.getByTestId("project-home-context-channel").hover();
|
|
await expect(channelAction).toHaveCSS("opacity", "1");
|
|
await page.getByTestId("project-home-context-codebase").hover();
|
|
await expect(repositoryAction).toHaveCSS("opacity", "1");
|
|
await page.getByTestId("project-home-context-channel").hover();
|
|
await page.getByTestId("add-project-channel").click();
|
|
await expect(page.getByTestId("create-project-channel-dialog")).toBeVisible();
|
|
await page.keyboard.press("Escape");
|
|
await expect(page.getByTestId("create-project-channel-dialog")).toBeHidden();
|
|
await expect(
|
|
page.getByTestId("sidebar-project-multi-repo-demo"),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("sidebar-project-home-channel-multi-repo-demo"),
|
|
).toHaveCount(0);
|
|
await expect(
|
|
page.getByTestId("sidebar-project-expand-multi-repo-demo"),
|
|
).toHaveCount(0);
|
|
await expect(page.getByTestId("project-home-context-codebase")).toContainText(
|
|
"multi-repo-demo",
|
|
);
|
|
await expect(
|
|
page.getByTestId("project-home-context-workspace"),
|
|
).toBeVisible();
|
|
await expect(
|
|
page
|
|
.getByTestId("project-home-context-workspace")
|
|
.getByRole("heading", { name: "Workspace" }),
|
|
).toHaveCount(0);
|
|
await expect(page.getByTestId("project-home-context-tasks")).toBeEnabled();
|
|
await expect(page.getByTestId("project-home-context-people")).toContainText(
|
|
"1",
|
|
);
|
|
await expect(page.getByTestId("project-home-drawer-toggle")).toHaveAttribute(
|
|
"aria-pressed",
|
|
"true",
|
|
);
|
|
await page.getByTestId("project-home-drawer-toggle").click();
|
|
await expect(page.getByTestId("project-home-summary-column")).toHaveCount(0);
|
|
await expect(page.getByTestId("project-home-drawer-toggle")).toHaveAttribute(
|
|
"aria-pressed",
|
|
"false",
|
|
);
|
|
await page.getByTestId("project-home-drawer-toggle").click();
|
|
await expect(page.getByTestId("project-home-summary-column")).toBeVisible();
|
|
await page.getByTestId("channel-management-trigger").click();
|
|
await expect(page.getByTestId("channel-management-type")).toContainText(
|
|
"Project",
|
|
);
|
|
await page
|
|
.getByTestId("channel-management-sheet")
|
|
.getByTestId("auxiliary-panel-close")
|
|
.click();
|
|
await expect(page.getByTestId("channel-management-sheet")).toHaveCount(0);
|
|
await page.getByTestId("project-home-context-files").click();
|
|
await expect(page.getByTestId("project-home-workspace-sheet")).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("project-home-workspace-sheet"),
|
|
).toHaveAttribute("data-tab", "files");
|
|
await expect(page.getByTestId("focus-thread-drawer")).toBeVisible();
|
|
await expect(page.getByTestId("project-home-summary-column")).toHaveCount(0);
|
|
await page
|
|
.getByTestId("focus-thread-drawer")
|
|
.getByTestId("auxiliary-panel-close")
|
|
.click();
|
|
await expect(page.getByTestId("project-home-workspace-sheet")).toHaveCount(0);
|
|
await expect(page.getByTestId("project-home-summary-column")).toBeVisible();
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page.getByTestId("project-channel-home")).toHaveCount(0);
|
|
await page.getByTestId("channel-multi-repo-demo").click();
|
|
await expect(page.getByTestId("project-channel-home")).toBeVisible();
|
|
await expect(page.getByTestId("project-home-summary-column")).toBeVisible();
|
|
|
|
const createdEvents = await page.evaluate(
|
|
() =>
|
|
window.__BUZZ_E2E_ACCEPTED_PROJECT_EVENTS__?.filter((event) =>
|
|
event.tags.some(
|
|
(tag) => tag[0] === "d" && tag[1] === "multi-repo-demo",
|
|
),
|
|
) ?? [],
|
|
);
|
|
expect(createdEvents.map((event) => event.kind)).toEqual([30621, 30617]);
|
|
const projectEvent = createdEvents.find((event) => event.kind === 30621);
|
|
expect(projectEvent?.content).toBe("");
|
|
expect(projectEvent?.tags.some((tag) => tag[0] === "a")).toBe(true);
|
|
expect(
|
|
projectEvent?.tags.find((tag) => tag[0] === "buzz-channel")?.[1],
|
|
).toMatch(
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
);
|
|
|
|
await page
|
|
.getByTestId("project-detail-chrome")
|
|
.getByRole("button", { name: "Projects" })
|
|
.click();
|
|
await openCreateProjectDialog(page);
|
|
await page.getByTestId("create-project-name").fill("multi-repo-demo");
|
|
await page.getByTestId("create-project-submit").click();
|
|
await expect(page.getByTestId("create-project-dialog")).toBeVisible();
|
|
await expect(
|
|
page.getByText('You already have a project named "multi-repo-demo".'),
|
|
).toBeVisible();
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(
|
|
() =>
|
|
window.__BUZZ_E2E_ACCEPTED_PROJECT_EVENTS__?.filter((event) =>
|
|
event.tags.some(
|
|
(tag) => tag[0] === "d" && tag[1] === "multi-repo-demo",
|
|
),
|
|
).length ?? 0,
|
|
),
|
|
)
|
|
.toBe(2);
|
|
});
|
|
|
|
test("unsupported relays cannot create a channel-first project", async ({
|
|
page,
|
|
}) => {
|
|
await enableProjectsFeature(page);
|
|
await page.addInitScript(() => {
|
|
window.__BUZZ_E2E_UNSUPPORTED_PROJECT_ANNOUNCEMENTS__ = true;
|
|
});
|
|
await installMockBridge(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByTestId("open-projects-view").click();
|
|
await openCreateProjectDialog(page);
|
|
await page.getByTestId("create-project-name").fill("legacy-fallback");
|
|
await page.getByTestId("create-project-submit").click();
|
|
|
|
await expect(page.getByTestId("create-project-dialog")).toBeVisible();
|
|
await expect(
|
|
page.getByText("This relay does not support projects yet"),
|
|
).toBeVisible();
|
|
|
|
const acceptedKinds = await page.evaluate(
|
|
() =>
|
|
window.__BUZZ_E2E_ACCEPTED_PROJECT_EVENTS__
|
|
?.filter((event) =>
|
|
event.tags.some(
|
|
(tag) => tag[0] === "d" && tag[1] === "legacy-fallback",
|
|
),
|
|
)
|
|
.map((event) => event.kind) ?? [],
|
|
);
|
|
expect(acceptedKinds).toEqual([]);
|
|
});
|
|
|
|
test("project creation can retry after its repository publication fails", async ({
|
|
page,
|
|
}) => {
|
|
await enableProjectsFeature(page);
|
|
await page.addInitScript(() => {
|
|
window.__BUZZ_E2E_REJECT_PROJECT_EVENT_KINDS__ = [30621];
|
|
});
|
|
await installMockBridge(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByTestId("open-projects-view").click();
|
|
await openCreateProjectDialog(page);
|
|
await page.getByTestId("create-project-name").fill("retry-project");
|
|
await page.getByTestId("create-project-submit").click();
|
|
|
|
await expect(page.getByTestId("create-project-dialog")).toBeVisible();
|
|
await expect(page.getByText("mock project event rejection")).toBeVisible();
|
|
|
|
await page.getByTestId("create-project-submit").click();
|
|
await expect(page.getByTestId("create-project-dialog")).toBeHidden();
|
|
await expect(page.getByTestId("project-channel-home")).toBeVisible();
|
|
await expect(page.getByTestId("project-breadcrumb-project")).toHaveText(
|
|
"retry-project",
|
|
);
|
|
});
|
|
|
|
test("project creation is idempotent after a lost publish acknowledgement", async ({
|
|
page,
|
|
}) => {
|
|
await enableProjectsFeature(page);
|
|
await page.addInitScript(() => {
|
|
window.__BUZZ_E2E_FAIL_PROJECT_EVENT_ACK_KINDS__ = [30621];
|
|
});
|
|
await installMockBridge(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByTestId("open-projects-view").click();
|
|
await openCreateProjectDialog(page);
|
|
await page.getByTestId("create-project-name").fill("lost-ack-project");
|
|
await page.getByTestId("create-project-submit").click();
|
|
|
|
await expect(page.getByTestId("create-project-dialog")).toBeVisible();
|
|
await expect(
|
|
page.getByText("mock lost project acknowledgement"),
|
|
).toBeVisible();
|
|
|
|
await page.getByTestId("create-project-submit").click();
|
|
await expect(page.getByTestId("create-project-dialog")).toBeHidden();
|
|
await expect(page.getByTestId("project-channel-home")).toBeVisible();
|
|
await expect(page.getByTestId("project-breadcrumb-project")).toHaveText(
|
|
"lost-ack-project",
|
|
);
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(
|
|
() =>
|
|
window.__BUZZ_E2E_ACCEPTED_PROJECT_EVENTS__?.filter((event) =>
|
|
event.tags.some(
|
|
(tag) => tag[0] === "d" && tag[1] === "lost-ack-project",
|
|
),
|
|
).length ?? 0,
|
|
),
|
|
)
|
|
.toBe(2);
|
|
});
|
|
|
|
test("project sidebar rows open the home channel and nest extra channels", async ({
|
|
page,
|
|
}) => {
|
|
await enableProjectsFeature(page);
|
|
await installMockBridge(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await addProjectToSidebar(page, "buzz");
|
|
|
|
const projectRow = page.getByTestId("sidebar-project-buzz");
|
|
const expand = page.getByTestId("sidebar-project-expand-buzz");
|
|
const nestedChannel = page.getByTestId("sidebar-project-channel-buzz-random");
|
|
await expect(page).toHaveURL(/\/projects\//);
|
|
await expect(page.getByTestId("project-channel-home")).toBeVisible();
|
|
await expect(projectRow).toHaveAttribute("data-active", "true");
|
|
await expect(expand).toHaveAttribute("aria-expanded", "false");
|
|
await expect(nestedChannel).toBeHidden();
|
|
await expect(page.getByTestId("sidebar-project-repository-buzz")).toHaveCount(
|
|
0,
|
|
);
|
|
await expect(
|
|
page.getByTestId("sidebar-project-home-channel-buzz"),
|
|
).toHaveCount(0);
|
|
|
|
await expand.click();
|
|
await expect(expand).toHaveAttribute("aria-expanded", "true");
|
|
await expect(nestedChannel).toBeVisible();
|
|
await expect(page).toHaveURL(/\/projects\//);
|
|
|
|
await page.reload({ waitUntil: "domcontentloaded" });
|
|
await addProjectToSidebar(page, "buzz");
|
|
await expect(expand).toHaveAttribute("aria-expanded", "true");
|
|
await expect(nestedChannel).toBeVisible();
|
|
|
|
await nestedChannel.click();
|
|
await expect(page).toHaveURL(/\/channels\//);
|
|
await expect(page.getByTestId("chat-title")).toHaveText("random");
|
|
await expect(nestedChannel).toHaveAttribute("data-active", "true");
|
|
await expect(projectRow).toHaveAttribute("data-active", "false");
|
|
|
|
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 expand.click();
|
|
await expect(expand).toHaveAttribute("aria-expanded", "false");
|
|
await expect(nestedChannel).toBeHidden();
|
|
await expect(page).toHaveURL(/\/channels\//);
|
|
|
|
await expand.click();
|
|
await expect(expand).toHaveAttribute("aria-expanded", "true");
|
|
await expect(nestedChannel).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(page).toHaveURL(/\/projects\//);
|
|
await expect(projectRow).toHaveAttribute("data-active", "true");
|
|
await waitForAnimations(page);
|
|
await page.screenshot({
|
|
path: `${SHOTS}/04-multi-repository-picker.png`,
|
|
});
|
|
|
|
await openProjectRepository(
|
|
page,
|
|
`${TEST_IDENTITIES.alice.pubkey}:relay-tools`,
|
|
);
|
|
await expect(page).toHaveURL(
|
|
new RegExp(`repositoryId=${TEST_IDENTITIES.alice.pubkey}%3Arelay-tools`),
|
|
);
|
|
|
|
await page.getByTestId("sidebar-project-buzz").click();
|
|
await expect(page.getByTestId("project-home-context-panel")).toBeVisible();
|
|
await page.getByTestId("add-project-repository").click();
|
|
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(
|
|
page.getByTestId("project-home-context-repo-mobile-app"),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("sidebar-project-repository-mobile-app"),
|
|
).toHaveCount(0);
|
|
const addedEvents = await page.evaluate(
|
|
() =>
|
|
window.__BUZZ_E2E_ACCEPTED_PROJECT_EVENTS__?.filter(
|
|
(event) =>
|
|
event.tags.some((tag) => tag[0] === "d" && tag[1] === "mobile-app") ||
|
|
event.tags.some(
|
|
(tag) =>
|
|
tag[0] === "a" &&
|
|
tag[1]?.endsWith(":mobile-app") &&
|
|
event.kind === 30621,
|
|
),
|
|
) ?? [],
|
|
);
|
|
expect(addedEvents.map((event) => event.kind)).toEqual([30621, 30617]);
|
|
expect(
|
|
addedEvents.find((event) => event.kind === 30617)?.tags,
|
|
).toContainEqual(["buzz-channel", "cf63feec-21bb-5bf0-a2f8-0e4c3de8ec73"]);
|
|
|
|
await page.getByTestId("add-project-repository").click();
|
|
await page.getByTestId("attach-project-repository").click();
|
|
await expect(
|
|
page.getByTestId("attach-project-repository-dialog"),
|
|
).toBeVisible();
|
|
await page.getByTestId("attach-existing-repository-design-system").click();
|
|
await expect(
|
|
page.getByTestId("attach-project-repository-dialog"),
|
|
).toBeHidden();
|
|
await expect(
|
|
page.getByTestId("project-home-context-repo-design-system"),
|
|
).toBeVisible();
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(
|
|
() =>
|
|
window.__BUZZ_E2E_ACCEPTED_PROJECT_EVENTS__?.some(
|
|
(event) =>
|
|
event.kind === 30621 &&
|
|
event.tags.some(
|
|
(tag) => tag[0] === "a" && tag[1]?.endsWith(":design-system"),
|
|
),
|
|
) ?? false,
|
|
),
|
|
)
|
|
.toBe(true);
|
|
});
|
|
|
|
test("latest files commit opens its detail without a divider", async ({
|
|
page,
|
|
}) => {
|
|
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();
|
|
const projectEntry = page
|
|
.locator(
|
|
'[data-testid="project-card-buzz"], [data-testid="project-row-buzz"]',
|
|
)
|
|
.first();
|
|
await expect(projectEntry).toBeVisible({ timeout: 10_000 });
|
|
await projectEntry.click();
|
|
await page.getByTestId("project-home-context-repo-buzz").click();
|
|
await page.getByRole("tab", { name: "Files" }).click();
|
|
|
|
const latestCommit = page.getByTestId("project-repository-latest-commit");
|
|
await expect(latestCommit).toBeVisible();
|
|
await expect(latestCommit).toHaveCSS("border-bottom-width", "0px");
|
|
await expect(
|
|
page.getByTestId("project-repository-latest-commit-summary"),
|
|
).toHaveCSS("font-size", "12px");
|
|
await expect(
|
|
page.getByTestId("project-repository-entry-row").first(),
|
|
).toHaveCSS("font-size", "12px");
|
|
const repositoryEntryRow = page
|
|
.getByTestId("project-repository-entry-row")
|
|
.first();
|
|
const repositoryEntryCells = repositoryEntryRow.locator("td");
|
|
await expect(repositoryEntryCells.first()).toHaveCSS("border-radius", "0px");
|
|
await repositoryEntryRow.hover();
|
|
await expect(repositoryEntryCells.first()).toHaveCSS(
|
|
"border-top-left-radius",
|
|
"8px",
|
|
);
|
|
await expect(repositoryEntryCells.last()).toHaveCSS(
|
|
"border-top-right-radius",
|
|
"8px",
|
|
);
|
|
await latestCommit.click();
|
|
await expect(page.getByTestId("project-commit-detail")).toBeVisible();
|
|
});
|
|
|
|
test("project workspace sheet stays independent from an open thread", async ({
|
|
page,
|
|
}) => {
|
|
await enableProjectsFeature(page);
|
|
await installMockBridge(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByTestId("open-projects-view").click();
|
|
await openCreateProjectDialog(page);
|
|
await page.getByTestId("create-project-name").fill("sheet-motion-demo");
|
|
await page.getByTestId("create-project-submit").click();
|
|
await expect(page.getByTestId("project-channel-home")).toBeVisible();
|
|
await page.setViewportSize({ height: 720, width: 820 });
|
|
|
|
const summaryColumn = page.getByTestId("project-home-summary-column");
|
|
const resizeHandle = summaryColumn.getByTestId(
|
|
"right-auxiliary-pane-resize-handle",
|
|
);
|
|
await expect(resizeHandle).toBeVisible();
|
|
await resizeHandle.hover();
|
|
await expect(
|
|
resizeHandle.getByTestId("right-auxiliary-pane-resize-indicator"),
|
|
).toHaveCount(0);
|
|
const summaryRail = page.getByTestId("project-home-summary-rail");
|
|
const openRailWidth = await summaryRail.evaluate(
|
|
(element) => element.getBoundingClientRect().width,
|
|
);
|
|
expect(openRailWidth).toBeGreaterThan(0);
|
|
await page.getByTestId("project-home-context-tasks").click();
|
|
await expect(page.getByTestId("project-home-workspace-sheet")).toBeVisible();
|
|
|
|
const collapsedRailWidth = await summaryRail.evaluate(
|
|
(element) => element.getBoundingClientRect().width,
|
|
);
|
|
expect(collapsedRailWidth).toBeLessThanOrEqual(1);
|
|
const focusDrawer = page.getByTestId("focus-thread-drawer");
|
|
const enteringDrawerWidth = (await focusDrawer.boundingBox())?.width ?? 0;
|
|
expect(enteringDrawerWidth).toBeGreaterThan(0);
|
|
await waitForAnimations(page);
|
|
const settledDrawerWidth = (await focusDrawer.boundingBox())?.width ?? 0;
|
|
expect(
|
|
Math.abs(settledDrawerWidth - enteringDrawerWidth),
|
|
).toBeLessThanOrEqual(1);
|
|
|
|
await focusDrawer.getByTestId("auxiliary-panel-close").click();
|
|
await expect(page.getByTestId("project-home-workspace-sheet")).toHaveCount(0);
|
|
await expect(page.getByTestId("project-home-summary-column")).toBeVisible();
|
|
await waitForMockLiveSubscription(page, "sheet-motion-demo");
|
|
const threadRootContent = "Workspace drawer thread root";
|
|
await page.evaluate((content) => {
|
|
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
|
channelName: "sheet-motion-demo",
|
|
content,
|
|
});
|
|
}, threadRootContent);
|
|
const threadRoot = page
|
|
.getByTestId("message-timeline")
|
|
.getByTestId("message-row")
|
|
.filter({ hasText: threadRootContent });
|
|
await expect(threadRoot).toBeVisible();
|
|
await threadRoot.hover();
|
|
await threadRoot.getByRole("button", { name: "Reply" }).click();
|
|
await expect(page.getByTestId("message-thread-panel")).toBeVisible();
|
|
|
|
await page.getByTestId("project-home-context-tasks").click();
|
|
await expect(page.getByTestId("project-home-workspace-sheet")).toBeVisible();
|
|
const workspaceDrawer = page.getByTestId("focus-thread-drawer");
|
|
await expect(workspaceDrawer).toHaveCount(1);
|
|
await expect(
|
|
workspaceDrawer.getByTestId("project-home-workspace-sheet"),
|
|
).toBeVisible();
|
|
await expect(page.getByTestId("message-thread-panel")).toHaveCount(1);
|
|
await expect(workspaceDrawer.getByTestId("message-thread-panel")).toHaveCount(
|
|
0,
|
|
);
|
|
const coveredThreadSurface = page.getByTestId("thread-surface");
|
|
await expect(coveredThreadSurface).toHaveAttribute("inert", "");
|
|
await expect(coveredThreadSurface).toHaveAttribute("aria-hidden", "true");
|
|
const coveredSnapshot = await page.locator("body").ariaSnapshot();
|
|
expect(coveredSnapshot).not.toContain(threadRootContent);
|
|
expect(coveredSnapshot).toContain("Tasks");
|
|
expect(coveredSnapshot).toContain("Close panel");
|
|
|
|
const workspaceClose = workspaceDrawer.getByTestId("auxiliary-panel-close");
|
|
await workspaceClose.focus();
|
|
await expect(workspaceClose).toBeFocused();
|
|
for (let index = 0; index < 8; index += 1) {
|
|
await page.keyboard.press("Tab");
|
|
expect(
|
|
await page.evaluate(
|
|
() =>
|
|
document.activeElement?.closest('[data-testid="thread-surface"]') !==
|
|
null,
|
|
),
|
|
).toBe(false);
|
|
}
|
|
|
|
await workspaceClose.click();
|
|
const exitingWorkspaceState = await page.evaluate(() => {
|
|
const workspaceSheet = document.querySelector(
|
|
'[data-testid="project-home-workspace-sheet"]',
|
|
);
|
|
const threadSurface = document.querySelector(
|
|
'[data-testid="thread-surface"]',
|
|
);
|
|
return {
|
|
threadAriaHidden: threadSurface?.getAttribute("aria-hidden"),
|
|
threadInert: threadSurface?.hasAttribute("inert") ?? false,
|
|
workspaceSheetMounted: workspaceSheet !== null,
|
|
};
|
|
});
|
|
expect(exitingWorkspaceState).toEqual({
|
|
threadAriaHidden: "true",
|
|
threadInert: true,
|
|
workspaceSheetMounted: true,
|
|
});
|
|
await expect(page.getByTestId("project-home-workspace-sheet")).toHaveCount(0);
|
|
await expect(page.getByTestId("message-thread-panel")).toBeVisible();
|
|
const threadClose = coveredThreadSurface.getByTestId("auxiliary-panel-close");
|
|
await expect(threadClose).toBeFocused();
|
|
await expect(coveredThreadSurface).not.toHaveAttribute("inert", "");
|
|
await expect(coveredThreadSurface).not.toHaveAttribute("aria-hidden", "true");
|
|
|
|
await page.evaluate(() => {
|
|
document.documentElement.style.fontSize = "140%";
|
|
});
|
|
await page.getByTestId("project-home-context-tasks").click();
|
|
await expect(page.getByTestId("project-home-workspace-sheet")).toBeVisible();
|
|
const enlargedTextWorkspaceClose = page
|
|
.getByTestId("focus-thread-drawer")
|
|
.getByTestId("auxiliary-panel-close");
|
|
await expect
|
|
.poll(() =>
|
|
enlargedTextWorkspaceClose.evaluate((element) => {
|
|
const bounds = element.getBoundingClientRect();
|
|
return {
|
|
leftInsideViewport: bounds.left >= 0,
|
|
rightInsideViewport: bounds.right <= window.innerWidth,
|
|
viewportWidth: window.innerWidth,
|
|
};
|
|
}),
|
|
)
|
|
.toEqual({
|
|
leftInsideViewport: true,
|
|
rightInsideViewport: true,
|
|
viewportWidth: 820,
|
|
});
|
|
await enlargedTextWorkspaceClose.click();
|
|
await expect(page.getByTestId("project-home-workspace-sheet")).toHaveCount(0);
|
|
await expect(page.getByTestId("message-thread-panel")).toBeVisible();
|
|
await page.evaluate(() => {
|
|
document.documentElement.style.removeProperty("font-size");
|
|
});
|
|
|
|
await page.setViewportSize({ height: 1080, width: 1920 });
|
|
const splitThreadPane = page
|
|
.locator(
|
|
'[data-testid="message-thread-panel"]:has([data-testid="right-auxiliary-pane-resize-handle"])',
|
|
)
|
|
.first();
|
|
await expect(splitThreadPane).toBeVisible();
|
|
const threadResizeHandle = splitThreadPane.getByTestId(
|
|
"right-auxiliary-pane-resize-handle",
|
|
);
|
|
const threadResizeHandleBox = await threadResizeHandle.boundingBox();
|
|
expect(threadResizeHandleBox).not.toBeNull();
|
|
|
|
await page.getByTestId("project-home-context-tasks").click();
|
|
await expect(page.getByTestId("project-home-workspace-sheet")).toBeVisible();
|
|
await waitForAnimations(page);
|
|
const workspaceCoversThreadDivider = await page.evaluate(
|
|
({ x, y }) => {
|
|
return Boolean(
|
|
document
|
|
.elementFromPoint(x, y)
|
|
?.closest('[data-testid="focus-thread-drawer"]'),
|
|
);
|
|
},
|
|
{
|
|
x:
|
|
(threadResizeHandleBox?.x ?? 0) +
|
|
(threadResizeHandleBox?.width ?? 0) / 2,
|
|
y: (threadResizeHandleBox?.y ?? 0) + 40,
|
|
},
|
|
);
|
|
expect(workspaceCoversThreadDivider).toBe(true);
|
|
await page.keyboard.press("Escape");
|
|
await expect(page.getByTestId("project-home-workspace-sheet")).toHaveCount(0);
|
|
await expect(splitThreadPane).toBeVisible();
|
|
await expect(threadClose).toBeFocused();
|
|
});
|
|
|
|
test("commit detail opens from the commits feed with a diff", async ({
|
|
page,
|
|
}) => {
|
|
await enableProjectsFeature(page);
|
|
await installMockBridge(page);
|
|
// The preview server is a static file server without SPA fallback, so
|
|
// enter at "/" and navigate via the sidebar.
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByTestId("open-projects-view").click();
|
|
|
|
// The overview no longer lists repository cards — switch to the
|
|
// Projects filter reveals the complete project cards/rows list.
|
|
await page.getByTestId("projects-section-projects").click();
|
|
|
|
// Open the first mock project (dtag "buzz" from the e2e bridge fixture).
|
|
const projectEntry = page
|
|
.locator(
|
|
'[data-testid="project-card-buzz"], [data-testid="project-row-buzz"]',
|
|
)
|
|
.first();
|
|
await expect(projectEntry).toBeVisible({ timeout: 10_000 });
|
|
await projectEntry.click();
|
|
await page.getByTestId("project-home-context-repo-buzz").click();
|
|
await page.getByTestId("project-workspace-back").click();
|
|
await page.getByTestId("project-home-context-tasks").click();
|
|
await expect(page.getByTestId("project-home-workspace-sheet")).toBeVisible();
|
|
const focusDrawer = page.getByTestId("focus-thread-drawer");
|
|
await expect(focusDrawer).toHaveCount(1);
|
|
await expect(focusDrawer).toHaveCSS("outline-style", "none");
|
|
const workspaceSheet = page.getByTestId("project-home-workspace-sheet");
|
|
await expect(workspaceSheet).toHaveAttribute("data-tab", "issues");
|
|
await expect(
|
|
page.getByTestId("project-home-workspace-sheet-expand"),
|
|
).toBeVisible();
|
|
await expect(
|
|
workspaceSheet.getByTestId("project-section-header"),
|
|
).toHaveCount(0);
|
|
await expect(
|
|
page.getByTestId("project-home-workspace-sheet-create"),
|
|
).toBeVisible();
|
|
const sheetGroup = workspaceSheet
|
|
.getByTestId("project-work-item-group-header")
|
|
.first();
|
|
const sheetRow = workspaceSheet.getByTestId("project-issue-row").first();
|
|
await expect(sheetGroup).toBeVisible();
|
|
await expect(sheetRow).toBeVisible();
|
|
const [sheetGroupBox, sheetGroupIconBox, sheetRowBox, sheetRowIconBox] =
|
|
await Promise.all([
|
|
sheetGroup.boundingBox(),
|
|
sheetGroup.getByTestId("project-group-icon").boundingBox(),
|
|
sheetRow.boundingBox(),
|
|
sheetRow.getByTestId("project-work-item-status-icon").boundingBox(),
|
|
]);
|
|
expect(sheetGroupBox).not.toBeNull();
|
|
expect(
|
|
Math.abs((sheetGroupIconBox?.x ?? 0) - (sheetRowIconBox?.x ?? 0)),
|
|
).toBeLessThanOrEqual(2);
|
|
expect(Math.round((sheetRowBox?.x ?? 0) - (sheetGroupBox?.x ?? 0))).toBe(8);
|
|
await page.getByTestId("project-home-workspace-sheet-create").click();
|
|
await expect(page.getByTestId("create-issue-dialog")).toBeVisible();
|
|
await page
|
|
.getByTestId("create-issue-dialog")
|
|
.getByRole("button", { name: "Close" })
|
|
.click();
|
|
await expect(page.getByTestId("create-issue-dialog")).toHaveCount(0);
|
|
await sheetRow.locator('[data-projects-text-priority="primary"]').click();
|
|
await expect(
|
|
workspaceSheet.getByTestId("project-issue-detail"),
|
|
).toBeVisible();
|
|
await expect(page.getByTestId("idle-auxiliary-back")).toHaveAttribute(
|
|
"aria-label",
|
|
"Back to Tasks",
|
|
);
|
|
await expect(
|
|
page.getByTestId("project-home-workspace-sheet-create"),
|
|
).toHaveCount(0);
|
|
await page.getByTestId("idle-auxiliary-back").click();
|
|
await expect(sheetRow).toBeVisible();
|
|
await expect(workspaceSheet.getByTestId("project-issue-detail")).toHaveCount(
|
|
0,
|
|
);
|
|
await expect(page.getByTestId("focus-thread-drawer")).toBeVisible();
|
|
await expect(page.getByTestId("project-channel-home")).toBeVisible();
|
|
await expect(page.getByTestId("project-home-summary-column")).toHaveCount(0);
|
|
await expect(page.getByTestId("project-workspace-back")).toHaveCount(0);
|
|
await page
|
|
.getByTestId("focus-thread-drawer")
|
|
.getByTestId("auxiliary-panel-close")
|
|
.click();
|
|
await expect(page.getByTestId("project-home-workspace-sheet")).toHaveCount(0);
|
|
await expect(page.getByTestId("project-home-summary-column")).toBeVisible();
|
|
await page.getByTestId("project-home-context-repo-buzz").click();
|
|
await expect(page.getByTestId("app-sidebar")).toBeVisible();
|
|
await expect(page.getByTestId("project-workspace-back")).toBeVisible();
|
|
await page.getByTestId("project-workspace-back").click();
|
|
await expect(page.getByTestId("project-channel-home")).toBeVisible();
|
|
await expect(page.getByTestId("app-sidebar")).toBeVisible();
|
|
await page.getByTestId("project-home-context-repo-buzz").click();
|
|
await expect(page.getByTestId("project-workspace-back")).toBeVisible();
|
|
|
|
await page.getByRole("tab", { name: "Commits" }).click();
|
|
const commitRows = page.getByTestId("project-activity-feed-item");
|
|
await expect(commitRows.first()).toBeVisible({ timeout: 10_000 });
|
|
|
|
// 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\/55/);
|
|
await waitForAnimations(page);
|
|
await page.screenshot({
|
|
fullPage: false,
|
|
path: `${SHOTS}/02-commits-feed.png`,
|
|
});
|
|
|
|
// Open the newest commit via its subject button.
|
|
await commitRows
|
|
.first()
|
|
.getByRole("button", { name: /Add Trello board workflow details/ })
|
|
.click();
|
|
|
|
// Detail header: static descriptor, subject, and hash.
|
|
await expect(
|
|
page.getByRole("heading", { name: "Add Trello board workflow details" }),
|
|
).toBeVisible();
|
|
const commitDetail = page.getByTestId("project-commit-detail");
|
|
const commitHeader = commitDetail.locator("header").first();
|
|
await expect(commitHeader).toContainText("Committed");
|
|
await expect(commitHeader).not.toContainText("Brain");
|
|
await expect(commitHeader.locator("img")).toHaveCount(0);
|
|
await expect(commitDetail.locator(":scope > div").first()).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 }),
|
|
).toHaveAttribute("href", "https://example.com/project-guide");
|
|
await expect(
|
|
page.getByRole("button", { name: "Architecture" }),
|
|
).toBeVisible();
|
|
await expect(page.locator("video")).toHaveAttribute(
|
|
"src",
|
|
"https://example.com/project-demo.mp4",
|
|
);
|
|
|
|
// Diff from the mocked get_project_repo_diff renders changed files.
|
|
await expect(page.getByText("2 changed files")).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
await expect(
|
|
page.getByText("CommunityTabs({ selectedCommitHash })"),
|
|
).toBeVisible();
|
|
|
|
await waitForAnimations(page);
|
|
await page.screenshot({
|
|
fullPage: false,
|
|
path: `${SHOTS}/01-commit-detail.png`,
|
|
});
|
|
|
|
// Breadcrumb category segment steps back to the commits feed.
|
|
await page
|
|
.getByRole("navigation", { name: "Project breadcrumb" })
|
|
.getByRole("button", { name: "Commits", exact: true })
|
|
.click();
|
|
await expect(commitRows.first()).toBeVisible();
|
|
await expect(page.getByTestId("project-workspace-tab-menu")).toBeVisible();
|
|
|
|
// The commits feed itself gets a grayed sub-tab crumb.
|
|
await expect(
|
|
page.getByRole("navigation", { name: "Project breadcrumb" }),
|
|
).toContainText("Commits");
|
|
|
|
// The repository segment returns to the project channel home.
|
|
await commitRows
|
|
.first()
|
|
.getByRole("button", { name: /Add Trello board workflow details/ })
|
|
.click();
|
|
await expect(page.getByTestId("project-commit-detail")).toBeVisible();
|
|
await page
|
|
.getByRole("navigation", { name: "Project breadcrumb" })
|
|
.getByTestId("project-breadcrumb-repository")
|
|
.click();
|
|
await expect(page.getByTestId("project-channel-home")).toBeVisible();
|
|
await expect(page.getByTestId("app-sidebar")).toBeVisible();
|
|
|
|
// The Projects root segment leaves the project entirely.
|
|
await page
|
|
.getByRole("navigation", { name: "Project breadcrumb" })
|
|
.getByRole("button", { name: "Projects", exact: true })
|
|
.click();
|
|
await expect(projectEntry).toBeVisible();
|
|
});
|
|
|
|
test("project home task sheet expands into the repository Tasks view", async ({
|
|
page,
|
|
}) => {
|
|
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();
|
|
const projectEntry = page
|
|
.locator(
|
|
'[data-testid="project-card-buzz"], [data-testid="project-row-buzz"]',
|
|
)
|
|
.first();
|
|
await expect(projectEntry).toBeVisible({ timeout: 10_000 });
|
|
await projectEntry.click();
|
|
await page.getByTestId("project-home-context-tasks").click();
|
|
const sheet = page.getByTestId("project-home-workspace-sheet");
|
|
await expect(sheet).toBeVisible();
|
|
const taskRow = sheet.getByTestId("project-issue-row").first();
|
|
const taskTitle = await taskRow
|
|
.locator('[data-projects-text-priority="primary"]')
|
|
.textContent();
|
|
const taskIdentifier = await taskRow
|
|
.getByTestId("project-work-item-identifier")
|
|
.textContent();
|
|
expect(taskTitle).toBeTruthy();
|
|
expect(taskIdentifier).toBeTruthy();
|
|
await taskRow.locator('[data-projects-text-priority="primary"]').click();
|
|
await expect(sheet.getByTestId("project-issue-detail")).toBeVisible();
|
|
await page.getByTestId("project-home-workspace-sheet-expand").click();
|
|
await expect(page.getByTestId("project-channel-home")).toHaveCount(0);
|
|
const expandedDetail = page.getByTestId("project-issue-detail");
|
|
await expect(expandedDetail).toContainText(taskTitle ?? "");
|
|
await expect(expandedDetail).toContainText(taskIdentifier ?? "");
|
|
await expect(page.getByTestId("project-detail-chrome")).toBeVisible();
|
|
await expect(page.getByTestId("app-sidebar")).toBeVisible();
|
|
});
|
|
|
|
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.getByTestId("project-home-context-repo-buzz").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);
|
|
await installMockBridge(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByTestId("open-projects-view").click();
|
|
|
|
// The overview no longer lists repository cards — switch to the
|
|
// Projects filter reveals the complete project cards/rows list.
|
|
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();
|
|
|
|
// Reviews use the compact single-line work-item row.
|
|
await page.getByTestId("project-home-context-repo-buzz").click();
|
|
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\/55/);
|
|
await expect(
|
|
page.getByTestId("project-work-item-group-header").first(),
|
|
).toBeVisible();
|
|
await expect(
|
|
prRows.first().locator("[data-projects-text-priority='primary']"),
|
|
).toHaveCSS("font-weight", "400");
|
|
await waitForAnimations(page);
|
|
await page.screenshot({ fullPage: false, path: `${SHOTS}/03-prs-feed.png` });
|
|
|
|
// 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("Review");
|
|
|
|
// Step back to the feed so the community tabs are available again.
|
|
await page
|
|
.getByRole("navigation", { name: "Project breadcrumb" })
|
|
.getByRole("button", { name: "Review", exact: true })
|
|
.click();
|
|
await expect(prRows.first()).toBeVisible();
|
|
|
|
// 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\/55/);
|
|
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,
|
|
path: `${SHOTS}/04-issues-feed.png`,
|
|
});
|
|
});
|
|
|
|
test("adding a repository retries and reports an error when the 30617 publication is rejected", async ({
|
|
page,
|
|
}) => {
|
|
await enableProjectsFeature(page);
|
|
// Reject the repository-announcement event on every attempt so the mutation
|
|
// exhausts its retry and surfaces a partial-write error.
|
|
await page.addInitScript(() => {
|
|
// Reject kind 30617 twice (initial attempt + one retry).
|
|
window.__BUZZ_E2E_REJECT_PROJECT_EVENT_KINDS__ = [30617, 30617];
|
|
});
|
|
await installMockBridge(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await addProjectToSidebar(page, "buzz");
|
|
|
|
await page.getByTestId("add-project-repository").click();
|
|
await page.getByTestId("create-project-repository").click();
|
|
await page.getByTestId("add-project-repository-name").fill("rejected-repo");
|
|
await page.getByTestId("add-project-repository-submit").click();
|
|
|
|
// The project event (30621) is published; the repository event (30617) is
|
|
// rejected on both attempts. The dialog must surface the partial-write error.
|
|
await expect(page.getByTestId("add-project-repository-dialog")).toBeVisible();
|
|
await expect(
|
|
page.getByText(/repository could not be created/i),
|
|
).toBeVisible();
|
|
|
|
// The 30621 must have been published (project was updated).
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(
|
|
() =>
|
|
window.__BUZZ_E2E_ACCEPTED_PROJECT_EVENTS__?.some(
|
|
(event) =>
|
|
event.kind === 30621 &&
|
|
event.tags.some(
|
|
(tag) => tag[0] === "a" && tag[1]?.endsWith(":rejected-repo"),
|
|
),
|
|
) ?? false,
|
|
),
|
|
)
|
|
.toBe(true);
|
|
|
|
// The 30617 must NOT have been accepted (both attempts were rejected).
|
|
const acceptedRepo = await page.evaluate(
|
|
() =>
|
|
window.__BUZZ_E2E_ACCEPTED_PROJECT_EVENTS__?.some(
|
|
(event) =>
|
|
event.kind === 30617 &&
|
|
event.tags.some(
|
|
(tag) => tag[0] === "d" && tag[1] === "rejected-repo",
|
|
),
|
|
) ?? false,
|
|
);
|
|
expect(
|
|
acceptedRepo,
|
|
"30617 must not be accepted when the relay rejects both attempts",
|
|
).toBe(false);
|
|
});
|
|
|
|
test("adding a repository treats a lost 30617 acknowledgement as success", async ({
|
|
page,
|
|
}) => {
|
|
await enableProjectsFeature(page);
|
|
// The relay will accept the 30617 but fail to deliver the ACK, then on the
|
|
// retry query the event will be found — the mutation must succeed.
|
|
await page.addInitScript(() => {
|
|
window.__BUZZ_E2E_FAIL_PROJECT_EVENT_ACK_KINDS__ = [30617];
|
|
});
|
|
await installMockBridge(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await addProjectToSidebar(page, "buzz");
|
|
|
|
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");
|
|
await page.getByTestId("add-project-repository-submit").click();
|
|
|
|
// The dialog should close — the operation recovered from the lost ACK.
|
|
await expect(page.getByTestId("add-project-repository-dialog")).toBeHidden();
|
|
// The overview must reflect the newly added repository.
|
|
await expect(
|
|
page.getByTestId("project-home-context-repo-lost-ack-repo"),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByTestId("sidebar-project-repository-lost-ack-repo"),
|
|
).toHaveCount(0);
|
|
|
|
// Both events must have been accepted: the 30621 (project update) and the
|
|
// 30617 (repository — accepted by relay even though ACK was lost).
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(
|
|
() =>
|
|
window.__BUZZ_E2E_ACCEPTED_PROJECT_EVENTS__?.filter((event) =>
|
|
event.tags.some(
|
|
(tag) => tag[0] === "d" && tag[1] === "lost-ack-repo",
|
|
),
|
|
).length ?? 0,
|
|
),
|
|
)
|
|
.toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
test("adding a repository blocks when a standalone 30617 already exists at that coordinate", async ({
|
|
page,
|
|
}) => {
|
|
await enableProjectsFeature(page);
|
|
// Seed a standalone 30617 (not a project member) owned by the mock identity.
|
|
// The add-repo mutation must block unconditionally when this coordinate exists,
|
|
// even though it is not yet in the "buzz" project's member list.
|
|
const MOCK_OWNER = "deadbeef".repeat(8);
|
|
const STANDALONE_DTAG = "existing-standalone";
|
|
await page.addInitScript(
|
|
({ owner, dtag }) => {
|
|
window.__BUZZ_E2E_EXTRA_PROJECT_EVENTS__ = [
|
|
{
|
|
id: "standalone00".padEnd(64, "0"),
|
|
kind: 30617,
|
|
pubkey: owner,
|
|
created_at: Math.floor(Date.now() / 1000) - 3600,
|
|
content: "A standalone repository that exists outside any project.",
|
|
tags: [
|
|
["d", dtag],
|
|
["name", "Existing Standalone"],
|
|
["clone", "https://git.example.com/standalone.git"],
|
|
],
|
|
},
|
|
];
|
|
},
|
|
{ owner: MOCK_OWNER, dtag: STANDALONE_DTAG },
|
|
);
|
|
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 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.
|
|
await page
|
|
.getByTestId("add-project-repository-name")
|
|
.fill("Existing Standalone");
|
|
await page.getByTestId("add-project-repository-submit").click();
|
|
|
|
// The dialog must remain open with a clobber error.
|
|
await expect(page.getByTestId("add-project-repository-dialog")).toBeVisible();
|
|
await expect(
|
|
page.getByText(/already exists.*standalone.*another project/i),
|
|
).toBeVisible();
|
|
|
|
// Neither a 30621 (project update) nor a 30617 (new repo) must have been published.
|
|
const publishedForStandalone = await page.evaluate(
|
|
({ dtag }) =>
|
|
window.__BUZZ_E2E_ACCEPTED_PROJECT_EVENTS__?.some(
|
|
(event) =>
|
|
event.tags.some((tag) => tag[0] === "d" && tag[1] === dtag) ||
|
|
event.tags.some(
|
|
(tag) => tag[0] === "a" && tag[1]?.endsWith(`:${dtag}`),
|
|
),
|
|
) ?? false,
|
|
{ dtag: STANDALONE_DTAG },
|
|
);
|
|
expect(
|
|
publishedForStandalone,
|
|
"neither project nor repository event must be published when clobber guard fires",
|
|
).toBe(false);
|
|
});
|
|
|
|
test("navigating via a 30617 entity-link route opens the correct non-primary repository and renders its PR", async ({
|
|
page,
|
|
}) => {
|
|
await enableProjectsFeature(page);
|
|
// Seed a known pull-request for relay-tools (the non-primary member of "buzz")
|
|
// with a deterministic id so the URL can be constructed before navigation.
|
|
const ALICE_PUBKEY =
|
|
"953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f";
|
|
const RELAY_TOOLS_ADDRESS = `30617:${ALICE_PUBKEY}:relay-tools`;
|
|
const KNOWN_PR_ID = "entity-link-pr-test".padEnd(64, "0");
|
|
|
|
await page.addInitScript(
|
|
({ repoAddress, prId, alicePubkey }) => {
|
|
window.__BUZZ_E2E_EXTRA_PROJECT_EVENTS__ = [
|
|
{
|
|
id: prId,
|
|
kind: 1618, // KIND_GIT_PULL_REQUEST
|
|
pubkey: alicePubkey,
|
|
created_at: Math.floor(Date.now() / 1000) - 60,
|
|
content: "Entity-link test PR from relay-tools",
|
|
tags: [
|
|
["a", repoAddress],
|
|
["subject", "Entity-link test PR from relay-tools"],
|
|
["c", "abc123".padEnd(40, "0")],
|
|
["h", "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50"],
|
|
["branch-name", "feature/entity-link-test"],
|
|
["clone", "https://github.com/block/relay-tools.git"],
|
|
],
|
|
},
|
|
];
|
|
},
|
|
{
|
|
repoAddress: RELAY_TOOLS_ADDRESS,
|
|
prId: KNOWN_PR_ID,
|
|
alicePubkey: ALICE_PUBKEY,
|
|
},
|
|
);
|
|
await installMockBridge(page);
|
|
|
|
// Navigate via the entity-link route using the hash router URL format.
|
|
// Python's http.server (the e2e web server) serves only index.html at `/`;
|
|
// a direct page.goto to `/projects/...` returns 404 because there is no
|
|
// SPA fallback. The app uses createHashHistory(), so the correct URL is
|
|
// `/#/projects/<id>?...` — the server always sees just `/` and the hash
|
|
// fragment is resolved entirely client-side by TanStack Router. Colons are
|
|
// valid in hash-fragment path segments and must NOT be percent-encoded:
|
|
// TanStack Router's param extractor receives the raw decoded segment, and
|
|
// %3A would be passed through literally (as the string "30617%3A…") rather
|
|
// than decoded to "30617:…", causing the project lookup to fail.
|
|
await page.goto(
|
|
`/#/projects/${RELAY_TOOLS_ADDRESS}?pullRequestId=${KNOWN_PR_ID}`,
|
|
{ waitUntil: "domcontentloaded" },
|
|
);
|
|
|
|
// 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(
|
|
page.getByText("Entity-link test PR from relay-tools").first(),
|
|
).toBeVisible({ timeout: 10_000 });
|
|
await expect(
|
|
page.getByText("feature/entity-link-test").first(),
|
|
).toBeVisible();
|
|
});
|