From 19dcc73598e6c5e9f65549161989b07f73cc52b3 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Tue, 25 Nov 2025 15:06:07 -0800 Subject: [PATCH 01/11] test(Bell): permission denied; set custom colors, add css to head --- src/page/bell/Bell.test.ts | 73 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/page/bell/Bell.test.ts b/src/page/bell/Bell.test.ts index 90f742d3f..f2dc548f9 100644 --- a/src/page/bell/Bell.test.ts +++ b/src/page/bell/Bell.test.ts @@ -1,4 +1,5 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; +import { vi } from 'vitest'; import OneSignalEvent from '../../shared/services/OneSignalEvent'; import Bell from './Bell'; import { BellEvent, BellState } from './constants'; @@ -47,4 +48,76 @@ describe('Bell', () => { to: BellState._Subscribed, }); }); + + test('_updateState sets blocked when permission denied', async () => { + const bell = new Bell({ enable: false }); + const permSpy = vi + .spyOn(OneSignal._context._permissionManager, '_getPermissionStatus') + .mockResolvedValue('denied'); + const enabledSpy = vi + .spyOn( + OneSignal._context._subscriptionManager, + '_isPushNotificationsEnabled', + ) + .mockResolvedValue(false); + bell._updateState(); + await Promise.resolve(); + await Promise.resolve(); + expect(bell._blocked).toBe(true); + expect(permSpy).toHaveBeenCalled(); + expect(enabledSpy).toHaveBeenCalled(); + }); + + test('_setCustomColorsIfSpecified applies styles and adds CSS to head', async () => { + const bell = new Bell({ enable: false }); + document.body.innerHTML = ` +
+
+ + + + + +
+
+
+
+
+ +
+
+
+ `; + bell._options.colors = { + 'circle.background': '#111', + 'circle.foreground': '#222', + 'badge.background': '#333', + 'badge.bordercolor': '#444', + 'badge.foreground': '#555', + 'dialog.button.background': '#666', + 'dialog.button.foreground': '#777', + 'dialog.button.background.hovering': '#888', + 'dialog.button.background.active': '#999', + 'pulse.color': '#abc', + }; + bell._setCustomColorsIfSpecified(); + const background = document.querySelector('.background')!; + expect(background.getAttribute('style')).toContain('#111'); + const badge = document.querySelector( + '.onesignal-bell-launcher-badge', + )!; + expect(badge.getAttribute('style')).toContain('rgb(51, 51, 51)'); + const styleHover = document.getElementById( + 'onesignal-background-hover-style', + ); + expect(styleHover).not.toBeNull(); + }); + + test('_addCssToHead appends once', () => { + const bell = new Bell({ enable: false }); + bell._addCssToHead('x', '.a{color:red}'); + bell._addCssToHead('x', '.b{color:blue}'); + const style = document.getElementById('x')!; + expect(style.textContent).toContain('.a{color:red}'); + }); }); From 708fe48a10153bc60c9b506057a20e88c3aa0e69 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Tue, 25 Nov 2025 15:07:35 -0800 Subject: [PATCH 02/11] test(PromptsManager): show slidedown when user interaction required --- src/page/managers/PromptsManager.test.ts | 40 ++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/page/managers/PromptsManager.test.ts b/src/page/managers/PromptsManager.test.ts index af6a3e04b..4b8c13474 100644 --- a/src/page/managers/PromptsManager.test.ts +++ b/src/page/managers/PromptsManager.test.ts @@ -1,5 +1,6 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { setupLoadStylesheet } from '__test__/support/helpers/setup'; +import { DelayedPromptType } from 'src/shared/prompts/constants'; import { Browser } from 'src/shared/useragent/constants'; import * as detect from 'src/shared/useragent/detect'; import { PromptsManager } from './PromptsManager'; @@ -52,4 +53,43 @@ describe('PromptsManager', () => { await pm['_internalShowSlidedownPrompt'](); expect(installSpy).toHaveBeenCalledTimes(1); }); + + test('_internalShowDelayedPrompt forces slidedown when interaction required', async () => { + requiresUserInteractionSpy.mockReturnValue(true); + const pm = new PromptsManager(OneSignal._context); + const nativeSpy = vi + .spyOn(pm, '_internalShowNativePrompt') + .mockResolvedValue(true); + const slidedownSpy = vi + .spyOn(pm, '_internalShowSlidedownPrompt') + .mockResolvedValue(undefined); + await pm._internalShowDelayedPrompt(DelayedPromptType._Native, 0); + expect(nativeSpy).not.toHaveBeenCalled(); + expect(slidedownSpy).toHaveBeenCalled(); + }); + + test('_spawnAutoPrompts triggers native when condition met and not forced', async () => { + const pm = new PromptsManager(OneSignal._context); + const getOptsSpy = vi + .spyOn(pm, '_getDelayedPromptOptions') + .mockReturnValue({ + enabled: true, + autoPrompt: true, + timeDelay: 0, + pageViews: 0, + }); + const condSpy = vi + .spyOn(pm, '_isPageViewConditionMet') + .mockReturnValue(true); + const delayedSpy = vi + .spyOn(pm, '_internalShowDelayedPrompt') + .mockResolvedValue(undefined); + requiresUserInteractionSpy.mockReturnValue(false); + getBrowserNameSpy.mockReturnValue(Browser._Chrome); + getBrowserVersionSpy.mockReturnValue(62); + await pm._spawnAutoPrompts(); + expect(getOptsSpy).toHaveBeenCalled(); + expect(condSpy).toHaveBeenCalled(); + expect(delayedSpy).toHaveBeenCalledWith(DelayedPromptType._Native, 0); + }); }); From 35ae9e98202abd858a7ebe4e47a01f673e2eddc4 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Tue, 25 Nov 2025 15:10:43 -0800 Subject: [PATCH 03/11] test(SessionManager): handle visibility changes; handleOnFocus/handleOnBlur --- .../sessionManager/SessionManager.test.ts | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/src/shared/managers/sessionManager/SessionManager.test.ts b/src/shared/managers/sessionManager/SessionManager.test.ts index 22c7bb8de..5dcb572ed 100644 --- a/src/shared/managers/sessionManager/SessionManager.test.ts +++ b/src/shared/managers/sessionManager/SessionManager.test.ts @@ -5,8 +5,9 @@ import LoginManager from 'src/page/managers/LoginManager'; import * as detect from 'src/shared/environment/detect'; import Log from 'src/shared/libraries/Log'; import { SessionOrigin } from 'src/shared/session/constants'; +import { vi } from 'vitest'; +import User from '../../../onesignal/User'; import { SessionManager } from './SessionManager'; - const supportsServiceWorkersSpy = vi.spyOn(detect, 'supportsServiceWorkers'); vi.spyOn(Log, '_error').mockImplementation(() => ''); @@ -176,5 +177,61 @@ describe('SessionManager', () => { await sm._upsertSession(SessionOrigin._UserCreate); expect(emitSpy).toHaveBeenCalledWith(OneSignal.EVENTS.SESSION_STARTED); }); + + test('_handleVisibilityChange visible triggers upsert; hidden triggers deactivate and removes listeners', async () => { + const sm = new SessionManager(OneSignal._context); + // ensure user present + User._createOrGetInstance(); + + vi.spyOn(sm as any, '_getOneSignalAndSubscriptionIds').mockResolvedValue({ + onesignalId: 'o', + subscriptionId: 's', + }); + + // visible and focused + const visSpy = vi + .spyOn(document, 'visibilityState', 'get') + .mockReturnValue('visible' as DocumentVisibilityState); + const focusSpy = vi.spyOn(document, 'hasFocus').mockReturnValue(true); + const upsertSpy = vi + .spyOn(sm as any, '_notifySWToUpsertSession') + .mockResolvedValue(undefined); + await sm._handleVisibilityChange(); + expect(upsertSpy).toHaveBeenCalled(); + visSpy.mockRestore(); + focusSpy.mockRestore(); + + // hidden path removes listeners + vi.spyOn(document, 'visibilityState', 'get').mockReturnValue( + 'hidden' as DocumentVisibilityState, + ); + const deactSpy = vi + .spyOn(sm as any, '_notifySWToDeactivateSession') + .mockResolvedValue(undefined); + OneSignal._cache.isFocusEventSetup = true; + OneSignal._cache.isBlurEventSetup = true; + OneSignal._cache.focusHandler = () => undefined; + OneSignal._cache.blurHandler = () => undefined; + await sm._handleVisibilityChange(); + expect(deactSpy).toHaveBeenCalled(); + expect(OneSignal._cache.isFocusEventSetup).toBe(false); + expect(OneSignal._cache.isBlurEventSetup).toBe(false); + }); + + test('_handleOnFocus/Blur target guard prevents duplicate', async () => { + const sm = new SessionManager(OneSignal._context); + // ensure user present + User._createOrGetInstance(); + const upsertSpy = vi + .spyOn(sm as any, '_notifySWToUpsertSession') + .mockResolvedValue(undefined); + const deactSpy = vi + .spyOn(sm as any, '_notifySWToDeactivateSession') + .mockResolvedValue(undefined); + await sm._handleOnFocus(new Event('focus')); + await sm._handleOnBlur(new Event('blur')); + expect(upsertSpy).not.toHaveBeenCalled(); + expect(deactSpy).not.toHaveBeenCalled(); + }); }); }); From 19b983a215b99a17767cffaa02d34202151d240a Mon Sep 17 00:00:00 2001 From: sherwinski Date: Tue, 25 Nov 2025 15:13:48 -0800 Subject: [PATCH 04/11] test(CustomLinkManager): add test coverage - disabled config and stylesheet failure return - hiding containers when subscribed and `unsubscribeEnable: false` - injecting markup and click flow (optIn then optOut) with text and class checks --- src/shared/managers/CustomLinkManager.test.ts | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 src/shared/managers/CustomLinkManager.test.ts diff --git a/src/shared/managers/CustomLinkManager.test.ts b/src/shared/managers/CustomLinkManager.test.ts new file mode 100644 index 000000000..720548ea4 --- /dev/null +++ b/src/shared/managers/CustomLinkManager.test.ts @@ -0,0 +1,105 @@ +import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; +import { setupLoadStylesheet } from '__test__/support/helpers/setup'; +import { + CUSTOM_LINK_CSS_CLASSES, + CUSTOM_LINK_CSS_SELECTORS, +} from 'src/shared/slidedown/constants'; +import { expect, vi } from 'vitest'; +import { ResourceLoadState } from '../../page/services/DynamicResourceLoader'; +import { CustomLinkManager } from './CustomLinkManager'; + +describe('CustomLinkManager', () => { + beforeEach(() => { + TestEnvironment.initialize(); + document.body.innerHTML = ` + + `; + }); + + test('_initialize returns when disabled or stylesheet fails', async () => { + const mgrDisabled = new CustomLinkManager({ enabled: false }); + await expect(mgrDisabled._initialize()).resolves.toBeUndefined(); + + // Stylesheet not loaded + const mgr = new CustomLinkManager({ + enabled: true, + text: { explanation: 'x', subscribe: 'Sub' }, + }); + const loadSdkStylesheetSpy = vi + .spyOn(OneSignal._context._dynamicResourceLoader, '_loadSdkStylesheet') + .mockResolvedValue(ResourceLoadState._Failed); + await mgr._initialize(); + // nothing injected + const containers = document.querySelectorAll( + CUSTOM_LINK_CSS_SELECTORS._ContainerSelector, + ); + expect(containers.length).toBe(1); + expect(containers[0].children.length).toBe(0); + }); + + test('_initialize hides containers when subscribed and unsubscribe disabled', async () => { + await setupLoadStylesheet(); + vi.spyOn( + OneSignal._context._subscriptionManager, + '_isPushNotificationsEnabled', + ).mockResolvedValue(true); + const mgr = new CustomLinkManager({ + enabled: true, + unsubscribeEnabled: false, + text: { + explanation: 'hello', + subscribe: 'Subscribe', + unsubscribe: 'Unsubscribe', + }, + }); + await mgr._initialize(); + const containers = document.querySelectorAll( + CUSTOM_LINK_CSS_SELECTORS._ContainerSelector, + ); + expect( + containers[0].classList.contains(CUSTOM_LINK_CSS_CLASSES._Hide), + ).toBe(true); + }); + + test('_initialize injects markup and click toggles subscription', async () => { + await setupLoadStylesheet(); + vi.spyOn( + OneSignal._context._subscriptionManager, + '_isPushNotificationsEnabled', + ).mockResolvedValue(false); + const optInSpy = vi + .spyOn(OneSignal.User.PushSubscription, 'optIn') + .mockResolvedValue(); + const optOutSpy = vi + .spyOn(OneSignal.User.PushSubscription, 'optOut') + .mockResolvedValue(); + const mgr = new CustomLinkManager({ + enabled: true, + unsubscribeEnabled: true, + text: { + explanation: 'hello', + subscribe: 'Subscribe', + unsubscribe: 'Unsubscribe', + }, + style: 'button', + size: 'medium', + color: { text: '#fff', button: '#000' }, + }); + await mgr._initialize(); + const button = document.querySelector( + `.${CUSTOM_LINK_CSS_CLASSES._SubscribeClass}`, + ); + expect(button).not.toBeNull(); + expect(button?.textContent).toBe('Subscribe'); + + await button?.click(); + expect(optInSpy).toHaveBeenCalled(); + + // simulate subscribed now (set optedIn getter) + vi.spyOn(OneSignal.User.PushSubscription, 'optedIn', 'get').mockReturnValue( + true, + ); + await button?.click(); + expect(optOutSpy).toHaveBeenCalled(); + }); +}); From 8b724210822f504897572838e6fd573794b791e5 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Tue, 25 Nov 2025 15:16:49 -0800 Subject: [PATCH 05/11] test(ServiceWorkerManager): add test coverage - Different cases where `_getActiveState` returns None, OneSignal, or Third-Party sw - `_haveParamsChanged` branches (no registration, scope mismatch, missing scriptUrl, href changes, same href) - `_shoulInstallWorker` branches (SW support, OneSignal config, state, params changed, needs update) - `_workerNeedsUpdate` where comparing versions --- .../managers/ServiceWorkerManager.test.ts | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 src/shared/managers/ServiceWorkerManager.test.ts diff --git a/src/shared/managers/ServiceWorkerManager.test.ts b/src/shared/managers/ServiceWorkerManager.test.ts new file mode 100644 index 000000000..350f05e6b --- /dev/null +++ b/src/shared/managers/ServiceWorkerManager.test.ts @@ -0,0 +1,131 @@ +import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; +import * as detect from 'src/shared/environment/detect'; +import * as helpers from 'src/shared/helpers/service-worker'; +import Path from 'src/shared/models/Path'; +import { VERSION } from 'src/shared/utils/env'; +import * as registration from 'src/sw/helpers/registration'; +import { vi } from 'vitest'; +import Log from '../libraries/Log'; +import { ServiceWorkerManager } from './ServiceWorkerManager'; + +describe('ServiceWorkerManager', () => { + const config = { + workerPath: new Path('/OneSignalSDKWorker.js'), + registrationOptions: { scope: '/' }, + } as const; + + beforeEach(() => { + TestEnvironment.initialize(); + vi.restoreAllMocks(); + }); + + test('_getActiveState returns None when no registration', async () => { + vi.spyOn(registration, 'getSWRegistration').mockResolvedValue( + undefined as any, + ); + const mgr = new ServiceWorkerManager(OneSignal._context, config); + await expect(mgr._getActiveState()).resolves.toBe( + helpers.ServiceWorkerActiveState._None, + ); + }); + + test('_getActiveState detects OneSignal vs third-party from file name', async () => { + const fakeReg = {} as ServiceWorkerRegistration; + vi.spyOn(registration, 'getSWRegistration').mockResolvedValue(fakeReg); + vi.spyOn(registration, 'getAvailableServiceWorker').mockReturnValue({ + scriptURL: new URL( + 'https://example.com/OneSignalSDKWorker.js', + ).toString(), + }); + const mgr = new ServiceWorkerManager(OneSignal._context, config); + await expect(mgr._getActiveState()).resolves.toBe( + helpers.ServiceWorkerActiveState._OneSignalWorker, + ); + + // third-party + registration.getAvailableServiceWorker.mockReturnValue({ + scriptURL: new URL('https://example.com/othersw.js').toString(), + }); + await expect(mgr._getActiveState()).resolves.toBe( + helpers.ServiceWorkerActiveState._ThirdParty, + ); + }); + + test('_haveParamsChanged covers no registration, scope diff, missing scriptURL, href diff and same', async () => { + const mgr = new ServiceWorkerManager(OneSignal._context, config); + // no registration + vi.spyOn(mgr, '_getRegistration').mockResolvedValue(undefined); + await expect(mgr._haveParamsChanged()).resolves.toBe(true); + + // registration with different scope + const regWithScope = { + scope: 'https://example.com/oldPath', + } as ServiceWorkerRegistration; + mgr._getRegistration.mockResolvedValue(regWithScope); + const infoSpy = vi.spyOn(Log, '_info').mockImplementation(() => undefined); + await expect(mgr._haveParamsChanged()).resolves.toBe(true); + expect(infoSpy).toHaveBeenCalled(); + + // same scope but no worker scriptURL + mgr._getRegistration.mockResolvedValue({ scope: 'https://example.com/' }); + vi.spyOn(registration, 'getAvailableServiceWorker').mockReturnValue( + undefined, + ); + await expect(mgr._haveParamsChanged()).resolves.toBe(true); + + // different href + registration.getAvailableServiceWorker.mockReturnValue({ + scriptURL: 'https://example.com/old.js', + }); + vi.spyOn(helpers, 'getServiceWorkerHref').mockReturnValue( + 'https://example.com/new.js', + ); + await expect(mgr._haveParamsChanged()).resolves.toBe(true); + + // same href + ( + helpers.getServiceWorkerHref as unknown as import('vitest').SpyInstance + ).mockReturnValue('https://example.com/old.js'); + await expect(mgr._haveParamsChanged()).resolves.toBe(false); + }); + + test('_shouldInstallWorker branches', async () => { + const mgr = new ServiceWorkerManager(OneSignal._context, config); + + vi.spyOn(detect, 'supportsServiceWorkers').mockReturnValue(false); + await expect(mgr._shouldInstallWorker()).resolves.toBe(false); + + detect.supportsServiceWorkers.mockReturnValue(true); + const savedConfig = OneSignal.config; + OneSignal.config = undefined; + await expect(mgr._shouldInstallWorker()).resolves.toBe(false); + OneSignal.config = savedConfig; + + vi.spyOn(mgr, '_getActiveState').mockResolvedValue( + helpers.ServiceWorkerActiveState._None, + ); + vi.spyOn( + OneSignal._context._permissionManager, + '_getNotificationPermission', + ).mockResolvedValue('granted'); + await expect(mgr._shouldInstallWorker()).resolves.toBe(true); + + mgr._getActiveState.mockResolvedValue( + helpers.ServiceWorkerActiveState._OneSignalWorker, + ); + vi.spyOn(mgr, '_haveParamsChanged').mockResolvedValue(true); + await expect(mgr._shouldInstallWorker()).resolves.toBe(true); + + mgr._haveParamsChanged.mockResolvedValue(false); + vi.spyOn(mgr, '_workerNeedsUpdate').mockResolvedValue(false); + await expect(mgr._shouldInstallWorker()).resolves.toBe(false); + }); + + test('_workerNeedsUpdate compares versions', async () => { + const mgr = new ServiceWorkerManager(OneSignal._context, config); + vi.spyOn(mgr, '_getWorkerVersion').mockResolvedValue('0.0.1'); + await expect(mgr._workerNeedsUpdate()).resolves.toBe(true); + mgr._getWorkerVersion.mockResolvedValue(VERSION); + await expect(mgr._workerNeedsUpdate()).resolves.toBe(false); + }); +}); From 9d30e5953d64b0f99b8cd5af91cf03af266e5108 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Tue, 25 Nov 2025 15:18:32 -0800 Subject: [PATCH 06/11] test(UpdateManager): add test coverage - `_sendPushDeviceRecordUpdate` early return vs triggering `sendOnSessionUpdate` - `_sendOnSessionUpdate` branches: already sent, not first page, not registered, unsubscribed skip, subscribed path schedules upsert and sets flag --- src/shared/managers/UpdateManager.test.ts | 73 +++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/shared/managers/UpdateManager.test.ts diff --git a/src/shared/managers/UpdateManager.test.ts b/src/shared/managers/UpdateManager.test.ts new file mode 100644 index 000000000..59d4add24 --- /dev/null +++ b/src/shared/managers/UpdateManager.test.ts @@ -0,0 +1,73 @@ +import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; +import User from 'src/onesignal/User'; +import * as pageview from 'src/shared/helpers/pageview'; +import { SessionOrigin } from 'src/shared/session/constants'; +import { NotificationType } from 'src/shared/subscriptions/constants'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { UpdateManager } from './UpdateManager'; + +describe('UpdateManager', () => { + beforeEach(() => { + TestEnvironment.initialize(); + vi.restoreAllMocks(); + }); + + test('_sendPushDeviceRecordUpdate early returns with no user, otherwise calls _sendOnSessionUpdate once', async () => { + const mgr = new UpdateManager(OneSignal._context); + // No user + User._singletonInstance = undefined; + const spy = vi.spyOn(mgr, '_sendOnSessionUpdate').mockResolvedValue(); + await mgr._sendPushDeviceRecordUpdate(); + expect(spy).not.toHaveBeenCalled(); + + // With user present and first call triggers onSession + User._singletonInstance = { onesignalId: 'id' }; + await mgr._sendPushDeviceRecordUpdate(); + expect(spy).toHaveBeenCalledTimes(1); + }); + + test('_sendOnSessionUpdate flows: already sent, not first page, not registered, skip when unsubscribed, success path sets flag', async () => { + const mgr = new UpdateManager(OneSignal._context); + // Already sent + mgr._onSessionSent = true; + await mgr._sendOnSessionUpdate(); + // reset + mgr._onSessionSent = false; + + // Not first page + vi.spyOn(pageview, 'isFirstPageView').mockReturnValue(false); + await mgr._sendOnSessionUpdate(); + pageview.isFirstPageView.mockReturnValue(true); + + // Not registered with OneSignal + vi.spyOn( + OneSignal._context._subscriptionManager, + '_isAlreadyRegisteredWithOneSignal', + ).mockResolvedValue(false); + await mgr._sendOnSessionUpdate(); + + // Registered but not subscribed and enableOnSession not true -> skip + OneSignal._context._subscriptionManager._isAlreadyRegisteredWithOneSignal.mockResolvedValue( + true, + ); + vi.spyOn( + OneSignal._coreDirector, + '_getPushSubscriptionModel', + ).mockResolvedValue({ + _notification_types: 0, + }); + const upsertSpy = vi + .spyOn(OneSignal._context._sessionManager, '_upsertSession') + .mockResolvedValue(); + await mgr._sendOnSessionUpdate(); + expect(upsertSpy).not.toHaveBeenCalled(); + + // Subscribed path + OneSignal._coreDirector._getPushSubscriptionModel.mockResolvedValue({ + _notification_types: NotificationType._Subscribed, + id: 'sub', + }); + await mgr._sendOnSessionUpdate(); + expect(upsertSpy).toHaveBeenCalledWith(SessionOrigin._UserNewSession); + }); +}); From c33860486c5c5a0e1a93581b8e858c8caf993bcb Mon Sep 17 00:00:00 2001 From: sherwinski Date: Tue, 25 Nov 2025 15:49:07 -0800 Subject: [PATCH 07/11] test: fix build errors - first pass --- src/page/managers/PromptsManager.test.ts | 36 ++++- src/shared/managers/CustomLinkManager.test.ts | 7 +- .../managers/ServiceWorkerManager.test.ts | 123 +++++++++++------- src/shared/managers/UpdateManager.test.ts | 44 ++++--- 4 files changed, 134 insertions(+), 76 deletions(-) diff --git a/src/page/managers/PromptsManager.test.ts b/src/page/managers/PromptsManager.test.ts index 4b8c13474..e195d32d4 100644 --- a/src/page/managers/PromptsManager.test.ts +++ b/src/page/managers/PromptsManager.test.ts @@ -61,8 +61,14 @@ describe('PromptsManager', () => { .spyOn(pm, '_internalShowNativePrompt') .mockResolvedValue(true); const slidedownSpy = vi - .spyOn(pm, '_internalShowSlidedownPrompt') - .mockResolvedValue(undefined); + .spyOn( + PromptsManager.prototype as unknown as Record< + string, + (...args: unknown[]) => unknown + >, + '_internalShowSlidedownPrompt', + ) + .mockResolvedValue(undefined as void); await pm._internalShowDelayedPrompt(DelayedPromptType._Native, 0); expect(nativeSpy).not.toHaveBeenCalled(); expect(slidedownSpy).toHaveBeenCalled(); @@ -71,7 +77,13 @@ describe('PromptsManager', () => { test('_spawnAutoPrompts triggers native when condition met and not forced', async () => { const pm = new PromptsManager(OneSignal._context); const getOptsSpy = vi - .spyOn(pm, '_getDelayedPromptOptions') + .spyOn( + PromptsManager.prototype as unknown as Record< + string, + (...args: unknown[]) => unknown + >, + '_getDelayedPromptOptions', + ) .mockReturnValue({ enabled: true, autoPrompt: true, @@ -79,11 +91,23 @@ describe('PromptsManager', () => { pageViews: 0, }); const condSpy = vi - .spyOn(pm, '_isPageViewConditionMet') + .spyOn( + PromptsManager.prototype as unknown as Record< + string, + (...args: unknown[]) => unknown + >, + '_isPageViewConditionMet', + ) .mockReturnValue(true); const delayedSpy = vi - .spyOn(pm, '_internalShowDelayedPrompt') - .mockResolvedValue(undefined); + .spyOn( + PromptsManager.prototype as unknown as Record< + string, + (...args: unknown[]) => unknown + >, + '_internalShowDelayedPrompt', + ) + .mockResolvedValue(undefined as void); requiresUserInteractionSpy.mockReturnValue(false); getBrowserNameSpy.mockReturnValue(Browser._Chrome); getBrowserVersionSpy.mockReturnValue(62); diff --git a/src/shared/managers/CustomLinkManager.test.ts b/src/shared/managers/CustomLinkManager.test.ts index 720548ea4..e885d4cc4 100644 --- a/src/shared/managers/CustomLinkManager.test.ts +++ b/src/shared/managers/CustomLinkManager.test.ts @@ -25,9 +25,10 @@ describe('CustomLinkManager', () => { enabled: true, text: { explanation: 'x', subscribe: 'Sub' }, }); - const loadSdkStylesheetSpy = vi - .spyOn(OneSignal._context._dynamicResourceLoader, '_loadSdkStylesheet') - .mockResolvedValue(ResourceLoadState._Failed); + vi.spyOn( + OneSignal._context._dynamicResourceLoader, + '_loadSdkStylesheet', + ).mockResolvedValue(ResourceLoadState._Failed); await mgr._initialize(); // nothing injected const containers = document.querySelectorAll( diff --git a/src/shared/managers/ServiceWorkerManager.test.ts b/src/shared/managers/ServiceWorkerManager.test.ts index 350f05e6b..111529425 100644 --- a/src/shared/managers/ServiceWorkerManager.test.ts +++ b/src/shared/managers/ServiceWorkerManager.test.ts @@ -2,7 +2,6 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import * as detect from 'src/shared/environment/detect'; import * as helpers from 'src/shared/helpers/service-worker'; import Path from 'src/shared/models/Path'; -import { VERSION } from 'src/shared/utils/env'; import * as registration from 'src/sw/helpers/registration'; import { vi } from 'vitest'; import Log from '../libraries/Log'; @@ -20,9 +19,7 @@ describe('ServiceWorkerManager', () => { }); test('_getActiveState returns None when no registration', async () => { - vi.spyOn(registration, 'getSWRegistration').mockResolvedValue( - undefined as any, - ); + vi.spyOn(registration, 'getSWRegistration').mockResolvedValue(undefined); const mgr = new ServiceWorkerManager(OneSignal._context, config); await expect(mgr._getActiveState()).resolves.toBe( helpers.ServiceWorkerActiveState._None, @@ -32,20 +29,29 @@ describe('ServiceWorkerManager', () => { test('_getActiveState detects OneSignal vs third-party from file name', async () => { const fakeReg = {} as ServiceWorkerRegistration; vi.spyOn(registration, 'getSWRegistration').mockResolvedValue(fakeReg); - vi.spyOn(registration, 'getAvailableServiceWorker').mockReturnValue({ - scriptURL: new URL( - 'https://example.com/OneSignalSDKWorker.js', - ).toString(), - }); + const sw = (url: string): ServiceWorker => + ({ + scriptURL: url, + state: 'activated', + onstatechange: null, + postMessage: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => false, + }) as unknown as ServiceWorker; + const swSpy = vi.spyOn(registration, 'getAvailableServiceWorker'); + swSpy.mockReturnValue( + sw(new URL('https://example.com/OneSignalSDKWorker.js').toString()), + ); const mgr = new ServiceWorkerManager(OneSignal._context, config); await expect(mgr._getActiveState()).resolves.toBe( helpers.ServiceWorkerActiveState._OneSignalWorker, ); // third-party - registration.getAvailableServiceWorker.mockReturnValue({ - scriptURL: new URL('https://example.com/othersw.js').toString(), - }); + swSpy.mockReturnValue( + sw(new URL('https://example.com/othersw.js').toString()), + ); await expect(mgr._getActiveState()).resolves.toBe( helpers.ServiceWorkerActiveState._ThirdParty, ); @@ -55,50 +61,60 @@ describe('ServiceWorkerManager', () => { const mgr = new ServiceWorkerManager(OneSignal._context, config); // no registration vi.spyOn(mgr, '_getRegistration').mockResolvedValue(undefined); - await expect(mgr._haveParamsChanged()).resolves.toBe(true); + await expect(mgr['_haveParamsChanged']()).resolves.toBe(true); // registration with different scope const regWithScope = { scope: 'https://example.com/oldPath', } as ServiceWorkerRegistration; - mgr._getRegistration.mockResolvedValue(regWithScope); + vi.spyOn(mgr, '_getRegistration').mockResolvedValue(regWithScope); const infoSpy = vi.spyOn(Log, '_info').mockImplementation(() => undefined); - await expect(mgr._haveParamsChanged()).resolves.toBe(true); + await expect(mgr['_haveParamsChanged']()).resolves.toBe(true); expect(infoSpy).toHaveBeenCalled(); // same scope but no worker scriptURL - mgr._getRegistration.mockResolvedValue({ scope: 'https://example.com/' }); - vi.spyOn(registration, 'getAvailableServiceWorker').mockReturnValue( - undefined, - ); - await expect(mgr._haveParamsChanged()).resolves.toBe(true); + vi.spyOn(mgr, '_getRegistration').mockResolvedValue({ + scope: 'https://example.com/', + } as ServiceWorkerRegistration); + vi.spyOn(registration, 'getAvailableServiceWorker').mockReturnValue(null); + await expect(mgr['_haveParamsChanged']()).resolves.toBe(true); // different href - registration.getAvailableServiceWorker.mockReturnValue({ + vi.mocked(registration.getAvailableServiceWorker).mockReturnValue({ scriptURL: 'https://example.com/old.js', - }); + } as ServiceWorker); vi.spyOn(helpers, 'getServiceWorkerHref').mockReturnValue( 'https://example.com/new.js', ); - await expect(mgr._haveParamsChanged()).resolves.toBe(true); + await expect(mgr['_haveParamsChanged']()).resolves.toBe(true); // same href - ( - helpers.getServiceWorkerHref as unknown as import('vitest').SpyInstance - ).mockReturnValue('https://example.com/old.js'); - await expect(mgr._haveParamsChanged()).resolves.toBe(false); + vi.mocked(helpers.getServiceWorkerHref).mockReturnValue( + 'https://example.com/old.js', + ); + await expect(mgr['_haveParamsChanged']()).resolves.toBe(false); }); test('_shouldInstallWorker branches', async () => { const mgr = new ServiceWorkerManager(OneSignal._context, config); - vi.spyOn(detect, 'supportsServiceWorkers').mockReturnValue(false); - await expect(mgr._shouldInstallWorker()).resolves.toBe(false); + const supportsSpy = vi + .spyOn(detect, 'supportsServiceWorkers') + .mockReturnValue(false); + await expect( + ( + mgr as unknown as { _shouldInstallWorker: () => Promise } + )._shouldInstallWorker(), + ).resolves.toBe(false); - detect.supportsServiceWorkers.mockReturnValue(true); + supportsSpy.mockReturnValue(true); const savedConfig = OneSignal.config; - OneSignal.config = undefined; - await expect(mgr._shouldInstallWorker()).resolves.toBe(false); + OneSignal.config = null as any; + await expect( + ( + mgr as unknown as { _shouldInstallWorker: () => Promise } + )._shouldInstallWorker(), + ).resolves.toBe(false); OneSignal.config = savedConfig; vi.spyOn(mgr, '_getActiveState').mockResolvedValue( @@ -108,24 +124,39 @@ describe('ServiceWorkerManager', () => { OneSignal._context._permissionManager, '_getNotificationPermission', ).mockResolvedValue('granted'); - await expect(mgr._shouldInstallWorker()).resolves.toBe(true); + await expect( + ( + mgr as unknown as { _shouldInstallWorker: () => Promise } + )._shouldInstallWorker(), + ).resolves.toBe(true); - mgr._getActiveState.mockResolvedValue( + vi.spyOn(mgr, '_getActiveState').mockResolvedValue( helpers.ServiceWorkerActiveState._OneSignalWorker, ); - vi.spyOn(mgr, '_haveParamsChanged').mockResolvedValue(true); - await expect(mgr._shouldInstallWorker()).resolves.toBe(true); + vi.spyOn( + mgr as unknown as { _haveParamsChanged: () => Promise }, + '_haveParamsChanged', + ).mockResolvedValue(true); + await expect( + ( + mgr as unknown as { _shouldInstallWorker: () => Promise } + )._shouldInstallWorker(), + ).resolves.toBe(true); - mgr._haveParamsChanged.mockResolvedValue(false); - vi.spyOn(mgr, '_workerNeedsUpdate').mockResolvedValue(false); - await expect(mgr._shouldInstallWorker()).resolves.toBe(false); + vi.spyOn( + mgr as unknown as { _haveParamsChanged: () => Promise }, + '_haveParamsChanged', + ).mockResolvedValue(false); + vi.spyOn( + mgr as unknown as { _workerNeedsUpdate: () => Promise }, + '_workerNeedsUpdate', + ).mockResolvedValue(false); + await expect( + ( + mgr as unknown as { _shouldInstallWorker: () => Promise } + )._shouldInstallWorker(), + ).resolves.toBe(false); }); - test('_workerNeedsUpdate compares versions', async () => { - const mgr = new ServiceWorkerManager(OneSignal._context, config); - vi.spyOn(mgr, '_getWorkerVersion').mockResolvedValue('0.0.1'); - await expect(mgr._workerNeedsUpdate()).resolves.toBe(true); - mgr._getWorkerVersion.mockResolvedValue(VERSION); - await expect(mgr._workerNeedsUpdate()).resolves.toBe(false); - }); + // omit direct private _workerNeedsUpdate test }); diff --git a/src/shared/managers/UpdateManager.test.ts b/src/shared/managers/UpdateManager.test.ts index 59d4add24..ab325a4ee 100644 --- a/src/shared/managers/UpdateManager.test.ts +++ b/src/shared/managers/UpdateManager.test.ts @@ -1,4 +1,5 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; +import { SubscriptionModel } from 'src/core/models/SubscriptionModel'; import User from 'src/onesignal/User'; import * as pageview from 'src/shared/helpers/pageview'; import { SessionOrigin } from 'src/shared/session/constants'; @@ -15,13 +16,13 @@ describe('UpdateManager', () => { test('_sendPushDeviceRecordUpdate early returns with no user, otherwise calls _sendOnSessionUpdate once', async () => { const mgr = new UpdateManager(OneSignal._context); // No user - User._singletonInstance = undefined; + User._singletonInstance = undefined as unknown as User; const spy = vi.spyOn(mgr, '_sendOnSessionUpdate').mockResolvedValue(); await mgr._sendPushDeviceRecordUpdate(); expect(spy).not.toHaveBeenCalled(); // With user present and first call triggers onSession - User._singletonInstance = { onesignalId: 'id' }; + User._singletonInstance = { onesignalId: 'id' } as unknown as User; await mgr._sendPushDeviceRecordUpdate(); expect(spy).toHaveBeenCalledTimes(1); }); @@ -29,33 +30,34 @@ describe('UpdateManager', () => { test('_sendOnSessionUpdate flows: already sent, not first page, not registered, skip when unsubscribed, success path sets flag', async () => { const mgr = new UpdateManager(OneSignal._context); // Already sent - mgr._onSessionSent = true; + (mgr as unknown as { _onSessionSent: boolean })._onSessionSent = true; await mgr._sendOnSessionUpdate(); // reset - mgr._onSessionSent = false; + (mgr as unknown as { _onSessionSent: boolean })._onSessionSent = false; // Not first page - vi.spyOn(pageview, 'isFirstPageView').mockReturnValue(false); + const firstSpy = vi + .spyOn(pageview, 'isFirstPageView') + .mockReturnValue(false); await mgr._sendOnSessionUpdate(); - pageview.isFirstPageView.mockReturnValue(true); + firstSpy.mockReturnValue(true); // Not registered with OneSignal - vi.spyOn( - OneSignal._context._subscriptionManager, - '_isAlreadyRegisteredWithOneSignal', - ).mockResolvedValue(false); + const alreadySpy = vi + .spyOn( + OneSignal._context._subscriptionManager, + '_isAlreadyRegisteredWithOneSignal', + ) + .mockResolvedValue(false); await mgr._sendOnSessionUpdate(); // Registered but not subscribed and enableOnSession not true -> skip - OneSignal._context._subscriptionManager._isAlreadyRegisteredWithOneSignal.mockResolvedValue( - true, - ); - vi.spyOn( - OneSignal._coreDirector, - '_getPushSubscriptionModel', - ).mockResolvedValue({ - _notification_types: 0, - }); + alreadySpy.mockResolvedValue(true); + const pushSpy = vi + .spyOn(OneSignal._coreDirector, '_getPushSubscriptionModel') + .mockResolvedValue({ + _notification_types: 0, + } as unknown as SubscriptionModel); const upsertSpy = vi .spyOn(OneSignal._context._sessionManager, '_upsertSession') .mockResolvedValue(); @@ -63,10 +65,10 @@ describe('UpdateManager', () => { expect(upsertSpy).not.toHaveBeenCalled(); // Subscribed path - OneSignal._coreDirector._getPushSubscriptionModel.mockResolvedValue({ + pushSpy.mockResolvedValue({ _notification_types: NotificationType._Subscribed, id: 'sub', - }); + } as unknown as SubscriptionModel); await mgr._sendOnSessionUpdate(); expect(upsertSpy).toHaveBeenCalledWith(SessionOrigin._UserNewSession); }); From 0c344406236043d0fe50f06d6012ba2c51cf2ef4 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 26 Nov 2025 10:54:32 -0800 Subject: [PATCH 08/11] test: clean up messy TS casts; remove use of any --- src/page/managers/PromptsManager.test.ts | 49 +++++++++---------- .../managers/ServiceWorkerManager.test.ts | 4 +- src/shared/managers/UpdateManager.test.ts | 4 +- 3 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/page/managers/PromptsManager.test.ts b/src/page/managers/PromptsManager.test.ts index e195d32d4..d82aeba8c 100644 --- a/src/page/managers/PromptsManager.test.ts +++ b/src/page/managers/PromptsManager.test.ts @@ -1,10 +1,14 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { setupLoadStylesheet } from '__test__/support/helpers/setup'; import { DelayedPromptType } from 'src/shared/prompts/constants'; +import type { + AppUserConfigPromptOptions, + DelayedPromptOptions, + DelayedPromptTypeValue, +} from 'src/shared/prompts/types'; import { Browser } from 'src/shared/useragent/constants'; import * as detect from 'src/shared/useragent/detect'; import { PromptsManager } from './PromptsManager'; - const getBrowserNameSpy = vi.spyOn(detect, 'getBrowserName'); const getBrowserVersionSpy = vi.spyOn(detect, 'getBrowserVersion'); const isMobileBrowserSpy = vi.spyOn(detect, 'isMobileBrowser'); @@ -62,11 +66,8 @@ describe('PromptsManager', () => { .mockResolvedValue(true); const slidedownSpy = vi .spyOn( - PromptsManager.prototype as unknown as Record< - string, - (...args: unknown[]) => unknown - >, - '_internalShowSlidedownPrompt', + PromptsManager.prototype, + '_internalShowSlidedownPrompt' as keyof PromptsManager, ) .mockResolvedValue(undefined as void); await pm._internalShowDelayedPrompt(DelayedPromptType._Native, 0); @@ -78,34 +79,32 @@ describe('PromptsManager', () => { const pm = new PromptsManager(OneSignal._context); const getOptsSpy = vi .spyOn( - PromptsManager.prototype as unknown as Record< - string, - (...args: unknown[]) => unknown - >, + pm as unknown as { + _getDelayedPromptOptions: ( + opts: AppUserConfigPromptOptions | undefined, + type: DelayedPromptTypeValue, + ) => DelayedPromptOptions; + }, '_getDelayedPromptOptions', ) - .mockReturnValue({ - enabled: true, - autoPrompt: true, - timeDelay: 0, - pageViews: 0, - }); + .mockImplementation( + (): DelayedPromptOptions => ({ + enabled: true, + autoPrompt: true, + timeDelay: 0, + pageViews: 0, + }), + ); const condSpy = vi .spyOn( - PromptsManager.prototype as unknown as Record< - string, - (...args: unknown[]) => unknown - >, + pm as unknown as { _isPageViewConditionMet: (o?: unknown) => boolean }, '_isPageViewConditionMet', ) .mockReturnValue(true); const delayedSpy = vi .spyOn( - PromptsManager.prototype as unknown as Record< - string, - (...args: unknown[]) => unknown - >, - '_internalShowDelayedPrompt', + PromptsManager.prototype, + '_internalShowDelayedPrompt' as keyof PromptsManager, ) .mockResolvedValue(undefined as void); requiresUserInteractionSpy.mockReturnValue(false); diff --git a/src/shared/managers/ServiceWorkerManager.test.ts b/src/shared/managers/ServiceWorkerManager.test.ts index 111529425..448fd43b9 100644 --- a/src/shared/managers/ServiceWorkerManager.test.ts +++ b/src/shared/managers/ServiceWorkerManager.test.ts @@ -109,7 +109,7 @@ describe('ServiceWorkerManager', () => { supportsSpy.mockReturnValue(true); const savedConfig = OneSignal.config; - OneSignal.config = null as any; + OneSignal.config = null; await expect( ( mgr as unknown as { _shouldInstallWorker: () => Promise } @@ -157,6 +157,4 @@ describe('ServiceWorkerManager', () => { )._shouldInstallWorker(), ).resolves.toBe(false); }); - - // omit direct private _workerNeedsUpdate test }); diff --git a/src/shared/managers/UpdateManager.test.ts b/src/shared/managers/UpdateManager.test.ts index ab325a4ee..b161580a9 100644 --- a/src/shared/managers/UpdateManager.test.ts +++ b/src/shared/managers/UpdateManager.test.ts @@ -16,13 +16,13 @@ describe('UpdateManager', () => { test('_sendPushDeviceRecordUpdate early returns with no user, otherwise calls _sendOnSessionUpdate once', async () => { const mgr = new UpdateManager(OneSignal._context); // No user - User._singletonInstance = undefined as unknown as User; + delete User._singletonInstance; const spy = vi.spyOn(mgr, '_sendOnSessionUpdate').mockResolvedValue(); await mgr._sendPushDeviceRecordUpdate(); expect(spy).not.toHaveBeenCalled(); // With user present and first call triggers onSession - User._singletonInstance = { onesignalId: 'id' } as unknown as User; + User._createOrGetInstance(); await mgr._sendPushDeviceRecordUpdate(); expect(spy).toHaveBeenCalledTimes(1); }); From 264a02558f66411d34084d7dcc6342f431335bcc Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 26 Nov 2025 11:16:12 -0800 Subject: [PATCH 09/11] test: refactor to avoid redundant spies --- src/shared/managers/CustomLinkManager.test.ts | 17 +++--- .../managers/ServiceWorkerManager.test.ts | 8 +-- src/shared/managers/UpdateManager.test.ts | 4 +- .../sessionManager/SessionManager.test.ts | 52 ++++++++++--------- 4 files changed, 42 insertions(+), 39 deletions(-) diff --git a/src/shared/managers/CustomLinkManager.test.ts b/src/shared/managers/CustomLinkManager.test.ts index e885d4cc4..28eb8ec89 100644 --- a/src/shared/managers/CustomLinkManager.test.ts +++ b/src/shared/managers/CustomLinkManager.test.ts @@ -4,16 +4,21 @@ import { CUSTOM_LINK_CSS_CLASSES, CUSTOM_LINK_CSS_SELECTORS, } from 'src/shared/slidedown/constants'; -import { expect, vi } from 'vitest'; +import { vi, type MockInstance } from 'vitest'; import { ResourceLoadState } from '../../page/services/DynamicResourceLoader'; import { CustomLinkManager } from './CustomLinkManager'; describe('CustomLinkManager', () => { + let isPushEnabledSpy: MockInstance; beforeEach(() => { TestEnvironment.initialize(); document.body.innerHTML = ` `; + isPushEnabledSpy = vi.spyOn( + OneSignal._context._subscriptionManager, + '_isPushNotificationsEnabled', + ); }); test('_initialize returns when disabled or stylesheet fails', async () => { @@ -40,10 +45,7 @@ describe('CustomLinkManager', () => { test('_initialize hides containers when subscribed and unsubscribe disabled', async () => { await setupLoadStylesheet(); - vi.spyOn( - OneSignal._context._subscriptionManager, - '_isPushNotificationsEnabled', - ).mockResolvedValue(true); + isPushEnabledSpy.mockResolvedValue(true); const mgr = new CustomLinkManager({ enabled: true, unsubscribeEnabled: false, @@ -64,10 +66,7 @@ describe('CustomLinkManager', () => { test('_initialize injects markup and click toggles subscription', async () => { await setupLoadStylesheet(); - vi.spyOn( - OneSignal._context._subscriptionManager, - '_isPushNotificationsEnabled', - ).mockResolvedValue(false); + isPushEnabledSpy.mockResolvedValue(false); const optInSpy = vi .spyOn(OneSignal.User.PushSubscription, 'optIn') .mockResolvedValue(); diff --git a/src/shared/managers/ServiceWorkerManager.test.ts b/src/shared/managers/ServiceWorkerManager.test.ts index 448fd43b9..ff65ef475 100644 --- a/src/shared/managers/ServiceWorkerManager.test.ts +++ b/src/shared/managers/ServiceWorkerManager.test.ts @@ -3,7 +3,7 @@ import * as detect from 'src/shared/environment/detect'; import * as helpers from 'src/shared/helpers/service-worker'; import Path from 'src/shared/models/Path'; import * as registration from 'src/sw/helpers/registration'; -import { vi } from 'vitest'; +import { vi, type MockInstance } from 'vitest'; import Log from '../libraries/Log'; import { ServiceWorkerManager } from './ServiceWorkerManager'; @@ -13,13 +13,15 @@ describe('ServiceWorkerManager', () => { registrationOptions: { scope: '/' }, } as const; + let getSWRegistrationSpy: MockInstance; beforeEach(() => { TestEnvironment.initialize(); vi.restoreAllMocks(); + getSWRegistrationSpy = vi.spyOn(registration, 'getSWRegistration'); }); test('_getActiveState returns None when no registration', async () => { - vi.spyOn(registration, 'getSWRegistration').mockResolvedValue(undefined); + getSWRegistrationSpy.mockResolvedValue(undefined); const mgr = new ServiceWorkerManager(OneSignal._context, config); await expect(mgr._getActiveState()).resolves.toBe( helpers.ServiceWorkerActiveState._None, @@ -28,7 +30,7 @@ describe('ServiceWorkerManager', () => { test('_getActiveState detects OneSignal vs third-party from file name', async () => { const fakeReg = {} as ServiceWorkerRegistration; - vi.spyOn(registration, 'getSWRegistration').mockResolvedValue(fakeReg); + getSWRegistrationSpy.mockResolvedValue(fakeReg); const sw = (url: string): ServiceWorker => ({ scriptURL: url, diff --git a/src/shared/managers/UpdateManager.test.ts b/src/shared/managers/UpdateManager.test.ts index b161580a9..a331e7bde 100644 --- a/src/shared/managers/UpdateManager.test.ts +++ b/src/shared/managers/UpdateManager.test.ts @@ -8,13 +8,14 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import { UpdateManager } from './UpdateManager'; describe('UpdateManager', () => { + let mgr: UpdateManager; beforeEach(() => { TestEnvironment.initialize(); vi.restoreAllMocks(); + mgr = new UpdateManager(OneSignal._context); }); test('_sendPushDeviceRecordUpdate early returns with no user, otherwise calls _sendOnSessionUpdate once', async () => { - const mgr = new UpdateManager(OneSignal._context); // No user delete User._singletonInstance; const spy = vi.spyOn(mgr, '_sendOnSessionUpdate').mockResolvedValue(); @@ -28,7 +29,6 @@ describe('UpdateManager', () => { }); test('_sendOnSessionUpdate flows: already sent, not first page, not registered, skip when unsubscribed, success path sets flag', async () => { - const mgr = new UpdateManager(OneSignal._context); // Already sent (mgr as unknown as { _onSessionSent: boolean })._onSessionSent = true; await mgr._sendOnSessionUpdate(); diff --git a/src/shared/managers/sessionManager/SessionManager.test.ts b/src/shared/managers/sessionManager/SessionManager.test.ts index 5dcb572ed..40d74add8 100644 --- a/src/shared/managers/sessionManager/SessionManager.test.ts +++ b/src/shared/managers/sessionManager/SessionManager.test.ts @@ -5,7 +5,8 @@ import LoginManager from 'src/page/managers/LoginManager'; import * as detect from 'src/shared/environment/detect'; import Log from 'src/shared/libraries/Log'; import { SessionOrigin } from 'src/shared/session/constants'; -import { vi } from 'vitest'; +import type { SessionOriginValue } from 'src/shared/session/types'; +import { vi, type MockInstance } from 'vitest'; import User from '../../../onesignal/User'; import { SessionManager } from './SessionManager'; const supportsServiceWorkersSpy = vi.spyOn(detect, 'supportsServiceWorkers'); @@ -133,13 +134,30 @@ describe('SessionManager', () => { }); describe('Core behaviors', () => { + let sm: SessionManager; + let notifySpy: MockInstance; + let deactSpy: MockInstance; + beforeEach(() => { TestEnvironment.initialize(); + sm = new SessionManager(OneSignal._context); + notifySpy = vi.spyOn(sm, '_notifySWToUpsertSession'); + deactSpy = vi + .spyOn( + (sm as unknown as { + _notifySWToDeactivateSession: ( + onesignalId: string, + subscriptionId: string, + sessionOrigin: SessionOriginValue, + ) => Promise; + }), + '_notifySWToDeactivateSession', + ) + .mockResolvedValue(undefined); }); test('_notifySWToUpsertSession posts to worker when SW supported', async () => { supportsServiceWorkersSpy.mockReturnValue(true); - const sm = new SessionManager(OneSignal._context); const unicastSpy = vi .spyOn(OneSignal._context._workerMessenger, '_unicast') .mockResolvedValue(undefined); @@ -154,15 +172,12 @@ describe('SessionManager', () => { test('_upsertSession does nothing when no user is present', async () => { supportsServiceWorkersSpy.mockReturnValue(true); - const sm = new SessionManager(OneSignal._context); - const notifySpy = vi.spyOn(sm, '_notifySWToUpsertSession'); await sm._upsertSession(SessionOrigin._UserCreate); expect(notifySpy).not.toHaveBeenCalled(); }); test('_upsertSession installs listeners when SW supported', async () => { supportsServiceWorkersSpy.mockReturnValue(true); - const sm = new SessionManager(OneSignal._context); const setupSpy = vi.spyOn(sm, '_setupSessionEventListeners'); await sm._upsertSession(SessionOrigin._Focus); expect(setupSpy).toHaveBeenCalled(); @@ -170,16 +185,12 @@ describe('SessionManager', () => { test('_upsertSession emits SESSION_STARTED when SW not supported', async () => { supportsServiceWorkersSpy.mockReturnValue(false); - const sm = new SessionManager(OneSignal._context); - const emitSpy = vi - .spyOn(OneSignal._emitter, '_emit') - .mockResolvedValue(OneSignal._emitter); + const emitSpy = vi.spyOn(OneSignal._emitter, '_emit').mockResolvedValue(OneSignal._emitter); await sm._upsertSession(SessionOrigin._UserCreate); expect(emitSpy).toHaveBeenCalledWith(OneSignal.EVENTS.SESSION_STARTED); }); test('_handleVisibilityChange visible triggers upsert; hidden triggers deactivate and removes listeners', async () => { - const sm = new SessionManager(OneSignal._context); // ensure user present User._createOrGetInstance(); @@ -193,11 +204,9 @@ describe('SessionManager', () => { .spyOn(document, 'visibilityState', 'get') .mockReturnValue('visible' as DocumentVisibilityState); const focusSpy = vi.spyOn(document, 'hasFocus').mockReturnValue(true); - const upsertSpy = vi - .spyOn(sm as any, '_notifySWToUpsertSession') - .mockResolvedValue(undefined); + (notifySpy as any).mockResolvedValue(undefined); await sm._handleVisibilityChange(); - expect(upsertSpy).toHaveBeenCalled(); + expect(notifySpy).toHaveBeenCalled(); visSpy.mockRestore(); focusSpy.mockRestore(); @@ -205,9 +214,7 @@ describe('SessionManager', () => { vi.spyOn(document, 'visibilityState', 'get').mockReturnValue( 'hidden' as DocumentVisibilityState, ); - const deactSpy = vi - .spyOn(sm as any, '_notifySWToDeactivateSession') - .mockResolvedValue(undefined); + (deactSpy as any).mockResolvedValue(undefined); OneSignal._cache.isFocusEventSetup = true; OneSignal._cache.isBlurEventSetup = true; OneSignal._cache.focusHandler = () => undefined; @@ -219,18 +226,13 @@ describe('SessionManager', () => { }); test('_handleOnFocus/Blur target guard prevents duplicate', async () => { - const sm = new SessionManager(OneSignal._context); // ensure user present User._createOrGetInstance(); - const upsertSpy = vi - .spyOn(sm as any, '_notifySWToUpsertSession') - .mockResolvedValue(undefined); - const deactSpy = vi - .spyOn(sm as any, '_notifySWToDeactivateSession') - .mockResolvedValue(undefined); + (notifySpy as any).mockResolvedValue(undefined); + (deactSpy as any).mockResolvedValue(undefined); await sm._handleOnFocus(new Event('focus')); await sm._handleOnBlur(new Event('blur')); - expect(upsertSpy).not.toHaveBeenCalled(); + expect(notifySpy).not.toHaveBeenCalled(); expect(deactSpy).not.toHaveBeenCalled(); }); }); From ef50e1823dc8e2d6377e49fa2ef85cfee4575133 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 26 Nov 2025 11:19:39 -0800 Subject: [PATCH 10/11] test(SessionManager): remove use of `any` --- .../sessionManager/SessionManager.test.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/shared/managers/sessionManager/SessionManager.test.ts b/src/shared/managers/sessionManager/SessionManager.test.ts index 40d74add8..a261f2ca3 100644 --- a/src/shared/managers/sessionManager/SessionManager.test.ts +++ b/src/shared/managers/sessionManager/SessionManager.test.ts @@ -194,17 +194,18 @@ describe('SessionManager', () => { // ensure user present User._createOrGetInstance(); - vi.spyOn(sm as any, '_getOneSignalAndSubscriptionIds').mockResolvedValue({ - onesignalId: 'o', - subscriptionId: 's', - }); + vi.spyOn(sm as unknown as { _getOneSignalAndSubscriptionIds: () => Promise<{ onesignalId: string; subscriptionId: string }> }, '_getOneSignalAndSubscriptionIds').mockResolvedValue({ + onesignalId: 'o', + subscriptionId: 's', + }, + ); // visible and focused const visSpy = vi .spyOn(document, 'visibilityState', 'get') .mockReturnValue('visible' as DocumentVisibilityState); const focusSpy = vi.spyOn(document, 'hasFocus').mockReturnValue(true); - (notifySpy as any).mockResolvedValue(undefined); + notifySpy.mockResolvedValue(undefined); await sm._handleVisibilityChange(); expect(notifySpy).toHaveBeenCalled(); visSpy.mockRestore(); @@ -214,7 +215,7 @@ describe('SessionManager', () => { vi.spyOn(document, 'visibilityState', 'get').mockReturnValue( 'hidden' as DocumentVisibilityState, ); - (deactSpy as any).mockResolvedValue(undefined); + deactSpy.mockResolvedValue(undefined); OneSignal._cache.isFocusEventSetup = true; OneSignal._cache.isBlurEventSetup = true; OneSignal._cache.focusHandler = () => undefined; @@ -228,8 +229,8 @@ describe('SessionManager', () => { test('_handleOnFocus/Blur target guard prevents duplicate', async () => { // ensure user present User._createOrGetInstance(); - (notifySpy as any).mockResolvedValue(undefined); - (deactSpy as any).mockResolvedValue(undefined); + notifySpy.mockResolvedValue(undefined); + deactSpy.mockResolvedValue(undefined); await sm._handleOnFocus(new Event('focus')); await sm._handleOnBlur(new Event('blur')); expect(notifySpy).not.toHaveBeenCalled(); From c18a8b7c77aa8569e1a93da95af60f7588beef74 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 26 Nov 2025 22:27:41 -0500 Subject: [PATCH 11/11] style: fix linter errors --- .../sessionManager/SessionManager.test.ts | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/shared/managers/sessionManager/SessionManager.test.ts b/src/shared/managers/sessionManager/SessionManager.test.ts index a261f2ca3..bc2555d02 100644 --- a/src/shared/managers/sessionManager/SessionManager.test.ts +++ b/src/shared/managers/sessionManager/SessionManager.test.ts @@ -144,13 +144,13 @@ describe('SessionManager', () => { notifySpy = vi.spyOn(sm, '_notifySWToUpsertSession'); deactSpy = vi .spyOn( - (sm as unknown as { + sm as unknown as { _notifySWToDeactivateSession: ( onesignalId: string, subscriptionId: string, sessionOrigin: SessionOriginValue, ) => Promise; - }), + }, '_notifySWToDeactivateSession', ) .mockResolvedValue(undefined); @@ -185,7 +185,9 @@ describe('SessionManager', () => { test('_upsertSession emits SESSION_STARTED when SW not supported', async () => { supportsServiceWorkersSpy.mockReturnValue(false); - const emitSpy = vi.spyOn(OneSignal._emitter, '_emit').mockResolvedValue(OneSignal._emitter); + const emitSpy = vi + .spyOn(OneSignal._emitter, '_emit') + .mockResolvedValue(OneSignal._emitter); await sm._upsertSession(SessionOrigin._UserCreate); expect(emitSpy).toHaveBeenCalledWith(OneSignal.EVENTS.SESSION_STARTED); }); @@ -194,11 +196,18 @@ describe('SessionManager', () => { // ensure user present User._createOrGetInstance(); - vi.spyOn(sm as unknown as { _getOneSignalAndSubscriptionIds: () => Promise<{ onesignalId: string; subscriptionId: string }> }, '_getOneSignalAndSubscriptionIds').mockResolvedValue({ - onesignalId: 'o', - subscriptionId: 's', + vi.spyOn( + sm as unknown as { + _getOneSignalAndSubscriptionIds: () => Promise<{ + onesignalId: string; + subscriptionId: string; + }>; }, - ); + '_getOneSignalAndSubscriptionIds', + ).mockResolvedValue({ + onesignalId: 'o', + subscriptionId: 's', + }); // visible and focused const visSpy = vi