Clean up screenshot e2e tests (#1284)

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Wes
2026-06-25 09:25:34 -07:00
committed by GitHub
parent e366372183
commit 2499d33971
20 changed files with 38 additions and 1219 deletions

View File

@@ -27,31 +27,29 @@ export default defineConfig({
"**/messaging.spec.ts",
"**/custom-emoji.spec.ts",
"**/profile-custom-emoji-status.spec.ts",
"**/custom-emoji-screenshots.spec.ts",
"**/channel-mute-screenshots.spec.ts",
"**/channel-star-screenshots.spec.ts",
"**/channel-controls-screenshots.spec.ts",
"**/team-management-screenshots.spec.ts",
"**/active-turn-screenshots.spec.ts",
"**/active-turn-resilience-screenshots.spec.ts",
"**/profile-active-turn-screenshots.spec.ts",
"**/custom-emoji-ui.spec.ts",
"**/channel-mute.spec.ts",
"**/channel-star.spec.ts",
"**/channel-controls.spec.ts",
"**/active-turn-resilience.spec.ts",
"**/profile-active-turn.spec.ts",
"**/file-attachment.spec.ts",
"**/video-attachment.spec.ts",
"**/spoiler.spec.ts",
"**/mentions.spec.ts",
"**/relay-reconnect.spec.ts",
"**/relay-reconnect-screenshots.spec.ts",
"**/relay-reconnect-affordance.spec.ts",
"**/workflows.spec.ts",
"**/identity-archive.spec.ts",
"**/identity-archive-hide.spec.ts",
"**/relay-connectivity-screenshots.spec.ts",
"**/unread-pill-screenshots.spec.ts",
"**/relay-connectivity.spec.ts",
"**/unread-pill.spec.ts",
"**/sidebar-more-unread-overlap.spec.ts",
"**/home-collapsed-top-chrome-screenshots.spec.ts",
"**/thread-unread-screenshots.spec.ts",
"**/animated-avatar-screenshots.spec.ts",
"**/reminders-screenshots.spec.ts",
"**/virtualization-screenshots.spec.ts",
"**/home-collapsed-top-chrome.spec.ts",
"**/thread-unread.spec.ts",
"**/animated-avatar.spec.ts",
"**/reminders.spec.ts",
"**/virtualization.spec.ts",
"**/scroll-history.spec.ts",
],
use: {

View File

@@ -2,8 +2,6 @@ import { expect, test } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
const SHOTS = "test-results/active-turn-resilience";
// Mock agent pubkeys (distinct from the relay agents seeded by default).
const AGENT_PAUL = "aa".repeat(32);
const AGENT_DUNCAN = "bb".repeat(32);
@@ -61,7 +59,7 @@ async function seedTurns(
}, turns);
}
test.describe("active turn badge resilience screenshots", () => {
test.describe("active turn badge resilience", () => {
test.use({ viewport: { width: 1280, height: 720 } });
test("badges persist through an all-at-once liveness gap", async ({
@@ -114,11 +112,6 @@ test.describe("active turn badge resilience screenshots", () => {
await expect(paulRow).toContainText("Working", { timeout: 5_000 });
await expect(duncanRow).toContainText("Working", { timeout: 5_000 });
const agentsSection = page.getByTestId("unified-agents-groups");
await agentsSection.screenshot({
path: `${SHOTS}/01-badges-before-gap.png`,
});
// Simulate the all-at-once relay drop: no further frames, advance the clock
// past both thresholds. This fires several real prune ticks; shouldPausePrune
// sees every turn's lastActivityAt stuck at T0 (gap > 20s) and pauses the
@@ -128,9 +121,5 @@ test.describe("active turn badge resilience screenshots", () => {
await expect(paulRow).toContainText("Working");
await expect(duncanRow).toContainText("Working");
await agentsSection.screenshot({
path: `${SHOTS}/02-badges-survive-gap.png`,
});
});
});

View File

@@ -1,199 +0,0 @@
import { expect, test } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
const SHOTS = "test-results/active-turns";
// Mock agent pubkeys (distinct from the relay agents seeded by default)
const AGENT_PAUL = "aa".repeat(32);
const AGENT_DUNCAN = "bb".repeat(32);
const AGENT_THUFIR = "cc".repeat(32);
// Mock channel IDs from the e2e bridge
const CHANNEL_GENERAL = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
const CHANNEL_ENGINEERING = "1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9";
async function waitForBridge(page: import("@playwright/test").Page) {
await page.waitForFunction(
() =>
typeof (window as Window & { __BUZZ_E2E_SEED_ACTIVE_TURNS__?: unknown })
.__BUZZ_E2E_SEED_ACTIVE_TURNS__ === "function",
null,
{ timeout: 10_000 },
);
}
async function openAgentsView(page: import("@playwright/test").Page) {
await page.goto("/", { waitUntil: "domcontentloaded" });
await waitForBridge(page);
await page.getByTestId("open-agents-view").click();
await expect(page.getByTestId("unified-agents-groups")).toBeVisible({
timeout: 10_000,
});
}
test.describe("active turn indicator screenshots", () => {
test.use({ viewport: { width: 1280, height: 720 } });
test("01 — baseline: agents running but idle", async ({ page }) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: AGENT_PAUL,
name: "Paul",
status: "running",
channelNames: ["general", "engineering"],
},
{
pubkey: AGENT_DUNCAN,
name: "Duncan",
status: "running",
channelNames: ["general", "design"],
},
{
pubkey: AGENT_THUFIR,
name: "Thufir",
status: "stopped",
channelNames: [],
},
],
});
await openAgentsView(page);
const agentsSection = page.getByTestId("unified-agents-groups");
await expect(agentsSection).toContainText("Paul");
await expect(agentsSection).toContainText("Duncan");
await expect(agentsSection).toContainText("Thufir");
await agentsSection.screenshot({
path: `${SHOTS}/01-baseline-idle.png`,
});
});
test("02 — single agent working in one channel", async ({ page }) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: AGENT_PAUL,
name: "Paul",
status: "running",
channelNames: ["general", "engineering"],
},
{
pubkey: AGENT_DUNCAN,
name: "Duncan",
status: "running",
channelNames: ["general", "design"],
},
{
pubkey: AGENT_THUFIR,
name: "Thufir",
status: "stopped",
channelNames: [],
},
],
});
await openAgentsView(page);
await waitForBridge(page);
// Seed Paul as actively working in #general
await page.evaluate(
({ pubkey, channelId }) => {
const win = window as Window & {
__BUZZ_E2E_SEED_ACTIVE_TURNS__?: (input: {
agentPubkey: string;
channelId: string;
turnId: string;
}) => void;
};
win.__BUZZ_E2E_SEED_ACTIVE_TURNS__?.({
agentPubkey: pubkey,
channelId,
turnId: "turn-001",
});
},
{ pubkey: AGENT_PAUL, channelId: CHANNEL_GENERAL },
);
// Wait for the "Working" badge to appear
await expect(page.getByTestId(`managed-agent-${AGENT_PAUL}`)).toContainText(
"Working",
{ timeout: 5_000 },
);
const agentsSection = page.getByTestId("unified-agents-groups");
await agentsSection.screenshot({
path: `${SHOTS}/02-single-agent-working.png`,
});
});
test("03 — mixed states: one working in 2 channels, one idle, one stopped", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: AGENT_PAUL,
name: "Paul",
status: "running",
channelNames: ["general", "engineering"],
},
{
pubkey: AGENT_DUNCAN,
name: "Duncan",
status: "running",
channelNames: ["general", "design"],
},
{
pubkey: AGENT_THUFIR,
name: "Thufir",
status: "stopped",
channelNames: [],
},
],
});
await openAgentsView(page);
await waitForBridge(page);
// Seed Paul as working in both #general and #engineering
await page.evaluate(
({ pubkey, channels }) => {
const win = window as Window & {
__BUZZ_E2E_SEED_ACTIVE_TURNS__?: (input: {
agentPubkey: string;
channelId: string;
turnId: string;
}) => void;
};
for (const { channelId, turnId } of channels) {
win.__BUZZ_E2E_SEED_ACTIVE_TURNS__?.({
agentPubkey: pubkey,
channelId,
turnId,
});
}
},
{
pubkey: AGENT_PAUL,
channels: [
{ channelId: CHANNEL_GENERAL, turnId: "turn-002" },
{ channelId: CHANNEL_ENGINEERING, turnId: "turn-003" },
],
},
);
// Wait for the "Working" indicators to appear
await expect(page.getByTestId(`managed-agent-${AGENT_PAUL}`)).toContainText(
"Working",
{ timeout: 5_000 },
);
const agentsSection = page.getByTestId("unified-agents-groups");
await agentsSection.screenshot({
path: `${SHOTS}/03-mixed-states.png`,
});
});
});

View File

