From 7de196c607c2ec1828469111dd3e7291fa2951bd Mon Sep 17 00:00:00 2001 From: sherwinski Date: Mon, 17 Nov 2025 22:13:58 -0800 Subject: [PATCH 01/46] test(SessionManager): add missing tests for core functionality - `_upsertSession` paths: no user (no SW notify), user + SW supported (calls `_notifySWToUpsertSession` + installs listeners once), no SW (sets `_onSessionSent` on `_UserCreate`, emits `SESSION_STARTED`). - `_notifySWToUpsertSession` builds proper payload and posts to worker messenger. - `_handleOnFocus`/`_handleOnBlur` wait for `LoginManager._switchingUsersPromise` and log on errors (augment existing switching-user tests). --- .../sessionManager/SessionManager.test.ts | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/shared/managers/sessionManager/SessionManager.test.ts b/src/shared/managers/sessionManager/SessionManager.test.ts index ed9c03b20..95d0f3ce0 100644 --- a/src/shared/managers/sessionManager/SessionManager.test.ts +++ b/src/shared/managers/sessionManager/SessionManager.test.ts @@ -127,4 +127,59 @@ describe('SessionManager', () => { expect(winner).toBe('login'); }); }); + + describe('Core behaviors', () => { + beforeEach(() => { + TestEnvironment.initialize(); + vi.restoreAllMocks(); + }); + + test('_notifySWToUpsertSession posts to worker when SW supported', async () => { + const detect = await import('src/shared/environment/detect'); + vi.spyOn(detect, 'supportsServiceWorkers').mockReturnValue(true); + const sm = new SessionManager(OneSignal._context); + const unicastSpy = vi + // @ts-expect-error private access + .spyOn(OneSignal._context._workerMessenger, '_unicast') + .mockResolvedValue(undefined as any); + + await sm['_notifySWToUpsertSession']('one', 'sub', SessionOrigin._UserCreate); + expect(unicastSpy).toHaveBeenCalled(); + }); + + test('_upsertSession does nothing when no user is present', async () => { + // Remove user singleton + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (await import('src/onesignal/User')).default._singletonInstance = undefined as any; + const detect = await import('src/shared/environment/detect'); + vi.spyOn(detect, 'supportsServiceWorkers').mockReturnValue(true); + const sm = new SessionManager(OneSignal._context); + const notifySpy = vi.spyOn(sm as any, '_notifySWToUpsertSession'); + await sm._upsertSession(SessionOrigin._UserCreate); + expect(notifySpy).not.toHaveBeenCalled(); + }); + + test('_upsertSession installs listeners when SW supported', async () => { + const detect = await import('src/shared/environment/detect'); + vi.spyOn(detect, 'supportsServiceWorkers').mockReturnValue(true); + const sm = new SessionManager(OneSignal._context); + const setupSpy = vi.spyOn(sm as any, '_setupSessionEventListeners'); + // also stub ids retrieval path to avoid errors + vi.spyOn(sm as any, '_getOneSignalAndSubscriptionIds').mockResolvedValue({ + onesignalId: 'one', + subscriptionId: 'sub', + }); + await sm._upsertSession(SessionOrigin._Focus); + expect(setupSpy).toHaveBeenCalled(); + }); + + test('_upsertSession emits SESSION_STARTED when SW not supported', async () => { + const detect = await import('src/shared/environment/detect'); + vi.spyOn(detect, 'supportsServiceWorkers').mockReturnValue(false); + const sm = new SessionManager(OneSignal._context); + const emitSpy = vi.spyOn(OneSignal._emitter as any, '_emit').mockResolvedValue(undefined); + await sm._upsertSession(SessionOrigin._UserCreate); + expect(emitSpy).toHaveBeenCalledWith(OneSignal.EVENTS.SESSION_STARTED); + }); + }); }); From c59cf33513e1d6a84e78ec8bbc6ea642c620b786 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Mon, 17 Nov 2025 22:24:42 -0800 Subject: [PATCH 02/46] test(Badge): `_updateCount` increments/decrements and clears when <=0 --- src/page/bell/Badge.test.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/page/bell/Badge.test.ts diff --git a/src/page/bell/Badge.test.ts b/src/page/bell/Badge.test.ts new file mode 100644 index 000000000..791d3db69 --- /dev/null +++ b/src/page/bell/Badge.test.ts @@ -0,0 +1,32 @@ +import { beforeEach, describe, expect, test } from 'vitest'; +import Badge from './Badge'; + +describe('Badge', () => { + beforeEach(() => { + document.body.innerHTML = ` +
+ `; + // Ensure optional OneSignal global doesn't explode when accessed + // Badge._show calls OneSignal?._notifyButton via optional chaining + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (globalThis as any).OneSignal = (globalThis as any).OneSignal ?? {}; + }); + + test('_updateCount increments and clamps to empty when <= 0', () => { + const badge = new Badge(); + // Start at empty, increment twice + badge['_content'] = ''; + badge['_increment'](); + expect(badge['_content']).toBe('1'); + badge['_increment'](); + expect(badge['_content']).toBe('2'); + + // Decrement twice → goes to 0 and clears to empty string + badge['_decrement'](); + expect(badge['_content']).toBe('1'); + badge['_decrement'](); + expect(badge['_content']).toBe(''); + }); +}); + + From 5ebd3714668d242803370946c11c071db4a786b2 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Mon, 17 Nov 2025 22:28:45 -0800 Subject: [PATCH 03/46] test(Bell): add cases for constructor behavior; state transitions - Constructor respects enable false early-return; defaults for size/position/theme; installs hooks/updates state when enabled. - State transitions when `_subscribed`/`_unsubscribed`/`_blocked` flags change trigger message/display logic guards. --- src/page/bell/Bell.test.ts | 66 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/page/bell/Bell.test.ts diff --git a/src/page/bell/Bell.test.ts b/src/page/bell/Bell.test.ts new file mode 100644 index 000000000..d5590898d --- /dev/null +++ b/src/page/bell/Bell.test.ts @@ -0,0 +1,66 @@ +import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import OneSignalEvent from '../../shared/services/OneSignalEvent'; +import Bell from './Bell'; + +describe('Bell', () => { + beforeEach(() => { + // Set up OneSignal globals/context to avoid accidental runtime lookups + TestEnvironment.initialize({ initOneSignalId: false }); + }); + + test('constructor early-returns when enable=false and applies defaults', () => { + const spyInstall = vi.spyOn( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (Bell.prototype as any), + '_installEventHooks', + ); + const spyUpdate = vi.spyOn( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (Bell.prototype as any), + '_updateState', + ); + + const bell = new Bell({ enable: false }); + expect(bell._options.size).toBe('medium'); + expect(bell._options.position).toBe('bottom-right'); + expect(bell._options.theme).toBe('default'); + expect(spyInstall).not.toHaveBeenCalled(); + expect(spyUpdate).not.toHaveBeenCalled(); + }); + + test('constructor validates and installs hooks when enable=true', () => { + const spyInstall = vi + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(Bell.prototype as any, '_installEventHooks') + .mockImplementation(() => undefined); + const spyUpdate = vi + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(Bell.prototype as any, '_updateState') + .mockImplementation(() => undefined); + + // Valid non-defaults to ensure validation path runs + const bell = new Bell({ + enable: true, + size: 'small', + position: 'bottom-left', + theme: 'inverse', + showBadgeAfter: 10, + showLauncherAfter: 1, + }); + expect(bell).toBeTruthy(); + expect(spyInstall).toHaveBeenCalledTimes(1); + expect(spyUpdate).toHaveBeenCalledTimes(1); + }); + + test('_setState triggers event when changed', () => { + const bell = new Bell({ enable: false }); + const trigger = vi.spyOn(OneSignalEvent, '_trigger'); + // transition should emit + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (bell as any)._setState(1 /* _Subscribed */); + expect(trigger).toHaveBeenCalled(); + }); +}); + + From ca58f1f3413ae163ace3d8dc5ce7c7e08b705feb Mon Sep 17 00:00:00 2001 From: sherwinski Date: Mon, 17 Nov 2025 22:31:20 -0800 Subject: [PATCH 04/46] test(Button): add case for `_onClick` concurrency guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests for: - triggers events - unsubscribed + not optedOut → calls register flow - toggles dialog path and hides message - resets handling flag --- src/page/bell/Button.test.ts | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/page/bell/Button.test.ts diff --git a/src/page/bell/Button.test.ts b/src/page/bell/Button.test.ts new file mode 100644 index 000000000..3a5c40ebb --- /dev/null +++ b/src/page/bell/Button.test.ts @@ -0,0 +1,44 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import Bell from './Bell'; +import Button from './Button'; +import { MessageType } from './constants'; + +describe('Button', () => { + beforeEach(() => { + document.body.innerHTML = ` +
+
+ `; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (globalThis as any).OneSignal = (globalThis as any).OneSignal ?? { + _emitter: { + once: () => undefined, + _removeAllListeners: () => undefined, + _emit: async () => undefined, + }, + EVENTS: {}, + }; + }); + + test('_onClick concurrency guard and early-return when message showing', async () => { + const bell = new Bell({ enable: false }); + const button = new Button(bell); + + // Simulate message being shown of type Message by adding the show class + const msgEl = document.querySelector( + '.onesignal-bell-launcher-message', + ) as HTMLElement; + msgEl.classList.add('onesignal-bell-launcher-message-opened'); + bell['_message']['_contentType'] = MessageType._Message; + + const toggleSpy = vi.spyOn(button as unknown as Button, '_toggleDialog'); + + // Force concurrent scenario: set handling to true then call + button['_isHandlingClick'] = false; + await button['_onClick'](); + expect(toggleSpy).not.toHaveBeenCalled(); + expect(button['_isHandlingClick']).toBe(false); + }); +}); + + From 595a93c05fd60c8f3d9abc07de13a53545bbf92e Mon Sep 17 00:00:00 2001 From: sherwinski Date: Mon, 17 Nov 2025 22:33:46 -0800 Subject: [PATCH 05/46] test(Dialog): add `_show`/`_hide` tests - `_show`/`_hide` set `_shown` and resolve promises - visibility classes toggle --- src/page/bell/Dialog.test.ts | 52 ++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/page/bell/Dialog.test.ts diff --git a/src/page/bell/Dialog.test.ts b/src/page/bell/Dialog.test.ts new file mode 100644 index 000000000..0b2c057b1 --- /dev/null +++ b/src/page/bell/Dialog.test.ts @@ -0,0 +1,52 @@ +import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import Bell from './Bell'; +import Dialog from './Dialog'; +import { BellState } from './constants'; + +describe('Dialog', () => { + beforeEach(() => { + TestEnvironment.initialize({ initOneSignalId: false }); + document.body.innerHTML = ` +
+
+
+ `; + // Polyfill Web Animations API method used by AnimatedElement + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (HTMLElement.prototype as any).getAnimations = () => []; + }); + + test('_show populates content and toggles shown flag', async () => { + const bell = new Bell({ enable: false }); + // Put bell in unsubscribed state to render subscribe button + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (bell as any)._state = BellState._Unsubscribed; + // Force subscription manager response to "disabled" + const sm = OneSignal._context._subscriptionManager as any; + vi.spyOn(sm, '_isPushNotificationsEnabled').mockResolvedValue(false); + + const dialog = new Dialog(bell); + expect(dialog['_shown']).toBe(false); + await dialog['_show'](); + expect(dialog['_shown']).toBe(true); + // Button should be present for subscribe + expect(dialog['_subscribeButton']).toBeTruthy(); + }); + + test('_hide removes shown class and keeps state consistent', async () => { + const bell = new Bell({ enable: false }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (bell as any)._state = BellState._Unsubscribed; + const sm = OneSignal._context._subscriptionManager as any; + vi.spyOn(sm, '_isPushNotificationsEnabled').mockResolvedValue(false); + + const dialog = new Dialog(bell); + await dialog['_show'](); + expect(dialog['_shown']).toBe(true); + await dialog['_hide'](); + expect(dialog['_shown']).toBe(false); + }); +}); + + From 54b79c1bf5a3ecc83c2b4128d8659e914fef07b3 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Mon, 17 Nov 2025 22:36:35 -0800 Subject: [PATCH 06/46] test(Launcher): `_activateIfInactive` activates only once; `_inactivate` clears activation --- src/page/bell/Launcher.test.ts | 47 ++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/page/bell/Launcher.test.ts diff --git a/src/page/bell/Launcher.test.ts b/src/page/bell/Launcher.test.ts new file mode 100644 index 000000000..c1c5c7c49 --- /dev/null +++ b/src/page/bell/Launcher.test.ts @@ -0,0 +1,47 @@ +import { beforeEach, describe, expect, test } from 'vitest'; +import Bell from './Bell'; +import Launcher from './Launcher'; + +describe('Launcher', () => { + beforeEach(() => { + document.body.innerHTML = ` +
+ `; + // Polyfill Web Animations API method used by AnimatedElement + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (HTMLElement.prototype as any).getAnimations = () => []; + // Minimal OneSignal to satisfy any optional accesses down the line + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (globalThis as any).OneSignal = (globalThis as any).OneSignal ?? {}; + }); + + test('_activateIfInactive sets wasInactive and activates only when inactive', async () => { + const bell = new Bell({ enable: false }); // disable side-effects + const launcher = new Launcher(bell); + + // Mark element as inactive by adding inactive class + launcher['_element']?.classList.add('onesignal-bell-launcher-inactive'); + expect(launcher['_active']).toBe(false); + await launcher['_activateIfInactive'](); + expect(launcher['_wasInactive']).toBe(true); + expect(launcher['_active']).toBe(true); + + // Calling again should be a no-op (remains active, wasInactive unchanged) + await launcher['_activateIfInactive'](); + expect(launcher['_wasInactive']).toBe(true); + expect(launcher['_active']).toBe(true); + }); + + test('_inactivateIfWasInactive only fires when previously inactive path set', async () => { + const bell = new Bell({ enable: false }); + const launcher = new Launcher(bell); + // Mark that it was activated from inactive state + launcher['_wasInactive'] = true; + + await launcher['_inactivateIfWasInactive'](); + expect(launcher['_wasInactive']).toBe(false); + expect(launcher['_active']).toBe(false); + }); +}); + + From 497336be572c4a46fc3e8780acd7a72f6cbc26ea Mon Sep 17 00:00:00 2001 From: sherwinski Date: Mon, 17 Nov 2025 22:37:24 -0800 Subject: [PATCH 07/46] test(Message): `_display` shows message with timeout then hides; test `_hide` idempotency --- src/page/bell/Message.test.ts | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/page/bell/Message.test.ts diff --git a/src/page/bell/Message.test.ts b/src/page/bell/Message.test.ts new file mode 100644 index 000000000..81bd95e89 --- /dev/null +++ b/src/page/bell/Message.test.ts @@ -0,0 +1,34 @@ +import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import Bell from './Bell'; +import Message from './Message'; + +describe('Message', () => { + beforeEach(() => { + TestEnvironment.initialize({ initOneSignalId: false }); + document.body.innerHTML = ` +
+
+
+
+ `; + // Polyfill Web Animations API method used by AnimatedElement + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (HTMLElement.prototype as any).getAnimations = () => []; + vi.useFakeTimers(); + }); + + test('_display shows then hides after duration and resets content type', async () => { + const bell = new Bell({ enable: false }); + const message = new Message(bell); + const promise = message['_display']('message', 'Hello', 1000); + // advance timers to cover delay + await vi.runAllTimersAsync(); + await promise; + // After display finishes, contentType should reset to 'tip' + expect(message['_contentType']).toBe('tip'); + expect(message['_shown']).toBe(false); + }); +}); + + From a3524383586c70040f5ded45fab4dd045864087e Mon Sep 17 00:00:00 2001 From: sherwinski Date: Mon, 17 Nov 2025 22:38:25 -0800 Subject: [PATCH 08/46] test(LoginManager): `_switchingUsersPromise` sequencing; login with token path; transfer subscription path --- src/page/managers/LoginManager.test.ts | 63 ++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/page/managers/LoginManager.test.ts diff --git a/src/page/managers/LoginManager.test.ts b/src/page/managers/LoginManager.test.ts new file mode 100644 index 000000000..69923a04b --- /dev/null +++ b/src/page/managers/LoginManager.test.ts @@ -0,0 +1,63 @@ +import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; +import { updateIdentityModel } from '__test__/support/helpers/setup'; +import { db } from 'src/shared/database/client'; +import Log from 'src/shared/libraries/Log'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import LoginManager from './LoginManager'; + +vi.mock('../../onesignal/userDirector', async (importOriginal) => { + const mod = await importOriginal(); + return { + ...mod, + createUserOnServer: vi.fn().mockResolvedValue(undefined), + }; +}); + +describe('LoginManager', () => { + beforeEach(() => { + TestEnvironment.initialize({ initOneSignalId: true, initUserAndPushSubscription: false }); + vi.restoreAllMocks(); + }); + + test('login: skips when externalId unchanged and logs debug', async () => { + const debugSpy = vi.spyOn(Log, '_debug').mockImplementation(() => undefined as any); + await updateIdentityModel('external_id', 'same-id'); + + await LoginManager.login('same-id'); + expect(debugSpy).toHaveBeenCalledWith('Login: External ID already set, skipping login'); + }); + + test('login: stores token when provided and enqueues operations', async () => { + const dbSpy = vi.spyOn(db, 'put'); + // mock push subscription exists so transfer op enqueues + vi.spyOn(OneSignal._coreDirector, '_getPushSubscriptionModel').mockResolvedValue({ + id: 'push-sub-id', + } as any); + const enqueueSpy = vi.spyOn(OneSignal._coreDirector._operationRepo, '_enqueue').mockResolvedValue(undefined as any); + const enqueueAndWaitSpy = vi + .spyOn(OneSignal._coreDirector._operationRepo, '_enqueueAndWait') + .mockResolvedValue(undefined as any); + + await LoginManager.login('new-external-id', 'jwt-token-123'); + expect(dbSpy).toHaveBeenCalledWith('Ids', { id: 'jwt-token-123', type: 'jwtToken' }); + expect(enqueueSpy).toHaveBeenCalled(); + expect(enqueueAndWaitSpy).toHaveBeenCalled(); + }); + + test('logout: no external id logs debug and returns', async () => { + const debugSpy = vi.spyOn(Log, '_debug').mockImplementation(() => undefined as any); + await updateIdentityModel('external_id', undefined); + await LoginManager.logout(); + expect(debugSpy).toHaveBeenCalledWith('Logout: User is not logged in, skipping logout'); + }); + + test('logout: with external id resets models and creates anonymous user', async () => { + const userDirector = await import('../../onesignal/userDirector'); + await updateIdentityModel('external_id', 'abc'); + await LoginManager.logout(); + expect(userDirector.createUserOnServer).toHaveBeenCalled(); + }); +}); + + + From 35e993e842360ae3571ec1d8b18dfbe4e52ebb59 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Mon, 17 Nov 2025 22:40:18 -0800 Subject: [PATCH 09/46] test(PromptsManager): `_shouldForceSlidedownOverNative` decisions; event hooks install once; native vs slidedown gating --- src/page/managers/PromptsManager.test.ts | 64 ++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/page/managers/PromptsManager.test.ts diff --git a/src/page/managers/PromptsManager.test.ts b/src/page/managers/PromptsManager.test.ts new file mode 100644 index 000000000..ba7dd02b4 --- /dev/null +++ b/src/page/managers/PromptsManager.test.ts @@ -0,0 +1,64 @@ +import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; +import { setupLoadStylesheet } from '__test__/support/helpers/setup'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { PromptsManager } from './PromptsManager'; + +vi.mock('src/shared/useragent/detect', async (importOriginal) => { + const mod = await importOriginal(); + return { + ...mod, + getBrowserName: vi.fn(), + getBrowserVersion: vi.fn(), + isMobileBrowser: vi.fn(), + isTabletBrowser: vi.fn(), + requiresUserInteraction: vi.fn(), + }; +}); + +describe('PromptsManager', () => { + beforeEach(() => { + TestEnvironment.initialize({ initOneSignalId: true, initUserAndPushSubscription: false }); + vi.restoreAllMocks(); + }); + + test('_shouldForceSlidedownOverNative returns true on Chrome>=63 mobile/tablet', async () => { + const detect = await import('src/shared/useragent/detect'); + (detect.getBrowserName as any).mockReturnValue('chrome'); + (detect.getBrowserVersion as any).mockReturnValue(70); + (detect.isMobileBrowser as any).mockReturnValue(true); + (detect.isTabletBrowser as any).mockReturnValue(false); + (detect.requiresUserInteraction as any).mockReturnValue(false); + + const pm = new PromptsManager(OneSignal._context); + // @ts-expect-error private access + expect(pm['_shouldForceSlidedownOverNative']()).toBe(true); + }); + + test('_shouldForceSlidedownOverNative returns true when requiresUserInteraction', async () => { + const detect = await import('src/shared/useragent/detect'); + (detect.getBrowserName as any).mockReturnValue('Firefox'); + (detect.getBrowserVersion as any).mockReturnValue(100); + (detect.isMobileBrowser as any).mockReturnValue(false); + (detect.isTabletBrowser as any).mockReturnValue(false); + (detect.requiresUserInteraction as any).mockReturnValue(true); + + const pm = new PromptsManager(OneSignal._context); + // @ts-expect-error private access + expect(pm['_shouldForceSlidedownOverNative']()).toBe(true); + }); + + test('event hooks install only once for slidedown path', async () => { + await setupLoadStylesheet(); + const pm = new PromptsManager(OneSignal._context); + + // stub _createSlidedown to avoid side effects + vi.spyOn(OneSignal._context._slidedownManager as any, '_createSlidedown').mockResolvedValue(undefined); + const installSpy = vi.spyOn(pm as any, '_installEventHooksForSlidedown'); + + await pm['_internalShowSlidedownPrompt'](); + await pm['_internalShowSlidedownPrompt'](); + expect(installSpy).toHaveBeenCalledTimes(1); + }); +}); + + From c9ab2ccbddbe755885a3888fa040dbb84fca863d Mon Sep 17 00:00:00 2001 From: sherwinski Date: Mon, 17 Nov 2025 22:41:02 -0800 Subject: [PATCH 10/46] test(TagManager): `_sendTags` calls `OneSignal.User.addTags` when diff non-empty; returns `{}` and warns when no diff; stores remote/local tags mutate internal state --- .../managers/tagManager/TagManager.test.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/page/managers/tagManager/TagManager.test.ts diff --git a/src/page/managers/tagManager/TagManager.test.ts b/src/page/managers/tagManager/TagManager.test.ts new file mode 100644 index 000000000..d510a6750 --- /dev/null +++ b/src/page/managers/tagManager/TagManager.test.ts @@ -0,0 +1,48 @@ +import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; +import Log from 'src/shared/libraries/Log'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import TagManager from './TagManager'; + +describe('TagManager', () => { + beforeEach(() => { + TestEnvironment.initialize({ initOneSignalId: true, initUserAndPushSubscription: false }); + vi.restoreAllMocks(); + // ensure addTags exists and is stubbed + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (OneSignal as any).User.addTags = vi.fn().mockResolvedValue(undefined); + }); + + test('_storeTagValuesToUpdate/_storeRemotePlayerTags set internal state', () => { + const tm = new TagManager(OneSignal._context); + tm._storeRemotePlayerTags({ a: 1 }); + tm._storeTagValuesToUpdate({ a: true, b: false }); + expect(OneSignal._context._tagManager._remoteTags).toEqual({ a: 1 }); + // @ts-expect-error private access for test + expect((tm as any)._tagsFromTaggingContainer).toEqual({ a: true, b: false }); + }); + + test('_sendTags calls addTags when diff is non-empty and returns diff', async () => { + const tm = new TagManager(OneSignal._context); + tm._storeRemotePlayerTags({ a: 1 }); + tm._storeTagValuesToUpdate({ a: false, b: true }); // converts to { a:0, b:1 } and diff => { a:0, b:1 } vs { a:1 } -> { a:0, b:1 } + const result = await tm._sendTags(); + expect(OneSignal.User.addTags).toHaveBeenCalledWith(result); + expect(result).toMatchObject({ a: '0', b: '1' }); + }); + + test('_sendTags returns {} and warns when no change', async () => { + const warnSpy = vi.spyOn(Log, '_warn').mockImplementation(() => undefined as any); + const tm = new TagManager(OneSignal._context); + // Ensure this instance uses the same remote tags that will be diffed + // @ts-expect-error private for test + (tm as any)._remoteTags = { c: '1' }; + tm._storeTagValuesToUpdate({ c: true }); // converts to { c:'1' } -> diff {} + + const result = await tm._sendTags(); + expect(result).toEqual({}); + expect(warnSpy).toHaveBeenCalled(); + expect(OneSignal.User.addTags).not.toHaveBeenCalled(); + }); +}); + + From 7b6eb3ed26c60938c91cd227ab8fd9382854f02f Mon Sep 17 00:00:00 2001 From: sherwinski Date: Mon, 17 Nov 2025 22:42:55 -0800 Subject: [PATCH 11/46] test(helpers): `timeoutPromise` resolves and times out, returns raced error on timeout; `enforceAppId`/`enforceAlias` throw and pass when expected --- src/shared/context/helpers.test.ts | 44 ++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/shared/context/helpers.test.ts diff --git a/src/shared/context/helpers.test.ts b/src/shared/context/helpers.test.ts new file mode 100644 index 000000000..026a8fe65 --- /dev/null +++ b/src/shared/context/helpers.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, test, vi } from 'vitest'; +import { enforceAlias, enforceAppId, timeoutPromise } from './helpers'; + +describe('context/helpers', () => { + test('timeoutPromise resolves before timeout', async () => { + vi.useFakeTimers(); + const p = new Promise((resolve) => setTimeout(() => resolve('ok'), 50)); + const raced = timeoutPromise(p, 100); + vi.advanceTimersByTime(60); + await expect(raced).resolves.toBe('ok'); + }); + + test('timeoutPromise rejects on timeout', async () => { + vi.useFakeTimers(); + const never = new Promise((_r) => undefined); + const raced = timeoutPromise(never, 10); + vi.advanceTimersByTime(11); + await expect(raced).rejects.toThrow('Async operation timed out'); + }); + + test('enforceAppId throws on empty', () => { + expect(() => enforceAppId(undefined)).toThrow('App id cannot be empty'); + expect(() => enforceAppId(null as unknown as string)).toThrow( + 'App id cannot be empty', + ); + expect(() => enforceAppId('abc')).not.toThrow(); + }); + + test('enforceAlias throws when label or id is empty', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(() => enforceAlias({ label: '', id: '' } as any)).toThrow( + 'Alias label cannot be empty', + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(() => enforceAlias({ label: 'x', id: '' } as any)).toThrow( + 'Alias id cannot be empty', + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(() => enforceAlias({ label: 'x', id: 'y' } as any)).not.toThrow(); + }); +}); + + + From 907acf81fedee02a1db3f21b2961c5108686b760 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Mon, 17 Nov 2025 22:44:48 -0800 Subject: [PATCH 12/46] test(IDManager): _createLocalId uses LOCAL_PREFIX and produces UUID-like uniqueness --- src/shared/managers/IDManager.test.ts | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/shared/managers/IDManager.test.ts diff --git a/src/shared/managers/IDManager.test.ts b/src/shared/managers/IDManager.test.ts new file mode 100644 index 000000000..6a1fa85d4 --- /dev/null +++ b/src/shared/managers/IDManager.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test, vi } from 'vitest'; +import { IDManager } from './IDManager'; + +describe('IDManager', () => { + test('_createLocalId uses prefix and is unique-ish', () => { + // mock crypto.randomUUID to deterministic value + const uuids = ['u1', 'u2', 'u3']; + let i = 0; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + vi.spyOn(global as any, 'crypto', 'get').mockReturnValue({ + randomUUID: () => uuids[i++], + }); + const a = IDManager._createLocalId(); + const b = IDManager._createLocalId(); + expect(a.startsWith(IDManager.LOCAL_PREFIX)).toBe(true); + expect(b.startsWith(IDManager.LOCAL_PREFIX)).toBe(true); + expect(a).not.toBe(b); + }); + + test('_isLocalId values', () => { + expect(IDManager._isLocalId(`${IDManager.LOCAL_PREFIX}abc`)).toBe(true); + expect(IDManager._isLocalId('xyz')).toBe(false); + expect(IDManager._isLocalId(undefined)).toBe(false); + expect(IDManager._isLocalId(null as unknown as string)).toBe(false); + }); +}); + + + From 631d23043d87cc4c787c9448016efcadb298431c Mon Sep 17 00:00:00 2001 From: sherwinski Date: Mon, 17 Nov 2025 22:47:46 -0800 Subject: [PATCH 13/46] test(PermissionManager): add tests for core functionality - Maps Notification permission states - guards for unavailable API in environment --- src/shared/managers/PermissionManager.test.ts | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/shared/managers/PermissionManager.test.ts diff --git a/src/shared/managers/PermissionManager.test.ts b/src/shared/managers/PermissionManager.test.ts new file mode 100644 index 000000000..1e90c41b0 --- /dev/null +++ b/src/shared/managers/PermissionManager.test.ts @@ -0,0 +1,61 @@ +import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import PermissionManager from './PermissionManager'; + +vi.mock('../environment/detect', async (importOriginal) => { + const mod = await importOriginal(); + return { + ...mod, + useSafariLegacyPush: vi.fn(), + }; +}); + +describe('PermissionManager', () => { + beforeEach(() => { + TestEnvironment.initialize({ initOneSignalId: true, initUserAndPushSubscription: false }); + vi.restoreAllMocks(); + }); + + test('_getPermissionStatus resolves from context manager', async () => { + const pm = new PermissionManager(); + const spy = vi + // @ts-expect-error private access + .spyOn(OneSignal._context._permissionManager, '_getNotificationPermission') + .mockResolvedValue('granted'); + await expect(pm._getPermissionStatus()).resolves.toBe('granted'); + expect(spy).toHaveBeenCalled(); + }); + + test('_getNotificationPermission uses legacy Safari path and requires webId', async () => { + const detect = await import('../environment/detect'); + (detect.useSafariLegacyPush as any).mockReturnValue(true); + const pm = new PermissionManager(); + + // Missing ID throws + await expect(pm._getNotificationPermission(undefined)).rejects.toThrow( + '"safariWebId" is empty', + ); + // emulate Safari API + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (globalThis as any).safari = { + pushNotification: { + permission: () => ({ permission: 'denied' }), + }, + }; + // When provided ID, it should read from safari path + const result = await pm._getNotificationPermission('webid'); + expect(result).toBe('denied'); + }); + + test('_getNotificationPermission uses W3C Notification.permission when not legacy', async () => { + const detect = await import('../environment/detect'); + (detect.useSafariLegacyPush as any).mockReturnValue(false); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (globalThis as any).Notification = { permission: 'default' }; + + const pm = new PermissionManager(); + await expect(pm['_getNotificationPermission']()).resolves?.toBe('default'); + }); +}); + + From a2e98bcab74a16589ad3de7bb6511d52b337e45d Mon Sep 17 00:00:00 2001 From: sherwinski Date: Tue, 18 Nov 2025 11:45:20 -0800 Subject: [PATCH 14/46] test: remove unnecessary vitest global imports --- src/page/bell/Badge.test.ts | 1 - src/page/bell/Bell.test.ts | 2 +- src/page/bell/Button.test.ts | 2 +- src/page/bell/Dialog.test.ts | 2 +- src/page/bell/Launcher.test.ts | 1 - src/page/bell/Message.test.ts | 2 +- src/page/managers/LoginManager.test.ts | 2 +- src/page/managers/PromptsManager.test.ts | 2 +- src/page/managers/tagManager/TagManager.test.ts | 2 +- src/shared/context/helpers.test.ts | 2 +- src/shared/managers/IDManager.test.ts | 2 +- src/shared/managers/PermissionManager.test.ts | 2 +- 12 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/page/bell/Badge.test.ts b/src/page/bell/Badge.test.ts index 791d3db69..67c75c8c3 100644 --- a/src/page/bell/Badge.test.ts +++ b/src/page/bell/Badge.test.ts @@ -1,4 +1,3 @@ -import { beforeEach, describe, expect, test } from 'vitest'; import Badge from './Badge'; describe('Badge', () => { diff --git a/src/page/bell/Bell.test.ts b/src/page/bell/Bell.test.ts index d5590898d..383057884 100644 --- a/src/page/bell/Bell.test.ts +++ b/src/page/bell/Bell.test.ts @@ -1,5 +1,5 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; -import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { vi } from 'vitest'; import OneSignalEvent from '../../shared/services/OneSignalEvent'; import Bell from './Bell'; diff --git a/src/page/bell/Button.test.ts b/src/page/bell/Button.test.ts index 3a5c40ebb..8c51df36c 100644 --- a/src/page/bell/Button.test.ts +++ b/src/page/bell/Button.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { vi } from 'vitest'; import Bell from './Bell'; import Button from './Button'; import { MessageType } from './constants'; diff --git a/src/page/bell/Dialog.test.ts b/src/page/bell/Dialog.test.ts index 0b2c057b1..5dc37a149 100644 --- a/src/page/bell/Dialog.test.ts +++ b/src/page/bell/Dialog.test.ts @@ -1,5 +1,5 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; -import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { vi } from 'vitest'; import Bell from './Bell'; import Dialog from './Dialog'; import { BellState } from './constants'; diff --git a/src/page/bell/Launcher.test.ts b/src/page/bell/Launcher.test.ts index c1c5c7c49..5e1e48643 100644 --- a/src/page/bell/Launcher.test.ts +++ b/src/page/bell/Launcher.test.ts @@ -1,4 +1,3 @@ -import { beforeEach, describe, expect, test } from 'vitest'; import Bell from './Bell'; import Launcher from './Launcher'; diff --git a/src/page/bell/Message.test.ts b/src/page/bell/Message.test.ts index 81bd95e89..c70366fab 100644 --- a/src/page/bell/Message.test.ts +++ b/src/page/bell/Message.test.ts @@ -1,5 +1,5 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; -import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { vi } from 'vitest'; import Bell from './Bell'; import Message from './Message'; diff --git a/src/page/managers/LoginManager.test.ts b/src/page/managers/LoginManager.test.ts index 69923a04b..0881289f2 100644 --- a/src/page/managers/LoginManager.test.ts +++ b/src/page/managers/LoginManager.test.ts @@ -2,7 +2,7 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { updateIdentityModel } from '__test__/support/helpers/setup'; import { db } from 'src/shared/database/client'; import Log from 'src/shared/libraries/Log'; -import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { vi } from 'vitest'; import LoginManager from './LoginManager'; vi.mock('../../onesignal/userDirector', async (importOriginal) => { diff --git a/src/page/managers/PromptsManager.test.ts b/src/page/managers/PromptsManager.test.ts index ba7dd02b4..14105e5e7 100644 --- a/src/page/managers/PromptsManager.test.ts +++ b/src/page/managers/PromptsManager.test.ts @@ -1,6 +1,6 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { setupLoadStylesheet } from '__test__/support/helpers/setup'; -import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { vi } from 'vitest'; import { PromptsManager } from './PromptsManager'; vi.mock('src/shared/useragent/detect', async (importOriginal) => { diff --git a/src/page/managers/tagManager/TagManager.test.ts b/src/page/managers/tagManager/TagManager.test.ts index d510a6750..5e0517c1e 100644 --- a/src/page/managers/tagManager/TagManager.test.ts +++ b/src/page/managers/tagManager/TagManager.test.ts @@ -1,6 +1,6 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import Log from 'src/shared/libraries/Log'; -import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { vi } from 'vitest'; import TagManager from './TagManager'; describe('TagManager', () => { diff --git a/src/shared/context/helpers.test.ts b/src/shared/context/helpers.test.ts index 026a8fe65..939bd5781 100644 --- a/src/shared/context/helpers.test.ts +++ b/src/shared/context/helpers.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test, vi } from 'vitest'; +import { vi } from 'vitest'; import { enforceAlias, enforceAppId, timeoutPromise } from './helpers'; describe('context/helpers', () => { diff --git a/src/shared/managers/IDManager.test.ts b/src/shared/managers/IDManager.test.ts index 6a1fa85d4..39dbebbb3 100644 --- a/src/shared/managers/IDManager.test.ts +++ b/src/shared/managers/IDManager.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test, vi } from 'vitest'; +import { vi } from 'vitest'; import { IDManager } from './IDManager'; describe('IDManager', () => { diff --git a/src/shared/managers/PermissionManager.test.ts b/src/shared/managers/PermissionManager.test.ts index 1e90c41b0..2f2e04a9e 100644 --- a/src/shared/managers/PermissionManager.test.ts +++ b/src/shared/managers/PermissionManager.test.ts @@ -1,5 +1,5 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; -import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { vi } from 'vitest'; import PermissionManager from './PermissionManager'; vi.mock('../environment/detect', async (importOriginal) => { From e10d88baa6110f639d04692b1df9f0ffae16053b Mon Sep 17 00:00:00 2001 From: sherwinski Date: Tue, 18 Nov 2025 11:52:02 -0800 Subject: [PATCH 15/46] test: remove unnecessary ts-expext-error comments --- src/page/managers/PromptsManager.test.ts | 2 -- .../managers/slidedownManager/SlidedownManager.test.ts | 1 - src/page/managers/tagManager/TagManager.test.ts | 8 +++----- src/shared/managers/PermissionManager.test.ts | 1 - src/shared/managers/sessionManager/SessionManager.test.ts | 1 - 5 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/page/managers/PromptsManager.test.ts b/src/page/managers/PromptsManager.test.ts index 14105e5e7..a42d16420 100644 --- a/src/page/managers/PromptsManager.test.ts +++ b/src/page/managers/PromptsManager.test.ts @@ -30,7 +30,6 @@ describe('PromptsManager', () => { (detect.requiresUserInteraction as any).mockReturnValue(false); const pm = new PromptsManager(OneSignal._context); - // @ts-expect-error private access expect(pm['_shouldForceSlidedownOverNative']()).toBe(true); }); @@ -43,7 +42,6 @@ describe('PromptsManager', () => { (detect.requiresUserInteraction as any).mockReturnValue(true); const pm = new PromptsManager(OneSignal._context); - // @ts-expect-error private access expect(pm['_shouldForceSlidedownOverNative']()).toBe(true); }); diff --git a/src/page/managers/slidedownManager/SlidedownManager.test.ts b/src/page/managers/slidedownManager/SlidedownManager.test.ts index 88163bc6c..b9c37256e 100644 --- a/src/page/managers/slidedownManager/SlidedownManager.test.ts +++ b/src/page/managers/slidedownManager/SlidedownManager.test.ts @@ -314,7 +314,6 @@ export const mockPhoneLibraryLoading = () => { window.intlTelInputUtils = { numberType: { MOBILE: 1 }, - // @ts-expect-error - mock intl-tel-input numberFormat: { E164: 0 }, }; return Promise.resolve(); diff --git a/src/page/managers/tagManager/TagManager.test.ts b/src/page/managers/tagManager/TagManager.test.ts index 5e0517c1e..96112fdb3 100644 --- a/src/page/managers/tagManager/TagManager.test.ts +++ b/src/page/managers/tagManager/TagManager.test.ts @@ -14,16 +14,15 @@ describe('TagManager', () => { test('_storeTagValuesToUpdate/_storeRemotePlayerTags set internal state', () => { const tm = new TagManager(OneSignal._context); - tm._storeRemotePlayerTags({ a: 1 }); + tm._storeRemotePlayerTags({ a: '1' }); tm._storeTagValuesToUpdate({ a: true, b: false }); - expect(OneSignal._context._tagManager._remoteTags).toEqual({ a: 1 }); - // @ts-expect-error private access for test + expect(OneSignal._context._tagManager._remoteTags).toEqual({ a: '1' }); expect((tm as any)._tagsFromTaggingContainer).toEqual({ a: true, b: false }); }); test('_sendTags calls addTags when diff is non-empty and returns diff', async () => { const tm = new TagManager(OneSignal._context); - tm._storeRemotePlayerTags({ a: 1 }); + tm._storeRemotePlayerTags({ a: '1' }); tm._storeTagValuesToUpdate({ a: false, b: true }); // converts to { a:0, b:1 } and diff => { a:0, b:1 } vs { a:1 } -> { a:0, b:1 } const result = await tm._sendTags(); expect(OneSignal.User.addTags).toHaveBeenCalledWith(result); @@ -34,7 +33,6 @@ describe('TagManager', () => { const warnSpy = vi.spyOn(Log, '_warn').mockImplementation(() => undefined as any); const tm = new TagManager(OneSignal._context); // Ensure this instance uses the same remote tags that will be diffed - // @ts-expect-error private for test (tm as any)._remoteTags = { c: '1' }; tm._storeTagValuesToUpdate({ c: true }); // converts to { c:'1' } -> diff {} diff --git a/src/shared/managers/PermissionManager.test.ts b/src/shared/managers/PermissionManager.test.ts index 2f2e04a9e..92c2bc9f9 100644 --- a/src/shared/managers/PermissionManager.test.ts +++ b/src/shared/managers/PermissionManager.test.ts @@ -19,7 +19,6 @@ describe('PermissionManager', () => { test('_getPermissionStatus resolves from context manager', async () => { const pm = new PermissionManager(); const spy = vi - // @ts-expect-error private access .spyOn(OneSignal._context._permissionManager, '_getNotificationPermission') .mockResolvedValue('granted'); await expect(pm._getPermissionStatus()).resolves.toBe('granted'); diff --git a/src/shared/managers/sessionManager/SessionManager.test.ts b/src/shared/managers/sessionManager/SessionManager.test.ts index 95d0f3ce0..0878311ba 100644 --- a/src/shared/managers/sessionManager/SessionManager.test.ts +++ b/src/shared/managers/sessionManager/SessionManager.test.ts @@ -139,7 +139,6 @@ describe('SessionManager', () => { vi.spyOn(detect, 'supportsServiceWorkers').mockReturnValue(true); const sm = new SessionManager(OneSignal._context); const unicastSpy = vi - // @ts-expect-error private access .spyOn(OneSignal._context._workerMessenger, '_unicast') .mockResolvedValue(undefined as any); From 8843f8edc88841246ad63dc920e4609da0cfaea1 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Tue, 18 Nov 2025 11:59:07 -0800 Subject: [PATCH 16/46] test: remove unnecessary vi global import --- src/page/bell/Bell.test.ts | 1 - src/page/bell/Button.test.ts | 1 - src/page/bell/Dialog.test.ts | 1 - src/page/bell/Message.test.ts | 1 - src/page/managers/LoginManager.test.ts | 1 - src/page/managers/PromptsManager.test.ts | 1 - src/page/managers/tagManager/TagManager.test.ts | 1 - src/shared/context/helpers.test.ts | 1 - src/shared/managers/IDManager.test.ts | 1 - src/shared/managers/PermissionManager.test.ts | 1 - 10 files changed, 10 deletions(-) diff --git a/src/page/bell/Bell.test.ts b/src/page/bell/Bell.test.ts index 383057884..240540658 100644 --- a/src/page/bell/Bell.test.ts +++ b/src/page/bell/Bell.test.ts @@ -1,5 +1,4 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; -import { vi } from 'vitest'; import OneSignalEvent from '../../shared/services/OneSignalEvent'; import Bell from './Bell'; diff --git a/src/page/bell/Button.test.ts b/src/page/bell/Button.test.ts index 8c51df36c..9d035eafb 100644 --- a/src/page/bell/Button.test.ts +++ b/src/page/bell/Button.test.ts @@ -1,4 +1,3 @@ -import { vi } from 'vitest'; import Bell from './Bell'; import Button from './Button'; import { MessageType } from './constants'; diff --git a/src/page/bell/Dialog.test.ts b/src/page/bell/Dialog.test.ts index 5dc37a149..5d0fbed7d 100644 --- a/src/page/bell/Dialog.test.ts +++ b/src/page/bell/Dialog.test.ts @@ -1,5 +1,4 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; -import { vi } from 'vitest'; import Bell from './Bell'; import Dialog from './Dialog'; import { BellState } from './constants'; diff --git a/src/page/bell/Message.test.ts b/src/page/bell/Message.test.ts index c70366fab..eb4c97f4e 100644 --- a/src/page/bell/Message.test.ts +++ b/src/page/bell/Message.test.ts @@ -1,5 +1,4 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; -import { vi } from 'vitest'; import Bell from './Bell'; import Message from './Message'; diff --git a/src/page/managers/LoginManager.test.ts b/src/page/managers/LoginManager.test.ts index 0881289f2..145ca3c58 100644 --- a/src/page/managers/LoginManager.test.ts +++ b/src/page/managers/LoginManager.test.ts @@ -2,7 +2,6 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { updateIdentityModel } from '__test__/support/helpers/setup'; import { db } from 'src/shared/database/client'; import Log from 'src/shared/libraries/Log'; -import { vi } from 'vitest'; import LoginManager from './LoginManager'; vi.mock('../../onesignal/userDirector', async (importOriginal) => { diff --git a/src/page/managers/PromptsManager.test.ts b/src/page/managers/PromptsManager.test.ts index a42d16420..04ea0b02c 100644 --- a/src/page/managers/PromptsManager.test.ts +++ b/src/page/managers/PromptsManager.test.ts @@ -1,6 +1,5 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { setupLoadStylesheet } from '__test__/support/helpers/setup'; -import { vi } from 'vitest'; import { PromptsManager } from './PromptsManager'; vi.mock('src/shared/useragent/detect', async (importOriginal) => { diff --git a/src/page/managers/tagManager/TagManager.test.ts b/src/page/managers/tagManager/TagManager.test.ts index 96112fdb3..1d21ecee6 100644 --- a/src/page/managers/tagManager/TagManager.test.ts +++ b/src/page/managers/tagManager/TagManager.test.ts @@ -1,6 +1,5 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import Log from 'src/shared/libraries/Log'; -import { vi } from 'vitest'; import TagManager from './TagManager'; describe('TagManager', () => { diff --git a/src/shared/context/helpers.test.ts b/src/shared/context/helpers.test.ts index 939bd5781..43633a045 100644 --- a/src/shared/context/helpers.test.ts +++ b/src/shared/context/helpers.test.ts @@ -1,4 +1,3 @@ -import { vi } from 'vitest'; import { enforceAlias, enforceAppId, timeoutPromise } from './helpers'; describe('context/helpers', () => { diff --git a/src/shared/managers/IDManager.test.ts b/src/shared/managers/IDManager.test.ts index 39dbebbb3..ea0c69d21 100644 --- a/src/shared/managers/IDManager.test.ts +++ b/src/shared/managers/IDManager.test.ts @@ -1,4 +1,3 @@ -import { vi } from 'vitest'; import { IDManager } from './IDManager'; describe('IDManager', () => { diff --git a/src/shared/managers/PermissionManager.test.ts b/src/shared/managers/PermissionManager.test.ts index 92c2bc9f9..2c3499992 100644 --- a/src/shared/managers/PermissionManager.test.ts +++ b/src/shared/managers/PermissionManager.test.ts @@ -1,5 +1,4 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; -import { vi } from 'vitest'; import PermissionManager from './PermissionManager'; vi.mock('../environment/detect', async (importOriginal) => { From 99fe01c369eef5deaa99fd18d59a8260c3a391c4 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Tue, 18 Nov 2025 15:05:04 -0800 Subject: [PATCH 17/46] test(PermissionManager): improve tests - use `MockNotification` helper - add no context test for `_getPermissionStatus` --- src/shared/managers/PermissionManager.test.ts | 53 +++++++++---------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/src/shared/managers/PermissionManager.test.ts b/src/shared/managers/PermissionManager.test.ts index 2c3499992..27f8ceae8 100644 --- a/src/shared/managers/PermissionManager.test.ts +++ b/src/shared/managers/PermissionManager.test.ts @@ -1,4 +1,6 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; +import MockNotification from '__test__/support/mocks/MockNotification'; +import * as detect from '../environment/detect'; import PermissionManager from './PermissionManager'; vi.mock('../environment/detect', async (importOriginal) => { @@ -15,41 +17,36 @@ describe('PermissionManager', () => { vi.restoreAllMocks(); }); - test('_getPermissionStatus resolves from context manager', async () => { - const pm = new PermissionManager(); - const spy = vi - .spyOn(OneSignal._context._permissionManager, '_getNotificationPermission') - .mockResolvedValue('granted'); - await expect(pm._getPermissionStatus()).resolves.toBe('granted'); - expect(spy).toHaveBeenCalled(); + describe('_getPermissionStatus', () => { + test('resolves from context manager', async () => { + const pm = new PermissionManager(); + const spy = vi + .spyOn(OneSignal._context._permissionManager, '_getNotificationPermission') + .mockResolvedValue('granted'); + await expect(pm._getPermissionStatus()).resolves.toBe('granted'); + expect(spy).toHaveBeenCalled(); + }); + + test('throws if context is undefined', async () => { + OneSignal._context = undefined; + const pm = new PermissionManager(); + await expect(pm._getPermissionStatus()).rejects.toThrow('OneSignal.context is undefined. Call init first'); + }); }); test('_getNotificationPermission uses legacy Safari path and requires webId', async () => { - const detect = await import('../environment/detect'); - (detect.useSafariLegacyPush as any).mockReturnValue(true); - const pm = new PermissionManager(); + vi.spyOn(detect, 'useSafariLegacyPush') + .mockReturnValue(true); - // Missing ID throws - await expect(pm._getNotificationPermission(undefined)).rejects.toThrow( - '"safariWebId" is empty', - ); - // emulate Safari API - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (globalThis as any).safari = { - pushNotification: { - permission: () => ({ permission: 'denied' }), - }, - }; - // When provided ID, it should read from safari path - const result = await pm._getNotificationPermission('webid'); - expect(result).toBe('denied'); + const pm = new PermissionManager(); + await expect(pm._getNotificationPermission(undefined)) + .rejects.toThrow('"safariWebId" is empty'); }); test('_getNotificationPermission uses W3C Notification.permission when not legacy', async () => { - const detect = await import('../environment/detect'); - (detect.useSafariLegacyPush as any).mockReturnValue(false); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (globalThis as any).Notification = { permission: 'default' }; + vi.spyOn(detect, 'useSafariLegacyPush') + .mockReturnValue(false); + MockNotification.permission = 'default'; const pm = new PermissionManager(); await expect(pm['_getNotificationPermission']()).resolves?.toBe('default'); From 2c9807988e9566498fb4ff1de5767ea65bd1a3f4 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Tue, 18 Nov 2025 15:07:51 -0800 Subject: [PATCH 18/46] test: remove unnecessary `initialize` parameters and `restoreAllMocks` --- src/page/bell/Bell.test.ts | 2 +- src/page/bell/Dialog.test.ts | 2 +- src/page/bell/Message.test.ts | 2 +- src/page/managers/LoginManager.test.ts | 5 ++--- src/page/managers/PromptsManager.test.ts | 5 ++--- src/page/managers/tagManager/TagManager.test.ts | 5 ++--- src/shared/helpers/init.test.ts | 3 +-- src/shared/managers/PermissionManager.test.ts | 3 +-- 8 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/page/bell/Bell.test.ts b/src/page/bell/Bell.test.ts index 240540658..677aa7b62 100644 --- a/src/page/bell/Bell.test.ts +++ b/src/page/bell/Bell.test.ts @@ -5,7 +5,7 @@ import Bell from './Bell'; describe('Bell', () => { beforeEach(() => { // Set up OneSignal globals/context to avoid accidental runtime lookups - TestEnvironment.initialize({ initOneSignalId: false }); + TestEnvironment.initialize(); }); test('constructor early-returns when enable=false and applies defaults', () => { diff --git a/src/page/bell/Dialog.test.ts b/src/page/bell/Dialog.test.ts index 5d0fbed7d..ee4bfac51 100644 --- a/src/page/bell/Dialog.test.ts +++ b/src/page/bell/Dialog.test.ts @@ -5,7 +5,7 @@ import { BellState } from './constants'; describe('Dialog', () => { beforeEach(() => { - TestEnvironment.initialize({ initOneSignalId: false }); + TestEnvironment.initialize(); document.body.innerHTML = `
diff --git a/src/page/bell/Message.test.ts b/src/page/bell/Message.test.ts index eb4c97f4e..027329c5a 100644 --- a/src/page/bell/Message.test.ts +++ b/src/page/bell/Message.test.ts @@ -4,7 +4,7 @@ import Message from './Message'; describe('Message', () => { beforeEach(() => { - TestEnvironment.initialize({ initOneSignalId: false }); + TestEnvironment.initialize(); document.body.innerHTML = `
diff --git a/src/page/managers/LoginManager.test.ts b/src/page/managers/LoginManager.test.ts index 145ca3c58..101474b40 100644 --- a/src/page/managers/LoginManager.test.ts +++ b/src/page/managers/LoginManager.test.ts @@ -14,9 +14,8 @@ vi.mock('../../onesignal/userDirector', async (importOriginal) => { describe('LoginManager', () => { beforeEach(() => { - TestEnvironment.initialize({ initOneSignalId: true, initUserAndPushSubscription: false }); - vi.restoreAllMocks(); - }); + TestEnvironment.initialize(); + }); test('login: skips when externalId unchanged and logs debug', async () => { const debugSpy = vi.spyOn(Log, '_debug').mockImplementation(() => undefined as any); diff --git a/src/page/managers/PromptsManager.test.ts b/src/page/managers/PromptsManager.test.ts index 04ea0b02c..70d09e41a 100644 --- a/src/page/managers/PromptsManager.test.ts +++ b/src/page/managers/PromptsManager.test.ts @@ -16,9 +16,8 @@ vi.mock('src/shared/useragent/detect', async (importOriginal) => { describe('PromptsManager', () => { beforeEach(() => { - TestEnvironment.initialize({ initOneSignalId: true, initUserAndPushSubscription: false }); - vi.restoreAllMocks(); - }); + TestEnvironment.initialize(); + }); test('_shouldForceSlidedownOverNative returns true on Chrome>=63 mobile/tablet', async () => { const detect = await import('src/shared/useragent/detect'); diff --git a/src/page/managers/tagManager/TagManager.test.ts b/src/page/managers/tagManager/TagManager.test.ts index 1d21ecee6..3ba588c1e 100644 --- a/src/page/managers/tagManager/TagManager.test.ts +++ b/src/page/managers/tagManager/TagManager.test.ts @@ -4,9 +4,8 @@ import TagManager from './TagManager'; describe('TagManager', () => { beforeEach(() => { - TestEnvironment.initialize({ initOneSignalId: true, initUserAndPushSubscription: false }); - vi.restoreAllMocks(); - // ensure addTags exists and is stubbed + TestEnvironment.initialize(); + // ensure addTags exists and is stubbed // eslint-disable-next-line @typescript-eslint/no-explicit-any (OneSignal as any).User.addTags = vi.fn().mockResolvedValue(undefined); }); diff --git a/src/shared/helpers/init.test.ts b/src/shared/helpers/init.test.ts index 38c75260e..ab189f9a8 100644 --- a/src/shared/helpers/init.test.ts +++ b/src/shared/helpers/init.test.ts @@ -17,8 +17,7 @@ beforeEach(() => { }); afterEach(() => { - vi.restoreAllMocks(); -}); + }); /** onSdkInitialized */ test('onSdkInitialized: ensure public sdk initialized triggered', async () => { diff --git a/src/shared/managers/PermissionManager.test.ts b/src/shared/managers/PermissionManager.test.ts index 27f8ceae8..1e256f0da 100644 --- a/src/shared/managers/PermissionManager.test.ts +++ b/src/shared/managers/PermissionManager.test.ts @@ -13,8 +13,7 @@ vi.mock('../environment/detect', async (importOriginal) => { describe('PermissionManager', () => { beforeEach(() => { - TestEnvironment.initialize({ initOneSignalId: true, initUserAndPushSubscription: false }); - vi.restoreAllMocks(); + TestEnvironment.initialize(); }); describe('_getPermissionStatus', () => { From 7c8f9b6f9a69fd41bd1252cbb68563bb368be7a0 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Tue, 18 Nov 2025 15:35:15 -0800 Subject: [PATCH 19/46] chore(scripts): add task for fixing lint issues --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index b00e8f78e..51f056847 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "test": "vitest run --coverage", "test:watch": "vitest watch --coverage", "validate:build": "node build/scripts/validate.js", - "lint": "eslint src --ext .js,.jsx,.ts,.tsx; prettylint 'src/**/*' 'test/**/*' '__test__/**/*' --no-editorconfig" + "lint": "eslint src --ext .js,.jsx,.ts,.tsx; prettylint 'src/**/*' 'test/**/*' '__test__/**/*' --no-editorconfig", + "lint:fix": "eslint src --ext .js,.jsx,.ts,.tsx --fix; prettylint 'src/**/*' 'test/**/*' '__test__/**/*' --no-editorconfig --fix" }, "config": { "sdkVersion": "160510" From 76837095f1e2b4264d12cb2594f0dcec28482e07 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Tue, 18 Nov 2025 15:36:50 -0800 Subject: [PATCH 20/46] test: fix linting issues; remove unnecessary `as any` where possible --- src/page/bell/Badge.test.ts | 2 - src/page/bell/Bell.test.ts | 21 ++-------- src/page/bell/Button.test.ts | 3 -- src/page/bell/Dialog.test.ts | 5 --- src/page/bell/Launcher.test.ts | 6 +-- src/page/bell/Message.test.ts | 5 +-- src/page/managers/LoginManager.test.ts | 40 +++++++++++++------ src/page/managers/PromptsManager.test.ts | 35 ++++++++-------- .../managers/tagManager/TagManager.test.ts | 17 ++++---- src/shared/context/helpers.test.ts | 12 ++---- src/shared/helpers/init.test.ts | 3 -- src/shared/managers/IDManager.test.ts | 4 -- src/shared/managers/PermissionManager.test.ts | 25 +++++++----- .../sessionManager/SessionManager.test.ts | 21 ++++++---- 14 files changed, 92 insertions(+), 107 deletions(-) diff --git a/src/page/bell/Badge.test.ts b/src/page/bell/Badge.test.ts index 67c75c8c3..19be85913 100644 --- a/src/page/bell/Badge.test.ts +++ b/src/page/bell/Badge.test.ts @@ -27,5 +27,3 @@ describe('Badge', () => { expect(badge['_content']).toBe(''); }); }); - - diff --git a/src/page/bell/Bell.test.ts b/src/page/bell/Bell.test.ts index 677aa7b62..7bf97a834 100644 --- a/src/page/bell/Bell.test.ts +++ b/src/page/bell/Bell.test.ts @@ -9,16 +9,8 @@ describe('Bell', () => { }); test('constructor early-returns when enable=false and applies defaults', () => { - const spyInstall = vi.spyOn( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (Bell.prototype as any), - '_installEventHooks', - ); - const spyUpdate = vi.spyOn( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (Bell.prototype as any), - '_updateState', - ); + const spyInstall = vi.spyOn(Bell.prototype as any, '_installEventHooks'); + const spyUpdate = vi.spyOn(Bell.prototype, '_updateState'); const bell = new Bell({ enable: false }); expect(bell._options.size).toBe('medium'); @@ -30,12 +22,10 @@ describe('Bell', () => { test('constructor validates and installs hooks when enable=true', () => { const spyInstall = vi - // eslint-disable-next-line @typescript-eslint/no-explicit-any .spyOn(Bell.prototype as any, '_installEventHooks') .mockImplementation(() => undefined); const spyUpdate = vi - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .spyOn(Bell.prototype as any, '_updateState') + .spyOn(Bell.prototype, '_updateState') .mockImplementation(() => undefined); // Valid non-defaults to ensure validation path runs @@ -56,10 +46,7 @@ describe('Bell', () => { const bell = new Bell({ enable: false }); const trigger = vi.spyOn(OneSignalEvent, '_trigger'); // transition should emit - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (bell as any)._setState(1 /* _Subscribed */); + bell._setState(1 /* _Subscribed */); expect(trigger).toHaveBeenCalled(); }); }); - - diff --git a/src/page/bell/Button.test.ts b/src/page/bell/Button.test.ts index 9d035eafb..f273ceac5 100644 --- a/src/page/bell/Button.test.ts +++ b/src/page/bell/Button.test.ts @@ -8,7 +8,6 @@ describe('Button', () => {
`; - // eslint-disable-next-line @typescript-eslint/no-explicit-any (globalThis as any).OneSignal = (globalThis as any).OneSignal ?? { _emitter: { once: () => undefined, @@ -39,5 +38,3 @@ describe('Button', () => { expect(button['_isHandlingClick']).toBe(false); }); }); - - diff --git a/src/page/bell/Dialog.test.ts b/src/page/bell/Dialog.test.ts index ee4bfac51..b7dfc5f84 100644 --- a/src/page/bell/Dialog.test.ts +++ b/src/page/bell/Dialog.test.ts @@ -12,14 +12,12 @@ describe('Dialog', () => {
`; // Polyfill Web Animations API method used by AnimatedElement - // eslint-disable-next-line @typescript-eslint/no-explicit-any (HTMLElement.prototype as any).getAnimations = () => []; }); test('_show populates content and toggles shown flag', async () => { const bell = new Bell({ enable: false }); // Put bell in unsubscribed state to render subscribe button - // eslint-disable-next-line @typescript-eslint/no-explicit-any (bell as any)._state = BellState._Unsubscribed; // Force subscription manager response to "disabled" const sm = OneSignal._context._subscriptionManager as any; @@ -35,7 +33,6 @@ describe('Dialog', () => { test('_hide removes shown class and keeps state consistent', async () => { const bell = new Bell({ enable: false }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any (bell as any)._state = BellState._Unsubscribed; const sm = OneSignal._context._subscriptionManager as any; vi.spyOn(sm, '_isPushNotificationsEnabled').mockResolvedValue(false); @@ -47,5 +44,3 @@ describe('Dialog', () => { expect(dialog['_shown']).toBe(false); }); }); - - diff --git a/src/page/bell/Launcher.test.ts b/src/page/bell/Launcher.test.ts index 5e1e48643..e5c351c29 100644 --- a/src/page/bell/Launcher.test.ts +++ b/src/page/bell/Launcher.test.ts @@ -7,10 +7,8 @@ describe('Launcher', () => {
`; // Polyfill Web Animations API method used by AnimatedElement - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (HTMLElement.prototype as any).getAnimations = () => []; + HTMLElement.prototype.getAnimations = () => []; // Minimal OneSignal to satisfy any optional accesses down the line - // eslint-disable-next-line @typescript-eslint/no-explicit-any (globalThis as any).OneSignal = (globalThis as any).OneSignal ?? {}; }); @@ -42,5 +40,3 @@ describe('Launcher', () => { expect(launcher['_active']).toBe(false); }); }); - - diff --git a/src/page/bell/Message.test.ts b/src/page/bell/Message.test.ts index 027329c5a..9917bcef7 100644 --- a/src/page/bell/Message.test.ts +++ b/src/page/bell/Message.test.ts @@ -12,8 +12,7 @@ describe('Message', () => {
`; // Polyfill Web Animations API method used by AnimatedElement - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (HTMLElement.prototype as any).getAnimations = () => []; + HTMLElement.prototype.getAnimations = () => []; vi.useFakeTimers(); }); @@ -29,5 +28,3 @@ describe('Message', () => { expect(message['_shown']).toBe(false); }); }); - - diff --git a/src/page/managers/LoginManager.test.ts b/src/page/managers/LoginManager.test.ts index 101474b40..27bcec998 100644 --- a/src/page/managers/LoginManager.test.ts +++ b/src/page/managers/LoginManager.test.ts @@ -5,7 +5,8 @@ import Log from 'src/shared/libraries/Log'; import LoginManager from './LoginManager'; vi.mock('../../onesignal/userDirector', async (importOriginal) => { - const mod = await importOriginal(); + const mod = + await importOriginal(); return { ...mod, createUserOnServer: vi.fn().mockResolvedValue(undefined), @@ -15,38 +16,54 @@ vi.mock('../../onesignal/userDirector', async (importOriginal) => { describe('LoginManager', () => { beforeEach(() => { TestEnvironment.initialize(); - }); + }); test('login: skips when externalId unchanged and logs debug', async () => { - const debugSpy = vi.spyOn(Log, '_debug').mockImplementation(() => undefined as any); + const debugSpy = vi + .spyOn(Log, '_debug') + .mockImplementation(() => undefined); await updateIdentityModel('external_id', 'same-id'); await LoginManager.login('same-id'); - expect(debugSpy).toHaveBeenCalledWith('Login: External ID already set, skipping login'); + expect(debugSpy).toHaveBeenCalledWith( + 'Login: External ID already set, skipping login', + ); }); test('login: stores token when provided and enqueues operations', async () => { const dbSpy = vi.spyOn(db, 'put'); // mock push subscription exists so transfer op enqueues - vi.spyOn(OneSignal._coreDirector, '_getPushSubscriptionModel').mockResolvedValue({ + vi.spyOn( + OneSignal._coreDirector, + '_getPushSubscriptionModel', + ).mockResolvedValue({ id: 'push-sub-id', } as any); - const enqueueSpy = vi.spyOn(OneSignal._coreDirector._operationRepo, '_enqueue').mockResolvedValue(undefined as any); + const enqueueSpy = vi + .spyOn(OneSignal._coreDirector._operationRepo, '_enqueue') + .mockResolvedValue(undefined); const enqueueAndWaitSpy = vi .spyOn(OneSignal._coreDirector._operationRepo, '_enqueueAndWait') - .mockResolvedValue(undefined as any); + .mockResolvedValue(undefined); await LoginManager.login('new-external-id', 'jwt-token-123'); - expect(dbSpy).toHaveBeenCalledWith('Ids', { id: 'jwt-token-123', type: 'jwtToken' }); + expect(dbSpy).toHaveBeenCalledWith('Ids', { + id: 'jwt-token-123', + type: 'jwtToken', + }); expect(enqueueSpy).toHaveBeenCalled(); expect(enqueueAndWaitSpy).toHaveBeenCalled(); }); test('logout: no external id logs debug and returns', async () => { - const debugSpy = vi.spyOn(Log, '_debug').mockImplementation(() => undefined as any); + const debugSpy = vi + .spyOn(Log, '_debug') + .mockImplementation(() => undefined); await updateIdentityModel('external_id', undefined); await LoginManager.logout(); - expect(debugSpy).toHaveBeenCalledWith('Logout: User is not logged in, skipping logout'); + expect(debugSpy).toHaveBeenCalledWith( + 'Logout: User is not logged in, skipping logout', + ); }); test('logout: with external id resets models and creates anonymous user', async () => { @@ -56,6 +73,3 @@ describe('LoginManager', () => { expect(userDirector.createUserOnServer).toHaveBeenCalled(); }); }); - - - diff --git a/src/page/managers/PromptsManager.test.ts b/src/page/managers/PromptsManager.test.ts index 70d09e41a..f8f3dccb2 100644 --- a/src/page/managers/PromptsManager.test.ts +++ b/src/page/managers/PromptsManager.test.ts @@ -1,9 +1,11 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { setupLoadStylesheet } from '__test__/support/helpers/setup'; +import { Browser } from 'src/shared/useragent/constants'; import { PromptsManager } from './PromptsManager'; vi.mock('src/shared/useragent/detect', async (importOriginal) => { - const mod = await importOriginal(); + const mod = + await importOriginal(); return { ...mod, getBrowserName: vi.fn(), @@ -17,15 +19,15 @@ vi.mock('src/shared/useragent/detect', async (importOriginal) => { describe('PromptsManager', () => { beforeEach(() => { TestEnvironment.initialize(); - }); + }); test('_shouldForceSlidedownOverNative returns true on Chrome>=63 mobile/tablet', async () => { const detect = await import('src/shared/useragent/detect'); - (detect.getBrowserName as any).mockReturnValue('chrome'); - (detect.getBrowserVersion as any).mockReturnValue(70); - (detect.isMobileBrowser as any).mockReturnValue(true); - (detect.isTabletBrowser as any).mockReturnValue(false); - (detect.requiresUserInteraction as any).mockReturnValue(false); + vi.spyOn(detect, 'getBrowserName').mockReturnValue(Browser._Chrome); + vi.spyOn(detect, 'getBrowserVersion').mockReturnValue(70); + vi.spyOn(detect, 'isMobileBrowser').mockReturnValue(true); + vi.spyOn(detect, 'isTabletBrowser').mockReturnValue(false); + vi.spyOn(detect, 'requiresUserInteraction').mockReturnValue(false); const pm = new PromptsManager(OneSignal._context); expect(pm['_shouldForceSlidedownOverNative']()).toBe(true); @@ -33,11 +35,11 @@ describe('PromptsManager', () => { test('_shouldForceSlidedownOverNative returns true when requiresUserInteraction', async () => { const detect = await import('src/shared/useragent/detect'); - (detect.getBrowserName as any).mockReturnValue('Firefox'); - (detect.getBrowserVersion as any).mockReturnValue(100); - (detect.isMobileBrowser as any).mockReturnValue(false); - (detect.isTabletBrowser as any).mockReturnValue(false); - (detect.requiresUserInteraction as any).mockReturnValue(true); + vi.spyOn(detect, 'getBrowserName').mockReturnValue(Browser._Firefox); + vi.spyOn(detect, 'getBrowserVersion').mockReturnValue(100); + vi.spyOn(detect, 'isMobileBrowser').mockReturnValue(false); + vi.spyOn(detect, 'isTabletBrowser').mockReturnValue(false); + vi.spyOn(detect, 'requiresUserInteraction').mockReturnValue(true); const pm = new PromptsManager(OneSignal._context); expect(pm['_shouldForceSlidedownOverNative']()).toBe(true); @@ -48,13 +50,14 @@ describe('PromptsManager', () => { const pm = new PromptsManager(OneSignal._context); // stub _createSlidedown to avoid side effects - vi.spyOn(OneSignal._context._slidedownManager as any, '_createSlidedown').mockResolvedValue(undefined); - const installSpy = vi.spyOn(pm as any, '_installEventHooksForSlidedown'); + vi.spyOn( + OneSignal._context._slidedownManager, + '_createSlidedown', + ).mockResolvedValue(undefined); + const installSpy = vi.spyOn(pm, '_installEventHooksForSlidedown'); await pm['_internalShowSlidedownPrompt'](); await pm['_internalShowSlidedownPrompt'](); expect(installSpy).toHaveBeenCalledTimes(1); }); }); - - diff --git a/src/page/managers/tagManager/TagManager.test.ts b/src/page/managers/tagManager/TagManager.test.ts index 3ba588c1e..c9a605d57 100644 --- a/src/page/managers/tagManager/TagManager.test.ts +++ b/src/page/managers/tagManager/TagManager.test.ts @@ -5,9 +5,8 @@ import TagManager from './TagManager'; describe('TagManager', () => { beforeEach(() => { TestEnvironment.initialize(); - // ensure addTags exists and is stubbed - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (OneSignal as any).User.addTags = vi.fn().mockResolvedValue(undefined); + // ensure addTags exists and is stubbed + OneSignal.User.addTags = vi.fn().mockResolvedValue(undefined); }); test('_storeTagValuesToUpdate/_storeRemotePlayerTags set internal state', () => { @@ -15,7 +14,11 @@ describe('TagManager', () => { tm._storeRemotePlayerTags({ a: '1' }); tm._storeTagValuesToUpdate({ a: true, b: false }); expect(OneSignal._context._tagManager._remoteTags).toEqual({ a: '1' }); - expect((tm as any)._tagsFromTaggingContainer).toEqual({ a: true, b: false }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((tm as any)._tagsFromTaggingContainer).toEqual({ + a: true, + b: false, + }); }); test('_sendTags calls addTags when diff is non-empty and returns diff', async () => { @@ -28,10 +31,10 @@ describe('TagManager', () => { }); test('_sendTags returns {} and warns when no change', async () => { - const warnSpy = vi.spyOn(Log, '_warn').mockImplementation(() => undefined as any); + const warnSpy = vi.spyOn(Log, '_warn').mockImplementation(() => undefined); const tm = new TagManager(OneSignal._context); // Ensure this instance uses the same remote tags that will be diffed - (tm as any)._remoteTags = { c: '1' }; + tm._remoteTags = { c: '1' }; tm._storeTagValuesToUpdate({ c: true }); // converts to { c:'1' } -> diff {} const result = await tm._sendTags(); @@ -40,5 +43,3 @@ describe('TagManager', () => { expect(OneSignal.User.addTags).not.toHaveBeenCalled(); }); }); - - diff --git a/src/shared/context/helpers.test.ts b/src/shared/context/helpers.test.ts index 43633a045..c580ab788 100644 --- a/src/shared/context/helpers.test.ts +++ b/src/shared/context/helpers.test.ts @@ -26,18 +26,12 @@ describe('context/helpers', () => { }); test('enforceAlias throws when label or id is empty', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect(() => enforceAlias({ label: '', id: '' } as any)).toThrow( + expect(() => enforceAlias({ label: '', id: '' })).toThrow( 'Alias label cannot be empty', ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect(() => enforceAlias({ label: 'x', id: '' } as any)).toThrow( + expect(() => enforceAlias({ label: 'x', id: '' })).toThrow( 'Alias id cannot be empty', ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect(() => enforceAlias({ label: 'x', id: 'y' } as any)).not.toThrow(); + expect(() => enforceAlias({ label: 'x', id: 'y' })).not.toThrow(); }); }); - - - diff --git a/src/shared/helpers/init.test.ts b/src/shared/helpers/init.test.ts index ab189f9a8..e4087b1e0 100644 --- a/src/shared/helpers/init.test.ts +++ b/src/shared/helpers/init.test.ts @@ -16,9 +16,6 @@ beforeEach(() => { ); }); -afterEach(() => { - }); - /** onSdkInitialized */ test('onSdkInitialized: ensure public sdk initialized triggered', async () => { OneSignal._emitter.on(OneSignal.EVENTS.SDK_INITIALIZED_PUBLIC, () => { diff --git a/src/shared/managers/IDManager.test.ts b/src/shared/managers/IDManager.test.ts index ea0c69d21..a88134e69 100644 --- a/src/shared/managers/IDManager.test.ts +++ b/src/shared/managers/IDManager.test.ts @@ -5,7 +5,6 @@ describe('IDManager', () => { // mock crypto.randomUUID to deterministic value const uuids = ['u1', 'u2', 'u3']; let i = 0; - // eslint-disable-next-line @typescript-eslint/no-explicit-any vi.spyOn(global as any, 'crypto', 'get').mockReturnValue({ randomUUID: () => uuids[i++], }); @@ -23,6 +22,3 @@ describe('IDManager', () => { expect(IDManager._isLocalId(null as unknown as string)).toBe(false); }); }); - - - diff --git a/src/shared/managers/PermissionManager.test.ts b/src/shared/managers/PermissionManager.test.ts index 1e256f0da..5876842f2 100644 --- a/src/shared/managers/PermissionManager.test.ts +++ b/src/shared/managers/PermissionManager.test.ts @@ -20,36 +20,39 @@ describe('PermissionManager', () => { test('resolves from context manager', async () => { const pm = new PermissionManager(); const spy = vi - .spyOn(OneSignal._context._permissionManager, '_getNotificationPermission') + .spyOn( + OneSignal._context._permissionManager, + '_getNotificationPermission', + ) .mockResolvedValue('granted'); await expect(pm._getPermissionStatus()).resolves.toBe('granted'); expect(spy).toHaveBeenCalled(); }); test('throws if context is undefined', async () => { - OneSignal._context = undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + OneSignal._context = undefined as any; const pm = new PermissionManager(); - await expect(pm._getPermissionStatus()).rejects.toThrow('OneSignal.context is undefined. Call init first'); + await expect(pm._getPermissionStatus()).rejects.toThrow( + 'OneSignal.context is undefined. Call init first', + ); }); }); test('_getNotificationPermission uses legacy Safari path and requires webId', async () => { - vi.spyOn(detect, 'useSafariLegacyPush') - .mockReturnValue(true); + vi.spyOn(detect, 'useSafariLegacyPush').mockReturnValue(true); const pm = new PermissionManager(); - await expect(pm._getNotificationPermission(undefined)) - .rejects.toThrow('"safariWebId" is empty'); + await expect(pm._getNotificationPermission(undefined)).rejects.toThrow( + '"safariWebId" is empty', + ); }); test('_getNotificationPermission uses W3C Notification.permission when not legacy', async () => { - vi.spyOn(detect, 'useSafariLegacyPush') - .mockReturnValue(false); + vi.spyOn(detect, 'useSafariLegacyPush').mockReturnValue(false); MockNotification.permission = 'default'; const pm = new PermissionManager(); await expect(pm['_getNotificationPermission']()).resolves?.toBe('default'); }); }); - - diff --git a/src/shared/managers/sessionManager/SessionManager.test.ts b/src/shared/managers/sessionManager/SessionManager.test.ts index 0878311ba..6d5242677 100644 --- a/src/shared/managers/sessionManager/SessionManager.test.ts +++ b/src/shared/managers/sessionManager/SessionManager.test.ts @@ -140,20 +140,24 @@ describe('SessionManager', () => { const sm = new SessionManager(OneSignal._context); const unicastSpy = vi .spyOn(OneSignal._context._workerMessenger, '_unicast') - .mockResolvedValue(undefined as any); + .mockResolvedValue(undefined); - await sm['_notifySWToUpsertSession']('one', 'sub', SessionOrigin._UserCreate); + await sm['_notifySWToUpsertSession']( + 'one', + 'sub', + SessionOrigin._UserCreate, + ); expect(unicastSpy).toHaveBeenCalled(); }); test('_upsertSession does nothing when no user is present', async () => { // Remove user singleton - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (await import('src/onesignal/User')).default._singletonInstance = undefined as any; + (await import('src/onesignal/User')).default._singletonInstance = + undefined; const detect = await import('src/shared/environment/detect'); vi.spyOn(detect, 'supportsServiceWorkers').mockReturnValue(true); const sm = new SessionManager(OneSignal._context); - const notifySpy = vi.spyOn(sm as any, '_notifySWToUpsertSession'); + const notifySpy = vi.spyOn(sm, '_notifySWToUpsertSession'); await sm._upsertSession(SessionOrigin._UserCreate); expect(notifySpy).not.toHaveBeenCalled(); }); @@ -162,7 +166,7 @@ describe('SessionManager', () => { const detect = await import('src/shared/environment/detect'); vi.spyOn(detect, 'supportsServiceWorkers').mockReturnValue(true); const sm = new SessionManager(OneSignal._context); - const setupSpy = vi.spyOn(sm as any, '_setupSessionEventListeners'); + const setupSpy = vi.spyOn(sm, '_setupSessionEventListeners'); // also stub ids retrieval path to avoid errors vi.spyOn(sm as any, '_getOneSignalAndSubscriptionIds').mockResolvedValue({ onesignalId: 'one', @@ -176,7 +180,10 @@ describe('SessionManager', () => { const detect = await import('src/shared/environment/detect'); vi.spyOn(detect, 'supportsServiceWorkers').mockReturnValue(false); const sm = new SessionManager(OneSignal._context); - const emitSpy = vi.spyOn(OneSignal._emitter as any, '_emit').mockResolvedValue(undefined); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const emitSpy = vi + .spyOn(OneSignal._emitter, '_emit') + .mockResolvedValue(OneSignal._emitter); await sm._upsertSession(SessionOrigin._UserCreate); expect(emitSpy).toHaveBeenCalledWith(OneSignal.EVENTS.SESSION_STARTED); }); From cfd4c993eb5d54d18282de69463caa4e9d5f8e80 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 10:43:48 -0800 Subject: [PATCH 21/46] test(PermissionManager): opt for spies over mocks --- src/shared/managers/PermissionManager.test.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/shared/managers/PermissionManager.test.ts b/src/shared/managers/PermissionManager.test.ts index 5876842f2..5021d903c 100644 --- a/src/shared/managers/PermissionManager.test.ts +++ b/src/shared/managers/PermissionManager.test.ts @@ -3,13 +3,7 @@ import MockNotification from '__test__/support/mocks/MockNotification'; import * as detect from '../environment/detect'; import PermissionManager from './PermissionManager'; -vi.mock('../environment/detect', async (importOriginal) => { - const mod = await importOriginal(); - return { - ...mod, - useSafariLegacyPush: vi.fn(), - }; -}); +const useSafariLegacyPushSpy = vi.spyOn(detect, 'useSafariLegacyPush'); describe('PermissionManager', () => { beforeEach(() => { @@ -40,8 +34,7 @@ describe('PermissionManager', () => { }); test('_getNotificationPermission uses legacy Safari path and requires webId', async () => { - vi.spyOn(detect, 'useSafariLegacyPush').mockReturnValue(true); - + useSafariLegacyPushSpy.mockImplementation(() => true); const pm = new PermissionManager(); await expect(pm._getNotificationPermission(undefined)).rejects.toThrow( '"safariWebId" is empty', @@ -49,7 +42,7 @@ describe('PermissionManager', () => { }); test('_getNotificationPermission uses W3C Notification.permission when not legacy', async () => { - vi.spyOn(detect, 'useSafariLegacyPush').mockReturnValue(false); + useSafariLegacyPushSpy.mockImplementation(() => false); MockNotification.permission = 'default'; const pm = new PermissionManager(); From f123ecdf74cb3f69ca646d58142c31018374b5e1 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 10:44:18 -0800 Subject: [PATCH 22/46] test(PromptsManager): opt for spies over mocks --- src/page/managers/PromptsManager.test.ts | 40 ++++++++++-------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/src/page/managers/PromptsManager.test.ts b/src/page/managers/PromptsManager.test.ts index f8f3dccb2..af6a3e04b 100644 --- a/src/page/managers/PromptsManager.test.ts +++ b/src/page/managers/PromptsManager.test.ts @@ -1,20 +1,14 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { setupLoadStylesheet } from '__test__/support/helpers/setup'; import { Browser } from 'src/shared/useragent/constants'; +import * as detect from 'src/shared/useragent/detect'; import { PromptsManager } from './PromptsManager'; -vi.mock('src/shared/useragent/detect', async (importOriginal) => { - const mod = - await importOriginal(); - return { - ...mod, - getBrowserName: vi.fn(), - getBrowserVersion: vi.fn(), - isMobileBrowser: vi.fn(), - isTabletBrowser: vi.fn(), - requiresUserInteraction: vi.fn(), - }; -}); +const getBrowserNameSpy = vi.spyOn(detect, 'getBrowserName'); +const getBrowserVersionSpy = vi.spyOn(detect, 'getBrowserVersion'); +const isMobileBrowserSpy = vi.spyOn(detect, 'isMobileBrowser'); +const isTabletBrowserSpy = vi.spyOn(detect, 'isTabletBrowser'); +const requiresUserInteractionSpy = vi.spyOn(detect, 'requiresUserInteraction'); describe('PromptsManager', () => { beforeEach(() => { @@ -22,24 +16,22 @@ describe('PromptsManager', () => { }); test('_shouldForceSlidedownOverNative returns true on Chrome>=63 mobile/tablet', async () => { - const detect = await import('src/shared/useragent/detect'); - vi.spyOn(detect, 'getBrowserName').mockReturnValue(Browser._Chrome); - vi.spyOn(detect, 'getBrowserVersion').mockReturnValue(70); - vi.spyOn(detect, 'isMobileBrowser').mockReturnValue(true); - vi.spyOn(detect, 'isTabletBrowser').mockReturnValue(false); - vi.spyOn(detect, 'requiresUserInteraction').mockReturnValue(false); + getBrowserNameSpy.mockReturnValue(Browser._Chrome); + getBrowserVersionSpy.mockReturnValue(70); + isMobileBrowserSpy.mockReturnValue(true); + isTabletBrowserSpy.mockReturnValue(false); + requiresUserInteractionSpy.mockReturnValue(false); const pm = new PromptsManager(OneSignal._context); expect(pm['_shouldForceSlidedownOverNative']()).toBe(true); }); test('_shouldForceSlidedownOverNative returns true when requiresUserInteraction', async () => { - const detect = await import('src/shared/useragent/detect'); - vi.spyOn(detect, 'getBrowserName').mockReturnValue(Browser._Firefox); - vi.spyOn(detect, 'getBrowserVersion').mockReturnValue(100); - vi.spyOn(detect, 'isMobileBrowser').mockReturnValue(false); - vi.spyOn(detect, 'isTabletBrowser').mockReturnValue(false); - vi.spyOn(detect, 'requiresUserInteraction').mockReturnValue(true); + getBrowserNameSpy.mockReturnValue(Browser._Firefox); + getBrowserVersionSpy.mockReturnValue(100); + isMobileBrowserSpy.mockReturnValue(false); + isTabletBrowserSpy.mockReturnValue(false); + requiresUserInteractionSpy.mockReturnValue(true); const pm = new PromptsManager(OneSignal._context); expect(pm['_shouldForceSlidedownOverNative']()).toBe(true); From cdd97659e8bc21f954b3c6c3cbae44d278da9c0a Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 10:51:02 -0800 Subject: [PATCH 23/46] test(IDManager): update uuid sample to be more similar to a realist shape The previous sample used a simple string, e.g. u1, whereas the updated version uses a 5-part string delimited by hyphens. This is closer to the representation of a UUID, which has a "a 128-bit value commonly represented as a string of 32 hexadecimal digits, separated into five groups by hyphens in the format (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)" --- src/shared/managers/IDManager.test.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/shared/managers/IDManager.test.ts b/src/shared/managers/IDManager.test.ts index a88134e69..35f800e99 100644 --- a/src/shared/managers/IDManager.test.ts +++ b/src/shared/managers/IDManager.test.ts @@ -3,11 +3,13 @@ import { IDManager } from './IDManager'; describe('IDManager', () => { test('_createLocalId uses prefix and is unique-ish', () => { // mock crypto.randomUUID to deterministic value - const uuids = ['u1', 'u2', 'u3']; let i = 0; - vi.spyOn(global as any, 'crypto', 'get').mockReturnValue({ - randomUUID: () => uuids[i++], - }); + const uuids = [ + 'a-b-c-d-e', + 'f-g-h-i-j', + ] satisfies `${string}-${string}-${string}-${string}-${string}`[]; + vi.spyOn(window.crypto, 'randomUUID').mockImplementation(() => uuids[i++]); + const a = IDManager._createLocalId(); const b = IDManager._createLocalId(); expect(a.startsWith(IDManager.LOCAL_PREFIX)).toBe(true); From e8258a70909853531b7c44eb9c6534156d92e074 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 14:18:17 -0800 Subject: [PATCH 24/46] test(SessionManager): extract out a single ServiceWorker spy --- .../sessionManager/SessionManager.test.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/shared/managers/sessionManager/SessionManager.test.ts b/src/shared/managers/sessionManager/SessionManager.test.ts index 6d5242677..d1d0c5852 100644 --- a/src/shared/managers/sessionManager/SessionManager.test.ts +++ b/src/shared/managers/sessionManager/SessionManager.test.ts @@ -2,10 +2,13 @@ import { EXTERNAL_ID } from '__test__/constants'; import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { setAddAliasResponse } from '__test__/support/helpers/requests'; 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 { SessionManager } from './SessionManager'; +const supportsServiceWorkersSpy = vi.spyOn(detect, 'supportsServiceWorkers'); + vi.spyOn(Log, '_error').mockImplementation(() => ''); describe('SessionManager', () => { @@ -131,12 +134,10 @@ describe('SessionManager', () => { describe('Core behaviors', () => { beforeEach(() => { TestEnvironment.initialize(); - vi.restoreAllMocks(); }); test('_notifySWToUpsertSession posts to worker when SW supported', async () => { - const detect = await import('src/shared/environment/detect'); - vi.spyOn(detect, 'supportsServiceWorkers').mockReturnValue(true); + supportsServiceWorkersSpy.mockReturnValue(true); const sm = new SessionManager(OneSignal._context); const unicastSpy = vi .spyOn(OneSignal._context._workerMessenger, '_unicast') @@ -154,8 +155,7 @@ describe('SessionManager', () => { // Remove user singleton (await import('src/onesignal/User')).default._singletonInstance = undefined; - const detect = await import('src/shared/environment/detect'); - vi.spyOn(detect, 'supportsServiceWorkers').mockReturnValue(true); + supportsServiceWorkersSpy.mockReturnValue(true); const sm = new SessionManager(OneSignal._context); const notifySpy = vi.spyOn(sm, '_notifySWToUpsertSession'); await sm._upsertSession(SessionOrigin._UserCreate); @@ -163,8 +163,7 @@ describe('SessionManager', () => { }); test('_upsertSession installs listeners when SW supported', async () => { - const detect = await import('src/shared/environment/detect'); - vi.spyOn(detect, 'supportsServiceWorkers').mockReturnValue(true); + supportsServiceWorkersSpy.mockReturnValue(true); const sm = new SessionManager(OneSignal._context); const setupSpy = vi.spyOn(sm, '_setupSessionEventListeners'); // also stub ids retrieval path to avoid errors @@ -177,8 +176,7 @@ describe('SessionManager', () => { }); test('_upsertSession emits SESSION_STARTED when SW not supported', async () => { - const detect = await import('src/shared/environment/detect'); - vi.spyOn(detect, 'supportsServiceWorkers').mockReturnValue(false); + supportsServiceWorkersSpy.mockReturnValue(false); const sm = new SessionManager(OneSignal._context); // eslint-disable-next-line @typescript-eslint/no-explicit-any const emitSpy = vi From b8a396d3d883d79705cdfd3cd3a6d41dcb0fa9a5 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 14:20:37 -0800 Subject: [PATCH 25/46] test(helpers): extract out fakeTimers; add timeoutPromise describe block --- src/shared/context/helpers.test.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/shared/context/helpers.test.ts b/src/shared/context/helpers.test.ts index c580ab788..c08e11db9 100644 --- a/src/shared/context/helpers.test.ts +++ b/src/shared/context/helpers.test.ts @@ -1,20 +1,22 @@ import { enforceAlias, enforceAppId, timeoutPromise } from './helpers'; +vi.useFakeTimers(); + describe('context/helpers', () => { - test('timeoutPromise resolves before timeout', async () => { - vi.useFakeTimers(); - const p = new Promise((resolve) => setTimeout(() => resolve('ok'), 50)); - const raced = timeoutPromise(p, 100); - vi.advanceTimersByTime(60); - await expect(raced).resolves.toBe('ok'); - }); + describe('timeoutPromise', () => { + test('resolves before timeout', async () => { + const p = new Promise((resolve) => setTimeout(() => resolve('ok'), 50)); + const raced = timeoutPromise(p, 100); + vi.advanceTimersByTime(60); + await expect(raced).resolves.toBe('ok'); + }); - test('timeoutPromise rejects on timeout', async () => { - vi.useFakeTimers(); - const never = new Promise((_r) => undefined); - const raced = timeoutPromise(never, 10); - vi.advanceTimersByTime(11); - await expect(raced).rejects.toThrow('Async operation timed out'); + test('rejects on timeout', async () => { + const never = new Promise((_r) => undefined); + const raced = timeoutPromise(never, 10); + vi.advanceTimersByTime(11); + await expect(raced).rejects.toThrow('Async operation timed out'); + }); }); test('enforceAppId throws on empty', () => { From d93fb2260346d800c0fd76f49a505da9b85c2a81 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 14:26:35 -0800 Subject: [PATCH 26/46] test(LoginManager): opt for spies over mocks --- src/page/managers/LoginManager.test.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/page/managers/LoginManager.test.ts b/src/page/managers/LoginManager.test.ts index 27bcec998..90d1cfbab 100644 --- a/src/page/managers/LoginManager.test.ts +++ b/src/page/managers/LoginManager.test.ts @@ -2,16 +2,13 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { updateIdentityModel } from '__test__/support/helpers/setup'; import { db } from 'src/shared/database/client'; import Log from 'src/shared/libraries/Log'; +import * as userDirector from '../../onesignal/userDirector'; import LoginManager from './LoginManager'; -vi.mock('../../onesignal/userDirector', async (importOriginal) => { - const mod = - await importOriginal(); - return { - ...mod, - createUserOnServer: vi.fn().mockResolvedValue(undefined), - }; -}); +const createUserOnServerSpy = vi.spyOn( + userDirector, + 'createUserOnServer', +).mockResolvedValue(undefined); describe('LoginManager', () => { beforeEach(() => { @@ -67,9 +64,8 @@ describe('LoginManager', () => { }); test('logout: with external id resets models and creates anonymous user', async () => { - const userDirector = await import('../../onesignal/userDirector'); await updateIdentityModel('external_id', 'abc'); await LoginManager.logout(); - expect(userDirector.createUserOnServer).toHaveBeenCalled(); + expect(createUserOnServerSpy).toHaveBeenCalled(); }); }); From 2c5f8fc9dc9591c4a98e71868cb50052255537d8 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 15:29:46 -0800 Subject: [PATCH 27/46] test(TagManager): opt for spies over mocks --- src/page/managers/tagManager/TagManager.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/page/managers/tagManager/TagManager.test.ts b/src/page/managers/tagManager/TagManager.test.ts index c9a605d57..23fb0fba5 100644 --- a/src/page/managers/tagManager/TagManager.test.ts +++ b/src/page/managers/tagManager/TagManager.test.ts @@ -1,12 +1,12 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; +import UserNamespace from 'src/onesignal/UserNamespace'; import Log from 'src/shared/libraries/Log'; import TagManager from './TagManager'; describe('TagManager', () => { + const addTagsSpy = vi.spyOn(UserNamespace.prototype, 'addTags') beforeEach(() => { TestEnvironment.initialize(); - // ensure addTags exists and is stubbed - OneSignal.User.addTags = vi.fn().mockResolvedValue(undefined); }); test('_storeTagValuesToUpdate/_storeRemotePlayerTags set internal state', () => { @@ -14,7 +14,6 @@ describe('TagManager', () => { tm._storeRemotePlayerTags({ a: '1' }); tm._storeTagValuesToUpdate({ a: true, b: false }); expect(OneSignal._context._tagManager._remoteTags).toEqual({ a: '1' }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any expect((tm as any)._tagsFromTaggingContainer).toEqual({ a: true, b: false, @@ -26,7 +25,7 @@ describe('TagManager', () => { tm._storeRemotePlayerTags({ a: '1' }); tm._storeTagValuesToUpdate({ a: false, b: true }); // converts to { a:0, b:1 } and diff => { a:0, b:1 } vs { a:1 } -> { a:0, b:1 } const result = await tm._sendTags(); - expect(OneSignal.User.addTags).toHaveBeenCalledWith(result); + expect(addTagsSpy).toHaveBeenCalledWith(result); expect(result).toMatchObject({ a: '0', b: '1' }); }); @@ -40,6 +39,6 @@ describe('TagManager', () => { const result = await tm._sendTags(); expect(result).toEqual({}); expect(warnSpy).toHaveBeenCalled(); - expect(OneSignal.User.addTags).not.toHaveBeenCalled(); + expect(addTagsSpy).not.toHaveBeenCalled(); }); }); From dbb9b01b1d89d7611fa5d0dda9b426e26324873d Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 15:34:26 -0800 Subject: [PATCH 28/46] test: avoid use of globalThis in favor of global --- src/page/bell/Badge.test.ts | 6 ++---- src/page/bell/Button.test.ts | 2 +- src/page/bell/Launcher.test.ts | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/page/bell/Badge.test.ts b/src/page/bell/Badge.test.ts index 19be85913..e1417adf8 100644 --- a/src/page/bell/Badge.test.ts +++ b/src/page/bell/Badge.test.ts @@ -1,14 +1,12 @@ +import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import Badge from './Badge'; describe('Badge', () => { beforeEach(() => { + TestEnvironment.initialize(); document.body.innerHTML = `
`; - // Ensure optional OneSignal global doesn't explode when accessed - // Badge._show calls OneSignal?._notifyButton via optional chaining - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (globalThis as any).OneSignal = (globalThis as any).OneSignal ?? {}; }); test('_updateCount increments and clamps to empty when <= 0', () => { diff --git a/src/page/bell/Button.test.ts b/src/page/bell/Button.test.ts index f273ceac5..7a1fae6a1 100644 --- a/src/page/bell/Button.test.ts +++ b/src/page/bell/Button.test.ts @@ -8,7 +8,7 @@ describe('Button', () => {
`; - (globalThis as any).OneSignal = (globalThis as any).OneSignal ?? { + (global as any).OneSignal = (global as any).OneSignal ?? { _emitter: { once: () => undefined, _removeAllListeners: () => undefined, diff --git a/src/page/bell/Launcher.test.ts b/src/page/bell/Launcher.test.ts index e5c351c29..d134cec78 100644 --- a/src/page/bell/Launcher.test.ts +++ b/src/page/bell/Launcher.test.ts @@ -9,7 +9,7 @@ describe('Launcher', () => { // Polyfill Web Animations API method used by AnimatedElement HTMLElement.prototype.getAnimations = () => []; // Minimal OneSignal to satisfy any optional accesses down the line - (globalThis as any).OneSignal = (globalThis as any).OneSignal ?? {}; + (global as any).OneSignal = (global as any).OneSignal ?? {}; }); test('_activateIfInactive sets wasInactive and activates only when inactive', async () => { From a455ebe4ea8ca21fe914ed84629007b1a4c7fef0 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 15:36:19 -0800 Subject: [PATCH 29/46] test(Button): clean up test env initilization step --- src/page/bell/Button.test.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/page/bell/Button.test.ts b/src/page/bell/Button.test.ts index 7a1fae6a1..b32140f09 100644 --- a/src/page/bell/Button.test.ts +++ b/src/page/bell/Button.test.ts @@ -1,3 +1,4 @@ +import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import Bell from './Bell'; import Button from './Button'; import { MessageType } from './constants'; @@ -8,14 +9,7 @@ describe('Button', () => {
`; - (global as any).OneSignal = (global as any).OneSignal ?? { - _emitter: { - once: () => undefined, - _removeAllListeners: () => undefined, - _emit: async () => undefined, - }, - EVENTS: {}, - }; + TestEnvironment.initialize(); }); test('_onClick concurrency guard and early-return when message showing', async () => { @@ -29,7 +23,7 @@ describe('Button', () => { msgEl.classList.add('onesignal-bell-launcher-message-opened'); bell['_message']['_contentType'] = MessageType._Message; - const toggleSpy = vi.spyOn(button as unknown as Button, '_toggleDialog'); + const toggleSpy = vi.spyOn(button, '_toggleDialog'); // Force concurrent scenario: set handling to true then call button['_isHandlingClick'] = false; From c652c5208bde103ff3799cd9b7f00c54c667a8c7 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 15:42:50 -0800 Subject: [PATCH 30/46] test(Bell): extract out install and update spies --- src/page/bell/Bell.test.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/page/bell/Bell.test.ts b/src/page/bell/Bell.test.ts index 7bf97a834..0ab46f1f7 100644 --- a/src/page/bell/Bell.test.ts +++ b/src/page/bell/Bell.test.ts @@ -3,15 +3,16 @@ import OneSignalEvent from '../../shared/services/OneSignalEvent'; import Bell from './Bell'; describe('Bell', () => { + // @ts-expect-error - _installEventHooks is not assignable + const spyInstall = vi.spyOn(Bell.prototype, '_installEventHooks'); + const spyUpdate = vi.spyOn(Bell.prototype, '_updateState'); + beforeEach(() => { // Set up OneSignal globals/context to avoid accidental runtime lookups TestEnvironment.initialize(); }); test('constructor early-returns when enable=false and applies defaults', () => { - const spyInstall = vi.spyOn(Bell.prototype as any, '_installEventHooks'); - const spyUpdate = vi.spyOn(Bell.prototype, '_updateState'); - const bell = new Bell({ enable: false }); expect(bell._options.size).toBe('medium'); expect(bell._options.position).toBe('bottom-right'); @@ -21,13 +22,6 @@ describe('Bell', () => { }); test('constructor validates and installs hooks when enable=true', () => { - const spyInstall = vi - .spyOn(Bell.prototype as any, '_installEventHooks') - .mockImplementation(() => undefined); - const spyUpdate = vi - .spyOn(Bell.prototype, '_updateState') - .mockImplementation(() => undefined); - // Valid non-defaults to ensure validation path runs const bell = new Bell({ enable: true, From e86c9ce5e25c3307529876c93c287ad43dd0c89c Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 15:43:13 -0800 Subject: [PATCH 31/46] test(Button): refactor to use dot notation over brackets --- src/page/bell/Button.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/page/bell/Button.test.ts b/src/page/bell/Button.test.ts index b32140f09..8832b4153 100644 --- a/src/page/bell/Button.test.ts +++ b/src/page/bell/Button.test.ts @@ -21,14 +21,14 @@ describe('Button', () => { '.onesignal-bell-launcher-message', ) as HTMLElement; msgEl.classList.add('onesignal-bell-launcher-message-opened'); - bell['_message']['_contentType'] = MessageType._Message; + bell._message._contentType = MessageType._Message; const toggleSpy = vi.spyOn(button, '_toggleDialog'); // Force concurrent scenario: set handling to true then call - button['_isHandlingClick'] = false; - await button['_onClick'](); + button._isHandlingClick = false; + await button._onClick(); expect(toggleSpy).not.toHaveBeenCalled(); - expect(button['_isHandlingClick']).toBe(false); + expect(button._isHandlingClick).toBe(false); }); }); From 79f7abe798f6c17d915fcc0dc75c0db524e5acac Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 15:44:20 -0800 Subject: [PATCH 32/46] style: fix lint errors --- src/page/managers/LoginManager.test.ts | 7 +++---- src/page/managers/tagManager/TagManager.test.ts | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/page/managers/LoginManager.test.ts b/src/page/managers/LoginManager.test.ts index 90d1cfbab..bc3d84de4 100644 --- a/src/page/managers/LoginManager.test.ts +++ b/src/page/managers/LoginManager.test.ts @@ -5,10 +5,9 @@ import Log from 'src/shared/libraries/Log'; import * as userDirector from '../../onesignal/userDirector'; import LoginManager from './LoginManager'; -const createUserOnServerSpy = vi.spyOn( - userDirector, - 'createUserOnServer', -).mockResolvedValue(undefined); +const createUserOnServerSpy = vi + .spyOn(userDirector, 'createUserOnServer') + .mockResolvedValue(undefined); describe('LoginManager', () => { beforeEach(() => { diff --git a/src/page/managers/tagManager/TagManager.test.ts b/src/page/managers/tagManager/TagManager.test.ts index 23fb0fba5..f963bfafc 100644 --- a/src/page/managers/tagManager/TagManager.test.ts +++ b/src/page/managers/tagManager/TagManager.test.ts @@ -4,7 +4,7 @@ import Log from 'src/shared/libraries/Log'; import TagManager from './TagManager'; describe('TagManager', () => { - const addTagsSpy = vi.spyOn(UserNamespace.prototype, 'addTags') + const addTagsSpy = vi.spyOn(UserNamespace.prototype, 'addTags'); beforeEach(() => { TestEnvironment.initialize(); }); From d8e82d6241de5dd1684513e25daa0d40db006341 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 19:31:35 -0800 Subject: [PATCH 33/46] test(Badge): refactor to use dot notation over brackets --- src/page/bell/Badge.test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/page/bell/Badge.test.ts b/src/page/bell/Badge.test.ts index e1417adf8..79b3035a9 100644 --- a/src/page/bell/Badge.test.ts +++ b/src/page/bell/Badge.test.ts @@ -12,16 +12,16 @@ describe('Badge', () => { test('_updateCount increments and clamps to empty when <= 0', () => { const badge = new Badge(); // Start at empty, increment twice - badge['_content'] = ''; - badge['_increment'](); - expect(badge['_content']).toBe('1'); - badge['_increment'](); - expect(badge['_content']).toBe('2'); + badge._content = ''; + badge._increment(); + expect(badge._content).toBe('1'); + badge._increment(); + expect(badge._content).toBe('2'); // Decrement twice → goes to 0 and clears to empty string - badge['_decrement'](); - expect(badge['_content']).toBe('1'); - badge['_decrement'](); - expect(badge['_content']).toBe(''); + badge._decrement(); + expect(badge._content).toBe('1'); + badge._decrement(); + expect(badge._content).toBe(''); }); }); From 2f675581a448d5e9f69b6449a20675f007b0ca21 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 20:13:41 -0800 Subject: [PATCH 34/46] test(Bell): use toHaveBeenCalledWith for more detailed assert --- src/page/bell/Bell.test.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/page/bell/Bell.test.ts b/src/page/bell/Bell.test.ts index 0ab46f1f7..32e5bb470 100644 --- a/src/page/bell/Bell.test.ts +++ b/src/page/bell/Bell.test.ts @@ -1,11 +1,12 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import OneSignalEvent from '../../shared/services/OneSignalEvent'; import Bell from './Bell'; +import { BellEvent, BellState } from './constants'; describe('Bell', () => { // @ts-expect-error - _installEventHooks is not assignable const spyInstall = vi.spyOn(Bell.prototype, '_installEventHooks'); - const spyUpdate = vi.spyOn(Bell.prototype, '_updateState'); + const updateStateSpy = vi.spyOn(Bell.prototype, '_updateState'); beforeEach(() => { // Set up OneSignal globals/context to avoid accidental runtime lookups @@ -18,7 +19,7 @@ describe('Bell', () => { expect(bell._options.position).toBe('bottom-right'); expect(bell._options.theme).toBe('default'); expect(spyInstall).not.toHaveBeenCalled(); - expect(spyUpdate).not.toHaveBeenCalled(); + expect(updateStateSpy).not.toHaveBeenCalled(); }); test('constructor validates and installs hooks when enable=true', () => { @@ -33,14 +34,18 @@ describe('Bell', () => { }); expect(bell).toBeTruthy(); expect(spyInstall).toHaveBeenCalledTimes(1); - expect(spyUpdate).toHaveBeenCalledTimes(1); + expect(updateStateSpy).toHaveBeenCalledTimes(1); }); test('_setState triggers event when changed', () => { const bell = new Bell({ enable: false }); const trigger = vi.spyOn(OneSignalEvent, '_trigger'); // transition should emit - bell._setState(1 /* _Subscribed */); - expect(trigger).toHaveBeenCalled(); + bell._setState(BellState._Subscribed); + + expect(trigger).toHaveBeenCalledWith(BellEvent._StateChanged, { + from: BellState._Uninitialized, + to: BellState._Subscribed, + }); }); }); From 700361c57f44a4f3fb83fe0fe76e20b081920054 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 20:17:04 -0800 Subject: [PATCH 35/46] test(Button): initialize test env before other test fixtures --- src/page/bell/Button.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/page/bell/Button.test.ts b/src/page/bell/Button.test.ts index 8832b4153..7c6f12a37 100644 --- a/src/page/bell/Button.test.ts +++ b/src/page/bell/Button.test.ts @@ -5,11 +5,11 @@ import { MessageType } from './constants'; describe('Button', () => { beforeEach(() => { + TestEnvironment.initialize(); document.body.innerHTML = `
`; - TestEnvironment.initialize(); }); test('_onClick concurrency guard and early-return when message showing', async () => { @@ -17,9 +17,7 @@ describe('Button', () => { const button = new Button(bell); // Simulate message being shown of type Message by adding the show class - const msgEl = document.querySelector( - '.onesignal-bell-launcher-message', - ) as HTMLElement; + const msgEl = document.querySelector('.onesignal-bell-launcher-message') as HTMLElement; msgEl.classList.add('onesignal-bell-launcher-message-opened'); bell._message._contentType = MessageType._Message; From 668b7658f719638f16bc04a86835eb31aa82f8e8 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 20:40:36 -0800 Subject: [PATCH 36/46] test: add Animations API polyfill to setupTests --- __test__/setupTests.ts | 3 +++ src/page/bell/Dialog.test.ts | 2 -- src/page/bell/Launcher.test.ts | 2 -- src/page/bell/Message.test.ts | 3 --- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/__test__/setupTests.ts b/__test__/setupTests.ts index 1362888d6..3849f42f9 100644 --- a/__test__/setupTests.ts +++ b/__test__/setupTests.ts @@ -60,3 +60,6 @@ export const mockJsonp = () => { downloadSpy = vi.spyOn(OneSignalApi, 'downloadServerAppConfig'); downloadSpy.mockResolvedValue(serverConfig); }; + +// Polyfill Web Animations API method used by AnimatedElement +HTMLElement.prototype.getAnimations = () => []; \ No newline at end of file diff --git a/src/page/bell/Dialog.test.ts b/src/page/bell/Dialog.test.ts index b7dfc5f84..6dbf4565a 100644 --- a/src/page/bell/Dialog.test.ts +++ b/src/page/bell/Dialog.test.ts @@ -11,8 +11,6 @@ describe('Dialog', () => {
`; - // Polyfill Web Animations API method used by AnimatedElement - (HTMLElement.prototype as any).getAnimations = () => []; }); test('_show populates content and toggles shown flag', async () => { diff --git a/src/page/bell/Launcher.test.ts b/src/page/bell/Launcher.test.ts index d134cec78..696b827af 100644 --- a/src/page/bell/Launcher.test.ts +++ b/src/page/bell/Launcher.test.ts @@ -6,8 +6,6 @@ describe('Launcher', () => { document.body.innerHTML = `
`; - // Polyfill Web Animations API method used by AnimatedElement - HTMLElement.prototype.getAnimations = () => []; // Minimal OneSignal to satisfy any optional accesses down the line (global as any).OneSignal = (global as any).OneSignal ?? {}; }); diff --git a/src/page/bell/Message.test.ts b/src/page/bell/Message.test.ts index 9917bcef7..f73fc1a26 100644 --- a/src/page/bell/Message.test.ts +++ b/src/page/bell/Message.test.ts @@ -11,9 +11,6 @@ describe('Message', () => {
`; - // Polyfill Web Animations API method used by AnimatedElement - HTMLElement.prototype.getAnimations = () => []; - vi.useFakeTimers(); }); test('_display shows then hides after duration and resets content type', async () => { From 95f047c0318ff3436c02d2f3bdb033e279b8e647 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 20:41:57 -0800 Subject: [PATCH 37/46] test(Dialog): extract out `_isPushNotificationsEnabled` spy --- src/page/bell/Dialog.test.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/page/bell/Dialog.test.ts b/src/page/bell/Dialog.test.ts index 6dbf4565a..0685b9f4d 100644 --- a/src/page/bell/Dialog.test.ts +++ b/src/page/bell/Dialog.test.ts @@ -1,9 +1,12 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; +import type { MockInstance } from 'vitest'; import Bell from './Bell'; import Dialog from './Dialog'; import { BellState } from './constants'; describe('Dialog', () => { + let isPushNotificationsEnabledSpy: MockInstance; + beforeEach(() => { TestEnvironment.initialize(); document.body.innerHTML = ` @@ -11,15 +14,15 @@ describe('Dialog', () => {
`; + const sm = OneSignal._context._subscriptionManager; + isPushNotificationsEnabledSpy = vi.spyOn(sm, '_isPushNotificationsEnabled'); }); test('_show populates content and toggles shown flag', async () => { const bell = new Bell({ enable: false }); // Put bell in unsubscribed state to render subscribe button - (bell as any)._state = BellState._Unsubscribed; - // Force subscription manager response to "disabled" - const sm = OneSignal._context._subscriptionManager as any; - vi.spyOn(sm, '_isPushNotificationsEnabled').mockResolvedValue(false); + bell._state = BellState._Unsubscribed; + isPushNotificationsEnabledSpy.mockResolvedValue(false); const dialog = new Dialog(bell); expect(dialog['_shown']).toBe(false); @@ -31,9 +34,8 @@ describe('Dialog', () => { test('_hide removes shown class and keeps state consistent', async () => { const bell = new Bell({ enable: false }); - (bell as any)._state = BellState._Unsubscribed; - const sm = OneSignal._context._subscriptionManager as any; - vi.spyOn(sm, '_isPushNotificationsEnabled').mockResolvedValue(false); + bell._state = BellState._Unsubscribed; + isPushNotificationsEnabledSpy.mockResolvedValue(false); const dialog = new Dialog(bell); await dialog['_show'](); From b42fb7f7d00bd73450535d62434732d50077f3e2 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 20:42:31 -0800 Subject: [PATCH 38/46] test(Dialog): refactor to use dot notation over brackets --- src/page/bell/Dialog.test.ts | 16 ++++++++-------- src/page/bell/Message.test.ts | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/page/bell/Dialog.test.ts b/src/page/bell/Dialog.test.ts index 0685b9f4d..29aaf02cc 100644 --- a/src/page/bell/Dialog.test.ts +++ b/src/page/bell/Dialog.test.ts @@ -25,11 +25,11 @@ describe('Dialog', () => { isPushNotificationsEnabledSpy.mockResolvedValue(false); const dialog = new Dialog(bell); - expect(dialog['_shown']).toBe(false); - await dialog['_show'](); - expect(dialog['_shown']).toBe(true); + expect(dialog._shown).toBe(false); + await dialog._show(); + expect(dialog._shown).toBe(true); // Button should be present for subscribe - expect(dialog['_subscribeButton']).toBeTruthy(); + expect(dialog._subscribeButton).toBeTruthy(); }); test('_hide removes shown class and keeps state consistent', async () => { @@ -38,9 +38,9 @@ describe('Dialog', () => { isPushNotificationsEnabledSpy.mockResolvedValue(false); const dialog = new Dialog(bell); - await dialog['_show'](); - expect(dialog['_shown']).toBe(true); - await dialog['_hide'](); - expect(dialog['_shown']).toBe(false); + await dialog._show(); + expect(dialog._shown).toBe(true); + await dialog._hide(); + expect(dialog._shown).toBe(false); }); }); diff --git a/src/page/bell/Message.test.ts b/src/page/bell/Message.test.ts index f73fc1a26..55c3d4cf1 100644 --- a/src/page/bell/Message.test.ts +++ b/src/page/bell/Message.test.ts @@ -16,7 +16,7 @@ describe('Message', () => { test('_display shows then hides after duration and resets content type', async () => { const bell = new Bell({ enable: false }); const message = new Message(bell); - const promise = message['_display']('message', 'Hello', 1000); + const promise = message._display('message', 'Hello', 1000); // advance timers to cover delay await vi.runAllTimersAsync(); await promise; From f2d4d4a886b042a7a1583afb881a161d7660654f Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 20:48:11 -0800 Subject: [PATCH 39/46] test(Launcher): initialize test env; use dot notation over brackets --- src/page/bell/Launcher.test.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/page/bell/Launcher.test.ts b/src/page/bell/Launcher.test.ts index 696b827af..28921a332 100644 --- a/src/page/bell/Launcher.test.ts +++ b/src/page/bell/Launcher.test.ts @@ -1,13 +1,13 @@ +import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import Bell from './Bell'; import Launcher from './Launcher'; describe('Launcher', () => { beforeEach(() => { + TestEnvironment.initialize(); document.body.innerHTML = `
`; - // Minimal OneSignal to satisfy any optional accesses down the line - (global as any).OneSignal = (global as any).OneSignal ?? {}; }); test('_activateIfInactive sets wasInactive and activates only when inactive', async () => { @@ -15,26 +15,26 @@ describe('Launcher', () => { const launcher = new Launcher(bell); // Mark element as inactive by adding inactive class - launcher['_element']?.classList.add('onesignal-bell-launcher-inactive'); - expect(launcher['_active']).toBe(false); - await launcher['_activateIfInactive'](); - expect(launcher['_wasInactive']).toBe(true); - expect(launcher['_active']).toBe(true); + launcher._element?.classList.add('onesignal-bell-launcher-inactive'); + expect(launcher._active).toBe(false); + await launcher._activateIfInactive(); + expect(launcher._wasInactive).toBe(true); + expect(launcher._active).toBe(true); // Calling again should be a no-op (remains active, wasInactive unchanged) - await launcher['_activateIfInactive'](); - expect(launcher['_wasInactive']).toBe(true); - expect(launcher['_active']).toBe(true); + await launcher._activateIfInactive(); + expect(launcher._wasInactive).toBe(true); + expect(launcher._active).toBe(true); }); test('_inactivateIfWasInactive only fires when previously inactive path set', async () => { const bell = new Bell({ enable: false }); const launcher = new Launcher(bell); // Mark that it was activated from inactive state - launcher['_wasInactive'] = true; + launcher._wasInactive = true; - await launcher['_inactivateIfWasInactive'](); - expect(launcher['_wasInactive']).toBe(false); - expect(launcher['_active']).toBe(false); + await launcher._inactivateIfWasInactive(); + expect(launcher._wasInactive).toBe(false); + expect(launcher._active).toBe(false); }); }); From 0baed685b9d696c9cb462e44640fa9d4b9a079fd Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 20:49:09 -0800 Subject: [PATCH 40/46] test(Message): extract out fake timers; use dot notation over brackets --- src/page/bell/Message.test.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/page/bell/Message.test.ts b/src/page/bell/Message.test.ts index 55c3d4cf1..aeebaca04 100644 --- a/src/page/bell/Message.test.ts +++ b/src/page/bell/Message.test.ts @@ -2,6 +2,8 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import Bell from './Bell'; import Message from './Message'; +vi.useFakeTimers(); + describe('Message', () => { beforeEach(() => { TestEnvironment.initialize(); @@ -18,10 +20,10 @@ describe('Message', () => { const message = new Message(bell); const promise = message._display('message', 'Hello', 1000); // advance timers to cover delay - await vi.runAllTimersAsync(); + await vi.runOnlyPendingTimersAsync(); await promise; // After display finishes, contentType should reset to 'tip' - expect(message['_contentType']).toBe('tip'); - expect(message['_shown']).toBe(false); + expect(message._contentType).toBe('tip'); + expect(message._shown).toBe(false); }); }); From 487ecd5b9c4c20c568fc65b8b41dd174c2e0097c Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 20:51:26 -0800 Subject: [PATCH 41/46] test(SlidedownManager): add back ts-expect-error comment --- src/page/managers/slidedownManager/SlidedownManager.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/page/managers/slidedownManager/SlidedownManager.test.ts b/src/page/managers/slidedownManager/SlidedownManager.test.ts index b9c37256e..88163bc6c 100644 --- a/src/page/managers/slidedownManager/SlidedownManager.test.ts +++ b/src/page/managers/slidedownManager/SlidedownManager.test.ts @@ -314,6 +314,7 @@ export const mockPhoneLibraryLoading = () => { window.intlTelInputUtils = { numberType: { MOBILE: 1 }, + // @ts-expect-error - mock intl-tel-input numberFormat: { E164: 0 }, }; return Promise.resolve(); From 1c689d67941c672f9f91c6bec0bb40d5579c9fe5 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 20:59:24 -0800 Subject: [PATCH 42/46] test(TagManager): clean up spies and asserts --- src/page/managers/tagManager/TagManager.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/page/managers/tagManager/TagManager.test.ts b/src/page/managers/tagManager/TagManager.test.ts index f963bfafc..c5b2623ec 100644 --- a/src/page/managers/tagManager/TagManager.test.ts +++ b/src/page/managers/tagManager/TagManager.test.ts @@ -3,8 +3,8 @@ import UserNamespace from 'src/onesignal/UserNamespace'; import Log from 'src/shared/libraries/Log'; import TagManager from './TagManager'; +const addTagsSpy = vi.spyOn(UserNamespace.prototype, 'addTags'); describe('TagManager', () => { - const addTagsSpy = vi.spyOn(UserNamespace.prototype, 'addTags'); beforeEach(() => { TestEnvironment.initialize(); }); @@ -14,7 +14,7 @@ describe('TagManager', () => { tm._storeRemotePlayerTags({ a: '1' }); tm._storeTagValuesToUpdate({ a: true, b: false }); expect(OneSignal._context._tagManager._remoteTags).toEqual({ a: '1' }); - expect((tm as any)._tagsFromTaggingContainer).toEqual({ + expect(tm['_tagsFromTaggingContainer']).toEqual({ a: true, b: false, }); @@ -30,7 +30,7 @@ describe('TagManager', () => { }); test('_sendTags returns {} and warns when no change', async () => { - const warnSpy = vi.spyOn(Log, '_warn').mockImplementation(() => undefined); + const warnSpy = vi.spyOn(Log, '_warn').mockImplementation(() => ''); const tm = new TagManager(OneSignal._context); // Ensure this instance uses the same remote tags that will be diffed tm._remoteTags = { c: '1' }; From f92c6bf260c6ec16d46668bad881b772d5b53924 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 21:07:01 -0800 Subject: [PATCH 43/46] test(LoginManager): clean up mocks --- src/page/managers/LoginManager.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/page/managers/LoginManager.test.ts b/src/page/managers/LoginManager.test.ts index bc3d84de4..6bff691f1 100644 --- a/src/page/managers/LoginManager.test.ts +++ b/src/page/managers/LoginManager.test.ts @@ -1,5 +1,6 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { updateIdentityModel } from '__test__/support/helpers/setup'; +import { SubscriptionModel } from 'src/core/models/SubscriptionModel'; import { db } from 'src/shared/database/client'; import Log from 'src/shared/libraries/Log'; import * as userDirector from '../../onesignal/userDirector'; @@ -7,7 +8,6 @@ import LoginManager from './LoginManager'; const createUserOnServerSpy = vi .spyOn(userDirector, 'createUserOnServer') - .mockResolvedValue(undefined); describe('LoginManager', () => { beforeEach(() => { @@ -29,15 +29,15 @@ describe('LoginManager', () => { test('login: stores token when provided and enqueues operations', async () => { const dbSpy = vi.spyOn(db, 'put'); // mock push subscription exists so transfer op enqueues + const createPushSub = () => ({ + id: 'push-sub-id', + }); vi.spyOn( OneSignal._coreDirector, '_getPushSubscriptionModel', - ).mockResolvedValue({ - id: 'push-sub-id', - } as any); + ).mockResolvedValue(createPushSub() as SubscriptionModel); const enqueueSpy = vi .spyOn(OneSignal._coreDirector._operationRepo, '_enqueue') - .mockResolvedValue(undefined); const enqueueAndWaitSpy = vi .spyOn(OneSignal._coreDirector._operationRepo, '_enqueueAndWait') .mockResolvedValue(undefined); From 639832350a2f347f7793e6f8ca650bec6359cea6 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 21:08:26 -0800 Subject: [PATCH 44/46] test(PermissionManager): remove use of `any` --- src/shared/managers/PermissionManager.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/managers/PermissionManager.test.ts b/src/shared/managers/PermissionManager.test.ts index 5021d903c..725054352 100644 --- a/src/shared/managers/PermissionManager.test.ts +++ b/src/shared/managers/PermissionManager.test.ts @@ -24,8 +24,8 @@ describe('PermissionManager', () => { }); test('throws if context is undefined', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - OneSignal._context = undefined as any; + // @ts-expect-error - testing undefined context + OneSignal._context = undefined; const pm = new PermissionManager(); await expect(pm._getPermissionStatus()).rejects.toThrow( 'OneSignal.context is undefined. Call init first', From 92e84f9ff29f697575c522abd1a70e2d529d9e74 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Wed, 19 Nov 2025 21:14:12 -0800 Subject: [PATCH 45/46] style: fix linting issues --- __test__/setupTests.ts | 2 +- src/page/bell/Button.test.ts | 4 +++- src/page/managers/LoginManager.test.ts | 9 +++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/__test__/setupTests.ts b/__test__/setupTests.ts index 3849f42f9..dfd7d5f8a 100644 --- a/__test__/setupTests.ts +++ b/__test__/setupTests.ts @@ -62,4 +62,4 @@ export const mockJsonp = () => { }; // Polyfill Web Animations API method used by AnimatedElement -HTMLElement.prototype.getAnimations = () => []; \ No newline at end of file +HTMLElement.prototype.getAnimations = () => []; diff --git a/src/page/bell/Button.test.ts b/src/page/bell/Button.test.ts index 7c6f12a37..2cf0e3543 100644 --- a/src/page/bell/Button.test.ts +++ b/src/page/bell/Button.test.ts @@ -17,7 +17,9 @@ describe('Button', () => { const button = new Button(bell); // Simulate message being shown of type Message by adding the show class - const msgEl = document.querySelector('.onesignal-bell-launcher-message') as HTMLElement; + const msgEl = document.querySelector( + '.onesignal-bell-launcher-message', + ) as HTMLElement; msgEl.classList.add('onesignal-bell-launcher-message-opened'); bell._message._contentType = MessageType._Message; diff --git a/src/page/managers/LoginManager.test.ts b/src/page/managers/LoginManager.test.ts index 6bff691f1..8224e7c32 100644 --- a/src/page/managers/LoginManager.test.ts +++ b/src/page/managers/LoginManager.test.ts @@ -6,8 +6,7 @@ import Log from 'src/shared/libraries/Log'; import * as userDirector from '../../onesignal/userDirector'; import LoginManager from './LoginManager'; -const createUserOnServerSpy = vi - .spyOn(userDirector, 'createUserOnServer') +const createUserOnServerSpy = vi.spyOn(userDirector, 'createUserOnServer'); describe('LoginManager', () => { beforeEach(() => { @@ -36,8 +35,10 @@ describe('LoginManager', () => { OneSignal._coreDirector, '_getPushSubscriptionModel', ).mockResolvedValue(createPushSub() as SubscriptionModel); - const enqueueSpy = vi - .spyOn(OneSignal._coreDirector._operationRepo, '_enqueue') + const enqueueSpy = vi.spyOn( + OneSignal._coreDirector._operationRepo, + '_enqueue', + ); const enqueueAndWaitSpy = vi .spyOn(OneSignal._coreDirector._operationRepo, '_enqueueAndWait') .mockResolvedValue(undefined); From 21cd8255091c9e1c3dc70a21a265398c306c8bdc Mon Sep 17 00:00:00 2001 From: sherwinski Date: Thu, 20 Nov 2025 20:18:59 -0800 Subject: [PATCH 46/46] test: clean up unused statements; better organize describes --- src/page/bell/Bell.test.ts | 7 ++-- src/page/bell/Dialog.test.ts | 8 +++-- .../slidedownManager/SlidedownManager.test.ts | 2 +- src/shared/context/helpers.test.ts | 36 +++++++++++-------- .../sessionManager/SessionManager.test.ts | 9 ----- 5 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/page/bell/Bell.test.ts b/src/page/bell/Bell.test.ts index 32e5bb470..90f742d3f 100644 --- a/src/page/bell/Bell.test.ts +++ b/src/page/bell/Bell.test.ts @@ -3,11 +3,10 @@ import OneSignalEvent from '../../shared/services/OneSignalEvent'; import Bell from './Bell'; import { BellEvent, BellState } from './constants'; +// @ts-expect-error - _installEventHooks is not assignable +const spyInstall = vi.spyOn(Bell.prototype, '_installEventHooks'); +const updateStateSpy = vi.spyOn(Bell.prototype, '_updateState'); describe('Bell', () => { - // @ts-expect-error - _installEventHooks is not assignable - const spyInstall = vi.spyOn(Bell.prototype, '_installEventHooks'); - const updateStateSpy = vi.spyOn(Bell.prototype, '_updateState'); - beforeEach(() => { // Set up OneSignal globals/context to avoid accidental runtime lookups TestEnvironment.initialize(); diff --git a/src/page/bell/Dialog.test.ts b/src/page/bell/Dialog.test.ts index 29aaf02cc..873746394 100644 --- a/src/page/bell/Dialog.test.ts +++ b/src/page/bell/Dialog.test.ts @@ -14,8 +14,10 @@ describe('Dialog', () => {
`; - const sm = OneSignal._context._subscriptionManager; - isPushNotificationsEnabledSpy = vi.spyOn(sm, '_isPushNotificationsEnabled'); + isPushNotificationsEnabledSpy = vi.spyOn( + OneSignal._context._subscriptionManager, + '_isPushNotificationsEnabled', + ); }); test('_show populates content and toggles shown flag', async () => { @@ -29,7 +31,7 @@ describe('Dialog', () => { await dialog._show(); expect(dialog._shown).toBe(true); // Button should be present for subscribe - expect(dialog._subscribeButton).toBeTruthy(); + expect(dialog._subscribeButton).not.toBeNull(); }); test('_hide removes shown class and keeps state consistent', async () => { diff --git a/src/page/managers/slidedownManager/SlidedownManager.test.ts b/src/page/managers/slidedownManager/SlidedownManager.test.ts index 88163bc6c..3ce9b74fa 100644 --- a/src/page/managers/slidedownManager/SlidedownManager.test.ts +++ b/src/page/managers/slidedownManager/SlidedownManager.test.ts @@ -314,7 +314,7 @@ export const mockPhoneLibraryLoading = () => { window.intlTelInputUtils = { numberType: { MOBILE: 1 }, - // @ts-expect-error - mock intl-tel-input + // @ts-ignore @ts-expect-error - mock intl-tel-input numberFormat: { E164: 0 }, }; return Promise.resolve(); diff --git a/src/shared/context/helpers.test.ts b/src/shared/context/helpers.test.ts index c08e11db9..72632b8d1 100644 --- a/src/shared/context/helpers.test.ts +++ b/src/shared/context/helpers.test.ts @@ -19,21 +19,29 @@ describe('context/helpers', () => { }); }); - test('enforceAppId throws on empty', () => { - expect(() => enforceAppId(undefined)).toThrow('App id cannot be empty'); - expect(() => enforceAppId(null as unknown as string)).toThrow( - 'App id cannot be empty', - ); - expect(() => enforceAppId('abc')).not.toThrow(); + describe('enforceAppId', () => { + test('throws on empty', () => { + expect(() => enforceAppId(undefined)).toThrow('App id cannot be empty'); + expect(() => enforceAppId(null)).toThrow('App id cannot be empty'); + }); + + test('does not throw on valid', () => { + expect(() => enforceAppId('abc')).not.toThrow(); + }); }); - test('enforceAlias throws when label or id is empty', () => { - expect(() => enforceAlias({ label: '', id: '' })).toThrow( - 'Alias label cannot be empty', - ); - expect(() => enforceAlias({ label: 'x', id: '' })).toThrow( - 'Alias id cannot be empty', - ); - expect(() => enforceAlias({ label: 'x', id: 'y' })).not.toThrow(); + describe('enforceAlias', () => { + test('throws when label or id is empty', () => { + expect(() => enforceAlias({ label: '', id: '' })).toThrow( + 'Alias label cannot be empty', + ); + expect(() => enforceAlias({ label: 'x', id: '' })).toThrow( + 'Alias id cannot be empty', + ); + }); + + test('does not throw on valid', () => { + expect(() => enforceAlias({ label: 'x', id: 'y' })).not.toThrow(); + }); }); }); diff --git a/src/shared/managers/sessionManager/SessionManager.test.ts b/src/shared/managers/sessionManager/SessionManager.test.ts index d1d0c5852..22c7bb8de 100644 --- a/src/shared/managers/sessionManager/SessionManager.test.ts +++ b/src/shared/managers/sessionManager/SessionManager.test.ts @@ -152,9 +152,6 @@ describe('SessionManager', () => { }); test('_upsertSession does nothing when no user is present', async () => { - // Remove user singleton - (await import('src/onesignal/User')).default._singletonInstance = - undefined; supportsServiceWorkersSpy.mockReturnValue(true); const sm = new SessionManager(OneSignal._context); const notifySpy = vi.spyOn(sm, '_notifySWToUpsertSession'); @@ -166,11 +163,6 @@ describe('SessionManager', () => { supportsServiceWorkersSpy.mockReturnValue(true); const sm = new SessionManager(OneSignal._context); const setupSpy = vi.spyOn(sm, '_setupSessionEventListeners'); - // also stub ids retrieval path to avoid errors - vi.spyOn(sm as any, '_getOneSignalAndSubscriptionIds').mockResolvedValue({ - onesignalId: 'one', - subscriptionId: 'sub', - }); await sm._upsertSession(SessionOrigin._Focus); expect(setupSpy).toHaveBeenCalled(); }); @@ -178,7 +170,6 @@ describe('SessionManager', () => { test('_upsertSession emits SESSION_STARTED when SW not supported', async () => { supportsServiceWorkersSpy.mockReturnValue(false); const sm = new SessionManager(OneSignal._context); - // eslint-disable-next-line @typescript-eslint/no-explicit-any const emitSpy = vi .spyOn(OneSignal._emitter, '_emit') .mockResolvedValue(OneSignal._emitter);