Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
73 changes: 73 additions & 0 deletions src/page/bell/Bell.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TestEnvironment } from '__test__/support/environment/TestEnvironment';
import { vi } from 'vitest';
import OneSignalEvent from '../../shared/services/OneSignalEvent';
import Bell from './Bell';
import { BellEvent, BellState } from './constants';
Expand Down Expand Up @@ -47,4 +48,76 @@ describe('Bell', () => {
to: BellState._Subscribed,
});
});

test('_updateState sets blocked when permission denied', async () => {
const bell = new Bell({ enable: false });
const permSpy = vi
.spyOn(OneSignal._context._permissionManager, '_getPermissionStatus')
.mockResolvedValue('denied');
const enabledSpy = vi
.spyOn(
OneSignal._context._subscriptionManager,
'_isPushNotificationsEnabled',
)
.mockResolvedValue(false);
bell._updateState();
await Promise.resolve();
await Promise.resolve();
expect(bell._blocked).toBe(true);
expect(permSpy).toHaveBeenCalled();
expect(enabledSpy).toHaveBeenCalled();
});

test('_setCustomColorsIfSpecified applies styles and adds CSS to head', async () => {
const bell = new Bell({ enable: false });
document.body.innerHTML = `
<div class="onesignal-bell-launcher">
<div class="onesignal-bell-launcher-button">
<svg>
<circle class="background"></circle>
<g class="foreground"></g>
<ellipse class="stroke"></ellipse>
</svg>
<div class="pulse-ring"></div>
</div>
<div class="onesignal-bell-launcher-badge"></div>
<div class="onesignal-bell-launcher-dialog">
<div class="onesignal-bell-launcher-dialog-body">
<button class="action">A</button>
</div>
</div>
</div>
`;
bell._options.colors = {
'circle.background': '#111',
'circle.foreground': '#222',
'badge.background': '#333',
'badge.bordercolor': '#444',
'badge.foreground': '#555',
'dialog.button.background': '#666',
'dialog.button.foreground': '#777',
'dialog.button.background.hovering': '#888',
'dialog.button.background.active': '#999',
'pulse.color': '#abc',
};
bell._setCustomColorsIfSpecified();
const background = document.querySelector<HTMLElement>('.background')!;
expect(background.getAttribute('style')).toContain('#111');
Copy link
Collaborator

@fadi-george fadi-george Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add new lines after expects

const badge = document.querySelector<HTMLElement>(
'.onesignal-bell-launcher-badge',
)!;
expect(badge.getAttribute('style')).toContain('rgb(51, 51, 51)');
const styleHover = document.getElementById(
'onesignal-background-hover-style',
);
expect(styleHover).not.toBeNull();
});

test('_addCssToHead appends once', () => {
const bell = new Bell({ enable: false });
bell._addCssToHead('x', '.a{color:red}');
bell._addCssToHead('x', '.b{color:blue}');
const style = document.getElementById('x')!;
expect(style.textContent).toContain('.a{color:red}');
});
});
65 changes: 64 additions & 1 deletion src/page/managers/PromptsManager.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { TestEnvironment } from '__test__/support/environment/TestEnvironment';
import { setupLoadStylesheet } from '__test__/support/helpers/setup';
import { DelayedPromptType } from 'src/shared/prompts/constants';
import type {
AppUserConfigPromptOptions,
DelayedPromptOptions,
DelayedPromptTypeValue,
} from 'src/shared/prompts/types';
import { Browser } from 'src/shared/useragent/constants';
import * as detect from 'src/shared/useragent/detect';
import { PromptsManager } from './PromptsManager';

const getBrowserNameSpy = vi.spyOn(detect, 'getBrowserName');
const getBrowserVersionSpy = vi.spyOn(detect, 'getBrowserVersion');
const isMobileBrowserSpy = vi.spyOn(detect, 'isMobileBrowser');
Expand Down Expand Up @@ -52,4 +57,62 @@ describe('PromptsManager', () => {
await pm['_internalShowSlidedownPrompt']();
expect(installSpy).toHaveBeenCalledTimes(1);
});

test('_internalShowDelayedPrompt forces slidedown when interaction required', async () => {
requiresUserInteractionSpy.mockReturnValue(true);
const pm = new PromptsManager(OneSignal._context);
const nativeSpy = vi
.spyOn(pm, '_internalShowNativePrompt')
.mockResolvedValue(true);
const slidedownSpy = vi
Copy link
Collaborator

@fadi-george fadi-george Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could just make _internalShowSlidedownPrompt public since it gets mangled anyway
and remove the type assertions

.spyOn(
PromptsManager.prototype,
'_internalShowSlidedownPrompt' as keyof PromptsManager,
)
.mockResolvedValue(undefined as void);
await pm._internalShowDelayedPrompt(DelayedPromptType._Native, 0);
expect(nativeSpy).not.toHaveBeenCalled();
expect(slidedownSpy).toHaveBeenCalled();
});

