Files
buzz/desktop/tests/e2e/voice-note.spec.ts
Wes 7b11c6147d test: fix recurring CI flakes in desktop and Postgres lanes (#8343)
Fixes the CI flakes that failed most often across `main` and unrelated
branches from Oct 5 to Oct 14. The counts come from 150 failed `CI`
runs. Every change except the nextest config is test-only.

| Flake | Runs hit | Root cause | Fix |
|---|---|---|---|
| `probe_codex_acp_version_…descendant_holds_pipe_open` | 5 | The test
writes its script in-process and then execs it. If a sibling test spawns
a process in that window, the child inherits the open write fd and Linux
rejects the exec with `ETXTBSY`. The probe swallows that error as
`None`. | Write probe scripts from a short-lived `/bin/sh`
(`write_executable_script`) |
| `empty-edit-delete.spec.ts` (`:51`, `:74`, `:95`) | 11 | The seeded
`mock-general-welcome` id isn't hex, so `formatTimelineMessages` can't
overlay its edit. The post-subscribe window refetch then reverts the
row. | Emit a per-test message with a real id after the exact channel
subscription is live |
| `huddle-transcription.spec.ts:798` | 9 | The composer's delayed
initial autofocus (TipTap `focus()` runs in rAF) lands after the voice
popover opens and dismisses it. | Wait for `message-input` focus before
opening the popover |
| `voice-note.spec.ts:906` | 6 | The fixture `ping.mp3` is about 1s
long, so playback can end before `paused === false` is observed. | Loop
the fixture audio |
| `workflows.spec.ts:291` | 5 | The `Message text` fill runs before the
inspector swaps to the new step, so it fills the trigger's condition
instead. | Scope the fill to the step inspector after its `Action` menu
appears |
| `cluster_global_probe_commits_tokens_and_sessions_prove_coverage` | 3
| The `max-threads = 1` group only serializes these tests with each
other. Other tests' sessions still mask cluster-wide `pg_stat_activity`
(`MaskedActivity`). | `threads-required = "num-test-threads"` |
| `test_usage_metrics_lock_has_single_owner_and_releases_on_drop` | 1
(this PR) | The test retries the lock right after dropping the leader.
Postgres releases a session advisory lock only after the backend notices
the disconnect, so the retry can run first. | Poll for the successor,
bounded at 5s |

### Validation
- **ETXTBSY:** a Linux experiment with Rust 1.95 using the same
write→chmod→spawn pattern under concurrent spawns hit `ETXTBSY` 4–7% of
the time. Using the `sh` writer, it hit 0 of 6,000. I did not catch it
inside the full suite locally, so this fix is based on the mechanism,
not an in-suite repro. All 11 probe tests pass on Linux and macOS.
- **empty-edit-delete:** the baseline failed 16 of 45 runs
(`--repeat-each`, 6 workers, no retries). The spec fix alone brings that
to 4/45. The remaining failures come from a separate product race,
described below.
- **huddle:** the baseline failed 5 of 10 runs. With the fix, 60 of 60
passed without retries.
- **nextest:** checked with a throwaway crate on 0.9.136. A grouped test
with `num-test-threads` ran alone, even with `max-threads = 1`.
- I have not stress-run the voice-note and workflows fixes; they rely on
CI.

### Not in this PR
- **Product bug in `useChannelLinks`:** the debounced `#channel`
autocomplete close lets Enter accept a stale suggestion for about 120ms.
That accounts for the remaining `empty-edit-delete` failures, and it
affects users too. With the spec fix plus a synchronous close, the spec
passed 120 of 120 runs. This needs a product decision, so it's held for
a separate PR.
- **Composer autofocus race:** the delayed autofocus can close an
overlay the user just opened. The real fix is to revalidate when the
delayed focus runs. Overlaps #6530.
- **`thread-head-stale-edit.spec.ts:55`:** a forced click on a
pointer-events-none action bar is suspected, but I couldn't reproduce
it.
- **Remaining ETXTBSY exposure:** about 40 other Rust tests write and
then exec scripts, and 17 specs still edit the non-hex seed message.

Root-cause lanes by Princess Donut, Mongo and Mordecai. Carl integrated
and verified them.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <5d97ac8c272fa949af56c71e586146b1706e5c4c3daa125db5b4625e12e0686c@buzz.block.builderlab.xyz>
Co-authored-by: Mongo <81cabd2ca1792494372ed410283c00770c0cf35afca052e940fa522883212b45@buzz.block.builderlab.xyz>
Co-authored-by: Mordecai <f314b6033b6e13df94c8e9e1187d3666b6b9ce54e66ee986e31cdaa909e8dcbc@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:32:00 -06:00

960 lines
34 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { expect, test } from "@playwright/test";
import type { Page } from "@playwright/test";
import { waitForAnimations } from "../helpers/animations";
import { installMockBridge } from "../helpers/bridge";
import { expectSmoothCorners } from "../helpers/css";
const AUDIO_URL = "http://127.0.0.1:4173/sounds/ping.mp3";
async function openMoreActionsMenu(page: Page, messageId: string) {
const row = page.locator(`[data-message-id="${messageId}"]`);
await row.hover();
await page.getByTestId(`more-actions-${messageId}`).click();
await expect(page.locator('[role="menuitem"]').first()).toBeVisible();
}
async function waitForMockLiveSubscription(page: Page, channelName: string) {
await expect
.poll(() =>
page.evaluate(
(currentChannelName) =>
(
window as Window & {
__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?: (input: {
channelName: string;
kind: number;
exactChannel: boolean;
}) => boolean;
}
).__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?.({
channelName: currentChannelName,
kind: 9,
exactChannel: true,
}) ?? false,
channelName,
),
)
.toBe(true);
}
test.beforeEach(async ({ page }, testInfo) => {
await page.addInitScript(() => {
Object.defineProperty(navigator.mediaDevices, "getUserMedia", {
configurable: true,
async value() {
const context = new AudioContext();
const oscillator = context.createOscillator();
const destination = context.createMediaStreamDestination();
oscillator.frequency.value = 220;
oscillator.connect(destination);
oscillator.start();
destination.stream.getAudioTracks()[0]?.addEventListener(
"ended",
() => {
oscillator.stop();
void context.close();
},
{ once: true },
);
return destination.stream;
},
});
});
await installMockBridge(page, {
deferredComposerUploads: true,
sendMessageErrors: testInfo.title.includes("shows a send error")
? ["relay rejected voice note"]
: undefined,
uploadDescriptors: [
{
duration: 9.4,
filename: "voice-note-123.mp4",
sha256: "a".repeat(64),
size: 16424,
type: "video/mp4",
uploaded: Math.floor(Date.now() / 1000),
url: AUDIO_URL,
},
],
});
});
test("restores the voice note and shows a send error after upload succeeds", async ({
page,
}) => {
await page.goto("/");
const channel = page.getByTestId("channel-general");
await expect(channel).toBeVisible({ timeout: 10_000 });
await channel.click();
await page.getByRole("button", { name: "Record voice note" }).click();
await expect(page.getByTestId("voice-note-recorder")).toBeVisible();
await page.waitForTimeout(100);
await page.getByRole("button", { name: "Finish voice note" }).click();
const composerCard = page.getByTestId("composer-voice-note-card");
await expect(composerCard).toBeVisible();
await page.getByTestId("send-message").click();
await expect(composerCard).toBeVisible();
await expect(
page.getByText("Message failed to send: relay rejected voice note"),
).toBeVisible();
});
test("keeps pasted snapshots and channel drops out of an active voice note", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await page.getByRole("button", { name: "Record voice note" }).click();
await expect(page.getByTestId("voice-note-recorder")).toBeVisible();
await page.waitForTimeout(100);
await page.getByRole("button", { name: "Finish voice note" }).click();
await expect(page.getByTestId("composer-voice-note-card")).toBeVisible();
await page
.getByTestId("message-composer")
.locator(".ProseMirror")
.evaluate((editor) => {
const payload = encodeURIComponent(
JSON.stringify({
version: 1,
displayName: "Snapshot",
filename: "shared.agent.png",
sha256: "b".repeat(64),
size: 128,
type: "image/png",
url: "https://relay.example/media/shared.agent.png",
}),
);
const clipboardData = new DataTransfer();
clipboardData.setData(
"text/html",
`<a data-buzz-agent-snapshot="${payload}" href="https://relay.example/media/shared.agent.png">Snapshot</a>`,
);
editor.dispatchEvent(
new ClipboardEvent("paste", {
bubbles: true,
cancelable: true,
clipboardData,
}),
);
});
await expect(page.getByTestId("composer-agent-snapshot-card")).toHaveCount(0);
const dataTransfer = await page.evaluateHandle(() => {
const transfer = new DataTransfer();
transfer.items.add(
new File(["second attachment"], "second-attachment.pdf", {
type: "application/pdf",
}),
);
return transfer;
});
const dropZone = page.getByTestId("channel-drop-zone");
await dropZone.dispatchEvent("dragenter", { dataTransfer });
await expect(dropZone.getByTestId("drop-zone-overlay")).toHaveCount(0);
await dropZone.dispatchEvent("drop", { dataTransfer });
await expect(page.getByTestId("message-composer")).not.toContainText(
"second-attachment.pdf",
);
await expect(page.getByTestId("composer-voice-note-card")).toBeVisible();
});
test("discards an active recording when entering edit mode", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await page.getByRole("button", { name: "Record voice note" }).click();
await expect(page.getByTestId("voice-note-recorder")).toBeVisible();
await openMoreActionsMenu(page, "mock-general-welcome");
await page.getByTestId("edit-message-mock-general-welcome").click();
await expect(page.getByTestId("edit-target")).toBeVisible();
await expect(page.getByTestId("voice-note-recorder")).toHaveCount(0);
await expect(page.getByTestId("composer-voice-note-card")).toHaveCount(0);
});
test("editor Enter never saves an edit while a voice note is recording", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
// Enter edit mode first, then start recording — the mic is available in edit
// mode, so this is the ordering the recorder submission gate must cover.
await openMoreActionsMenu(page, "mock-general-welcome");
await page.getByTestId("edit-message-mock-general-welcome").click();
await expect(page.getByTestId("edit-target")).toBeVisible();
const input = page.getByTestId("message-input");
await expect(input).not.toBeEmpty();
// Change the text so a save (if it wrongly happened) is observable, then
// start recording.
const editedContent = `Edited mid-recording ${Date.now()}`;
await input.click();
await page.keyboard.press("ControlOrMeta+A");
await page.keyboard.type(editedContent);
await page.getByRole("button", { name: "Record voice note" }).click();
await expect(page.getByTestId("voice-note-recorder")).toBeVisible();
await page.waitForTimeout(100);
// The editor's Enter shortcut must not slip past the Finish/Discard flow: the
// edit stays unsaved and the recording stays live until explicitly resolved.
await input.press("Enter");
await expect(page.getByTestId("edit-target")).toBeVisible();
await expect(page.getByTestId("voice-note-recorder")).toBeVisible();
await expect(page.getByTestId("message-timeline")).not.toContainText(
editedContent,
);
// Finishing the recording still works, proving the note was never discarded.
await page.getByRole("button", { name: "Finish voice note" }).click();
await expect(page.getByTestId("composer-voice-note-card")).toBeVisible();
});
test("discards while voice-note processing is pending", async ({ page }) => {
await page.addInitScript(() => {
const pendingDecodes: Array<(buffer: AudioBuffer) => void> = [];
(
window as Window & {
__BUZZ_E2E_RESOLVE_VOICE_NOTE_DECODE__?: () => void;
}
).__BUZZ_E2E_RESOLVE_VOICE_NOTE_DECODE__ = () => {
pendingDecodes.shift()?.({
duration: 1,
getChannelData: () => new Float32Array([0]),
numberOfChannels: 1,
sampleRate: 8_000,
} as AudioBuffer);
};
AudioContext.prototype.decodeAudioData = () =>
new Promise<AudioBuffer>((resolve) => pendingDecodes.push(resolve));
});
await page.goto("/");
await page.getByTestId("channel-general").click();
const discardPendingRecording = async (keyboard: boolean) => {
await page.getByRole("button", { name: "Record voice note" }).click();
await expect(page.getByTestId("voice-note-recorder")).toBeVisible();
await page.waitForTimeout(100);
await page.getByRole("button", { name: "Finish voice note" }).click();
await expect(page.getByText("Preparing voice note…")).toBeVisible();
const discard = page.getByRole("button", { name: "Discard voice note" });
await expect(discard).toBeEnabled();
if (keyboard) {
await discard.focus();
await page.keyboard.press("Enter");
} else {
await discard.click();
}
await expect(page.getByTestId("voice-note-recorder")).toHaveCount(0);
await page.evaluate(() =>
(
window as Window & {
__BUZZ_E2E_RESOLVE_VOICE_NOTE_DECODE__?: () => void;
}
).__BUZZ_E2E_RESOLVE_VOICE_NOTE_DECODE__?.(),
);
await expect(page.getByTestId("composer-voice-note-card")).toHaveCount(0);
};
await discardPendingRecording(false);
await discardPendingRecording(true);
});
test("surfaces waveform and playback failures with retry", async ({ page }) => {
await page.addInitScript(() => {
AudioContext.prototype.decodeAudioData = () =>
Promise.reject(new DOMException("corrupt audio", "EncodingError"));
HTMLMediaElement.prototype.play = () =>
Promise.reject(new DOMException("playback denied", "NotAllowedError"));
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await waitForMockLiveSubscription(page, "general");
await page.evaluate(
({ audioUrl }) => {
const emit = (
window as Window & {
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
channelName: string;
content: string;
extraTags: string[][];
}) => unknown;
}
).__BUZZ_E2E_EMIT_MOCK_MESSAGE__;
if (!emit) throw new Error("Mock message emitter is unavailable.");
emit({
channelName: "general",
content: `[voice-note-123.mp4](${audioUrl})`,
extraTags: [
[
"imeta",
`url ${audioUrl}`,
"m video/mp4",
"duration 9.4",
"filename voice-note-123.mp4",
],
],
});
},
{ audioUrl: AUDIO_URL },
);
const card = page.getByTestId("audio-message-attachment").last();
const waveform = card.getByTestId("voice-note-playback-waveform");
await expect(waveform).toHaveAttribute("data-waveform-state", "error");
await expect(card.getByRole("status")).toContainText(
"Waveform preview unavailable. Playback may still work.",
);
await card.getByRole("button", { name: "Play voice note" }).click();
await expect(card.getByRole("alert")).toContainText("Audio unavailable");
const retry = card.getByRole("button", { name: "Retry voice note" });
await retry.click();
await expect(
card.getByRole("button", { name: "Play voice note" }),
).toBeVisible();
await card.locator("audio").dispatchEvent("error");
await expect(card.getByRole("alert")).toContainText("Audio unavailable");
});
test("resumes a Play click that landed before the source finished loading", async ({
page,
}) => {
let releaseFetch: (() => void) | undefined;
const fetchGate = new Promise<void>((resolve) => {
releaseFetch = resolve;
});
// Gate the media fetch so the received card has no playback source when the
// user first clicks Play.
await page.route(AUDIO_URL, async (route) => {
await fetchGate;
await route.continue();
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await waitForMockLiveSubscription(page, "general");
await page.evaluate(
({ audioUrl }) => {
const emit = (
window as Window & {
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
channelName: string;
content: string;
extraTags: string[][];
}) => unknown;
}
).__BUZZ_E2E_EMIT_MOCK_MESSAGE__;
if (!emit) throw new Error("Mock message emitter is unavailable.");
emit({
channelName: "general",
content: `[voice-note-123.mp4](${audioUrl})`,
extraTags: [
[
"imeta",
`url ${audioUrl}`,
"m video/mp4",
"duration 9.4",
"filename voice-note-123.mp4",
],
],
});
},
{ audioUrl: AUDIO_URL },
);
const card = page.getByTestId("audio-message-attachment").last();
await expect(card).toBeVisible();
// Click Play while the fetch is still held: the intent must be remembered and
// surfaced as a loading state, not silently dropped.
await card.getByRole("button", { name: "Play voice note" }).click();
await expect(
card.getByRole("button", { name: "Loading voice note" }),
).toBeVisible();
// Releasing the fetch resolves the source; playback starts without a second
// click.
releaseFetch?.();
await expect(
card.getByRole("button", { name: "Pause voice note" }),
).toBeVisible();
});
test("generic audio retains the relay-native download action", async ({
page,
}) => {
const audioUrl = `http://localhost:3000/media/${"d".repeat(64)}.mp3`;
await page.goto("/");
await page.getByTestId("channel-general").click();
await waitForMockLiveSubscription(page, "general");
await page.evaluate(
({ href }) => {
const emit = window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__;
if (!emit) throw new Error("Mock message emitter is unavailable.");
emit({
channelName: "general",
content: `[meeting.mp3](${href})`,
extraTags: [
["imeta", `url ${href}`, "m audio/mpeg", "filename meeting.mp3"],
],
});
},
{ href: audioUrl },
);
const card = page.getByTestId("audio-message-attachment").last();
const download = card.getByRole("button", { name: "Download meeting.mp3" });
await expect(download).toBeVisible();
await download.click();
await expect
.poll(() =>
page.evaluate(
() =>
window.__BUZZ_E2E_COMMAND_LOG__?.find(
({ command }) => command === "download_file",
) ?? null,
),
)
.toEqual({
command: "download_file",
payload: { filename: "meeting.mp3", url: audioUrl },
});
});
test("voice notes omit the relay-native download action", async ({ page }) => {
const audioUrl = `http://localhost:3000/media/${"e".repeat(64)}.mp4`;
await page.goto("/");
await page.getByTestId("channel-general").click();
await waitForMockLiveSubscription(page, "general");
await page.evaluate(
({ href }) => {
const emit = window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__;
if (!emit) throw new Error("Mock message emitter is unavailable.");
emit({
channelName: "general",
content: `[voice-note-123.mp4](${href})`,
extraTags: [
[
"imeta",
`url ${href}`,
"m video/mp4",
"duration 9.4",
"filename voice-note-123.mp4",
],
],
});
},
{ href: audioUrl },
);
const card = page.getByTestId("audio-message-attachment").last();
await expect(card).toBeVisible();
await expect(
card.getByRole("button", { name: "Download voice-note-123.mp4" }),
).toHaveCount(0);
});
test("hard-caps audio work and cancels active and queued loads on unmount", async ({
page,
}) => {
await page.addInitScript(() => {
window.__BUZZ_E2E_HOLD_MEDIA_FETCHES__ = true;
const originalCreate = URL.createObjectURL.bind(URL);
const originalRevoke = URL.revokeObjectURL.bind(URL);
const counters = { created: 0, revoked: 0 };
(
window as Window & {
__BUZZ_E2E_AUDIO_OBJECT_URLS__?: typeof counters;
}
).__BUZZ_E2E_AUDIO_OBJECT_URLS__ = counters;
URL.createObjectURL = (object) => {
counters.created += 1;
return originalCreate(object);
};
URL.revokeObjectURL = (url) => {
counters.revoked += 1;
originalRevoke(url);
};
});
// Keep all 24 cards in the load margin so the test proves queued cancellation,
// not just the three cards that happened to intersect a short viewport.
await page.setViewportSize({ width: 1280, height: 3000 });
await page.goto("/");
// Seed offscreen before opening the channel: live injection during initial
// history/scroll settlement can unmount one visible set and load another.
await page.getByTestId("channel-random").click();
await expect(page.getByTestId("chat-title")).toHaveText("random");
await page.evaluate(
({ audioUrl }) => {
const emit = (
window as Window & {
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
channelName: string;
content: string;
extraTags: string[][];
createdAt: number;
}) => unknown;
}
).__BUZZ_E2E_EMIT_MOCK_MESSAGE__;
if (!emit) throw new Error("Mock message emitter is unavailable.");
const createdAt = Math.floor(Date.now() / 1000);
for (let index = 0; index < 24; index += 1) {
emit({
channelName: "general",
createdAt: createdAt + index,
content: `[voice-note-${index}.mp4](${audioUrl})`,
extraTags: [
[
"imeta",
`url ${audioUrl}`,
"m video/mp4",
"duration 9.4",
`filename voice-note-${index}.mp4`,
],
],
});
}
},
{ audioUrl: AUDIO_URL },
);
await page.getByTestId("channel-general").click();
const cards = page.getByTestId("audio-message-attachment");
await expect(cards).toHaveCount(24);
const readFetchCount = () =>
page.evaluate(
() =>
window.__BUZZ_E2E_COMMANDS__?.filter(
(command) => command === "fetch_media_bytes",
).length ?? 0,
);
await expect
.poll(() =>
page.evaluate(
() => window.__BUZZ_E2E_MEDIA_FETCH_STATE__ ?? { active: 0, peak: 0 },
),
)
.toEqual({ active: 3, peak: 3 });
await expect
.poll(() => page.evaluate(() => window.__BUZZ_E2E_AUDIO_LOAD_STATE__?.()))
.toEqual({ active: 3, queued: 21 });
expect(await readFetchCount()).toBe(3);
expect(
await page.evaluate(
() => window.__BUZZ_E2E_AUDIO_OBJECT_URLS__?.created ?? 0,
),
).toBe(0);
await page.getByTestId("channel-random").click();
await expect(cards).toHaveCount(0);
await expect
.poll(() =>
page.evaluate(() => window.__BUZZ_E2E_MEDIA_FETCH_STATE__?.active ?? -1),
)
.toBe(0);
await expect
.poll(() => page.evaluate(() => window.__BUZZ_E2E_AUDIO_LOAD_STATE__?.()))
.toEqual({ active: 0, queued: 0 });
const commandCounts = await page.evaluate(() => {
const commands = window.__BUZZ_E2E_COMMANDS__ ?? [];
return {
cancelled: commands.filter((command) => command === "cancel_media_fetch")
.length,
fetched: commands.filter((command) => command === "fetch_media_bytes")
.length,
released: commands.filter((command) => command === "release_media_fetch")
.length,
};
});
expect(commandCounts).toEqual({ cancelled: 3, fetched: 3, released: 3 });
expect(
await page.evaluate(() => window.__BUZZ_E2E_AUDIO_OBJECT_URLS__),
).toEqual({ created: 0, revoked: 0 });
});
test("records from the composer and renders an inline waveform card", async ({
page,
}) => {
await page.emulateMedia({ reducedMotion: "no-preference" });
await page.setViewportSize({ width: 2200, height: 720 });
await page.goto("/");
const showError = page.getByRole("button", { name: "Show Error" });
const channel = page.getByTestId("channel-general");
await expect(channel.or(showError)).toBeVisible({ timeout: 10_000 });
if (await showError.isVisible()) {
await showError.click();
throw new Error(await page.locator("body").innerText());
}
await channel.click();
const attach = page.getByRole("button", { name: "Attach file" });
const record = page.getByRole("button", { name: "Record voice note" });
const emoji = page.getByRole("button", { name: "Emoji" });
await expect(record).toBeVisible();
const [attachBox, recordBox, emojiBox] = await Promise.all([
attach.boundingBox(),
record.boundingBox(),
emoji.boundingBox(),
]);
expect(attachBox).not.toBeNull();
expect(recordBox).not.toBeNull();
expect(emojiBox).not.toBeNull();
expect(recordBox?.x).toBeGreaterThan(attachBox?.x ?? 0);
expect(recordBox?.x).toBeLessThan(emojiBox?.x ?? Number.POSITIVE_INFINITY);
await record.click();
const recorder = page.getByTestId("voice-note-recorder");
await expect(recorder).toBeVisible();
await expect(
page.getByRole("status", { name: "Recording voice note" }),
).toHaveCount(0);
await expect(attach).toBeHidden();
const discard = page.getByRole("button", { name: "Discard voice note" });
await expect(discard).toBeVisible();
const [recorderBox, discardBox] = await Promise.all([
recorder.boundingBox(),
discard.boundingBox(),
]);
expect(recorderBox).not.toBeNull();
expect(discardBox).not.toBeNull();
expect(discardBox?.x ?? Number.POSITIVE_INFINITY).toBeLessThanOrEqual(
(recorderBox?.x ?? 0) + 1,
);
await expect(recorder).toContainText(/\d+:\d{2} \/ 5:00/);
const finish = page.getByRole("button", { name: "Finish voice note" });
await expect(finish).toBeEnabled();
const liveWaveform = page.getByTestId("voice-note-live-waveform");
const firstRecordedSample = liveWaveform.locator(
'[data-waveform-sample="recorded-0"]',
);
await expect(firstRecordedSample).toBeAttached();
const firstSampleStart = await firstRecordedSample.boundingBox();
await page.waitForTimeout(200);
const firstSampleAfter = await firstRecordedSample.boundingBox();
expect(firstSampleStart).not.toBeNull();
expect(firstSampleAfter).not.toBeNull();
expect(firstSampleAfter?.x ?? 0).toBeLessThan((firstSampleStart?.x ?? 0) - 2);
expect(firstSampleAfter?.height).toBe(firstSampleStart?.height);
const [waveformBox, lastBarBox] = await Promise.all([
liveWaveform.boundingBox(),
liveWaveform.locator("span").last().boundingBox(),
]);
expect(waveformBox).not.toBeNull();
expect(lastBarBox).not.toBeNull();
expect((lastBarBox?.x ?? 0) + (lastBarBox?.width ?? 0)).toBeGreaterThan(
(waveformBox?.x ?? 0) + (waveformBox?.width ?? 0) - 6,
);
await waitForAnimations(page);
await page.getByTestId("message-composer-toolbar").screenshot({
path: "test-results/voice-note/voice-note-recorder.png",
});
await finish.click();
const composerCard = page.getByTestId("composer-voice-note-card");
await expect(composerCard).toBeVisible();
await waitForAnimations(page);
const composerCardBox = await composerCard.boundingBox();
expect(composerCardBox).not.toBeNull();
expect(
composerCardBox?.width ?? Number.POSITIVE_INFINITY,
).toBeLessThanOrEqual(336);
await expect(
composerCard.getByRole("button", { name: "Play voice note" }),
).toBeVisible();
await expect(
composerCard.getByRole("slider", { name: "Voice note playback position" }),
).toBeVisible();
await expect(composerCard).toContainText(/\d+:\d{2}/);
await expect(composerCard).not.toContainText(/· Voice note/);
const composerVoiceNote = composerCard.locator("..");
const removeVoiceNote = composerVoiceNote.getByTestId(
"remove-composer-voice-note",
);
await expect(removeVoiceNote).toHaveCSS("opacity", "0");
await composerVoiceNote.hover();
await expect(removeVoiceNote).toHaveCSS("opacity", "1");
const removeVoiceNoteBox = await removeVoiceNote.boundingBox();
expect(removeVoiceNoteBox).not.toBeNull();
expect(removeVoiceNoteBox?.x ?? Number.POSITIVE_INFINITY).toBeLessThan(
(composerCardBox?.x ?? 0) + (composerCardBox?.width ?? 0),
);
expect(removeVoiceNoteBox?.y ?? Number.POSITIVE_INFINITY).toBeLessThan(
composerCardBox?.y ?? 0,
);
expect(
(removeVoiceNoteBox?.x ?? 0) + (removeVoiceNoteBox?.width ?? 0),
).toBeGreaterThan((composerCardBox?.x ?? 0) + (composerCardBox?.width ?? 0));
expect(
(removeVoiceNoteBox?.y ?? 0) + (removeVoiceNoteBox?.height ?? 0),
).toBeGreaterThan(composerCardBox?.y ?? 0);
const composerPlaybackWaveform = composerCard.getByTestId(
"voice-note-playback-waveform",
);
const [playbackBox, playbackLastBarBox] = await Promise.all([
composerPlaybackWaveform.boundingBox(),
composerPlaybackWaveform.locator("span").last().boundingBox(),
]);
expect(playbackBox).not.toBeNull();
expect(playbackLastBarBox).not.toBeNull();
expect(
(playbackLastBarBox?.x ?? 0) + (playbackLastBarBox?.width ?? 0),
).toBeGreaterThan((playbackBox?.x ?? 0) + (playbackBox?.width ?? 0) - 6);
await waitForAnimations(page);
await page.screenshot({
clip: {
height: (composerCardBox?.height ?? 0) + 16,
width: (composerCardBox?.width ?? 0) + 16,
x: (composerCardBox?.x ?? 0) - 8,
y: (composerCardBox?.y ?? 0) - 8,
},
path: "test-results/voice-note/voice-note-composer-card.png",
});
await expect(page.getByTestId("send-message")).toBeEnabled();
await page.getByTestId("send-message").click();
const card = page.getByTestId("audio-message-attachment").last();
await expect(card).toBeVisible();
const playbackWaveform = card.getByTestId("voice-note-playback-waveform");
await expect(playbackWaveform).toHaveAttribute(
"data-waveform-state",
"ready",
);
await expect
.poll(() =>
playbackWaveform
.locator("span")
.evaluateAll((bars) =>
bars.some((bar) => bar.getBoundingClientRect().height > 3),
),
)
.toBe(true);
const waveformGeometry = await playbackWaveform.evaluate((waveform) => {
const waveformBox = waveform.getBoundingClientRect();
return Array.from(waveform.querySelectorAll("span")).map((bar) => {
const barBox = bar.getBoundingClientRect();
return {
bottom: barBox.bottom,
radius: Number.parseFloat(getComputedStyle(bar).borderTopLeftRadius),
top: barBox.top,
waveformBottom: waveformBox.bottom,
waveformTop: waveformBox.top,
width: barBox.width,
};
});
});
expect(waveformGeometry.length).toBeGreaterThan(0);
for (const bar of waveformGeometry) {
expect(bar.radius).toBeGreaterThanOrEqual(bar.width / 2);
expect(bar.top).toBeGreaterThanOrEqual(bar.waveformTop);
expect(bar.bottom).toBeLessThanOrEqual(bar.waveformBottom);
}
const playbackControl = card.getByTestId("voice-note-playback-control");
await expect(card).toHaveClass(/\brounded-2xl\b/);
await expect(playbackControl).toHaveClass(/\brounded-lg\b/);
await expectSmoothCorners(card);
await expectSmoothCorners(playbackControl);
await expect(
card.getByRole("button", { name: "Play voice note" }),
).toBeVisible();
const playPauseIcon = card.getByTestId("voice-note-play-pause-icon");
const primaryIconPath = card.getByTestId("voice-note-play-pause-path-0");
await expect(playPauseIcon).toHaveAttribute("data-icon-state", "play");
await expect(playPauseIcon).toHaveCSS("width", "23px");
await expect(playPauseIcon).toHaveCSS("height", "23px");
await expect(primaryIconPath).toHaveCSS("fill", /rgb/);
await expect(primaryIconPath).toHaveCSS("stroke-linejoin", "round");
const playPath = await primaryIconPath.getAttribute("d");
await primaryIconPath.evaluate((element) => {
element.setAttribute("data-morph-identity", "preserved");
});
await card.getByRole("button", { name: "Play voice note" }).click();
await expect(
card.getByRole("button", { name: "Pause voice note" }),
).toBeVisible();
await expect(playPauseIcon).toHaveAttribute("data-icon-state", "pause");
await expect(primaryIconPath).toHaveAttribute(
"data-morph-identity",
"preserved",
);
await expect.poll(() => primaryIconPath.getAttribute("d")).not.toBe(playPath);
await waitForAnimations(page);
await card.screenshot({
path: "test-results/voice-note/voice-note-card-paused.png",
});
await card.getByRole("button", { name: "Pause voice note" }).click();
await page.mouse.move(0, 0);
await page.evaluate(() => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
});
await expect(
card.getByRole("slider", { name: "Voice note playback position" }),
).toBeVisible();
await expect(card).toContainText(/\d+:\d{2}/);
await expect(card).not.toContainText(/· Voice note/);
const playbackRate = card.getByTestId("voice-note-playback-rate");
const playbackRateValue = card.getByTestId("voice-note-playback-rate-value");
await expect(playbackRate).toHaveCSS("opacity", "0");
await waitForAnimations(page);
await card.hover();
await expect(playbackRate).toHaveCSS("opacity", "1");
await expectSmoothCorners(playbackRate);
await expect(playbackRate).toHaveCSS("padding-left", "10px");
await expect(playbackRate).toHaveCSS("padding-top", "2px");
const [rateBackground, playButtonBackground] = await Promise.all([
playbackRate.evaluate(
(element) => getComputedStyle(element).backgroundColor,
),
card
.getByRole("button", { name: "Play voice note" })
.locator("..")
.evaluate((element) => getComputedStyle(element).backgroundColor),
]);
expect(rateBackground).toBe(playButtonBackground);
const widestPlaybackRateWidth = (await playbackRate.boundingBox())?.width;
expect(widestPlaybackRateWidth).toBeGreaterThan(0);
await expect(playbackRateValue).toHaveText("1×");
await playbackRate.click();
await expect(playbackRateValue).toHaveText("1.5×");
expect((await playbackRate.boundingBox())?.width).toBe(
widestPlaybackRateWidth,
);
await expect(card.locator("audio")).toHaveJSProperty("playbackRate", 1.5);
await playbackRate.click();
await expect(playbackRateValue).toHaveText("2×");
expect((await playbackRate.boundingBox())?.width).toBe(
widestPlaybackRateWidth,
);
await playbackRate.click();
await expect(playbackRateValue).toHaveText(".5×");
expect((await playbackRate.boundingBox())?.width).toBe(
widestPlaybackRateWidth,
);
await playbackRate.click();
await expect(playbackRateValue).toHaveText("1×");
const slider = card.getByRole("slider", {
name: "Voice note playback position",
});
await slider.focus();
await expect(card.getByTestId("voice-note-playback-waveform")).toHaveCSS(
"box-shadow",
/rgb/,
);
await slider.evaluate((element: HTMLInputElement) => {
element.value = String(Number(element.max) / 2);
element.dispatchEvent(new Event("input", { bubbles: true }));
});
const progressClip = await card
.getByTestId("voice-note-progress-waveform")
.evaluate((element) => getComputedStyle(element).clipPath);
expect(progressClip).toContain("50%");
await waitForAnimations(page);
await card.screenshot({
path: "test-results/voice-note/voice-note-card.png",
});
});
test("keeps emoji available but blocks GIFs beside a queued voice note", async ({
page,
}) => {
await page.route("http://localhost:3000/info", (route) =>
route.fulfill({
body: JSON.stringify({
gif: {
provider: "klipy",
search: "/gifs/search",
share: "/gifs/share",
},
supported_extensions: ["buzz-gif"],
}),
contentType: "application/nostr+json",
}),
);
await page.goto("/");
await page.getByTestId("channel-general").click();
await page.getByRole("button", { name: "Record voice note" }).click();
await page.waitForTimeout(100);
await page.getByRole("button", { name: "Finish voice note" }).click();
await expect(page.getByTestId("composer-voice-note-card")).toBeVisible();
const pickerButton = page.getByTestId("composer-emoji-button");
await expect(pickerButton).toBeEnabled();
await expect(pickerButton).toHaveAccessibleName("Insert emoji or GIF");
await pickerButton.click();
await expect(page.getByRole("tab", { name: "Emoji" })).toBeEnabled();
await expect(page.getByRole("tab", { name: "GIFs" })).toBeDisabled();
});
test("starting a duplicate voice-note player pauses the other instance", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await waitForMockLiveSubscription(page, "general");
await page.evaluate(
({ audioUrl }) => {
const emit = (
window as Window & {
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
channelName: string;
content: string;
extraTags: string[][];
}) => unknown;
}
).__BUZZ_E2E_EMIT_MOCK_MESSAGE__;
if (!emit) throw new Error("Mock message emitter is unavailable.");
const input = {
channelName: "general",
content: `[voice-note-123.mp4](${audioUrl})`,
extraTags: [
[
"imeta",
`url ${audioUrl}`,
"m video/mp4",
"duration 9.4",
"filename voice-note-123.mp4",
],
],
};
emit(input);
emit(input);
},
{ audioUrl: AUDIO_URL },
);
const cards = page.getByTestId("audio-message-attachment");
await expect(cards).toHaveCount(2);
// The ping fixture lasts about one second, so it can finish before the
// playing state is observed. Loop it so only exclusivity pauses a player.
await cards.locator("audio").evaluateAll((audios) => {
for (const audio of audios) (audio as HTMLAudioElement).loop = true;
});
const firstAudio = cards.nth(0).locator("audio");
const secondAudio = cards.nth(1).locator("audio");
await cards.nth(0).getByRole("button", { name: "Play voice note" }).click();
await expect(firstAudio).toHaveJSProperty("paused", false);
await cards.nth(1).getByRole("button", { name: "Play voice note" }).click();
await expect(firstAudio).toHaveJSProperty("paused", true);
await expect(secondAudio).toHaveJSProperty("paused", false);
});