Merge matrix-react-sdk into element-web

Merge remote-tracking branch 'repomerge/t3chguy/repomerge' into t3chguy/repo-merge

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2024-10-15 14:57:26 +01:00
commit f0ee7f7905
No known key found for this signature in database
GPG key ID: A2B008A5F49F5D0D
3265 changed files with 484599 additions and 699 deletions

View file

@ -0,0 +1,156 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { mocked } from "jest-mock";
import { logger } from "matrix-js-sdk/src/logger";
import { fireEvent, render, RenderResult } from "jest-matrix-react";
import RecordingPlayback, {
PlaybackLayout,
} from "../../../../../src/components/views/audio_messages/RecordingPlayback";
import { Playback } from "../../../../../src/audio/Playback";
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { createAudioContext } from "../../../../../src/audio/compat";
import { flushPromises } from "../../../../test-utils";
import { IRoomState } from "../../../../../src/components/structures/RoomView";
jest.mock("../../../../../src/WorkerManager", () => ({
WorkerManager: jest.fn(() => ({
call: jest.fn().mockResolvedValue({ waveform: [0, 0, 1, 1] }),
})),
}));
jest.mock("../../../../../src/audio/compat", () => ({
createAudioContext: jest.fn(),
decodeOgg: jest.fn().mockResolvedValue({}),
}));
describe("<RecordingPlayback />", () => {
const mockAudioBufferSourceNode = {
addEventListener: jest.fn(),
connect: jest.fn(),
start: jest.fn(),
};
const mockAudioContext = {
decodeAudioData: jest.fn(),
suspend: jest.fn(),
resume: jest.fn(),
currentTime: 0,
createBufferSource: jest.fn().mockReturnValue(mockAudioBufferSourceNode),
};
const mockAudioBuffer = {
duration: 99,
getChannelData: jest.fn(),
};
const mockChannelData = new Float32Array();
const defaultRoom = { roomId: "!room:server.org", timelineRenderingType: TimelineRenderingType.File } as IRoomState;
const getComponent = (props: React.ComponentProps<typeof RecordingPlayback>, room = defaultRoom) =>
render(
<RoomContext.Provider value={room}>
<RecordingPlayback {...props} />
</RoomContext.Provider>,
);
beforeEach(() => {
jest.spyOn(logger, "error").mockRestore();
mockAudioBuffer.getChannelData.mockClear().mockReturnValue(mockChannelData);
mockAudioContext.decodeAudioData.mockReset().mockImplementation((_b, callback) => callback(mockAudioBuffer));
mocked(createAudioContext).mockReturnValue(mockAudioContext as unknown as AudioContext);
});
const getPlayButton = (component: RenderResult) => component.getByTestId("play-pause-button");
it("renders recording playback", () => {
const playback = new Playback(new ArrayBuffer(8));
const component = getComponent({ playback });
expect(component).toBeTruthy();
});
it("disables play button while playback is decoding", async () => {
const playback = new Playback(new ArrayBuffer(8));
const component = getComponent({ playback });
expect(getPlayButton(component)).toHaveAttribute("disabled");
expect(getPlayButton(component)).toHaveAttribute("aria-disabled", "true");
});
it("enables play button when playback is finished decoding", async () => {
const playback = new Playback(new ArrayBuffer(8));
const component = getComponent({ playback });
await flushPromises();
expect(getPlayButton(component)).not.toHaveAttribute("disabled");
expect(getPlayButton(component)).not.toHaveAttribute("aria-disabled", "true");
});
it("displays error when playback decoding fails", async () => {
// stub logger to keep console clean from expected error
jest.spyOn(logger, "error").mockReturnValue(undefined);
jest.spyOn(logger, "warn").mockReturnValue(undefined);
mockAudioContext.decodeAudioData.mockImplementation((_b, _cb, error) => error(new Error("oh no")));
const playback = new Playback(new ArrayBuffer(8));
const component = getComponent({ playback });
await flushPromises();
expect(component.container.querySelector(".text-warning")).toBeDefined();
});
it("displays pre-prepared playback with correct playback phase", async () => {
const playback = new Playback(new ArrayBuffer(8));
await playback.prepare();
const component = getComponent({ playback });
// playback already decoded, button is not disabled
expect(getPlayButton(component)).not.toHaveAttribute("disabled");
expect(getPlayButton(component)).not.toHaveAttribute("aria-disabled", "true");
expect(component.container.querySelector(".text-warning")).toBeFalsy();
});
it("toggles playback on play pause button click", async () => {
const playback = new Playback(new ArrayBuffer(8));
jest.spyOn(playback, "toggle").mockResolvedValue(undefined);
await playback.prepare();
const component = getComponent({ playback });
fireEvent.click(getPlayButton(component));
expect(playback.toggle).toHaveBeenCalled();
});
describe("Composer Layout", () => {
it("should have a waveform, no seek bar, and clock", () => {
const playback = new Playback(new ArrayBuffer(8));
const component = getComponent({ playback, layout: PlaybackLayout.Composer });
expect(component.container.querySelector(".mx_Clock")).toBeDefined();
expect(component.container.querySelector(".mx_Waveform")).toBeDefined();
expect(component.container.querySelector(".mx_SeekBar")).toBeFalsy();
});
});
describe("Timeline Layout", () => {
it("should have a waveform, a seek bar, and clock", () => {
const playback = new Playback(new ArrayBuffer(8));
const component = getComponent({ playback, layout: PlaybackLayout.Timeline });
expect(component.container.querySelector(".mx_Clock")).toBeDefined();
expect(component.container.querySelector(".mx_Waveform")).toBeDefined();
expect(component.container.querySelector(".mx_SeekBar")).toBeDefined();
});
it("should be the default", () => {
const playback = new Playback(new ArrayBuffer(8));
const component = getComponent({ playback }); // no layout set for test
expect(component.container.querySelector(".mx_Clock")).toBeDefined();
expect(component.container.querySelector(".mx_Waveform")).toBeDefined();
expect(component.container.querySelector(".mx_SeekBar")).toBeDefined();
});
});
});

