Files
buzz/desktop/tests/e2e/reminder-click-repro.spec.ts
cls 9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

145 lines
5.7 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { waitForAnimations } from "../helpers/animations";
import { installMockBridge } from "../helpers/bridge";
/**
* Regression suite for the "app freezes after setting a reminder" bug.
*
* The app used to bundle two copies of @radix-ui/react-dismissable-layer
* (react-menu resolved 1.1.11 while react-dialog resolved 1.1.15). Each copy
* keeps its own module-level `originalBodyPointerEvents`, so when a *modal*
* menu opened a *modal* dialog in the same React commit, the dialog's copy
* saved the menu's `pointer-events: none` as the "original" body style and
* restored it when the dialog closed — leaving `pointer-events: none` stuck
* on <body> and deadening the whole app to the mouse.
*
* The inbox row context menu hit this: its "Remind me later" item opens
* RemindMeLaterDialog synchronously. Fixed by deduplicating the layer via
* the pnpm override in pnpm-workspace.yaml — a single copy coordinates
* nested modal layers correctly, so the menu keeps its default modal
* behavior.
*
* Each test drives one reminder entry point through a full set-reminder
* cycle, then asserts <body> pointer-events is not stuck and that a real
* click still lands. The inbox right-click test fails if the dismissable
* layer ever duplicates again; the message-menu tests pin the already-safe
* paths.
*/
const GENERAL_MESSAGE_ROW = "message-row";
// The default mock feed's mention row (from alice, in #general) has a
// channel target, so its "Remind me later" context-menu item is enabled.
const INBOX_MENTION_ROW = "home-inbox-item-mock-feed-mention";
/**
* Assert the app survived the menu → dialog → close cycle.
*
* The stuck state is inline `pointer-events: none` left on <body>. The buggy
* restore runs when the dialog's dismissable layer unmounts — after its exit
* animation — so first wait for the dialog to leave the DOM, then check the
* body style, then prove clicks still land by navigating via the sidebar.
*/
async function expectAppAliveAfterDialogClose(
page: import("@playwright/test").Page,
) {
await expect(page.getByRole("dialog")).toHaveCount(0);
await waitForAnimations(page);
expect(await page.evaluate(() => document.body.style.pointerEvents)).not.toBe(
"none",
);
await page.getByTestId("channel-random").click();
await expect(page.getByTestId("chat-title")).toHaveText("random");
}
/** Open the Remind-me-later dialog from a message row's More-actions menu. */
async function openReminderDialogFromMessageMenu(
page: import("@playwright/test").Page,
) {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const messageRow = page.getByTestId(GENERAL_MESSAGE_ROW).first();
await messageRow.hover();
await messageRow.getByRole("button", { name: "More actions" }).click();
const remindItem = page.getByRole("menuitem", { name: "Remind me later" });
await expect(remindItem).toBeVisible();
await waitForAnimations(page);
await remindItem.click();
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
await waitForAnimations(page);
return dialog;
}
async function expectReminderSetToast(page: import("@playwright/test").Page) {
await expect(
page.locator("[data-sonner-toast]").filter({ hasText: "Reminder set" }),
).toBeVisible();
}
test.describe("reminder set → app stays clickable", () => {
test.beforeEach(async ({ page }) => {
await installMockBridge(page);
});
test("01 — message menu → time preset", async ({ page }) => {
const dialog = await openReminderDialogFromMessageMenu(page);
await dialog.getByRole("button", { name: "In 30 minutes" }).click();
await expectReminderSetToast(page);
await expectAppAliveAfterDialogClose(page);
});
test("02 — message menu → custom time via Set reminder footer button", async ({
page,
}) => {
const dialog = await openReminderDialogFromMessageMenu(page);
// The custom timestamp must be strictly in the future or the Set
// reminder button stays disabled — pick tomorrow at 09:00 local time.
const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);
const date = [
tomorrow.getFullYear(),
String(tomorrow.getMonth() + 1).padStart(2, "0"),
String(tomorrow.getDate()).padStart(2, "0"),
].join("-");
await dialog.getByLabel("Reminder date").fill(date);
await dialog.getByLabel("Reminder time").fill("09:00");
await dialog.getByRole("button", { name: "Set reminder" }).click();
await expectReminderSetToast(page);
await expectAppAliveAfterDialogClose(page);
});
// The regression path: only the inbox right-click stacks a modal
// ContextMenu under the modal dialog. Fails if two copies of the
// dismissable layer are ever bundled again — body pointer-events stays
// "none" after the dialog closes and the sidebar click below times out.
test("03 — inbox row right-click → Remind me later", async ({ page }) => {
await page.goto("/");
await expect(page.getByTestId("home-inbox")).toBeVisible();
const row = page.getByTestId(INBOX_MENTION_ROW);
await expect(row).toBeVisible();
await row.click({ button: "right" });
const remindItem = page.getByRole("menuitem", { name: "Remind me later" });
await expect(remindItem).toBeVisible();
await waitForAnimations(page);
await remindItem.click();
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
await waitForAnimations(page);
await dialog.getByRole("button", { name: "In 30 minutes" }).click();
await expectReminderSetToast(page);
await expectAppAliveAfterDialogClose(page);
});
});