Use server side relations for voice broadcasts (#9534)
This commit is contained in:
parent
3747464b41
commit
36a574a14f
11 changed files with 396 additions and 192 deletions
|
@ -38,6 +38,8 @@ export class RelationsHelper
|
||||||
extends TypedEventEmitter<RelationsHelperEvent, EventMap>
|
extends TypedEventEmitter<RelationsHelperEvent, EventMap>
|
||||||
implements IDestroyable {
|
implements IDestroyable {
|
||||||
private relations?: Relations;
|
private relations?: Relations;
|
||||||
|
private eventId: string;
|
||||||
|
private roomId: string;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private event: MatrixEvent,
|
private event: MatrixEvent,
|
||||||
|
@ -46,6 +48,21 @@ export class RelationsHelper
|
||||||
private client: MatrixClient,
|
private client: MatrixClient,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
const eventId = event.getId();
|
||||||
|
|
||||||
|
if (!eventId) {
|
||||||
|
throw new Error("unable to create RelationsHelper: missing event ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomId = event.getRoomId();
|
||||||
|
|
||||||
|
if (!roomId) {
|
||||||
|
throw new Error("unable to create RelationsHelper: missing room ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventId = eventId;
|
||||||
|
this.roomId = roomId;
|
||||||
this.setUpRelations();
|
this.setUpRelations();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +90,7 @@ export class RelationsHelper
|
||||||
private setRelations(): void {
|
private setRelations(): void {
|
||||||
const room = this.client.getRoom(this.event.getRoomId());
|
const room = this.client.getRoom(this.event.getRoomId());
|
||||||
this.relations = room?.getUnfilteredTimelineSet()?.relations?.getChildEventsForEvent(
|
this.relations = room?.getUnfilteredTimelineSet()?.relations?.getChildEventsForEvent(
|
||||||
this.event.getId(),
|
this.eventId,
|
||||||
this.relationType,
|
this.relationType,
|
||||||
this.relationEventType,
|
this.relationEventType,
|
||||||
);
|
);
|
||||||
|
@ -87,6 +104,32 @@ export class RelationsHelper
|
||||||
this.relations?.getRelations()?.forEach(e => this.emit(RelationsHelperEvent.Add, e));
|
this.relations?.getRelations()?.forEach(e => this.emit(RelationsHelperEvent.Add, e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getCurrent(): MatrixEvent[] {
|
||||||
|
return this.relations?.getRelations() || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all related events from the server and emits them.
|
||||||
|
*/
|
||||||
|
public async emitFetchCurrent(): Promise<void> {
|
||||||
|
let nextBatch: string | undefined = undefined;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const response = await this.client.relations(
|
||||||
|
this.roomId,
|
||||||
|
this.eventId,
|
||||||
|
this.relationType,
|
||||||
|
this.relationEventType,
|
||||||
|
{
|
||||||
|
from: nextBatch,
|
||||||
|
limit: 50,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
nextBatch = response?.nextBatch;
|
||||||
|
response?.events.forEach(e => this.emit(RelationsHelperEvent.Add, e));
|
||||||
|
} while (nextBatch);
|
||||||
|
}
|
||||||
|
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
this.removeAllListeners();
|
this.removeAllListeners();
|
||||||
this.event.off(MatrixEventEvent.RelationsCreated, this.onRelationsCreated);
|
this.event.off(MatrixEventEvent.RelationsCreated, this.onRelationsCreated);
|
||||||
|
|
|
@ -32,7 +32,6 @@ import { MediaEventHelper } from "../../utils/MediaEventHelper";
|
||||||
import { IDestroyable } from "../../utils/IDestroyable";
|
import { IDestroyable } from "../../utils/IDestroyable";
|
||||||
import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "..";
|
import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "..";
|
||||||
import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper";
|
import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper";
|
||||||
import { getReferenceRelationsForEvent } from "../../events";
|
|
||||||
import { VoiceBroadcastChunkEvents } from "../utils/VoiceBroadcastChunkEvents";
|
import { VoiceBroadcastChunkEvents } from "../utils/VoiceBroadcastChunkEvents";
|
||||||
|
|
||||||
export enum VoiceBroadcastPlaybackState {
|
export enum VoiceBroadcastPlaybackState {
|
||||||
|
@ -89,15 +88,27 @@ export class VoiceBroadcastPlayback
|
||||||
this.setUpRelationsHelper();
|
this.setUpRelationsHelper();
|
||||||
}
|
}
|
||||||
|
|
||||||
private setUpRelationsHelper(): void {
|
private async setUpRelationsHelper(): Promise<void> {
|
||||||
this.infoRelationHelper = new RelationsHelper(
|
this.infoRelationHelper = new RelationsHelper(
|
||||||
this.infoEvent,
|
this.infoEvent,
|
||||||
RelationType.Reference,
|
RelationType.Reference,
|
||||||
VoiceBroadcastInfoEventType,
|
VoiceBroadcastInfoEventType,
|
||||||
this.client,
|
this.client,
|
||||||
);
|
);
|
||||||
this.infoRelationHelper.on(RelationsHelperEvent.Add, this.addInfoEvent);
|
this.infoRelationHelper.getCurrent().forEach(this.addInfoEvent);
|
||||||
this.infoRelationHelper.emitCurrent();
|
|
||||||
|
if (this.infoState !== VoiceBroadcastInfoState.Stopped) {
|
||||||
|
// Only required if not stopped. Stopped is the final state.
|
||||||
|
this.infoRelationHelper.on(RelationsHelperEvent.Add, this.addInfoEvent);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.infoRelationHelper.emitFetchCurrent();
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn("error fetching server side relation for voice broadcast info", err);
|
||||||
|
// fall back to local events
|
||||||
|
this.infoRelationHelper.emitCurrent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.chunkRelationHelper = new RelationsHelper(
|
this.chunkRelationHelper = new RelationsHelper(
|
||||||
this.infoEvent,
|
this.infoEvent,
|
||||||
|
@ -106,7 +117,15 @@ export class VoiceBroadcastPlayback
|
||||||
this.client,
|
this.client,
|
||||||
);
|
);
|
||||||
this.chunkRelationHelper.on(RelationsHelperEvent.Add, this.addChunkEvent);
|
this.chunkRelationHelper.on(RelationsHelperEvent.Add, this.addChunkEvent);
|
||||||
this.chunkRelationHelper.emitCurrent();
|
|
||||||
|
try {
|
||||||
|
// TODO Michael W: only fetch events if needed, blocked by PSF-1708
|
||||||
|
await this.chunkRelationHelper.emitFetchCurrent();
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn("error fetching server side relation for voice broadcast chunks", err);
|
||||||
|
// fall back to local events
|
||||||
|
this.chunkRelationHelper.emitCurrent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private addChunkEvent = async (event: MatrixEvent): Promise<boolean> => {
|
private addChunkEvent = async (event: MatrixEvent): Promise<boolean> => {
|
||||||
|
@ -150,23 +169,18 @@ export class VoiceBroadcastPlayback
|
||||||
this.setInfoState(state);
|
this.setInfoState(state);
|
||||||
};
|
};
|
||||||
|
|
||||||
private async loadChunks(): Promise<void> {
|
private async enqueueChunks(): Promise<void> {
|
||||||
const relations = getReferenceRelationsForEvent(this.infoEvent, EventType.RoomMessage, this.client);
|
const promises = this.chunkEvents.getEvents().reduce((promises, event: MatrixEvent) => {
|
||||||
const chunkEvents = relations?.getRelations();
|
if (!this.playbacks.has(event.getId() || "")) {
|
||||||
|
promises.push(this.enqueueChunk(event));
|
||||||
|
}
|
||||||
|
return promises;
|
||||||
|
}, [] as Promise<void>[]);
|
||||||
|
|
||||||
if (!chunkEvents) {
|
await Promise.all(promises);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.chunkEvents.addEvents(chunkEvents);
|
|
||||||
this.setDuration(this.chunkEvents.getLength());
|
|
||||||
|
|
||||||
for (const chunkEvent of chunkEvents) {
|
|
||||||
await this.enqueueChunk(chunkEvent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async enqueueChunk(chunkEvent: MatrixEvent) {
|
private async enqueueChunk(chunkEvent: MatrixEvent): Promise<void> {
|
||||||
const eventId = chunkEvent.getId();
|
const eventId = chunkEvent.getId();
|
||||||
|
|
||||||
if (!eventId) {
|
if (!eventId) {
|
||||||
|
@ -317,10 +331,7 @@ export class VoiceBroadcastPlayback
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start(): Promise<void> {
|
public async start(): Promise<void> {
|
||||||
if (this.playbacks.size === 0) {
|
await this.enqueueChunks();
|
||||||
await this.loadChunks();
|
|
||||||
}
|
|
||||||
|
|
||||||
const chunkEvents = this.chunkEvents.getEvents();
|
const chunkEvents = this.chunkEvents.getEvents();
|
||||||
|
|
||||||
const toPlay = this.getInfoState() === VoiceBroadcastInfoState.Stopped
|
const toPlay = this.getInfoState() === VoiceBroadcastInfoState.Stopped
|
||||||
|
|
19
test/@types/common.ts
Normal file
19
test/@types/common.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type PublicInterface<T> = {
|
||||||
|
[P in keyof T]: T[P];
|
||||||
|
};
|
|
@ -28,13 +28,15 @@ import { Relations } from "matrix-js-sdk/src/models/relations";
|
||||||
import { RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
|
import { RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
|
||||||
|
|
||||||
import { RelationsHelper, RelationsHelperEvent } from "../../src/events/RelationsHelper";
|
import { RelationsHelper, RelationsHelperEvent } from "../../src/events/RelationsHelper";
|
||||||
import { mkEvent, mkStubRoom, stubClient } from "../test-utils";
|
import { mkEvent, mkRelationsContainer, mkStubRoom, stubClient } from "../test-utils";
|
||||||
|
|
||||||
describe("RelationsHelper", () => {
|
describe("RelationsHelper", () => {
|
||||||
const roomId = "!room:example.com";
|
const roomId = "!room:example.com";
|
||||||
|
let userId: string;
|
||||||
let event: MatrixEvent;
|
let event: MatrixEvent;
|
||||||
let relatedEvent1: MatrixEvent;
|
let relatedEvent1: MatrixEvent;
|
||||||
let relatedEvent2: MatrixEvent;
|
let relatedEvent2: MatrixEvent;
|
||||||
|
let relatedEvent3: MatrixEvent;
|
||||||
let room: Room;
|
let room: Room;
|
||||||
let client: MatrixClient;
|
let client: MatrixClient;
|
||||||
let relationsHelper: RelationsHelper;
|
let relationsHelper: RelationsHelper;
|
||||||
|
@ -46,47 +48,81 @@ describe("RelationsHelper", () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
client = stubClient();
|
client = stubClient();
|
||||||
|
userId = client.getUserId() || "";
|
||||||
|
mocked(client.relations).mockClear();
|
||||||
room = mkStubRoom(roomId, "test room", client);
|
room = mkStubRoom(roomId, "test room", client);
|
||||||
mocked(client.getRoom).mockImplementation((getRoomId: string) => {
|
mocked(client.getRoom).mockImplementation((getRoomId?: string) => {
|
||||||
if (getRoomId === roomId) {
|
if (getRoomId === roomId) {
|
||||||
return room;
|
return room;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
event = mkEvent({
|
event = mkEvent({
|
||||||
event: true,
|
event: true,
|
||||||
type: EventType.RoomMessage,
|
type: EventType.RoomMessage,
|
||||||
room: roomId,
|
room: roomId,
|
||||||
user: client.getUserId(),
|
user: userId,
|
||||||
content: {},
|
content: {},
|
||||||
});
|
});
|
||||||
relatedEvent1 = mkEvent({
|
relatedEvent1 = mkEvent({
|
||||||
event: true,
|
event: true,
|
||||||
type: EventType.RoomMessage,
|
type: EventType.RoomMessage,
|
||||||
room: roomId,
|
room: roomId,
|
||||||
user: client.getUserId(),
|
user: userId,
|
||||||
content: {},
|
content: { relatedEvent: 1 },
|
||||||
});
|
});
|
||||||
relatedEvent2 = mkEvent({
|
relatedEvent2 = mkEvent({
|
||||||
event: true,
|
event: true,
|
||||||
type: EventType.RoomMessage,
|
type: EventType.RoomMessage,
|
||||||
room: roomId,
|
room: roomId,
|
||||||
user: client.getUserId(),
|
user: userId,
|
||||||
content: {},
|
content: { relatedEvent: 2 },
|
||||||
|
});
|
||||||
|
relatedEvent3 = mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
room: roomId,
|
||||||
|
user: userId,
|
||||||
|
content: { relatedEvent: 3 },
|
||||||
});
|
});
|
||||||
onAdd = jest.fn();
|
onAdd = jest.fn();
|
||||||
|
relationsContainer = mkRelationsContainer();
|
||||||
// TODO Michael W: create test utils, remove casts
|
// TODO Michael W: create test utils, remove casts
|
||||||
relationsContainer = {
|
|
||||||
getChildEventsForEvent: jest.fn(),
|
|
||||||
} as unknown as RelationsContainer;
|
|
||||||
relations = {
|
relations = {
|
||||||
getRelations: jest.fn(),
|
getRelations: jest.fn(),
|
||||||
on: jest.fn().mockImplementation((type, l) => relationsOnAdd = l),
|
on: jest.fn().mockImplementation((type, l) => relationsOnAdd = l),
|
||||||
|
off: jest.fn(),
|
||||||
} as unknown as Relations;
|
} as unknown as Relations;
|
||||||
timelineSet = {
|
timelineSet = {
|
||||||
relations: relationsContainer,
|
relations: relationsContainer,
|
||||||
} as unknown as EventTimelineSet;
|
} as unknown as EventTimelineSet;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
relationsHelper?.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when there is an event without ID", () => {
|
||||||
|
it("should raise an error", () => {
|
||||||
|
jest.spyOn(event, "getId").mockReturnValue(undefined);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
new RelationsHelper(event, RelationType.Reference, EventType.RoomMessage, client);
|
||||||
|
}).toThrowError("unable to create RelationsHelper: missing event ID");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when there is an event without room ID", () => {
|
||||||
|
it("should raise an error", () => {
|
||||||
|
jest.spyOn(event, "getRoomId").mockReturnValue(undefined);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
new RelationsHelper(event, RelationType.Reference, EventType.RoomMessage, client);
|
||||||
|
}).toThrowError("unable to create RelationsHelper: missing room ID");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("when there is an event without relations", () => {
|
describe("when there is an event without relations", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
relationsHelper = new RelationsHelper(event, RelationType.Reference, EventType.RoomMessage, client);
|
relationsHelper = new RelationsHelper(event, RelationType.Reference, EventType.RoomMessage, client);
|
||||||
|
@ -118,6 +154,34 @@ describe("RelationsHelper", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("when there is an event with two pages server side relations", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocked(client.relations)
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
events: [relatedEvent1, relatedEvent2],
|
||||||
|
nextBatch: "next",
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
events: [relatedEvent3],
|
||||||
|
nextBatch: null,
|
||||||
|
});
|
||||||
|
relationsHelper = new RelationsHelper(event, RelationType.Reference, EventType.RoomMessage, client);
|
||||||
|
relationsHelper.on(RelationsHelperEvent.Add, onAdd);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("emitFetchCurrent", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await relationsHelper.emitFetchCurrent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should emit the server side events", () => {
|
||||||
|
expect(onAdd).toHaveBeenCalledWith(relatedEvent1);
|
||||||
|
expect(onAdd).toHaveBeenCalledWith(relatedEvent2);
|
||||||
|
expect(onAdd).toHaveBeenCalledWith(relatedEvent3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("when there is an event with relations", () => {
|
describe("when there is an event with relations", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mocked(room.getUnfilteredTimelineSet).mockReturnValue(timelineSet);
|
mocked(room.getUnfilteredTimelineSet).mockReturnValue(timelineSet);
|
||||||
|
|
|
@ -20,10 +20,7 @@ import { SimpleObservable } from "matrix-widget-api";
|
||||||
import { Playback, PlaybackState } from "../../src/audio/Playback";
|
import { Playback, PlaybackState } from "../../src/audio/Playback";
|
||||||
import { PlaybackClock } from "../../src/audio/PlaybackClock";
|
import { PlaybackClock } from "../../src/audio/PlaybackClock";
|
||||||
import { UPDATE_EVENT } from "../../src/stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../src/stores/AsyncStore";
|
||||||
|
import { PublicInterface } from "../@types/common";
|
||||||
type PublicInterface<T> = {
|
|
||||||
[P in keyof T]: T[P];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createTestPlayback = (): Playback => {
|
export const createTestPlayback = (): Playback => {
|
||||||
const eventEmitter = new EventEmitter();
|
const eventEmitter = new EventEmitter();
|
||||||
|
|
|
@ -25,3 +25,4 @@ export * from './call';
|
||||||
export * from './wrappers';
|
export * from './wrappers';
|
||||||
export * from './utilities';
|
export * from './utilities';
|
||||||
export * from './date';
|
export * from './date';
|
||||||
|
export * from './relations';
|
||||||
|
|
35
test/test-utils/relations.ts
Normal file
35
test/test-utils/relations.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Relations } from "matrix-js-sdk/src/models/relations";
|
||||||
|
import { RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
|
||||||
|
|
||||||
|
import { PublicInterface } from "../@types/common";
|
||||||
|
|
||||||
|
export const mkRelations = (): Relations => {
|
||||||
|
return {
|
||||||
|
|
||||||
|
} as PublicInterface<Relations> as Relations;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mkRelationsContainer = (): RelationsContainer => {
|
||||||
|
return {
|
||||||
|
aggregateChildEvent: jest.fn(),
|
||||||
|
aggregateParentEvent: jest.fn(),
|
||||||
|
getAllChildEventsForEvent: jest.fn(),
|
||||||
|
getChildEventsForEvent: jest.fn(),
|
||||||
|
} as PublicInterface<RelationsContainer> as RelationsContainer;
|
||||||
|
};
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { ReactElement } from "react";
|
||||||
import { act, render, screen } from "@testing-library/react";
|
import { act, render, screen } from "@testing-library/react";
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
@ -31,6 +31,8 @@ import {
|
||||||
} from "../../../src/voice-broadcast";
|
} from "../../../src/voice-broadcast";
|
||||||
import { stubClient } from "../../test-utils";
|
import { stubClient } from "../../test-utils";
|
||||||
import { mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
|
import { mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
|
||||||
|
import { MediaEventHelper } from "../../../src/utils/MediaEventHelper";
|
||||||
|
import { RoomPermalinkCreator } from "../../../src/utils/permalinks/Permalinks";
|
||||||
|
|
||||||
jest.mock("../../../src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody", () => ({
|
jest.mock("../../../src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody", () => ({
|
||||||
VoiceBroadcastRecordingBody: jest.fn(),
|
VoiceBroadcastRecordingBody: jest.fn(),
|
||||||
|
@ -40,8 +42,13 @@ jest.mock("../../../src/voice-broadcast/components/molecules/VoiceBroadcastPlayb
|
||||||
VoiceBroadcastPlaybackBody: jest.fn(),
|
VoiceBroadcastPlaybackBody: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock("../../../src/utils/permalinks/Permalinks");
|
||||||
|
jest.mock("../../../src/utils/MediaEventHelper");
|
||||||
|
|
||||||
describe("VoiceBroadcastBody", () => {
|
describe("VoiceBroadcastBody", () => {
|
||||||
const roomId = "!room:example.com";
|
const roomId = "!room:example.com";
|
||||||
|
let userId: string;
|
||||||
|
let deviceId: string;
|
||||||
let client: MatrixClient;
|
let client: MatrixClient;
|
||||||
let room: Room;
|
let room: Room;
|
||||||
let infoEvent: MatrixEvent;
|
let infoEvent: MatrixEvent;
|
||||||
|
@ -52,62 +59,75 @@ describe("VoiceBroadcastBody", () => {
|
||||||
const renderVoiceBroadcast = () => {
|
const renderVoiceBroadcast = () => {
|
||||||
render(<VoiceBroadcastBody
|
render(<VoiceBroadcastBody
|
||||||
mxEvent={infoEvent}
|
mxEvent={infoEvent}
|
||||||
mediaEventHelper={null}
|
mediaEventHelper={new MediaEventHelper(infoEvent)}
|
||||||
onHeightChanged={() => {}}
|
onHeightChanged={() => {}}
|
||||||
onMessageAllowed={() => {}}
|
onMessageAllowed={() => {}}
|
||||||
permalinkCreator={null}
|
permalinkCreator={new RoomPermalinkCreator(room)}
|
||||||
/>);
|
/>);
|
||||||
testRecording = VoiceBroadcastRecordingsStore.instance().getByInfoEvent(infoEvent, client);
|
testRecording = VoiceBroadcastRecordingsStore.instance().getByInfoEvent(infoEvent, client);
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
client = stubClient();
|
client = stubClient();
|
||||||
room = new Room(roomId, client, client.getUserId());
|
userId = client.getUserId() || "";
|
||||||
mocked(client.getRoom).mockImplementation((getRoomId: string) => {
|
deviceId = client.getDeviceId() || "";
|
||||||
|
mocked(client.relations).mockClear();
|
||||||
|
mocked(client.relations).mockResolvedValue({ events: [] });
|
||||||
|
room = new Room(roomId, client, userId);
|
||||||
|
mocked(client.getRoom).mockImplementation((getRoomId?: string) => {
|
||||||
if (getRoomId === roomId) return room;
|
if (getRoomId === roomId) return room;
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
infoEvent = mkVoiceBroadcastInfoStateEvent(
|
infoEvent = mkVoiceBroadcastInfoStateEvent(
|
||||||
roomId,
|
roomId,
|
||||||
VoiceBroadcastInfoState.Started,
|
VoiceBroadcastInfoState.Started,
|
||||||
client.getUserId(),
|
userId,
|
||||||
client.getDeviceId(),
|
deviceId,
|
||||||
);
|
);
|
||||||
stoppedEvent = mkVoiceBroadcastInfoStateEvent(
|
stoppedEvent = mkVoiceBroadcastInfoStateEvent(
|
||||||
roomId,
|
roomId,
|
||||||
VoiceBroadcastInfoState.Stopped,
|
VoiceBroadcastInfoState.Stopped,
|
||||||
client.getUserId(),
|
userId,
|
||||||
client.getDeviceId(),
|
deviceId,
|
||||||
infoEvent,
|
infoEvent,
|
||||||
);
|
);
|
||||||
room.addEventsToTimeline([infoEvent], true, room.getLiveTimeline());
|
room.addEventsToTimeline([infoEvent], true, room.getLiveTimeline());
|
||||||
testRecording = new VoiceBroadcastRecording(infoEvent, client);
|
testRecording = new VoiceBroadcastRecording(infoEvent, client);
|
||||||
testPlayback = new VoiceBroadcastPlayback(infoEvent, client);
|
testPlayback = new VoiceBroadcastPlayback(infoEvent, client);
|
||||||
mocked(VoiceBroadcastRecordingBody).mockImplementation(({ recording }) => {
|
mocked(VoiceBroadcastRecordingBody).mockImplementation(({ recording }): ReactElement | null => {
|
||||||
if (testRecording === recording) {
|
if (testRecording === recording) {
|
||||||
return <div data-testid="voice-broadcast-recording-body" />;
|
return <div data-testid="voice-broadcast-recording-body" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
mocked(VoiceBroadcastPlaybackBody).mockImplementation(({ playback }) => {
|
mocked(VoiceBroadcastPlaybackBody).mockImplementation(({ playback }): ReactElement | null => {
|
||||||
if (testPlayback === playback) {
|
if (testPlayback === playback) {
|
||||||
return <div data-testid="voice-broadcast-playback-body" />;
|
return <div data-testid="voice-broadcast-playback-body" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.spyOn(VoiceBroadcastRecordingsStore.instance(), "getByInfoEvent").mockImplementation(
|
jest.spyOn(VoiceBroadcastRecordingsStore.instance(), "getByInfoEvent").mockImplementation(
|
||||||
(getEvent: MatrixEvent, getClient: MatrixClient) => {
|
(getEvent: MatrixEvent, getClient: MatrixClient): VoiceBroadcastRecording => {
|
||||||
if (getEvent === infoEvent && getClient === client) {
|
if (getEvent === infoEvent && getClient === client) {
|
||||||
return testRecording;
|
return testRecording;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Error("unexpected event");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
jest.spyOn(VoiceBroadcastPlaybacksStore.instance(), "getByInfoEvent").mockImplementation(
|
jest.spyOn(VoiceBroadcastPlaybacksStore.instance(), "getByInfoEvent").mockImplementation(
|
||||||
(getEvent: MatrixEvent) => {
|
(getEvent: MatrixEvent): VoiceBroadcastPlayback => {
|
||||||
if (getEvent === infoEvent) {
|
if (getEvent === infoEvent) {
|
||||||
return testPlayback;
|
return testPlayback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Error("unexpected event");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -48,6 +48,9 @@ describe("VoiceBroadcastPlaybackBody", () => {
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
client = stubClient();
|
client = stubClient();
|
||||||
|
mocked(client.relations).mockClear();
|
||||||
|
mocked(client.relations).mockResolvedValue({ events: [] });
|
||||||
|
|
||||||
infoEvent = mkVoiceBroadcastInfoStateEvent(
|
infoEvent = mkVoiceBroadcastInfoStateEvent(
|
||||||
roomId,
|
roomId,
|
||||||
VoiceBroadcastInfoState.Started,
|
VoiceBroadcastInfoState.Started,
|
||||||
|
|
|
@ -15,24 +15,21 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
import { EventType, MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
import { Relations } from "matrix-js-sdk/src/models/relations";
|
|
||||||
|
|
||||||
import { Playback, PlaybackState } from "../../../src/audio/Playback";
|
import { Playback, PlaybackState } from "../../../src/audio/Playback";
|
||||||
import { PlaybackManager } from "../../../src/audio/PlaybackManager";
|
import { PlaybackManager } from "../../../src/audio/PlaybackManager";
|
||||||
import { getReferenceRelationsForEvent } from "../../../src/events";
|
|
||||||
import { RelationsHelperEvent } from "../../../src/events/RelationsHelper";
|
import { RelationsHelperEvent } from "../../../src/events/RelationsHelper";
|
||||||
import { MediaEventHelper } from "../../../src/utils/MediaEventHelper";
|
import { MediaEventHelper } from "../../../src/utils/MediaEventHelper";
|
||||||
import {
|
import {
|
||||||
VoiceBroadcastInfoEventType,
|
|
||||||
VoiceBroadcastInfoState,
|
VoiceBroadcastInfoState,
|
||||||
VoiceBroadcastPlayback,
|
VoiceBroadcastPlayback,
|
||||||
VoiceBroadcastPlaybackEvent,
|
VoiceBroadcastPlaybackEvent,
|
||||||
VoiceBroadcastPlaybackState,
|
VoiceBroadcastPlaybackState,
|
||||||
} from "../../../src/voice-broadcast";
|
} from "../../../src/voice-broadcast";
|
||||||
import { mkEvent, stubClient } from "../../test-utils";
|
import { flushPromises, stubClient } from "../../test-utils";
|
||||||
import { createTestPlayback } from "../../test-utils/audio";
|
import { createTestPlayback } from "../../test-utils/audio";
|
||||||
import { mkVoiceBroadcastChunkEvent } from "../utils/test-utils";
|
import { mkVoiceBroadcastChunkEvent, mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
|
||||||
|
|
||||||
jest.mock("../../../src/events/getReferenceRelationsForEvent", () => ({
|
jest.mock("../../../src/events/getReferenceRelationsForEvent", () => ({
|
||||||
getReferenceRelationsForEvent: jest.fn(),
|
getReferenceRelationsForEvent: jest.fn(),
|
||||||
|
@ -44,6 +41,7 @@ jest.mock("../../../src/utils/MediaEventHelper", () => ({
|
||||||
|
|
||||||
describe("VoiceBroadcastPlayback", () => {
|
describe("VoiceBroadcastPlayback", () => {
|
||||||
const userId = "@user:example.com";
|
const userId = "@user:example.com";
|
||||||
|
let deviceId: string;
|
||||||
const roomId = "!room:example.com";
|
const roomId = "!room:example.com";
|
||||||
let client: MatrixClient;
|
let client: MatrixClient;
|
||||||
let infoEvent: MatrixEvent;
|
let infoEvent: MatrixEvent;
|
||||||
|
@ -98,7 +96,7 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
const mkChunkHelper = (data: ArrayBuffer): MediaEventHelper => {
|
const mkChunkHelper = (data: ArrayBuffer): MediaEventHelper => {
|
||||||
return {
|
return {
|
||||||
sourceBlob: {
|
sourceBlob: {
|
||||||
cachedValue: null,
|
cachedValue: new Blob(),
|
||||||
done: false,
|
done: false,
|
||||||
value: {
|
value: {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -109,32 +107,31 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const mkInfoEvent = (state: VoiceBroadcastInfoState) => {
|
const mkInfoEvent = (state: VoiceBroadcastInfoState) => {
|
||||||
return mkEvent({
|
return mkVoiceBroadcastInfoStateEvent(
|
||||||
event: true,
|
roomId,
|
||||||
type: VoiceBroadcastInfoEventType,
|
state,
|
||||||
user: userId,
|
userId,
|
||||||
room: roomId,
|
deviceId,
|
||||||
content: {
|
);
|
||||||
state,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mkPlayback = () => {
|
const mkPlayback = async () => {
|
||||||
const playback = new VoiceBroadcastPlayback(infoEvent, client);
|
const playback = new VoiceBroadcastPlayback(infoEvent, client);
|
||||||
jest.spyOn(playback, "removeAllListeners");
|
jest.spyOn(playback, "removeAllListeners");
|
||||||
playback.on(VoiceBroadcastPlaybackEvent.StateChanged, onStateChanged);
|
playback.on(VoiceBroadcastPlaybackEvent.StateChanged, onStateChanged);
|
||||||
|
await flushPromises();
|
||||||
return playback;
|
return playback;
|
||||||
};
|
};
|
||||||
|
|
||||||
const setUpChunkEvents = (chunkEvents: MatrixEvent[]) => {
|
const setUpChunkEvents = (chunkEvents: MatrixEvent[]) => {
|
||||||
const relations = new Relations(RelationType.Reference, EventType.RoomMessage, client);
|
mocked(client.relations).mockResolvedValueOnce({
|
||||||
jest.spyOn(relations, "getRelations").mockReturnValue(chunkEvents);
|
events: chunkEvents,
|
||||||
mocked(getReferenceRelationsForEvent).mockReturnValue(relations);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
client = stubClient();
|
client = stubClient();
|
||||||
|
deviceId = client.getDeviceId() || "";
|
||||||
|
|
||||||
chunk1Event = mkVoiceBroadcastChunkEvent(userId, roomId, chunk1Length, 1);
|
chunk1Event = mkVoiceBroadcastChunkEvent(userId, roomId, chunk1Length, 1);
|
||||||
chunk2Event = mkVoiceBroadcastChunkEvent(userId, roomId, chunk2Length, 2);
|
chunk2Event = mkVoiceBroadcastChunkEvent(userId, roomId, chunk2Length, 2);
|
||||||
|
@ -153,6 +150,8 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
if (buffer === chunk1Data) return chunk1Playback;
|
if (buffer === chunk1Data) return chunk1Playback;
|
||||||
if (buffer === chunk2Data) return chunk2Playback;
|
if (buffer === chunk2Data) return chunk2Playback;
|
||||||
if (buffer === chunk3Data) return chunk3Playback;
|
if (buffer === chunk3Data) return chunk3Playback;
|
||||||
|
|
||||||
|
throw new Error("unexpected buffer");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -168,11 +167,17 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
onStateChanged = jest.fn();
|
onStateChanged = jest.fn();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
playback.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
describe(`when there is a ${VoiceBroadcastInfoState.Resumed} broadcast without chunks yet`, () => {
|
describe(`when there is a ${VoiceBroadcastInfoState.Resumed} broadcast without chunks yet`, () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Resumed);
|
// info relation
|
||||||
playback = mkPlayback();
|
mocked(client.relations).mockResolvedValueOnce({ events: [] });
|
||||||
setUpChunkEvents([]);
|
setUpChunkEvents([]);
|
||||||
|
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Resumed);
|
||||||
|
playback = await mkPlayback();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("and calling start", () => {
|
describe("and calling start", () => {
|
||||||
|
@ -227,10 +232,12 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`when there is a ${VoiceBroadcastInfoState.Resumed} voice broadcast with some chunks`, () => {
|
describe(`when there is a ${VoiceBroadcastInfoState.Resumed} voice broadcast with some chunks`, () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Resumed);
|
// info relation
|
||||||
playback = mkPlayback();
|
mocked(client.relations).mockResolvedValueOnce({ events: [] });
|
||||||
setUpChunkEvents([chunk2Event, chunk1Event]);
|
setUpChunkEvents([chunk2Event, chunk1Event]);
|
||||||
|
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Resumed);
|
||||||
|
playback = await mkPlayback();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("and calling start", () => {
|
describe("and calling start", () => {
|
||||||
|
@ -267,158 +274,153 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when there is a stopped voice broadcast", () => {
|
describe("when there is a stopped voice broadcast", () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
|
setUpChunkEvents([chunk2Event, chunk1Event]);
|
||||||
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Stopped);
|
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Stopped);
|
||||||
playback = mkPlayback();
|
playback = await mkPlayback();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("and there are some chunks", () => {
|
it("should expose the info event", () => {
|
||||||
beforeEach(() => {
|
expect(playback.infoEvent).toBe(infoEvent);
|
||||||
setUpChunkEvents([chunk2Event, chunk1Event]);
|
});
|
||||||
|
|
||||||
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
|
||||||
|
|
||||||
|
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should expose the info event", () => {
|
describe("and the chunk playback progresses", () => {
|
||||||
expect(playback.infoEvent).toBe(infoEvent);
|
beforeEach(() => {
|
||||||
|
chunk1Playback.clockInfo.liveData.update([11]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update the time", () => {
|
||||||
|
expect(playback.timeSeconds).toBe(11);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
|
describe("and skipping to the middle of the second chunk", () => {
|
||||||
|
const middleOfSecondChunk = (chunk1Length + (chunk2Length / 2)) / 1000;
|
||||||
|
|
||||||
describe("and calling start", () => {
|
beforeEach(async () => {
|
||||||
startPlayback();
|
await playback.skipTo(middleOfSecondChunk);
|
||||||
|
|
||||||
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 the chunk playback progresses", () => {
|
it("should play the second chunk", () => {
|
||||||
beforeEach(() => {
|
expect(chunk1Playback.stop).toHaveBeenCalled();
|
||||||
chunk1Playback.clockInfo.liveData.update([11]);
|
expect(chunk2Playback.play).toHaveBeenCalled();
|
||||||
});
|
|
||||||
|
|
||||||
it("should update the time", () => {
|
|
||||||
expect(playback.timeSeconds).toBe(11);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("and skipping to the middle of the second chunk", () => {
|
it("should update the time", () => {
|
||||||
const middleOfSecondChunk = (chunk1Length + (chunk2Length / 2)) / 1000;
|
expect(playback.timeSeconds).toBe(middleOfSecondChunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("and skipping to the start", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await playback.skipTo(middleOfSecondChunk);
|
await playback.skipTo(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should play the second chunk", () => {
|
it("should play the second chunk", () => {
|
||||||
expect(chunk1Playback.stop).toHaveBeenCalled();
|
expect(chunk1Playback.play).toHaveBeenCalled();
|
||||||
expect(chunk2Playback.play).toHaveBeenCalled();
|
expect(chunk2Playback.stop).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update the time", () => {
|
it("should update the time", () => {
|
||||||
expect(playback.timeSeconds).toBe(middleOfSecondChunk);
|
expect(playback.timeSeconds).toBe(0);
|
||||||
});
|
|
||||||
|
|
||||||
describe("and skipping to the start", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await playback.skipTo(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should play the second chunk", () => {
|
|
||||||
expect(chunk1Playback.play).toHaveBeenCalled();
|
|
||||||
expect(chunk2Playback.stop).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update the time", () => {
|
|
||||||
expect(playback.timeSeconds).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("and the first chunk ends", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
chunk1Playback.emit(PlaybackState.Stopped);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should play until the end", () => {
|
|
||||||
// assert that the second chunk is being played
|
|
||||||
expect(chunk2Playback.play).toHaveBeenCalled();
|
|
||||||
|
|
||||||
// simulate end of second chunk
|
|
||||||
chunk2Playback.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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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", () => {
|
describe("and the first chunk ends", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(() => {
|
||||||
await playback.toggle();
|
chunk1Playback.emit(PlaybackState.Stopped);
|
||||||
});
|
});
|
||||||
|
|
||||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
|
it("should play until the end", () => {
|
||||||
|
// assert that the second chunk is being played
|
||||||
|
expect(chunk2Playback.play).toHaveBeenCalled();
|
||||||
|
|
||||||
describe("and calling toggle a second time", () => {
|
// simulate end of second chunk
|
||||||
beforeEach(async () => {
|
chunk2Playback.emit(PlaybackState.Stopped);
|
||||||
await playback.toggle();
|
|
||||||
});
|
|
||||||
|
|
||||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
|
// assert that the entire playback is now in stopped state
|
||||||
|
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped);
|
||||||
describe("and calling toggle a third time", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await playback.toggle();
|
|
||||||
});
|
|
||||||
|
|
||||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("and calling pause", () => {
|
||||||
|
pausePlayback();
|
||||||
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
|
||||||
|
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Paused);
|
||||||
|
});
|
||||||
|
|
||||||
describe("and calling stop", () => {
|
describe("and calling stop", () => {
|
||||||
stopPlayback();
|
stopPlayback();
|
||||||
|
|
||||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
|
||||||
|
});
|
||||||
|
|
||||||
describe("and calling toggle", () => {
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
|
||||||
|
|
||||||
|
describe("and calling toggle a second time", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await playback.toggle();
|
||||||
|
});
|
||||||
|
|
||||||
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
|
||||||
|
|
||||||
|
describe("and calling toggle a third time", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mocked(onStateChanged).mockReset();
|
|
||||||
await playback.toggle();
|
await playback.toggle();
|
||||||
});
|
});
|
||||||
|
|
||||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
|
||||||
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Playing);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("and calling stop", () => {
|
||||||
|
stopPlayback();
|
||||||
|
|
||||||
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
|
||||||
|
|
||||||
|
describe("and calling toggle", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
mocked(onStateChanged).mockReset();
|
||||||
|
await playback.toggle();
|
||||||
|
});
|
||||||
|
|
||||||
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
|
||||||
|
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Playing);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -35,6 +35,8 @@ import { mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
|
||||||
describe("VoiceBroadcastPlaybacksStore", () => {
|
describe("VoiceBroadcastPlaybacksStore", () => {
|
||||||
const roomId = "!room:example.com";
|
const roomId = "!room:example.com";
|
||||||
let client: MatrixClient;
|
let client: MatrixClient;
|
||||||
|
let userId: string;
|
||||||
|
let deviceId: string;
|
||||||
let room: Room;
|
let room: Room;
|
||||||
let infoEvent1: MatrixEvent;
|
let infoEvent1: MatrixEvent;
|
||||||
let infoEvent2: MatrixEvent;
|
let infoEvent2: MatrixEvent;
|
||||||
|
@ -45,24 +47,31 @@ describe("VoiceBroadcastPlaybacksStore", () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
client = stubClient();
|
client = stubClient();
|
||||||
|
userId = client.getUserId() || "";
|
||||||
|
deviceId = client.getDeviceId() || "";
|
||||||
|
mocked(client.relations).mockClear();
|
||||||
|
mocked(client.relations).mockResolvedValue({ events: [] });
|
||||||
|
|
||||||
room = mkStubRoom(roomId, "test room", client);
|
room = mkStubRoom(roomId, "test room", client);
|
||||||
mocked(client.getRoom).mockImplementation((roomId: string) => {
|
mocked(client.getRoom).mockImplementation((roomId: string): Room | null => {
|
||||||
if (roomId === room.roomId) {
|
if (roomId === room.roomId) {
|
||||||
return room;
|
return room;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
infoEvent1 = mkVoiceBroadcastInfoStateEvent(
|
infoEvent1 = mkVoiceBroadcastInfoStateEvent(
|
||||||
roomId,
|
roomId,
|
||||||
VoiceBroadcastInfoState.Started,
|
VoiceBroadcastInfoState.Started,
|
||||||
client.getUserId(),
|
userId,
|
||||||
client.getDeviceId(),
|
deviceId,
|
||||||
);
|
);
|
||||||
infoEvent2 = mkVoiceBroadcastInfoStateEvent(
|
infoEvent2 = mkVoiceBroadcastInfoStateEvent(
|
||||||
roomId,
|
roomId,
|
||||||
VoiceBroadcastInfoState.Started,
|
VoiceBroadcastInfoState.Started,
|
||||||
client.getUserId(),
|
userId,
|
||||||
client.getDeviceId(),
|
deviceId,
|
||||||
);
|
);
|
||||||
playback1 = new VoiceBroadcastPlayback(infoEvent1, client);
|
playback1 = new VoiceBroadcastPlayback(infoEvent1, client);
|
||||||
jest.spyOn(playback1, "off");
|
jest.spyOn(playback1, "off");
|
||||||
|
|
Loading…
Reference in a new issue