View file

@ -0,0 +1,125 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React, { createRef, RefObject } from "react";
import { mocked } from "jest-mock";
import { act, fireEvent, render, RenderResult } from "jest-matrix-react";
import { Playback } from "../../../../../src/audio/Playback";
import { createTestPlayback } from "../../../../test-utils/audio";
import SeekBar from "../../../../../src/components/views/audio_messages/SeekBar";
describe("SeekBar", () => {
let playback: Playback;
let renderResult: RenderResult;
let frameRequestCallback: FrameRequestCallback;
let seekBarRef: RefObject<SeekBar>;
beforeEach(() => {
seekBarRef = createRef();
jest.spyOn(window, "requestAnimationFrame").mockImplementation((callback: FrameRequestCallback) => {
frameRequestCallback = callback;
return 0;
});
});
afterEach(() => {
mocked(window.requestAnimationFrame).mockRestore();
});
describe("when rendering a SeekBar for an empty playback", () => {
beforeEach(() => {
playback = createTestPlayback({
durationSeconds: 0,
timeSeconds: 0,
});
renderResult = render(<SeekBar ref={seekBarRef} playback={playback} />);
});
it("should render correctly", () => {
expect(renderResult.container).toMatchSnapshot();
});
});
describe("when rendering a SeekBar", () => {
beforeEach(() => {
playback = createTestPlayback();
renderResult = render(<SeekBar ref={seekBarRef} playback={playback} />);
});
it("should render the initial position", () => {
// expected value 3141 / 31415 ~ 0.099984084
expect(renderResult.container).toMatchSnapshot();
});
describe("and the playback proceeds", () => {
beforeEach(async () => {
// @ts-ignore
playback.timeSeconds = 6969;
act(() => {
playback.liveData.update([playback.timeSeconds, playback.durationSeconds]);
frameRequestCallback(0);
});
});
it("should render as expected", () => {
// expected value 6969 / 31415 ~ 0.221836702
expect(renderResult.container).toMatchSnapshot();
});
});
describe("and seeking position with the slider", () => {
beforeEach(() => {
const rangeInput = renderResult.container.querySelector("[type='range']");
act(() => {
fireEvent.change(rangeInput!, { target: { value: 0.5 } });
});
});
it("should update the playback", () => {
expect(playback.skipTo).toHaveBeenCalledWith(0.5 * playback.durationSeconds);
});
describe("and seeking left", () => {
beforeEach(() => {
mocked(playback.skipTo).mockClear();
act(() => {
seekBarRef.current!.left();
});
});
it("should skip to minus 5 seconds", () => {
expect(playback.skipTo).toHaveBeenCalledWith(playback.timeSeconds - 5);
});
});
describe("and seeking right", () => {
beforeEach(() => {
mocked(playback.skipTo).mockClear();
act(() => {
seekBarRef.current!.right();
});
});
it("should skip to plus 5 seconds", () => {
expect(playback.skipTo).toHaveBeenCalledWith(playback.timeSeconds + 5);
});
});
});
});
describe("when rendering a disabled SeekBar", () => {
beforeEach(async () => {
renderResult = render(<SeekBar disabled={true} playback={playback} />);
});
it("should render as expected", () => {
expect(renderResult.container).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,66 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SeekBar when rendering a SeekBar and the playback proceeds should render as expected 1`] = `
<div>
<input
aria-label="Audio seek bar"
class="mx_SeekBar"
max="1"
min="0"
step="0.001"
style="--fillTo: 0.22183670221231896;"
tabindex="0"
type="range"
value="0.22183670221231896"
/>
</div>
`;
exports[`SeekBar when rendering a SeekBar for an empty playback should render correctly 1`] = `
<div>
<input
aria-label="Audio seek bar"
class="mx_SeekBar"
max="1"
min="0"
step="0.001"
style="--fillTo: 0;"
tabindex="0"
type="range"
value="0"
/>
</div>
`;
exports[`SeekBar when rendering a SeekBar should render the initial position 1`] = `
<div>
<input
aria-label="Audio seek bar"
class="mx_SeekBar"
max="1"
min="0"
step="0.001"
style="--fillTo: 0.0999840840362884;"
tabindex="0"
type="range"
value="0.0999840840362884"
/>
</div>
`;
exports[`SeekBar when rendering a disabled SeekBar should render as expected 1`] = `
<div>
<input
aria-label="Audio seek bar"
class="mx_SeekBar"
disabled=""
max="1"
min="0"
step="0.001"
style="--fillTo: 0.0999840840362884;"
tabindex="0"
type="range"
value="0.0999840840362884"
/>
</div>
`;