@@ -3,8 +3,6 @@ import { expect, test } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
import { openSettings } from "../helpers/settings";
const SHOTS = "test-results/animated-avatar";
/**
* Stub getUserMedia with a canvas-generated stream (animated gradient with a
* moving circle) so the camera phases work headless and deterministically —
@@ -118,8 +116,7 @@ function installFakeCamera(
}
// The review editor (preview + framing + poster strip + backdrop panel) is
// taller than the default 720px viewport — raise it so element screenshots
// capture the whole editor.
// taller than the default 720px viewport — raise it so the whole editor remains visible.
test.use({ viewport: { height: 1280, width: 1280 } });
const UPLOADED_PNG_DESCRIPTOR = {
@@ -147,7 +144,7 @@ async function openAnimatedTab(
await page.waitForTimeout(400);
}
test.describe("animated avatar screenshots", () => {
test.describe("animated avatar", () => {
test("01 — animated tab idle state", async ({ page }) => {
await openAnimatedTab(page);
await expect(
@@ -169,9 +166,6 @@ test.describe("animated avatar screenshots", () => {
page.getByTestId("profile-avatar-animated-camera-select"),
).toHaveCount(0);
await expect(page.getByTestId("profile-avatar-done")).toHaveCount(0);
await page
.getByTestId("settings-profile")
.screenshot({ path: `${SHOTS}/01-animated-tab-idle.png` });
});
test("02 — live camera preview", async ({ page }) => {
@@ -247,9 +241,6 @@ test.describe("animated avatar screenshots", () => {
);
await expect(page.getByTestId("profile-avatar-done")).toHaveCount(0);
await page.waitForTimeout(500);
await page
.getByTestId("settings-profile")
.screenshot({ path: `${SHOTS}/02-live-camera.png` });
await page.getByTestId("profile-avatar-animated-record").click();
await expect(
@@ -276,10 +267,6 @@ test.describe("animated avatar screenshots", () => {
page.getByTestId("profile-avatar-animated-timer-ring"),
).toBeVisible();
await page.waitForTimeout(1500);
await page.getByTestId("settings-profile").screenshot({
animations: "allow",
path: `${SHOTS}/03-recording-ring.png`,
});
// Review: the section icon row appears once processing finishes.
await expect(
@@ -340,10 +327,6 @@ test.describe("animated avatar screenshots", () => {
},
});
await expect(scrubber).not.toHaveAttribute("aria-valuenow", "0");
await page.getByTestId("settings-profile").screenshot({
animations: "allow",
path: `${SHOTS}/06-poster-picker.png`,
});
// The Circle tool stays implemented, but it is hidden in the review row for now.
await expect(
@@ -379,13 +362,9 @@ test.describe("animated avatar screenshots", () => {
"aria-valuenow",
initialSizeValue ?? "",
);
// Restore defaults so the screenshot shows the standard framing.
// Restore defaults so the standard framing remains covered.
await page.getByTestId("profile-avatar-animated-reset-framing").click();
await page.waitForTimeout(400);
await page.getByTestId("settings-profile").screenshot({
animations: "allow",
path: `${SHOTS}/04-review-popout-backdrop.png`,
});
// Custom backdrop color panel (shared HSV picker).
await page.getByTestId("profile-avatar-animated-section-color").click();
@@ -413,10 +392,6 @@ test.describe("animated avatar screenshots", () => {
await page.waitForTimeout(400);
const customColorPreview = await previewSnapshot();
expect(customColorPreview).not.toEqual(beforeCustomColor);
await page.getByTestId("settings-profile").screenshot({
animations: "allow",
path: `${SHOTS}/05-custom-backdrop-color.png`,
});
await page.getByTestId("profile-avatar-animated-custom-color-done").click();
await expect(
page.getByTestId("profile-avatar-animated-backdrop-custom"),

View File

@@ -2,8 +2,6 @@ import { expect, test } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
const SHOTS = "test-results/channel-controls";
// `general` seeds the mock identity as owner, so the owner/admin-gated
// visibility + ephemeral controls are live and interactive.
async function openManagementSheet(page: import("@playwright/test").Page) {
@@ -27,7 +25,7 @@ async function settle(page: import("@playwright/test").Page) {
);
}
test.describe("channel controls screenshots", () => {
test.describe("channel controls", () => {
test("01 — lifecycle section: Private + Ephemeral switches", async ({
page,
}) => {
@@ -44,8 +42,6 @@ test.describe("channel controls screenshots", () => {
page.getByTestId("channel-management-ephemeral-toggle"),
).toBeVisible();
await settle(page);
await lifecycle.screenshot({ path: `${SHOTS}/01-lifecycle-default.png` });
});
test("02 — Private toggled on", async ({ page }) => {
@@ -63,8 +59,6 @@ test.describe("channel controls screenshots", () => {
page.getByTestId("channel-management-save-changes"),
).toBeEnabled();
await settle(page);
await lifecycle.screenshot({ path: `${SHOTS}/02-private-on.png` });
});
test("03 — Ephemeral on with friendly timeout field", async ({ page }) => {
@@ -83,8 +77,6 @@ test.describe("channel controls screenshots", () => {
page.getByTestId("channel-management-save-changes"),
).toBeEnabled();
await settle(page);
await lifecycle.screenshot({ path: `${SHOTS}/03-ephemeral-ttl.png` });
});
test("04 — invalid timeout blocks save with inline error", async ({
@@ -105,8 +97,6 @@ test.describe("channel controls screenshots", () => {
page.getByTestId("channel-management-save-changes"),
).toBeDisabled();
await settle(page);
await lifecycle.screenshot({ path: `${SHOTS}/04-ttl-invalid.png` });
});
test("05 — sticky footer pins lifecycle buttons", async ({ page }) => {
@@ -117,8 +107,6 @@ test.describe("channel controls screenshots", () => {
await expect(footer).toBeVisible();
await expect(page.getByTestId("channel-management-archive")).toBeVisible();
await settle(page);
await footer.screenshot({ path: `${SHOTS}/05-sticky-footer.png` });
});
test("06 — full sheet with new controls", async ({ page }) => {
@@ -128,8 +116,6 @@ test.describe("channel controls screenshots", () => {
const sheet = page.getByTestId("channel-management-sheet");
await expect(sheet).toBeVisible();
await settle(page);
await sheet.screenshot({ path: `${SHOTS}/06-management-sheet.png` });
});
test("07 — saving lifecycle uses unified save", async ({ page }) => {
@@ -137,7 +123,6 @@ test.describe("channel controls screenshots", () => {
await openManagementSheet(page);
await openEditDialog(page);
const sheet = page.getByTestId("channel-management-sheet");
await page.getByTestId("channel-management-ephemeral-toggle").click();
await expect(
page.getByTestId("channel-management-save-changes"),
@@ -147,9 +132,6 @@ test.describe("channel controls screenshots", () => {
await expect(
page.getByTestId("channel-management-save-changes"),
).toHaveText("Saving...");
await sheet.screenshot({
path: `${SHOTS}/07-lifecycle-saving-unified-save.png`,
});
await expect(
page.getByRole("dialog", { name: "Edit channel" }),
@@ -187,9 +169,5 @@ test.describe("channel controls screenshots", () => {
).toHaveAttribute("data-state", "checked");
await expect(page.getByTestId("channel-management-ttl")).toHaveValue("7d");
await settle(page);
await lifecycle.screenshot({
path: `${SHOTS}/08-ephemeral-persisted-after-reopen.png`,
});
});
});

View File

@@ -5,7 +5,6 @@ import { TEST_IDENTITIES, installMockBridge } from "../helpers/bridge";
const MOCK_PUBKEY = "deadbeef".repeat(8);
const ENGINEERING_CHANNEL_ID = "1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9";
const MUTE_STORAGE_KEY = `buzz-channel-mutes.v1:${MOCK_PUBKEY}`;
const SHOTS = "test-results/channel-mute";
function seedMuteState(
page: import("@playwright/test").Page,
@@ -49,7 +48,7 @@ async function waitForMockLiveSubscription(
.toBe(true);
}
test.describe("channel muting screenshots", () => {
test.describe("channel muting", () => {
test("01 — context menu shows Mute channel", async ({ page }) => {
await installMockBridge(page);
await page.goto("/");
@@ -70,11 +69,6 @@ test.describe("channel muting screenshots", () => {
.map((a) => a.finished) ?? [],
),
);
await page.screenshot({
path: `${SHOTS}/01-context-menu-mute.png`,
clip: { x: 0, y: 0, width: 450, height: 720 },
});
});
test("02 — muted channel is dimmed with BellOff icon", async ({ page }) => {
@@ -89,11 +83,6 @@ test.describe("channel muting screenshots", () => {
await expect(engRow).toBeVisible();
await expect(engRow).toHaveCSS("opacity", "0.5");
await expect(engRow.locator("svg.lucide-bell-off")).toHaveCount(1);
await page.screenshot({
path: `${SHOTS}/02-muted-channel-dimmed.png`,
clip: { x: 0, y: 0, width: 256, height: 720 },
});
});
test("03 — muted channel with @mention shows unread dot", async ({
@@ -135,11 +124,6 @@ test.describe("channel muting screenshots", () => {
);
await expect(page.getByTestId("channel-unread-engineering")).toBeVisible();
await page.screenshot({
path: `${SHOTS}/03-muted-with-mention.png`,
clip: { x: 0, y: 0, width: 256, height: 720 },
});
});
test("04 — context menu shows Unmute channel when muted", async ({
@@ -163,11 +147,6 @@ test.describe("channel muting screenshots", () => {
.map((a) => a.finished) ?? [],
),
);
await page.screenshot({
path: `${SHOTS}/04-context-menu-unmute.png`,
clip: { x: 0, y: 0, width: 450, height: 720 },
});
});
test("05 — muted icon visible on selected channel", async ({ page }) => {
@@ -180,10 +159,5 @@ test.describe("channel muting screenshots", () => {
const engRow = page.getByTestId("channel-engineering");
await expect(engRow.locator("svg.lucide-bell-off")).toHaveCount(1);
await page.screenshot({
path: `${SHOTS}/05-muted-icon-on-selected.png`,
clip: { x: 0, y: 0, width: 256, height: 720 },
});
});
});

View File

@@ -5,8 +5,6 @@ import { installMockBridge } from "../helpers/bridge";
const MOCK_PUBKEY = "deadbeef".repeat(8);
const ENGINEERING_CHANNEL_ID = "1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9";
const STAR_STORAGE_KEY = `buzz-channel-stars.v1:${MOCK_PUBKEY}`;
const SHOTS = "test-results/channel-star";
function seedStarState(
page: import("@playwright/test").Page,
channelId: string,
@@ -27,7 +25,7 @@ function seedStarState(
);
}
test.describe("channel starring screenshots", () => {
test.describe("channel starring", () => {
test("01 — context menu shows Star channel", async ({ page }) => {
await installMockBridge(page);
await page.goto("/");
@@ -45,11 +43,6 @@ test.describe("channel starring screenshots", () => {
.map((a) => a.finished) ?? [],
),
);
await page.screenshot({
path: `${SHOTS}/01-context-menu-star.png`,
clip: { x: 0, y: 0, width: 450, height: 720 },
});
});
test("02 — starred channel appears in Starred section", async ({ page }) => {
@@ -63,11 +56,6 @@ test.describe("channel starring screenshots", () => {
const starredList = page.getByTestId("starred-list");
await expect(starredList).toBeVisible();
await expect(starredList.getByTestId("channel-engineering")).toBeVisible();
await page.screenshot({
path: `${SHOTS}/02-starred-section.png`,
clip: { x: 0, y: 0, width: 256, height: 720 },
});
});
test("03 — context menu shows Unstar channel when starred", async ({
@@ -94,11 +82,6 @@ test.describe("channel starring screenshots", () => {
.map((a) => a.finished) ?? [],
),
);
await page.screenshot({
path: `${SHOTS}/03-context-menu-unstar.png`,
clip: { x: 0, y: 0, width: 450, height: 720 },
});
});
test("04 — starred channel is removed from the Channels group", async ({

View File

@@ -2,19 +2,13 @@ import { expect, test } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
// Screenshot capture for the user-owned custom-emoji rebuild (PR 816). Not a
// hard assertion suite — it documents the two user-visible surfaces Tyler asked
// to verify: the composer rendering and the settings card's own-vs-workspace
// split. Artifacts land in test-results/.
const SHORTCODE = "buzz";
const SHOTS = "test-results/custom-emoji";
test.beforeEach(async ({ page }) => {
await installMockBridge(page);
// The mock emoji sets point at example.com placeholder URLs that don't
// resolve, so the <img> would render broken in screenshots. Serve a visible
// square glyph for any example.com emoji image so the captures show the
// custom-emoji sizing/alignment rather than a broken-image icon.
// resolve, so the <img> would render broken. Serve a visible square glyph
// for any example.com emoji image so sizing/alignment can be asserted.
const SVG = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="7" fill="#22c55e"/>
@@ -35,8 +29,6 @@ test("composer renders a custom emoji inline", async ({ page }) => {
await input.click();
await input.pressSequentially(`shipping it :${SHORTCODE}:`);
await expect(input.locator("img[data-custom-emoji]")).toHaveCount(1);
await page.screenshot({ path: `${SHOTS}/01-composer-inline-emoji.png` });
});
test("settings card splits My emoji from read-only Workspace emoji", async ({
@@ -63,11 +55,6 @@ test("settings card splits My emoji from read-only Workspace emoji", async ({
await expect(
workspace.getByRole("button", { name: /^Remove :/ }),
).toHaveCount(0);
await page.screenshot({
path: `${SHOTS}/02-settings-own-vs-workspace.png`,
fullPage: true,
});
});
test("message list renders inline and emoji-only messages with Slack-style emoji sizing", async ({
@@ -126,8 +113,4 @@ test("message list renders inline and emoji-only messages with Slack-style emoji
return emojiOnlyBox.height / inlineBox.height;
})
.toBeGreaterThan(1.8);
await page.screenshot({
path: `${SHOTS}/03-message-list-emoji-sizing.png`,
});
});

View File

@@ -3,8 +3,6 @@ import { expect, test } from "@playwright/test";
import { waitForAnimations } from "../helpers/animations";
import { installMockBridge } from "../helpers/bridge";
const SHOTS = "test-results/home-collapsed-top-chrome";
test.describe("home inbox header collapsed-sidebar chrome clearance", () => {
test.use({ viewport: { width: 1280, height: 720 } });
@@ -28,9 +26,5 @@ test.describe("home inbox header collapsed-sidebar chrome clearance", () => {
.toBeGreaterThanOrEqual(168);
await waitForAnimations(page);
await page.screenshot({
path: `${SHOTS}/01-collapsed-inbox-header-clears-traffic-lights.png`,
clip: { x: 0, y: 0, width: 420, height: 120 },
});
});
});

View File

@@ -238,8 +238,7 @@ test("env vars editor renders in PersonaDialog new-persona form", async ({
await page.getByTestId("env-vars-add").click();
await expect(page.getByTestId("env-vars-key")).toHaveCount(1);
// Fill it in. Use realistic-looking keys/values so the screenshot
// captured below illustrates the feature for reviewers.
// Fill it in with realistic-looking keys/values to cover masked secrets and row controls.
const keys = page.getByTestId("env-vars-key");
const values = page.getByTestId("env-vars-value");
await keys.nth(0).fill("ANTHROPIC_API_KEY");
@@ -251,12 +250,6 @@ test("env vars editor renders in PersonaDialog new-persona form", async ({
await keys.nth(2).fill("OPENAI_BASE_URL");
await values.nth(2).fill("https://api.openai.com/v1");
// Capture a screenshot of the dialog with three env vars filled. Helps
// reviewers see the UI at a glance.
await page
.getByRole("dialog")
.screenshot({ path: "test-results/persona-env-dialog.png" });
// Remove the first row to verify per-row removal still works.
await page.getByTestId("env-vars-remove").first().click();
await expect(keys).toHaveCount(2);

View File

@@ -2,8 +2,6 @@ import { expect, test } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
const SHOTS = "test-results/profile-active-turns";
// Charlie is a `bot` member of #agents and authors the seeded "Indexing the
// channel catalog now." message (see e2eBridge.ts). Seeding a managed agent
// with this same pubkey makes the message avatar open a managed-agent profile
@@ -77,7 +75,7 @@ function agentAvatar(page: import("@playwright/test").Page) {
return page.getByTestId("message-row").last().getByRole("button").first();
}
test.describe("profile active turn indicator screenshots", () => {
test.describe("profile active turn indicator", () => {
test.use({ viewport: { width: 1280, height: 720 } });
test("01 — profile panel: agent working in one channel", async ({ page }) => {
@@ -94,10 +92,6 @@ test.describe("profile active turn indicator screenshots", () => {
await expect(panel).toContainText("Working in #general", {
timeout: 5_000,
});
await panel.screenshot({
path: `${SHOTS}/01-profile-panel-single-channel.png`,
});
});
test("02 — profile panel: agent working in two channels", async ({
@@ -118,10 +112,6 @@ test.describe("profile active turn indicator screenshots", () => {
timeout: 5_000,
});
await expect(panel).toContainText("Working in #engineering");
await panel.screenshot({
path: `${SHOTS}/02-profile-panel-multi-channel.png`,
});
});
test("03 — hover popover: agent working", async ({ page }) => {
@@ -136,9 +126,5 @@ test.describe("profile active turn indicator screenshots", () => {
const popover = page.getByTestId("user-profile-popover");
await expect(popover).toBeVisible({ timeout: 5_000 });
await expect(popover).toContainText("Working in #general");
await popover.screenshot({
path: `${SHOTS}/03-popover-working.png`,
});
});
});

View File

@@ -1,434 +0,0 @@
import { expect, type Page, test } from "@playwright/test";
import { decode } from "nostr-tools/nip19";
import { getPublicKey } from "nostr-tools/pure";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
// Ground-truth reproduction for Tyler's "reactions don't show on first load"
// bug. Drives the REAL GUI (relay-mode e2e bridge) against the staging relay
// with Eva's real key, cold-loads #buzz-bugs, and observes whether messages
// known to carry reactions render their reaction pills on first paint vs only
// after a channel switch-away-and-back.
//
// NOT a CI test — points at a live external relay and a real member identity.
// Run explicitly: pnpm exec playwright test reaction-coldload-repro
const RELAY_WS = "wss://sprout-oss.stage.blox.sqprod.co";
const RELAY_HTTP = "https://sprout-oss.stage.blox.sqprod.co";
const BUZZ_BUGS_NAME = "buzz-bugs";
function loadTestIdentity() {
const rawPrivateKey = process.env.BUZZ_PRIVATE_KEY ?? "";
if (!rawPrivateKey) {
throw new Error("BUZZ_PRIVATE_KEY is required for the live staging repro");
}
const privateKey = rawPrivateKey.startsWith("nsec")
? bytesToHex(decode(rawPrivateKey).data)
: bytesToHex(hexToBytes(rawPrivateKey));
return {
privateKey,
pubkey: getPublicKey(hexToBytes(privateKey)),
username: "Max",
};
}
const TEST_IDENTITY = loadTestIdentity();
async function collectRenderedReactionState(page: Page) {
return page.locator("[data-message-id]").evaluateAll((rows) =>
rows.map((row) => {
const el = row as HTMLElement;
const reactions = Array.from(
el.querySelectorAll<HTMLElement>(
'[data-testid="message-reactions"] button',
),
).map(
(button) =>
button.getAttribute("aria-label") ?? button.textContent?.trim() ?? "",
);
return {
id: el.dataset.messageId ?? "",
hasReactions: reactions.length > 0,
reactions,
text: el.innerText.replace(/\s+/g, " ").slice(0, 120),
};
}),
);
}
test("cold-load buzz-bugs and observe reaction render", async ({ page }) => {
test.setTimeout(180_000);
// Seed workspace + onboarding for Eva's pubkey so the app skips WelcomeSetup
// and boots straight into the workspace pointed at staging.
await page.addInitScript(
({ relayUrl, pubkey }) => {
const workspaceId = "e2e-repro-workspace";
window.localStorage.setItem(
"buzz-workspaces",
JSON.stringify([
{
id: workspaceId,
name: "Staging Repro",
relayUrl,
addedAt: new Date().toISOString(),
},
]),
);
window.localStorage.setItem("buzz-active-workspace-id", workspaceId);
const scope = encodeURIComponent(relayUrl);
window.localStorage.setItem(
`buzz-onboarding-complete.v1:${pubkey}`,
"true",
);
window.localStorage.setItem(
`buzz-welcome-channel-ensured.v1:${scope}:${pubkey}`,
"true",
);
},
{ relayUrl: RELAY_WS, pubkey: TEST_IDENTITY.pubkey },
);
await page.addInitScript(
({ identity, relayHttpUrl, relayWsUrl }) => {
(window as unknown as { __BUZZ_E2E__: unknown }).__BUZZ_E2E__ = {
mode: "relay",
identity,
relayHttpUrl,
relayWsUrl,
};
},
{ identity: TEST_IDENTITY, relayHttpUrl: RELAY_HTTP, relayWsUrl: RELAY_WS },
);
const consoleLines: string[] = [];
page.on("console", (msg) => {
consoleLines.push(`[${msg.type()}] ${msg.text()}`);
});
page.on("pageerror", (err) => {
consoleLines.push(`[pageerror] ${err.message}`);
});
// The staging relay's HTTP API does not return Access-Control-Allow-Origin,
// so a browser fetch from the test origin (127.0.0.1:4173) is blocked by
// CORS — every get_channels / backfill /query fails with "Failed to fetch".
// That's a harness artifact, NOT Tyler's bug. Proxy the relay's HTTP origin
// through Node fetch (no CORS) and inject the missing CORS header so the
// real GUI data path runs exactly as shipped, just reachable from the test.
const relayQueryBodies: string[] = [];
await page.route(`${RELAY_HTTP}/**`, async (route) => {
const req = route.request();
const url = req.url();
const postData = req.postData() ?? undefined;
if (url.endsWith("/query") && postData) {
relayQueryBodies.push(postData.slice(0, 400));
}
const headers = { ...req.headers() };
delete headers.origin;
// The staging relay sits behind a WAF that 403s any request whose
// User-Agent contains the `Mozilla/` token (confirmed: `Mozilla/5.0` ->
// 403, `curl`/`buzz-desktop`/empty -> 200). Chromium always sends a
// Mozilla UA, so the browser data path is blocked at infra — a harness
// artifact, NOT Tyler's bug (the real Tauri app queries via the Rust
// reqwest client, which is not browser-UA-shaped). Rewrite the UA so the
// shipped data path can run from the test browser.
headers["user-agent"] = "buzz-desktop-e2e";
const upstream = await fetch(url, {
method: req.method(),
headers,
body:
req.method() === "GET" || req.method() === "HEAD"
? undefined
: postData,
});
const bodyBuf = Buffer.from(await upstream.arrayBuffer());
const respHeaders: Record<string, string> = {};
upstream.headers.forEach((v, k) => {
respHeaders[k] = v;
});
respHeaders["access-control-allow-origin"] = "*";
respHeaders["access-control-allow-headers"] = "*";
respHeaders["access-control-allow-methods"] = "*";
await route.fulfill({
status: upstream.status,
headers: respHeaders,
body: bodyBuf,
});
});
// Capture the aux backfill REQ + EVENT traffic over the websocket.
const wsFrames: string[] = [];
// The same `Mozilla/`-UA WAF that 403s the HTTP path also 403s the browser's
// WS upgrade (confirmed by probe), and message history + reaction backfill
// both ride the WebSocket, not the HTTP /query bridge. Playwright's
// `connectToServer()` is ALSO WAF-blocked (its upstream handshake closes
// 1006), and it exposes no way to set the UA. Node's built-in global
// `WebSocket` (undici) connects fine — its UA is not `Mozilla/`-shaped — so
// bridge the page's WS to a Node-side undici WebSocket by hand. The shipped
// WS data path (auth, history, aux reaction backfill) then runs as-is, just
// reachable from the test browser.
// Per-REQ latency tracking: maps a subscription id to the wall-clock time
// its REQ was sent, so we can measure REQ -> EOSE latency for each aux
// backfill chunk. This is what separates "deterministic >8s timeout" (a
// consistent GUI bug, as Tyler insists) from "random latency".
const reqSentAt = new Map<string, number>();
const reqFilters = new Map<string, string>();
// Aux-backfill subscription tracking + a bridge-throughput counter, to
// distinguish "relay never sent the EOSE" from "bridge stalled and dropped
// it" (harness artifact). auxSubIds holds the aux REQ ids (kinds 5,7,9005,
// 40003 keyed by #e); auxFrameLog records every upstream frame for them.
const auxSubIds = new Set<string>();
const auxFrameLog: string[] = [];
let upstreamFrameCount = 0;
const t0 = Date.now();
const rel = () => `${((Date.now() - t0) / 1000).toFixed(2)}s`;
await page.routeWebSocket(/sprout-oss/, (ws) => {
wsFrames.push(`ROUTE HIT url=${ws.url()}`);
const upstream = new WebSocket(ws.url());
const pageQueue: (string | Buffer)[] = [];
let upstreamOpen = false;
upstream.addEventListener("open", () => {
upstreamOpen = true;
for (const m of pageQueue) upstream.send(m);
pageQueue.length = 0;
});
upstream.addEventListener("message", (ev: MessageEvent) => {
const m = ev.data as string;
upstreamFrameCount++;
if (typeof m === "string" && m.startsWith("[")) {
try {
const arr = JSON.parse(m) as unknown[];
const verb = arr[0];
const sid = arr[1];
// Track every frame for the aux-backfill sub specifically, so we can
// tell whether the relay delivered its EVENTs/EOSE to the bridge even
// if the page never rendered them (= harness bottleneck, not the bug).
if (typeof sid === "string" && auxSubIds.has(sid)) {
auxFrameLog.push(
`[${rel()}] AUX-FRAME #${upstreamFrameCount} verb=${verb} sid=${sid.slice(0, 20)}`,
);
}
if (verb === "EOSE" && typeof sid === "string") {
const sent = reqSentAt.get(sid);
const filt = reqFilters.get(sid) ?? "";
if (sent !== undefined) {
wsFrames.push(
`EOSE sid=${sid} latency=${((Date.now() - sent) / 1000).toFixed(2)}s filter=${filt}`,
);
reqSentAt.delete(sid);
}
}
} catch {
/* not JSON array */
}
}
if (
typeof m === "string" &&
(m.includes('"kind":7') ||
m.includes("EOSE") ||
m.includes("OK") ||
m.includes("AUTH"))
)
wsFrames.push(`[${rel()}] << ${m.slice(0, 200)}`);
ws.send(m);
});
upstream.addEventListener("close", (ev: CloseEvent) => {
wsFrames.push(`UPSTREAM CLOSE code=${ev.code}`);
void ws
.close({ code: ev.code || 1000, reason: "upstream closed" })
.catch(() => {});
});
upstream.addEventListener("error", () => wsFrames.push(`UPSTREAM ERROR`));
ws.onMessage((m) => {
const p = typeof m === "string" ? m : "<binary>";
if (typeof m === "string" && m.startsWith("[")) {
try {
const arr = JSON.parse(m) as unknown[];
if (arr[0] === "REQ" && typeof arr[1] === "string") {
const sid = arr[1];
reqSentAt.set(sid, Date.now());
// Record the kinds + which #e tag count so we can attribute the
// latency to aux-backfill (kinds 5,7,9005,40003 + #e) vs other REQs.
const filterObj = arr[2] as
| { kinds?: number[]; "#e"?: string[] }
| undefined;
const kinds = filterObj?.kinds?.join(",") ?? "?";
const eCount = filterObj?.["#e"]?.length ?? 0;
reqFilters.set(sid, `kinds=[${kinds}] #e=${eCount}`);
// Aux backfill REQs are keyed by #e. Post kind-split (the fix),
// reactions ride a kind:7-only REQ and the structural overlay rides
// a 5/9005/40003 REQ; pre-fix they were one bundled 5+7+... REQ.
// Track all aux REQs so we can measure each one's REQ->EOSE latency
// and prove the reaction REQ now beats the 8s timeout.
const kindSet = new Set(filterObj?.kinds ?? []);
const isReactionReq =
eCount > 0 && kindSet.has(7) && !kindSet.has(5);
const isStructuralReq =
eCount > 0 && kindSet.has(5) && !kindSet.has(7);
const isBundledReq = eCount > 0 && kindSet.has(7) && kindSet.has(5);
if (isReactionReq || isStructuralReq || isBundledReq) {
auxSubIds.add(sid);
}
wsFrames.push(
`[${rel()}] REQ sid=${sid} kinds=[${kinds}] #e=${eCount}${
isReactionReq
? " [REACTION-AUX]"
: isStructuralReq
? " [STRUCTURAL-AUX]"
: isBundledReq
? " [BUNDLED-AUX]"
: ""
}`,
);
}
} catch {
/* not JSON array */
}
}
if (p.includes("REQ") || p.includes("kinds") || p.includes("AUTH"))
wsFrames.push(`[${rel()}] >> ${p.slice(0, 300)}`);
if (upstreamOpen) upstream.send(m);
else pageQueue.push(m);
});
ws.onClose(() => {
try {
upstream.close();
} catch {
/* noop */
}
});
});
await page.goto("/");
try {
// Wait for the sidebar + buzz-bugs channel entry to materialize from the
// relay (kind:39002 membership -> kind:39000 metadata).
const channelEntry = page.getByTestId(`channel-${BUZZ_BUGS_NAME}`);
await channelEntry.waitFor({ state: "visible", timeout: 60_000 });
// ---- COLD LOAD: first entry into the channel ----
await channelEntry.click();
await expect(page.getByTestId("chat-title")).toHaveText(BUZZ_BUGS_NAME, {
timeout: 20_000,
});
// Let messages render.
await page.getByTestId("message-row").first().waitFor({ timeout: 20_000 });
// Give the cold-load aux backfill a generous window to commit.
await page.waitForTimeout(6_000);
const coldLoadReactionCount = await page
.getByTestId("message-reactions")
.count();
const coldLoadState = await collectRenderedReactionState(page);
const coldLoadMessageCount = coldLoadState.length;
await page.screenshot({
path: "test-results/reaction-coldload/01-coldload.png",
fullPage: true,
});
// ---- DISAMBIGUATE: switch away then back ----
// Click any other channel, then return to buzz-bugs.
const otherChannel = page
.locator('[data-testid^="channel-"]')
.filter({ hasNot: channelEntry })
.first();
await otherChannel.click();
await page.waitForTimeout(1_500);
await channelEntry.click();
await expect(page.getByTestId("chat-title")).toHaveText(BUZZ_BUGS_NAME);
await page.getByTestId("message-row").first().waitFor({ timeout: 20_000 });
await page.waitForTimeout(6_000);
const afterSwitchReactionCount = await page
.getByTestId("message-reactions")
.count();
const afterSwitchState = await collectRenderedReactionState(page);
await page.screenshot({
path: "test-results/reaction-coldload/02-afterswitch.png",
fullPage: true,
});
console.log("=== REPRO RESULT ===");
console.log("cold-load message rows:", coldLoadMessageCount);
console.log("after-switch message rows:", afterSwitchState.length);
console.log("cold-load reaction containers:", coldLoadReactionCount);
console.log("after-switch reaction containers:", afterSwitchReactionCount);
const coldReactionIds = new Set(
coldLoadState.filter((r) => r.hasReactions).map((r) => r.id),
);
const afterReactionIds = new Set(
afterSwitchState.filter((r) => r.hasReactions).map((r) => r.id),
);
const afterSwitchOnlyReactionRows = afterSwitchState.filter(
(r) => r.hasReactions && !coldReactionIds.has(r.id),
);
const coldLoadOnlyReactionRows = coldLoadState.filter(
(r) => r.hasReactions && !afterReactionIds.has(r.id),
);
console.log(
"cold-load reaction row ids:",
JSON.stringify([...coldReactionIds]),
);
console.log(
"after-switch reaction row ids:",
JSON.stringify([...afterReactionIds]),
);
console.log(
"after-switch-only reaction rows:",
JSON.stringify(afterSwitchOnlyReactionRows),
);
console.log(
"cold-load-only reaction rows:",
JSON.stringify(coldLoadOnlyReactionRows),
);
// The proof: switching away-and-back is what "fixes" the symptom for the
// user today, so after-switch is the ground-truth count of reactions that
// exist for the loaded window. The fix means the FIRST cold-load paint must
// already show them — not zero, and not far short of after-switch. Pre-fix
// this was 0 on a busy workspace (all-or-nothing drop on the bundled REQ
// timeout); post-fix the kind:7 REQ lands well inside the 8s budget.
expect(
coldLoadReactionCount,
"cold-load must render reactions on first paint (pre-fix: 0)",
).toBeGreaterThan(0);
expect(
coldLoadState.map((row) => row.id),
"cold-load and after-switch must compare the same rendered message rows",
).toEqual(afterSwitchState.map((row) => row.id));
expect(
afterSwitchOnlyReactionRows,
"cold-load must hydrate every reaction-bearing row visible after switch-back",
).toEqual([]);
} finally {
// Always dump diagnostics, even if a waitFor above timed out — this is the
// whole point of the harness. Without the finally, a timeout fires before
// the log block and we learn nothing.
const channelTestIds = await page
.locator('[data-testid^="channel-"]')
.evaluateAll((els) =>
els.map((e) => (e as HTMLElement).dataset.testid ?? ""),
)
.catch(() => [] as string[]);
console.log("=== CHANNELS RENDERED IN SIDEBAR ===");
console.log(JSON.stringify(channelTestIds));
console.log("=== WS FRAMES (reaction/REQ) ===");
for (const f of wsFrames) console.log(f);
console.log("=== AUX-BACKFILL SUB FRAMES (relay -> bridge) ===");
console.log("total upstream frames bridged:", upstreamFrameCount);
console.log("aux sub ids tracked:", [...auxSubIds].join(", "));
for (const f of auxFrameLog) console.log(f);
console.log("=== RELAY /query BODIES ===");
for (const b of relayQueryBodies.slice(0, 40)) console.log(b);
console.log("=== CONSOLE (last 40) ===");
for (const l of consoleLines.slice(-40)) console.log(l);
await page
.screenshot({
path: "test-results/reaction-coldload/99-final.png",
fullPage: true,
})
.catch(() => {});
}
});

