From 0f530f636bd66785da8cfe42c849b506ffeb7ba7 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 7 Oct 2025 23:03:25 +0100 Subject: [PATCH] Fix voice notes rendering at 00:00 when playback had not begun. (#30961) * Fix clocks rendering at 00:00 when playback had not begun. * Add a rendering test * Add a test * remove only * add another test --- playwright/e2e/timeline/timeline.spec.ts | 20 ++++- .../timeline.spec.ts/voice-message-linux.png | Bin 0 -> 2351 bytes src/audio/PlaybackQueue.ts | 12 ++- test/unit-tests/audio/PlaybackQueue-test.ts | 74 ++++++++++++++++++ 4 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 playwright/snapshots/timeline/timeline.spec.ts/voice-message-linux.png create mode 100644 test/unit-tests/audio/PlaybackQueue-test.ts diff --git a/playwright/e2e/timeline/timeline.spec.ts b/playwright/e2e/timeline/timeline.spec.ts index 55ae3b16dc..c3104ad076 100644 --- a/playwright/e2e/timeline/timeline.spec.ts +++ b/playwright/e2e/timeline/timeline.spec.ts @@ -961,7 +961,7 @@ test.describe("Timeline", () => { const reply = "Reply"; const viewRoomSendMessageAndSetupReply = async (page: Page, app: ElementAppPage, roomId: string) => { // View room - await page.goto(`/#/room/${roomId}`); + await app.viewRoomById(roomId); // Send a message const composer = app.getComposerField(); @@ -993,6 +993,24 @@ test.describe("Timeline", () => { await expect(eventTileLine.getByText(reply)).toHaveCount(1); }); + test("can send a voice message", { tag: "@screenshot" }, async ({ page, app, room, context }) => { + await app.viewRoomById(room.roomId); + + const composerOptions = await app.openMessageComposerOptions(); + await composerOptions.getByRole("menuitem", { name: "Voice Message" }).click(); + + // Record an empty message + await page.waitForTimeout(3000); + + const roomViewBody = page.locator(".mx_RoomView_body"); + await roomViewBody + .locator(".mx_MessageComposer") + .getByRole("button", { name: "Send voice message" }) + .click(); + + await expect(roomViewBody.locator(".mx_MVoiceMessageBody")).toMatchScreenshot("voice-message.png"); + }); + test("can reply with a voice message", async ({ page, app, room, context }) => { await viewRoomSendMessageAndSetupReply(page, app, room.roomId); diff --git a/playwright/snapshots/timeline/timeline.spec.ts/voice-message-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/voice-message-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..ba1cb6e3f6019cecca0003611733d91a64d6dae1 GIT binary patch literal 2351 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 0) { + // skipTo will pause playback, which causes the clock to render the current + // playback seconds. If the clock state is 0, then we can just ignore + // skipping entirely. + // noinspection JSIgnoredPromiseFromCall + playback.skipTo(currentClockState); + } } else if (newState === PlaybackState.Stopped) { // Remove the now-useless clock for some space savings this.clockStates.delete(mxEvent.getId()!); diff --git a/test/unit-tests/audio/PlaybackQueue-test.ts b/test/unit-tests/audio/PlaybackQueue-test.ts new file mode 100644 index 0000000000..e43b975e2c --- /dev/null +++ b/test/unit-tests/audio/PlaybackQueue-test.ts @@ -0,0 +1,74 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { type Mocked } from "jest-mock"; +import { type MatrixEvent, type Room } from "matrix-js-sdk/src/matrix"; +import { SimpleObservable } from "matrix-widget-api"; + +import { PlaybackQueue } from "../../../src/audio/PlaybackQueue"; +import { PlaybackState, type Playback } from "../../../src/audio/Playback"; +import { MockEventEmitter } from "../../test-utils"; +import { UPDATE_EVENT } from "../../../src/stores/AsyncStore"; + +describe("PlaybackQueue", () => { + let playbackQueue: PlaybackQueue; + + beforeEach(() => { + const mockRoom = { + getMember: jest.fn(), + } as unknown as Mocked; + playbackQueue = new PlaybackQueue(mockRoom); + }); + + it("does not call skipTo on playback if clock advances to 0s", () => { + const mockEvent = { + getId: jest.fn().mockReturnValue("$foo:bar"), + } as unknown as Mocked; + const mockPlayback = new MockEventEmitter({ + clockInfo: { + liveData: new SimpleObservable(), + }, + skipTo: jest.fn(), + }) as unknown as Mocked; + + // Enqueue + playbackQueue.unsortedEnqueue(mockEvent, mockPlayback); + + // Emit our clockInfo of 0, which will playbackQueue to save the state. + mockPlayback.clockInfo.liveData.update([0]); + + // Fire an update event to say that we have stopped. + // Note that Playback really emits an UPDATE_EVENT whenever state changes, the types are lies. + mockPlayback.emit(UPDATE_EVENT as any, PlaybackState.Stopped); + + expect(mockPlayback.skipTo).not.toHaveBeenCalled(); + }); + + it("does call skipTo on playback if clock advances to 0s", () => { + const mockEvent = { + getId: jest.fn().mockReturnValue("$foo:bar"), + } as unknown as Mocked; + const mockPlayback = new MockEventEmitter({ + clockInfo: { + liveData: new SimpleObservable(), + }, + skipTo: jest.fn(), + }) as unknown as Mocked; + + // Enqueue + playbackQueue.unsortedEnqueue(mockEvent, mockPlayback); + + // Emit our clockInfo of 0, which will playbackQueue to save the state. + mockPlayback.clockInfo.liveData.update([1]); + + // Fire an update event to say that we have stopped. + // Note that Playback really emits an UPDATE_EVENT whenever state changes, the types are lies. + mockPlayback.emit(UPDATE_EVENT as any, PlaybackState.Stopped); + + expect(mockPlayback.skipTo).toHaveBeenCalledWith(1); + }); +});