diff --git a/examples/index.html b/examples/index.html index f1eef67..26e1f6e 100644 --- a/examples/index.html +++ b/examples/index.html @@ -5,7 +5,7 @@ Adobe Data Layer | Examples - +

Adobe Client Data Layer | Examples

diff --git a/examples/js/datalayer.mocks.2.js b/examples/js/datalayer.mocks.2.js new file mode 100644 index 0000000..772e3b9 --- /dev/null +++ b/examples/js/datalayer.mocks.2.js @@ -0,0 +1,52 @@ +/* +Copyright 2019 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +/* global console, window, dataLayer, CustomEvent */ +(function() { + 'use strict'; + + /* eslint no-console: "off" */ + /* eslint no-unused-vars: "off" */ + + // Test case: scope = past -> console output should be: event1, event2 + + window.adobeDataLayer = window.adobeDataLayer || []; + + var myHandler = function(event) { + console.log(event.event); + }; + + adobeDataLayer.push({ + event: "event1" + }); + + adobeDataLayer.push(function(dl) { + dl.push({ + event: "event2" + }); + + dl.addEventListener( + "adobeDataLayer:event", + myHandler, + {"scope": "past"} + ); + + dl.push({ + event: "event3" + }); + + }); + + adobeDataLayer.push({ + event: "event4" + }); + +})(); diff --git a/examples/js/datalayer.mocks.3.js b/examples/js/datalayer.mocks.3.js new file mode 100644 index 0000000..98e5d03 --- /dev/null +++ b/examples/js/datalayer.mocks.3.js @@ -0,0 +1,52 @@ +/* +Copyright 2019 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +/* global console, window, dataLayer, CustomEvent */ +(function() { + 'use strict'; + + /* eslint no-console: "off" */ + /* eslint no-unused-vars: "off" */ + + // Test case: scope = future -> console output should be: event3, event4 + + window.adobeDataLayer = window.adobeDataLayer || []; + + var myHandler = function(event) { + console.log(event.event); + }; + + adobeDataLayer.push({ + event: "event1" + }); + + adobeDataLayer.push(function(dl) { + dl.push({ + event: "event2" + }); + + dl.addEventListener( + "adobeDataLayer:event", + myHandler, + {"scope": "future"} + ); + + dl.push({ + event: "event3" + }); + + }); + + adobeDataLayer.push({ + event: "event4" + }); + +})(); diff --git a/src/__tests__/DataLayer.test.js b/src/__tests__/DataLayer.test.js deleted file mode 100644 index cebd155..0000000 --- a/src/__tests__/DataLayer.test.js +++ /dev/null @@ -1,810 +0,0 @@ -/* -Copyright 2019 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ -const isEqual = require('lodash/isEqual'); -const isEmpty = require('lodash/isEmpty'); -const merge = require('lodash/merge'); - -const testData = require('./testData'); -const ITEM_CONSTRAINTS = require('../itemConstraints'); -const DataLayer = require('../'); -let adobeDataLayer; - -const ancestorRemoved = require('../utils/ancestorRemoved'); -const customMerge = require('../utils/customMerge'); -const dataMatchesContraints = require('../utils/dataMatchesContraints'); -const indexOfListener = require('../utils/indexOfListener'); -const listenerMatch = require('../utils/listenerMatch'); - -const clearDL = function() { - beforeEach(() => { - adobeDataLayer = []; - DataLayer.Manager({ dataLayer: adobeDataLayer }); - }); -}; - -// ----------------------------------------------------------------------------------------------------------------- -// State -// ----------------------------------------------------------------------------------------------------------------- - -describe('State', () => { - clearDL(); - - test('getState()', () => { - const carousel1 = { - id: '/content/mysite/en/home/jcr:content/root/carousel1', - items: {} - }; - const data = { - component: { - carousel: { - carousel1: carousel1 - } - } - }; - adobeDataLayer.push(data); - expect(adobeDataLayer.getState()).toEqual(data); - expect(adobeDataLayer.getState('component.carousel.carousel1')).toEqual(carousel1); - expect(isEmpty(adobeDataLayer.getState('undefined-path'))); - }); -}); - -// ----------------------------------------------------------------------------------------------------------------- -// Initialization order -// ----------------------------------------------------------------------------------------------------------------- - -describe('Initialization order', () => { - beforeEach(() => { - adobeDataLayer = []; - }); - - test('listener > event > initialization', () => { - const mockCallback = jest.fn(); - adobeDataLayer.push(function(dl) { dl.addEventListener('adobeDataLayer:event', mockCallback); }); - adobeDataLayer.push(testData.carousel1click); - DataLayer.Manager({ dataLayer: adobeDataLayer }); - - expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); - }); - - test('event > listener > initialization', () => { - const mockCallback = jest.fn(); - adobeDataLayer.push(testData.carousel1click); - adobeDataLayer.push(function(dl) { dl.addEventListener('adobeDataLayer:event', mockCallback); }); - DataLayer.Manager({ dataLayer: adobeDataLayer }); - - expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); - }); - - test('listener > initialization > event', () => { - const mockCallback = jest.fn(); - adobeDataLayer.push(function(dl) { dl.addEventListener('adobeDataLayer:event', mockCallback); }); - DataLayer.Manager({ dataLayer: adobeDataLayer }); - adobeDataLayer.push(testData.carousel1click); - - expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); - }); - - test('event > initialization > listener', () => { - const mockCallback = jest.fn(); - adobeDataLayer.push(testData.carousel1click); - DataLayer.Manager({ dataLayer: adobeDataLayer }); - adobeDataLayer.push(function(dl) { dl.addEventListener('adobeDataLayer:event', mockCallback); }); - - expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); - }); -}); - -// ----------------------------------------------------------------------------------------------------------------- -// Data -// ----------------------------------------------------------------------------------------------------------------- - -describe('Data', () => { - clearDL(); - - test('push page', () => { - adobeDataLayer.push(testData.page1); - expect(adobeDataLayer.getState(), 'page is in data layer after push').toStrictEqual(testData.page1); - }); - - test('push data, override and remove', () => { - adobeDataLayer.push({ test: 'foo' }); - expect(adobeDataLayer.getState(), 'data pushed').toStrictEqual({ test: 'foo' }); - - adobeDataLayer.push({ test: 'bar' }); - expect(adobeDataLayer.getState(), 'data overriden').toStrictEqual({ test: 'bar' }); - - adobeDataLayer.push({ test: null }); - expect(adobeDataLayer.getState(), 'data removed').toStrictEqual({}); - }); - - test('push components and override', () => { - const twoCarousels = merge({}, testData.carousel1, testData.carousel2); - const carousel1empty = merge({}, testData.carousel1empty, testData.carousel2); - const carousel2empty = merge({}, testData.carousel1, testData.carousel2empty); - const twoCarouselsEmpty = merge({}, testData.carousel1empty, testData.carousel2empty); - - adobeDataLayer.push(testData.carousel1); - adobeDataLayer.push(testData.carousel1withNullAndUndefinedArrayItems); - expect(adobeDataLayer.getState(), 'carousel 1 with removed items').toStrictEqual(testData.carousel1withRemovedArrayItems); - - adobeDataLayer.push(twoCarousels); - expect(adobeDataLayer.getState(), 'carousel 1 with data, carousel 2 with data').toStrictEqual(twoCarousels); - - adobeDataLayer.push(testData.carousel1withUndefined); - expect(adobeDataLayer.getState(), 'carousel 1 empty, carousel 2 with data').toStrictEqual(carousel1empty); - - adobeDataLayer.push(testData.carousel2withUndefined); - expect(adobeDataLayer.getState(), 'carousel 1 empty, carousel 2 empty').toStrictEqual(twoCarouselsEmpty); - - adobeDataLayer.push(testData.carousel1); - expect(adobeDataLayer.getState(), 'carousel 1 with data, carousel 2 empty').toStrictEqual(carousel2empty); - - adobeDataLayer.push(testData.carousel1withNull); - expect(adobeDataLayer.getState(), 'carousel 1 empty, carousel 2 empty').toStrictEqual(twoCarouselsEmpty); - - adobeDataLayer.push(testData.carousel1); - expect(adobeDataLayer.getState(), 'carousel 1 with data, carousel 2 empty').toStrictEqual(carousel2empty); - }); - - test('push eventInfo without event', () => { - adobeDataLayer.push({ eventInfo: 'test' }); - - expect(adobeDataLayer.getState(), 'no event info added').toStrictEqual({}); - }); - - test('push invalid data type - string', () => { - adobeDataLayer.push('test'); - - expect(adobeDataLayer.getState(), 'string is invalid data type and is not part of the state').toStrictEqual({}); - }); - - test('push invalid data type - array of strings', () => { - adobeDataLayer.push(['test1', 'test2']); - - expect(adobeDataLayer.getState(), 'string is invalid data type and is not part of the state').toStrictEqual({}); - }); - - test('push initial data provided before data layer initialization', () => { - adobeDataLayer = [testData.carousel1, testData.carousel2]; - DataLayer.Manager({ dataLayer: adobeDataLayer }); - - expect(adobeDataLayer.getState(), 'all items are pushed to data layer state').toStrictEqual(merge({}, testData.carousel1, testData.carousel2)); - }); - - test('invalid initial data triggers error', () => { - // Catches console.error function which should be triggered by data layer during this test - var consoleSpy = jest.spyOn(console, 'error').mockImplementation(); - adobeDataLayer = ['test']; - DataLayer.Manager({ dataLayer: adobeDataLayer }); - - expect(adobeDataLayer.getState(), 'initialization').toStrictEqual({}); - expect(consoleSpy).toHaveBeenCalled(); - // Restores console.error to default behaviour - consoleSpy.mockRestore(); - }); - - test('push on / off listeners is not allowed', () => { - adobeDataLayer.push({ - on: 'click', - handler: jest.fn() - }); - adobeDataLayer.push({ - off: 'click', - handler: jest.fn() - }); - expect(adobeDataLayer.getState()).toStrictEqual({}); - }); -}); - -// ----------------------------------------------------------------------------------------------------------------- -// Events -// ----------------------------------------------------------------------------------------------------------------- - -describe('Events', () => { - clearDL(); - - test('push simple event', () => { - adobeDataLayer.push(testData.carousel1click); - expect(adobeDataLayer.getState()).toStrictEqual(testData.carousel1); - }); - - test('check number of arguments in callback', () => { - let calls = 0; - - adobeDataLayer.addEventListener('test', function() { calls = arguments.length; }); - - adobeDataLayer.push({ event: 'test' }); - expect(calls, 'just one argument if no data is added').toStrictEqual(1); - - adobeDataLayer.push({ event: 'test', eventInfo: 'test' }); - expect(calls, 'just one argument if no data is added').toStrictEqual(1); - - adobeDataLayer.push({ event: 'test', somekey: 'somedata' }); - expect(calls, 'three arguments if data is added').toStrictEqual(3); - }); - - test('check if eventInfo is passed to callback', () => { - adobeDataLayer.addEventListener('test', function() { - expect(arguments[0].eventInfo).toStrictEqual('test'); - }); - - adobeDataLayer.push({ event: 'test', eventInfo: 'test' }); - }); -}); - -// ----------------------------------------------------------------------------------------------------------------- -// Functions -// ----------------------------------------------------------------------------------------------------------------- - -describe('Functions', () => { - clearDL(); - - test('push simple function', () => { - const mockCallback = jest.fn(); - adobeDataLayer.push(mockCallback); - expect(mockCallback.mock.calls.length).toBe(1); - }); - - test('function adds event listener for adobeDataLayer:change', () => { - const mockCallback = jest.fn(); - const addEventListener = function(adl) { - adl.addEventListener('adobeDataLayer:change', mockCallback); - }; - - adobeDataLayer.push(testData.carousel1); - adobeDataLayer.push(addEventListener); - adobeDataLayer.push(testData.carousel2); - - expect(mockCallback.mock.calls.length, 'event triggered twice').toBe(2); - }); - - test('function updates component in data layer state', () => { - const updateCarousel = function(adl) { - adl.push(testData.carousel1new); - }; - - adobeDataLayer.push(testData.carousel1); - expect(adobeDataLayer.getState(), 'carousel set to carousel1').toEqual(testData.carousel1); - - adobeDataLayer.push(updateCarousel); - expect(adobeDataLayer.getState(), 'carousel set to carousel1new').toEqual(testData.carousel1new); - }); -}); - -// ----------------------------------------------------------------------------------------------------------------- -// Event listeners -// ----------------------------------------------------------------------------------------------------------------- - -describe('Event listeners', () => { - clearDL(); - - describe('types', () => { - test('adobeDataLayer:change triggered by component push', () => { - const mockCallback = jest.fn(); - - // edge case: unregister when no listener had been registered - adobeDataLayer.removeEventListener('adobeDataLayer:change'); - - adobeDataLayer.addEventListener('adobeDataLayer:change', mockCallback); - adobeDataLayer.push(testData.carousel1); - expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); - - adobeDataLayer.removeEventListener('adobeDataLayer:change'); - adobeDataLayer.push(testData.carousel2); - expect(mockCallback.mock.calls.length, 'callback not triggered second time').toBe(1); - }); - - test('adobeDataLayer:change triggered by event push', () => { - const mockCallback = jest.fn(); - - adobeDataLayer.addEventListener('adobeDataLayer:change', mockCallback); - adobeDataLayer.push(testData.carousel1click); - expect(mockCallback.mock.calls.length).toBe(1); - - adobeDataLayer.removeEventListener('adobeDataLayer:change'); - adobeDataLayer.push(testData.carousel1change); - expect(mockCallback.mock.calls.length).toBe(1); - }); - - test('adobeDataLayer:event triggered by event push', () => { - const mockCallback = jest.fn(); - - adobeDataLayer.addEventListener('adobeDataLayer:event', mockCallback); - adobeDataLayer.push(testData.carousel1click); - expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); - - adobeDataLayer.removeEventListener('adobeDataLayer:event'); - adobeDataLayer.push(testData.carousel1click); - expect(mockCallback.mock.calls.length, 'callback not triggered second time').toBe(1); - }); - - test('adobeDataLayer:change not triggered by event push', () => { - const mockCallback = jest.fn(); - - adobeDataLayer.addEventListener('adobeDataLayer:change', mockCallback); - adobeDataLayer.push({ - event: 'page loaded' - }); - expect(mockCallback.mock.calls.length, 'callback not triggered').toBe(0); - adobeDataLayer.push(testData.carousel1click); - expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); - }); - - test('custom event triggered by event push', () => { - const mockCallback = jest.fn(); - - adobeDataLayer.addEventListener('carousel clicked', mockCallback); - adobeDataLayer.push(testData.carousel1click); - expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); - - adobeDataLayer.removeEventListener('carousel clicked'); - adobeDataLayer.push(testData.carousel1click); - expect(mockCallback.mock.calls.length, 'callback not triggered second time').toBe(1); - }); - }); - - describe('scopes', () => { - test('past', () => { - const mockCallback = jest.fn(); - - adobeDataLayer.push(testData.carousel1click); - adobeDataLayer.addEventListener('carousel clicked', mockCallback, { scope: 'past' }); - expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); - - adobeDataLayer.push(testData.carousel1click); - expect(mockCallback.mock.calls.length, 'callback not triggered second time').toBe(1); - }); - - test('future', () => { - const mockCallback = jest.fn(); - - adobeDataLayer.push(testData.carousel1click); - adobeDataLayer.addEventListener('carousel clicked', mockCallback, { scope: 'future' }); - expect(mockCallback.mock.calls.length, 'callback not triggered first time').toBe(0); - - adobeDataLayer.push(testData.carousel1click); - expect(mockCallback.mock.calls.length, 'callback triggered only second time').toBe(1); - }); - - test('all', () => { - const mockCallback = jest.fn(); - - adobeDataLayer.push(testData.carousel1click); - adobeDataLayer.addEventListener('carousel clicked', mockCallback, { scope: 'all' }); - expect(mockCallback.mock.calls.length, 'callback triggered first time').toBe(1); - - adobeDataLayer.push(testData.carousel1click); - expect(mockCallback.mock.calls.length, 'callback triggered second time').toBe(2); - }); - - test('undefined (defaults to all)', () => { - const mockCallback = jest.fn(); - - adobeDataLayer.push(testData.carousel1click); - adobeDataLayer.addEventListener('carousel clicked', mockCallback); - expect(mockCallback.mock.calls.length, 'callback triggered first time').toBe(1); - - adobeDataLayer.push(testData.carousel1click); - expect(mockCallback.mock.calls.length, 'callback triggered second time').toBe(2); - }); - - test('invalid', () => { - const mockCallback = jest.fn(); - adobeDataLayer.addEventListener('carousel clicked', mockCallback, { scope: 'invalid' }); - adobeDataLayer.push(testData.carousel1click); - expect(mockCallback.mock.calls.length).toBe(0); - }); - }); - - describe('duplications', () => { - test('register a handler that has already been registered', () => { - const mockCallback = jest.fn(); - - adobeDataLayer.push(testData.carousel1click); - adobeDataLayer.addEventListener('carousel clicked', mockCallback); - adobeDataLayer.addEventListener('carousel clicked', mockCallback); - - // only one listener is registered - - expect(mockCallback.mock.calls.length, 'callback triggered just once').toBe(1); - adobeDataLayer.push(testData.carousel1click); - expect(mockCallback.mock.calls.length, 'callback triggered just once (second time)').toBe(2); - }); - - test('register a handler (with a static function) that has already been registered', () => { - const mockCallback = jest.fn(); - adobeDataLayer.addEventListener('carousel clicked', function() { - mockCallback(); - }); - adobeDataLayer.addEventListener('carousel clicked', function() { - mockCallback(); - }); - - // both listeners are registered - - adobeDataLayer.push(testData.carousel1click); - expect(mockCallback.mock.calls.length, 'callback triggered twice').toBe(2); - }); - }); - - describe('with path', () => { - const mockCallback = jest.fn(); - const changeEventArguments = ['adobeDataLayer:change', mockCallback, { path: 'component.image' }]; - - beforeEach(() => { - mockCallback.mockClear(); - }); - - test('adobeDataLayer:change triggers on component.image', () => { - adobeDataLayer.addEventListener.apply(adobeDataLayer, changeEventArguments); - adobeDataLayer.push(testData.carousel1); - expect(mockCallback.mock.calls.length).toBe(0); - - adobeDataLayer.push(testData.image1); - expect(mockCallback.mock.calls.length).toBe(1); - }); - - test('adobeDataLayer:change triggers on component.image with data', () => { - adobeDataLayer.addEventListener.apply(adobeDataLayer, changeEventArguments); - adobeDataLayer.push(testData.carousel1change); - expect(mockCallback.mock.calls.length).toBe(0); - - adobeDataLayer.push(testData.image1change); - expect(mockCallback.mock.calls.length).toBe(1); - }); - - test('event triggers when the ancestor is removed with null', () => { - adobeDataLayer.addEventListener.apply(adobeDataLayer, changeEventArguments); - adobeDataLayer.push(testData.componentNull); - expect(mockCallback.mock.calls.length).toBe(1); - }); - - test('event triggers when the ancestor is removed with undefined', () => { - adobeDataLayer.addEventListener.apply(adobeDataLayer, changeEventArguments); - adobeDataLayer.push(testData.componentUndefined); - expect(mockCallback.mock.calls.length).toBe(1); - }); - - test('event does not trigger when the ancestor does not exist', () => { - const changeEventArguments1 = ['adobeDataLayer:change', mockCallback, { path: 'component1.image' }]; - adobeDataLayer.addEventListener.apply(adobeDataLayer, changeEventArguments1); - adobeDataLayer.push(testData.componentUndefined); - expect(mockCallback.mock.calls.length).toBe(0); - }); - - test('viewed event triggers on component.image', () => { - adobeDataLayer.addEventListener('viewed', mockCallback, { path: 'component.image' }); - adobeDataLayer.push(testData.carousel1viewed); - expect(mockCallback.mock.calls.length).toBe(0); - - adobeDataLayer.push(testData.image1viewed); - expect(mockCallback.mock.calls.length).toBe(1); - }); - - test('viewed event does not trigger on a non existing object', () => { - adobeDataLayer.addEventListener('viewed', mockCallback, { path: 'component.image.undefined' }); - adobeDataLayer.push(testData.image1viewed); - expect(mockCallback.mock.calls.length).toBe(0); - }); - - test('custom event triggers on all components', () => { - adobeDataLayer.push(testData.carousel1change); - adobeDataLayer.push(testData.image1change); - adobeDataLayer.addEventListener('adobeDataLayer:change', mockCallback, { path: 'component' }); - adobeDataLayer.push(testData.image1change); - expect(mockCallback.mock.calls.length).toBe(3); - }); - - test('old / new value', () => { - const compareOldNewValueFunction = function(event, oldValue, newValue) { - if (oldValue === 'old') mockCallback(); - if (newValue === 'new') mockCallback(); - }; - - adobeDataLayer.push(testData.carousel1oldId); - adobeDataLayer.addEventListener('adobeDataLayer:change', compareOldNewValueFunction, { - path: 'component.carousel.carousel1.id' - }); - adobeDataLayer.push(testData.carousel1newId); - expect(mockCallback.mock.calls.length).toBe(2); - }); - - test('old / new state', () => { - const compareOldNewStateFunction = function(event, oldState, newState) { - if (isEqual(oldState, testData.carousel1oldId)) mockCallback(); - if (isEqual(newState, testData.carousel1newId)) mockCallback(); - }; - - adobeDataLayer.push(merge({ event: 'adobeDataLayer:change' }, testData.carousel1oldId)); - adobeDataLayer.addEventListener('adobeDataLayer:change', compareOldNewStateFunction); - adobeDataLayer.push(merge({ event: 'adobeDataLayer:change' }, testData.carousel1newId)); - expect(mockCallback.mock.calls.length).toBe(2); - }); - - test('calling getState() within a handler should return the state after the event', () => { - const compareGetStateWithNewStateFunction = function(event, oldState, newState) { - if (isEqual(this.getState(), newState)) mockCallback(); - }; - - adobeDataLayer.push(merge({ event: 'adobeDataLayer:change' }, testData.carousel1oldId)); - adobeDataLayer.addEventListener('adobeDataLayer:change', compareGetStateWithNewStateFunction); - adobeDataLayer.push(merge({ event: 'adobeDataLayer:change' }, testData.carousel1oldId)); - expect(mockCallback.mock.calls.length).toBe(1); - }); - - test('undefined old / new state for past events', () => { - // this behaviour is explained at: https://github.com/adobe/adobe-client-data-layer/issues/33 - const isOldNewStateUndefinedFunction = function(event, oldState, newState) { - if (isEqual(oldState, undefined) && isEqual(newState, undefined)) mockCallback(); - }; - - adobeDataLayer.push(testData.carousel1change); - adobeDataLayer.addEventListener('adobeDataLayer:change', isOldNewStateUndefinedFunction, { scope: 'past' }); - expect(mockCallback.mock.calls.length).toBe(1); - }); - }); - - describe('unregister', () => { - test('one handler', () => { - const mockCallback = jest.fn(); - - adobeDataLayer.push(testData.carousel1click); - adobeDataLayer.addEventListener('carousel clicked', mockCallback, { scope: 'all' }); - expect(mockCallback.mock.calls.length).toBe(1); - - adobeDataLayer.removeEventListener('carousel clicked', mockCallback); - adobeDataLayer.push(testData.carousel1click); - expect(mockCallback.mock.calls.length).toBe(1); - }); - - test('handler with an anonymous function', () => { - const mockCallback = jest.fn(); - - adobeDataLayer.addEventListener('carousel clicked', function() { mockCallback(); }); - adobeDataLayer.removeEventListener('carousel clicked', function() { mockCallback(); }); - - // an anonymous handler cannot be unregistered (similar to EventTarget.addEventListener()) - - adobeDataLayer.push(testData.carousel1click); - expect(mockCallback.mock.calls.length).toBe(1); - }); - - test('multiple handlers', () => { - const mockCallback1 = jest.fn(); - const mockCallback2 = jest.fn(); - const userLoadedEvent = { event: 'user loaded' }; - - adobeDataLayer.addEventListener('user loaded', mockCallback1); - adobeDataLayer.addEventListener('user loaded', mockCallback2); - adobeDataLayer.push(userLoadedEvent); - - expect(mockCallback1.mock.calls.length).toBe(1); - expect(mockCallback2.mock.calls.length).toBe(1); - - adobeDataLayer.removeEventListener('user loaded'); - adobeDataLayer.push(userLoadedEvent); - - expect(mockCallback1.mock.calls.length).toBe(1); - expect(mockCallback2.mock.calls.length).toBe(1); - }); - }); -}); - -// ----------------------------------------------------------------------------------------------------------------- -// Performance -// ----------------------------------------------------------------------------------------------------------------- - -describe('Performance', () => { - clearDL(); - - // high load benchmark: runs alone in 10.139s with commit: df0fef59c86635d3c29e6f698352491dcf39003c (15/oct/2019) - test.skip('high load', () => { - const mockCallback = jest.fn(); - const data = {}; - - adobeDataLayer.addEventListener('carousel clicked', mockCallback); - - for (let i = 0; i < 1000; i++) { - const pageId = '/content/mysite/en/products/crossfit' + i; - const pageKey = 'page' + i; - data[pageKey] = { - id: pageId, - siteLanguage: 'en-us', - siteCountry: 'US', - pageType: 'product detail', - pageName: 'pdp - crossfit zoom', - pageCategory: 'womens > shoes > athletic' - }; - - adobeDataLayer.push({ - event: 'carousel clicked', - data: data - }); - expect(adobeDataLayer.getState()).toStrictEqual(data); - expect(mockCallback.mock.calls.length).toBe(i + 1); - } - }); -}); - -// ----------------------------------------------------------------------------------------------------------------- -// Utils -// ----------------------------------------------------------------------------------------------------------------- - -describe('Utils', () => { - clearDL(); - - describe('ancestorRemoved', () => { - test('removed', () => { - expect(ancestorRemoved(testData.componentNull, 'component.carousel')).toBeTruthy(); - expect(ancestorRemoved(testData.componentNull, 'component.carousel.carousel1')).toBeTruthy(); - }); - test('not removed', () => { - expect(ancestorRemoved(testData.carousel1, 'component.carousel')).toBeFalsy(); - expect(ancestorRemoved(testData.carousel1, 'component.carousel.carousel1')).toBeFalsy(); - }); - }); - - describe('customMerge', () => { - test('merges object', () => { - const objectInitial = { prop1: 'foo' }; - const objectSource = { prop2: 'bar' }; - const objectFinal = { prop1: 'foo', prop2: 'bar' }; - customMerge(objectInitial, objectSource); - expect(objectInitial).toEqual(objectFinal); - }); - test('overrides with null and undefined', () => { - const objectInitial = { prop1: 'foo', prop2: 'bar' }; - const objectSource = { prop1: null, prop2: undefined }; - const objectFinal = { prop1: null, prop2: null }; - customMerge(objectInitial, objectSource); - expect(objectInitial).toEqual(objectFinal); - }); - }); - - describe('dataMatchesContraints', () => { - test('event', () => { - expect(dataMatchesContraints(testData.carousel1click, ITEM_CONSTRAINTS.event)).toBeTruthy(); - }); - test('listenerOn', () => { - const listenerOn = { - on: 'event', - handler: () => {}, - scope: 'future', - path: 'component.carousel1' - }; - expect(dataMatchesContraints(listenerOn, ITEM_CONSTRAINTS.listenerOn)).toBeTruthy(); - }); - test('listenerOn with wrong scope (optional)', () => { - const listenerOn = { - on: 'event', - handler: () => {}, - scope: 'wrong', - path: 'component.carousel1' - }; - expect(dataMatchesContraints(listenerOn, ITEM_CONSTRAINTS.listenerOn)).toBeFalsy(); - }); - test('listenerOn with wrong scope (not optional)', () => { - const constraints = { - scope: { - type: 'string', - values: ['past', 'future', 'all'] - } - }; - const listenerOn = { - on: 'event', - handler: () => {}, - scope: 'past' - }; - expect(dataMatchesContraints(listenerOn, constraints)).toBeTruthy(); - }); - test('listenerOff', () => { - const listenerOff = { - off: 'event', - handler: () => {}, - scope: 'future', - path: 'component.carousel1' - }; - expect(dataMatchesContraints(listenerOff, ITEM_CONSTRAINTS.listenerOff)).toBeTruthy(); - }); - }); - - describe('indexOfListener', () => { - test('indexOfListener', () => { - const fct1 = jest.fn(); - const fct2 = jest.fn(); - const listener1 = { - event: 'click', - handler: fct1 - }; - const listener2 = { - event: 'click', - handler: fct2 - }; - const listener3 = { - event: 'load', - handler: fct1 - }; - const listeners = { - click: [listener1, listener2] - }; - expect(indexOfListener(listeners, listener2)).toBe(1); - expect(indexOfListener(listeners, listener3)).toBe(-1); - }); - }); - - describe('listenerMatch', () => { - test('event type', () => { - const listener = { - event: 'user loaded', - handler: () => {}, - scope: 'all', - path: null - }; - const item = { - config: { event: 'user loaded' }, - type: 'event' - }; - expect(listenerMatch(listener, item)).toBeTruthy(); - }); - test('with correct path', () => { - const listener = { - event: 'viewed', - handler: () => {}, - scope: 'all', - path: 'component.image.image1' - }; - const item = { - config: testData.image1viewed, - type: 'event', - data: testData.image1 - }; - expect(listenerMatch(listener, item)).toBeTruthy(); - }); - test('with incorrect path', () => { - const listener = { - event: 'viewed', - handler: () => {}, - scope: 'all', - path: 'component.carousel' - }; - const item = { - config: testData.image1viewed, - type: 'event', - data: testData.image1 - }; - expect(listenerMatch(listener, item)).toBeFalsy(); - }); - test('wrong item type', () => { - const listener = { - event: 'user loaded', - handler: () => {}, - scope: 'all', - path: null - }; - const item = { - config: { event: 'user loaded' }, - type: 'wrong' - }; - expect(listenerMatch(listener, item)).toBeFalsy(); - }); - test('item type == data', () => { - const listener = { - event: 'user loaded', - handler: () => {} - }; - const item = { - type: 'data' - }; - expect(listenerMatch(listener, item)).toBeFalsy(); - }); - }); -}); diff --git a/src/__tests__/scenarios/data.test.js b/src/__tests__/scenarios/data.test.js new file mode 100644 index 0000000..38b47ea --- /dev/null +++ b/src/__tests__/scenarios/data.test.js @@ -0,0 +1,122 @@ +/* +Copyright 2020 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +const merge = require('lodash/merge'); + +const testData = require('../testData'); +const DataLayerManager = require('../../dataLayerManager'); +const DataLayer = { Manager: DataLayerManager }; +let adobeDataLayer; + +const clearDL = function() { + beforeEach(() => { + adobeDataLayer = []; + DataLayer.Manager({ dataLayer: adobeDataLayer }); + }); +}; + +describe('Data', () => { + clearDL(); + + test('push page', () => { + adobeDataLayer.push(testData.page1); + expect(adobeDataLayer.getState(), 'page is in data layer after push').toStrictEqual(testData.page1); + }); + + test('push data, override and remove', () => { + adobeDataLayer.push({ test: 'foo' }); + expect(adobeDataLayer.getState(), 'data pushed').toStrictEqual({ test: 'foo' }); + + adobeDataLayer.push({ test: 'bar' }); + expect(adobeDataLayer.getState(), 'data overriden').toStrictEqual({ test: 'bar' }); + + adobeDataLayer.push({ test: null }); + expect(adobeDataLayer.getState(), 'data removed').toStrictEqual({}); + }); + + test('push components and override', () => { + const twoCarousels = merge({}, testData.carousel1, testData.carousel2); + const carousel1empty = merge({}, testData.carousel1empty, testData.carousel2); + const carousel2empty = merge({}, testData.carousel1, testData.carousel2empty); + const twoCarouselsEmpty = merge({}, testData.carousel1empty, testData.carousel2empty); + + adobeDataLayer.push(testData.carousel1); + adobeDataLayer.push(testData.carousel1withNullAndUndefinedArrayItems); + expect(adobeDataLayer.getState(), 'carousel 1 with removed items').toStrictEqual(testData.carousel1withRemovedArrayItems); + + adobeDataLayer.push(twoCarousels); + expect(adobeDataLayer.getState(), 'carousel 1 with data, carousel 2 with data').toStrictEqual(twoCarousels); + + adobeDataLayer.push(testData.carousel1withUndefined); + expect(adobeDataLayer.getState(), 'carousel 1 empty, carousel 2 with data').toStrictEqual(carousel1empty); + + adobeDataLayer.push(testData.carousel2withUndefined); + expect(adobeDataLayer.getState(), 'carousel 1 empty, carousel 2 empty').toStrictEqual(twoCarouselsEmpty); + + adobeDataLayer.push(testData.carousel1); + expect(adobeDataLayer.getState(), 'carousel 1 with data, carousel 2 empty').toStrictEqual(carousel2empty); + + adobeDataLayer.push(testData.carousel1withNull); + expect(adobeDataLayer.getState(), 'carousel 1 empty, carousel 2 empty').toStrictEqual(twoCarouselsEmpty); + + adobeDataLayer.push(testData.carousel1); + expect(adobeDataLayer.getState(), 'carousel 1 with data, carousel 2 empty').toStrictEqual(carousel2empty); + }); + + test('push eventInfo without event', () => { + adobeDataLayer.push({ eventInfo: 'test' }); + + expect(adobeDataLayer.getState(), 'no event info added').toStrictEqual({}); + }); + + test('push invalid data type - string', () => { + adobeDataLayer.push('test'); + + expect(adobeDataLayer.getState(), 'string is invalid data type and is not part of the state').toStrictEqual({}); + }); + + test('push invalid data type - array of strings', () => { + adobeDataLayer.push(['test1', 'test2']); + + expect(adobeDataLayer.getState(), 'string is invalid data type and is not part of the state').toStrictEqual({}); + }); + + test('push initial data provided before data layer initialization', () => { + adobeDataLayer = [testData.carousel1, testData.carousel2]; + DataLayer.Manager({ dataLayer: adobeDataLayer }); + + expect(adobeDataLayer.getState(), 'all items are pushed to data layer state').toStrictEqual(merge({}, testData.carousel1, testData.carousel2)); + }); + + test('invalid initial data triggers error', () => { + // Catches console.error function which should be triggered by data layer during this test + var consoleSpy = jest.spyOn(console, 'error').mockImplementation(); + adobeDataLayer = ['test']; + DataLayer.Manager({ dataLayer: adobeDataLayer }); + + expect(adobeDataLayer.getState(), 'initialization').toStrictEqual({}); + expect(consoleSpy).toHaveBeenCalled(); + // Restores console.error to default behaviour + consoleSpy.mockRestore(); + }); + + test('push on / off listeners is not allowed', () => { + adobeDataLayer.push({ + on: 'click', + handler: jest.fn() + }); + adobeDataLayer.push({ + off: 'click', + handler: jest.fn() + }); + expect(adobeDataLayer.getState()).toStrictEqual({}); + }); +}); diff --git a/src/__tests__/scenarios/events.test.js b/src/__tests__/scenarios/events.test.js new file mode 100644 index 0000000..f7363b3 --- /dev/null +++ b/src/__tests__/scenarios/events.test.js @@ -0,0 +1,55 @@ +/* +Copyright 2020 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const testData = require('../testData'); +const DataLayerManager = require('../../dataLayerManager'); +const DataLayer = { Manager: DataLayerManager }; +let adobeDataLayer; + +const clearDL = function() { + beforeEach(() => { + adobeDataLayer = []; + DataLayer.Manager({ dataLayer: adobeDataLayer }); + }); +}; + +describe('Events', () => { + clearDL(); + + test('push simple event', () => { + adobeDataLayer.push(testData.carousel1click); + expect(adobeDataLayer.getState()).toStrictEqual(testData.carousel1); + }); + + test('check number of arguments in callback', () => { + let calls = 0; + + adobeDataLayer.addEventListener('test', function() { calls = arguments.length; }); + + adobeDataLayer.push({ event: 'test' }); + expect(calls, 'just one argument if no data is added').toStrictEqual(1); + + adobeDataLayer.push({ event: 'test', eventInfo: 'test' }); + expect(calls, 'just one argument if no data is added').toStrictEqual(1); + + adobeDataLayer.push({ event: 'test', somekey: 'somedata' }); + expect(calls, 'three arguments if data is added').toStrictEqual(3); + }); + + test('check if eventInfo is passed to callback', () => { + adobeDataLayer.addEventListener('test', function() { + expect(arguments[0].eventInfo).toStrictEqual('test'); + }); + + adobeDataLayer.push({ event: 'test', eventInfo: 'test' }); + }); +}); diff --git a/src/__tests__/scenarios/functions.test.js b/src/__tests__/scenarios/functions.test.js new file mode 100644 index 0000000..f6b8e47 --- /dev/null +++ b/src/__tests__/scenarios/functions.test.js @@ -0,0 +1,99 @@ +/* +Copyright 2020 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const testData = require('../testData'); +const DataLayerManager = require('../../dataLayerManager'); +const DataLayer = { Manager: DataLayerManager }; +let adobeDataLayer; + +const clearDL = function() { + beforeEach(() => { + adobeDataLayer = []; + DataLayer.Manager({ dataLayer: adobeDataLayer }); + }); +}; + +const createEventListener = function(dl, eventName, callback, eventData) { + dl.addEventListener(eventName, function(eventData) { + expect(eventData, 'data layer object as an argument of callback').toEqual(eventData); + callback(); + }); +}; + +describe('Functions', () => { + describe('simple', () => { + clearDL(); + + test('push simple function', () => { + const mockCallback = jest.fn(); + adobeDataLayer.push(mockCallback); + expect(mockCallback.mock.calls.length).toBe(1); + }); + + test('function adds event listener for adobeDataLayer:change', () => { + const mockCallback = jest.fn(); + const addEventListener = function(adl) { + adl.addEventListener('adobeDataLayer:change', mockCallback); + }; + + adobeDataLayer.push(testData.carousel1); + adobeDataLayer.push(addEventListener); + adobeDataLayer.push(testData.carousel2); + + expect(mockCallback.mock.calls.length, 'event triggered twice').toBe(2); + }); + + test('function updates component in data layer state', () => { + const updateCarousel = function(adl) { + adl.push(testData.carousel1new); + }; + + adobeDataLayer.push(testData.carousel1); + expect(adobeDataLayer.getState(), 'carousel set to carousel1').toEqual(testData.carousel1); + + adobeDataLayer.push(updateCarousel); + expect(adobeDataLayer.getState(), 'carousel set to carousel1new').toEqual(testData.carousel1new); + }); + }); + + test('nested anonymous functions', () => { + const mockCallback1 = jest.fn(); + const mockCallback2 = jest.fn(); + const mockCallback3 = jest.fn(); + + adobeDataLayer.addEventListener('adobeDataLayer:event', function(eventData) { + mockCallback1(); + }); + + adobeDataLayer.push(testData.carousel1click); + + adobeDataLayer.push(function(dl) { + createEventListener(dl, 'carousel clicked', mockCallback2, testData.carousel1click); + + dl.push(function(dl2) { + createEventListener(dl2, 'viewed', mockCallback3, testData.carousel1viewed); + + dl2.push(function(dl3) { + dl3.push(testData.carousel1click); + }); + }); + + adobeDataLayer.push(testData.carousel1viewed); + }); + + DataLayer.Manager({ dataLayer: adobeDataLayer }); + + expect(mockCallback1.mock.calls.length, 'callback triggered 3 times').toBe(3); + expect(mockCallback2.mock.calls.length, 'callback triggered 2 times').toBe(2); + expect(mockCallback3.mock.calls.length, 'callback triggered 1 times').toBe(1); + }); +}); diff --git a/src/__tests__/scenarios/initialization.test.js b/src/__tests__/scenarios/initialization.test.js new file mode 100644 index 0000000..e5d601a --- /dev/null +++ b/src/__tests__/scenarios/initialization.test.js @@ -0,0 +1,155 @@ +/* +Copyright 2020 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const testData = require('../testData'); +const DataLayerManager = require('../../dataLayerManager'); +const DataLayer = { Manager: DataLayerManager }; +let adobeDataLayer; + +const createEventListener = function(dl, callback, options) { + dl.addEventListener('adobeDataLayer:event', function(eventData) { + expect(eventData, 'data layer object as an argument of callback').toEqual(testData.carousel1click); + callback(); + }, options); +}; + +describe('Initialization', () => { + describe('arguments', () => { + test('empty array', () => { + adobeDataLayer = []; + DataLayer.Manager({ dataLayer: adobeDataLayer }); + + expect(adobeDataLayer.getState()).toEqual({}); + }); + + test('array with data', () => { + adobeDataLayer = [testData.carousel1]; + DataLayer.Manager({ dataLayer: adobeDataLayer }); + + expect(adobeDataLayer.getState()).toEqual(testData.carousel1); + }); + + test('wrong type', () => { + adobeDataLayer = DataLayer.Manager({ dataLayer: {} }); + + expect(adobeDataLayer.getState()).toEqual({}); + }); + + test('null', () => { + adobeDataLayer = DataLayer.Manager(null); + + expect(adobeDataLayer.getState()).toEqual({}); + }); + }); + + describe('events', () => { + beforeEach(() => { + adobeDataLayer = []; + }); + + test('scope past with early initialization', () => { + const mockCallback = jest.fn(); + DataLayer.Manager({ dataLayer: adobeDataLayer }); + adobeDataLayer.push(function(dl) { createEventListener(dl, mockCallback, { scope: 'past' }); }); + adobeDataLayer.push(testData.carousel1click); + + expect(mockCallback.mock.calls.length, 'callback not triggered').toBe(0); + }); + + test('scope past with late initialization', () => { + const mockCallback = jest.fn(); + adobeDataLayer.push(function(dl) { createEventListener(dl, mockCallback, { scope: 'past' }); }); + adobeDataLayer.push(testData.carousel1click); + DataLayer.Manager({ dataLayer: adobeDataLayer }); + + expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(0); + }); + + test('scope future with early initialization', () => { + const mockCallback = jest.fn(); + DataLayer.Manager({ dataLayer: adobeDataLayer }); + adobeDataLayer.push(function(dl) { createEventListener(dl, mockCallback, { scope: 'future' }); }); + adobeDataLayer.push(testData.carousel1click); + + expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); + }); + + test('scope future with late initialization', () => { + const mockCallback = jest.fn(); + adobeDataLayer.push(function(dl) { createEventListener(dl, mockCallback, { scope: 'future' }); }); + adobeDataLayer.push(testData.carousel1click); + DataLayer.Manager({ dataLayer: adobeDataLayer }); + + expect(mockCallback.mock.calls.length, 'callback not triggered').toBe(1); + }); + }); + + describe('order', () => { + beforeEach(() => { + adobeDataLayer = []; + }); + + test('listener > event > initialization', () => { + const mockCallback = jest.fn(); + adobeDataLayer.push(function(dl) { createEventListener(dl, mockCallback); }); + adobeDataLayer.push(testData.carousel1click); + DataLayer.Manager({ dataLayer: adobeDataLayer }); + + expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); + }); + + test('event > listener > initialization', () => { + const mockCallback = jest.fn(); + adobeDataLayer.push(testData.carousel1click); + adobeDataLayer.push(function(dl) { createEventListener(dl, mockCallback); }); + DataLayer.Manager({ dataLayer: adobeDataLayer }); + + expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); + }); + + test('listener > initialization > event', () => { + const mockCallback = jest.fn(); + adobeDataLayer.push(function(dl) { createEventListener(dl, mockCallback); }); + DataLayer.Manager({ dataLayer: adobeDataLayer }); + adobeDataLayer.push(testData.carousel1click); + + expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); + }); + + test('event > initialization > listener', () => { + const mockCallback = jest.fn(); + adobeDataLayer.push(testData.carousel1click); + DataLayer.Manager({ dataLayer: adobeDataLayer }); + adobeDataLayer.push(function(dl) { createEventListener(dl, mockCallback); }); + + expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); + }); + + test('initialization > event > listener', () => { + const mockCallback = jest.fn(); + DataLayer.Manager({ dataLayer: adobeDataLayer }); + adobeDataLayer.push(testData.carousel1click); + adobeDataLayer.push(function(dl) { createEventListener(dl, mockCallback); }); + + expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); + }); + + test('initialization > listener > event', () => { + const mockCallback = jest.fn(); + DataLayer.Manager({ dataLayer: adobeDataLayer }); + adobeDataLayer.push(function(dl) { createEventListener(dl, mockCallback); }); + adobeDataLayer.push(testData.carousel1click); + + expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); + }); + }); +}); diff --git a/src/__tests__/scenarios/listeners.test.js b/src/__tests__/scenarios/listeners.test.js new file mode 100644 index 0000000..f0240f3 --- /dev/null +++ b/src/__tests__/scenarios/listeners.test.js @@ -0,0 +1,340 @@ +/* +Copyright 2020 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +const isEqual = require('lodash/isEqual'); +const merge = require('lodash/merge'); + +const testData = require('../testData'); +const DataLayerManager = require('../../dataLayerManager'); +const DataLayer = { Manager: DataLayerManager }; +let adobeDataLayer; + +const clearDL = function() { + beforeEach(() => { + adobeDataLayer = []; + DataLayer.Manager({ dataLayer: adobeDataLayer }); + }); +}; + +describe('Event listeners', () => { + clearDL(); + + describe('types', () => { + test('adobeDataLayer:change triggered by component push', () => { + const mockCallback = jest.fn(); + + // edge case: unregister when no listener had been registered + adobeDataLayer.removeEventListener('adobeDataLayer:change'); + + adobeDataLayer.addEventListener('adobeDataLayer:change', mockCallback); + adobeDataLayer.push(testData.carousel1); + expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); + + adobeDataLayer.removeEventListener('adobeDataLayer:change'); + adobeDataLayer.push(testData.carousel2); + expect(mockCallback.mock.calls.length, 'callback not triggered second time').toBe(1); + }); + + test('adobeDataLayer:change triggered by event push', () => { + const mockCallback = jest.fn(); + + adobeDataLayer.addEventListener('adobeDataLayer:change', mockCallback); + adobeDataLayer.push(testData.carousel1click); + expect(mockCallback.mock.calls.length).toBe(1); + + adobeDataLayer.removeEventListener('adobeDataLayer:change'); + adobeDataLayer.push(testData.carousel1change); + expect(mockCallback.mock.calls.length).toBe(1); + }); + + test('adobeDataLayer:event triggered by event push', () => { + const mockCallback = jest.fn(); + + adobeDataLayer.addEventListener('adobeDataLayer:event', mockCallback); + adobeDataLayer.push(testData.carousel1click); + expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); + + adobeDataLayer.removeEventListener('adobeDataLayer:event'); + adobeDataLayer.push(testData.carousel1click); + expect(mockCallback.mock.calls.length, 'callback not triggered second time').toBe(1); + }); + + test('adobeDataLayer:change not triggered by event push', () => { + const mockCallback = jest.fn(); + + adobeDataLayer.addEventListener('adobeDataLayer:change', mockCallback); + adobeDataLayer.push({ + event: 'page loaded' + }); + expect(mockCallback.mock.calls.length, 'callback not triggered').toBe(0); + adobeDataLayer.push(testData.carousel1click); + expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); + }); + + test('custom event triggered by event push', () => { + const mockCallback = jest.fn(); + + adobeDataLayer.addEventListener('carousel clicked', mockCallback); + adobeDataLayer.push(testData.carousel1click); + expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); + + adobeDataLayer.removeEventListener('carousel clicked'); + adobeDataLayer.push(testData.carousel1click); + expect(mockCallback.mock.calls.length, 'callback not triggered second time').toBe(1); + }); + }); + + describe('scopes', () => { + test('past', () => { + const mockCallback = jest.fn(); + + adobeDataLayer.push(testData.carousel1click); + adobeDataLayer.addEventListener('carousel clicked', mockCallback, { scope: 'past' }); + expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); + + adobeDataLayer.push(testData.carousel1click); + expect(mockCallback.mock.calls.length, 'callback not triggered second time').toBe(1); + }); + + test('future', () => { + const mockCallback = jest.fn(); + + adobeDataLayer.push(testData.carousel1click); + adobeDataLayer.addEventListener('carousel clicked', mockCallback, { scope: 'future' }); + expect(mockCallback.mock.calls.length, 'callback not triggered first time').toBe(0); + + adobeDataLayer.push(testData.carousel1click); + expect(mockCallback.mock.calls.length, 'callback triggered only second time').toBe(1); + }); + + test('all', () => { + const mockCallback = jest.fn(); + + adobeDataLayer.push(testData.carousel1click); + adobeDataLayer.addEventListener('carousel clicked', mockCallback, { scope: 'all' }); + expect(mockCallback.mock.calls.length, 'callback triggered first time').toBe(1); + + adobeDataLayer.push(testData.carousel1click); + expect(mockCallback.mock.calls.length, 'callback triggered second time').toBe(2); + }); + + test('undefined (defaults to all)', () => { + const mockCallback = jest.fn(); + + adobeDataLayer.push(testData.carousel1click); + adobeDataLayer.addEventListener('carousel clicked', mockCallback); + expect(mockCallback.mock.calls.length, 'callback triggered first time').toBe(1); + + adobeDataLayer.push(testData.carousel1click); + expect(mockCallback.mock.calls.length, 'callback triggered second time').toBe(2); + }); + + test('invalid', () => { + const mockCallback = jest.fn(); + adobeDataLayer.addEventListener('carousel clicked', mockCallback, { scope: 'invalid' }); + adobeDataLayer.push(testData.carousel1click); + expect(mockCallback.mock.calls.length).toBe(0); + }); + }); + + describe('duplications', () => { + test('register a handler that has already been registered', () => { + const mockCallback = jest.fn(); + + adobeDataLayer.push(testData.carousel1click); + adobeDataLayer.addEventListener('carousel clicked', mockCallback); + adobeDataLayer.addEventListener('carousel clicked', mockCallback); + + // only one listener is registered + + expect(mockCallback.mock.calls.length, 'callback triggered just once').toBe(1); + adobeDataLayer.push(testData.carousel1click); + expect(mockCallback.mock.calls.length, 'callback triggered just once (second time)').toBe(2); + }); + + test('register a handler (with a static function) that has already been registered', () => { + const mockCallback = jest.fn(); + adobeDataLayer.addEventListener('carousel clicked', function() { + mockCallback(); + }); + adobeDataLayer.addEventListener('carousel clicked', function() { + mockCallback(); + }); + + // both listeners are registered + + adobeDataLayer.push(testData.carousel1click); + expect(mockCallback.mock.calls.length, 'callback triggered twice').toBe(2); + }); + }); + + describe('with path', () => { + const mockCallback = jest.fn(); + const changeEventArguments = ['adobeDataLayer:change', mockCallback, { path: 'component.image' }]; + + beforeEach(() => { + mockCallback.mockClear(); + }); + + test('adobeDataLayer:change triggers on component.image', () => { + adobeDataLayer.addEventListener.apply(adobeDataLayer, changeEventArguments); + adobeDataLayer.push(testData.carousel1); + expect(mockCallback.mock.calls.length).toBe(0); + + adobeDataLayer.push(testData.image1); + expect(mockCallback.mock.calls.length).toBe(1); + }); + + test('adobeDataLayer:change triggers on component.image with data', () => { + adobeDataLayer.addEventListener.apply(adobeDataLayer, changeEventArguments); + adobeDataLayer.push(testData.carousel1change); + expect(mockCallback.mock.calls.length).toBe(0); + + adobeDataLayer.push(testData.image1change); + expect(mockCallback.mock.calls.length).toBe(1); + }); + + test('event triggers when the ancestor is removed with null', () => { + adobeDataLayer.addEventListener.apply(adobeDataLayer, changeEventArguments); + adobeDataLayer.push(testData.componentNull); + expect(mockCallback.mock.calls.length).toBe(1); + }); + + test('event triggers when the ancestor is removed with undefined', () => { + adobeDataLayer.addEventListener.apply(adobeDataLayer, changeEventArguments); + adobeDataLayer.push(testData.componentUndefined); + expect(mockCallback.mock.calls.length).toBe(1); + }); + + test('event does not trigger when the ancestor does not exist', () => { + const changeEventArguments1 = ['adobeDataLayer:change', mockCallback, { path: 'component1.image' }]; + adobeDataLayer.addEventListener.apply(adobeDataLayer, changeEventArguments1); + adobeDataLayer.push(testData.componentUndefined); + expect(mockCallback.mock.calls.length).toBe(0); + }); + + test('viewed event triggers on component.image', () => { + adobeDataLayer.addEventListener('viewed', mockCallback, { path: 'component.image' }); + adobeDataLayer.push(testData.carousel1viewed); + expect(mockCallback.mock.calls.length).toBe(0); + + adobeDataLayer.push(testData.image1viewed); + expect(mockCallback.mock.calls.length).toBe(1); + }); + + test('viewed event does not trigger on a non existing object', () => { + adobeDataLayer.addEventListener('viewed', mockCallback, { path: 'component.image.undefined' }); + adobeDataLayer.push(testData.image1viewed); + expect(mockCallback.mock.calls.length).toBe(0); + }); + + test('custom event triggers on all components', () => { + adobeDataLayer.push(testData.carousel1change); + adobeDataLayer.push(testData.image1change); + adobeDataLayer.addEventListener('adobeDataLayer:change', mockCallback, { path: 'component' }); + adobeDataLayer.push(testData.image1change); + expect(mockCallback.mock.calls.length).toBe(3); + }); + + test('old / new value', () => { + const compareOldNewValueFunction = function(event, oldValue, newValue) { + if (oldValue === 'old') mockCallback(); + if (newValue === 'new') mockCallback(); + }; + + adobeDataLayer.push(testData.carousel1oldId); + adobeDataLayer.addEventListener('adobeDataLayer:change', compareOldNewValueFunction, { + path: 'component.carousel.carousel1.id' + }); + adobeDataLayer.push(testData.carousel1newId); + expect(mockCallback.mock.calls.length).toBe(2); + }); + + test('old / new state', () => { + const compareOldNewStateFunction = function(event, oldState, newState) { + if (isEqual(oldState, testData.carousel1oldId)) mockCallback(); + if (isEqual(newState, testData.carousel1newId)) mockCallback(); + }; + + adobeDataLayer.push(merge({ event: 'adobeDataLayer:change' }, testData.carousel1oldId)); + adobeDataLayer.addEventListener('adobeDataLayer:change', compareOldNewStateFunction); + adobeDataLayer.push(merge({ event: 'adobeDataLayer:change' }, testData.carousel1newId)); + expect(mockCallback.mock.calls.length).toBe(2); + }); + + test('calling getState() within a handler should return the state after the event', () => { + const compareGetStateWithNewStateFunction = function(event, oldState, newState) { + if (isEqual(this.getState(), newState)) mockCallback(); + }; + + adobeDataLayer.push(merge({ event: 'adobeDataLayer:change' }, testData.carousel1oldId)); + adobeDataLayer.addEventListener('adobeDataLayer:change', compareGetStateWithNewStateFunction); + adobeDataLayer.push(merge({ event: 'adobeDataLayer:change' }, testData.carousel1oldId)); + expect(mockCallback.mock.calls.length).toBe(1); + }); + + test('undefined old / new state for past events', () => { + // this behaviour is explained at: https://github.com/adobe/adobe-client-data-layer/issues/33 + const isOldNewStateUndefinedFunction = function(event, oldState, newState) { + if (isEqual(oldState, undefined) && isEqual(newState, undefined)) mockCallback(); + }; + + adobeDataLayer.push(testData.carousel1change); + adobeDataLayer.addEventListener('adobeDataLayer:change', isOldNewStateUndefinedFunction, { scope: 'past' }); + expect(mockCallback.mock.calls.length).toBe(1); + }); + }); + + describe('unregister', () => { + test('one handler', () => { + const mockCallback = jest.fn(); + + adobeDataLayer.push(testData.carousel1click); + adobeDataLayer.addEventListener('carousel clicked', mockCallback, { scope: 'all' }); + expect(mockCallback.mock.calls.length).toBe(1); + + adobeDataLayer.removeEventListener('carousel clicked', mockCallback); + adobeDataLayer.push(testData.carousel1click); + expect(mockCallback.mock.calls.length).toBe(1); + }); + + test('handler with an anonymous function', () => { + const mockCallback = jest.fn(); + + adobeDataLayer.addEventListener('carousel clicked', function() { mockCallback(); }); + adobeDataLayer.removeEventListener('carousel clicked', function() { mockCallback(); }); + + // an anonymous handler cannot be unregistered (similar to EventTarget.addEventListener()) + + adobeDataLayer.push(testData.carousel1click); + expect(mockCallback.mock.calls.length).toBe(1); + }); + + test('multiple handlers', () => { + const mockCallback1 = jest.fn(); + const mockCallback2 = jest.fn(); + const userLoadedEvent = { event: 'user loaded' }; + + adobeDataLayer.addEventListener('user loaded', mockCallback1); + adobeDataLayer.addEventListener('user loaded', mockCallback2); + adobeDataLayer.push(userLoadedEvent); + + expect(mockCallback1.mock.calls.length).toBe(1); + expect(mockCallback2.mock.calls.length).toBe(1); + + adobeDataLayer.removeEventListener('user loaded'); + adobeDataLayer.push(userLoadedEvent); + + expect(mockCallback1.mock.calls.length).toBe(1); + expect(mockCallback2.mock.calls.length).toBe(1); + }); + }); +}); diff --git a/src/__tests__/scenarios/performance.test.js b/src/__tests__/scenarios/performance.test.js new file mode 100644 index 0000000..a550384 --- /dev/null +++ b/src/__tests__/scenarios/performance.test.js @@ -0,0 +1,58 @@ +/* +Copyright 2020 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const DataLayerManager = require('../../dataLayerManager'); +const DataLayer = { Manager: DataLayerManager }; +let adobeDataLayer; + +const clearDL = function() { + beforeEach(() => { + adobeDataLayer = []; + DataLayer.Manager({ dataLayer: adobeDataLayer }); + }); +}; + +describe('Performance', () => { + clearDL(); + + // high load benchmark: runs alone in 16.078s (28/mon/2020) + test('high load', () => { + const mockCallback = jest.fn(); + const data = {}; + const start = new Date(); + + adobeDataLayer.addEventListener('carousel clicked', mockCallback); + + for (let i = 0; i < 1000; i++) { + const pageId = '/content/mysite/en/products/crossfit' + i; + const pageKey = 'page' + i; + const page = { + id: pageId, + siteLanguage: 'en-us', + siteCountry: 'US', + pageType: 'product detail', + pageName: 'pdp - crossfit zoom', + pageCategory: 'women > shoes > athletic' + }; + const pushArg = { + event: 'carousel clicked' + }; + data[pageKey] = page; + pushArg[pageKey] = page; + adobeDataLayer.push(pushArg); + expect(adobeDataLayer.getState()).toStrictEqual(data); + expect(mockCallback.mock.calls.length).toBe(i + 1); + } + + expect(new Date() - start, 'to be smaller ms time than').toBeLessThan(60000); + }); +}); diff --git a/src/__tests__/scenarios/state.test.js b/src/__tests__/scenarios/state.test.js new file mode 100644 index 0000000..b757331 --- /dev/null +++ b/src/__tests__/scenarios/state.test.js @@ -0,0 +1,45 @@ +/* +Copyright 2020 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +const isEmpty = require('lodash/isEmpty'); + +const DataLayerManager = require('../../dataLayerManager'); +const DataLayer = { Manager: DataLayerManager }; +let adobeDataLayer; + +const clearDL = function() { + beforeEach(() => { + adobeDataLayer = []; + DataLayer.Manager({ dataLayer: adobeDataLayer }); + }); +}; + +describe('State', () => { + clearDL(); + + test('getState()', () => { + const carousel1 = { + id: '/content/mysite/en/home/jcr:content/root/carousel1', + items: {} + }; + const data = { + component: { + carousel: { + carousel1: carousel1 + } + } + }; + adobeDataLayer.push(data); + expect(adobeDataLayer.getState()).toEqual(data); + expect(adobeDataLayer.getState('component.carousel.carousel1')).toEqual(carousel1); + expect(isEmpty(adobeDataLayer.getState('undefined-path'))); + }); +}); diff --git a/src/__tests__/scenarios/utils.test.js b/src/__tests__/scenarios/utils.test.js new file mode 100644 index 0000000..0d81d98 --- /dev/null +++ b/src/__tests__/scenarios/utils.test.js @@ -0,0 +1,200 @@ +/* +Copyright 2020 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const testData = require('../testData'); +const ITEM_CONSTRAINTS = require('../../itemConstraints'); +const DataLayerManager = require('../../dataLayerManager'); +const DataLayer = { Manager: DataLayerManager }; +let adobeDataLayer; + +const ancestorRemoved = require('../../utils/ancestorRemoved'); +const customMerge = require('../../utils/customMerge'); +const dataMatchesContraints = require('../../utils/dataMatchesContraints'); +const indexOfListener = require('../../utils/indexOfListener'); +const listenerMatch = require('../../utils/listenerMatch'); + +const clearDL = function() { + beforeEach(() => { + adobeDataLayer = []; + DataLayer.Manager({ dataLayer: adobeDataLayer }); + }); +}; + +describe('Utils', () => { + clearDL(); + + describe('ancestorRemoved', () => { + test('removed', () => { + expect(ancestorRemoved(testData.componentNull, 'component.carousel')).toBeTruthy(); + expect(ancestorRemoved(testData.componentNull, 'component.carousel.carousel1')).toBeTruthy(); + }); + test('not removed', () => { + expect(ancestorRemoved(testData.carousel1, 'component.carousel')).toBeFalsy(); + expect(ancestorRemoved(testData.carousel1, 'component.carousel.carousel1')).toBeFalsy(); + }); + }); + + describe('customMerge', () => { + test('merges object', () => { + const objectInitial = { prop1: 'foo' }; + const objectSource = { prop2: 'bar' }; + const objectFinal = { prop1: 'foo', prop2: 'bar' }; + customMerge(objectInitial, objectSource); + expect(objectInitial).toEqual(objectFinal); + }); + test('overrides with null and undefined', () => { + const objectInitial = { prop1: 'foo', prop2: 'bar' }; + const objectSource = { prop1: null, prop2: undefined }; + const objectFinal = { prop1: null, prop2: null }; + customMerge(objectInitial, objectSource); + expect(objectInitial).toEqual(objectFinal); + }); + }); + + describe('dataMatchesContraints', () => { + test('event', () => { + expect(dataMatchesContraints(testData.carousel1click, ITEM_CONSTRAINTS.event)).toBeTruthy(); + }); + test('listenerOn', () => { + const listenerOn = { + on: 'event', + handler: () => {}, + scope: 'future', + path: 'component.carousel1' + }; + expect(dataMatchesContraints(listenerOn, ITEM_CONSTRAINTS.listenerOn)).toBeTruthy(); + }); + test('listenerOn with wrong scope (optional)', () => { + const listenerOn = { + on: 'event', + handler: () => {}, + scope: 'wrong', + path: 'component.carousel1' + }; + expect(dataMatchesContraints(listenerOn, ITEM_CONSTRAINTS.listenerOn)).toBeFalsy(); + }); + test('listenerOn with wrong scope (not optional)', () => { + const constraints = { + scope: { + type: 'string', + values: ['past', 'future', 'all'] + } + }; + const listenerOn = { + on: 'event', + handler: () => {}, + scope: 'past' + }; + expect(dataMatchesContraints(listenerOn, constraints)).toBeTruthy(); + }); + test('listenerOff', () => { + const listenerOff = { + off: 'event', + handler: () => {}, + scope: 'future', + path: 'component.carousel1' + }; + expect(dataMatchesContraints(listenerOff, ITEM_CONSTRAINTS.listenerOff)).toBeTruthy(); + }); + }); + + describe('indexOfListener', () => { + test('indexOfListener', () => { + const fct1 = jest.fn(); + const fct2 = jest.fn(); + const listener1 = { + event: 'click', + handler: fct1 + }; + const listener2 = { + event: 'click', + handler: fct2 + }; + const listener3 = { + event: 'load', + handler: fct1 + }; + const listeners = { + click: [listener1, listener2] + }; + expect(indexOfListener(listeners, listener2)).toBe(1); + expect(indexOfListener(listeners, listener3)).toBe(-1); + }); + }); + + describe('listenerMatch', () => { + test('event type', () => { + const listener = { + event: 'user loaded', + handler: () => {}, + scope: 'all', + path: null + }; + const item = { + config: { event: 'user loaded' }, + type: 'event' + }; + expect(listenerMatch(listener, item)).toBeTruthy(); + }); + test('with correct path', () => { + const listener = { + event: 'viewed', + handler: () => {}, + scope: 'all', + path: 'component.image.image1' + }; + const item = { + config: testData.image1viewed, + type: 'event', + data: testData.image1 + }; + expect(listenerMatch(listener, item)).toBeTruthy(); + }); + test('with incorrect path', () => { + const listener = { + event: 'viewed', + handler: () => {}, + scope: 'all', + path: 'component.carousel' + }; + const item = { + config: testData.image1viewed, + type: 'event', + data: testData.image1 + }; + expect(listenerMatch(listener, item)).toBeFalsy(); + }); + test('wrong item type', () => { + const listener = { + event: 'user loaded', + handler: () => {}, + scope: 'all', + path: null + }; + const item = { + config: { event: 'user loaded' }, + type: 'wrong' + }; + expect(listenerMatch(listener, item)).toBeFalsy(); + }); + test('item type == data', () => { + const listener = { + event: 'user loaded', + handler: () => {} + }; + const item = { + type: 'data' + }; + expect(listenerMatch(listener, item)).toBeFalsy(); + }); + }); +}); diff --git a/src/__tests__/testData.js b/src/__tests__/testData.js index 96c8573..08148bd 100644 --- a/src/__tests__/testData.js +++ b/src/__tests__/testData.js @@ -48,7 +48,7 @@ const testData = { siteCountry: 'US', pageType: 'product detail', pageName: 'pdp - crossfit zoom', - pageCategory: 'womens > shoes > athletic' + pageCategory: 'women > shoes > athletic' } }, page2: { @@ -58,7 +58,7 @@ const testData = { siteCountry: 'US', pageType: 'product detail', pageName: 'pdp - running zoom', - pageCategory: 'womens > shoes > running' + pageCategory: 'women > shoes > running' } }, diff --git a/src/dataLayerManager.js b/src/dataLayerManager.js index 2c8bd63..43956ac 100644 --- a/src/dataLayerManager.js +++ b/src/dataLayerManager.js @@ -11,10 +11,10 @@ governing permissions and limitations under the License. */ const _ = require('../custom-lodash'); +const version = require('../version.json').version; const cloneDeep = _.cloneDeep; const get = _.get; -const version = require('../version.json').version; const Item = require('./item'); const Listener = require('./listener'); const ListenerManager = require('./listenerManager'); @@ -29,8 +29,9 @@ const customMerge = require('./utils/customMerge'); * @param {Object} config The Data Layer manager configuration. */ module.exports = function(config) { - const _config = config; + const _config = config || {}; let _dataLayer = []; + let _preLoadedItems = []; let _state = {}; let _previousStateCopy = {}; let _listenerManager; @@ -61,6 +62,8 @@ module.exports = function(config) { _config.dataLayer = []; } + // Remove preloaded items from the data layer array and add those to the array of preloaded items + _preLoadedItems = _config.dataLayer.splice(0, _config.dataLayer.length); _dataLayer = _config.dataLayer; _dataLayer.version = version; _state = {}; @@ -100,6 +103,7 @@ module.exports = function(config) { const item = Item(itemConfig); if (!item.valid) { + _logInvalidItemError(item); delete filteredArguments[key]; } switch (item.type) { @@ -197,19 +201,8 @@ module.exports = function(config) { * @private */ function _processItems() { - for (let i = 0; i < _dataLayer.length; i++) { - const item = Item(_dataLayer[i], i); - - _processItem(item); - - // remove event listener or invalid item from the data layer array - if (item.type === CONSTANTS.itemType.LISTENER_ON || - item.type === CONSTANTS.itemType.LISTENER_OFF || - item.type === CONSTANTS.itemType.FCTN || - !item.valid) { - _dataLayer.splice(i, 1); - i--; - } + for (let i = 0; i < _preLoadedItems.length; i++) { + _dataLayer.push(_preLoadedItems[i]); } }; @@ -221,10 +214,7 @@ module.exports = function(config) { */ function _processItem(item) { if (!item.valid) { - const message = 'The following item cannot be handled by the data layer ' + - 'because it does not have a valid format: ' + - JSON.stringify(item.config); - console.error(message); + _logInvalidItemError(item); return; } @@ -287,5 +277,18 @@ module.exports = function(config) { typeProcessors[item.type](item); }; + /** + * Logs error for invalid item pushed to the data layer. + * + * @param {Item} item The invalid item. + * @private + */ + function _logInvalidItemError(item) { + const message = 'The following item cannot be handled by the data layer ' + + 'because it does not have a valid format: ' + + JSON.stringify(item.config); + console.error(message); + }; + return DataLayerManager; }; diff --git a/src/index.js b/src/index.js index f3b0860..3b1cf0f 100644 --- a/src/index.js +++ b/src/index.js @@ -21,6 +21,8 @@ const DataLayer = { Manager: DataLayerManager }; +window.adobeDataLayer = window.adobeDataLayer || []; + DataLayer.Manager({ dataLayer: window.adobeDataLayer }); diff --git a/src/listenerManager.js b/src/listenerManager.js index e3f0716..8c3070b 100644 --- a/src/listenerManager.js +++ b/src/listenerManager.js @@ -112,9 +112,6 @@ module.exports = function(dataLayerManager) { * @private */ function _callHandler(listener, item, isPastItem) { - // Do not trigger event during initialization when event was pushed after adding listener and before DL initialization - if (typeof item.index !== 'undefined') return; - if (listenerMatch(listener, item)) { const callbackArgs = [cloneDeep(item.config)]; diff --git a/src/utils/dataMatchesContraints.js b/src/utils/dataMatchesContraints.js index d827d45..cd04b9f 100644 --- a/src/utils/dataMatchesContraints.js +++ b/src/utils/dataMatchesContraints.js @@ -14,7 +14,7 @@ module.exports = function(data, constraints) { // Go through all constraints and find one which does not match the data const foundObjection = Object.keys(constraints).find(key => { const type = constraints[key].type; - const supportedValues = constraints[key].values; + const supportedValues = key && constraints[key].values; const mandatory = !constraints[key].optional; const value = data[key]; const valueType = typeof value;