View File

@@ -2,7 +2,6 @@ import { expect, test } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
const SHOTS = "test-results/relay-connectivity-screenshots";
const RELAY_UNREACHABLE = "relay unreachable: connection refused";
// Minimal teal 8×8 PNG as a data URL — satisfies avatarDataUrl's data:image/ guard.
@@ -48,7 +47,7 @@ async function driveConnectionDegraded(
}, state);
}
test.describe("relay connectivity screenshots", () => {
test.describe("relay connectivity", () => {
test("01 — sidebar unreachable card", async ({ page }) => {
await installMockBridge(page, { channelsReadError: RELAY_UNREACHABLE });
await page.goto("/");
@@ -62,10 +61,6 @@ test.describe("relay connectivity screenshots", () => {
await settle(page);
// Clip to sidebar width (256px) so the card and channel list are both visible.
await page.screenshot({
path: `${SHOTS}/01-sidebar-unreachable.png`,
clip: { x: 0, y: 0, width: 256, height: 720 },
});
});
test("02 — sidebar reconnect card while reconnecting", async ({ page }) => {
@@ -87,10 +82,6 @@ test.describe("relay connectivity screenshots", () => {
await settle(page);
// Clip to the sidebar, where degraded relay state is now surfaced.
await page.screenshot({
path: `${SHOTS}/02-sidebar-reconnecting.png`,
clip: { x: 0, y: 0, width: 256, height: 720 },
});
});
test("03 — canvas unreachable in management sheet", async ({ page }) => {
@@ -111,7 +102,7 @@ test.describe("relay connectivity screenshots", () => {
canvasSection.getByText("Can't reach the relay."),
).toBeVisible();
// Await Radix sheet animations before screenshotting.
// Await Radix sheet animations before measuring the settled state.
const sheet = page.getByTestId("channel-management-sheet");
await sheet.evaluate((el) =>
Promise.all(
@@ -124,9 +115,6 @@ test.describe("relay connectivity screenshots", () => {
await settle(page);
// Capture the whole sheet so the error renders in its Canvas-section context.
await sheet.screenshot({
path: `${SHOTS}/03-canvas-unreachable.png`,
});
});
test("04 — cached identity shown offline (avatar + display name)", async ({
@@ -156,10 +144,6 @@ test.describe("relay connectivity screenshots", () => {
const profileCard = page.getByTestId("sidebar-profile-card");
await expect(profileCard).toContainText("Tyler Durden");
await settle(page);
await profileCard.screenshot({
path: `${SHOTS}/04-cached-identity-offline.png`,
});
});
test("05 — no-cache npub fallback when offline", async ({ page }) => {
@@ -171,10 +155,6 @@ test.describe("relay connectivity screenshots", () => {
// Default mock identity display name is "npub1mock...".
await expect(profileCard).toContainText("npub1mock");
await settle(page);
await profileCard.screenshot({
path: `${SHOTS}/05-no-cache-npub-fallback.png`,
});
});
test("06 — sidebar card shows connected after external relay recovery", async ({

View File

@@ -2,8 +2,6 @@ import { expect, test } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
const SHOTS = "test-results/relay-reconnect-screenshots";
async function settle(page: import("@playwright/test").Page) {
// Tolerate cancelled animations (skeleton → live swap rejects `.finished`
// with AbortError) AND indefinitely-running ones (the degraded-state pulse
@@ -49,7 +47,7 @@ async function scrollSidebarToBottom(page: import("@playwright/test").Page) {
});
}
test.describe("relay reconnect affordance screenshots", () => {
test.describe("relay reconnect affordance", () => {
test("01 — sidebar has no reconnect prompt when healthy", async ({
page,
}) => {
@@ -67,9 +65,6 @@ test.describe("relay reconnect affordance screenshots", () => {
// Frame the left sidebar directly — the relay-unreachable block lives there,
// and a full-window shot makes its presence/absence illegible against the
// unrelated top connection banner.
await page.getByTestId("app-sidebar").screenshot({
path: `${SHOTS}/01-sidebar-healthy.png`,
});
});
test("02 — sidebar reconnect prompt shown when degraded, channels visible", async ({
@@ -89,9 +84,5 @@ test.describe("relay reconnect affordance screenshots", () => {
// Scroll the block clear of the occluding footer (symmetric with 01).
await scrollSidebarToBottom(page);
await settle(page);
await page.getByTestId("app-sidebar").screenshot({
path: `${SHOTS}/02-sidebar-degraded.png`,
});
});
});

View File

@@ -3,7 +3,6 @@ import { expect, test } from "@playwright/test";
import { waitForAnimations } from "../helpers/animations";
import { installMockBridge } from "../helpers/bridge";
const SHOTS = "test-results/reminders";
const MOCK_PUBKEY = "deadbeef".repeat(8);
// The inbox filter dropdown lives in the home pane, not the chat view. Land on
@@ -60,7 +59,7 @@ function mockReminderEvent(opts: {
};
}
test.describe("reminders screenshots", () => {
test.describe("reminders", () => {
test.beforeEach(async ({ page }) => {
await installMockBridge(page);
});
@@ -76,11 +75,6 @@ test.describe("reminders screenshots", () => {
});
await expect(remindersOption).toBeVisible();
await waitForAnimations(page);
await page.screenshot({
path: `${SHOTS}/01-inbox-filter-reminders-option.png`,
clip: { x: 0, y: 0, width: 900, height: 720 },
});
});
test("02 — message action menu shows Remind me later", async ({ page }) => {
@@ -102,10 +96,6 @@ test.describe("reminders screenshots", () => {
});
await expect(remindItem).toBeVisible();
await waitForAnimations(page);
await page.screenshot({
path: `${SHOTS}/02-message-action-remind-later.png`,
});
});
test("03 — Remind me later dialog with time presets", async ({ page }) => {
@@ -134,11 +124,6 @@ test.describe("reminders screenshots", () => {
await expect(dialog.getByText("In 30 minutes")).toBeVisible();
await expect(dialog.getByText("Custom date & time")).toBeVisible();
await waitForAnimations(page);
await page.screenshot({
path: `${SHOTS}/03-remind-me-later-dialog.png`,
clip: { x: 300, y: 50, width: 680, height: 620 },
});
});
test("04 — Reminders panel empty state", async ({ page }) => {
@@ -147,11 +132,6 @@ test.describe("reminders screenshots", () => {
await openRemindersFilter(page);
await expect(page.getByText("No reminders")).toBeVisible();
await waitForAnimations(page);
await page.screenshot({
path: `${SHOTS}/04-reminders-panel-empty.png`,
clip: { x: 0, y: 0, width: 900, height: 720 },
});
});
test("05 — Reminders panel with active pending reminder", async ({
@@ -184,11 +164,6 @@ test.describe("reminders screenshots", () => {
await openRemindersFilter(page);
await expect(page.getByText("Follow up on this message")).toBeVisible();
await waitForAnimations(page);
await page.screenshot({
path: `${SHOTS}/05-reminders-panel-active.png`,
clip: { x: 0, y: 0, width: 900, height: 720 },
});
});
test("06 — Reminders panel with fired/overdue reminder", async ({ page }) => {
@@ -239,11 +214,6 @@ test.describe("reminders screenshots", () => {
await expect(page.getByText("Reply to Alice")).toBeVisible();
await expect(page.getByRole("heading", { name: "Overdue" })).toBeVisible();
await waitForAnimations(page);
await page.screenshot({
path: `${SHOTS}/06-reminders-panel-fired.png`,
clip: { x: 0, y: 0, width: 900, height: 720 },
});
});
});
@@ -252,7 +222,6 @@ test.describe("reminders screenshots", () => {
// `mock-general-alice`, channel `9a1657ac-…`), so the author resolves to
// "alice" and the channel label to "general" from the live profile/channel
// queries — no "Unknown channel" fallback.
const PHASE2_SHOTS = "test-results/reminders-phase2";
const GENERAL_CHANNEL_ID = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
const ALICE_PUBKEY =
"953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f";
@@ -274,7 +243,6 @@ function aliceReminderContent() {
// Inbox nav item's `(1)` count. The count is gated behind `homeBadgeEnabled`,
// so seed that setting on before installMockBridge (addInitScript runs at
// document start, ahead of the app reading localStorage).
const NAVBADGE_SHOTS = "test-results/reminders-navbadge";
const NOTIFICATION_SETTINGS_KEY = `buzz-notification-settings.v2:${MOCK_PUBKEY}`;
test.describe("reminders nav badge", () => {
@@ -306,11 +274,6 @@ test.describe("reminders nav badge", () => {
await expect(page.getByTestId("sidebar-home-count")).toHaveText("1");
await waitForAnimations(page);
await page.screenshot({
path: `${NAVBADGE_SHOTS}/01-inbox-nav-badge-due-reminder.png`,
clip: { x: 0, y: 0, width: 900, height: 720 },
});
});
});
@@ -347,11 +310,6 @@ test.describe("reminders phase 2 — author, source, navigation", () => {
).toBeVisible();
await expect(remindersPanel.getByText("Reply to Alice")).toBeVisible();
await waitForAnimations(page);
await page.screenshot({
path: `${PHASE2_SHOTS}/01-reminder-row-author-channel.png`,
clip: { x: 0, y: 0, width: 900, height: 720 },
});
});
test("08 — clicking a reminder navigates to the message in context", async ({
@@ -378,9 +336,5 @@ test.describe("reminders phase 2 — author, source, navigation", () => {
await expect(page.getByTestId("chat-title")).toHaveText("general");
await expect(page.getByText("Hey team — checking in.")).toBeVisible();
await waitForAnimations(page);
await page.screenshot({
path: `${PHASE2_SHOTS}/02-click-navigates-to-message.png`,
});
});
});

View File

@@ -11,16 +11,13 @@
* strip.
*
* This spec injects a synthetic pill into the live sidebar's relative
* container and screenshots it next to the chrome strip so the positioning is
* visible.
* container and asserts the pill clears the chrome strip.
*/
import { expect, test } from "@playwright/test";
import { waitForAnimations } from "../helpers/animations";
import { installMockBridge } from "../helpers/bridge";
const SHOTS = "test-results/sidebar-more-unread-overlap";
const TOP_CLASS = "top-0";
const PILL_BASE =
@@ -46,7 +43,7 @@ async function injectSyntheticPill(
) as HTMLElement | null;
if (!container) throw new Error("sidebar scroll anchor not found");
// Remove any prior injection so we can screenshot variants in sequence.
// Remove any prior injection so retries start from a clean sidebar.
container
.querySelectorAll("[data-synthetic-more-unread]")
.forEach((el) => {
@@ -83,9 +80,5 @@ test.describe("sidebar MoreUnreadButton top chrome overlap", () => {
expect(box?.y ?? Number.NaN).toBeGreaterThanOrEqual(40);
await waitForAnimations(page);
await page.screenshot({
path: `${SHOTS}/top-pill-below-in-flow-chrome.png`,
clip: { x: 0, y: 0, width: 320, height: 120 },
});
});
});

View File

@@ -1,218 +0,0 @@
import { expect, test } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
const SHOTS = "test-results/team-management";
async function waitForInvokeBridge(page: import("@playwright/test").Page) {
await page.waitForFunction(
() => {
const tauriWindow = window as Window & {
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: unknown;
__TAURI_INTERNALS__?: {
invoke?: unknown;
};
};
return (
typeof tauriWindow.__BUZZ_E2E_INVOKE_MOCK_COMMAND__ === "function" ||
typeof tauriWindow.__TAURI_INTERNALS__?.invoke === "function"
);
},
null,
{ timeout: 5_000 },
);
}
async function invokeMockCommand(
page: import("@playwright/test").Page,
command: string,
payload?: Record<string, unknown>,
): Promise<unknown> {
await waitForInvokeBridge(page);
return page.evaluate(
async ({ command: cmd, payload: pl }) => {
const tauriWindow = window as Window & {
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
command: string,
payload?: Record<string, unknown>,
) => Promise<unknown>;
__TAURI_INTERNALS__?: {
invoke?: (
command: string,
payload?: Record<string, unknown>,
) => Promise<unknown>;
};
};
const invoke =
tauriWindow.__BUZZ_E2E_INVOKE_MOCK_COMMAND__ ??
tauriWindow.__TAURI_INTERNALS__?.invoke;
if (!invoke) {
throw new Error("Mock invoke bridge is unavailable.");
}
return invoke(cmd, pl);
},
{ command, payload },
);
}
async function activatePersonas(page: import("@playwright/test").Page) {
for (const id of ["builtin:fizz"]) {
await invokeMockCommand(page, "set_persona_active", { id, active: true });
}
}
async function openAgentsView(page: import("@playwright/test").Page) {
await page.goto("/", { waitUntil: "domcontentloaded" });
await waitForInvokeBridge(page);
await activatePersonas(page);
await page.getByTestId("open-agents-view").click();
await expect(page.getByTestId("agents-library-teams")).toBeVisible({
timeout: 10_000,
});
}
test.describe("team management screenshots", () => {
test.use({ viewport: { width: 1280, height: 720 } });
test("01 — teams section with cards", async ({ page }) => {
await installMockBridge(page);
await openAgentsView(page);
const teamsSection = page.getByTestId("agents-library-teams");
await expect(teamsSection).toContainText("Engineering");
await expect(teamsSection).toContainText("Research Agents");
await expect(teamsSection).toContainText("Platform Tools");
await teamsSection.screenshot({
path: `${SHOTS}/01-teams-section.png`,
});
});
test("02 — regular team context menu", async ({ page }) => {
await installMockBridge(page);
await openAgentsView(page);
const teamsSection = page.getByTestId("agents-library-teams");
const engineeringCard = teamsSection
.locator("[class*='card']")
.filter({ hasText: "Engineering" })
.first();
await engineeringCard.locator("button").last().click();
const deployItem = page.getByRole("menuitem", {
name: "Deploy to channel",
});
await expect(deployItem).toBeVisible();
await expect(page.getByRole("menuitem", { name: "Edit" })).toBeVisible();
await expect(
page.getByRole("menuitem", { name: "Duplicate" }),
).toBeVisible();
await expect(page.getByRole("menuitem", { name: "Export" })).toBeVisible();
await expect(page.getByRole("menuitem", { name: "Delete" })).toBeVisible();
await deployItem.evaluate((el) =>
Promise.all(
el
.closest("[data-state]")
?.getAnimations()
.map((a) => a.finished) ?? [],
),
);
await page.screenshot({
path: `${SHOTS}/02-team-card-menu.png`,
clip: { x: 0, y: 0, width: 1280, height: 720 },
});
});
test("03 — directory-backed team with version badge", async ({ page }) => {
await installMockBridge(page);
await openAgentsView(page);
const teamsSection = page.getByTestId("agents-library-teams");
const versionBadge = teamsSection.locator("span", { hasText: "v1.2.0" });
await expect(versionBadge).toBeVisible();
const researchCard = teamsSection
.locator("[class*='card']")
.filter({ hasText: "Research Agents" })
.first();
await expect(researchCard).toBeVisible();
await researchCard.screenshot({
path: `${SHOTS}/03-directory-team-card.png`,
});
});
test("04 — directory team context menu", async ({ page }) => {
await installMockBridge(page);
await openAgentsView(page);
const teamsSection = page.getByTestId("agents-library-teams");
const researchCard = teamsSection
.locator("[class*='card']")
.filter({ hasText: "Research Agents" })
.first();
await researchCard.locator("button").last().click();
const syncItem = page.getByRole("menuitem", {
name: "Sync from directory",
});
await expect(syncItem).toBeVisible();
await expect(
page.getByRole("menuitem", { name: "Reveal in Finder" }),
).toBeVisible();
await syncItem.evaluate((el) =>
Promise.all(
el
.closest("[data-state]")
?.getAnimations()
.map((a) => a.finished) ?? [],
),
);
await page.screenshot({
path: `${SHOTS}/04-directory-team-menu.png`,
clip: { x: 0, y: 0, width: 1280, height: 720 },
});
});
test("05 — symlinked team with link icon", async ({ page }) => {
await installMockBridge(page);
await openAgentsView(page);
const teamsSection = page.getByTestId("agents-library-teams");
const platformCard = teamsSection
.locator("[class*='card']")
.filter({ hasText: "Platform Tools" })
.first();
await expect(platformCard).toBeVisible();
const versionBadge = platformCard.locator("span", { hasText: "v2.0.1" });
await expect(versionBadge).toBeVisible();
await platformCard.screenshot({
path: `${SHOTS}/05-symlinked-team.png`,
});
});
test("06 — install from directory button", async ({ page }) => {
await installMockBridge(page);
await openAgentsView(page);
const teamsSection = page.getByTestId("agents-library-teams");
const installButton = teamsSection.getByRole("button", {
name: "Install from directory",
});
await expect(installButton).toBeVisible();
await installButton.screenshot({
path: `${SHOTS}/06-install-from-directory.png`,
});
});
});

View File

@@ -2,8 +2,6 @@ import { expect, test } from "@playwright/test";
import { TEST_IDENTITIES, installMockBridge } from "../helpers/bridge";
const SHOTS = "test-results/thread-unread";
type MockMessageEvent = {
id: string;
created_at: number;
@@ -111,7 +109,7 @@ async function expandReply(
await expect.poll(() => replies.count()).toBeGreaterThan(before);
}
test.describe("thread unread indicator screenshots", () => {
test.describe("thread unread indicator", () => {
test("01-thread-unread-badge", async ({ page }) => {
await installMockBridge(page);
await page.goto("/");
@@ -167,10 +165,6 @@ test.describe("thread unread indicator screenshots", () => {
const badge = page.getByTestId("thread-unread-badge");
await expect(badge).toBeVisible();
await expect(badge).toContainText("3");
await page.screenshot({
path: `${SHOTS}/01-thread-unread-badge.png`,
});
});
test("02-thread-new-divider", async ({ page }) => {
@@ -222,10 +216,6 @@ test.describe("thread unread indicator screenshots", () => {
await expect(divider).toBeVisible();
await divider.scrollIntoViewIfNeeded();
await page.waitForTimeout(300);
await page.screenshot({
path: `${SHOTS}/02-thread-new-divider.png`,
});
});
test("03-thread-badge-casual-browse", async ({ page }) => {
@@ -269,10 +259,6 @@ test.describe("thread unread indicator screenshots", () => {
await expect(badges).toHaveCount(1);
await expect(badges).toContainText("2");
await page.screenshot({
path: `${SHOTS}/03-thread-badge-casual-browse.png`,
});
// Opening a casual, unmuted thread should clear its local badge too. The
// badge render gate and read-on-open gate must stay aligned.
await page.locator(`[data-thread-head-id="${rootEvent.id}"]`).click();
@@ -379,10 +365,6 @@ test.describe("thread unread indicator screenshots", () => {
await divider.scrollIntoViewIfNeeded();
await page.waitForTimeout(300);
await page.screenshot({
path: `${SHOTS}/04-thread-deep-nested-unread.png`,
});
await page.getByTestId("message-thread-head").scrollIntoViewIfNeeded();
await page
.locator(
@@ -510,10 +492,6 @@ test.describe("thread unread indicator screenshots", () => {
await expect(inPanelBadge).toBeVisible();
await expect(inPanelBadge).toContainText("2");
await page.screenshot({
path: `${SHOTS}/05-thread-in-panel-subtree-badge.png`,
});
// v3 contract: expanding a branch marks only its REVEALED direct children
// read, never the whole subtree. The unread replies sit two levels under p
// (p -> c -> c2 -> c2-child), so a single expand of p only reveals c — the
@@ -524,10 +502,6 @@ test.describe("thread unread indicator screenshots", () => {
await expandReply(page, p.id);
await expect(inPanelBadge).toBeVisible();
await page.screenshot({
path: `${SHOTS}/06-thread-expand-clears-subtree-badge.png`,
});
await expandReply(page, c.id);
await expandReply(page, c2.id);
await expect(inPanelBadge).toHaveCount(0);
@@ -594,10 +568,6 @@ test.describe("thread unread indicator screenshots", () => {
createdAt: base + 1,
});
await expect(inPanelBadge).toContainText("2");
await page.screenshot({
path: `${SHOTS}/07-in-panel-badge-bumps-on-live-reply.png`,
});
});
test("07-expand-clears-own-branch-badge-sibling-survives", async ({
@@ -674,10 +644,6 @@ test.describe("thread unread indicator screenshots", () => {
.getByTestId("thread-unread-badge");
await expect(inPanelBadges).toHaveCount(2);
await page.screenshot({
path: `${SHOTS}/08-two-sibling-badges-before-expand.png`,
});
// Expand the LATER branch down to where its unread sits: revealing
// branchNew shows newChild (still collapsed over the unread reply, so the
// badge survives), then revealing newChild marks the unread reply read and
@@ -688,18 +654,10 @@ test.describe("thread unread indicator screenshots", () => {
await expandReply(page, newChild.id);
await expect(inPanelBadges).toHaveCount(1);
await page.screenshot({
path: `${SHOTS}/09-expand-clears-own-branch-sibling-survives.png`,
});
// Expanding the older branch to its unread depth clears the last badge.
await expandReply(page, branchOld.id);
await expandReply(page, oldChild.id);
await expect(inPanelBadges).toHaveCount(0);
await page.screenshot({
path: `${SHOTS}/10-both-branches-expanded-all-cleared.png`,
});
});
// Regression guard for the Option-1 channel-marker fix: viewing a channel
@@ -756,10 +714,6 @@ test.describe("thread unread indicator screenshots", () => {
await expect(page.getByTestId("chat-title")).toHaveText("general");
await expect(badge).toBeVisible();
await expect(badge).toContainText("3");
await page.screenshot({
path: `${SHOTS}/10-thread-badge-survives-channel-reentry.png`,
});
});
// Thread-only replies now also light the channel sidebar badge. Viewing the
@@ -803,10 +757,6 @@ test.describe("thread unread indicator screenshots", () => {
await page.getByTestId("channel-random").click();
await expect(page.getByTestId("chat-title")).toHaveText("random");
await expect(page.getByTestId("channel-unread-general")).toBeVisible();
await page.screenshot({
path: `${SHOTS}/11-thread-reply-sidebar-badge.png`,
});
});
// Regression guard for the all-replies window: when the loaded window holds
@@ -845,10 +795,6 @@ test.describe("thread unread indicator screenshots", () => {
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await expect(page.getByTestId("channel-unread-all-replies")).toBeVisible();
await page.screenshot({
path: `${SHOTS}/12-thread-reply-all-replies-sidebar-badge.png`,
});
});
// Regression guard for BUG-2 (clear-on-read): opening an unread thread marks
@@ -901,10 +847,6 @@ test.describe("thread unread indicator screenshots", () => {
await expect(badge).toBeVisible();
await expect(badge).toContainText("3");
await page.screenshot({
path: `${SHOTS}/13-thread-badge-before-read.png`,
});
// The crux: open the thread (mark-read advances the frontier past all three
// direct replies), then close it. Stay in general the entire time — no
// channel switch. The badge must clear to zero off the readStateVersion
@@ -916,10 +858,6 @@ test.describe("thread unread indicator screenshots", () => {
await expect(page.getByTestId("chat-title")).toHaveText("general");
await expect(badge).toHaveCount(0);
await page.screenshot({
path: `${SHOTS}/13-thread-badge-clears-on-read.png`,
});
});
// Regression guard for the mention-gate + subtree-count fixes. The viewer is

View File

@@ -2,8 +2,6 @@ import { expect, test } from "@playwright/test";
import { TEST_IDENTITIES, installMockBridge } from "../helpers/bridge";
const SHOTS = "test-results/unread-pill";
async function waitForMockLiveSubscription(
page: import("@playwright/test").Page,
channelName: string,
@@ -90,9 +88,9 @@ async function emitUnreadMessages(
// Scroll the timeline up so the viewport is no longer pinned to the bottom.
// The pill auto-dismisses once the user reaches the bottom of the timeline, so
// it only stays rendered while scrolled up — which is the state these shots
// need to capture. Scrolling part-way (rather than to the very top) keeps real
// message context on screen instead of the channel's empty-state intro.
// it only stays rendered while scrolled up. Scrolling part-way (rather than to
// the very top) keeps real message context on screen instead of the channel's
// empty-state intro.
async function scrollTimelineUp(page: import("@playwright/test").Page) {
await page.getByTestId("message-timeline").evaluate((el) => {
el.scrollTop = Math.floor(el.scrollHeight * 0.35);
@@ -100,7 +98,7 @@ async function scrollTimelineUp(page: import("@playwright/test").Page) {
await page.waitForTimeout(300);
}
test.describe("unread pill & divider screenshots", () => {
test.describe("unread pill & divider", () => {
test("01-unread-pill-visible", async ({ page }) => {
await installMockBridge(page);
await page.goto("/");
@@ -127,10 +125,6 @@ test.describe("unread pill & divider screenshots", () => {
const pill = page.getByTestId("message-unread-pill");
await expect(pill).toBeVisible();
await expect(pill).toContainText("20 new messages");
await page.screenshot({
path: `${SHOTS}/01-unread-pill-visible.png`,
});
});
test("02-unread-divider-visible", async ({ page }) => {
@@ -152,13 +146,9 @@ test.describe("unread pill & divider screenshots", () => {
const divider = page.getByTestId("message-unread-divider");
await expect(divider).toBeVisible();
// Scroll the divider into view for a clear screenshot
// Scroll the divider into view before asserting it.
await divider.scrollIntoViewIfNeeded();
await page.waitForTimeout(300);
await page.screenshot({
path: `${SHOTS}/02-unread-divider-visible.png`,
});
});
test("03-pill-dismissed-after-scroll", async ({ page }) => {
@@ -191,10 +181,6 @@ test.describe("unread pill & divider screenshots", () => {
const divider = page.getByTestId("message-unread-divider");
await expect(divider).toBeVisible();
await page.screenshot({
path: `${SHOTS}/03-pill-dismissed-after-scroll.png`,
});
});
test("04-mark-unread-suppresses-pill", async ({ page }) => {
@@ -222,9 +208,5 @@ test.describe("unread pill & divider screenshots", () => {
// Pill and divider should NOT appear (suppressed for forced-unread)
await expect(page.getByTestId("message-unread-pill")).toHaveCount(0);
await expect(page.getByTestId("message-unread-divider")).toHaveCount(0);
await page.screenshot({
path: `${SHOTS}/04-mark-unread-suppresses-pill.png`,
});
});
});

View File

@@ -3,15 +3,6 @@ import type { Locator, Page } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
// Screenshot capture for the desktop list-virtualization pass (PR #1089). Each
// shot is gated by an assertion so a geometry regression fails the run rather
// than silently producing a misleading image. The shots are the empirical gate
// that closes the correctness residual flagged in review: the absolute-position
// / scrollMargin geometry under live dynamic measurement, and the
// content-visibility "rows stay committed" claim on the dnd-coupled surface.
// Artifacts land in test-results/virtualization/.
const SHOTS = "test-results/virtualization";
const WATERCOOLER_CHANNEL_ID = "a27e1ee9-76a6-5bdf-a5d5-1d85610dad11";
const FORUM_THREAD_ID = "mock-forum-release-thread";
const FORUM_DEEPLINK_REPLY_ID = "mock-forum-release-deeplink";
@@ -52,7 +43,7 @@ async function dragOver(page: Page, source: Locator, target: Locator) {
await page.mouse.up();
}
test.describe("list virtualization screenshots", () => {
test.describe("list virtualization", () => {
test("01 — Pulse windowed feed with sticky composer pinned mid-scroll", async ({
page,
}) => {
@@ -89,8 +80,6 @@ test.describe("list virtualization screenshots", () => {
),
)
.toBeLessThan(80);
await page.screenshot({ path: `${SHOTS}/01-pulse-sticky-composer.png` });
});
test("02 — forum deep-link lands on an offscreen reply", async ({ page }) => {
@@ -116,8 +105,6 @@ test.describe("list virtualization screenshots", () => {
}),
)
.toBe(true);
await page.screenshot({ path: `${SHOTS}/02-forum-deeplink-offscreen.png` });
});
test("03 — members search shows both sticky titles under content-visibility", async ({
@@ -145,10 +132,6 @@ test.describe("list virtualization screenshots", () => {
await expect(
page.locator('[data-testid^="channel-user-search-result-"]').first(),
).toBeVisible();
await page.screenshot({
path: `${SHOTS}/03-members-both-sticky-titles.png`,
});
});
test("06 — custom-section dnd reorder commits under content-visibility", async ({
@@ -182,8 +165,6 @@ test.describe("list virtualization screenshots", () => {
);
expect(await sectionOrder()).toEqual(["Priority", "Archive"]);
await page.screenshot({ path: `${SHOTS}/06a-sections-before-reorder.png` });
// Drag "Priority" past "Archive" — onDragEnd commits arrayMove and persists
// the new order. The drop must land for the order to flip.
await dragOver(page, topHeader, bottomHeader);
@@ -193,7 +174,5 @@ test.describe("list virtualization screenshots", () => {
// Both section rows stayed committed in the DOM across the reorder — the
// content-visibility invariant the divergence rests on (no unmount).
await expect(headers).toHaveCount(2);
await page.screenshot({ path: `${SHOTS}/06b-sections-after-reorder.png` });
});
});