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,373 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
|
||||
|
||||
const IMAGE_SHA = "c".repeat(64);
|
||||
const IMAGE_URL = "http://127.0.0.1:4173/buzz.svg";
|
||||
const IMAGE_DESCRIPTOR = {
|
||||
url: IMAGE_URL,
|
||||
sha256: IMAGE_SHA,
|
||||
size: 646,
|
||||
type: "image/svg+xml",
|
||||
uploaded: Math.floor(Date.now() / 1000),
|
||||
thumb: IMAGE_URL,
|
||||
dim: "64x64",
|
||||
filename: "buzz.svg",
|
||||
};
|
||||
const GENERAL_CHANNEL_ID = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
|
||||
|
||||
type MockFeedItem = {
|
||||
id: string;
|
||||
kind: number;
|
||||
pubkey: string;
|
||||
content: string;
|
||||
created_at: number;
|
||||
channel_id: string | null;
|
||||
channel_name: string;
|
||||
tags: string[][];
|
||||
category: "mention" | "needs_action" | "activity" | "agent_activity";
|
||||
};
|
||||
|
||||
type MockFeedWindow = Window &
|
||||
typeof globalThis & {
|
||||
__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__?: (item: MockFeedItem) => MockFeedItem;
|
||||
};
|
||||
|
||||
async function installSpoilerBridge(
|
||||
page: Page,
|
||||
mock: Parameters<typeof installMockBridge>[1] = {},
|
||||
) {
|
||||
await installMockBridge(page, {
|
||||
...mock,
|
||||
uploadDescriptors: [IMAGE_DESCRIPTOR],
|
||||
});
|
||||
}
|
||||
|
||||
test("no-selection spoiler applies to every composer paragraph", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installSpoilerBridge(page);
|
||||
await page.context().grantPermissions(["clipboard-read", "clipboard-write"], {
|
||||
origin: "http://127.0.0.1:4173",
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
|
||||
const input = page.getByTestId("message-input");
|
||||
const paragraphs = [
|
||||
"First hidden paragraph",
|
||||
"Second hidden paragraph",
|
||||
"Third hidden paragraph",
|
||||
];
|
||||
|
||||
await page.evaluate(
|
||||
(text) => navigator.clipboard.writeText(text),
|
||||
paragraphs.join("\n\n"),
|
||||
);
|
||||
await input.click();
|
||||
await page.keyboard.press("ControlOrMeta+V");
|
||||
await expect(input.locator("p")).toHaveCount(paragraphs.length);
|
||||
|
||||
await page.getByRole("button", { name: "Toggle formatting" }).click();
|
||||
await page.getByRole("button", { name: "Spoiler", exact: true }).click();
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
input.evaluate(() =>
|
||||
Array.from(
|
||||
document.querySelectorAll(
|
||||
'[data-testid="message-input"] .buzz-spoiler[data-spoiler]',
|
||||
),
|
||||
(node) => node.textContent,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toEqual(paragraphs);
|
||||
});
|
||||
|
||||
test("image attachments can be marked and sent as hidden spoilers", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installSpoilerBridge(page);
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
|
||||
await page.getByRole("button", { name: "Attach file" }).click();
|
||||
|
||||
const composer = page.getByTestId("message-composer");
|
||||
await expect(composer.getByAltText("Attachment cccc")).toBeVisible();
|
||||
|
||||
// Media spoilers are toggled per-attachment from the lightbox.
|
||||
await composer.getByTestId("composer-media-attachment").hover();
|
||||
await page.getByTestId("composer-attachment-annotate").click();
|
||||
await page.getByTestId("composer-attachment-spoiler").click();
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(composer.locator("[data-composer-media-spoiler]")).toBeVisible();
|
||||
|
||||
await page.getByTestId("send-message").click();
|
||||
|
||||
const lastMessage = page.getByTestId("message-row").last();
|
||||
const spoilerBlock = lastMessage.locator(".buzz-spoiler--block");
|
||||
await expect(spoilerBlock).toBeVisible();
|
||||
await expect(spoilerBlock).toHaveAttribute("data-revealed", "false");
|
||||
await expect(spoilerBlock.locator("[data-block-media] img")).toHaveAttribute(
|
||||
"src",
|
||||
IMAGE_URL,
|
||||
);
|
||||
|
||||
await spoilerBlock.click();
|
||||
await expect(spoilerBlock).toHaveAttribute("data-revealed", "true");
|
||||
await expect(page.getByRole("dialog", { name: "image" })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("text spoiler stays usable while attachment upload is pending", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installSpoilerBridge(page, { uploadDelayMs: 1_000 });
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
|
||||
const input = page.getByTestId("message-input");
|
||||
await input.click();
|
||||
await page.keyboard.type("pending secret");
|
||||
|
||||
// Kick off the (delayed) upload first — the attach button lives in the
|
||||
// passive toolbar, which is replaced while formatting is expanded.
|
||||
await page.getByRole("button", { name: "Attach file" }).click();
|
||||
|
||||
await page.getByRole("button", { name: "Toggle formatting" }).click();
|
||||
const spoilerButton = page.getByRole("button", {
|
||||
name: "Spoiler",
|
||||
exact: true,
|
||||
});
|
||||
|
||||
// Text spoilers are independent of media uploads, so the button stays
|
||||
// enabled and works while the upload is still in flight.
|
||||
await expect(spoilerButton).toBeEnabled();
|
||||
await spoilerButton.click();
|
||||
await expect(input.locator(".buzz-spoiler[data-spoiler]")).toContainText(
|
||||
"pending secret",
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.getByTestId("message-composer").getByAltText("Attachment cccc"),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("hidden spoiler links reveal without opening on the first click", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installSpoilerBridge(page);
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
|
||||
const input = page.getByTestId("message-input");
|
||||
await input.click();
|
||||
await page.keyboard.type("||[secret](https://example.com)||");
|
||||
await page.getByTestId("send-message").click();
|
||||
|
||||
const lastMessage = page.getByTestId("message-row").last();
|
||||
const spoiler = lastMessage.locator(".buzz-spoiler").first();
|
||||
await expect(spoiler).toHaveAttribute("data-revealed", "false");
|
||||
|
||||
// The freshly sent row can still be settling layout; a forced click
|
||||
// computed from a pre-shift bounding box lands on the wrong element
|
||||
// (force skips Playwright's stability check). Wait until the link's
|
||||
// position is identical across two animation frames before clicking.
|
||||
const secretLink = spoiler.getByRole("link", { name: "secret" });
|
||||
await secretLink.evaluate(
|
||||
(el) =>
|
||||
new Promise<void>((resolve) => {
|
||||
let last = -1;
|
||||
const tick = () =>
|
||||
requestAnimationFrame(() => {
|
||||
const { y } = el.getBoundingClientRect();
|
||||
if (y === last) resolve();
|
||||
else {
|
||||
last = y;
|
||||
tick();
|
||||
}
|
||||
});
|
||||
tick();
|
||||
}),
|
||||
);
|
||||
|
||||
const popupPromise = page
|
||||
.waitForEvent("popup", { timeout: 500 })
|
||||
.catch(() => null);
|
||||
await secretLink.click({ force: true });
|
||||
|
||||
const popup = await popupPromise;
|
||||
await popup?.close();
|
||||
expect(popup).toBeNull();
|
||||
await expect(spoiler).toHaveAttribute("data-revealed", "true");
|
||||
});
|
||||
|
||||
test("hidden spoilers stay masked on hover and focus until reveal", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installSpoilerBridge(page);
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
|
||||
const input = page.getByTestId("message-input");
|
||||
await input.click();
|
||||
await page.keyboard.type("||secret hover focus note||");
|
||||
await page.getByTestId("send-message").click();
|
||||
|
||||
const lastMessage = page.getByTestId("message-row").last();
|
||||
const spoiler = lastMessage.locator(".buzz-spoiler").first();
|
||||
const content = spoiler.locator(".buzz-spoiler__content");
|
||||
const particles = spoiler.locator(".buzz-spoiler__particles");
|
||||
|
||||
await expect(spoiler).toHaveAttribute("data-revealed", "false");
|
||||
await expect(content).toHaveCSS("opacity", "0");
|
||||
await expect(particles).toHaveCSS("opacity", "1");
|
||||
|
||||
await spoiler.hover();
|
||||
await expect(spoiler).toHaveAttribute("data-revealed", "false");
|
||||
await expect(content).toHaveCSS("opacity", "0");
|
||||
await expect(particles).toHaveCSS("opacity", "1");
|
||||
|
||||
await page.mouse.move(0, 0);
|
||||
await spoiler.focus();
|
||||
await expect(spoiler).toBeFocused();
|
||||
await expect(spoiler).toHaveAttribute("data-revealed", "false");
|
||||
await expect(content).toHaveCSS("opacity", "0");
|
||||
await expect(particles).toHaveCSS("opacity", "1");
|
||||
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(spoiler).toHaveAttribute("data-revealed", "true");
|
||||
await expect(content).toHaveCSS("opacity", "1");
|
||||
await expect(particles).toHaveCSS("opacity", "0");
|
||||
});
|
||||
|
||||
test("masked link inside a hidden spoiler does not leak its URL until revealed", async ({
|
||||
page,
|
||||
}) => {
|
||||
const SECRET_URL = "https://private.example/leak-path";
|
||||
await installSpoilerBridge(page);
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
|
||||
const input = page.getByTestId("message-input");
|
||||
await input.click();
|
||||
await page.keyboard.type(`||[secret](${SECRET_URL})||`);
|
||||
await page.getByTestId("send-message").click();
|
||||
|
||||
const lastMessage = page.getByTestId("message-row").last();
|
||||
const spoiler = lastMessage.locator(".buzz-spoiler").first();
|
||||
await expect(spoiler).toHaveAttribute("data-revealed", "false");
|
||||
|
||||
const secretLink = spoiler.getByRole("link", { name: "secret" });
|
||||
|
||||
// Neither hovering nor focusing the still-hidden link may open the URL
|
||||
// tooltip — that would leak the destination before the spoiler is revealed.
|
||||
await secretLink.hover({ force: true });
|
||||
await page.waitForTimeout(500); // exceed the tooltip open delay
|
||||
await expect(page.getByRole("tooltip")).toHaveCount(0);
|
||||
|
||||
await page.mouse.move(0, 0);
|
||||
await secretLink.focus();
|
||||
await page.waitForTimeout(500);
|
||||
await expect(page.getByRole("tooltip")).toHaveCount(0);
|
||||
await expect(page.getByText(SECRET_URL)).toHaveCount(0);
|
||||
await secretLink.blur();
|
||||
|
||||
// Reveal the spoiler (first click reveals; it must not open the link).
|
||||
await page.mouse.move(0, 0);
|
||||
await secretLink.evaluate(
|
||||
(el) =>
|
||||
new Promise<void>((resolve) => {
|
||||
let last = -1;
|
||||
const tick = () =>
|
||||
requestAnimationFrame(() => {
|
||||
const { y } = el.getBoundingClientRect();
|
||||
if (y === last) resolve();
|
||||
else {
|
||||
last = y;
|
||||
tick();
|
||||
}
|
||||
});
|
||||
tick();
|
||||
}),
|
||||
);
|
||||
await secretLink.click({ force: true });
|
||||
await expect(spoiler).toHaveAttribute("data-revealed", "true");
|
||||
|
||||
// Now revealed, hovering the link reveals the URL tooltip as normal.
|
||||
await secretLink.hover();
|
||||
await expect(page.getByRole("tooltip")).toContainText(SECRET_URL);
|
||||
});
|
||||
|
||||
test("non-interactive inbox preview spoilers let row clicks pass through", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installSpoilerBridge(page);
|
||||
await page.goto("/");
|
||||
await expect(page.getByTestId("home-inbox-list")).toBeVisible();
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
typeof (window as MockFeedWindow).__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__ ===
|
||||
"function",
|
||||
);
|
||||
|
||||
await page.evaluate(
|
||||
({ channelId, createdAt, currentPubkey, senderPubkey }) => {
|
||||
const pushFeedItem = (window as MockFeedWindow)
|
||||
.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__;
|
||||
if (!pushFeedItem) {
|
||||
throw new Error("Mock feed injection helper is not installed.");
|
||||
}
|
||||
|
||||
pushFeedItem({
|
||||
id: "mock-feed-spoiler-preview",
|
||||
kind: 9,
|
||||
pubkey: senderPubkey,
|
||||
content: "Preview contains ||hidden launch note|| for review.",
|
||||
created_at: createdAt,
|
||||
channel_id: channelId,
|
||||
channel_name: "general",
|
||||
tags: [
|
||||
["e", channelId],
|
||||
["p", currentPubkey],
|
||||
],
|
||||
category: "mention",
|
||||
});
|
||||
},
|
||||
{
|
||||
channelId: GENERAL_CHANNEL_ID,
|
||||
createdAt: Math.floor(Date.now() / 1000),
|
||||
currentPubkey: TEST_IDENTITIES.tyler.pubkey,
|
||||
senderPubkey: TEST_IDENTITIES.alice.pubkey,
|
||||
},
|
||||
);
|
||||
|
||||
const item = page.getByTestId("home-inbox-item-mock-feed-spoiler-preview");
|
||||
await expect(item).toContainText("Preview contains");
|
||||
|
||||
const spoiler = item.locator(".buzz-spoiler").first();
|
||||
await expect(spoiler).toBeVisible();
|
||||
await expect(spoiler).not.toHaveAttribute("role", "button");
|
||||
await expect(spoiler).not.toHaveAttribute("tabindex", "0");
|
||||
await expect(spoiler).toHaveCSS("pointer-events", "none");
|
||||
|
||||
const box = await spoiler.boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
if (!box) {
|
||||
throw new Error("Expected inbox preview spoiler to have a bounding box.");
|
||||
}
|
||||
|
||||
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
|
||||
await expect(page.getByTestId("home-inbox-detail")).toContainText(
|
||||
"Preview contains",
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user