diff --git a/fluxer_app/src/features/ui/state/Tick.test.ts b/fluxer_app/src/features/ui/state/Tick.test.ts new file mode 100644 index 000000000..83b7f89a3 --- /dev/null +++ b/fluxer_app/src/features/ui/state/Tick.test.ts @@ -0,0 +1,65 @@ +// @vitest-environment happy-dom +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {autorun} from 'mobx'; +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; + +async function loadTick() { + vi.resetModules(); + const {default: Window} = await import('@app/features/window/state/Window'); + const {default: Tick} = await import('@app/features/ui/state/Tick'); + return {Tick, Window}; +} + +describe('Tick', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('keeps counting while the window is visible but not focused', async () => { + const {Tick, Window} = await loadTick(); + const dispose = autorun(() => Tick.nowSecond); + const started = Tick.nowSecond; + Window.setFocused(false); + vi.advanceTimersByTime(3000); + expect(Window.visible).toBe(true); + expect(Tick.nowSecond).toBe(started + 3); + dispose(); + }); + + it('stops counting while the window is hidden', async () => { + const {Tick, Window} = await loadTick(); + const dispose = autorun(() => Tick.nowSecond); + Window.setVisible(false); + const stopped = Tick.nowSecond; + vi.advanceTimersByTime(5000); + expect(Tick.nowSecond).toBe(stopped); + dispose(); + }); + + it('resynchronises to wall clock as soon as the window becomes visible again', async () => { + const {Tick, Window} = await loadTick(); + const dispose = autorun(() => Tick.nowSecond); + Window.setVisible(false); + const stopped = Tick.nowSecond; + vi.advanceTimersByTime(60_000); + expect(Tick.nowSecond).toBe(stopped); + Window.setVisible(true); + expect(Tick.nowSecond).toBe(stopped + 60); + dispose(); + }); + + it('does not run an interval while nothing observes the clock', async () => { + const {Tick} = await loadTick(); + const idle = Tick.nowSecond; + vi.advanceTimersByTime(5000); + expect(Tick.nowSecond).toBe(idle); + const dispose = autorun(() => Tick.nowSecond); + expect(Tick.nowSecond).toBe(idle + 5); + dispose(); + }); +}); diff --git a/fluxer_app/src/features/ui/state/Tick.ts b/fluxer_app/src/features/ui/state/Tick.ts index 38abdd424..5f2cdaa41 100644 --- a/fluxer_app/src/features/ui/state/Tick.ts +++ b/fluxer_app/src/features/ui/state/Tick.ts @@ -20,9 +20,9 @@ class TickRegistryImpl { this.stop(); }); reaction( - () => Window.focused, - (focused) => { - if (focused) this.start(); + () => Window.visible, + (visible) => { + if (visible) this.start(); else this.stop(); }, {fireImmediately: true}, @@ -30,7 +30,7 @@ class TickRegistryImpl { } private start(): void { - if (!this.observed || !Window.focused || this.intervalId !== null) return; + if (!this.observed || !Window.visible || this.intervalId !== null) return; this.tick(); this.intervalId = window.setInterval(() => this.tick(), 1000); }