Merge branch 'develop' into unread-title-indicator

This commit is contained in:
Florian Duros 2023-02-07 11:37:34 +01:00 committed by GitHub
commit 4c7945552c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 312 additions and 191 deletions

View file

@ -1,6 +1,9 @@
module.exports = {
plugins: ["matrix-org"],
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"],
parserOptions: {
project: ["./tsconfig.json"],
},
env: {
browser: true,
node: true,
@ -168,6 +171,12 @@ module.exports = {
"@typescript-eslint/explicit-member-accessibility": "off",
},
},
{
files: ["cypress/**/*.ts"],
parserOptions: {
project: ["./cypress/tsconfig.json"],
},
},
],
settings: {
react: {

View file

@ -52,6 +52,8 @@ const handleVerificationRequest = (request: VerificationRequest): Chainable<Emoj
verifier.on("show_sas", onShowSas);
verifier.verify();
}),
// extra timeout, as this sometimes takes a while
{ timeout: 30_000 },
);
};
@ -111,9 +113,8 @@ describe("Decryption Failure Bar", () => {
})
.then(() => {
cy.botSendMessage(bot, roomId, "test");
cy.wait(5000);
cy.get(".mx_DecryptionFailureBar .mx_DecryptionFailureBar_message_headline").should(
"have.text",
cy.contains(
".mx_DecryptionFailureBar .mx_DecryptionFailureBar_message_headline",
"Verify this device to access all messages",
);
@ -124,6 +125,7 @@ describe("Decryption Failure Bar", () => {
const verificationRequestPromise = waitForVerificationRequest(otherDevice);
cy.get(".mx_CompleteSecurity_actionRow .mx_AccessibleButton").click();
cy.contains("To proceed, please accept the verification request on your other device.");
cy.wrap(verificationRequestPromise).then((verificationRequest: VerificationRequest) => {
cy.wrap(verificationRequest.accept());
handleVerificationRequest(verificationRequest).then((emojis) => {
@ -170,9 +172,8 @@ describe("Decryption Failure Bar", () => {
);
cy.botSendMessage(bot, roomId, "test");
cy.wait(5000);
cy.get(".mx_DecryptionFailureBar .mx_DecryptionFailureBar_message_headline").should(
"have.text",
cy.contains(
".mx_DecryptionFailureBar .mx_DecryptionFailureBar_message_headline",
"Reset your keys to prevent future decryption errors",
);

View file

@ -163,6 +163,8 @@ function setupBotClient(
}
})
.then(() => cli),
// extra timeout, as this sometimes takes a while
{ timeout: 30_000 },
);
});
}

View file

@ -190,7 +190,7 @@
"eslint-plugin-deprecate": "^0.7.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-matrix-org": "0.9.0",
"eslint-plugin-matrix-org": "0.10.0",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-unicorn": "^45.0.0",

View file

@ -38,6 +38,8 @@ limitations under the License.
}
.mx_AddExistingToSpace_section {
margin-right: 12px; // provides space for scrollbar so that checkbox and scrollbar do not collide
&:not(:first-child) {
margin-top: 24px;
}

View file

@ -258,17 +258,16 @@ class PipContainerInner extends React.Component<IProps, IState> {
}
private createVoiceBroadcastPlaybackPipContent(voiceBroadcastPlayback: VoiceBroadcastPlayback): CreatePipChildren {
if (this.state.viewedRoomId === voiceBroadcastPlayback.infoEvent.getRoomId()) {
return ({ onStartMoving }) => (
<div onMouseDown={onStartMoving}>
<VoiceBroadcastPlaybackBody playback={voiceBroadcastPlayback} pip={true} />
</div>
const content =
this.state.viewedRoomId === voiceBroadcastPlayback.infoEvent.getRoomId() ? (
<VoiceBroadcastPlaybackBody playback={voiceBroadcastPlayback} pip={true} />
) : (
<VoiceBroadcastSmallPlaybackBody playback={voiceBroadcastPlayback} />
);
}
return ({ onStartMoving }) => (
<div onMouseDown={onStartMoving}>
<VoiceBroadcastSmallPlaybackBody playback={voiceBroadcastPlayback} />
<div key={voiceBroadcastPlayback.infoEvent.getId()} onMouseDown={onStartMoving}>
{content}
</div>
);
}

View file

@ -48,7 +48,8 @@ export default class ServerOfflineDialog extends React.PureComponent<IProps> {
private renderTimeline(): React.ReactElement[] {
return EchoStore.instance.contexts.map((c, i) => {
if (!c.firstFailedTime) return null; // not useful
if (!(c instanceof RoomEchoContext)) throw new Error("Cannot render unknown context: " + c);
if (!(c instanceof RoomEchoContext))
throw new Error("Cannot render unknown context: " + c.constructor.name);
const header = (
<div className="mx_ServerOfflineDialog_content_context_timeline_header">
<RoomAvatar width={24} height={24} room={c.room} />

View file

@ -507,39 +507,36 @@ export default class EventListSummary extends React.Component<IProps> {
eventsToRender.forEach((e, index) => {
const type = e.getType();
let userId = e.getSender();
if (type === EventType.RoomMember) {
userId = e.getStateKey();
let userKey = e.getSender()!;
if (type === EventType.RoomThirdPartyInvite) {
userKey = e.getContent().display_name;
} else if (type === EventType.RoomMember) {
userKey = e.getStateKey();
} else if (e.isRedacted()) {
userId = e.getUnsigned()?.redacted_because?.sender;
userKey = e.getUnsigned()?.redacted_because?.sender;
}
// Initialise a user's events
if (!userEvents[userId]) {
userEvents[userId] = [];
if (!userEvents[userKey]) {
userEvents[userKey] = [];
}
let displayName = userId;
if (type === EventType.RoomThirdPartyInvite) {
displayName = e.getContent().display_name;
if (e.sender) {
latestUserAvatarMember.set(userId, e.sender);
}
} else if (e.isRedacted()) {
const sender = this.context?.room.getMember(userId);
let displayName = userKey;
if (e.isRedacted()) {
const sender = this.context?.room?.getMember(userKey);
if (sender) {
displayName = sender.name;
latestUserAvatarMember.set(userId, sender);
latestUserAvatarMember.set(userKey, sender);
}
} else if (e.target && TARGET_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
displayName = e.target.name;
latestUserAvatarMember.set(userId, e.target);
} else if (e.sender) {
latestUserAvatarMember.set(userKey, e.target);
} else if (e.sender && type !== EventType.RoomThirdPartyInvite) {
displayName = e.sender.name;
latestUserAvatarMember.set(userId, e.sender);
latestUserAvatarMember.set(userKey, e.sender);
}
userEvents[userId].push({
userEvents[userKey].push({
mxEvent: e,
displayName,
index: index,

View file

@ -116,7 +116,7 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
joinButtons = (
<>
<AccessibleButton
kind="secondary"
kind="primary_outline"
onClick={() => {
setBusy(true);
onRejectButtonClicked();

View file

@ -185,6 +185,10 @@ export default class ProfileSettings extends React.Component<{}, IState> {
withDisplayName: true,
});
// False negative result from no-base-to-string rule, doesn't seem to account for Symbol.toStringTag
// eslint-disable-next-line @typescript-eslint/no-base-to-string
const avatarUrl = this.state.avatarUrl?.toString();
return (
<form onSubmit={this.saveProfile} autoComplete="off" noValidate={true} className="mx_ProfileSettings">
<input
@ -216,7 +220,7 @@ export default class ProfileSettings extends React.Component<{}, IState> {
</p>
</div>
<AvatarSetting
avatarUrl={this.state.avatarUrl?.toString()}
avatarUrl={avatarUrl}
avatarName={this.state.displayName || this.state.userId}
avatarAltText={_t("Profile picture")}
uploadAvatar={this.uploadAvatar}

View file

@ -662,7 +662,7 @@
"Unable to decrypt voice broadcast": "Unable to decrypt voice broadcast",
"Unable to play this voice broadcast": "Unable to play this voice broadcast",
"Stop live broadcasting?": "Stop live broadcasting?",
"Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.",
"Are you sure you want to stop your live broadcast? This will end the broadcast and the full recording will be available in the room.": "Are you sure you want to stop your live broadcast? This will end the broadcast and the full recording will be available in the room.",
"Yes, stop broadcast": "Yes, stop broadcast",
"Listen to live broadcast?": "Listen to live broadcast?",
"If you start listening to this live broadcast, your current live broadcast recording will be ended.": "If you start listening to this live broadcast, your current live broadcast recording will be ended.",

View file

@ -84,7 +84,8 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true): Promise<Form
body.append("user_id", client.credentials.userId);
body.append("device_id", client.deviceId);
if (client.isCryptoEnabled()) {
// TODO: make this work with rust crypto
if (client.isCryptoEnabled() && client.crypto) {
const keys = [`ed25519:${client.getDeviceEd25519Key()}`];
if (client.getDeviceCurve25519Key) {
keys.push(`curve25519:${client.getDeviceCurve25519Key()}`);
@ -259,7 +260,7 @@ export async function downloadBugReport(opts: IOpts = {}): Promise<void> {
reader.readAsArrayBuffer(value as Blob);
});
} else {
metadata += `${key} = ${value}\n`;
metadata += `${key} = ${value as string}\n`;
}
}
tape.append("issue.txt", metadata);

View file

@ -116,7 +116,8 @@ function getEnabledLabs(): string {
}
async function getCryptoContext(client: MatrixClient): Promise<CryptoContext> {
if (!client.isCryptoEnabled()) {
// TODO: make this work with rust crypto
if (!client.isCryptoEnabled() || !client.crypto) {
return {};
}
const keys = [`ed25519:${client.getDeviceEd25519Key()}`];

View file

@ -389,7 +389,7 @@ export class StopGapWidget extends EventEmitter {
// Now open the integration manager
// TODO: Spec this interaction.
const data = ev.detail.data;
const integType = data?.integType;
const integType = data?.integType as string;
const integId = <string>data?.integId;
// noinspection JSIgnoredPromiseFromCall

View file

@ -69,7 +69,7 @@ export function presentableTextForFile(
// it since it is "ugly", users generally aren't aware what it
// means and the type of the attachment can usually be inferred
// from the file extension.
text += " (" + filesize(content.info.size) + ")";
text += " (" + <string>filesize(content.info.size) + ")";
}
return text;
}

View file

@ -19,7 +19,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { IDestroyable } from "./IDestroyable";
import { arrayFastClone } from "./arrays";
export type WhenFn<T> = (w: Whenable<T>) => void;
export type WhenFn<T extends string | number> = (w: Whenable<T>) => void;
/**
* Whenables are a cheap way to have Observable patterns mixed with typical
@ -27,7 +27,7 @@ export type WhenFn<T> = (w: Whenable<T>) => void;
* are intended to be used when a condition will be met multiple times and
* the consumer needs to know *when* that happens.
*/
export abstract class Whenable<T> implements IDestroyable {
export abstract class Whenable<T extends string | number> implements IDestroyable {
private listeners: { condition: T | null; fn: WhenFn<T> }[] = [];
/**

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ReactNode } from "react";
import React from "react";
import ReactDOM from "react-dom";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
@ -65,7 +65,7 @@ export default class HTMLExporter extends Exporter {
this.threadsEnabled = SettingsStore.getValue("feature_threadenabled");
}
protected async getRoomAvatar(): Promise<ReactNode> {
protected async getRoomAvatar(): Promise<string> {
let blob: Blob | undefined = undefined;
const avatarUrl = Avatar.avatarUrlForRoom(this.room, 32, 32, "crop");
const avatarPath = "room.png";

View file

@ -36,7 +36,7 @@ const showStopBroadcastingDialog = async (): Promise<boolean> => {
description: (
<p>
{_t(
"Are you sure you want to stop your live broadcast?" +
"Are you sure you want to stop your live broadcast? " +
"This will end the broadcast and the full recording will be available in the room.",
)}
</p>

View file

@ -396,7 +396,11 @@ export class VoiceBroadcastPlayback
}
if (!this.playbacks.has(eventId)) {
// set to buffering while loading the chunk data
const currentState = this.getState();
this.setState(VoiceBroadcastPlaybackState.Buffering);
await this.loadPlayback(event);
this.setState(currentState);
}
const playback = this.playbacks.get(eventId);

View file

@ -21,6 +21,7 @@ import { MatrixEvent, RoomMember } from "matrix-js-sdk/src/matrix";
import {
getMockClientWithEventEmitter,
mkEvent,
mkMembership,
mockClientMethodsUser,
unmockClientPeg,
@ -100,7 +101,7 @@ describe("EventListSummary", function () {
// is created by replacing the first "$" in userIdTemplate with `i` for
// `i = 0 .. n`.
const generateEventsForUsers = (userIdTemplate, n, events) => {
let eventsForUsers = [];
let eventsForUsers: MatrixEvent[] = [];
let userId = "";
for (let i = 0; i < n; i++) {
userId = userIdTemplate.replace("$", i);
@ -656,4 +657,56 @@ describe("EventListSummary", function () {
expect(summaryText).toBe("user_0, user_1 and 18 others joined");
});
it("should not blindly group 3pid invites and treat them as distinct users instead", () => {
const events = [
mkEvent({
event: true,
skey: "randomstring1",
user: "@user1:server",
type: "m.room.third_party_invite",
content: {
display_name: "n...@d...",
key_validity_url: "https://blah",
public_key: "public_key",
},
}),
mkEvent({
event: true,
skey: "randomstring2",
user: "@user1:server",
type: "m.room.third_party_invite",
content: {
display_name: "n...@d...",
key_validity_url: "https://blah",
public_key: "public_key",
},
}),
mkEvent({
event: true,
skey: "randomstring3",
user: "@user1:server",
type: "m.room.third_party_invite",
content: {
display_name: "d...@w...",
key_validity_url: "https://blah",
public_key: "public_key",
},
}),
];
const props = {
events: events,
children: generateTiles(events),
summaryLength: 2,
avatarsMaxLength: 5,
threshold: 3,
};
const wrapper = renderComponent(props);
const summary = wrapper.find(".mx_GenericEventListSummary_summary");
const summaryText = summary.text();
expect(summaryText).toBe("n...@d... was invited 2 times, d...@w... was invited");
});
});

View file

@ -18,6 +18,7 @@ import { mocked } from "jest-mock";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MatrixClient, MatrixEvent, MatrixEventEvent, Room } from "matrix-js-sdk/src/matrix";
import { defer } from "matrix-js-sdk/src/utils";
import { Playback, PlaybackState } from "../../../src/audio/Playback";
import { PlaybackManager } from "../../../src/audio/PlaybackManager";
@ -31,9 +32,10 @@ import {
VoiceBroadcastPlaybackState,
VoiceBroadcastRecording,
} from "../../../src/voice-broadcast";
import { filterConsole, flushPromises, stubClient } from "../../test-utils";
import { filterConsole, flushPromises, flushPromisesWithFakeTimers, stubClient } from "../../test-utils";
import { createTestPlayback } from "../../test-utils/audio";
import { mkVoiceBroadcastChunkEvent, mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
import { LazyValue } from "../../../src/utils/LazyValue";
jest.mock("../../../src/utils/MediaEventHelper", () => ({
MediaEventHelper: jest.fn(),
@ -49,6 +51,7 @@ describe("VoiceBroadcastPlayback", () => {
let playback: VoiceBroadcastPlayback;
let onStateChanged: (state: VoiceBroadcastPlaybackState) => void;
let chunk1Event: MatrixEvent;
let deplayedChunk1Event: MatrixEvent;
let chunk2Event: MatrixEvent;
let chunk2BEvent: MatrixEvent;
let chunk3Event: MatrixEvent;
@ -58,6 +61,7 @@ describe("VoiceBroadcastPlayback", () => {
const chunk1Data = new ArrayBuffer(2);
const chunk2Data = new ArrayBuffer(3);
const chunk3Data = new ArrayBuffer(3);
let delayedChunk1Helper: MediaEventHelper;
let chunk1Helper: MediaEventHelper;
let chunk2Helper: MediaEventHelper;
let chunk3Helper: MediaEventHelper;
@ -97,8 +101,8 @@ describe("VoiceBroadcastPlayback", () => {
};
const startPlayback = () => {
beforeEach(async () => {
await playback.start();
beforeEach(() => {
playback.start();
});
};
@ -127,11 +131,36 @@ describe("VoiceBroadcastPlayback", () => {
};
};
const mkDeplayedChunkHelper = (data: ArrayBuffer): MediaEventHelper => {
const deferred = defer<LazyValue<Blob>>();
setTimeout(() => {
deferred.resolve({
// @ts-ignore
arrayBuffer: jest.fn().mockResolvedValue(data),
});
}, 7500);
return {
sourceBlob: {
cachedValue: new Blob(),
done: false,
// @ts-ignore
value: deferred.promise,
},
};
};
const simulateFirstChunkArrived = async (): Promise<void> => {
jest.advanceTimersByTime(10000);
await flushPromisesWithFakeTimers();
};
const mkInfoEvent = (state: VoiceBroadcastInfoState) => {
return mkVoiceBroadcastInfoStateEvent(roomId, state, userId, deviceId);
};
const mkPlayback = async () => {
const mkPlayback = async (fakeTimers = false): Promise<VoiceBroadcastPlayback> => {
const playback = new VoiceBroadcastPlayback(
infoEvent,
client,
@ -140,7 +169,7 @@ describe("VoiceBroadcastPlayback", () => {
jest.spyOn(playback, "removeAllListeners");
jest.spyOn(playback, "destroy");
playback.on(VoiceBroadcastPlaybackEvent.StateChanged, onStateChanged);
await flushPromises();
fakeTimers ? await flushPromisesWithFakeTimers() : await flushPromises();
return playback;
};
@ -152,6 +181,7 @@ describe("VoiceBroadcastPlayback", () => {
const createChunkEvents = () => {
chunk1Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk1Length, 1);
deplayedChunk1Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk1Length, 1);
chunk2Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk2Length, 2);
chunk2Event.setTxnId("tx-id-1");
chunk2BEvent = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk2Length, 2);
@ -159,6 +189,7 @@ describe("VoiceBroadcastPlayback", () => {
chunk3Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk3Length, 3);
chunk1Helper = mkChunkHelper(chunk1Data);
delayedChunk1Helper = mkDeplayedChunkHelper(chunk1Data);
chunk2Helper = mkChunkHelper(chunk2Data);
chunk3Helper = mkChunkHelper(chunk3Data);
@ -181,6 +212,7 @@ describe("VoiceBroadcastPlayback", () => {
mocked(MediaEventHelper).mockImplementation((event: MatrixEvent): any => {
if (event === chunk1Event) return chunk1Helper;
if (event === deplayedChunk1Event) return delayedChunk1Helper;
if (event === chunk2Event) return chunk2Helper;
if (event === chunk3Event) return chunk3Helper;
});
@ -488,11 +520,17 @@ describe("VoiceBroadcastPlayback", () => {
describe("when there is a stopped voice broadcast", () => {
beforeEach(async () => {
jest.useFakeTimers();
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Stopped);
createChunkEvents();
setUpChunkEvents([chunk2Event, chunk1Event, chunk3Event]);
room.addLiveEvents([infoEvent, chunk1Event, chunk2Event, chunk3Event]);
playback = await mkPlayback();
// use delayed first chunk here to simulate loading time
setUpChunkEvents([chunk2Event, deplayedChunk1Event, chunk3Event]);
room.addLiveEvents([infoEvent, deplayedChunk1Event, chunk2Event, chunk3Event]);
playback = await mkPlayback(true);
});
afterEach(() => {
jest.useRealTimers();
});
it("should expose the info event", () => {
@ -504,166 +542,174 @@ describe("VoiceBroadcastPlayback", () => {
describe("and calling start", () => {
startPlayback();
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
it("should play the chunks beginning with the first one", () => {
// assert that the first chunk is being played
expect(chunk1Playback.play).toHaveBeenCalled();
expect(chunk2Playback.play).not.toHaveBeenCalled();
});
describe("and calling start again", () => {
it("should not play the first chunk a second time", () => {
expect(chunk1Playback.play).toHaveBeenCalledTimes(1);
});
});
describe("and the chunk playback progresses", () => {
beforeEach(() => {
chunk1Playback.clockInfo.liveData.update([11]);
});
it("should update the time", () => {
expect(playback.timeSeconds).toBe(11);
expect(playback.timeLeftSeconds).toBe(2);
});
});
describe("and the chunk playback progresses across the actual time", () => {
// This can be the case if the meta data is out of sync with the actual audio data.
beforeEach(() => {
chunk1Playback.clockInfo.liveData.update([15]);
});
it("should update the time", () => {
expect(playback.timeSeconds).toBe(15);
expect(playback.timeLeftSeconds).toBe(0);
});
});
describe("and skipping to the middle of the second chunk", () => {
const middleOfSecondChunk = (chunk1Length + chunk2Length / 2) / 1000;
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Buffering);
describe("and the first chunk data has been loaded", () => {
beforeEach(async () => {
await playback.skipTo(middleOfSecondChunk);
await simulateFirstChunkArrived();
});
it("should play the second chunk", () => {
expect(chunk1Playback.stop).toHaveBeenCalled();
expect(chunk1Playback.destroy).toHaveBeenCalled();
expect(chunk2Playback.play).toHaveBeenCalled();
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
it("should play the chunks beginning with the first one", () => {
// assert that the first chunk is being played
expect(chunk1Playback.play).toHaveBeenCalled();
expect(chunk2Playback.play).not.toHaveBeenCalled();
});
it("should update the time", () => {
expect(playback.timeSeconds).toBe(middleOfSecondChunk);
describe("and calling start again", () => {
it("should not play the first chunk a second time", () => {
expect(chunk1Playback.play).toHaveBeenCalledTimes(1);
});
});
describe("and skipping to the start", () => {
beforeEach(async () => {
await playback.skipTo(0);
describe("and the chunk playback progresses", () => {
beforeEach(() => {
chunk1Playback.clockInfo.liveData.update([11]);
});
it("should play the first chunk", () => {
it("should update the time", () => {
expect(playback.timeSeconds).toBe(11);
});
});
describe("and the chunk playback progresses across the actual time", () => {
// This can be the case if the meta data is out of sync with the actual audio data.
beforeEach(() => {
chunk1Playback.clockInfo.liveData.update([15]);
});
it("should update the time", () => {
expect(playback.timeSeconds).toBe(15);
expect(playback.timeLeftSeconds).toBe(0);
});
});
describe("and skipping to the middle of the second chunk", () => {
const middleOfSecondChunk = (chunk1Length + chunk2Length / 2) / 1000;
beforeEach(async () => {
await playback.skipTo(middleOfSecondChunk);
});
it("should play the second chunk", () => {
expect(chunk1Playback.stop).toHaveBeenCalled();
expect(chunk1Playback.destroy).toHaveBeenCalled();
expect(chunk2Playback.play).toHaveBeenCalled();
});
it("should update the time", () => {
expect(playback.timeSeconds).toBe(middleOfSecondChunk);
});
describe("and skipping to the start", () => {
beforeEach(async () => {
await playback.skipTo(0);
});
it("should play the first chunk", () => {
expect(chunk2Playback.stop).toHaveBeenCalled();
expect(chunk2Playback.destroy).toHaveBeenCalled();
expect(chunk1Playback.play).toHaveBeenCalled();
});
it("should update the time", () => {
expect(playback.timeSeconds).toBe(0);
});
});
});
describe("and skipping multiple times", () => {
beforeEach(async () => {
return Promise.all([
playback.skipTo(middleOfSecondChunk),
playback.skipTo(middleOfThirdChunk),
playback.skipTo(0),
]);
});
it("should only skip to the first and last position", () => {
expect(chunk1Playback.stop).toHaveBeenCalled();
expect(chunk1Playback.destroy).toHaveBeenCalled();
expect(chunk2Playback.play).toHaveBeenCalled();
expect(chunk3Playback.play).not.toHaveBeenCalled();
expect(chunk2Playback.stop).toHaveBeenCalled();
expect(chunk2Playback.destroy).toHaveBeenCalled();
expect(chunk1Playback.play).toHaveBeenCalled();
});
it("should update the time", () => {
expect(playback.timeSeconds).toBe(0);
});
});
});
describe("and skipping multiple times", () => {
beforeEach(async () => {
return Promise.all([
playback.skipTo(middleOfSecondChunk),
playback.skipTo(middleOfThirdChunk),
playback.skipTo(0),
]);
});
it("should only skip to the first and last position", () => {
expect(chunk1Playback.stop).toHaveBeenCalled();
expect(chunk1Playback.destroy).toHaveBeenCalled();
expect(chunk2Playback.play).toHaveBeenCalled();
expect(chunk3Playback.play).not.toHaveBeenCalled();
expect(chunk2Playback.stop).toHaveBeenCalled();
expect(chunk2Playback.destroy).toHaveBeenCalled();
expect(chunk1Playback.play).toHaveBeenCalled();
});
});
describe("and the first chunk ends", () => {
beforeEach(() => {
chunk1Playback.emit(PlaybackState.Stopped);
});
it("should play until the end", () => {
// assert first chunk was unloaded
expect(chunk1Playback.destroy).toHaveBeenCalled();
// assert that the second chunk is being played
expect(chunk2Playback.play).toHaveBeenCalled();
// simulate end of second and third chunk
chunk2Playback.emit(PlaybackState.Stopped);
chunk3Playback.emit(PlaybackState.Stopped);
// assert that the entire playback is now in stopped state
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped);
});
});
describe("and calling pause", () => {
pausePlayback();
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Paused);
});
describe("and calling stop", () => {
stopPlayback();
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
it("should stop the playback", () => {
expect(chunk1Playback.stop).toHaveBeenCalled();
});
describe("and skipping to somewhere in the middle of the first chunk", () => {
beforeEach(async () => {
mocked(chunk1Playback.play).mockClear();
await playback.skipTo(1);
describe("and the first chunk ends", () => {
beforeEach(() => {
chunk1Playback.emit(PlaybackState.Stopped);
});
it("should not start the playback", () => {
expect(chunk1Playback.play).not.toHaveBeenCalled();
it("should play until the end", () => {
// assert first chunk was unloaded
expect(chunk1Playback.destroy).toHaveBeenCalled();
// assert that the second chunk is being played
expect(chunk2Playback.play).toHaveBeenCalled();
// simulate end of second and third chunk
chunk2Playback.emit(PlaybackState.Stopped);
chunk3Playback.emit(PlaybackState.Stopped);
// assert that the entire playback is now in stopped state
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped);
});
});
});
describe("and calling destroy", () => {
beforeEach(() => {
playback.destroy();
describe("and calling pause", () => {
pausePlayback();
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Paused);
});
it("should call removeAllListeners", () => {
expect(playback.removeAllListeners).toHaveBeenCalled();
describe("and calling stop", () => {
stopPlayback();
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
it("should stop the playback", () => {
expect(chunk1Playback.stop).toHaveBeenCalled();
});
describe("and skipping to somewhere in the middle of the first chunk", () => {
beforeEach(async () => {
mocked(chunk1Playback.play).mockClear();
await playback.skipTo(1);
});
it("should not start the playback", () => {
expect(chunk1Playback.play).not.toHaveBeenCalled();
});
});
});
it("should call destroy on the playbacks", () => {
expect(chunk1Playback.destroy).toHaveBeenCalled();
expect(chunk2Playback.destroy).toHaveBeenCalled();
describe("and calling destroy", () => {
beforeEach(() => {
playback.destroy();
});
it("should call removeAllListeners", () => {
expect(playback.removeAllListeners).toHaveBeenCalled();
});
it("should call destroy on the playbacks", () => {
expect(chunk1Playback.destroy).toHaveBeenCalled();
expect(chunk2Playback.destroy).toHaveBeenCalled();
});
});
});
});
describe("and calling toggle for the first time", () => {
beforeEach(async () => {
await playback.toggle();
playback.toggle();
await simulateFirstChunkArrived();
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
@ -693,7 +739,8 @@ describe("VoiceBroadcastPlayback", () => {
describe("and calling toggle", () => {
beforeEach(async () => {
mocked(onStateChanged).mockReset();
await playback.toggle();
playback.toggle();
await simulateFirstChunkArrived();
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);

View file

@ -4226,10 +4226,10 @@ eslint-plugin-jsx-a11y@^6.5.1:
minimatch "^3.1.2"
semver "^6.3.0"
eslint-plugin-matrix-org@0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-0.9.0.tgz#b2a5186052ddbfa7dc9878779bafa5d68681c7b4"
integrity sha512-+j6JuMnFH421Z2vOxc+0YMt5Su5vD76RSatviy3zHBaZpgd+sOeAWoCLBHD5E7mMz5oKae3Y3wewCt9LRzq2Nw==
eslint-plugin-matrix-org@0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-0.10.0.tgz#8d0998641a4d276343cae2abf253a01bb4d4cc60"
integrity sha512-L7ail0x1yUlF006kn4mHc+OT8/aYZI++i852YXPHxCbM1EY7jeg/fYAQ8tCx5+x08LyqXeS7inAVSL784m0C6Q==
eslint-plugin-react-hooks@^4.3.0:
version "4.6.0"