UNPKG

onesignal-web-sdk

Version:

Web push notifications from OneSignal.

483 lines (385 loc) 21.1 kB
import "../../support/polyfills/polyfills"; import test, { AssertContext } from "ava"; import { BrowserUserAgent, HttpHttpsEnvironment, TestEnvironment } from '../../support/sdk/TestEnvironment'; import { OneSignalStubES5 } from "../../../src/utils/OneSignalStubES5"; import { OneSignalStubES6 } from "../../../src/utils/OneSignalStubES6"; import '../../support/sdk/TestEnvironment'; import { ReplayCallsOnOneSignal } from "../../../src/utils/ReplayCallsOnOneSignal"; import { ProcessOneSignalPushCalls } from '../../../src/utils/ProcessOneSignalPushCalls'; import { OneSignalShimLoader } from "../../../src/utils/OneSignalShimLoader"; import { SinonSandbox } from "sinon"; import sinon from 'sinon'; import Log from "../../../src/libraries/Log"; import { setupBrowserWithPushAPIWithVAPIDEnv } from "../../support/tester/utils"; let sandbox: SinonSandbox; test.beforeEach(async function() { sandbox = sinon.sandbox.create(); await TestEnvironment.stubDomEnvironment({ httpOrHttps: HttpHttpsEnvironment.Https }); }); test.afterEach(function () { sandbox.restore(); }); class Defaults { public static delayedFunctionCall = { functionName: "", args: [], delayedPromise: undefined }; } class OneSignalStubES5Test extends OneSignalStubES5 { public lastDirectFunctionCall: DelayedFunctionCall<any> = Defaults.delayedFunctionCall; public lastDirectPromiseFunctionCall: DelayedFunctionCall<any> = Defaults.delayedFunctionCall; protected stubFunction(thisObj: OneSignalStubES5Test, functionName: string, args: any[]): any { thisObj.lastDirectFunctionCall = { functionName: functionName, args: args, delayedPromise: undefined }; } protected stubPromiseFunction(thisObj: OneSignalStubES5Test, functionName: string, args: any[]): Promise<any> { thisObj.lastDirectPromiseFunctionCall = { functionName: functionName, args: args, delayedPromise: undefined }; return new Promise(() => {}); } } function assertES5MethodIsCalled(t: AssertContext, oneSignalStub: OneSignalStubES5Test, functionName: string) { const retValue = (oneSignalStub as any)[functionName].call(null, `${functionName}:arg1`); t.is(oneSignalStub.lastDirectFunctionCall.functionName, functionName); t.is(oneSignalStub.lastDirectFunctionCall.args[0], `${functionName}:arg1`); t.is(oneSignalStub.lastDirectFunctionCall.args.length, 1); t.is(retValue,undefined); } function assertES5PromiseMethodIsCalled(t: AssertContext, oneSignalStub: OneSignalStubES5Test, functionName: string) { const retValue = (oneSignalStub as any)[functionName].call(null, `${functionName}:arg1`); t.is(oneSignalStub.lastDirectPromiseFunctionCall.functionName, functionName); t.is(oneSignalStub.lastDirectPromiseFunctionCall.args[0], `${functionName}:arg1`); t.is(oneSignalStub.lastDirectPromiseFunctionCall.args.length, 1); t.notDeepEqual(retValue, new Promise(() => {})); } test("correctly stubs all methods for ES5", async t => { // Make the environment unsupported by deleting Promise functionality await TestEnvironment.initialize(); (window as any).Promise = undefined; const oneSignalStub = new OneSignalStubES5Test(); t.false(oneSignalStub.isPushNotificationsSupported()); t.false(await oneSignalStub.isPushNotificationsEnabled()); // Test OneSignal.push let didCallPushFunction = false; oneSignalStub.push(() => { didCallPushFunction = true; }); t.true(didCallPushFunction); assertES5MethodIsCalled(t, oneSignalStub, "on"); assertES5MethodIsCalled(t, oneSignalStub, "off"); assertES5MethodIsCalled(t, oneSignalStub, "once"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "init"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "_initHttp"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "showHttpPrompt"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "registerForPushNotifications"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "showHttpPermissionRequest"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "getNotificationPermission"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "setDefaultTitle"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "setDefaultNotificationUrl"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "syncHashedEmail"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "getTags"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "sendTag"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "sendTags"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "deleteTag"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "deleteTags"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "addListenerForNotificationOpened"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "setSubscription"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "getUserId"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "getRegistrationId"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "getSubscription"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "sendSelfNotification"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "setEmail"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "logoutEmail"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "setExternalUserId"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "removeExternalUserId"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "getExternalUserId"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "provideUserConsent"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "isOptedOut"); assertES5PromiseMethodIsCalled(t, oneSignalStub, "getEmailId"); }); class OneSignalStubES6Test extends OneSignalStubES6 { public getLastStubCall(): DelayedFunctionCall<any> { return this.directFunctionCallsArray[this.directFunctionCallsArray.length - 1]; } } function assertES6MethodIsCalled(t: AssertContext, oneSignalStub: OneSignalStubES6Test, functionName: string) { const retValue = (oneSignalStub as any)[functionName].call(null, `${functionName}:arg1`); t.is(oneSignalStub.getLastStubCall().functionName, functionName); t.is(oneSignalStub.getLastStubCall().args[0], `${functionName}:arg1`); t.is(oneSignalStub.getLastStubCall().args.length, 1); t.is(retValue,undefined); } function assertES6PromiseMethodIsCalled(t: AssertContext, oneSignalStub: OneSignalStubES6Test, functionName: string) { const retValue = (oneSignalStub as any)[functionName].call(null, `${functionName}:arg1`); t.is(oneSignalStub.getLastStubCall().functionName, functionName); t.is(oneSignalStub.getLastStubCall().args[0], `${functionName}:arg1`); t.is(oneSignalStub.getLastStubCall().args.length, 1); t.notDeepEqual(retValue, new Promise(() => {})); } test("correctly stubs all methods for ES6", async t => { const oneSignalStub = new OneSignalStubES6Test(); t.true(oneSignalStub.isPushNotificationsSupported()); // These methods should be stub out in a generic way, make sure they don't error out and call the correct stub assertES6MethodIsCalled(t, oneSignalStub, "on"); assertES6MethodIsCalled(t, oneSignalStub, "off"); assertES6MethodIsCalled(t, oneSignalStub, "once"); assertES6MethodIsCalled(t, oneSignalStub, "push"); // These methods should be stub out in a generic way, make sure they don't error out and return a promise. assertES6PromiseMethodIsCalled(t, oneSignalStub, "init"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "_initHttp"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "showHttpPrompt"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "registerForPushNotifications"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "showHttpPermissionRequest"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "getNotificationPermission"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "isPushNotificationsEnabled"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "setDefaultTitle"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "setDefaultNotificationUrl"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "syncHashedEmail"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "getTags"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "sendTag"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "sendTags"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "deleteTag"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "deleteTags"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "addListenerForNotificationOpened"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "setSubscription"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "getUserId"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "getRegistrationId"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "getSubscription"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "sendSelfNotification"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "setEmail"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "logoutEmail"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "setExternalUserId"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "removeExternalUserId"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "getExternalUserId"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "provideUserConsent"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "isOptedOut"); assertES6PromiseMethodIsCalled(t, oneSignalStub, "getEmailId"); }); // Creating an object like OneSignal, but with only the methods we need to mock class MockOneSignal implements IOneSignal { public lastSendTags: IndexableByString<string> = {}; push(item: Function | object[]): void { ProcessOneSignalPushCalls.processItem(this, item); } // Mocking implementation of sendTags async sendTag(key: string, value: any, _callback?: Action<Object>): Promise<Object> { this.lastSendTags[key] = value; return new Promise((resolve, _reject) => { resolve(); }); } } test("Test ReplayCallsOnOneSignal replays ES6 calls with expected params", async t => { // Setup an OneSignalStubES6 instance like the OneSignalSDK.js Shim does. const oneSignalStub = new OneSignalStubES6(); // Call OneSignal.sendTags(...) directly like a site developer would const sendTagsPromise = (oneSignalStub as any).sendTag("key", "value"); // Set our fake mock to as window.OneSignal const mockOneSignal = new MockOneSignal(); (global as any).OneSignal = mockOneSignal; // Replay function calls we called on the stub on our mock ReplayCallsOnOneSignal.doReplay(oneSignalStub); // Ensure our mock's sendTags got called correctly. t.deepEqual(mockOneSignal.lastSendTags, { key: "value" }); // And the promise we resolved. await sendTagsPromise; }); test("Test ReplayCallsOnOneSignal replays ES6 calls with expected params using push with function", async t => { // Setup an OneSignalStubES6 instance like the OneSignalSDK.js Shim does. const oneSignalStub = new OneSignalStubES6(); // Call OneSignal.push(function(){}) like a site developer should be doing. const sendTagsPromise = (oneSignalStub as any).push(async function () { await (oneSignalStub as any).sendTag("key", "value"); }); // Set our fake mock to as window.OneSignal const mockOneSignal = new MockOneSignal(); (global as any).OneSignal = mockOneSignal; // Replay function calls we called on the stub on our mock ReplayCallsOnOneSignal.doReplay(oneSignalStub); // Ensure our mock's sendTags got called correctly. t.deepEqual(mockOneSignal.lastSendTags, { key: "value" }); // And the promise we resolved. await sendTagsPromise; }); test("Test ReplayCallsOnOneSignal replays ES6 calls with expected params using push with params list", async t => { // Setup an OneSignalStubES6 instance like the OneSignalSDK.js Shim does. const oneSignalStub = new OneSignalStubES6(); // Call OneSignal.push([]]) like a site developer should be doing. const sendTagsPromise = (oneSignalStub as any).push(["sendTag", "key1", "value2"]); // Set our fake mock to as window.OneSignal const mockOneSignal = new MockOneSignal(); (global as any).OneSignal = mockOneSignal; // Replay function calls we called on the stub on our mock ReplayCallsOnOneSignal.doReplay(oneSignalStub); // Ensure our mock's sendTags got called correctly. t.deepEqual(mockOneSignal.lastSendTags, { key1: "value2" }); // And the promise we resolved. await sendTagsPromise; }); test("Test ReplayCallsOnOneSignal replays ES6 calls from preExistingArray with expected params with a function", async t => { // Setup an OneSignalStubES6 instance like the OneSignalSDK.js Shim does, taking in any predefined window.OneSignal const oneSignalStub = new OneSignalStubES6([() => { (<any>global).OneSignal.sendTag("key", "value"); }]); // Set our fake mock to as window.OneSignal const mockOneSignal = new MockOneSignal(); (<any>global).OneSignal = mockOneSignal; // Replay function calls we called on the stub on our mock ReplayCallsOnOneSignal.doReplay(oneSignalStub); // Ensure our mock's sendTags got called correctly. t.deepEqual(mockOneSignal.lastSendTags, { key: "value" }); }); test("Test ReplayCallsOnOneSignal replays ES6 calls from preExistingArray with expected params using push with params list", async t => { // Setup an OneSignalStubES6 instance like the OneSignalSDK.js Shim does. const oneSignalStub = new OneSignalStubES6([["sendTag", "key1", "value2"]]); // Set our fake mock to as window.OneSignal const mockOneSignal = new MockOneSignal(); (<any>global).OneSignal = mockOneSignal; // Replay function calls we called on the stub on our mock ReplayCallsOnOneSignal.doReplay(oneSignalStub); // Ensure our mock's sendTags got called correctly. t.deepEqual(mockOneSignal.lastSendTags, { key1: "value2" }); }); class MockOneSignalWithPromiseControl { public resolveValue: any; public rejectValue: any; async sendTag(_key: string, _value: any, _callback?: Action<Object>): Promise<Object> { return new Promise((resolve, reject) => { if (this.resolveValue) resolve(this.resolveValue); if (this.rejectValue) { reject(this.rejectValue); } }); } } test("Test ReplayCallsOnOneSignal replays ES6 calls executing resolve promise", async t => { // Setup an OneSignalStubES6 instance like the OneSignalSDK.js Shim does. const oneSignalStub = new OneSignalStubES6(); // Call OneSignal.sendTags(...) directly like a site developer may have done const sendTagsPromise = (oneSignalStub as any).sendTag("key", "value"); // Set our fake mock to as window.OneSignal const mockOneSignal = new MockOneSignalWithPromiseControl(); mockOneSignal.resolveValue = "resolveValue"; (global as any).OneSignal = mockOneSignal; // Replay function calls we called on the stub on our mock ReplayCallsOnOneSignal.doReplay(oneSignalStub); sendTagsPromise.then((value: string) => { t.is(value, "resolveValue"); }); await sendTagsPromise; // Make sure 1 assert is made for the then t.plan(1); }); test("Test ReplayCallsOnOneSignal replays ES6 calls executing resolve promise", async t => { // Setup an OneSignalStubES6 instance like the OneSignalSDK.js Shim does. const oneSignalStub = new OneSignalStubES6(); // Call OneSignal.sendTags(...) directly like a site developer may have done const sendTagsPromise = (oneSignalStub as any).sendTag("key", "value"); // Set our fake mock to as window.OneSignal const mockOneSignal = new MockOneSignalWithPromiseControl(); mockOneSignal.rejectValue = "rejectValue"; (global as any).OneSignal = mockOneSignal; // Replay function calls we called on the stub on our mock ReplayCallsOnOneSignal.doReplay(oneSignalStub); sendTagsPromise.catch((value: string) => { t.is(value, "rejectValue"); }); try { await sendTagsPromise; } catch (e) { t.is(e, "rejectValue"); } // Make sure 2 asserts are made, one for the local catch and the other on the await t.plan(2); }); class MockOneSignalWithPublicProperties { public SERVICE_WORKER_UPDATER_PATH: string | undefined; public SERVICE_WORKER_PATH: string | undefined; public SERVICE_WORKER_PARAM: { scope: string } | undefined; public currentLogLevel: string | undefined; public log = { setLevel: (level: string): void => { this.currentLogLevel = level; } }; } test("Make sure property field transfer over", async t => { const oneSignalStub = new OneSignalStubES6(); oneSignalStub.SERVICE_WORKER_PATH = "SERVICE_WORKER_UPDATER_PATH"; oneSignalStub.SERVICE_WORKER_UPDATER_PATH = "SERVICE_WORKER_UPDATER_PATH"; oneSignalStub.SERVICE_WORKER_PARAM = { scope: "scope" }; oneSignalStub.log.setLevel("trace"); const mockOneSignal = new MockOneSignalWithPublicProperties(); (global as any).OneSignal = mockOneSignal; // Replay function calls we called on the stub on our mock ReplayCallsOnOneSignal.doReplay(oneSignalStub); t.is(mockOneSignal.SERVICE_WORKER_PATH, "SERVICE_WORKER_UPDATER_PATH"); t.is(mockOneSignal.SERVICE_WORKER_UPDATER_PATH, "SERVICE_WORKER_UPDATER_PATH"); t.is(mockOneSignal.currentLogLevel, "trace"); t.deepEqual(mockOneSignal.SERVICE_WORKER_PARAM, { scope: "scope" }); }); test("Expect Promise to never resolve for ES5 stubs", async t => { // Setup an OneSignalStubES5 instance like the OneSignalSDK.js Shim does. const oneSignalStub = new OneSignalStubES5(); const sendTagsPromise = (oneSignalStub as any).sendTag("key", "value"); sendTagsPromise .then(() => t.fail() ) .catch(() => t.fail()); t.pass(); }); test("OneSignalSDK.js loads OneSignalStubES6 is loaded on a page on a browser supports push", async t => { setupBrowserWithPushAPIWithVAPIDEnv(sandbox); OneSignalShimLoader.start(); t.true((<any>window).OneSignal instanceof OneSignalStubES6); }); test("OneSignalSDK.js is loaded on a page on a browser that does NOT support push", async t => { // Setup spy for OneSignalShimLoader.addScriptToPage const addScriptToPageSpy = sandbox.spy(OneSignalShimLoader, <any>'addScriptToPage'); OneSignalShimLoader.start(); // Load ES5 stub on IE11. Built into shim, no need to load another js file. t.true((<any>window).OneSignal instanceof OneSignalStubES5); // Should NOT load any other .js files, such as the ES6 SDK t.is(addScriptToPageSpy.callCount, 0); }); test("OneSignalSDK.js load from service worker context that does NOT support push", async t => { // 2 stub function calls to mock being a ServiceWorker that supports push. sandbox.stub(OneSignalShimLoader, <any>'isServiceWorkerRuntime').callsFake(() => { return true; }); // Setup spy for self.importScripts (<any>global).self = { importScripts: () => {} }; const importScriptsSpy = sandbox.spy((<any>global).self, 'importScripts'); // Setup spy for OneSignalShimLoader.addScriptToPage const addScriptToPageSpy = sandbox.spy(OneSignalShimLoader, <any>'addScriptToPage'); OneSignalShimLoader.start(); t.is(importScriptsSpy.callCount, 0); t.is(addScriptToPageSpy.callCount, 0); }); test("OneSignalSDK.js load from service worker context that supports push", async t => { // 2 stub function calls to mock being a ServiceWorker that supports push. sandbox.stub(OneSignalShimLoader, <any>'isServiceWorkerRuntime').callsFake(() => { return true; }); sandbox.stub(OneSignalShimLoader, <any>'serviceWorkerSupportsPush').callsFake(() => { return true; }); // Setup mock for self.importScripts (<any>global).self = { importScripts: () => {} }; const importScriptsSpy = sandbox.spy((<any>global).self, 'importScripts'); OneSignalShimLoader.start(); // Ensure we load the worker build of the SDK with self.importScripts(<string>) t.true(importScriptsSpy.getCall(0).calledWithExactly("https://cdn.onesignal.com/sdks/OneSignalSDKWorker.js?v=1")); }); test("Existing OneSignal array before OneSignalSDK.js loaded ES6", async t => { const preExistingArray = [() => {}, ["init", "test"]]; (<any>window).OneSignal = preExistingArray; setupBrowserWithPushAPIWithVAPIDEnv(sandbox); OneSignalShimLoader.start(); t.deepEqual(<object[]>(<OneSignalStubES6>(<any>window).OneSignal).preExistingArray, preExistingArray); }); test("Existing OneSignal array before OneSignalSDK.js loaded ES5", async t => { const logErrorSpy = sandbox.spy(Log, "error"); let didCallFunction = false; const preExistingArray = [() => { didCallFunction = true; (<any>window).OneSignal.setDefaultNotificationUrl("test"); }]; (<any>window).OneSignal = preExistingArray; OneSignalShimLoader.start(); t.true(didCallFunction); t.false(logErrorSpy.called); });