Skip to content

Commit 6f30e7c

Browse files
authored
test: increase code coverage across various modules (#1409)
1 parent b70f071 commit 6f30e7c

File tree

17 files changed

+578
-6
lines changed

17 files changed

+578
-6
lines changed

__test__/setupTests.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,6 @@ export const mockJsonp = () => {
6060
downloadSpy = vi.spyOn(OneSignalApi, 'downloadServerAppConfig');
6161
downloadSpy.mockResolvedValue(serverConfig);
6262
};
63+
64+
// Polyfill Web Animations API method used by AnimatedElement
65+
HTMLElement.prototype.getAnimations = () => [];

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"test": "vitest run --coverage",
3434
"test:watch": "vitest watch --coverage",
3535
"validate:build": "node build/scripts/validate.js",
36-
"lint": "eslint src --ext .js,.jsx,.ts,.tsx; prettylint 'src/**/*' 'test/**/*' '__test__/**/*' --no-editorconfig"
36+
"lint": "eslint src --ext .js,.jsx,.ts,.tsx; prettylint 'src/**/*' 'test/**/*' '__test__/**/*' --no-editorconfig",
37+
"lint:fix": "eslint src --ext .js,.jsx,.ts,.tsx --fix; prettylint 'src/**/*' 'test/**/*' '__test__/**/*' --no-editorconfig --fix"
3738
},
3839
"config": {
3940
"sdkVersion": "160510"

src/page/bell/Badge.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { TestEnvironment } from '__test__/support/environment/TestEnvironment';
2+
import Badge from './Badge';
3+
4+
describe('Badge', () => {
5+
beforeEach(() => {
6+
TestEnvironment.initialize();
7+
document.body.innerHTML = `
8+
<div class="onesignal-bell-launcher-badge"></div>
9+
`;
10+
});
11+
12+
test('_updateCount increments and clamps to empty when <= 0', () => {
13+
const badge = new Badge();
14+
// Start at empty, increment twice
15+
badge._content = '';
16+
badge._increment();
17+
expect(badge._content).toBe('1');
18+
badge._increment();
19+
expect(badge._content).toBe('2');
20+
21+
// Decrement twice → goes to 0 and clears to empty string
22+
badge._decrement();
23+
expect(badge._content).toBe('1');
24+
badge._decrement();
25+
expect(badge._content).toBe('');
26+
});
27+
});

src/page/bell/Bell.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { TestEnvironment } from '__test__/support/environment/TestEnvironment';
2+
import OneSignalEvent from '../../shared/services/OneSignalEvent';
3+
import Bell from './Bell';
4+
import { BellEvent, BellState } from './constants';
5+
6+
// @ts-expect-error - _installEventHooks is not assignable
7+
const spyInstall = vi.spyOn(Bell.prototype, '_installEventHooks');
8+
const updateStateSpy = vi.spyOn(Bell.prototype, '_updateState');
9+
describe('Bell', () => {
10+
beforeEach(() => {
11+
// Set up OneSignal globals/context to avoid accidental runtime lookups
12+
TestEnvironment.initialize();
13+
});
14+
15+
test('constructor early-returns when enable=false and applies defaults', () => {
16+
const bell = new Bell({ enable: false });
17+
expect(bell._options.size).toBe('medium');
18+
expect(bell._options.position).toBe('bottom-right');
19+
expect(bell._options.theme).toBe('default');
20+
expect(spyInstall).not.toHaveBeenCalled();
21+
expect(updateStateSpy).not.toHaveBeenCalled();
22+
});
23+
24+
test('constructor validates and installs hooks when enable=true', () => {
25+
// Valid non-defaults to ensure validation path runs
26+
const bell = new Bell({
27+
enable: true,
28+
size: 'small',
29+
position: 'bottom-left',
30+
theme: 'inverse',
31+
showBadgeAfter: 10,
32+
showLauncherAfter: 1,
33+
});
34+
expect(bell).toBeTruthy();
35+
expect(spyInstall).toHaveBeenCalledTimes(1);
36+
expect(updateStateSpy).toHaveBeenCalledTimes(1);
37+
});
38+
39+
test('_setState triggers event when changed', () => {
40+
const bell = new Bell({ enable: false });
41+
const trigger = vi.spyOn(OneSignalEvent, '_trigger');
42+
// transition should emit
43+
bell._setState(BellState._Subscribed);
44+
45+
expect(trigger).toHaveBeenCalledWith(BellEvent._StateChanged, {
46+
from: BellState._Uninitialized,
47+
to: BellState._Subscribed,
48+
});
49+
});
50+
});

src/page/bell/Button.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { TestEnvironment } from '__test__/support/environment/TestEnvironment';
2+
import Bell from './Bell';
3+
import Button from './Button';
4+
import { MessageType } from './constants';
5+
6+
describe('Button', () => {
7+
beforeEach(() => {
8+
TestEnvironment.initialize();
9+
document.body.innerHTML = `
10+
<div class="onesignal-bell-launcher-button"></div>
11+
<div class="onesignal-bell-launcher-message"></div>
12+
`;
13+
});
14+
15+
test('_onClick concurrency guard and early-return when message showing', async () => {
16+
const bell = new Bell({ enable: false });
17+
const button = new Button(bell);
18+
19+
// Simulate message being shown of type Message by adding the show class
20+
const msgEl = document.querySelector(
21+
'.onesignal-bell-launcher-message',
22+
) as HTMLElement;
23+
msgEl.classList.add('onesignal-bell-launcher-message-opened');
24+
bell._message._contentType = MessageType._Message;
25+
26+
const toggleSpy = vi.spyOn(button, '_toggleDialog');
27+
28+
// Force concurrent scenario: set handling to true then call
29+
button._isHandlingClick = false;
30+
await button._onClick();
31+
expect(toggleSpy).not.toHaveBeenCalled();
32+
expect(button._isHandlingClick).toBe(false);
33+
});
34+
});

src/page/bell/Dialog.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { TestEnvironment } from '__test__/support/environment/TestEnvironment';
2+
import type { MockInstance } from 'vitest';
3+
import Bell from './Bell';
4+
import Dialog from './Dialog';
5+
import { BellState } from './constants';
6+
7+
describe('Dialog', () => {
8+
let isPushNotificationsEnabledSpy: MockInstance;
9+
10+
beforeEach(() => {
11+
TestEnvironment.initialize();
12+
document.body.innerHTML = `
13+
<div class="onesignal-bell-launcher-dialog">
14+
<div class="onesignal-bell-launcher-dialog-body"></div>
15+
</div>
16+
`;
17+
isPushNotificationsEnabledSpy = vi.spyOn(
18+
OneSignal._context._subscriptionManager,
19+
'_isPushNotificationsEnabled',
20+
);
21+
});
22+
23+
test('_show populates content and toggles shown flag', async () => {
24+
const bell = new Bell({ enable: false });
25+
// Put bell in unsubscribed state to render subscribe button
26+
bell._state = BellState._Unsubscribed;
27+
isPushNotificationsEnabledSpy.mockResolvedValue(false);
28+
29+
const dialog = new Dialog(bell);
30+
expect(dialog._shown).toBe(false);
31+
await dialog._show();
32+
expect(dialog._shown).toBe(true);
33+
// Button should be present for subscribe
34+
expect(dialog._subscribeButton).not.toBeNull();
35+
});
36+
37+
test('_hide removes shown class and keeps state consistent', async () => {
38+
const bell = new Bell({ enable: false });
39+
bell._state = BellState._Unsubscribed;
40+
isPushNotificationsEnabledSpy.mockResolvedValue(false);
41+
42+
const dialog = new Dialog(bell);
43+
await dialog._show();
44+
expect(dialog._shown).toBe(true);
45+
await dialog._hide();
46+
expect(dialog._shown).toBe(false);
47+
});
48+
});

src/page/bell/Launcher.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { TestEnvironment } from '__test__/support/environment/TestEnvironment';
2+
import Bell from './Bell';
3+
import Launcher from './Launcher';
4+
5+
describe('Launcher', () => {
6+
beforeEach(() => {
7+
TestEnvironment.initialize();
8+
document.body.innerHTML = `
9+
<div class="onesignal-bell-launcher"></div>
10+
`;
11+
});
12+
13+
test('_activateIfInactive sets wasInactive and activates only when inactive', async () => {
14+
const bell = new Bell({ enable: false }); // disable side-effects
15+
const launcher = new Launcher(bell);
16+
17+
// Mark element as inactive by adding inactive class
18+
launcher._element?.classList.add('onesignal-bell-launcher-inactive');
19+
expect(launcher._active).toBe(false);
20+
await launcher._activateIfInactive();
21+
expect(launcher._wasInactive).toBe(true);
22+
expect(launcher._active).toBe(true);
23+
24+
// Calling again should be a no-op (remains active, wasInactive unchanged)
25+
await launcher._activateIfInactive();
26+
expect(launcher._wasInactive).toBe(true);
27+
expect(launcher._active).toBe(true);
28+
});
29+
30+
test('_inactivateIfWasInactive only fires when previously inactive path set', async () => {
31+
const bell = new Bell({ enable: false });
32+
const launcher = new Launcher(bell);
33+
// Mark that it was activated from inactive state
34+
launcher._wasInactive = true;
35+
36+
await launcher._inactivateIfWasInactive();
37+
expect(launcher._wasInactive).toBe(false);
38+
expect(launcher._active).toBe(false);
39+
});
40+
});

src/page/bell/Message.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { TestEnvironment } from '__test__/support/environment/TestEnvironment';
2+
import Bell from './Bell';
3+
import Message from './Message';
4+
5+
vi.useFakeTimers();
6+
7+
describe('Message', () => {
8+
beforeEach(() => {
9+
TestEnvironment.initialize();
10+
document.body.innerHTML = `
11+
<div class="onesignal-bell-launcher-message">
12+
<div class="onesignal-bell-launcher-message-body"></div>
13+
</div>
14+
<div class="onesignal-bell-launcher-badge"></div>
15+
`;
16+
});
17+
18+
test('_display shows then hides after duration and resets content type', async () => {
19+
const bell = new Bell({ enable: false });
20+
const message = new Message(bell);
21+
const promise = message._display('message', 'Hello', 1000);
22+
// advance timers to cover delay
23+
await vi.runOnlyPendingTimersAsync();
24+
await promise;
25+
// After display finishes, contentType should reset to 'tip'
26+
expect(message._contentType).toBe('tip');
27+
expect(message._shown).toBe(false);
28+
});
29+
});
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { TestEnvironment } from '__test__/support/environment/TestEnvironment';
2+
import { updateIdentityModel } from '__test__/support/helpers/setup';
3+
import { SubscriptionModel } from 'src/core/models/SubscriptionModel';
4+
import { db } from 'src/shared/database/client';
5+
import Log from 'src/shared/libraries/Log';
6+
import * as userDirector from '../../onesignal/userDirector';
7+
import LoginManager from './LoginManager';
8+
9+
const createUserOnServerSpy = vi.spyOn(userDirector, 'createUserOnServer');
10+
11+
describe('LoginManager', () => {
12+
beforeEach(() => {
13+
TestEnvironment.initialize();
14+
});
15+
16+
test('login: skips when externalId unchanged and logs debug', async () => {
17+
const debugSpy = vi
18+
.spyOn(Log, '_debug')
19+
.mockImplementation(() => undefined);
20+
await updateIdentityModel('external_id', 'same-id');
21+
22+
await LoginManager.login('same-id');
23+
expect(debugSpy).toHaveBeenCalledWith(
24+
'Login: External ID already set, skipping login',
25+
);
26+
});
27+
28+
test('login: stores token when provided and enqueues operations', async () => {
29+
const dbSpy = vi.spyOn(db, 'put');
30+
// mock push subscription exists so transfer op enqueues
31+
const createPushSub = () => ({
32+
id: 'push-sub-id',
33+
});
34+
vi.spyOn(
35+
OneSignal._coreDirector,
36+
'_getPushSubscriptionModel',
37+
).mockResolvedValue(createPushSub() as SubscriptionModel);
38+
const enqueueSpy = vi.spyOn(
39+
OneSignal._coreDirector._operationRepo,
40+
'_enqueue',
41+
);
42+
const enqueueAndWaitSpy = vi
43+
.spyOn(OneSignal._coreDirector._operationRepo, '_enqueueAndWait')
44+
.mockResolvedValue(undefined);
45+
46+
await LoginManager.login('new-external-id', 'jwt-token-123');
47+
expect(dbSpy).toHaveBeenCalledWith('Ids', {
48+
id: 'jwt-token-123',
49+
type: 'jwtToken',
50+
});
51+
expect(enqueueSpy).toHaveBeenCalled();
52+
expect(enqueueAndWaitSpy).toHaveBeenCalled();
53+
});
54+
55+
test('logout: no external id logs debug and returns', async () => {
56+
const debugSpy = vi
57+
.spyOn(Log, '_debug')
58+
.mockImplementation(() => undefined);
59+
await updateIdentityModel('external_id', undefined);
60+
await LoginManager.logout();
61+
expect(debugSpy).toHaveBeenCalledWith(
62+
'Logout: User is not logged in, skipping logout',
63+
);
64+
});
65+
66+
test('logout: with external id resets models and creates anonymous user', async () => {
67+
await updateIdentityModel('external_id', 'abc');
68+
await LoginManager.logout();
69+
expect(createUserOnServerSpy).toHaveBeenCalled();
70+
});
71+
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { TestEnvironment } from '__test__/support/environment/TestEnvironment';
2+
import { setupLoadStylesheet } from '__test__/support/helpers/setup';
3+
import { Browser } from 'src/shared/useragent/constants';
4+
import * as detect from 'src/shared/useragent/detect';
5+
import { PromptsManager } from './PromptsManager';
6+
7+
const getBrowserNameSpy = vi.spyOn(detect, 'getBrowserName');
8+
const getBrowserVersionSpy = vi.spyOn(detect, 'getBrowserVersion');
9+
const isMobileBrowserSpy = vi.spyOn(detect, 'isMobileBrowser');
10+
const isTabletBrowserSpy = vi.spyOn(detect, 'isTabletBrowser');
11+
const requiresUserInteractionSpy = vi.spyOn(detect, 'requiresUserInteraction');
12+
13+
describe('PromptsManager', () => {
14+
beforeEach(() => {
15+
TestEnvironment.initialize();
16+
});
17+
18+
test('_shouldForceSlidedownOverNative returns true on Chrome>=63 mobile/tablet', async () => {
19+
getBrowserNameSpy.mockReturnValue(Browser._Chrome);
20+
getBrowserVersionSpy.mockReturnValue(70);
21+
isMobileBrowserSpy.mockReturnValue(true);
22+
isTabletBrowserSpy.mockReturnValue(false);
23+
requiresUserInteractionSpy.mockReturnValue(false);
24+
25+
const pm = new PromptsManager(OneSignal._context);
26+
expect(pm['_shouldForceSlidedownOverNative']()).toBe(true);
27+
});
28+
29+
test('_shouldForceSlidedownOverNative returns true when requiresUserInteraction', async () => {
30+
getBrowserNameSpy.mockReturnValue(Browser._Firefox);
31+
getBrowserVersionSpy.mockReturnValue(100);
32+
isMobileBrowserSpy.mockReturnValue(false);
33+
isTabletBrowserSpy.mockReturnValue(false);
34+
requiresUserInteractionSpy.mockReturnValue(true);
35+
36+
const pm = new PromptsManager(OneSignal._context);
37+
expect(pm['_shouldForceSlidedownOverNative']()).toBe(true);
38+
});
39+
40+
test('event hooks install only once for slidedown path', async () => {
41+
await setupLoadStylesheet();
42+
const pm = new PromptsManager(OneSignal._context);
43+
44+
// stub _createSlidedown to avoid side effects
45+
vi.spyOn(
46+
OneSignal._context._slidedownManager,
47+
'_createSlidedown',
48+
).mockResolvedValue(undefined);
49+
const installSpy = vi.spyOn(pm, '_installEventHooksForSlidedown');
50+
51+
await pm['_internalShowSlidedownPrompt']();
52+
await pm['_internalShowSlidedownPrompt']();
53+
expect(installSpy).toHaveBeenCalledTimes(1);
54+
});
55+
});

0 commit comments

Comments
 (0)