Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default defineConfig({
"**/navigation.spec.ts",
"**/channels.spec.ts",
"**/channel-shared-header-backdrop.spec.ts",
"**/auxiliary-pane-close-visibility.spec.ts",
"**/channel-composer-overflow.spec.ts",
"**/badge.spec.ts",
"**/channel-browser.spec.ts",
Expand Down
6 changes: 5 additions & 1 deletion desktop/src/features/channels/ui/RightAuxiliaryPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ export function RightAuxiliaryPane({
return (
<aside
className={cn(
"group/right-pane relative isolate flex h-full shrink-0 flex-col overflow-hidden bg-background",
// `isolate` lets project sheets cover threads cleanly (#6901) but
// traps this pane's z-40 header chrome (X/Edit) below the channel's
// shared z-30 header blur strip in split layout. `z-31` lifts the
// pane above that backdrop while staying under the z-41 thread drawer.
"group/right-pane relative isolate z-31 flex h-full shrink-0 flex-col overflow-hidden bg-background",
detached
? "bg-transparent"
: "before:pointer-events-none before:absolute before:bottom-0 before:left-0 before:top-0 before:z-50 before:w-px before:bg-border/80 before:content-['']",
Expand Down
113 changes: 113 additions & 0 deletions desktop/tests/e2e/auxiliary-pane-close-visibility.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { expect, test } from "@playwright/test";

import { waitForAnimations } from "../helpers/animations";
import { installMockBridge } from "../helpers/bridge";

type MockMessageWindow = Window & {
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
channelName: string;
content: string;
parentEventId?: string | null;
pubkey?: string;
}) => { id: string } | undefined;
__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?: (input: {
channelName: string;
}) => boolean;
};

const CHANNEL_NAME = "engineering";
const MOCK_IDENTITY_PUBKEY = "deadbeef".repeat(8);
const ALICE_PUBKEY =
"953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f";

async function waitForMockLiveSubscription(
page: import("@playwright/test").Page,
channelName: string,
) {
await expect
.poll(async () => {
return page.evaluate((name) => {
return (
(
window as MockMessageWindow
).__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?.({ channelName: name }) ??
false
);
}, channelName);
})
.toBe(true);
}

test.describe("auxiliary pane close visibility", () => {
test.use({ viewport: { width: 1280, height: 720 } });

// Regression for #6901: `isolate` on the right auxiliary pane makes it its
// own stacking context. With no z-index (`auto`, i.e. level 0) the entire
// pane subtree — including the z-40 header chrome where X/Edit live — paints
// below the channel's sibling z-30 shared-header backdrop in split layout,
// washing out the controls (they stay clickable because the backdrop is
// pointer-events-none, matching the reported videos). The pane's own
// stacking level must sit above the backdrop for the header to show through.
test("close button paints above the shared header backdrop in a channel thread", async ({
page,
}) => {
await installMockBridge(page);
await page.goto("/");
await page.getByTestId(`channel-${CHANNEL_NAME}`).click();
await expect(page.getByTestId("chat-title")).toHaveText(CHANNEL_NAME);
await waitForMockLiveSubscription(page, CHANNEL_NAME);

const rootId = await page.evaluate(
({ channelName, pubkey }) =>
(window as MockMessageWindow).__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName,
content: "Root message for auxiliary pane close visibility.",
pubkey,
})?.id ?? null,
{ channelName: CHANNEL_NAME, pubkey: MOCK_IDENTITY_PUBKEY },
);
expect(rootId).not.toBeNull();

await page.evaluate(
({ channelName, parentEventId, pubkey }) => {
(window as MockMessageWindow).__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName,
content: "Reply that opens a split thread panel.",
parentEventId,
pubkey,
});
},
{
channelName: CHANNEL_NAME,
parentEventId: rootId,
pubkey: ALICE_PUBKEY,
},
);

const replyButton = page.locator('[data-testid^="reply-message-"]').first();
await expect(replyButton).toBeVisible();
await replyButton.click({ force: true });
await expect(page.getByTestId("message-thread-panel")).toBeVisible();
await waitForAnimations(page);

const closeButton = page.getByTestId("auxiliary-panel-close");
await expect(closeButton).toBeVisible();

const backdrop = page.getByTestId("channel-shared-header-backdrop");
await expect(backdrop).toHaveCount(1);

// The pane is an isolated stacking context (`isolate`) that sits over the
// channel timeline. Its close button paints correctly only when the pane's
// own stacking level clears the shared-header backdrop it overlaps. Assert
// the pane both establishes that context and outranks the backdrop.
const pane = page.getByTestId("message-thread-panel");
const [paneIsolation, paneZIndex, backdropZIndex] = await Promise.all([
pane.evaluate((element) => getComputedStyle(element).isolation),
pane.evaluate((element) => Number(getComputedStyle(element).zIndex)),
backdrop.evaluate((element) => Number(getComputedStyle(element).zIndex)),
]);

expect(paneIsolation).toBe("isolate");
expect(paneZIndex).toBeGreaterThan(backdropZIndex);
});
});
Loading