feat: Add basic fullscreen support for iPhone (#1857)

- Closes #1444
This commit is contained in:
Dermot Duffy
2025-01-27 07:23:07 -08:00
committed by GitHub
parent d1afbc95b6
commit 3e8e688b36
44 changed files with 1150 additions and 308 deletions
@@ -6,11 +6,13 @@ import {
evaluateConditionViaEvent,
getOverriddenConfig,
} from '../../src/card-controller/conditions-manager';
import { MicrophoneState } from '../../src/card-controller/types';
import { FrigateCardCondition } from '../../src/config/types';
import {
createCardAPI,
createCondition,
createConfig,
createMediaLoadedInfo,
createStateEntity,
createUser,
} from '../test-utils';
@@ -780,9 +782,9 @@ describe('ConditionsManager', () => {
const manager = new ConditionsManager(createCardAPI());
const conditions = [{ condition: 'media_loaded' as const, media_loaded: true }];
expect(manager.evaluateConditions(conditions)).toBeFalsy();
manager.setState({ media_loaded: true });
manager.setState({ mediaLoadedInfo: createMediaLoadedInfo() });
expect(manager.evaluateConditions(conditions)).toBeTruthy();
manager.setState({ media_loaded: false });
manager.setState({ mediaLoadedInfo: null });
expect(manager.evaluateConditions(conditions)).toBeFalsy();
});
@@ -898,17 +900,27 @@ describe('ConditionsManager', () => {
});
describe('with microphone condition', () => {
const createMicrophoneState = (
state: Partial<MicrophoneState>,
): MicrophoneState => {
return {
connected: false,
muted: false,
forbidden: false,
...state,
};
};
it('empty', () => {
const manager = new ConditionsManager(createCardAPI());
const conditions = [{ condition: 'microphone' as const }];
expect(manager.evaluateConditions(conditions)).toBeTruthy();
manager.setState({ microphone: { connected: true } });
manager.setState({ microphone: createMicrophoneState({ connected: true }) });
expect(manager.evaluateConditions(conditions)).toBeTruthy();
manager.setState({ microphone: { connected: false } });
manager.setState({ microphone: createMicrophoneState({ connected: false }) });
expect(manager.evaluateConditions(conditions)).toBeTruthy();
manager.setState({ microphone: { muted: true } });
manager.setState({ microphone: createMicrophoneState({ muted: true }) });
expect(manager.evaluateConditions(conditions)).toBeTruthy();
manager.setState({ microphone: { muted: false } });
manager.setState({ microphone: createMicrophoneState({ muted: false }) });
expect(manager.evaluateConditions(conditions)).toBeTruthy();
});
@@ -916,9 +928,9 @@ describe('ConditionsManager', () => {
const manager = new ConditionsManager(createCardAPI());
const conditions = [{ condition: 'microphone' as const, connected: true }];
expect(manager.evaluateConditions(conditions)).toBeFalsy();
manager.setState({ microphone: { connected: true } });
manager.setState({ microphone: createMicrophoneState({ connected: true }) });
expect(manager.evaluateConditions(conditions)).toBeTruthy();
manager.setState({ microphone: { connected: false } });
manager.setState({ microphone: createMicrophoneState({ connected: false }) });
expect(manager.evaluateConditions(conditions)).toBeFalsy();
});
@@ -926,9 +938,9 @@ describe('ConditionsManager', () => {
const manager = new ConditionsManager(createCardAPI());
const conditions = [{ condition: 'microphone' as const, connected: false }];
expect(manager.evaluateConditions(conditions)).toBeFalsy();
manager.setState({ microphone: { connected: true } });
manager.setState({ microphone: createMicrophoneState({ connected: true }) });
expect(manager.evaluateConditions(conditions)).toBeFalsy();
manager.setState({ microphone: { connected: false } });
manager.setState({ microphone: createMicrophoneState({ connected: false }) });
expect(manager.evaluateConditions(conditions)).toBeTruthy();
});
@@ -936,9 +948,9 @@ describe('ConditionsManager', () => {
const manager = new ConditionsManager(createCardAPI());
const conditions = [{ condition: 'microphone' as const, muted: true }];
expect(manager.evaluateConditions(conditions)).toBeFalsy();
manager.setState({ microphone: { muted: true } });
manager.setState({ microphone: createMicrophoneState({ muted: true }) });
expect(manager.evaluateConditions(conditions)).toBeTruthy();
manager.setState({ microphone: { muted: false } });
manager.setState({ microphone: createMicrophoneState({ muted: false }) });
expect(manager.evaluateConditions(conditions)).toBeFalsy();
});
@@ -946,9 +958,9 @@ describe('ConditionsManager', () => {
const manager = new ConditionsManager(createCardAPI());
const conditions = [{ condition: 'microphone' as const, muted: false }];
expect(manager.evaluateConditions(conditions)).toBeFalsy();
manager.setState({ microphone: { muted: true } });
manager.setState({ microphone: createMicrophoneState({ muted: true }) });
expect(manager.evaluateConditions(conditions)).toBeFalsy();
manager.setState({ microphone: { muted: false } });
manager.setState({ microphone: createMicrophoneState({ muted: false }) });
expect(manager.evaluateConditions(conditions)).toBeTruthy();
});
@@ -958,13 +970,17 @@ describe('ConditionsManager', () => {
{ condition: 'microphone' as const, muted: false, connected: true },
];
expect(manager.evaluateConditions(conditions)).toBeFalsy();
manager.setState({ microphone: { muted: true } });
manager.setState({ microphone: createMicrophoneState({ muted: true }) });
expect(manager.evaluateConditions(conditions)).toBeFalsy();
manager.setState({ microphone: { muted: false } });
manager.setState({ microphone: createMicrophoneState({ muted: false }) });
expect(manager.evaluateConditions(conditions)).toBeFalsy();
manager.setState({ microphone: { connected: false, muted: false } });
manager.setState({
microphone: createMicrophoneState({ connected: false, muted: false }),
});
expect(manager.evaluateConditions(conditions)).toBeFalsy();
manager.setState({ microphone: { connected: true, muted: false } });
manager.setState({
microphone: createMicrophoneState({ connected: true, muted: false }),
});
expect(manager.evaluateConditions(conditions)).toBeTruthy();
});
});
@@ -1033,11 +1049,11 @@ describe('ConditionsManager', () => {
const conditions = [{ condition: 'user_agent' as const, user_agent: userAgent }];
expect(manager.evaluateConditions(conditions)).toBeFalsy();
manager.setState({
user_agent: userAgent,
userAgent: userAgent,
});
expect(manager.evaluateConditions(conditions)).toBeTruthy();
manager.setState({
user_agent: 'Something else',
userAgent: 'Something else',
});
expect(manager.evaluateConditions(conditions)).toBeFalsy();
});
@@ -1049,11 +1065,11 @@ describe('ConditionsManager', () => {
];
expect(manager.evaluateConditions(conditions)).toBeFalsy();
manager.setState({
user_agent: userAgent,
userAgent: userAgent,
});
expect(manager.evaluateConditions(conditions)).toBeTruthy();
manager.setState({
user_agent: 'Something else',
userAgent: 'Something else',
});
expect(manager.evaluateConditions(conditions)).toBeFalsy();
});
@@ -1063,11 +1079,11 @@ describe('ConditionsManager', () => {
const conditions = [{ condition: 'user_agent' as const, companion: true }];
expect(manager.evaluateConditions(conditions)).toBeFalsy();
manager.setState({
user_agent: 'Home Assistant/',
userAgent: 'Home Assistant/',
});
expect(manager.evaluateConditions(conditions)).toBeTruthy();
manager.setState({
user_agent: userAgent,
userAgent: userAgent,
});
expect(manager.evaluateConditions(conditions)).toBeFalsy();
});
@@ -1084,14 +1100,37 @@ describe('ConditionsManager', () => {
];
expect(manager.evaluateConditions(conditions)).toBeFalsy();
manager.setState({
user_agent: 'Home Assistant/',
userAgent: 'Home Assistant/',
});
expect(manager.evaluateConditions(conditions)).toBeTruthy();
manager.setState({
user_agent: 'Something else',
userAgent: 'Something else',
});
expect(manager.evaluateConditions(conditions)).toBeFalsy();
});
});
});
it('should add listener', () => {
const listener = vi.fn();
const manager = new ConditionsManager(createCardAPI());
manager.addListener(listener);
const state = { fullscreen: true };
manager.setState(state);
expect(listener).toBeCalledWith(state, {});
});
it('should remove listener', () => {
const listener = vi.fn();
const manager = new ConditionsManager(createCardAPI());
manager.addListener(listener);
manager.removeListener(listener);
const state = { fullscreen: true };
manager.setState(state);
expect(listener).not.toBeCalled();
});
});
+2 -2
View File
@@ -13,7 +13,7 @@ import { CardController } from '../../src/card-controller/controller';
import { DefaultManager } from '../../src/card-controller/default-manager';
import { DownloadManager } from '../../src/card-controller/download-manager';
import { ExpandManager } from '../../src/card-controller/expand-manager';
import { FullscreenManager } from '../../src/card-controller/fullscreen-manager';
import { FullscreenManager } from '../../src/card-controller/fullscreen/fullscreen-manager';
import { HASSManager } from '../../src/card-controller/hass/hass-manager';
import { InitializationManager } from '../../src/card-controller/initialization-manager';
import { InteractionManager } from '../../src/card-controller/interaction-manager';
@@ -42,7 +42,7 @@ vi.mock('../../src/card-controller/config/config-manager');
vi.mock('../../src/card-controller/default-manager');
vi.mock('../../src/card-controller/download-manager');
vi.mock('../../src/card-controller/expand-manager');
vi.mock('../../src/card-controller/fullscreen-manager');
vi.mock('../../src/card-controller/fullscreen/fullscreen-manager');
vi.mock('../../src/card-controller/hass/hass-manager');
vi.mock('../../src/card-controller/initialization-manager');
vi.mock('../../src/card-controller/interaction-manager');
+2 -2
View File
@@ -25,7 +25,7 @@ describe('ExpandManager', () => {
manager.setExpanded(true);
expect(manager.isExpanded()).toBeTruthy();
expect(api.getFullscreenManager().stopFullscreen).toBeCalled();
expect(api.getFullscreenManager().setFullscreen).toBeCalledWith(false);
expect(api.getConditionsManager().setState).toBeCalledWith({ expand: true });
expect(api.getCardElementManager().update).toBeCalled();
});
@@ -37,7 +37,7 @@ describe('ExpandManager', () => {
manager.setExpanded(true);
expect(api.getFullscreenManager().stopFullscreen).not.toBeCalled();
expect(api.getFullscreenManager().setFullscreen).not.toBeCalled();
});
it('should toggle expanded', () => {
@@ -1,138 +0,0 @@
import screenfull from 'screenfull';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { FullscreenManager } from '../../src/card-controller/fullscreen-manager';
import { createCardAPI } from '../test-utils';
vi.mock('screenfull', () => ({
default: {
exit: vi.fn(),
toggle: vi.fn(),
off: vi.fn(),
on: vi.fn(),
},
}));
const setScreenfulEnabled = (enabled: boolean): void => {
Object.defineProperty(screenfull, 'isEnabled', { value: enabled, writable: true });
};
const setScreenfulFullscreen = (fullscreen: boolean): void => {
Object.defineProperty(screenfull, 'isFullscreen', {
value: fullscreen,
writable: true,
});
};
// @vitest-environment jsdom
describe('FullscreenManager', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should initialize', () => {
const api = createCardAPI();
const manager = new FullscreenManager(api);
setScreenfulEnabled(true);
setScreenfulFullscreen(false);
manager.initialize();
expect(api.getConditionsManager().setState).toBeCalledWith({ fullscreen: false });
});
it('should correctly determine whether in fullscreen', () => {
const manager = new FullscreenManager(createCardAPI());
setScreenfulEnabled(true);
setScreenfulFullscreen(true);
expect(manager.isInFullscreen()).toBeTruthy();
setScreenfulFullscreen(false);
expect(manager.isInFullscreen()).toBeFalsy();
});
it('should toggle fullscreen', () => {
const toggle = vi.mocked(screenfull.toggle);
const element = document.createElement('div');
const api = createCardAPI();
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
const manager = new FullscreenManager(api);
manager.toggleFullscreen();
expect(toggle).toBeCalledWith(element);
});
it('should stop fullscreen', () => {
const manager = new FullscreenManager(createCardAPI());
const exit = vi.mocked(screenfull.exit);
manager.stopFullscreen();
expect(exit).toBeCalled();
});
it('should disconnect', () => {
const manager = new FullscreenManager(createCardAPI());
const off = vi.mocked(screenfull.off);
setScreenfulEnabled(true);
manager.disconnect();
expect(off).toBeCalledWith('change', expect.anything());
});
it('should not disconnect when screenfull disabled', () => {
const manager = new FullscreenManager(createCardAPI());
const off = vi.mocked(screenfull.off);
setScreenfulEnabled(false);
manager.disconnect();
expect(off).not.toBeCalled();
});
it('should connect', () => {
const manager = new FullscreenManager(createCardAPI());
const on = vi.mocked(screenfull.on);
setScreenfulEnabled(true);
manager.connect();
expect(on).toBeCalledWith('change', expect.anything());
});
it('should not connect when screenfull disabled', () => {
const manager = new FullscreenManager(createCardAPI());
const on = vi.mocked(screenfull.on);
setScreenfulEnabled(false);
manager.connect();
expect(on).not.toBeCalled();
});
it('should make correct api calls on fullscreen change', () => {
const api = createCardAPI();
const manager = new FullscreenManager(api);
const on = vi.mocked(screenfull.on);
setScreenfulEnabled(true);
setScreenfulFullscreen(true);
manager.connect();
expect(on).toBeCalled();
on.mock.calls[0][1](new Event('fullscreen'));
expect(api.getExpandManager().setExpanded).toBeCalledWith(false);
expect(api.getConditionsManager().setState).toBeCalledWith({
fullscreen: true,
});
expect(api.getCardElementManager().update).toBeCalled();
});
});
@@ -0,0 +1,54 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { FullscreenProviderFactory } from '../../../src/card-controller/fullscreen/factory';
import { ScreenfullFullScreenProvider } from '../../../src/card-controller/fullscreen/screenfull';
import { WebkitFullScreenProvider } from '../../../src/card-controller/fullscreen/webkit';
import { WebkitHTMLVideoElement } from '../../../src/types';
import { createCardAPI, setScreenfulEnabled } from '../../test-utils';
// @vitest-environment jsdom
describe('FullscreenProviderFactory', () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it('should return screenful when enabled', () => {
setScreenfulEnabled(true);
expect(FullscreenProviderFactory.create(createCardAPI(), vi.fn())).toBeInstanceOf(
ScreenfullFullScreenProvider,
);
});
it('should return webkit when enabled', () => {
setScreenfulEnabled(false);
const element = document.createElement('video') as HTMLVideoElement &
Partial<WebkitHTMLVideoElement>;
element['webkitEnterFullscreen'] = vi.fn();
const stubDocument = mock<Document>();
stubDocument.createElement.mockReturnValue(element);
vi.stubGlobal('document', stubDocument);
expect(FullscreenProviderFactory.create(createCardAPI(), vi.fn())).toBeInstanceOf(
WebkitFullScreenProvider,
);
});
it('should return null without any provider', () => {
setScreenfulEnabled(false);
const element = document.createElement('video') as HTMLVideoElement &
Partial<WebkitHTMLVideoElement>;
element['webkitEnterFullscreen'] = undefined;
const stubDocument = mock<Document>();
stubDocument.createElement.mockReturnValue(element);
vi.stubGlobal('document', stubDocument);
expect(FullscreenProviderFactory.create(createCardAPI(), vi.fn())).toBeNull();
});
});
@@ -0,0 +1,149 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { FullscreenProviderFactory } from '../../../src/card-controller/fullscreen/factory';
import { FullscreenManager } from '../../../src/card-controller/fullscreen/fullscreen-manager';
import { FullscreenProvider } from '../../../src/card-controller/fullscreen/types';
import { createCardAPI } from '../../test-utils';
vi.mock('../../../src/card-controller/fullscreen/factory');
describe('FullscreenManager', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('should initialize', () => {
const api = createCardAPI();
const manager = new FullscreenManager(api, mock<FullscreenProvider>());
manager.initialize();
expect(api.getConditionsManager().setState).toBeCalledWith({ fullscreen: false });
});
it('should correctly determine whether in fullscreen', () => {
const provider = mock<FullscreenProvider>();
const manager = new FullscreenManager(createCardAPI(), provider);
provider.isInFullscreen.mockReturnValue(true);
expect(manager.isInFullscreen()).toBeTruthy();
provider.isInFullscreen.mockReturnValue(false);
expect(manager.isInFullscreen()).toBeFalsy();
});
describe('describe toggle fullscreen', () => {
it.each([
[true, false],
[false, true],
])('%s -> %s', async (state: boolean, expected: boolean) => {
const provider = mock<FullscreenProvider>();
provider.isInFullscreen.mockReturnValue(state);
const manager = new FullscreenManager(createCardAPI(), provider);
manager.toggleFullscreen();
expect(provider.setFullscreen).toBeCalledWith(expected);
});
});
describe('describe set fullscreen', () => {
it.each([[true], [false]])('%s', async (fullscreen: boolean) => {
const provider = mock<FullscreenProvider>();
const manager = new FullscreenManager(createCardAPI(), provider);
manager.setFullscreen(fullscreen);
expect(provider.setFullscreen).toBeCalledWith(fullscreen);
});
});
it('should connect', () => {
const provider = mock<FullscreenProvider>();
const manager = new FullscreenManager(createCardAPI(), provider);
manager.connect();
expect(provider.connect).toBeCalled();
});
it('should disconnect', () => {
const provider = mock<FullscreenProvider>();
const manager = new FullscreenManager(createCardAPI(), provider);
manager.disconnect();
expect(provider.disconnect).toBeCalled();
});
describe('should confirm whether fullscreen is supported', () => {
beforeEach(() => {
vi.stubGlobal('navigator', { userAgent: 'foo' });
});
afterEach(() => {
vi.restoreAllMocks();
});
it('when being casted', () => {
vi.stubGlobal('navigator', { userAgent: 'CrKey/' });
const api = createCardAPI();
const provider = mock<FullscreenProvider>();
provider.isSupported.mockReturnValue(true);
const manager = new FullscreenManager(api, provider);
expect(manager.isSupported()).toBeFalsy();
});
describe('should return provider support', () => {
it.each([[true], [false]])('%s', async (support: boolean) => {
const api = createCardAPI();
const provider = mock<FullscreenProvider>();
provider.isSupported.mockReturnValue(support);
const manager = new FullscreenManager(api, provider);
expect(manager.isSupported()).toBe(support);
});
});
it('without a provider', () => {
vi.mocked(FullscreenProviderFactory).create.mockReturnValue(null);
const manager = new FullscreenManager(createCardAPI());
expect(manager.isSupported()).toBeFalsy();
});
});
describe('should handle state change', () => {
it.each([[true], [false]])('%s', async (fullscreen: boolean) => {
const api = createCardAPI();
const provider = mock<FullscreenProvider>();
vi.mocked(FullscreenProviderFactory.create).mockReturnValue(provider);
new FullscreenManager(api);
const handler = vi.mocked(FullscreenProviderFactory.create).mock.calls[0][1];
provider.isInFullscreen.mockReturnValue(fullscreen);
handler();
expect(api.getConditionsManager().setState).toBeCalledWith({
fullscreen: fullscreen,
});
});
});
it('should create provider from factory', () => {
const api = createCardAPI();
new FullscreenManager(api);
expect(FullscreenProviderFactory.create).toBeCalledWith(api, expect.anything());
});
});
@@ -0,0 +1,162 @@
import screenfull from 'screenfull';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { ScreenfullFullScreenProvider } from '../../../../src/card-controller/fullscreen/screenfull';
import { createCardAPI, setScreenfulEnabled } from '../../../test-utils';
vi.mock('screenfull', () => ({
default: {
exit: vi.fn(),
request: vi.fn(),
off: vi.fn(),
on: vi.fn(),
},
}));
const setScreenfulFullscreen = (fullscreen: boolean): void => {
Object.defineProperty(screenfull, 'isFullscreen', {
value: fullscreen,
writable: true,
});
};
// @vitest-environment jsdom
describe('ScreenfullFullScreenProvider', () => {
afterEach(() => {
vi.restoreAllMocks();
});
describe('should connect', () => {
it('should connect if enabled', () => {
const provider = new ScreenfullFullScreenProvider(createCardAPI(), vi.fn());
const on = vi.mocked(screenfull.on);
setScreenfulEnabled(true);
provider.connect();
expect(on).toBeCalledWith('change', expect.anything());
});
it('should not connect if not enabled', () => {
const provider = new ScreenfullFullScreenProvider(createCardAPI(), vi.fn());
const on = vi.mocked(screenfull.on);
setScreenfulEnabled(false);
provider.connect();
expect(on).not.toBeCalled();
});
});
describe('should disconnect', () => {
it('should disconnect if enabled', () => {
const provider = new ScreenfullFullScreenProvider(createCardAPI(), vi.fn());
const off = vi.mocked(screenfull.off);
setScreenfulEnabled(true);
provider.disconnect();
expect(off).toBeCalledWith('change', expect.anything());
});
it('should not disconnect if not enabled', () => {
const provider = new ScreenfullFullScreenProvider(createCardAPI(), vi.fn());
const off = vi.mocked(screenfull.off);
setScreenfulEnabled(false);
provider.disconnect();
expect(off).not.toBeCalled();
});
});
describe('should return if in fullscreen', () => {
it('should return true if in fullscreen', () => {
const provider = new ScreenfullFullScreenProvider(createCardAPI(), vi.fn());
setScreenfulEnabled(true);
setScreenfulFullscreen(true);
expect(provider.isInFullscreen()).toBeTruthy();
});
it('should return false if not in fullscreen', () => {
const provider = new ScreenfullFullScreenProvider(createCardAPI(), vi.fn());
setScreenfulEnabled(true);
setScreenfulFullscreen(false);
expect(provider.isInFullscreen()).toBeFalsy();
});
it('should return false if not supported', () => {
const provider = new ScreenfullFullScreenProvider(createCardAPI(), vi.fn());
setScreenfulEnabled(false);
setScreenfulFullscreen(true);
expect(provider.isInFullscreen()).toBeFalsy();
});
});
describe('should return if supported', () => {
it('should return true if supported', () => {
const provider = new ScreenfullFullScreenProvider(createCardAPI(), vi.fn());
setScreenfulEnabled(true);
expect(provider.isSupported()).toBeTruthy();
});
it('should return false if not supported', () => {
const provider = new ScreenfullFullScreenProvider(createCardAPI(), vi.fn());
setScreenfulEnabled(false);
expect(provider.isSupported()).toBeFalsy();
});
});
describe('should set fullscreen', () => {
it('should request fullscreen if fullscreen is true', () => {
setScreenfulEnabled(true);
const api = createCardAPI();
const element = document.createElement('div');
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
const provider = new ScreenfullFullScreenProvider(api, vi.fn());
provider.setFullscreen(true);
expect(screenfull.request).toBeCalledWith(element);
});
it('should exit fullscreen if fullscreen is false', () => {
setScreenfulEnabled(true);
const provider = new ScreenfullFullScreenProvider(createCardAPI(), vi.fn());
provider.setFullscreen(false);
expect(screenfull.exit).toBeCalled();
});
it('should take no action if not supported', () => {
setScreenfulEnabled(false);
const api = createCardAPI();
const element = document.createElement('div');
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
const provider = new ScreenfullFullScreenProvider(api, vi.fn());
provider.setFullscreen(true);
provider.setFullscreen(false);
expect(screenfull.request).not.toBeCalled();
expect(screenfull.exit).not.toBeCalled();
});
});
});
@@ -0,0 +1,236 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { CardController } from '../../../../src/card-controller/controller';
import { WebkitFullScreenProvider } from '../../../../src/card-controller/fullscreen/webkit';
import { FrigateCardMediaPlayer, WebkitHTMLVideoElement } from '../../../../src/types';
import { createCardAPI, createMediaLoadedInfo } from '../../../test-utils';
const createWebkitVideoElement = (): HTMLVideoElement &
Partial<WebkitHTMLVideoElement> => {
return document.createElement('video');
};
const setElement = (api: CardController, element: HTMLElement): void => {
const player = mock<FrigateCardMediaPlayer>();
player.getFullscreenElement.mockReturnValue(element);
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
createMediaLoadedInfo({
player: player,
}),
);
};
// @vitest-environment jsdom
describe('WebkitFullScreenProvider', () => {
afterEach(() => {
vi.restoreAllMocks();
vi.useRealTimers();
});
it('should connect', () => {
const api = createCardAPI();
const provider = new WebkitFullScreenProvider(api, vi.fn());
provider.connect();
expect(api.getConditionsManager().addListener).toBeCalledWith(expect.anything());
});
it('should disconnect', () => {
const api = createCardAPI();
const provider = new WebkitFullScreenProvider(api, vi.fn());
provider.disconnect();
expect(api.getConditionsManager().removeListener).toBeCalledWith(expect.anything());
});
describe('should return if in fullscreen', () => {
it.each([[true], [false]])('%s', async (fullscreen: boolean) => {
const api = createCardAPI();
const provider = new WebkitFullScreenProvider(api, vi.fn());
const element = createWebkitVideoElement();
element.webkitDisplayingFullscreen = fullscreen;
setElement(api, element);
expect(provider.isInFullscreen()).toBe(fullscreen);
});
});
describe('should return if supported', () => {
it.each([[true], [false]])('%s', async (supported: boolean) => {
const api = createCardAPI();
const provider = new WebkitFullScreenProvider(api, vi.fn());
const element = createWebkitVideoElement();
element.webkitSupportsFullscreen = supported;
setElement(api, element);
expect(provider.isSupported()).toBe(supported);
});
});
describe('should set fullscreen', () => {
it('should request fullscreen if fullscreen is true', () => {
const api = createCardAPI();
const provider = new WebkitFullScreenProvider(api, vi.fn());
const element = createWebkitVideoElement();
element.webkitEnterFullscreen = vi.fn();
element.webkitSupportsFullscreen = true;
setElement(api, element);
provider.setFullscreen(true);
expect(element.webkitEnterFullscreen).toBeCalled();
});
it('should exit fullscreen if fullscreen is true', () => {
const api = createCardAPI();
const provider = new WebkitFullScreenProvider(api, vi.fn());
const element = createWebkitVideoElement();
element.webkitExitFullscreen = vi.fn();
element.webkitSupportsFullscreen = true;
setElement(api, element);
provider.setFullscreen(false);
expect(element.webkitExitFullscreen).toBeCalled();
});
it('should take no action if not supported', () => {
const api = createCardAPI();
const provider = new WebkitFullScreenProvider(api, vi.fn());
const element = createWebkitVideoElement();
element.webkitEnterFullscreen = vi.fn();
element.webkitExitFullscreen = vi.fn();
element.webkitSupportsFullscreen = false;
setElement(api, element);
provider.setFullscreen(true);
provider.setFullscreen(false);
expect(element.webkitEnterFullscreen).not.toBeCalled();
expect(element.webkitExitFullscreen).not.toBeCalled();
});
it('should take no action if element is not a video', () => {
const api = createCardAPI();
const provider = new WebkitFullScreenProvider(api, vi.fn());
const element = document.createElement('img') as HTMLImageElement &
Partial<WebkitHTMLVideoElement>;
element.webkitEnterFullscreen = vi.fn();
element.webkitExitFullscreen = vi.fn();
setElement(api, element);
provider.setFullscreen(true);
provider.setFullscreen(false);
expect(element.webkitEnterFullscreen).not.toBeCalled();
expect(element.webkitExitFullscreen).not.toBeCalled();
});
});
describe('should handle state changes', () => {
describe('should call handler when fullscreen state changes', () => {
it.each([['webkitbeginfullscreen'], ['webkitendfullscreen']])(
'%s',
(event: string) => {
const handler = vi.fn();
const api = createCardAPI();
const provider = new WebkitFullScreenProvider(api, handler);
provider.connect();
const conditionChangeHandler = vi.mocked(
api.getConditionsManager().addListener,
).mock.calls[0][0];
const element_1 = createWebkitVideoElement();
const player_1 = mock<FrigateCardMediaPlayer>();
player_1.getFullscreenElement.mockReturnValue(element_1);
const mediaLoadedInfo_1 = createMediaLoadedInfo({ player: player_1 });
conditionChangeHandler({ mediaLoadedInfo: mediaLoadedInfo_1 });
element_1.dispatchEvent(new Event(event));
expect(handler).toBeCalledTimes(1);
const element_2 = createWebkitVideoElement();
const player_2 = mock<FrigateCardMediaPlayer>();
player_2.getFullscreenElement.mockReturnValue(element_2);
const mediaLoadedInfo_2 = createMediaLoadedInfo({ player: player_2 });
conditionChangeHandler(
{ mediaLoadedInfo: mediaLoadedInfo_2 },
{ mediaLoadedInfo: mediaLoadedInfo_1 },
);
element_2.dispatchEvent(new Event(event));
expect(handler).toBeCalledTimes(2);
// Events on the old element should be ignored.
element_1.dispatchEvent(new Event(event));
expect(handler).toBeCalledTimes(2);
// Test the media loaded info not changing.
conditionChangeHandler(
{ mediaLoadedInfo: mediaLoadedInfo_2 },
{ mediaLoadedInfo: mediaLoadedInfo_2 },
);
// Events on the new element should still be handled.
element_2.dispatchEvent(new Event(event));
expect(handler).toBeCalledTimes(3);
},
);
});
});
it('should play the video after fullscreen ends', () => {
vi.useFakeTimers();
const handler = vi.fn();
const api = createCardAPI();
const provider = new WebkitFullScreenProvider(api, handler);
provider.connect();
const conditionChangeHandler = vi.mocked(api.getConditionsManager().addListener).mock
.calls[0][0];
const element = createWebkitVideoElement();
element.play = vi.fn();
setElement(api, element);
const player = mock<FrigateCardMediaPlayer>();
player.getFullscreenElement.mockReturnValue(element);
const mediaLoadedInfo = createMediaLoadedInfo({ player });
conditionChangeHandler({ mediaLoadedInfo });
element.dispatchEvent(new Event('webkitendfullscreen'));
expect(element.play).not.toBeCalled();
vi.runOnlyPendingTimers();
expect(element.play).toBeCalled();
});
});
@@ -8,7 +8,9 @@ describe('MediaLoadedInfoManager', () => {
const manager = new MediaLoadedInfoManager(api);
manager.initialize();
expect(api.getConditionsManager().setState).toBeCalledWith({ media_loaded: false });
expect(api.getConditionsManager().setState).toBeCalledWith({
mediaLoadedInfo: null,
});
});
it('should set', () => {
@@ -21,7 +23,7 @@ describe('MediaLoadedInfoManager', () => {
expect(manager.has()).toBeTruthy();
expect(manager.get()).toBe(mediaInfo);
expect(api.getConditionsManager().setState).toBeCalledWith(
expect.objectContaining({ media_loaded: true }),
expect.objectContaining({ mediaLoadedInfo: mediaInfo }),
);
expect(api.getStyleManager().setExpandedMode).toBeCalled();
expect(api.getCardElementManager().update).toBeCalled();
@@ -30,9 +32,9 @@ describe('MediaLoadedInfoManager', () => {
it('should not set invalid media info', () => {
const api = createCardAPI();
const manager = new MediaLoadedInfoManager(api);
const mediaInfo = createMediaLoadedInfo({ width: 0, height: 0 });
const mediaLoadedInfo = createMediaLoadedInfo({ width: 0, height: 0 });
manager.set(mediaInfo);
manager.set(mediaLoadedInfo);
expect(manager.has()).toBeFalsy();
expect(manager.get()).toBeNull();
@@ -42,18 +44,18 @@ describe('MediaLoadedInfoManager', () => {
it('should get last known', () => {
const api = createCardAPI();
const manager = new MediaLoadedInfoManager(api);
const mediaInfo = createMediaLoadedInfo();
const mediaLoadedInfo = createMediaLoadedInfo();
manager.set(mediaInfo);
manager.set(mediaLoadedInfo);
expect(manager.has()).toBeTruthy();
manager.clear();
expect(manager.has()).toBeFalsy();
expect(manager.getLastKnown()).toBe(mediaInfo);
expect(manager.getLastKnown()).toBe(mediaLoadedInfo);
expect(api.getConditionsManager().setState).toBeCalledWith(
expect.objectContaining({ media_loaded: false }),
expect.objectContaining({ mediaLoadedInfo }),
);
});
});