2026-06-02 00:43:05 +03:00
|
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
|
|
|
|
|
|
import { createPushRuntime } from './push-runtime.js';
|
|
|
|
|
|
|
|
|
|
const createRuntime = () => createPushRuntime({
|
|
|
|
|
fsPromises: {
|
|
|
|
|
mkdir: vi.fn(async () => {}),
|
|
|
|
|
readFile: vi.fn(async () => JSON.stringify({ version: 1, subscriptionsBySession: {} })),
|
|
|
|
|
writeFile: vi.fn(async () => {}),
|
|
|
|
|
},
|
|
|
|
|
path: { dirname: () => '/tmp' },
|
|
|
|
|
webPush: {
|
|
|
|
|
generateVAPIDKeys: vi.fn(() => ({ publicKey: 'public', privateKey: 'private' })),
|
|
|
|
|
sendNotification: vi.fn(async () => {}),
|
|
|
|
|
setVapidDetails: vi.fn(),
|
|
|
|
|
},
|
|
|
|
|
PUSH_SUBSCRIPTIONS_FILE_PATH: '/tmp/push-subscriptions.json',
|
|
|
|
|
readSettingsFromDiskMigrated: vi.fn(async () => ({})),
|
|
|
|
|
writeSettingsToDisk: vi.fn(async () => {}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
vi.useRealTimers();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('push runtime visibility tracking', () => {
|
|
|
|
|
it('keeps visible UI state when another client reports hidden', () => {
|
|
|
|
|
vi.useFakeTimers();
|
|
|
|
|
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'));
|
|
|
|
|
|
|
|
|
|
const runtime = createRuntime();
|
|
|
|
|
|
|
|
|
|
runtime.updateUiVisibility('visible-client', true);
|
|
|
|
|
runtime.updateUiVisibility('hidden-client', false);
|
|
|
|
|
|
|
|
|
|
expect(runtime.isAnyUiVisible()).toBe(true);
|
|
|
|
|
expect(runtime.isUiVisible('visible-client')).toBe(true);
|
|
|
|
|
expect(runtime.isUiVisible('hidden-client')).toBe(false);
|
|
|
|
|
|
|
|
|
|
vi.advanceTimersByTime(30_001);
|
|
|
|
|
|
|
|
|
|
expect(runtime.isAnyUiVisible()).toBe(false);
|
|
|
|
|
expect(runtime.isUiVisible('visible-client')).toBe(false);
|
|
|
|
|
});
|
2026-07-01 09:55:41 +03:00
|
|
|
|
|
|
|
|
it('treats only mobile platforms as non-interactive for isAnyInteractiveClientVisible', () => {
|
|
|
|
|
vi.useFakeTimers();
|
|
|
|
|
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'));
|
|
|
|
|
|
|
|
|
|
const runtime = createRuntime();
|
|
|
|
|
|
|
|
|
|
// Only the phone (foreground) is connected → no interactive client to absorb the notification.
|
|
|
|
|
runtime.updateUiVisibility('phone', true, 'ios');
|
|
|
|
|
expect(runtime.isAnyUiVisible()).toBe(true);
|
|
|
|
|
expect(runtime.isAnyInteractiveClientVisible()).toBe(false);
|
|
|
|
|
|
|
|
|
|
// A visible desktop counts as interactive → suppress mobile push.
|
|
|
|
|
runtime.updateUiVisibility('desktop', true, 'desktop');
|
|
|
|
|
expect(runtime.isAnyInteractiveClientVisible()).toBe(true);
|
|
|
|
|
|
|
|
|
|
// Desktop hidden again → back to mobile-only, push should flow to the phone.
|
|
|
|
|
runtime.updateUiVisibility('desktop', false, 'desktop');
|
|
|
|
|
expect(runtime.isAnyInteractiveClientVisible()).toBe(false);
|
|
|
|
|
|
|
|
|
|
// A client that never reported a platform is treated as interactive (conservative).
|
|
|
|
|
runtime.updateUiVisibility('legacy', true);
|
|
|
|
|
expect(runtime.isAnyInteractiveClientVisible()).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('remembers the last platform when a heartbeat omits it', () => {
|
|
|
|
|
vi.useFakeTimers();
|
|
|
|
|
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'));
|
|
|
|
|
|
|
|
|
|
const runtime = createRuntime();
|
|
|
|
|
runtime.updateUiVisibility('phone', true, 'android');
|
|
|
|
|
runtime.updateUiVisibility('phone', true); // heartbeat without platform
|
|
|
|
|
expect(runtime.isAnyInteractiveClientVisible()).toBe(false);
|
|
|
|
|
});
|
2026-06-02 00:43:05 +03:00
|
|
|
});
|