useElementFullscreen.test.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. /**
  2. * useElementFullscreen (#2976): Fullscreen API where the browser grants it,
  3. * the viewport-filling fallback everywhere else.
  4. */
  5. import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
  6. import { act, fireEvent, render, screen } from '@testing-library/react';
  7. import { useRef } from 'react';
  8. import { useElementFullscreen } from '../../hooks/useElementFullscreen';
  9. function Panel() {
  10. const ref = useRef<HTMLDivElement>(null);
  11. const { isFullscreen, toggleFullscreen, apiAvailable } = useElementFullscreen(ref);
  12. return (
  13. <div ref={ref} data-testid="panel" data-fullscreen={String(isFullscreen)} data-api={String(apiAvailable)}>
  14. <button onClick={toggleFullscreen}>toggle</button>
  15. </div>
  16. );
  17. }
  18. const requestFullscreen = vi.fn();
  19. const exitFullscreen = vi.fn();
  20. function installApi() {
  21. Object.defineProperty(document, 'fullscreenEnabled', { configurable: true, value: true });
  22. Object.defineProperty(document, 'fullscreenElement', { configurable: true, value: null, writable: true });
  23. Object.defineProperty(document, 'exitFullscreen', { configurable: true, value: exitFullscreen });
  24. Object.defineProperty(HTMLElement.prototype, 'requestFullscreen', { configurable: true, value: requestFullscreen });
  25. }
  26. function removeApi() {
  27. delete (document as { fullscreenEnabled?: boolean }).fullscreenEnabled;
  28. delete (document as { fullscreenElement?: Element | null }).fullscreenElement;
  29. delete (document as { exitFullscreen?: () => Promise<void> }).exitFullscreen;
  30. delete (HTMLElement.prototype as { requestFullscreen?: () => Promise<void> }).requestFullscreen;
  31. }
  32. // The mocks neither flip fullscreenElement nor fire fullscreenchange, so the
  33. // tests play the browser's part.
  34. function browserSetsFullscreen(element: Element | null) {
  35. (document as { fullscreenElement: Element | null }).fullscreenElement = element;
  36. act(() => {
  37. document.dispatchEvent(new Event('fullscreenchange'));
  38. });
  39. }
  40. describe('useElementFullscreen', () => {
  41. beforeEach(() => {
  42. requestFullscreen.mockReset().mockResolvedValue(undefined);
  43. exitFullscreen.mockReset().mockResolvedValue(undefined);
  44. });
  45. afterEach(() => {
  46. removeApi();
  47. });
  48. it('requests fullscreen on the element and follows fullscreenchange', async () => {
  49. installApi();
  50. render(<Panel />);
  51. const panel = screen.getByTestId('panel');
  52. expect(panel.dataset.api).toBe('true');
  53. fireEvent.click(screen.getByText('toggle'));
  54. expect(requestFullscreen).toHaveBeenCalledTimes(1);
  55. expect(requestFullscreen.mock.instances[0]).toBe(panel);
  56. // Nothing changes until the browser confirms.
  57. expect(panel.dataset.fullscreen).toBe('false');
  58. browserSetsFullscreen(panel);
  59. expect(panel.dataset.fullscreen).toBe('true');
  60. fireEvent.click(screen.getByText('toggle'));
  61. expect(exitFullscreen).toHaveBeenCalledTimes(1);
  62. browserSetsFullscreen(null);
  63. expect(panel.dataset.fullscreen).toBe('false');
  64. });
  65. it('ignores another element going fullscreen', () => {
  66. installApi();
  67. render(<Panel />);
  68. const panel = screen.getByTestId('panel');
  69. browserSetsFullscreen(document.createElement('video'));
  70. expect(panel.dataset.fullscreen).toBe('false');
  71. });
  72. it('falls back to the viewport-filling layout when the request is refused', async () => {
  73. installApi();
  74. requestFullscreen.mockRejectedValue(new TypeError('Permissions check failed'));
  75. render(<Panel />);
  76. const panel = screen.getByTestId('panel');
  77. await act(async () => {
  78. fireEvent.click(screen.getByText('toggle'));
  79. });
  80. expect(panel.dataset.fullscreen).toBe('true');
  81. // Leaving the fallback never calls the API.
  82. fireEvent.click(screen.getByText('toggle'));
  83. expect(panel.dataset.fullscreen).toBe('false');
  84. expect(exitFullscreen).not.toHaveBeenCalled();
  85. });
  86. it('falls back where the Fullscreen API is missing', () => {
  87. render(<Panel />);
  88. const panel = screen.getByTestId('panel');
  89. expect(panel.dataset.api).toBe('false');
  90. fireEvent.click(screen.getByText('toggle'));
  91. expect(panel.dataset.fullscreen).toBe('true');
  92. fireEvent.click(screen.getByText('toggle'));
  93. expect(panel.dataset.fullscreen).toBe('false');
  94. });
  95. it('leaves fullscreen when the panel unmounts while fullscreen', () => {
  96. installApi();
  97. const { unmount } = render(<Panel />);
  98. const panel = screen.getByTestId('panel');
  99. fireEvent.click(screen.getByText('toggle'));
  100. browserSetsFullscreen(panel);
  101. unmount();
  102. expect(exitFullscreen).toHaveBeenCalledTimes(1);
  103. });
  104. });