mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
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:
@@ -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: {
|
||||
|
||||
@@ -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`,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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`,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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"),
|
||||
@@ -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`,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 ({
|
||||
@@ -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`,
|
||||
});
|
||||
});
|
||||
@@ -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 },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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`,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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(() => {});
|
||||
}
|
||||
});
|
||||
@@ -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 ({
|
||||
@@ -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`,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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`,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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`,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
@@ -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`,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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` });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user