1
0
mirror of https://github.com/matrix-org/matrix-react-sdk.git synced 2025-08-09 08:42:50 +03:00

Merge remote-tracking branch 'origin/develop' into feat/emoji-picker-rich-text-mode

This commit is contained in:
Florian Duros
2022-12-05 17:40:46 +01:00
139 changed files with 2830 additions and 3202 deletions

View File

@@ -91,7 +91,7 @@ describe("ContentMessages", () => {
Object.defineProperty(global.Image.prototype, 'src', {
// Define the property setter
set(src) {
setTimeout(() => this.onload());
window.setTimeout(() => this.onload());
},
});
Object.defineProperty(global.Image.prototype, 'height', {

View File

@@ -15,10 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventEmitter } from "events";
import { mocked } from "jest-mock";
import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { Mocked, mocked } from "jest-mock";
import { MatrixEvent, Room, MatrixClient } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
import { CrossSigningInfo, DeviceTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
import DeviceListener from "../src/DeviceListener";
import { MatrixClientPeg } from "../src/MatrixClientPeg";
@@ -30,7 +33,8 @@ import dis from "../src/dispatcher/dispatcher";
import { Action } from "../src/dispatcher/actions";
import SettingsStore from "../src/settings/SettingsStore";
import { SettingLevel } from "../src/settings/SettingLevel";
import { mockPlatformPeg } from "./test-utils";
import { getMockClientWithEventEmitter, mockPlatformPeg } from "./test-utils";
import { UIFeature } from "../src/settings/UIFeature";
// don't litter test console with logs
jest.mock("matrix-js-sdk/src/logger");
@@ -44,35 +48,13 @@ jest.mock("../src/SecurityManager", () => ({
isSecretStorageBeingAccessed: jest.fn(), accessSecretStorage: jest.fn(),
}));
const userId = '@user:server';
const deviceId = 'my-device-id';
class MockClient extends EventEmitter {
isGuest = jest.fn();
getUserId = jest.fn();
getKeyBackupVersion = jest.fn().mockResolvedValue(undefined);
getRooms = jest.fn().mockReturnValue([]);
doesServerSupportUnstableFeature = jest.fn().mockResolvedValue(true);
isCrossSigningReady = jest.fn().mockResolvedValue(true);
isSecretStorageReady = jest.fn().mockResolvedValue(true);
isCryptoEnabled = jest.fn().mockReturnValue(true);
isInitialSyncComplete = jest.fn().mockReturnValue(true);
getKeyBackupEnabled = jest.fn();
getStoredDevicesForUser = jest.fn().mockReturnValue([]);
getCrossSigningId = jest.fn();
getStoredCrossSigningForUser = jest.fn();
waitForClientWellKnown = jest.fn();
downloadKeys = jest.fn();
isRoomEncrypted = jest.fn();
getClientWellKnown = jest.fn();
getDeviceId = jest.fn().mockReturnValue(deviceId);
setAccountData = jest.fn();
getAccountData = jest.fn();
}
const mockDispatcher = mocked(dis);
const flushPromises = async () => await new Promise(process.nextTick);
describe('DeviceListener', () => {
let mockClient;
let mockClient: Mocked<MatrixClient> | undefined;
// spy on various toasts' hide and show functions
// easier than mocking
@@ -88,7 +70,29 @@ describe('DeviceListener', () => {
mockPlatformPeg({
getAppVersion: jest.fn().mockResolvedValue('1.2.3'),
});
mockClient = new MockClient();
mockClient = getMockClientWithEventEmitter({
isGuest: jest.fn(),
getUserId: jest.fn().mockReturnValue(userId),
getKeyBackupVersion: jest.fn().mockResolvedValue(undefined),
getRooms: jest.fn().mockReturnValue([]),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(true),
isCrossSigningReady: jest.fn().mockResolvedValue(true),
isSecretStorageReady: jest.fn().mockResolvedValue(true),
isCryptoEnabled: jest.fn().mockReturnValue(true),
isInitialSyncComplete: jest.fn().mockReturnValue(true),
getKeyBackupEnabled: jest.fn(),
getStoredDevicesForUser: jest.fn().mockReturnValue([]),
getCrossSigningId: jest.fn(),
getStoredCrossSigningForUser: jest.fn(),
waitForClientWellKnown: jest.fn(),
downloadKeys: jest.fn(),
isRoomEncrypted: jest.fn(),
getClientWellKnown: jest.fn(),
getDeviceId: jest.fn().mockReturnValue(deviceId),
setAccountData: jest.fn(),
getAccountData: jest.fn(),
checkDeviceTrust: jest.fn().mockReturnValue(new DeviceTrustLevel(false, false, false, false)),
});
jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient);
jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false);
});
@@ -124,7 +128,7 @@ describe('DeviceListener', () => {
it('saves client information on start', async () => {
await createAndStart();
expect(mockClient.setAccountData).toHaveBeenCalledWith(
expect(mockClient!.setAccountData).toHaveBeenCalledWith(
`io.element.matrix_client_information.${deviceId}`,
{ name: 'Element', url: 'localhost', version: '1.2.3' },
);
@@ -133,7 +137,7 @@ describe('DeviceListener', () => {
it('catches error and logs when saving client information fails', async () => {
const errorLogSpy = jest.spyOn(logger, 'error');
const error = new Error('oups');
mockClient.setAccountData.mockRejectedValue(error);
mockClient!.setAccountData.mockRejectedValue(error);
// doesn't throw
await createAndStart();
@@ -147,14 +151,14 @@ describe('DeviceListener', () => {
it('saves client information on logged in action', async () => {
const instance = await createAndStart();
mockClient.setAccountData.mockClear();
mockClient!.setAccountData.mockClear();
// @ts-ignore calling private function
instance.onAction({ action: Action.OnLoggedIn });
await flushPromises();
expect(mockClient.setAccountData).toHaveBeenCalledWith(
expect(mockClient!.setAccountData).toHaveBeenCalledWith(
`io.element.matrix_client_information.${deviceId}`,
{ name: 'Element', url: 'localhost', version: '1.2.3' },
);
@@ -169,30 +173,30 @@ describe('DeviceListener', () => {
beforeEach(() => {
jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false);
mockClient.getAccountData.mockReturnValue(undefined);
mockClient!.getAccountData.mockReturnValue(undefined);
});
it('does not save client information on start', async () => {
await createAndStart();
expect(mockClient.setAccountData).not.toHaveBeenCalled();
expect(mockClient!.setAccountData).not.toHaveBeenCalled();
});
it('removes client information on start if it exists', async () => {
mockClient.getAccountData.mockReturnValue(clientInfoEvent);
mockClient!.getAccountData.mockReturnValue(clientInfoEvent);
await createAndStart();
expect(mockClient.setAccountData).toHaveBeenCalledWith(
expect(mockClient!.setAccountData).toHaveBeenCalledWith(
`io.element.matrix_client_information.${deviceId}`,
{},
);
});
it('does not try to remove client info event that are already empty', async () => {
mockClient.getAccountData.mockReturnValue(emptyClientInfoEvent);
mockClient!.getAccountData.mockReturnValue(emptyClientInfoEvent);
await createAndStart();
expect(mockClient.setAccountData).not.toHaveBeenCalled();
expect(mockClient!.setAccountData).not.toHaveBeenCalled();
});
it('does not save client information on logged in action', async () => {
@@ -203,7 +207,7 @@ describe('DeviceListener', () => {
await flushPromises();
expect(mockClient.setAccountData).not.toHaveBeenCalled();
expect(mockClient!.setAccountData).not.toHaveBeenCalled();
});
it('saves client information after setting is enabled', async () => {
@@ -218,7 +222,7 @@ describe('DeviceListener', () => {
await flushPromises();
expect(mockClient.setAccountData).toHaveBeenCalledWith(
expect(mockClient!.setAccountData).toHaveBeenCalledWith(
`io.element.matrix_client_information.${deviceId}`,
{ name: 'Element', url: 'localhost', version: '1.2.3' },
);
@@ -228,22 +232,22 @@ describe('DeviceListener', () => {
describe('recheck', () => {
it('does nothing when cross signing feature is not supported', async () => {
mockClient.doesServerSupportUnstableFeature.mockResolvedValue(false);
mockClient!.doesServerSupportUnstableFeature.mockResolvedValue(false);
await createAndStart();
expect(mockClient.isCrossSigningReady).not.toHaveBeenCalled();
expect(mockClient!.isCrossSigningReady).not.toHaveBeenCalled();
});
it('does nothing when crypto is not enabled', async () => {
mockClient.isCryptoEnabled.mockReturnValue(false);
mockClient!.isCryptoEnabled.mockReturnValue(false);
await createAndStart();
expect(mockClient.isCrossSigningReady).not.toHaveBeenCalled();
expect(mockClient!.isCrossSigningReady).not.toHaveBeenCalled();
});
it('does nothing when initial sync is not complete', async () => {
mockClient.isInitialSyncComplete.mockReturnValue(false);
mockClient!.isInitialSyncComplete.mockReturnValue(false);
await createAndStart();
expect(mockClient.isCrossSigningReady).not.toHaveBeenCalled();
expect(mockClient!.isCrossSigningReady).not.toHaveBeenCalled();
});
describe('set up encryption', () => {
@@ -253,15 +257,15 @@ describe('DeviceListener', () => {
] as unknown as Room[];
beforeEach(() => {
mockClient.isCrossSigningReady.mockResolvedValue(false);
mockClient.isSecretStorageReady.mockResolvedValue(false);
mockClient.getRooms.mockReturnValue(rooms);
mockClient.isRoomEncrypted.mockReturnValue(true);
mockClient!.isCrossSigningReady.mockResolvedValue(false);
mockClient!.isSecretStorageReady.mockResolvedValue(false);
mockClient!.getRooms.mockReturnValue(rooms);
mockClient!.isRoomEncrypted.mockReturnValue(true);
});
it('hides setup encryption toast when cross signing and secret storage are ready', async () => {
mockClient.isCrossSigningReady.mockResolvedValue(true);
mockClient.isSecretStorageReady.mockResolvedValue(true);
mockClient!.isCrossSigningReady.mockResolvedValue(true);
mockClient!.isSecretStorageReady.mockResolvedValue(true);
await createAndStart();
expect(SetupEncryptionToast.hideToast).toHaveBeenCalled();
});
@@ -277,49 +281,49 @@ describe('DeviceListener', () => {
mocked(isSecretStorageBeingAccessed).mockReturnValue(true);
await createAndStart();
expect(mockClient.downloadKeys).not.toHaveBeenCalled();
expect(mockClient!.downloadKeys).not.toHaveBeenCalled();
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalled();
});
it('does not do any checks or show any toasts when no rooms are encrypted', async () => {
mockClient.isRoomEncrypted.mockReturnValue(false);
mockClient!.isRoomEncrypted.mockReturnValue(false);
await createAndStart();
expect(mockClient.downloadKeys).not.toHaveBeenCalled();
expect(mockClient!.downloadKeys).not.toHaveBeenCalled();
expect(SetupEncryptionToast.showToast).not.toHaveBeenCalled();
});
describe('when user does not have a cross signing id on this device', () => {
beforeEach(() => {
mockClient.getCrossSigningId.mockReturnValue(undefined);
mockClient!.getCrossSigningId.mockReturnValue(null);
});
it('shows verify session toast when account has cross signing', async () => {
mockClient.getStoredCrossSigningForUser.mockReturnValue(true);
mockClient!.getStoredCrossSigningForUser.mockReturnValue(new CrossSigningInfo(userId));
await createAndStart();
expect(mockClient.downloadKeys).toHaveBeenCalled();
expect(mockClient!.downloadKeys).toHaveBeenCalled();
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
SetupEncryptionToast.Kind.VERIFY_THIS_SESSION);
});
it('checks key backup status when when account has cross signing', async () => {
mockClient.getCrossSigningId.mockReturnValue(undefined);
mockClient.getStoredCrossSigningForUser.mockReturnValue(true);
mockClient!.getCrossSigningId.mockReturnValue(null);
mockClient!.getStoredCrossSigningForUser.mockReturnValue(new CrossSigningInfo(userId));
await createAndStart();
expect(mockClient.getKeyBackupEnabled).toHaveBeenCalled();
expect(mockClient!.getKeyBackupEnabled).toHaveBeenCalled();
});
});
describe('when user does have a cross signing id on this device', () => {
beforeEach(() => {
mockClient.getCrossSigningId.mockReturnValue('abc');
mockClient!.getCrossSigningId.mockReturnValue('abc');
});
it('shows upgrade encryption toast when user has a key backup available', async () => {
// non falsy response
mockClient.getKeyBackupVersion.mockResolvedValue({});
mockClient!.getKeyBackupVersion.mockResolvedValue({} as unknown as IKeyBackupInfo);
await createAndStart();
expect(SetupEncryptionToast.showToast).toHaveBeenCalledWith(
@@ -332,51 +336,193 @@ describe('DeviceListener', () => {
it('checks keybackup status when cross signing and secret storage are ready', async () => {
// default mocks set cross signing and secret storage to ready
await createAndStart();
expect(mockClient.getKeyBackupEnabled).toHaveBeenCalled();
expect(mockClient!.getKeyBackupEnabled).toHaveBeenCalled();
expect(mockDispatcher.dispatch).not.toHaveBeenCalled();
});
it('checks keybackup status when setup encryption toast has been dismissed', async () => {
mockClient.isCrossSigningReady.mockResolvedValue(false);
mockClient!.isCrossSigningReady.mockResolvedValue(false);
const instance = await createAndStart();
instance.dismissEncryptionSetup();
await flushPromises();
expect(mockClient.getKeyBackupEnabled).toHaveBeenCalled();
expect(mockClient!.getKeyBackupEnabled).toHaveBeenCalled();
});
it('does not dispatch keybackup event when key backup check is not finished', async () => {
// returns null when key backup status hasn't finished being checked
mockClient.getKeyBackupEnabled.mockReturnValue(null);
mockClient!.getKeyBackupEnabled.mockReturnValue(null);
await createAndStart();
expect(mockDispatcher.dispatch).not.toHaveBeenCalled();
});
it('dispatches keybackup event when key backup is not enabled', async () => {
mockClient.getKeyBackupEnabled.mockReturnValue(false);
mockClient!.getKeyBackupEnabled.mockReturnValue(false);
await createAndStart();
expect(mockDispatcher.dispatch).toHaveBeenCalledWith({ action: Action.ReportKeyBackupNotEnabled });
});
it('does not check key backup status again after check is complete', async () => {
mockClient.getKeyBackupEnabled.mockReturnValue(null);
mockClient!.getKeyBackupEnabled.mockReturnValue(null);
const instance = await createAndStart();
expect(mockClient.getKeyBackupEnabled).toHaveBeenCalled();
expect(mockClient!.getKeyBackupEnabled).toHaveBeenCalled();
// keyback check now complete
mockClient.getKeyBackupEnabled.mockReturnValue(true);
mockClient!.getKeyBackupEnabled.mockReturnValue(true);
// trigger a recheck
instance.dismissEncryptionSetup();
await flushPromises();
expect(mockClient.getKeyBackupEnabled).toHaveBeenCalledTimes(2);
expect(mockClient!.getKeyBackupEnabled).toHaveBeenCalledTimes(2);
// trigger another recheck
instance.dismissEncryptionSetup();
await flushPromises();
// not called again, check was complete last time
expect(mockClient.getKeyBackupEnabled).toHaveBeenCalledTimes(2);
expect(mockClient!.getKeyBackupEnabled).toHaveBeenCalledTimes(2);
});
});
describe('unverified sessions toasts', () => {
const currentDevice = new DeviceInfo(deviceId);
const device2 = new DeviceInfo('d2');
const device3 = new DeviceInfo('d3');
const deviceTrustVerified = new DeviceTrustLevel(true, false, false, false);
const deviceTrustUnverified = new DeviceTrustLevel(false, false, false, false);
beforeEach(() => {
mockClient!.isCrossSigningReady.mockResolvedValue(true);
mockClient!.getStoredDevicesForUser.mockReturnValue([
currentDevice, device2, device3,
]);
// all devices verified by default
mockClient!.checkDeviceTrust.mockReturnValue(deviceTrustVerified);
mockClient!.deviceId = currentDevice.deviceId;
jest.spyOn(SettingsStore, 'getValue').mockImplementation(
settingName => settingName === UIFeature.BulkUnverifiedSessionsReminder,
);
});
describe('bulk unverified sessions toasts', () => {
it('hides toast when cross signing is not ready', async () => {
mockClient!.isCrossSigningReady.mockResolvedValue(false);
await createAndStart();
expect(BulkUnverifiedSessionsToast.hideToast).toHaveBeenCalled();
expect(BulkUnverifiedSessionsToast.showToast).not.toHaveBeenCalled();
});
it('hides toast when all devices at app start are verified', async () => {
await createAndStart();
expect(BulkUnverifiedSessionsToast.hideToast).toHaveBeenCalled();
expect(BulkUnverifiedSessionsToast.showToast).not.toHaveBeenCalled();
});
it('hides toast when feature is disabled', async () => {
// BulkUnverifiedSessionsReminder set to false
jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false);
// currentDevice, device2 are verified, device3 is unverified
// ie if reminder was enabled it should be shown
mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => {
switch (deviceId) {
case currentDevice.deviceId:
case device2.deviceId:
return deviceTrustVerified;
default:
return deviceTrustUnverified;
}
});
await createAndStart();
expect(BulkUnverifiedSessionsToast.hideToast).toHaveBeenCalled();
});
it('hides toast when current device is unverified', async () => {
// device2 verified, current and device3 unverified
mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => {
switch (deviceId) {
case device2.deviceId:
return deviceTrustVerified;
default:
return deviceTrustUnverified;
}
});
await createAndStart();
expect(BulkUnverifiedSessionsToast.hideToast).toHaveBeenCalled();
expect(BulkUnverifiedSessionsToast.showToast).not.toHaveBeenCalled();
});
it('shows toast with unverified devices at app start', async () => {
// currentDevice, device2 are verified, device3 is unverified
mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => {
switch (deviceId) {
case currentDevice.deviceId:
case device2.deviceId:
return deviceTrustVerified;
default:
return deviceTrustUnverified;
}
});
await createAndStart();
expect(BulkUnverifiedSessionsToast.showToast).toHaveBeenCalledWith(
new Set<string>([device3.deviceId]),
);
expect(BulkUnverifiedSessionsToast.hideToast).not.toHaveBeenCalled();
});
it('hides toast when unverified sessions at app start have been dismissed', async () => {
// currentDevice, device2 are verified, device3 is unverified
mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => {
switch (deviceId) {
case currentDevice.deviceId:
case device2.deviceId:
return deviceTrustVerified;
default:
return deviceTrustUnverified;
}
});
const instance = await createAndStart();
expect(BulkUnverifiedSessionsToast.showToast).toHaveBeenCalledWith(
new Set<string>([device3.deviceId]),
);
await instance.dismissUnverifiedSessions([device3.deviceId]);
await flushPromises();
expect(BulkUnverifiedSessionsToast.hideToast).toHaveBeenCalled();
});
it('hides toast when unverified sessions are added after app start', async () => {
// currentDevice, device2 are verified, device3 is unverified
mockClient!.checkDeviceTrust.mockImplementation((_userId, deviceId) => {
switch (deviceId) {
case currentDevice.deviceId:
case device2.deviceId:
return deviceTrustVerified;
default:
return deviceTrustUnverified;
}
});
mockClient!.getStoredDevicesForUser.mockReturnValue([
currentDevice, device2,
]);
await createAndStart();
expect(BulkUnverifiedSessionsToast.hideToast).toHaveBeenCalled();
// add an unverified device
mockClient!.getStoredDevicesForUser.mockReturnValue([
currentDevice, device2, device3,
]);
// trigger a recheck
mockClient!.emit(CryptoEvent.DevicesUpdated, [userId], false);
await flushPromises();
// bulk unverified sessions toast only shown for devices that were
// there at app start
// individual nags are shown for new unverified devices
expect(BulkUnverifiedSessionsToast.hideToast).toHaveBeenCalledTimes(2);
expect(BulkUnverifiedSessionsToast.showToast).not.toHaveBeenCalled();
});
});
});
});

View File

@@ -28,7 +28,7 @@ import { mocked } from 'jest-mock';
import { CallEventHandlerEvent } from 'matrix-js-sdk/src/webrtc/callEventHandler';
import LegacyCallHandler, {
LegacyCallHandlerEvent, PROTOCOL_PSTN, PROTOCOL_PSTN_PREFIXED, PROTOCOL_SIP_NATIVE, PROTOCOL_SIP_VIRTUAL,
LegacyCallHandlerEvent, AudioID, PROTOCOL_PSTN, PROTOCOL_PSTN_PREFIXED, PROTOCOL_SIP_NATIVE, PROTOCOL_SIP_VIRTUAL,
} from '../src/LegacyCallHandler';
import { stubClient, mkStubRoom, untilDispatch } from './test-utils';
import { MatrixClientPeg } from '../src/MatrixClientPeg';
@@ -445,6 +445,9 @@ describe('LegacyCallHandler without third party protocols', () => {
const mockAudioElement = {
play: jest.fn(),
pause: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
muted: false,
} as unknown as HTMLMediaElement;
beforeEach(() => {
jest.clearAllMocks();
@@ -488,6 +491,19 @@ describe('LegacyCallHandler without third party protocols', () => {
});
});
it('should unmute <audio> before playing', () => {
// Test setup: set the audio element as muted
mockAudioElement.muted = true;
expect(mockAudioElement.muted).toStrictEqual(true);
callHandler.play(AudioID.Ring);
// Ensure audio is no longer muted
expect(mockAudioElement.muted).toStrictEqual(false);
// Ensure the audio was played
expect(mockAudioElement.play).toHaveBeenCalled();
});
it('listens for incoming call events when voip is enabled', () => {
const call = new MatrixCall({
client: MatrixClientPeg.get(),

View File

@@ -16,8 +16,8 @@ limitations under the License.
import { mocked, MockedObject } from "jest-mock";
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { IContent, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { SyncState } from "matrix-js-sdk/src/sync";
import { waitFor } from "@testing-library/react";
@@ -60,12 +60,19 @@ describe("Notifier", () => {
let mockClient: MockedObject<MatrixClient>;
let testRoom: Room;
let accountDataEventKey: string;
let accountDataStore = {};
let accountDataStore: Record<string, MatrixEvent | undefined> = {};
let mockSettings: Record<string, boolean> = {};
const userId = "@bob:example.org";
const emitLiveEvent = (event: MatrixEvent): void => {
mockClient!.emit(RoomEvent.Timeline, event, testRoom, false, false, {
liveEvent: true,
timeline: testRoom.getLiveTimeline(),
});
};
beforeEach(() => {
accountDataStore = {};
mockClient = getMockClientWithEventEmitter({
@@ -150,7 +157,7 @@ describe("Notifier", () => {
});
it('does not create notifications before syncing has started', () => {
mockClient!.emit(ClientEvent.Event, event);
emitLiveEvent(event);
expect(MockPlatform.displayNotification).not.toHaveBeenCalled();
expect(MockPlatform.loudNotification).not.toHaveBeenCalled();
@@ -160,7 +167,30 @@ describe("Notifier", () => {
const ownEvent = new MatrixEvent({ sender: userId });
mockClient!.emit(ClientEvent.Sync, SyncState.Syncing, null);
mockClient!.emit(ClientEvent.Event, ownEvent);
emitLiveEvent(ownEvent);
expect(MockPlatform.displayNotification).not.toHaveBeenCalled();
expect(MockPlatform.loudNotification).not.toHaveBeenCalled();
});
it('does not create notifications for non-live events (scrollback)', () => {
mockClient!.emit(ClientEvent.Sync, SyncState.Syncing, null);
mockClient!.emit(RoomEvent.Timeline, event, testRoom, false, false, {
liveEvent: false,
timeline: testRoom.getLiveTimeline(),
});
expect(MockPlatform.displayNotification).not.toHaveBeenCalled();
expect(MockPlatform.loudNotification).not.toHaveBeenCalled();
});
it('does not create notifications for rooms which cannot be obtained via client.getRoom', () => {
mockClient!.emit(ClientEvent.Sync, SyncState.Syncing, null);
mockClient.getRoom.mockReturnValue(null);
mockClient!.emit(RoomEvent.Timeline, event, testRoom, false, false, {
liveEvent: true,
timeline: testRoom.getLiveTimeline(),
});
expect(MockPlatform.displayNotification).not.toHaveBeenCalled();
expect(MockPlatform.loudNotification).not.toHaveBeenCalled();
@@ -175,7 +205,7 @@ describe("Notifier", () => {
});
mockClient!.emit(ClientEvent.Sync, SyncState.Syncing, null);
mockClient!.emit(ClientEvent.Event, event);
emitLiveEvent(event);
expect(MockPlatform.displayNotification).not.toHaveBeenCalled();
expect(MockPlatform.loudNotification).not.toHaveBeenCalled();
@@ -183,7 +213,7 @@ describe("Notifier", () => {
it('creates desktop notification when enabled', () => {
mockClient!.emit(ClientEvent.Sync, SyncState.Syncing, null);
mockClient!.emit(ClientEvent.Event, event);
emitLiveEvent(event);
expect(MockPlatform.displayNotification).toHaveBeenCalledWith(
testRoom.name,
@@ -196,7 +226,7 @@ describe("Notifier", () => {
it('creates a loud notification when enabled', () => {
mockClient!.emit(ClientEvent.Sync, SyncState.Syncing, null);
mockClient!.emit(ClientEvent.Event, event);
emitLiveEvent(event);
expect(MockPlatform.loudNotification).toHaveBeenCalledWith(
event, testRoom,
@@ -212,7 +242,7 @@ describe("Notifier", () => {
});
mockClient!.emit(ClientEvent.Sync, SyncState.Syncing, null);
mockClient!.emit(ClientEvent.Event, event);
emitLiveEvent(event);
// desktop notification created
expect(MockPlatform.displayNotification).toHaveBeenCalled();
@@ -222,12 +252,13 @@ describe("Notifier", () => {
});
describe("_displayPopupNotification", () => {
it.each([
const testCases: {event: IContent | undefined, count: number}[] = [
{ event: { is_silenced: true }, count: 0 },
{ event: { is_silenced: false }, count: 1 },
{ event: undefined, count: 1 },
])("does not dispatch when notifications are silenced", ({ event, count }) => {
mockClient.setAccountData(accountDataEventKey, event);
];
it.each(testCases)("does not dispatch when notifications are silenced", ({ event, count }) => {
mockClient.setAccountData(accountDataEventKey, event!);
Notifier._displayPopupNotification(testEvent, testRoom);
expect(MockPlatform.displayNotification).toHaveBeenCalledTimes(count);
});
@@ -243,16 +274,17 @@ describe("Notifier", () => {
});
describe("_playAudioNotification", () => {
it.each([
const testCases: {event: IContent | undefined, count: number}[] = [
{ event: { is_silenced: true }, count: 0 },
{ event: { is_silenced: false }, count: 1 },
{ event: undefined, count: 1 },
])("does not dispatch when notifications are silenced", ({ event, count }) => {
];
it.each(testCases)("does not dispatch when notifications are silenced", ({ event, count }) => {
// It's not ideal to only look at whether this function has been called
// but avoids starting to look into DOM stuff
Notifier.getSoundForRoom = jest.fn();
mockClient.setAccountData(accountDataEventKey, event);
mockClient.setAccountData(accountDataEventKey, event!);
Notifier._playAudioNotification(testEvent, testRoom);
expect(Notifier.getSoundForRoom).toHaveBeenCalledTimes(count);
});
@@ -267,7 +299,7 @@ describe("Notifier", () => {
notify: true,
tweaks: {},
});
Notifier.start();
Notifier.onSyncStateChange(SyncState.Syncing);
});
@@ -283,7 +315,7 @@ describe("Notifier", () => {
content: {},
event: true,
});
Notifier.onEvent(callEvent);
emitLiveEvent(callEvent);
return callEvent;
};

View File

@@ -14,7 +14,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { VoiceRecording } from "../../src/audio/VoiceRecording";
import { mocked } from 'jest-mock';
// @ts-ignore
import Recorder from 'opus-recorder/dist/recorder.min.js';
import { VoiceRecording, voiceRecorderOptions, highQualityRecorderOptions } from "../../src/audio/VoiceRecording";
import { createAudioContext } from '../..//src/audio/compat';
import MediaDeviceHandler from "../../src/MediaDeviceHandler";
jest.mock('opus-recorder/dist/recorder.min.js');
const RecorderMock = mocked(Recorder);
jest.mock('../../src/audio/compat', () => ({
createAudioContext: jest.fn(),
}));
const createAudioContextMock = mocked(createAudioContext);
jest.mock("../../src/MediaDeviceHandler");
const MediaDeviceHandlerMock = mocked(MediaDeviceHandler);
/**
* The tests here are heavily using access to private props.
@@ -43,6 +60,7 @@ describe("VoiceRecording", () => {
// @ts-ignore
recording.observable = {
update: jest.fn(),
close: jest.fn(),
};
jest.spyOn(recording, "stop").mockImplementation();
recorderSecondsSpy = jest.spyOn(recording, "recorderSeconds", "get");
@@ -52,6 +70,56 @@ describe("VoiceRecording", () => {
jest.resetAllMocks();
});
describe("when starting a recording", () => {
beforeEach(() => {
const mockAudioContext = {
createMediaStreamSource: jest.fn().mockReturnValue({
connect: jest.fn(),
disconnect: jest.fn(),
}),
createScriptProcessor: jest.fn().mockReturnValue({
connect: jest.fn(),
disconnect: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
}),
destination: {},
close: jest.fn(),
};
createAudioContextMock.mockReturnValue(mockAudioContext as unknown as AudioContext);
});
afterEach(async () => {
await recording.stop();
});
it("should record high-quality audio if voice processing is disabled", async () => {
MediaDeviceHandlerMock.getAudioNoiseSuppression.mockReturnValue(false);
await recording.start();
expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalledWith(expect.objectContaining({
audio: expect.objectContaining({ noiseSuppression: { ideal: false } }),
}));
expect(RecorderMock).toHaveBeenCalledWith(expect.objectContaining({
encoderBitRate: highQualityRecorderOptions.bitrate,
encoderApplication: highQualityRecorderOptions.encoderApplication,
}));
});
it("should record normal-quality voice if voice processing is enabled", async () => {
MediaDeviceHandlerMock.getAudioNoiseSuppression.mockReturnValue(true);
await recording.start();
expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalledWith(expect.objectContaining({
audio: expect.objectContaining({ noiseSuppression: { ideal: true } }),
}));
expect(RecorderMock).toHaveBeenCalledWith(expect.objectContaining({
encoderBitRate: voiceRecorderOptions.bitrate,
encoderApplication: voiceRecorderOptions.encoderApplication,
}));
});
});
describe("when recording", () => {
beforeEach(() => {
// @ts-ignore

View File

@@ -352,7 +352,7 @@ describe('<LocationShareMenu />', () => {
// @ts-ignore
mocked(SettingsStore.watchSetting).mockImplementation((featureName, roomId, callback) => {
callback(featureName, roomId, SettingLevel.DEVICE, '', '');
setTimeout(() => {
window.setTimeout(() => {
callback(featureName, roomId, SettingLevel.DEVICE, '', '');
}, 1000);
});

View File

@@ -54,7 +54,7 @@ const encryptedGroupRule = { "conditions": [{ "kind": "event_match", "key": "typ
// eslint-disable-next-line max-len
const pushRules: IPushRules = { "global": { "underride": [{ "conditions": [{ "kind": "event_match", "key": "type", "pattern": "m.call.invite" }], "actions": ["notify", { "set_tweak": "sound", "value": "ring" }, { "set_tweak": "highlight", "value": false }], "rule_id": ".m.rule.call", "default": true, "enabled": true }, oneToOneRule, encryptedOneToOneRule, { "conditions": [{ "kind": "event_match", "key": "type", "pattern": "m.room.message" }], "actions": ["notify", { "set_tweak": "sound", "value": "default" }, { "set_tweak": "highlight", "value": false }], "rule_id": ".m.rule.message", "default": true, "enabled": true }, encryptedGroupRule, { "conditions": [{ "kind": "event_match", "key": "type", "pattern": "im.vector.modular.widgets" }, { "kind": "event_match", "key": "content.type", "pattern": "jitsi" }, { "kind": "event_match", "key": "state_key", "pattern": "*" }], "actions": ["notify", { "set_tweak": "highlight", "value": false }], "rule_id": ".im.vector.jitsi", "default": true, "enabled": true }], "sender": [], "room": [{ "actions": ["dont_notify"], "rule_id": "!zJPyWqpMorfCcWObge:matrix.org", "default": false, "enabled": true }], "content": [{ "actions": ["notify", { "set_tweak": "highlight", "value": false }], "pattern": "banana", "rule_id": "banana", "default": false, "enabled": true }, { "actions": ["notify", { "set_tweak": "sound", "value": "default" }, { "set_tweak": "highlight" }], "pattern": "kadev1", "rule_id": ".m.rule.contains_user_name", "default": true, "enabled": true }], "override": [{ "conditions": [], "actions": ["dont_notify"], "rule_id": ".m.rule.master", "default": true, "enabled": false }, { "conditions": [{ "kind": "event_match", "key": "content.msgtype", "pattern": "m.notice" }], "actions": ["dont_notify"], "rule_id": ".m.rule.suppress_notices", "default": true, "enabled": true }, { "conditions": [{ "kind": "event_match", "key": "type", "pattern": "m.room.member" }, { "kind": "event_match", "key": "content.membership", "pattern": "invite" }, { "kind": "event_match", "key": "state_key", "pattern": "@kadev1:matrix.org" }], "actions": ["notify", { "set_tweak": "sound", "value": "default" }, { "set_tweak": "highlight", "value": false }], "rule_id": ".m.rule.invite_for_me", "default": true, "enabled": true }, { "conditions": [{ "kind": "event_match", "key": "type", "pattern": "m.room.member" }], "actions": ["dont_notify"], "rule_id": ".m.rule.member_event", "default": true, "enabled": true }, { "conditions": [{ "kind": "contains_display_name" }], "actions": ["notify", { "set_tweak": "sound", "value": "default" }, { "set_tweak": "highlight" }], "rule_id": ".m.rule.contains_display_name", "default": true, "enabled": true }, { "conditions": [{ "kind": "event_match", "key": "content.body", "pattern": "@room" }, { "kind": "sender_notification_permission", "key": "room" }], "actions": ["notify", { "set_tweak": "highlight", "value": true }], "rule_id": ".m.rule.roomnotif", "default": true, "enabled": true }, { "conditions": [{ "kind": "event_match", "key": "type", "pattern": "m.room.tombstone" }, { "kind": "event_match", "key": "state_key", "pattern": "" }], "actions": ["notify", { "set_tweak": "highlight", "value": true }], "rule_id": ".m.rule.tombstone", "default": true, "enabled": true }, { "conditions": [{ "kind": "event_match", "key": "type", "pattern": "m.reaction" }], "actions": ["dont_notify"], "rule_id": ".m.rule.reaction", "default": true, "enabled": true }] }, "device": {} } as IPushRules;
const flushPromises = async () => await new Promise(resolve => setTimeout(resolve));
const flushPromises = async () => await new Promise(resolve => window.setTimeout(resolve));
describe('<Notifications />', () => {
const getComponent = () => render(<Notifications />);

View File

@@ -18,6 +18,9 @@ import React from "react";
import { fireEvent, render, RenderResult } from "@testing-library/react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { mocked } from "jest-mock";
import RolesRoomSettingsTab from "../../../../../../src/components/views/settings/tabs/room/RolesRoomSettingsTab";
import { mkStubRoom, stubClient } from "../../../../../test-utils";
@@ -29,23 +32,52 @@ import { ElementCall } from "../../../../../../src/models/Call";
describe("RolesRoomSettingsTab", () => {
const roomId = "!room:example.com";
let cli: MatrixClient;
let room: Room;
const renderTab = (): RenderResult => {
return render(<RolesRoomSettingsTab roomId={roomId} />);
};
const getVoiceBroadcastsSelect = () => {
const getVoiceBroadcastsSelect = (): HTMLElement => {
return renderTab().container.querySelector("select[label='Voice broadcasts']");
};
const getVoiceBroadcastsSelectedOption = () => {
const getVoiceBroadcastsSelectedOption = (): HTMLElement => {
return renderTab().container.querySelector("select[label='Voice broadcasts'] option:checked");
};
beforeEach(() => {
stubClient();
cli = MatrixClientPeg.get();
mkStubRoom(roomId, "test room", cli);
room = mkStubRoom(roomId, "test room", cli);
});
it("should allow an Admin to demote themselves but not others", () => {
mocked(cli.getRoom).mockReturnValue(room);
// @ts-ignore - mocked doesn't support overloads properly
mocked(room.currentState.getStateEvents).mockImplementation((type, key) => {
if (key === undefined) return [] as MatrixEvent[];
if (type === "m.room.power_levels") {
return new MatrixEvent({
sender: "@sender:server",
room_id: roomId,
type: "m.room.power_levels",
state_key: "",
content: {
users: {
[cli.getUserId()]: 100,
"@admin:server": 100,
},
},
});
}
return null;
});
mocked(room.currentState.mayClientSendStateEvent).mockReturnValue(true);
const { container } = renderTab();
expect(container.querySelector(`[placeholder="${cli.getUserId()}"]`)).not.toBeDisabled();
expect(container.querySelector(`[placeholder="@admin:server"]`)).toBeDisabled();
});
it("should initially show »Moderator« permission for »Voice broadcasts«", () => {
@@ -79,19 +111,19 @@ describe("RolesRoomSettingsTab", () => {
});
};
const getStartCallSelect = (tab: RenderResult) => {
const getStartCallSelect = (tab: RenderResult): HTMLElement => {
return tab.container.querySelector("select[label='Start Element Call calls']");
};
const getStartCallSelectedOption = (tab: RenderResult) => {
const getStartCallSelectedOption = (tab: RenderResult): HTMLElement => {
return tab.container.querySelector("select[label='Start Element Call calls'] option:checked");
};
const getJoinCallSelect = (tab: RenderResult) => {
const getJoinCallSelect = (tab: RenderResult): HTMLElement => {
return tab.container.querySelector("select[label='Join Element Call calls']");
};
const getJoinCallSelectedOption = (tab: RenderResult) => {
const getJoinCallSelectedOption = (tab: RenderResult): HTMLElement => {
return tab.container.querySelector("select[label='Join Element Call calls'] option:checked");
};

View File

@@ -184,6 +184,7 @@ describe("PipView", () => {
room,
alice,
client,
voiceBroadcastPlaybacksStore,
voiceBroadcastRecordingsStore,
);
voiceBroadcastPreRecordingStore.setCurrent(voiceBroadcastPreRecording);
@@ -271,6 +272,19 @@ describe("PipView", () => {
});
});
describe("when there is a voice broadcast playback and pre-recording", () => {
beforeEach(() => {
startVoiceBroadcastPlayback(room);
setUpVoiceBroadcastPreRecording();
renderPip();
});
it("should render the voice broadcast pre-recording PiP", () => {
// check for the „Go live“ button
expect(screen.queryByText("Go live")).toBeInTheDocument();
});
});
describe("when there is a voice broadcast pre-recording", () => {
beforeEach(() => {
setUpVoiceBroadcastPreRecording();

View File

@@ -939,96 +939,6 @@ describe("ElementCall", () => {
call.off(CallEvent.Destroy, onDestroy);
});
describe("clean", () => {
const aliceWeb: IMyDevice = {
device_id: "aliceweb",
last_seen_ts: 0,
};
const aliceDesktop: IMyDevice = {
device_id: "alicedesktop",
last_seen_ts: 0,
};
const aliceDesktopOffline: IMyDevice = {
device_id: "alicedesktopoffline",
last_seen_ts: 1000 * 60 * 60 * -2, // 2 hours ago
};
const aliceDesktopNeverOnline: IMyDevice = {
device_id: "alicedesktopneveronline",
};
const mkContent = (devices: IMyDevice[]) => ({
"m.calls": [{
"m.call_id": call.groupCall.groupCallId,
"m.devices": devices.map(d => ({
device_id: d.device_id, session_id: "1", feeds: [], expires_ts: 1000 * 60 * 10,
})),
}],
});
const expectDevices = (devices: IMyDevice[]) => expect(
room.currentState.getStateEvents(ElementCall.MEMBER_EVENT_TYPE.name, alice.userId)?.getContent(),
).toEqual({
"m.calls": [{
"m.call_id": call.groupCall.groupCallId,
"m.devices": devices.map(d => ({
device_id: d.device_id, session_id: "1", feeds: [], expires_ts: expect.any(Number),
})),
}],
});
beforeEach(() => {
client.getDeviceId.mockReturnValue(aliceWeb.device_id);
client.getDevices.mockResolvedValue({
devices: [
aliceWeb,
aliceDesktop,
aliceDesktopOffline,
aliceDesktopNeverOnline,
],
});
});
it("doesn't clean up valid devices", async () => {
await client.sendStateEvent(
room.roomId,
ElementCall.MEMBER_EVENT_TYPE.name,
mkContent([aliceDesktop]),
alice.userId,
);
await call.clean();
expectDevices([aliceDesktop]);
});
it("cleans up our own device if we're disconnected", async () => {
await client.sendStateEvent(
room.roomId,
ElementCall.MEMBER_EVENT_TYPE.name,
mkContent([aliceWeb, aliceDesktop]),
alice.userId,
);
await call.clean();
expectDevices([aliceDesktop]);
});
it("cleans up devices that have never been online", async () => {
await client.sendStateEvent(
room.roomId,
ElementCall.MEMBER_EVENT_TYPE.name,
mkContent([aliceDesktop, aliceDesktopNeverOnline]),
alice.userId,
);
await call.clean();
expectDevices([aliceDesktop]);
});
it("no-ops if there are no state events", async () => {
await call.clean();
expect(room.currentState.getStateEvents(JitsiCall.MEMBER_EVENT_TYPE, alice.userId)).toBe(null);
});
});
});
describe("instance in a video room", () => {

View File

@@ -21,7 +21,7 @@ import fetch from 'node-fetch';
// jest 27 removes setImmediate from jsdom
// polyfill until setImmediate use in client can be removed
// @ts-ignore - we know the contract is wrong. That's why we're stubbing it.
global.setImmediate = callback => setTimeout(callback, 0);
global.setImmediate = callback => window.setTimeout(callback, 0);
// Stub ResizeObserver
// @ts-ignore - we know it's a duplicate (that's why we're stubbing it)

View File

@@ -69,7 +69,6 @@ describe("StopGapWidgetDriver", () => {
"org.matrix.msc2762.send.event:org.matrix.rageshake_request",
"org.matrix.msc2762.receive.event:org.matrix.rageshake_request",
"org.matrix.msc2762.receive.state_event:m.room.member",
"org.matrix.msc2762.send.state_event:org.matrix.msc3401.call",
"org.matrix.msc2762.receive.state_event:org.matrix.msc3401.call",
"org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@alice:example.org",
"org.matrix.msc2762.receive.state_event:org.matrix.msc3401.call.member",

View File

@@ -179,7 +179,7 @@ export const watchPositionMockImplementation = (delays: number[], errorCodes: nu
let totalDelay = 0;
delays.map((delayMs, index) => {
totalDelay += delayMs;
const timeout = setTimeout(() => {
const timeout = window.setTimeout(() => {
if (errorCodes[index]) {
error(getMockGeolocationPositionError(errorCodes[index], 'error message'));
} else {

View File

@@ -91,6 +91,7 @@ export function createTestClient(): MatrixClient {
getDeviceId: jest.fn().mockReturnValue("ABCDEFGHI"),
deviceId: "ABCDEFGHI",
getDevices: jest.fn().mockResolvedValue({ devices: [{ device_id: "ABCDEFGHI" }] }),
getSessionId: jest.fn().mockReturnValue("iaszphgvfku"),
credentials: { userId: "@userId:matrix.org" },
store: {

View File

@@ -48,7 +48,7 @@ export function untilDispatch(
let timeoutId;
// set a timeout handler if needed
if (timeout > 0) {
timeoutId = setTimeout(() => {
timeoutId = window.setTimeout(() => {
if (!fulfilled) {
reject(new Error(`untilDispatch: timed out at ${callerLine}`));
fulfilled = true;
@@ -92,7 +92,7 @@ export function untilEmission(
let timeoutId;
// set a timeout handler if needed
if (timeout > 0) {
timeoutId = setTimeout(() => {
timeoutId = window.setTimeout(() => {
if (!fulfilled) {
reject(new Error(`untilEmission: timed out at ${callerLine}`));
fulfilled = true;
@@ -134,7 +134,7 @@ const findByTagAndAttr = (attr: string) =>
export const findByTagAndTestId = findByTagAndAttr('data-test-id');
export const flushPromises = async () => await new Promise(resolve => setTimeout(resolve));
export const flushPromises = async () => await new Promise(resolve => window.setTimeout(resolve));
// with jest's modern fake timers process.nextTick is also mocked,
// flushing promises in the normal way then waits for some advancement

View File

@@ -35,12 +35,17 @@ describe("VoiceBroadcastHeader", () => {
const sender = new RoomMember(roomId, userId);
let container: Container;
const renderHeader = (live: VoiceBroadcastLiveness, showBroadcast: boolean = undefined): RenderResult => {
const renderHeader = (
live: VoiceBroadcastLiveness,
showBroadcast?: boolean,
buffering?: boolean,
): RenderResult => {
return render(<VoiceBroadcastHeader
live={live}
microphoneLabel={sender.name}
room={room}
showBroadcast={showBroadcast}
showBuffering={buffering}
/>);
};
@@ -51,6 +56,16 @@ describe("VoiceBroadcastHeader", () => {
});
describe("when rendering a live broadcast header with broadcast info", () => {
beforeEach(() => {
container = renderHeader("live", true, true).container;
});
it("should render the header with a red live badge", () => {
expect(container).toMatchSnapshot();
});
});
describe("when rendering a buffering live broadcast header with broadcast info", () => {
beforeEach(() => {
container = renderHeader("live", true).container;
});

View File

@@ -1,5 +1,55 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`VoiceBroadcastHeader when rendering a buffering live broadcast header with broadcast info should render the header with a red live badge 1`] = `
<div>
<div
class="mx_VoiceBroadcastHeader"
>
<div
data-testid="room-avatar"
>
room avatar:
!room:example.com
</div>
<div
class="mx_VoiceBroadcastHeader_content"
>
<div
class="mx_VoiceBroadcastHeader_room"
>
!room:example.com
</div>
<div
class="mx_VoiceBroadcastHeader_line"
>
<div
class="mx_Icon mx_Icon_16"
/>
<span>
test user
</span>
</div>
<div
class="mx_VoiceBroadcastHeader_line"
>
<div
class="mx_Icon mx_Icon_16"
/>
Voice broadcast
</div>
</div>
<div
class="mx_LiveBadge"
>
<div
class="mx_Icon mx_Icon_16"
/>
Live
</div>
</div>
</div>
`;
exports[`VoiceBroadcastHeader when rendering a live (grey) broadcast header with broadcast info should render the header with a grey live badge 1`] = `
<div>
<div
@@ -87,6 +137,22 @@ exports[`VoiceBroadcastHeader when rendering a live broadcast header with broadc
/>
Voice broadcast
</div>
<div
class="mx_VoiceBroadcastHeader_line"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading..."
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 14px; height: 14px;"
/>
</div>
Buffering…
</div>
</div>
<div
class="mx_LiveBadge"

View File

@@ -21,6 +21,7 @@ import { act, render, RenderResult, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {
VoiceBroadcastPlaybacksStore,
VoiceBroadcastPreRecording,
VoiceBroadcastPreRecordingPip,
VoiceBroadcastRecordingsStore,
@@ -42,6 +43,7 @@ jest.mock("../../../../src/components/views/avatars/RoomAvatar", () => ({
describe("VoiceBroadcastPreRecordingPip", () => {
let renderResult: RenderResult;
let preRecording: VoiceBroadcastPreRecording;
let playbacksStore: VoiceBroadcastPlaybacksStore;
let recordingsStore: VoiceBroadcastRecordingsStore;
let client: MatrixClient;
let room: Room;
@@ -51,6 +53,7 @@ describe("VoiceBroadcastPreRecordingPip", () => {
client = stubClient();
room = new Room("!room@example.com", client, client.getUserId() || "");
sender = new RoomMember(room.roomId, client.getUserId() || "");
playbacksStore = new VoiceBroadcastPlaybacksStore();
recordingsStore = new VoiceBroadcastRecordingsStore();
mocked(requestMediaPermissions).mockReturnValue(new Promise<MediaStream>((r) => {
r({
@@ -76,6 +79,7 @@ describe("VoiceBroadcastPreRecordingPip", () => {
room,
sender,
client,
playbacksStore,
recordingsStore,
);
});

View File

@@ -252,9 +252,17 @@ exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast s
class="mx_VoiceBroadcastHeader_line"
>
<div
class="mx_Icon mx_Icon_16"
/>
Voice broadcast
class="mx_Spinner"
>
<div
aria-label="Loading..."
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 14px; height: 14px;"
/>
</div>
Buffering…
</div>
</div>
<div
@@ -280,14 +288,13 @@ exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast s
/>
</div>
<div
class="mx_Spinner"
aria-label="pause voice broadcast"
class="mx_AccessibleButton mx_VoiceBroadcastControl"
role="button"
tabindex="0"
>
<div
aria-label="Loading..."
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
class="mx_Icon mx_Icon_16"
/>
</div>
<div

View File

@@ -407,6 +407,17 @@ describe("VoiceBroadcastPlayback", () => {
describe("and calling stop", () => {
stopPlayback();
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
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();
});
});
});
describe("and calling destroy", () => {

View File

@@ -18,6 +18,7 @@ import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix";
import {
startNewVoiceBroadcastRecording,
VoiceBroadcastPlaybacksStore,
VoiceBroadcastPreRecording,
VoiceBroadcastRecordingsStore,
} from "../../../src/voice-broadcast";
@@ -30,6 +31,7 @@ describe("VoiceBroadcastPreRecording", () => {
let client: MatrixClient;
let room: Room;
let sender: RoomMember;
let playbacksStore: VoiceBroadcastPlaybacksStore;
let recordingsStore: VoiceBroadcastRecordingsStore;
let preRecording: VoiceBroadcastPreRecording;
let onDismiss: (voiceBroadcastPreRecording: VoiceBroadcastPreRecording) => void;
@@ -38,12 +40,13 @@ describe("VoiceBroadcastPreRecording", () => {
client = stubClient();
room = new Room(roomId, client, client.getUserId() || "");
sender = new RoomMember(roomId, client.getUserId() || "");
playbacksStore = new VoiceBroadcastPlaybacksStore();
recordingsStore = new VoiceBroadcastRecordingsStore();
});
beforeEach(() => {
onDismiss = jest.fn();
preRecording = new VoiceBroadcastPreRecording(room, sender, client, recordingsStore);
preRecording = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore);
preRecording.on("dismiss", onDismiss);
});
@@ -56,6 +59,7 @@ describe("VoiceBroadcastPreRecording", () => {
expect(startNewVoiceBroadcastRecording).toHaveBeenCalledWith(
room,
client,
playbacksStore,
recordingsStore,
);
});

View File

@@ -18,6 +18,7 @@ import { mocked } from "jest-mock";
import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix";
import {
VoiceBroadcastPlaybacksStore,
VoiceBroadcastPreRecording,
VoiceBroadcastPreRecordingStore,
VoiceBroadcastRecordingsStore,
@@ -31,6 +32,7 @@ describe("VoiceBroadcastPreRecordingStore", () => {
let client: MatrixClient;
let room: Room;
let sender: RoomMember;
let playbacksStore: VoiceBroadcastPlaybacksStore;
let recordingsStore: VoiceBroadcastRecordingsStore;
let store: VoiceBroadcastPreRecordingStore;
let preRecording1: VoiceBroadcastPreRecording;
@@ -39,6 +41,7 @@ describe("VoiceBroadcastPreRecordingStore", () => {
client = stubClient();
room = new Room(roomId, client, client.getUserId() || "");
sender = new RoomMember(roomId, client.getUserId() || "");
playbacksStore = new VoiceBroadcastPlaybacksStore();
recordingsStore = new VoiceBroadcastRecordingsStore();
});
@@ -46,7 +49,7 @@ describe("VoiceBroadcastPreRecordingStore", () => {
store = new VoiceBroadcastPreRecordingStore();
jest.spyOn(store, "emit");
jest.spyOn(store, "removeAllListeners");
preRecording1 = new VoiceBroadcastPreRecording(room, sender, client, recordingsStore);
preRecording1 = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore);
jest.spyOn(preRecording1, "off");
});
@@ -117,7 +120,7 @@ describe("VoiceBroadcastPreRecordingStore", () => {
beforeEach(() => {
mocked(store.emit).mockClear();
mocked(preRecording1.off).mockClear();
preRecording2 = new VoiceBroadcastPreRecording(room, sender, client, recordingsStore);
preRecording2 = new VoiceBroadcastPreRecording(room, sender, client, playbacksStore, recordingsStore);
store.setCurrent(preRecording2);
});

View File

@@ -15,16 +15,20 @@ limitations under the License.
*/
import { mocked } from "jest-mock";
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import {
checkVoiceBroadcastPreConditions,
VoiceBroadcastInfoState,
VoiceBroadcastPlayback,
VoiceBroadcastPlaybacksStore,
VoiceBroadcastPreRecording,
VoiceBroadcastPreRecordingStore,
VoiceBroadcastRecordingsStore,
} from "../../../src/voice-broadcast";
import { setUpVoiceBroadcastPreRecording } from "../../../src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording";
import { mkRoomMemberJoinEvent, stubClient } from "../../test-utils";
import { mkVoiceBroadcastInfoStateEvent } from "./test-utils";
jest.mock("../../../src/voice-broadcast/utils/checkVoiceBroadcastPreConditions");
@@ -34,11 +38,20 @@ describe("setUpVoiceBroadcastPreRecording", () => {
let userId: string;
let room: Room;
let preRecordingStore: VoiceBroadcastPreRecordingStore;
let infoEvent: MatrixEvent;
let playback: VoiceBroadcastPlayback;
let playbacksStore: VoiceBroadcastPlaybacksStore;
let recordingsStore: VoiceBroadcastRecordingsStore;
const itShouldReturnNull = () => {
it("should return null", () => {
expect(setUpVoiceBroadcastPreRecording(room, client, recordingsStore, preRecordingStore)).toBeNull();
expect(setUpVoiceBroadcastPreRecording(
room,
client,
playbacksStore,
recordingsStore,
preRecordingStore,
)).toBeNull();
expect(checkVoiceBroadcastPreConditions).toHaveBeenCalledWith(room, client, recordingsStore);
});
};
@@ -51,7 +64,16 @@ describe("setUpVoiceBroadcastPreRecording", () => {
userId = clientUserId;
room = new Room(roomId, client, userId);
infoEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
client.getUserId()!,
client.getDeviceId()!,
);
preRecordingStore = new VoiceBroadcastPreRecordingStore();
playback = new VoiceBroadcastPlayback(infoEvent, client);
jest.spyOn(playback, "pause");
playbacksStore = new VoiceBroadcastPlaybacksStore();
recordingsStore = new VoiceBroadcastRecordingsStore();
});
@@ -85,15 +107,25 @@ describe("setUpVoiceBroadcastPreRecording", () => {
itShouldReturnNull();
});
describe("and there is a room member", () => {
describe("and there is a room member and listening to another broadcast", () => {
beforeEach(() => {
playbacksStore.setCurrent(playback);
room.currentState.setStateEvents([
mkRoomMemberJoinEvent(userId, roomId),
]);
});
it("should create a voice broadcast pre-recording", () => {
const result = setUpVoiceBroadcastPreRecording(room, client, recordingsStore, preRecordingStore);
it("should pause the current playback and create a voice broadcast pre-recording", () => {
const result = setUpVoiceBroadcastPreRecording(
room,
client,
playbacksStore,
recordingsStore,
preRecordingStore,
);
expect(playback.pause).toHaveBeenCalled();
expect(playbacksStore.getCurrent()).toBeNull();
expect(checkVoiceBroadcastPreConditions).toHaveBeenCalledWith(room, client, recordingsStore);
expect(result).toBeInstanceOf(VoiceBroadcastPreRecording);
});

View File

@@ -15,7 +15,7 @@ limitations under the License.
*/
import { mocked } from "jest-mock";
import { EventType, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { EventType, ISendEventResponse, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import Modal from "../../../src/Modal";
import {
@@ -24,6 +24,8 @@ import {
VoiceBroadcastInfoState,
VoiceBroadcastRecordingsStore,
VoiceBroadcastRecording,
VoiceBroadcastPlaybacksStore,
VoiceBroadcastPlayback,
} from "../../../src/voice-broadcast";
import { mkEvent, stubClient } from "../../test-utils";
import { mkVoiceBroadcastInfoStateEvent } from "./test-utils";
@@ -38,6 +40,7 @@ describe("startNewVoiceBroadcastRecording", () => {
const roomId = "!room:example.com";
const otherUserId = "@other:example.com";
let client: MatrixClient;
let playbacksStore: VoiceBroadcastPlaybacksStore;
let recordingsStore: VoiceBroadcastRecordingsStore;
let room: Room;
let infoEvent: MatrixEvent;
@@ -46,45 +49,50 @@ describe("startNewVoiceBroadcastRecording", () => {
beforeEach(() => {
client = stubClient();
room = new Room(roomId, client, client.getUserId());
room = new Room(roomId, client, client.getUserId()!);
jest.spyOn(room.currentState, "maySendStateEvent");
mocked(client.getRoom).mockImplementation((getRoomId: string) => {
if (getRoomId === roomId) {
return room;
}
return null;
});
mocked(client.sendStateEvent).mockImplementation((
sendRoomId: string,
eventType: string,
_content: any,
_stateKey: string,
) => {
content: any,
stateKey: string,
): Promise<ISendEventResponse> => {
if (sendRoomId === roomId && eventType === VoiceBroadcastInfoEventType) {
return Promise.resolve({ event_id: infoEvent.getId() });
return Promise.resolve({ event_id: infoEvent.getId()! });
}
});
recordingsStore = {
setCurrent: jest.fn(),
getCurrent: jest.fn(),
} as unknown as VoiceBroadcastRecordingsStore;
throw new Error("Unexpected sendStateEvent call");
});
infoEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
client.getUserId(),
client.getDeviceId(),
client.getUserId()!,
client.getDeviceId()!,
);
otherEvent = mkEvent({
event: true,
type: EventType.RoomMember,
content: {},
user: client.getUserId(),
user: client.getUserId()!,
room: roomId,
skey: "",
});
playbacksStore = new VoiceBroadcastPlaybacksStore();
recordingsStore = {
setCurrent: jest.fn(),
getCurrent: jest.fn(),
} as unknown as VoiceBroadcastRecordingsStore;
mocked(VoiceBroadcastRecording).mockImplementation((
infoEvent: MatrixEvent,
client: MatrixClient,
@@ -106,22 +114,35 @@ describe("startNewVoiceBroadcastRecording", () => {
mocked(room.currentState.maySendStateEvent).mockReturnValue(true);
});
describe("when there currently is no other broadcast", () => {
it("should create a new Voice Broadcast", async () => {
describe("when currently listening to a broadcast and there is no recording", () => {
let playback: VoiceBroadcastPlayback;
beforeEach(() => {
playback = new VoiceBroadcastPlayback(infoEvent, client);
jest.spyOn(playback, "pause");
playbacksStore.setCurrent(playback);
});
it("should stop listen to the current broadcast and create a new recording", async () => {
mocked(client.sendStateEvent).mockImplementation(async (
_roomId: string,
_eventType: string,
_content: any,
_stateKey = "",
) => {
setTimeout(() => {
): Promise<ISendEventResponse> => {
window.setTimeout(() => {
// emit state events after resolving the promise
room.currentState.setStateEvents([otherEvent]);
room.currentState.setStateEvents([infoEvent]);
}, 0);
return { event_id: infoEvent.getId() };
return { event_id: infoEvent.getId()! };
});
const recording = await startNewVoiceBroadcastRecording(room, client, recordingsStore);
const recording = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore);
expect(recording).not.toBeNull();
// expect to stop and clear the current playback
expect(playback.pause).toHaveBeenCalled();
expect(playbacksStore.getCurrent()).toBeNull();
expect(client.sendStateEvent).toHaveBeenCalledWith(
roomId,
@@ -133,8 +154,8 @@ describe("startNewVoiceBroadcastRecording", () => {
},
client.getUserId(),
);
expect(recording.infoEvent).toBe(infoEvent);
expect(recording.start).toHaveBeenCalled();
expect(recording!.infoEvent).toBe(infoEvent);
expect(recording!.start).toHaveBeenCalled();
});
});
@@ -144,7 +165,7 @@ describe("startNewVoiceBroadcastRecording", () => {
new VoiceBroadcastRecording(infoEvent, client),
);
result = await startNewVoiceBroadcastRecording(room, client, recordingsStore);
result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore);
});
it("should not start a voice broadcast", () => {
@@ -162,12 +183,12 @@ describe("startNewVoiceBroadcastRecording", () => {
mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Resumed,
client.getUserId(),
client.getDeviceId(),
client.getUserId()!,
client.getDeviceId()!,
),
]);
result = await startNewVoiceBroadcastRecording(room, client, recordingsStore);
result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore);
});
it("should not start a voice broadcast", () => {
@@ -190,7 +211,7 @@ describe("startNewVoiceBroadcastRecording", () => {
),
]);
result = await startNewVoiceBroadcastRecording(room, client, recordingsStore);
result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore);
});
it("should not start a voice broadcast", () => {
@@ -206,7 +227,7 @@ describe("startNewVoiceBroadcastRecording", () => {
describe("when the current user is not allowed to send voice broadcast info state events", () => {
beforeEach(async () => {
mocked(room.currentState.maySendStateEvent).mockReturnValue(false);
result = await startNewVoiceBroadcastRecording(room, client, recordingsStore);
result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore);
});
it("should not start a voice broadcast", () => {