Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
7de196c
test(SessionManager): add missing tests for core functionality
sherwinski Nov 18, 2025
c59cf33
test(Badge): `_updateCount` increments/decrements and clears when <=0
sherwinski Nov 18, 2025
5ebd371
test(Bell): add cases for constructor behavior; state transitions
sherwinski Nov 18, 2025
ca58f1f
test(Button): add case for `_onClick` concurrency guard
sherwinski Nov 18, 2025
595a93c
test(Dialog): add `_show`/`_hide` tests
sherwinski Nov 18, 2025
54b79c1
test(Launcher): `_activateIfInactive` activates only once; `_inactiva…
sherwinski Nov 18, 2025
497336b
test(Message): `_display` shows message with timeout then hides; test…
sherwinski Nov 18, 2025
a352438
test(LoginManager): `_switchingUsersPromise` sequencing; login with t…
sherwinski Nov 18, 2025
35e993e
test(PromptsManager): `_shouldForceSlidedownOverNative` decisions; ev…
sherwinski Nov 18, 2025
c9ab2cc
test(TagManager): `_sendTags` calls `OneSignal.User.addTags` when dif…
sherwinski Nov 18, 2025
7b6eb3e
test(helpers): `timeoutPromise` resolves and times out, returns raced…
sherwinski Nov 18, 2025
907acf8
test(IDManager): _createLocalId uses LOCAL_PREFIX and produces UUID-l…
sherwinski Nov 18, 2025
631d230
test(PermissionManager): add tests for core functionality
sherwinski Nov 18, 2025
a2e98bc
test: remove unnecessary vitest global imports
sherwinski Nov 18, 2025
e10d88b
test: remove unnecessary ts-expext-error comments
sherwinski Nov 18, 2025
8843f8e
test: remove unnecessary vi global import
sherwinski Nov 18, 2025
99fe01c
test(PermissionManager): improve tests
sherwinski Nov 18, 2025
2c98079
test: remove unnecessary `initialize` parameters and `restoreAllMocks`
sherwinski Nov 18, 2025
7c8f9b6
chore(scripts): add task for fixing lint issues
sherwinski Nov 18, 2025
7683709
test: fix linting issues; remove unnecessary `as any` where possible
sherwinski Nov 18, 2025
cfd4c99
test(PermissionManager): opt for spies over mocks
sherwinski Nov 19, 2025
f123ecd
test(PromptsManager): opt for spies over mocks
sherwinski Nov 19, 2025
cdd9765
test(IDManager): update uuid sample to be more similar to a realist s…
sherwinski Nov 19, 2025
e8258a7
test(SessionManager): extract out a single ServiceWorker spy
sherwinski Nov 19, 2025
b8a396d
test(helpers): extract out fakeTimers; add timeoutPromise describe block
sherwinski Nov 19, 2025
d93fb22
test(LoginManager): opt for spies over mocks
sherwinski Nov 19, 2025
2c5f8fc
test(TagManager): opt for spies over mocks
sherwinski Nov 19, 2025
dbb9b01
test: avoid use of globalThis in favor of global
sherwinski Nov 19, 2025
a455ebe
test(Button): clean up test env initilization step
sherwinski Nov 19, 2025
c652c52
test(Bell): extract out install and update spies
sherwinski Nov 19, 2025
e86c9ce
test(Button): refactor to use dot notation over brackets
sherwinski Nov 19, 2025
79f7abe
style: fix lint errors
sherwinski Nov 19, 2025
d8e82d6
test(Badge): refactor to use dot notation over brackets
sherwinski Nov 20, 2025
2f67558
test(Bell): use toHaveBeenCalledWith for more detailed assert
sherwinski Nov 20, 2025
700361c
test(Button): initialize test env before other test fixtures
sherwinski Nov 20, 2025
668b765
test: add Animations API polyfill to setupTests
sherwinski Nov 20, 2025
95f047c
test(Dialog): extract out `_isPushNotificationsEnabled` spy
sherwinski Nov 20, 2025
b42fb7f
test(Dialog): refactor to use dot notation over brackets
sherwinski Nov 20, 2025
f2d4d4a
test(Launcher): initialize test env; use dot notation over brackets
sherwinski Nov 20, 2025
0baed68
test(Message): extract out fake timers; use dot notation over brackets
sherwinski Nov 20, 2025
487ecd5
test(SlidedownManager): add back ts-expect-error comment
sherwinski Nov 20, 2025
1c689d6
test(TagManager): clean up spies and asserts
sherwinski Nov 20, 2025
f92c6bf
test(LoginManager): clean up mocks
sherwinski Nov 20, 2025
6398323
test(PermissionManager): remove use of `any`
sherwinski Nov 20, 2025
92e84f9
style: fix linting issues
sherwinski Nov 20, 2025
21cd825
test: clean up unused statements; better organize describes
sherwinski Nov 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions __test__/setupTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => [];
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
27 changes: 27 additions & 0 deletions src/page/bell/Badge.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { TestEnvironment } from '__test__/support/environment/TestEnvironment';
import Badge from './Badge';

describe('Badge', () => {
beforeEach(() => {
TestEnvironment.initialize();
document.body.innerHTML = `
<div class="onesignal-bell-launcher-badge"></div>
`;
});

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('');
});
});
50 changes: 50 additions & 0 deletions src/page/bell/Bell.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { TestEnvironment } from '__test__/support/environment/TestEnvironment';
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', () => {
beforeEach(() => {
// Set up OneSignal globals/context to avoid accidental runtime lookups
TestEnvironment.initialize();
});

test('constructor early-returns when enable=false and applies defaults', () => {
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(updateStateSpy).not.toHaveBeenCalled();
});

test('constructor validates and installs hooks when enable=true', () => {
// 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(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(BellState._Subscribed);

expect(trigger).toHaveBeenCalledWith(BellEvent._StateChanged, {
from: BellState._Uninitialized,
to: BellState._Subscribed,
});
});
});
34 changes: 34 additions & 0 deletions src/page/bell/Button.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { TestEnvironment } from '__test__/support/environment/TestEnvironment';
import Bell from './Bell';
import Button from './Button';
import { MessageType } from './constants';

describe('Button', () => {
beforeEach(() => {
TestEnvironment.initialize();
document.body.innerHTML = `
<div class="onesignal-bell-launcher-button"></div>
<div class="onesignal-bell-launcher-message"></div>
`;
});

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, '_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);
});
});
48 changes: 48 additions & 0 deletions src/page/bell/Dialog.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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 = `
<div class="onesignal-bell-launcher-dialog">
<div class="onesignal-bell-launcher-dialog-body"></div>
</div>
`;
isPushNotificationsEnabledSpy = vi.spyOn(
OneSignal._context._subscriptionManager,
'_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._state = BellState._Unsubscribed;
isPushNotificationsEnabledSpy.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).not.toBeNull();
});

test('_hide removes shown class and keeps state consistent', async () => {
const bell = new Bell({ enable: false });
bell._state = BellState._Unsubscribed;
isPushNotificationsEnabledSpy.mockResolvedValue(false);

const dialog = new Dialog(bell);
await dialog._show();
expect(dialog._shown).toBe(true);
await dialog._hide();
expect(dialog._shown).toBe(false);
});
});
40 changes: 40 additions & 0 deletions src/page/bell/Launcher.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { TestEnvironment } from '__test__/support/environment/TestEnvironment';
import Bell from './Bell';
import Launcher from './Launcher';

describe('Launcher', () => {
beforeEach(() => {
TestEnvironment.initialize();
document.body.innerHTML = `
<div class="onesignal-bell-launcher"></div>
`;
});

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);
});
});
29 changes: 29 additions & 0 deletions src/page/bell/Message.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { TestEnvironment } from '__test__/support/environment/TestEnvironment';
import Bell from './Bell';
import Message from './Message';

vi.useFakeTimers();

describe('Message', () => {
beforeEach(() => {
TestEnvironment.initialize();
document.body.innerHTML = `
<div class="onesignal-bell-launcher-message">
<div class="onesignal-bell-launcher-message-body"></div>
</div>
<div class="onesignal-bell-launcher-badge"></div>
`;
});

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.runOnlyPendingTimersAsync();
await promise;
// After display finishes, contentType should reset to 'tip'
expect(message._contentType).toBe('tip');
expect(message._shown).toBe(false);
});
});
71 changes: 71 additions & 0 deletions src/page/managers/LoginManager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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';
import LoginManager from './LoginManager';

const createUserOnServerSpy = vi.spyOn(userDirector, 'createUserOnServer');

describe('LoginManager', () => {
beforeEach(() => {
TestEnvironment.initialize();
});

test('login: skips when externalId unchanged and logs debug', async () => {
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',
);
});

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(createPushSub() as SubscriptionModel);
const enqueueSpy = vi.spyOn(
OneSignal._coreDirector._operationRepo,
'_enqueue',
);
const enqueueAndWaitSpy = vi
.spyOn(OneSignal._coreDirector._operationRepo, '_enqueueAndWait')
.mockResolvedValue(undefined);

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);
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 () => {
await updateIdentityModel('external_id', 'abc');
await LoginManager.logout();
expect(createUserOnServerSpy).toHaveBeenCalled();
});
});
55 changes: 55 additions & 0 deletions src/page/managers/PromptsManager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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';

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(() => {
TestEnvironment.initialize();
});

test('_shouldForceSlidedownOverNative returns true on Chrome>=63 mobile/tablet', async () => {
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 () => {
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);
});

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,
'_createSlidedown',
).mockResolvedValue(undefined);
const installSpy = vi.spyOn(pm, '_installEventHooksForSlidedown');

await pm['_internalShowSlidedownPrompt']();
await pm['_internalShowSlidedownPrompt']();
expect(installSpy).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -305,16 +305,16 @@
OneSignal._didLoadITILibrary = true;

// @ts-expect-error - mock intl-tel-input
window.intlTelInput = vi.fn().mockImplementation((input) => ({

Check warning on line 308 in src/page/managers/slidedownManager/SlidedownManager.test.ts

View workflow job for this annotation

GitHub Actions / test

'input' is defined but never used
getNumber: () => '+1234567890', // Return formatted number
isValidNumber: () => true,
getNumberType: () => 0,
destroy: () => {},

Check failure on line 312 in src/page/managers/slidedownManager/SlidedownManager.test.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected empty method 'destroy'
}));

window.intlTelInputUtils = {
numberType: { MOBILE: 1 },
// @ts-expect-error - mock intl-tel-input
// @ts-ignore @ts-expect-error - mock intl-tel-input

Check failure on line 317 in src/page/managers/slidedownManager/SlidedownManager.test.ts

View workflow job for this annotation

GitHub Actions / test

Do not use "@ts-ignore" because it alters compilation errors
numberFormat: { E164: 0 },
};
return Promise.resolve();
Expand Down
Loading
Loading