fix: Microphone should still work after a view change (#1835)

- Closes #1810
This commit is contained in:
Dermot Duffy
2025-01-18 14:54:40 -08:00
committed by GitHub
parent 57a687d659
commit 3003f82170
14 changed files with 186 additions and 212 deletions
@@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { MicrophoneManager } from '../../src/card-controller/microphone-manager';
import { MicrophoneState } from '../../src/card-controller/types';
import { createCardAPI, createConfig } from '../test-utils';
const navigatorMock: Navigator = {
@@ -162,7 +163,7 @@ describe('MicrophoneManager', () => {
expect(manager.isConnected()).toBeTruthy();
expect(manager.isMuted()).toBeFalsy();
expect(api.getCardElementManager().update).toBeCalledTimes(2);
expect(api.getCardElementManager().update).toBeCalled();
});
it('should disconnect', async () => {
@@ -296,95 +297,82 @@ describe('MicrophoneManager', () => {
});
});
it('should respect listeners', async () => {
const api = createCardAPI();
const manager = new MicrophoneManager(api);
vi.mocked(navigatorMock.mediaDevices.getUserMedia).mockResolvedValue(
createMockStream(),
);
const listener = vi.fn();
manager.addListener(listener);
await manager.connect();
expect(listener).not.toHaveBeenCalled();
manager.mute();
expect(listener).not.toHaveBeenCalled();
await manager.unmute();
expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenLastCalledWith('unmuted');
await manager.unmute();
expect(listener).toHaveBeenCalledTimes(1);
manager.mute();
expect(listener).toHaveBeenCalledTimes(2);
expect(listener).toHaveBeenLastCalledWith('muted');
manager.removeListener(listener);
await manager.unmute();
expect(listener).toHaveBeenCalledTimes(2);
});
it('should initialize', () => {
const api = createCardAPI();
const manager = new MicrophoneManager(api);
manager.initialize();
expect(api.getConditionsManager().setState).toBeCalledWith({
microphone: { connected: false, muted: true },
microphone: { connected: false, muted: true, forbidden: false, stream: undefined },
});
});
it('should set condition state', async () => {
it('should set state', async () => {
const api = createCardAPI();
const manager = new MicrophoneManager(api);
vi.mocked(navigatorMock.mediaDevices.getUserMedia).mockResolvedValue(
createMockStream(),
);
const stream = createMockStream();
vi.mocked(navigatorMock.mediaDevices.getUserMedia).mockResolvedValue(stream);
expect(api.getConditionsManager().setState).not.toBeCalled();
await manager.connect();
let expectedState: MicrophoneState = {
forbidden: false,
stream: stream,
connected: true,
muted: true,
};
expect(manager.getState()).toEqual(expectedState);
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
expect.objectContaining({
microphone: {
connected: true,
muted: true,
},
microphone: expectedState,
}),
);
await manager.unmute();
expectedState = {
forbidden: false,
stream: stream,
connected: true,
muted: false,
};
expect(manager.getState()).toEqual(expectedState);
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
expect.objectContaining({
microphone: {
connected: true,
muted: false,
},
microphone: expectedState,
}),
);
manager.mute();
expectedState = {
forbidden: false,
stream: stream,
connected: true,
muted: true,
};
expect(manager.getState()).toEqual(expectedState);
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
expect.objectContaining({
microphone: {
connected: true,
muted: true,
},
microphone: expectedState,
}),
);
manager.disconnect();
expectedState = {
forbidden: false,
stream: undefined,
connected: false,
muted: true,
};
expect(manager.getState()).toEqual(expectedState);
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
expect.objectContaining({
microphone: {
connected: false,
muted: true,
},
microphone: expectedState,
}),
);
});
@@ -1,8 +1,5 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import {
MicrophoneManagerListenerChange,
ReadonlyMicrophoneManager,
} from '../../src/card-controller/microphone-manager';
import { MicrophoneState } from '../../src/card-controller/types';
import {
MediaActionsController,
MediaActionsControllerOptions,
@@ -17,7 +14,6 @@ import {
flushPromises,
} from '../test-utils';
import { callVisibilityHandler, createTestSlideNodes } from '../utils/embla/test-utils';
import { mock } from 'vitest-mock-extended';
const getPlayer = (
element: HTMLElement,
@@ -50,15 +46,6 @@ const createPlayerSlideNodes = (n = 10): HTMLElement[] => {
return divs;
};
const callMicrophoneListener = (
microphoneManager: ReadonlyMicrophoneManager,
action: MicrophoneManagerListenerChange,
n = 0,
): void => {
const mock = vi.mocked(microphoneManager.addListener).mock;
mock.calls[n][0](action);
};
// @vitest-environment jsdom
describe('MediaActionsController', () => {
beforeAll(() => {
@@ -541,7 +528,7 @@ describe('MediaActionsController', () => {
);
});
describe('should take action on microphone changes', () => {
describe('should take action on microphone state changes', () => {
beforeAll(() => {
vi.useFakeTimers();
});
@@ -550,14 +537,24 @@ describe('MediaActionsController', () => {
vi.useRealTimers();
});
const createMicrophoneState = (
state?: Partial<MicrophoneState>,
): MicrophoneState => {
return {
muted: true,
forbidden: false,
connected: false,
...state,
};
};
it('should unmute when microphone unmuted', async () => {
const microphoneManager = mock<ReadonlyMicrophoneManager>();
const controller = new MediaActionsController();
controller.setOptions({
autoUnmuteConditions: ['microphone' as const],
playerSelector: 'video',
microphoneManager: microphoneManager,
microphoneState: createMicrophoneState({ muted: true }),
});
const children = createPlayerSlideNodes();
@@ -565,19 +562,22 @@ describe('MediaActionsController', () => {
await controller.setTarget(0, true);
callMicrophoneListener(microphoneManager, 'unmuted');
controller.setOptions({
autoUnmuteConditions: ['microphone' as const],
playerSelector: 'video',
microphoneState: createMicrophoneState({ muted: false }),
});
expect(getPlayer(children[0], 'video')?.unmute).toBeCalled();
});
it('should re-mute after delay after microphone unmuted', async () => {
const microphoneManager = mock<ReadonlyMicrophoneManager>();
it('should mute after delay after microphone muted', async () => {
const controller = new MediaActionsController();
controller.setOptions({
autoMuteConditions: ['microphone' as const],
playerSelector: 'video',
microphoneManager: microphoneManager,
microphoneState: createMicrophoneState({ muted: false }),
});
const children = createPlayerSlideNodes();
@@ -585,21 +585,24 @@ describe('MediaActionsController', () => {
await controller.setTarget(0, true);
callMicrophoneListener(microphoneManager, 'muted');
controller.setOptions({
autoMuteConditions: ['microphone' as const],
playerSelector: 'video',
microphoneState: createMicrophoneState({ muted: true }),
});
vi.runOnlyPendingTimers();
expect(getPlayer(children[0], 'video')?.mute).toBeCalled();
});
it('should not re-mute after delay after microphone unmuted', async () => {
const microphoneManager = mock<ReadonlyMicrophoneManager>();
it('should not mute after delay after microphone muted', async () => {
const controller = new MediaActionsController();
controller.setOptions({
autoMuteConditions: [],
playerSelector: 'video',
microphoneManager: microphoneManager,
microphoneState: createMicrophoneState({ muted: false }),
});
const children = createPlayerSlideNodes();
@@ -607,7 +610,11 @@ describe('MediaActionsController', () => {
await controller.setTarget(0, true);
callMicrophoneListener(microphoneManager, 'muted');
controller.setOptions({
autoMuteConditions: ['microphone' as const],
playerSelector: 'video',
microphoneState: createMicrophoneState({ muted: true }),
});
vi.runOnlyPendingTimers();