await promises (#10992)

This commit is contained in:
Kerry 2023-05-29 10:19:37 +12:00 committed by GitHub
parent 3bba816b10
commit 9080f3dd55
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 58 additions and 10 deletions

View file

@ -34,9 +34,9 @@ import SettingsSubsection, { SettingsSubsectionText } from "../../shared/Setting
type InteractionName = "WebSettingsSidebarTabSpacesCheckbox" | "WebQuickSettingsPinToSidebarCheckbox"; type InteractionName = "WebSettingsSidebarTabSpacesCheckbox" | "WebQuickSettingsPinToSidebarCheckbox";
export const onMetaSpaceChangeFactory = export const onMetaSpaceChangeFactory =
(metaSpace: MetaSpace, interactionName: InteractionName) => (e: ChangeEvent<HTMLInputElement>) => { (metaSpace: MetaSpace, interactionName: InteractionName) => async (e: ChangeEvent<HTMLInputElement>) => {
const currentValue = SettingsStore.getValue("Spaces.enabledMetaSpaces"); const currentValue = SettingsStore.getValue("Spaces.enabledMetaSpaces");
SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.ACCOUNT, { await SettingsStore.setValue("Spaces.enabledMetaSpaces", null, SettingLevel.ACCOUNT, {
...currentValue, ...currentValue,
[metaSpace]: e.target.checked, [metaSpace]: e.target.checked,
}); });

View file

@ -16,6 +16,7 @@ limitations under the License.
*/ */
import React, { ReactNode } from "react"; import React, { ReactNode } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../../../languageHandler"; import { _t } from "../../../../../languageHandler";
import MediaDeviceHandler, { IMediaDevices, MediaDeviceKindEnum } from "../../../../../MediaDeviceHandler"; import MediaDeviceHandler, { IMediaDevices, MediaDeviceKindEnum } from "../../../../../MediaDeviceHandler";
@ -40,6 +41,21 @@ interface IState {
audioNoiseSuppression: boolean; audioNoiseSuppression: boolean;
} }
/**
* Maps deviceKind to the right get method on MediaDeviceHandler
* Helpful for setting state
*/
const mapDeviceKindToHandlerValue = (deviceKind: MediaDeviceKindEnum): string | null => {
switch (deviceKind) {
case MediaDeviceKindEnum.AudioOutput:
return MediaDeviceHandler.getAudioOutput();
case MediaDeviceKindEnum.AudioInput:
return MediaDeviceHandler.getAudioInput();
case MediaDeviceKindEnum.VideoInput:
return MediaDeviceHandler.getVideoInput();
}
};
export default class VoiceUserSettingsTab extends React.Component<{}, IState> { export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
public constructor(props: {}) { public constructor(props: {}) {
super(props); super(props);
@ -58,16 +74,16 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
public async componentDidMount(): Promise<void> { public async componentDidMount(): Promise<void> {
const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices(); const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices();
if (canSeeDeviceLabels) { if (canSeeDeviceLabels) {
this.refreshMediaDevices(); await this.refreshMediaDevices();
} }
} }
private refreshMediaDevices = async (stream?: MediaStream): Promise<void> => { private refreshMediaDevices = async (stream?: MediaStream): Promise<void> => {
this.setState({ this.setState({
mediaDevices: (await MediaDeviceHandler.getDevices()) ?? null, mediaDevices: (await MediaDeviceHandler.getDevices()) ?? null,
[MediaDeviceKindEnum.AudioOutput]: MediaDeviceHandler.getAudioOutput(), [MediaDeviceKindEnum.AudioOutput]: mapDeviceKindToHandlerValue(MediaDeviceKindEnum.AudioOutput),
[MediaDeviceKindEnum.AudioInput]: MediaDeviceHandler.getAudioInput(), [MediaDeviceKindEnum.AudioInput]: mapDeviceKindToHandlerValue(MediaDeviceKindEnum.AudioInput),
[MediaDeviceKindEnum.VideoInput]: MediaDeviceHandler.getVideoInput(), [MediaDeviceKindEnum.VideoInput]: mapDeviceKindToHandlerValue(MediaDeviceKindEnum.VideoInput),
}); });
if (stream) { if (stream) {
// kill stream (after we've enumerated the devices, otherwise we'd get empty labels again) // kill stream (after we've enumerated the devices, otherwise we'd get empty labels again)
@ -80,13 +96,20 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
private requestMediaPermissions = async (): Promise<void> => { private requestMediaPermissions = async (): Promise<void> => {
const stream = await requestMediaPermissions(); const stream = await requestMediaPermissions();
if (stream) { if (stream) {
this.refreshMediaDevices(stream); await this.refreshMediaDevices(stream);
} }
}; };
private setDevice = (deviceId: string, kind: MediaDeviceKindEnum): void => { private setDevice = async (deviceId: string, kind: MediaDeviceKindEnum): Promise<void> => {
MediaDeviceHandler.instance.setDevice(deviceId, kind); // set state immediately so UI is responsive
this.setState<any>({ [kind]: deviceId }); this.setState<any>({ [kind]: deviceId });
try {
await MediaDeviceHandler.instance.setDevice(deviceId, kind);
} catch (error) {
logger.error(`Failed to set device ${kind}: ${deviceId}`);
// reset state to current value
this.setState<any>({ [kind]: mapDeviceKindToHandlerValue(kind) });
}
}; };
private changeWebRtcMethod = (p2p: boolean): void => { private changeWebRtcMethod = (p2p: boolean): void => {

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from "react"; import React from "react";
import { mocked } from "jest-mock"; import { mocked } from "jest-mock";
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import { logger } from "matrix-js-sdk/src/logger";
import VoiceUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/VoiceUserSettingsTab"; import VoiceUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/VoiceUserSettingsTab";
import MediaDeviceHandler, { IMediaDevices, MediaDeviceKindEnum } from "../../../../../../src/MediaDeviceHandler"; import MediaDeviceHandler, { IMediaDevices, MediaDeviceKindEnum } from "../../../../../../src/MediaDeviceHandler";
@ -56,9 +57,10 @@ describe("<VoiceUserSettingsTab />", () => {
jest.clearAllMocks(); jest.clearAllMocks();
MediaDeviceHandlerMock.hasAnyLabeledDevices.mockResolvedValue(true); MediaDeviceHandlerMock.hasAnyLabeledDevices.mockResolvedValue(true);
MediaDeviceHandlerMock.getDevices.mockResolvedValue(defaultMediaDevices); MediaDeviceHandlerMock.getDevices.mockResolvedValue(defaultMediaDevices);
MediaDeviceHandlerMock.getVideoInput.mockReturnValue(videoIn1.deviceId);
// @ts-ignore bad mocking // @ts-ignore bad mocking
MediaDeviceHandlerMock.instance = { setDevice: jest.fn() }; MediaDeviceHandlerMock.instance = { setDevice: jest.fn().mockResolvedValue(undefined) };
}); });
describe("devices", () => { describe("devices", () => {
@ -84,6 +86,29 @@ describe("<VoiceUserSettingsTab />", () => {
expect(screen.getByLabelText("Camera")).toHaveDisplayValue(videoIn2.label); expect(screen.getByLabelText("Camera")).toHaveDisplayValue(videoIn2.label);
}); });
it("logs and resets device when update fails", async () => {
// stub to avoid littering console with expected error
jest.spyOn(logger, "error").mockImplementation(() => {});
MediaDeviceHandlerMock.instance.setDevice.mockRejectedValue("oups!");
render(getComponent());
await flushPromises();
fireEvent.change(screen.getByLabelText("Camera"), { target: { value: videoIn2.deviceId } });
expect(MediaDeviceHandlerMock.instance.setDevice).toHaveBeenCalledWith(
videoIn2.deviceId,
MediaDeviceKindEnum.VideoInput,
);
expect(screen.getByLabelText("Camera")).toHaveDisplayValue(videoIn2.label);
await flushPromises();
expect(logger.error).toHaveBeenCalledWith("Failed to set device videoinput: 3");
// reset to original
expect(screen.getByLabelText("Camera")).toHaveDisplayValue(videoIn1.label);
});
it("does not render dropdown when no devices exist for type", async () => { it("does not render dropdown when no devices exist for type", async () => {
render(getComponent()); render(getComponent());
await flushPromises(); await flushPromises();