test('_spawnAutoPrompts triggers native when condition met and not forced', async () => {
const pm = new PromptsManager(OneSignal._context);
const getOptsSpy = vi
.spyOn(
pm as unknown as {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same thing here

_getDelayedPromptOptions: (
opts: AppUserConfigPromptOptions | undefined,
type: DelayedPromptTypeValue,
) => DelayedPromptOptions;
},
'_getDelayedPromptOptions',
)
.mockImplementation(
(): DelayedPromptOptions => ({
enabled: true,
autoPrompt: true,
timeDelay: 0,
pageViews: 0,
}),
);
const condSpy = vi
Copy link
Collaborator

@fadi-george fadi-george Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_isPageViewConditionMet can be separated out of this class since theres no this usage,


export function isPageViewConditionMet(options?: DelayedPromptOptions): boolean {
  if (!options || typeof options.pageViews === 'undefined') {
    return false;
  }

  if (!options.autoPrompt || !options.enabled) {
    return false;
  }

  const localPageViews = getLocalPageViewCount();
  return localPageViews >= options.pageViews;
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it test can do:

import * as prompts from './PromptsManager';
const { PromptsManager } = prompts;
...
    const slidedownSpy = vi
      .spyOn(prompts, 'isPageViewConditionMet')
      .mockReturnValue(false);

.spyOn(
pm as unknown as { _isPageViewConditionMet: (o?: unknown) => boolean },
'_isPageViewConditionMet',
)
.mockReturnValue(true);
const delayedSpy = vi
.spyOn(
PromptsManager.prototype,
'_internalShowDelayedPrompt' as keyof PromptsManager,
)
.mockResolvedValue(undefined as void);
requiresUserInteractionSpy.mockReturnValue(false);
getBrowserNameSpy.mockReturnValue(Browser._Chrome);
getBrowserVersionSpy.mockReturnValue(62);
await pm._spawnAutoPrompts();
expect(getOptsSpy).toHaveBeenCalled();
expect(condSpy).toHaveBeenCalled();
expect(delayedSpy).toHaveBeenCalledWith(DelayedPromptType._Native, 0);
});
});
105 changes: 105 additions & 0 deletions src/shared/managers/CustomLinkManager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { TestEnvironment } from '__test__/support/environment/TestEnvironment';
import { setupLoadStylesheet } from '__test__/support/helpers/setup';
import {
CUSTOM_LINK_CSS_CLASSES,
CUSTOM_LINK_CSS_SELECTORS,
} from 'src/shared/slidedown/constants';
import { vi, type MockInstance } from 'vitest';
import { ResourceLoadState } from '../../page/services/DynamicResourceLoader';
import { CustomLinkManager } from './CustomLinkManager';

describe('CustomLinkManager', () => {
let isPushEnabledSpy: MockInstance;
beforeEach(() => {
TestEnvironment.initialize();
document.body.innerHTML = `
<div class="${CUSTOM_LINK_CSS_SELECTORS._ContainerSelector.replace('.', '')}"></div>
`;
isPushEnabledSpy = vi.spyOn(
OneSignal._context._subscriptionManager,
'_isPushNotificationsEnabled',
);
});

test('_initialize returns when disabled or stylesheet fails', async () => {
const mgrDisabled = new CustomLinkManager({ enabled: false });
await expect(mgrDisabled._initialize()).resolves.toBeUndefined();

// Stylesheet not loaded
const mgr = new CustomLinkManager({
enabled: true,
text: { explanation: 'x', subscribe: 'Sub' },
});
vi.spyOn(
OneSignal._context._dynamicResourceLoader,
'_loadSdkStylesheet',
).mockResolvedValue(ResourceLoadState._Failed);
await mgr._initialize();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: new line after

// nothing injected
const containers = document.querySelectorAll(
CUSTOM_LINK_CSS_SELECTORS._ContainerSelector,
);
expect(containers.length).toBe(1);
expect(containers[0].children.length).toBe(0);
});

test('_initialize hides containers when subscribed and unsubscribe disabled', async () => {
await setupLoadStylesheet();
isPushEnabledSpy.mockResolvedValue(true);
const mgr = new CustomLinkManager({
enabled: true,
unsubscribeEnabled: false,
text: {
explanation: 'hello',
subscribe: 'Subscribe',
unsubscribe: 'Unsubscribe',
},
});
await mgr._initialize();
const containers = document.querySelectorAll<HTMLElement>(
CUSTOM_LINK_CSS_SELECTORS._ContainerSelector,
);
expect(
containers[0].classList.contains(CUSTOM_LINK_CSS_CLASSES._Hide),
).toBe(true);
});

test('_initialize injects markup and click toggles subscription', async () => {
await setupLoadStylesheet();
isPushEnabledSpy.mockResolvedValue(false);
const optInSpy = vi
.spyOn(OneSignal.User.PushSubscription, 'optIn')
.mockResolvedValue();
const optOutSpy = vi
.spyOn(OneSignal.User.PushSubscription, 'optOut')
.mockResolvedValue();
const mgr = new CustomLinkManager({
enabled: true,
unsubscribeEnabled: true,
text: {
explanation: 'hello',
subscribe: 'Subscribe',
unsubscribe: 'Unsubscribe',
},
style: 'button',
size: 'medium',
color: { text: '#fff', button: '#000' },
});
await mgr._initialize();
const button = document.querySelector<HTMLButtonElement>(
`.${CUSTOM_LINK_CSS_CLASSES._SubscribeClass}`,
);
expect(button).not.toBeNull();
expect(button?.textContent).toBe('Subscribe');

await button?.click();
expect(optInSpy).toHaveBeenCalled();

// simulate subscribed now (set optedIn getter)
vi.spyOn(OneSignal.User.PushSubscription, 'optedIn', 'get').mockReturnValue(
true,
);
await button?.click();
expect(optOutSpy).toHaveBeenCalled();
});
});
Loading
Loading