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;