cameraPopIn.test.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. /**
  2. * A camera window moved back into a Bambuddy tab as an overlay (#2737): one
  3. * tab takes it, the one it was opened from first, and with none the window
  4. * is told so it can stay open.
  5. */
  6. import { afterEach, describe, expect, it, vi } from 'vitest';
  7. import {
  8. isStandaloneWebApp,
  9. listenForCameraPopIn,
  10. readOpenerTabId,
  11. registerCameraTab,
  12. requestCameraPopIn,
  13. } from '../../utils/camera';
  14. const cleanups: Array<() => void> = [];
  15. function tab(tabId: string, canShow: (printerId: number) => Promise<boolean> = async () => true) {
  16. const open = vi.fn();
  17. cleanups.push(listenForCameraPopIn(tabId, open, canShow));
  18. return open;
  19. }
  20. afterEach(() => {
  21. cleanups.splice(0).forEach(stop => stop());
  22. sessionStorage.clear();
  23. });
  24. describe('requestCameraPopIn', () => {
  25. it('opens the camera in the tab that answers', async () => {
  26. const open = tab('t1');
  27. await expect(requestCameraPopIn(7, 'P1S', null, { answerMs: 50 })).resolves.toBe(true);
  28. expect(open).toHaveBeenCalledExactlyOnceWith(7, 'P1S');
  29. });
  30. it('reports no tab when none is open, so the window stays', async () => {
  31. await expect(requestCameraPopIn(7, 'P1S', null, { answerMs: 50 })).resolves.toBe(false);
  32. });
  33. it('opens it in one tab only when several are open', async () => {
  34. const tabs = [tab('t1'), tab('t2'), tab('t3')];
  35. await expect(requestCameraPopIn(7, 'P1S', null, { answerMs: 50 })).resolves.toBe(true);
  36. expect(tabs.map(open => open.mock.calls.length).reduce((a, b) => a + b)).toBe(1);
  37. });
  38. it('prefers the tab the window was opened from', async () => {
  39. const first = tab('t1');
  40. const opener = tab('t2');
  41. await expect(requestCameraPopIn(7, 'P1S', 't2', { answerMs: 50 })).resolves.toBe(true);
  42. expect(opener).toHaveBeenCalledExactlyOnceWith(7, 'P1S');
  43. expect(first).not.toHaveBeenCalled();
  44. });
  45. it('falls back to another tab when the one it came from is gone', async () => {
  46. const other = tab('t1');
  47. await expect(requestCameraPopIn(7, 'P1S', 'gone', { answerMs: 50 })).resolves.toBe(true);
  48. expect(other).toHaveBeenCalledExactlyOnceWith(7, 'P1S');
  49. });
  50. it('stops answering once the tab is cleaned up', async () => {
  51. const open = vi.fn();
  52. listenForCameraPopIn('t1', open)();
  53. await expect(requestCameraPopIn(7, 'P1S', null, { answerMs: 50 })).resolves.toBe(false);
  54. expect(open).not.toHaveBeenCalled();
  55. });
  56. it('opens it once when a duplicated tab answers under the same tab id', async () => {
  57. const original = tab('t1');
  58. const duplicate = tab('t1');
  59. await expect(requestCameraPopIn(7, 'P1S', 't1', { answerMs: 50 })).resolves.toBe(true);
  60. expect(original.mock.calls.length + duplicate.mock.calls.length).toBe(1);
  61. });
  62. it('is not opened by a tab that gets the claim after the window gave up', async () => {
  63. const open = tab('t1');
  64. // The window sets the deadline at 1000; the tab reads the clock long after.
  65. vi.spyOn(Date, 'now').mockReturnValueOnce(1000).mockReturnValue(10_000_000);
  66. await expect(requestCameraPopIn(7, 'P1S', null, { answerMs: 50, doneMs: 200 })).resolves.toBe(false);
  67. expect(open).not.toHaveBeenCalled();
  68. vi.restoreAllMocks();
  69. });
  70. it('moves on to another tab when the claimed one cannot show the printer', async () => {
  71. // Each tab has its own sign-in: the window's own tab may now be someone
  72. // who cannot see this printer.
  73. const refusing = tab('opener', async () => false);
  74. const other = tab('t2');
  75. await expect(requestCameraPopIn(7, 'P1S', 'opener', { answerMs: 50 })).resolves.toBe(true);
  76. expect(refusing).not.toHaveBeenCalled();
  77. expect(other).toHaveBeenCalledExactlyOnceWith(7, 'P1S');
  78. });
  79. it('stays in the window when no tab can show the printer', async () => {
  80. const refusing = tab('t1', async () => false);
  81. await expect(requestCameraPopIn(7, 'P1S', null, { answerMs: 50 })).resolves.toBe(false);
  82. expect(refusing).not.toHaveBeenCalled();
  83. });
  84. it('treats a failed check as cannot show', async () => {
  85. const failing = tab('t1', async () => {
  86. throw new Error('offline');
  87. });
  88. await expect(requestCameraPopIn(7, 'P1S', null, { answerMs: 50 })).resolves.toBe(false);
  89. expect(failing).not.toHaveBeenCalled();
  90. });
  91. it('ignores an offer for no valid printer', async () => {
  92. const open = tab('t1');
  93. await expect(requestCameraPopIn(0, 'P1S', null, { answerMs: 50 })).resolves.toBe(false);
  94. expect(open).not.toHaveBeenCalled();
  95. });
  96. });
  97. describe('the opener tab id', () => {
  98. it('is what a window opened from the tab reads back', () => {
  99. // A popup starts with a copy of its opener's sessionStorage.
  100. const tabId = registerCameraTab();
  101. expect(readOpenerTabId()).toBe(tabId);
  102. });
  103. it('stays the same across a reload, so an open window still finds its tab', () => {
  104. expect(registerCameraTab()).toBe(registerCameraTab());
  105. });
  106. it('is made fresh in a tab that has none', () => {
  107. const first = registerCameraTab();
  108. sessionStorage.clear();
  109. expect(registerCameraTab()).not.toBe(first);
  110. });
  111. });
  112. describe('isStandaloneWebApp (#3419)', () => {
  113. afterEach(() => {
  114. delete (navigator as Navigator & { standalone?: boolean }).standalone;
  115. vi.restoreAllMocks();
  116. });
  117. it('is false in a browser tab', () => {
  118. expect(isStandaloneWebApp()).toBe(false);
  119. });
  120. it('reads iOS Safari home-screen apps by navigator.standalone', () => {
  121. Object.defineProperty(navigator, 'standalone', { value: true, configurable: true });
  122. expect(isStandaloneWebApp()).toBe(true);
  123. });
  124. it('leaves an app installed on a desktop or Android alone: window.open works there', () => {
  125. vi.spyOn(window, 'matchMedia').mockImplementation(
  126. (query: string) => ({ matches: query === '(display-mode: standalone)' }) as MediaQueryList,
  127. );
  128. expect(isStandaloneWebApp()).toBe(false);
  129. });
  130. });