Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions projects/core/browser/fullscreen/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,4 +304,109 @@ describe(fullscreen.name, () => {
expect(component.fs.isActive()).toBe(true);
});
});

describe('concurrent transitions', () => {
// Replaces the instant mock with a controllable one: the test decides when the browser
// finishes the transition (resolves the native promise and fires `fullscreenchange`).
let finishRequest: () => void;

beforeEach(() => {
requestFullscreenSpy.mockImplementation(
() =>
new Promise<void>(resolve => {
finishRequest = () => {
fullscreenElementValue = document.documentElement;
document.dispatchEvent(new Event('fullscreenchange'));
resolve();
};
})
);
});

it('should not issue a second native request while enter is pending', async () => {
const component = createComponent();

const first = component.fs.enter();
const second = component.fs.enter();

finishRequest();
await Promise.all([first, second]);

expect(requestFullscreenSpy).toHaveBeenCalledTimes(1);
});

it('should make exit join a pending enter instead of racing it', async () => {
const component = createComponent();

const entering = component.fs.enter();
const exiting = component.fs.exit();

finishRequest();
await Promise.all([entering, exiting]);

expect(exitFullscreenSpy).not.toHaveBeenCalled();
expect(component.fs.isActive()).toBe(true);
});

it('should enter exactly once on a double toggle', async () => {
const component = createComponent();

const first = component.fs.toggle();
const second = component.fs.toggle();

finishRequest();
await Promise.all([first, second]);

expect(requestFullscreenSpy).toHaveBeenCalledTimes(1);
expect(exitFullscreenSpy).not.toHaveBeenCalled();
expect(component.fs.isActive()).toBe(true);
});

it('should allow a new transition once the pending one settles', async () => {
const component = createComponent();

const entering = component.fs.enter();
finishRequest();
await entering;

await component.fs.exit();

expect(exitFullscreenSpy).toHaveBeenCalledTimes(1);
});

it('should release the pending transition when the native request rejects', async () => {
const component = createComponent();

requestFullscreenSpy.mockRejectedValueOnce(new TypeError('Permissions check failed'));

await expect(component.fs.enter()).rejects.toThrow('Permissions check failed');

const retry = component.fs.enter();
finishRequest();
await retry;

expect(requestFullscreenSpy).toHaveBeenCalledTimes(2);
expect(component.fs.isActive()).toBe(true);
});

it('should propagate the pending rejection to every joined caller', async () => {
const component = createComponent();

let rejectRequest: (error: Error) => void;
requestFullscreenSpy.mockImplementationOnce(
() =>
new Promise<void>((_, reject) => {
rejectRequest = reject;
})
);

const first = component.fs.enter();
const second = component.fs.enter();

rejectRequest!(new TypeError('Permissions check failed'));

await expect(first).rejects.toThrow('Permissions check failed');
await expect(second).rejects.toThrow('Permissions check failed');
});
});
});
30 changes: 26 additions & 4 deletions projects/core/browser/fullscreen/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,23 +118,45 @@ export function fullscreen(options?: FullscreenOptions): FullscreenRef {
return current != null && current === toElement(target);
});

let transition: Promise<void> | null = null;

const runTransition = (startFn: () => Promise<void>): Promise<void> => {
const result = startFn().finally(() => {
transition = null;
});
transition = result;
return result;
};

const enter = async (): Promise<void> => {
if (transition) {
return transition;
}

const el = toElement.untracked(target);
ngDevMode && assertElement(el, 'fullscreen');
if (getFullscreenElement(document) !== el) {
await el?.requestFullscreen();
if (el && getFullscreenElement(document) !== el) {
await runTransition(() => el.requestFullscreen());
}
};

const exit = async (): Promise<void> => {
if (transition) {
return transition;
}

const el = toElement.untracked(target);
ngDevMode && assertElement(el, 'fullscreen');
if (getFullscreenElement(document) === el) {
await document.exitFullscreen();
if (el && getFullscreenElement(document) === el) {
await runTransition(() => document.exitFullscreen());
}
};

const toggle = async (): Promise<void> => {
if (transition) {
return transition;
}

if (untracked(isActive)) {
await exit();
} else {
Expand Down
Loading