Files
buzz/desktop/tests/e2e/file-attachment.spec.ts
klopez4212 99da5b7ebb Fix composer selection formatting and drop overlay (#3172)
## Summary
- scope code blocks and list formatting to the selected composer text
- use the Buzz primary color for the selection formatter
- extend the channel drop overlay over the composer with matching
corners, blur, and accessible contrast across themes

## Validation
- `just ci`
- composer selection formatting E2E tests
- file attachment and all-theme drop contrast E2E tests

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-07-27 20:39:44 +00:00

234 lines
8.4 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
import { expectCornerRadiusPx, expectSmoothCorners } from "../helpers/css";
// Exercises the generic file-attachment UI contract end-to-end through the
// mock Tauri bridge: paperclip upload → composer chip → send → FileCard in the
// timeline. This guards the frontend wiring (the riskiest, previously
// untested path). It does NOT prove the real relay store/serve round-trip —
// that lives in the Rust media + relay tests.
test.beforeEach(async ({ page }) => {
await installMockBridge(page, {
uploadDescriptors: [
{
url: `https://mock.relay/media/${"a".repeat(64)}.pdf`,
sha256: "a".repeat(64),
size: 12345,
type: "application/pdf",
uploaded: Math.floor(Date.now() / 1000),
filename: "quarterly-report.pdf",
},
],
});
});
test("upload a file and see a FileCard in the timeline", async ({ page }) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
// Paperclip → mocked pick_and_upload_media returns the PDF descriptor.
await page.getByRole("button", { name: "Attach image" }).click();
// The composer shows a chip with the original filename.
await expect(page.getByTestId("message-composer")).toContainText(
"quarterly-report.pdf",
);
// Send the (attachment-only) message.
await page.getByTestId("send-message").click();
await expect(page.getByText("Sending")).toHaveCount(0);
// A FileCard renders in the timeline: a button carrying the filename. It
// downloads via the native `download_file` command (HTTP inside the app's
// tunnel + save dialog), NOT a plain `<a download>` link — a bare link
// escapes the webview to the OS browser and hits a corporate CDN page.
const card = page.getByTestId("file-card").last();
await expect(card).toBeVisible();
await expectCornerRadiusPx(card, 16);
await expectSmoothCorners(card);
await expect(card).toContainText("quarterly-report.pdf");
await card.click();
await expect
.poll(() =>
page.evaluate(
() =>
(window as Window & { __BUZZ_E2E_COMMANDS__?: string[] })
.__BUZZ_E2E_COMMANDS__ ?? [],
),
)
.toContain("download_file");
});
test("dropping a file on the channel column attaches it to the composer", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const dataTransfer = await page.evaluateHandle(() => {
const transfer = new DataTransfer();
transfer.items.add(
new File(["quarterly report"], "quarterly-report.pdf", {
type: "application/pdf",
}),
);
return transfer;
});
const dropZone = page.getByTestId("channel-drop-zone");
await dropZone.dispatchEvent("dragenter", { dataTransfer });
const overlay = dropZone.getByTestId("drop-zone-overlay");
const label = dropZone.getByTestId("drop-zone-label");
await expect(overlay).toBeVisible();
await expect(label).toContainText("Drop files to upload");
const [dropZoneBox, overlayBox, overlayStyles, stacking] = await Promise.all([
dropZone.boundingBox(),
overlay.boundingBox(),
page.evaluate(() => {
const overlayElement = document.querySelector<HTMLElement>(
'[data-testid="drop-zone-overlay"]',
);
const contentSurface = document.querySelector<HTMLElement>(
"[data-buzz-content-surface]",
);
if (!(overlayElement && contentSurface)) return null;
const overlayStyle = getComputedStyle(overlayElement);
return {
backdropFilter: overlayStyle.backdropFilter,
containerRadius: getComputedStyle(contentSurface).borderRadius,
overlayRadius: overlayStyle.borderRadius,
};
}),
page.evaluate(() => {
const overlayElement = document.querySelector<HTMLElement>(
'[data-testid="drop-zone-overlay"]',
);
const composerOverlayElement = document.querySelector<HTMLElement>(
'[data-testid="channel-composer-overlay"]',
);
if (!(overlayElement && composerOverlayElement)) return null;
return {
composer: Number.parseInt(
getComputedStyle(composerOverlayElement).zIndex,
10,
),
dropZone: Number.parseInt(getComputedStyle(overlayElement).zIndex, 10),
};
}),
]);
expect(overlayBox).toEqual(dropZoneBox);
expect(overlayStyles).not.toBeNull();
expect(overlayStyles?.overlayRadius).toBe(overlayStyles?.containerRadius);
expect(overlayStyles?.backdropFilter).toContain("blur");
expect(stacking).not.toBeNull();
expect(stacking?.dropZone).toBeGreaterThan(stacking?.composer ?? 0);
await dropZone.dispatchEvent("drop", { dataTransfer });
await expect(page.getByTestId("message-composer")).toContainText(
"quarterly-report.pdf",
);
});
for (const theme of ["buzz", "buzz-dark", "github-light", "github-dark"]) {
test(`drop prompt has accessible text contrast in ${theme}`, async ({
page,
}) => {
await page.goto("/");
await page.evaluate((selectedTheme) => {
window.localStorage.setItem("buzz-theme", selectedTheme);
}, theme);
await page.reload();
await page.getByTestId("channel-general").click();
const dataTransfer = await page.evaluateHandle(() => {
const transfer = new DataTransfer();
transfer.items.add(
new File(["contrast check"], "contrast-check.txt", {
type: "text/plain",
}),
);
return transfer;
});
const dropZone = page.getByTestId("channel-drop-zone");
await dropZone.dispatchEvent("dragenter", { dataTransfer });
const contrastRatio = await dropZone
.getByTestId("drop-zone-label")
.evaluate((element) => {
const parseRgb = (value: string) =>
(value.match(/[\d.]+/g) ?? []).slice(0, 3).map(Number);
const luminance = (color: number[]) =>
color
.map((channel) => {
const value = channel / 255;
return value <= 0.04045
? value / 12.92
: ((value + 0.055) / 1.055) ** 2.4;
})
.reduce(
(sum, channel, index) =>
sum + channel * [0.2126, 0.7152, 0.0722][index],
0,
);
const style = getComputedStyle(element);
const foreground = luminance(parseRgb(style.color));
const background = luminance(parseRgb(style.backgroundColor));
return (
(Math.max(foreground, background) + 0.05) /
(Math.min(foreground, background) + 0.05)
);
});
expect(contrastRatio).toBeGreaterThanOrEqual(4.5);
});
}
test("forum posts emit a FileCard for generic attachments, not a broken image", async ({
page,
}) => {
// Regression guard for the ForumComposer bug: it used to hand-build content
// as `![image](url)` for every non-video attachment (and omit the `filename`
// imeta tag), so a PDF posted in a forum rendered as a broken inline image
// and lost its label. The fix routes forum/notes posts through the same
// `buildOutgoingMessage` builder as chat. This test would fail (no FileCard)
// if ForumComposer ever drifts back to hand-building media markdown.
await page.goto("/");
// "watercooler" is a seeded forum the mock identity is a member of.
await page.getByTestId("channel-watercooler").click();
// Open the new-post composer ("Start a new post...").
await page.getByRole("button", { name: "Start a new post..." }).click();
// Paperclip → mocked pick_and_upload_media returns the PDF descriptor.
await page.getByRole("button", { name: "Attach image" }).click();
// Submit the (attachment-only) forum post.
await page.getByTestId("send-message").click();
// The post renders through the shared Markdown component as a FileCard —
// a button carrying the filename that downloads via the native
// `download_file` command — NOT an inline image and NOT a bare link.
const card = page.getByTestId("file-card");
await expect(card).toBeVisible();
await expect(card).toContainText("quarterly-report.pdf");
await card.click();
await expect
.poll(() =>
page.evaluate(
() =>
(window as Window & { __BUZZ_E2E_COMMANDS__?: string[] })
.__BUZZ_E2E_COMMANDS__ ?? [],
),
)
.toContain("download_file");
});