feat: import Chinese-localized Buzz source snapshot
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
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
Signed-off-by: cls_宁波本机 <908705107@qq.com>
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { installMockBridge } from "../helpers/bridge";
|
||||
|
||||
async function seedLongThread(page: import("@playwright/test").Page) {
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(
|
||||
() => typeof window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__ === "function",
|
||||
),
|
||||
)
|
||||
.toBe(true);
|
||||
return page.evaluate(() => {
|
||||
const root = window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
||||
channelName: "general",
|
||||
content: "Focus mode integration thread",
|
||||
createdAt: 1_700_900_000,
|
||||
});
|
||||
if (!root) throw new Error("Failed to seed focus thread root");
|
||||
|
||||
for (let index = 0; index < 48; index += 1) {
|
||||
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
||||
channelName: "general",
|
||||
content: `Focus reply ${index}: this deliberately wraps across several lines so changing the thread measure causes real layout reflow.`,
|
||||
parentEventId: root.id,
|
||||
createdAt: 1_700_900_001 + index,
|
||||
});
|
||||
}
|
||||
return root.id;
|
||||
});
|
||||
}
|
||||
|
||||
async function scrollToMiddleVisibleMessage(
|
||||
body: import("@playwright/test").Locator,
|
||||
threadRootId: string,
|
||||
): Promise<string> {
|
||||
let anchorId: string | null = null;
|
||||
await expect
|
||||
.poll(async () => {
|
||||
anchorId = await body.evaluate((element) => {
|
||||
const maxScrollTop = element.scrollHeight - element.clientHeight;
|
||||
if (maxScrollTop <= 0) return null;
|
||||
|
||||
const targetScrollTop = Math.floor(maxScrollTop * 0.4);
|
||||
element.scrollTop = targetScrollTop;
|
||||
element.dispatchEvent(new Event("scroll", { bubbles: true }));
|
||||
|
||||
if (Math.abs(element.scrollTop - targetScrollTop) > 1) return null;
|
||||
const bounds = element.getBoundingClientRect();
|
||||
const row = Array.from(
|
||||
element.querySelectorAll<HTMLElement>("[data-message-id]"),
|
||||
).find((candidate) => {
|
||||
const rect = candidate.getBoundingClientRect();
|
||||
return rect.bottom > bounds.top && rect.top < bounds.bottom;
|
||||
});
|
||||
return row?.dataset.messageId ?? null;
|
||||
});
|
||||
return anchorId !== null && anchorId !== threadRootId;
|
||||
})
|
||||
.toBe(true);
|
||||
|
||||
if (!anchorId) throw new Error("No visible middle-thread anchor");
|
||||
return anchorId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The channel header must own its pixels, not merely be "visible".
|
||||
*
|
||||
* Regression guard for the focus-mode launch: a `z-0` on the channel section
|
||||
* created a stacking context that flattened the header's `z-30` beneath the
|
||||
* sibling shared header backdrop (also `z-30`), painting the backdrop over the
|
||||
* name and actions. Neither `toBeVisible()` nor `elementFromPoint` can catch
|
||||
* this — the backdrop is `pointer-events-none`, so hit-testing skips it. We
|
||||
* compare CSS paint order directly: walk each element's stacking-context
|
||||
* chain, find the branches under their common stacking context, and check the
|
||||
* header's branch wins (higher z-index, or later in DOM order on a tie).
|
||||
*/
|
||||
async function expectChannelHeaderUnobscured(
|
||||
page: import("@playwright/test").Page,
|
||||
) {
|
||||
const title = page.getByTestId("chat-title");
|
||||
await expect(title).toBeVisible();
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() => {
|
||||
const titleEl = document.querySelector('[data-testid="chat-title"]');
|
||||
const backdropEl = document.querySelector(
|
||||
'[data-testid="channel-shared-header-backdrop"]',
|
||||
);
|
||||
if (!titleEl) return "missing title";
|
||||
if (!backdropEl) return "missing backdrop";
|
||||
|
||||
const createsStackingContext = (el: Element): boolean => {
|
||||
const style = getComputedStyle(el);
|
||||
if (style.position !== "static" && style.zIndex !== "auto")
|
||||
return true;
|
||||
if (parseFloat(style.opacity) < 1) return true;
|
||||
if (style.transform !== "none") return true;
|
||||
if (style.filter !== "none") return true;
|
||||
const backdropFilter =
|
||||
style.backdropFilter ??
|
||||
(style as unknown as { webkitBackdropFilter?: string })
|
||||
.webkitBackdropFilter;
|
||||
if (backdropFilter && backdropFilter !== "none") return true;
|
||||
if (style.isolation === "isolate") return true;
|
||||
if (
|
||||
style.contain.includes("paint") ||
|
||||
style.contain.includes("strict")
|
||||
)
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
// Chain of stacking-context roots from the element up to <html>.
|
||||
const stackingChain = (el: Element): Element[] => {
|
||||
const chain: Element[] = [el];
|
||||
let current: Element | null = el.parentElement;
|
||||
while (current) {
|
||||
if (
|
||||
createsStackingContext(current) ||
|
||||
current === document.documentElement
|
||||
) {
|
||||
chain.push(current);
|
||||
}
|
||||
current = current.parentElement;
|
||||
}
|
||||
return chain;
|
||||
};
|
||||
|
||||
const titleChain = stackingChain(titleEl);
|
||||
const backdropChain = stackingChain(backdropEl);
|
||||
const common = titleChain.find((el) => backdropChain.includes(el));
|
||||
if (!common) return "no common stacking context";
|
||||
|
||||
// Branch = the child-of-common entry each element paints through.
|
||||
const titleBranch = titleChain[titleChain.indexOf(common) - 1];
|
||||
const backdropBranch = backdropChain[backdropChain.indexOf(common) - 1];
|
||||
if (!titleBranch || !backdropBranch) return "degenerate chain";
|
||||
|
||||
const effectiveZ = (el: Element): number => {
|
||||
const z = getComputedStyle(el).zIndex;
|
||||
return z === "auto" ? 0 : parseInt(z, 10);
|
||||
};
|
||||
const titleZ = effectiveZ(titleBranch);
|
||||
const backdropZ = effectiveZ(backdropBranch);
|
||||
if (titleZ !== backdropZ) {
|
||||
return titleZ > backdropZ ? true : "backdrop paints above header";
|
||||
}
|
||||
// Tie: later in DOM order paints on top.
|
||||
const order = backdropBranch.compareDocumentPosition(titleBranch);
|
||||
return (order & Node.DOCUMENT_POSITION_FOLLOWING) !== 0
|
||||
? true
|
||||
: "backdrop paints above header";
|
||||
}),
|
||||
)
|
||||
.toBe(true);
|
||||
}
|
||||
|
||||
test("focus and split preserve reading context and interaction ownership", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem("buzz.channels.threadViewMode", "focus");
|
||||
});
|
||||
await installMockBridge(page, {
|
||||
managedAgents: [
|
||||
{
|
||||
pubkey:
|
||||
"953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f",
|
||||
name: "alice",
|
||||
status: "stopped",
|
||||
},
|
||||
],
|
||||
});
|
||||
await page.goto("/");
|
||||
const rootId = await seedLongThread(page);
|
||||
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expectChannelHeaderUnobscured(page);
|
||||
const summary = page.locator(
|
||||
`[data-testid="message-thread-summary"][data-thread-head-id="${rootId}"]`,
|
||||
);
|
||||
await expect(summary).toBeVisible();
|
||||
await summary.click();
|
||||
|
||||
const channel = page.getByTestId("channel-drop-zone");
|
||||
const drawer = page.getByTestId("focus-thread-drawer");
|
||||
const body = page.getByTestId("message-thread-body");
|
||||
await expect(drawer).toBeVisible();
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() =>
|
||||
Boolean(
|
||||
document
|
||||
.querySelector('[data-testid="focus-thread-drawer"]')
|
||||
?.contains(document.activeElement),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toBe(true);
|
||||
await expect(channel).toHaveAttribute("inert", "");
|
||||
|
||||
const anchorId = await scrollToMiddleVisibleMessage(body, rootId);
|
||||
|
||||
const focusModeToggle = page.getByRole("button", {
|
||||
name: "Show thread beside channel",
|
||||
});
|
||||
await focusModeToggle.hover();
|
||||
await expect(
|
||||
page.getByRole("tooltip", { name: "Show thread beside channel" }),
|
||||
).toBeVisible();
|
||||
await focusModeToggle.click();
|
||||
await expect(drawer).toHaveCount(0);
|
||||
await expect(channel).not.toHaveAttribute("inert", "");
|
||||
await expectChannelHeaderUnobscured(page);
|
||||
await expect(page.getByRole("tooltip")).toHaveCount(0);
|
||||
await expect(page.getByTestId("message-thread-body")).toBeFocused();
|
||||
await expect(summary).not.toBeFocused();
|
||||
await expect(
|
||||
body.locator(`[data-message-id="${anchorId}"]`),
|
||||
).toBeInViewport();
|
||||
await expect(
|
||||
body.locator(`[data-message-id="${anchorId}"]`),
|
||||
).not.toHaveAttribute("data-highlighted", "true");
|
||||
|
||||
// Sidebar background dismissal belongs to the overlay presentation only.
|
||||
await page
|
||||
.getByTestId("app-sidebar-scroll-anchor")
|
||||
.evaluate((element) => (element as HTMLElement).click());
|
||||
await expect(page.getByTestId("message-thread-panel")).toBeVisible();
|
||||
|
||||
const splitModeToggle = page.getByRole("button", { name: "Expand thread" });
|
||||
await splitModeToggle.focus();
|
||||
await splitModeToggle.press("Enter");
|
||||
await expect(drawer).toBeVisible();
|
||||
await expect(channel).toHaveAttribute("inert", "");
|
||||
await expect(page.getByTestId("thread-view-mode-toggle")).toBeFocused();
|
||||
await expect(
|
||||
body.locator(`[data-message-id="${anchorId}"]`),
|
||||
).toBeInViewport();
|
||||
|
||||
// Focus mode owns Escape even while the rich-text composer and one of its
|
||||
// nested controls has focus: one press exits the focused thread.
|
||||
const threadInput = page
|
||||
.getByTestId("message-thread-panel")
|
||||
.getByTestId("message-input");
|
||||
await threadInput.click();
|
||||
await threadInput.pressSequentially("@al");
|
||||
await expect(
|
||||
page
|
||||
.getByTestId("message-thread-panel")
|
||||
.getByTestId("mention-autocomplete"),
|
||||
).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(page.getByTestId("focus-thread-drawer-overlay")).toHaveCount(0);
|
||||
await expect(channel).not.toHaveAttribute("inert", "");
|
||||
|
||||
await summary.click();
|
||||
await expect(drawer).toBeVisible();
|
||||
const profileCard = page.getByTestId("sidebar-profile-card");
|
||||
await profileCard.click({ position: { x: 8, y: 8 } });
|
||||
await expect(page.getByTestId("profile-popover")).toBeVisible();
|
||||
await expect(drawer).toBeVisible();
|
||||
await profileCard.click({ position: { x: 8, y: 8 } });
|
||||
await expect(page.getByTestId("profile-popover")).toHaveCount(0);
|
||||
await page
|
||||
.getByTestId("app-sidebar-scroll-anchor")
|
||||
.evaluate((element) => (element as HTMLElement).click());
|
||||
await expect(page.getByTestId("focus-thread-drawer-overlay")).toHaveCount(0);
|
||||
|
||||
await summary.click();
|
||||
await expect(drawer).toBeVisible();
|
||||
await page.getByTestId("focus-thread-drawer-scrim").click({
|
||||
position: { x: 24, y: 200 },
|
||||
});
|
||||
await expect(page.getByTestId("focus-thread-drawer-overlay")).toHaveCount(0);
|
||||
await expect(channel).not.toHaveAttribute("inert", "");
|
||||
});
|
||||
|
||||
test("narrow threads do not offer an unavailable layout switch", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 860, height: 720 });
|
||||
await installMockBridge(page);
|
||||
await page.goto("/");
|
||||
const rootId = await seedLongThread(page);
|
||||
await page.getByTestId("channel-general").click();
|
||||
const summary = page.locator(
|
||||
`[data-testid="message-thread-summary"][data-thread-head-id="${rootId}"]`,
|
||||
);
|
||||
await expect(summary).toBeVisible();
|
||||
await summary.click();
|
||||
await expect(page.getByTestId("message-thread-panel")).toBeVisible();
|
||||
await expect(page.getByTestId("thread-view-mode-toggle")).toHaveCount(0);
|
||||
});
|
||||
Reference in New Issue
Block a user