Files
buzz/desktop/tests/e2e/channel-mute.spec.ts
Wes fe56d311a7 test(desktop): stabilize unread and audio release smoke fixtures (#7821)
## Summary
Repair the mock-backed desktop smoke fixtures blocking release PR #7817.
No application unread logic, styling, audio component, scheduler, or
release-tool changes.

- Require an exact channel-scoped kind-9 subscription before injecting
unread messages, including after navigation. The old readiness check
could accept a global-only, wrong-channel, or wrong-kind consumer. The
extracted predicate is used by the real mock bridge and has regression
coverage for those false positives and legacy behavior.
- Seed the 24 audio messages offscreen with distinct timestamps, then
open the channel with all cards in the load margin. Same-second random
event IDs previously reordered the settling timeline, causing different
visible cards to start loads. This was viewport churn, not a component
restart defect.
- Expose the existing real audio scheduler snapshot through the E2E
bridge. Assert 3 active / 21 queued before navigation and 0 active / 0
queued afterward. Preserve the peak-3 cap, exact 3 fetches /
cancellations / releases, zero object URLs, and existing unread/bold
assertions. No sleeps, retries, skipped tests, or relaxed limits were
added.

## Validation
- Before commit, all 30 tests in `channel-activity-popover.spec.ts`,
`channel-mute.spec.ts`, and `voice-note.spec.ts` passed five repetitions
with retries disabled: **150/150**. This was the working repair on base
`312cf674f`, before final formatting/cleanup, not the entire smoke
suite.
- At committed head `1b126afbe75c2a2afbdfe13744f0938fa584764e`, normal
pre-push hooks passed: **6,591/6,591 desktop unit tests**, desktop
checks, TypeScript typechecking, differential file-size gate, and
branch-skew check. Pre-commit Biome formatting passed; working tree is
clean.
- Independent sidebar and audio reviews found no substantive blocker.
Their formatting findings are resolved in this commit.
- Full repository `just ci`, native release packaging, and the entire
Playwright smoke suite were not run locally. GitHub CI is the remaining
broader gate; this PR is not a release certification.

## Release follow-up
After this repair is merged and required checks pass, regenerate #7817
through the desktop release tooling and validate the new candidate's
exact head. This PR does not merge or publish a release.

Originating discussion:
buzz://message?channel=3428ec3f-a58b-429b-afbc-dc6a77918ce8&id=3303565b14334696e9bbf433ef3ad1aea4307acc69834f7f685a412e81354493

Signed-off-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
2026-09-23 01:07:00 +00:00

202 lines
6.2 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { TEST_IDENTITIES, installMockBridge } from "../helpers/bridge";
const MOCK_PUBKEY = "deadbeef".repeat(8);
const ENGINEERING_CHANNEL_ID = "1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9";
const MUTE_STORAGE_KEY = `buzz-channel-mutes.v1:${MOCK_PUBKEY}`;
function seedMuteState(
page: import("@playwright/test").Page,
channelId: string,
) {
return page.addInitScript(
({ key, id }) => {
localStorage.setItem(
key,
JSON.stringify({
version: 1,
channels: {
[id]: { muted: true, updatedAt: 1700000000 },
},
}),
);
},
{ key: MUTE_STORAGE_KEY, id: channelId },
);
}
async function waitForMockLiveSubscription(
page: import("@playwright/test").Page,
channelName: string,
) {
await expect
.poll(async () => {
return page.evaluate(
({ ch }) =>
(
window as Window & {
__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?: (input: {
channelName: string;
kind: number;
exactChannel: boolean;
}) => boolean;
}
).__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?.({
channelName: ch,
kind: 9,
exactChannel: true,
}) ?? false,
{ ch: channelName },
);
})
.toBe(true);
}
test.describe("channel muting", () => {
test("01 — context menu shows Mute channel", async ({ page }) => {
await installMockBridge(page);
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await page.getByTestId("channel-random").click();
await expect(page.getByTestId("chat-title")).toHaveText("random");
await page.getByTestId("channel-general").click({ button: "right" });
const muteItem = page.getByRole("menuitem", { name: "Mute channel" });
await expect(muteItem).toBeVisible();
await muteItem.evaluate((el) =>
Promise.all(
el
.closest("[data-state]")
?.getAnimations()
.map((a) => a.finished) ?? [],
),
);
});
test("02 — muted channel is dimmed with BellOff icon", async ({ page }) => {
await seedMuteState(page, ENGINEERING_CHANNEL_ID);
await installMockBridge(page);
await page.goto("/");
await page.getByTestId("channel-random").click();
await expect(page.getByTestId("chat-title")).toHaveText("random");
const engRow = page.getByTestId("channel-engineering");
const engLabel = engRow.locator("[data-sidebar-row-label]");
await expect(engRow).toBeVisible();
await expect(engRow).toHaveCSS("opacity", "1");
await expect(engLabel).toHaveCSS("opacity", "0.5");
await expect(engRow.locator("svg.lucide-hash")).toHaveCSS("opacity", "0.5");
await expect(engRow.locator("svg.lucide-bell-off")).toHaveCount(1);
});
test("02b — muted channels recede further in dark mode", async ({ page }) => {
await page.addInitScript(() => {
window.localStorage.setItem("buzz-theme", "buzz-dark");
});
await seedMuteState(page, ENGINEERING_CHANNEL_ID);
await installMockBridge(page);
await page.goto("/");
await expect(page.locator("html")).toHaveClass(/dark/);
await page.getByTestId("channel-random").click();
const engRow = page.getByTestId("channel-engineering");
await expect(engRow).toHaveCSS("opacity", "1");
await expect(engRow.locator("[data-sidebar-row-label]")).toHaveCSS(
"opacity",
"0.45",
);
await expect(engRow.locator("svg.lucide-hash")).toHaveCSS(
"opacity",
"0.45",
);
});
test("03 — muted channel with a top-level @mention is emphasized", async ({
page,
}) => {
await seedMuteState(page, ENGINEERING_CHANNEL_ID);
await installMockBridge(page);
await page.goto("/");
await page.getByTestId("channel-engineering").click();
await expect(page.getByTestId("chat-title")).toHaveText("engineering");
await waitForMockLiveSubscription(page, "engineering");
await page.getByTestId("channel-random").click();
await expect(page.getByTestId("chat-title")).toHaveText("random");
await waitForMockLiveSubscription(page, "engineering");
await page.evaluate(
({ pubkey, mockPubkey }) => {
(
window as Window & {
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
channelName: string;
content: string;
pubkey: string;
mentionPubkeys: string[];
}) => unknown;
}
).__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "engineering",
content: "Hey check this out",
pubkey,
mentionPubkeys: [mockPubkey],
});
},
{
pubkey: TEST_IDENTITIES.alice.pubkey,
mockPubkey: MOCK_PUBKEY,
},
);
await expect(page.getByTestId("channel-engineering")).toHaveCSS(
"font-weight",
"700",
);
await expect(
page.getByTestId("channel-unread-dot-engineering"),
).toHaveCount(0);
});
test("04 — context menu shows Unmute channel when muted", async ({
page,
}) => {
await seedMuteState(page, ENGINEERING_CHANNEL_ID);
await installMockBridge(page);
await page.goto("/");
await page.getByTestId("channel-random").click();
await expect(page.getByTestId("chat-title")).toHaveText("random");
await page.getByTestId("channel-engineering").click({ button: "right" });
const unmuteItem = page.getByRole("menuitem", { name: "Unmute channel" });
await expect(unmuteItem).toBeVisible();
await unmuteItem.evaluate((el) =>
Promise.all(
el
.closest("[data-state]")
?.getAnimations()
.map((a) => a.finished) ?? [],
),
);
});
test("05 — muted icon visible on selected channel", async ({ page }) => {
await seedMuteState(page, ENGINEERING_CHANNEL_ID);
await installMockBridge(page);
await page.goto("/");
await page.getByTestId("channel-engineering").click();
await expect(page.getByTestId("chat-title")).toHaveText("engineering");
const engRow = page.getByTestId("channel-engineering");
await expect(engRow.locator("svg.lucide-bell-off")).toHaveCount(1);
});
});