├── version.json ├── .terserrc ├── .gitignore ├── jest.setup.js ├── renovate.json ├── .babelrc ├── .npmignore ├── COPYRIGHT ├── .editorconfig ├── .circleci └── config.yml ├── examples ├── index.html └── js │ ├── datalayer.mocks.2.js │ ├── datalayer.mocks.3.js │ ├── datalayer.mocks.4.js │ └── datalayer.mocks.1.js ├── .github ├── ISSUE_TEMPLATE.md ├── workflows │ └── npm-semantic-publish.yml ├── PULL_REQUEST_TEMPLATE.md └── CONTRIBUTING.md ├── src ├── utils │ ├── get.js │ ├── indexOfListener.js │ ├── mergeWith.js │ ├── ancestorRemoved.js │ ├── dataMatchesContraints.js │ ├── customMerge.js │ └── listenerMatch.js ├── itemConstraints.js ├── __tests__ │ ├── scenarios │ │ ├── state.test.js │ │ ├── performance.test.js │ │ ├── events.test.js │ │ ├── functions.test.js │ │ ├── data.test.js │ │ ├── initialization.test.js │ │ ├── utils.test.js │ │ └── listeners.test.js │ ├── testData.js │ └── demo.js ├── listener.js ├── constants.js ├── item.js ├── index.js ├── listenerManager.js └── dataLayerManager.js ├── jest.config.js ├── .eslintrc.js ├── package.json ├── README.md ├── CODE_OF_CONDUCT.md └── LICENSE /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "3.0.1" 3 | } 4 | -------------------------------------------------------------------------------- /.terserrc: -------------------------------------------------------------------------------- 1 | { 2 | "output": { 3 | "comments": false 4 | } 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules 3 | /test 4 | .idea/ 5 | /dist 6 | custom-lodash.js 7 | .parcel-cache -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | const structuredClone = require('@ungap/structured-clone').default; 2 | 3 | global.structuredClone = structuredClone; 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [ 5 | "@babel/preset-env" 6 | ] 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.github 2 | /.idea 3 | 4 | /custom-lodash.js 5 | /.editorconfig 6 | /.gitignore 7 | /jest.config.js 8 | 9 | /examples 10 | /src/__tests__ 11 | /test 12 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | © Copyright 2015-2019 Adobe. All rights reserved. 2 | 3 | Adobe holds the copyright for all the files found in this repository. 4 | 5 | See the LICENSE file for licensing information. 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf # Unix-style newlines 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.html] 12 | insert_final_newline = false 13 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | node: circleci/node@2.0.3 4 | jobs: 5 | test: 6 | executor: 7 | name: node/default 8 | tag: lts 9 | steps: 10 | - checkout 11 | - node/install-packages 12 | - run: npm run lint 13 | - run: npm test 14 | - run: bash <(curl -s https://codecov.io/bash) 15 | workflows: 16 | test: 17 | jobs: 18 | - test -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Adobe Data Layer | Examples 6 | 7 | 8 | 9 | 10 | 11 |

Adobe Client Data Layer | Examples

12 | 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ### Expected Behaviour 5 | 6 | ### Actual Behaviour 7 | 8 | ### Reproduce Scenario (including but not limited to) 9 | 10 | #### Steps to Reproduce 11 | 12 | #### Platform and Version 13 | 14 | #### Sample Code that illustrates the problem 15 | 16 | #### Logs taken while reproducing problem 17 | -------------------------------------------------------------------------------- /src/utils/get.js: -------------------------------------------------------------------------------- 1 | function get(obj, path, defaultValue) { 2 | const keys = Array.isArray(path) ? path : path.split('.'); 3 | let result = obj; 4 | 5 | for (const key of keys) { 6 | result = result[key]; 7 | 8 | if (result === undefined) { 9 | return defaultValue; 10 | } 11 | } 12 | 13 | return result; 14 | } 15 | 16 | function has(obj, path) { 17 | const keys = Array.isArray(path) ? path : path.split('.'); 18 | let result = obj; 19 | 20 | for (const key of keys) { 21 | /* eslint-disable no-prototype-builtins */ 22 | if (!result?.hasOwnProperty(key)) { 23 | return false; 24 | } 25 | result = result[key]; 26 | } 27 | 28 | return true; 29 | } 30 | 31 | module.exports = { 32 | get, 33 | has 34 | }; 35 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | module.exports = { 13 | collectCoverage: true, 14 | coverageDirectory: '/test/unit-test-coverage', 15 | testMatch: ['/src/__tests__/**/*.test.js'], 16 | setupFilesAfterEnv: ['./jest.setup.js', 'jest-expect-message'], 17 | coveragePathIgnorePatterns: ['/src/__tests__'] 18 | }; 19 | -------------------------------------------------------------------------------- /src/utils/indexOfListener.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | 13 | module.exports = function(listeners, listener) { 14 | const event = listener.event; 15 | 16 | if (Object.prototype.hasOwnProperty.call(listeners, event)) { 17 | for (const [index, registeredListener] of listeners[event].entries()) { 18 | if (registeredListener.handler === listener.handler) { 19 | return index; 20 | } 21 | } 22 | } 23 | return -1; 24 | }; 25 | -------------------------------------------------------------------------------- /src/utils/mergeWith.js: -------------------------------------------------------------------------------- 1 | 2 | function mergeWith(target, source, customizer) { 3 | if (!source || !target) { 4 | return; 5 | } 6 | 7 | Object.keys(source).forEach(key => { 8 | let newValue = customizer ? customizer(target[key], source[key], key, target) : undefined; 9 | 10 | if (newValue === undefined) { 11 | if (source[key] === Object(source[key]) && key in target && !Array.isArray(source[key])) { 12 | newValue = mergeWith(target[key], source[key], customizer); 13 | } else { 14 | newValue = source[key]; 15 | } 16 | } 17 | target[key] = newValue; 18 | }); 19 | 20 | return target; 21 | } 22 | 23 | function cloneDeepWith(target, customizer) { 24 | let newTarget = customizer ? customizer(target) : undefined; 25 | 26 | if (newTarget === undefined) { 27 | if (target === Object(target) && !Array.isArray(target)) { 28 | newTarget = {}; 29 | const keys = Object.keys(target); 30 | for (let i = 0; i < keys.length; i++) { 31 | const key = keys[i]; 32 | newTarget[key] = cloneDeepWith(target[key], customizer); 33 | } 34 | } 35 | newTarget = structuredClone(target); 36 | } 37 | 38 | return newTarget; 39 | } 40 | 41 | module.exports = { 42 | mergeWith, 43 | cloneDeepWith 44 | }; 45 | -------------------------------------------------------------------------------- /src/utils/ancestorRemoved.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | 13 | import { has, get } from './get.js'; 14 | 15 | /** 16 | * Checks if the object contains an ancestor that is set to null or undefined. 17 | * 18 | * @param {Object} object The object to check. 19 | * @param {String} path The path to check. 20 | * @returns {Boolean} true if the object contains an ancestor that is set to null or undefined, false otherwise. 21 | * @private 22 | */ 23 | module.exports = function(object, path) { 24 | let ancestorPath = path.substring(0, path.lastIndexOf('.')); 25 | while (ancestorPath) { 26 | if (has(object, ancestorPath)) { 27 | const ancestorValue = get(object, ancestorPath); 28 | if (ancestorValue === null || ancestorValue === undefined) { 29 | return true; 30 | } 31 | } 32 | ancestorPath = ancestorPath.substring(0, ancestorPath.lastIndexOf('.')); 33 | } 34 | 35 | return false; 36 | }; 37 | -------------------------------------------------------------------------------- /examples/js/datalayer.mocks.2.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | /* global console, window, dataLayer, CustomEvent */ 13 | (function() { 14 | 'use strict'; 15 | 16 | /* eslint no-console: "off" */ 17 | /* eslint no-unused-vars: "off" */ 18 | 19 | // Test case: scope = past -> console output should be: event1, event2 20 | 21 | window.adobeDataLayer = window.adobeDataLayer || []; 22 | 23 | var myHandler = function(event) { 24 | console.log(event.event); 25 | }; 26 | 27 | adobeDataLayer.push({ 28 | event: "event1" 29 | }); 30 | 31 | adobeDataLayer.push(function(dl) { 32 | dl.push({ 33 | event: "event2" 34 | }); 35 | 36 | dl.addEventListener( 37 | "adobeDataLayer:event", 38 | myHandler, 39 | {"scope": "past"} 40 | ); 41 | 42 | dl.push({ 43 | event: "event3" 44 | }); 45 | 46 | }); 47 | 48 | adobeDataLayer.push({ 49 | event: "event4" 50 | }); 51 | 52 | })(); 53 | -------------------------------------------------------------------------------- /examples/js/datalayer.mocks.3.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | /* global console, window, dataLayer, CustomEvent */ 13 | (function() { 14 | 'use strict'; 15 | 16 | /* eslint no-console: "off" */ 17 | /* eslint no-unused-vars: "off" */ 18 | 19 | // Test case: scope = future -> console output should be: event3, event4 20 | 21 | window.adobeDataLayer = window.adobeDataLayer || []; 22 | 23 | var myHandler = function(event) { 24 | console.log(event.event); 25 | }; 26 | 27 | adobeDataLayer.push({ 28 | event: "event1" 29 | }); 30 | 31 | adobeDataLayer.push(function(dl) { 32 | dl.push({ 33 | event: "event2" 34 | }); 35 | 36 | dl.addEventListener( 37 | "adobeDataLayer:event", 38 | myHandler, 39 | {"scope": "future"} 40 | ); 41 | 42 | dl.push({ 43 | event: "event3" 44 | }); 45 | 46 | }); 47 | 48 | adobeDataLayer.push({ 49 | event: "event4" 50 | }); 51 | 52 | })(); 53 | -------------------------------------------------------------------------------- /src/utils/dataMatchesContraints.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | 13 | module.exports = function(data, constraints) { 14 | // Go through all constraints and find one which does not match the data 15 | const foundObjection = Object.keys(constraints).find(key => { 16 | const type = constraints[key].type; 17 | const supportedValues = key && constraints[key].values; 18 | const mandatory = !constraints[key].optional; 19 | const value = data[key]; 20 | const valueType = typeof value; 21 | const incorrectType = type && valueType !== type; 22 | const noMatchForSupportedValues = supportedValues && !supportedValues.includes(value); 23 | if (mandatory) { 24 | return !value || incorrectType || noMatchForSupportedValues; 25 | } else { 26 | return value && (incorrectType || noMatchForSupportedValues); 27 | } 28 | }); 29 | // If no objections found then data matches contraints 30 | return typeof foundObjection === 'undefined'; 31 | }; 32 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | module.exports = { 13 | "extends": ["eslint:recommended"], 14 | "parserOptions": { 15 | "ecmaVersion": 2021, 16 | "sourceType": "module" 17 | }, 18 | "env": { 19 | "browser": true, 20 | "es6": true, 21 | "node": true, 22 | "jest": true 23 | }, 24 | "rules": { 25 | "complexity": ["warn", 15], 26 | "no-new": "off", 27 | "semi": ["error", "always"], 28 | "space-before-function-paren": ["error", "never"], 29 | "valid-jsdoc": [ "warn", { 30 | "prefer": { 31 | "arg": "param", 32 | "argument": "param", 33 | "return": "returns" 34 | }, 35 | "preferType": { 36 | "boolean": "Boolean", 37 | "number": "Number", 38 | "object": "Object", 39 | "string": "String" 40 | }, 41 | "matchDescription": ".+", 42 | "requireReturn": false, 43 | "requireReturnType": true, 44 | "requireParamDescription": true, 45 | "requireReturnDescription": true 46 | }] 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adobe/adobe-client-data-layer", 3 | "description": "Adobe Client Data Layer", 4 | "version": "3.0.1", 5 | "acdl": "dist/adobe-client-data-layer.min.js", 6 | "source": "src/index.js", 7 | "license": "Apache-2.0", 8 | "private": false, 9 | "browserslist": "> 1%", 10 | "homepage": "https://github.com/adobe/adobe-client-data-layer#readme", 11 | "scripts": { 12 | "build": "parcel build", 13 | "dev": "concurrently \"npm:watch\" \"npm:live-server\"", 14 | "watch": "parcel watch", 15 | "live-server": "live-server --port=3000 --open=examples/index.html --watch=examples --verbose", 16 | "lint": "eslint src", 17 | "test": "jest" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/adobe/adobe-client-data-layer" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/adobe/adobe-client-data-layer/issues" 25 | }, 26 | "devDependencies": { 27 | "@babel/core": "^7.6.4", 28 | "@babel/plugin-transform-modules-commonjs": "^7.23.0", 29 | "@babel/preset-env": "^7.24.0", 30 | "@parcel/reporter-bundle-analyzer": "^2.10.2", 31 | "@ungap/structured-clone": "^1.2.0", 32 | "babel-jest": "^29.7.0", 33 | "codecov": "^3.6.5", 34 | "concurrently": "^8.2.2", 35 | "eslint": "^8.57.0", 36 | "jest": "^24.9.0", 37 | "jest-cli": "^24.9.0", 38 | "jest-expect-message": "^1.0.2", 39 | "live-server": "^1.2.2", 40 | "parcel": "^2.10.2" 41 | }, 42 | "dependencies": { 43 | "@swc/helpers": "^0.5.3" 44 | }, 45 | "targets": { 46 | "acdl": { 47 | "outputFormat": "global" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/itemConstraints.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | 13 | /** 14 | * Constraints for each type of the item configuration. 15 | */ 16 | 17 | const itemConstraints = { 18 | event: { 19 | event: { 20 | type: 'string' 21 | }, 22 | eventInfo: { 23 | optional: true 24 | } 25 | }, 26 | listenerOn: { 27 | on: { 28 | type: 'string' 29 | }, 30 | handler: { 31 | type: 'function' 32 | }, 33 | scope: { 34 | type: 'string', 35 | values: ['past', 'future', 'all'], 36 | optional: true 37 | }, 38 | path: { 39 | type: 'string', 40 | optional: true 41 | } 42 | }, 43 | listenerOff: { 44 | off: { 45 | type: 'string' 46 | }, 47 | handler: { 48 | type: 'function', 49 | optional: true 50 | }, 51 | scope: { 52 | type: 'string', 53 | values: ['past', 'future', 'all'], 54 | optional: true 55 | }, 56 | path: { 57 | type: 'string', 58 | optional: true 59 | } 60 | } 61 | }; 62 | 63 | module.exports = itemConstraints; 64 | -------------------------------------------------------------------------------- /src/__tests__/scenarios/state.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | 13 | const DataLayerManager = require('../../dataLayerManager'); 14 | const DataLayer = { Manager: DataLayerManager }; 15 | const isEmpty = obj => [Object, Array].includes((obj || {}).constructor) && !Object.entries((obj || {})).length; 16 | 17 | let adobeDataLayer; 18 | 19 | const clearDL = function() { 20 | beforeEach(() => { 21 | adobeDataLayer = []; 22 | DataLayer.Manager({ dataLayer: adobeDataLayer }); 23 | }); 24 | }; 25 | 26 | describe('State', () => { 27 | clearDL(); 28 | 29 | test('getState()', () => { 30 | const carousel1 = { 31 | id: '/content/mysite/en/home/jcr:content/root/carousel1', 32 | items: {} 33 | }; 34 | const data = { 35 | component: { 36 | carousel: { 37 | carousel1: carousel1 38 | } 39 | } 40 | }; 41 | adobeDataLayer.push(data); 42 | expect(adobeDataLayer.getState()).toEqual(data); 43 | expect(adobeDataLayer.getState('component.carousel.carousel1')).toEqual(carousel1); 44 | expect(isEmpty(adobeDataLayer.getState('undefined-path'))); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /.github/workflows/npm-semantic-publish.yml: -------------------------------------------------------------------------------- 1 | name: Release and publish to npm 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | semantic_version: 7 | description: 'Release version (major.minor.patch)' 8 | required: true 9 | default: '' 10 | 11 | jobs: 12 | release_and_deploy: 13 | name: Release and publish 14 | runs-on: ubuntu-latest 15 | 16 | if: github.ref == 'refs/heads/master' 17 | 18 | permissions: 19 | contents: write 20 | pull-requests: write 21 | 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v5 25 | 26 | - name: Set up Node.js 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: 'lts/*' 30 | 31 | - name: Install dependencies 32 | run: npm ci 33 | 34 | - name: Build scripts and run tests 35 | run: npm run build 36 | 37 | - name: Bump version, commit, and tag 38 | run: | 39 | git config --local user.email "${{ github.actor }}@users.noreply.github.com" 40 | git config --local user.name "Release action on behalf of ${{ github.actor }}" 41 | npm version ${{ github.event.inputs.semantic_version }} -m '@releng - release %s' 42 | 43 | - name: Build again to ensure version bump is picked up 44 | run: npm run build 45 | 46 | - name: Push commit and tags 47 | uses: ad-m/github-push-action@master 48 | with: 49 | github_token: ${{ secrets.GITHUB_TOKEN }} 50 | branch: ${{ github.ref }} 51 | tags: true 52 | 53 | - name: Publish to npm 54 | env: 55 | NODE_AUTH_TOKEN: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} 56 | run: | 57 | echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > ~/.npmrc 58 | npm publish --access public 59 | -------------------------------------------------------------------------------- /src/listener.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | 13 | const CONSTANTS = require('./constants'); 14 | 15 | /** 16 | * Constructs a data layer listener. 17 | * 18 | * @param {Item} item The item from which to construct the listener. 19 | */ 20 | 21 | module.exports = function(item) { 22 | const _event = item.config.on || item.config.off; 23 | const _handler = item.config.handler || null; 24 | const _scope = item.config.scope || (item.config.on && CONSTANTS.listenerScope.ALL) || null; 25 | const _path = item.config.path || null; 26 | 27 | return { 28 | /** 29 | * Returns the listener event name. 30 | * 31 | * @returns {String} The listener event name. 32 | */ 33 | event: _event, 34 | 35 | /** 36 | * Returns the listener handler. 37 | * 38 | * @returns {(Function|null)} The listener handler. 39 | */ 40 | handler: _handler, 41 | 42 | /** 43 | * Returns the listener scope. 44 | * 45 | * @returns {(String|null)} The listener scope. 46 | */ 47 | scope: _scope, 48 | 49 | /** 50 | * Returns the listener path. 51 | * 52 | * @returns {(String|null)} The listener path. 53 | */ 54 | path: _path 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | 7 | ## Related Issue 8 | 9 | 10 | 11 | 12 | 13 | 14 | ## Motivation and Context 15 | 16 | 17 | 18 | ## How Has This Been Tested? 19 | 20 | 21 | 22 | 23 | 24 | ## Screenshots (if appropriate): 25 | 26 | ## Types of changes 27 | 28 | 29 | 30 | - [ ] Bug fix (non-breaking change which fixes an issue) 31 | - [ ] New feature (non-breaking change which adds functionality) 32 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 33 | 34 | ## Checklist: 35 | 36 | 37 | 38 | 39 | - [ ] I have signed the [Adobe Open Source CLA](https://opensource.adobe.com/cla.html). 40 | - [ ] My code follows the code style of this project. 41 | - [ ] My change requires a change to the documentation. 42 | - [ ] I have updated the documentation accordingly. 43 | - [ ] I have read the **CONTRIBUTING** document. 44 | - [ ] I have added tests to cover my changes. 45 | - [ ] All new and existing tests passed. 46 | -------------------------------------------------------------------------------- /src/__tests__/scenarios/performance.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | 13 | const DataLayerManager = require('../../dataLayerManager'); 14 | const DataLayer = { Manager: DataLayerManager }; 15 | let adobeDataLayer; 16 | 17 | const clearDL = function() { 18 | beforeEach(() => { 19 | adobeDataLayer = []; 20 | DataLayer.Manager({ dataLayer: adobeDataLayer }); 21 | }); 22 | }; 23 | 24 | describe('Performance', () => { 25 | clearDL(); 26 | 27 | // high load benchmark: runs alone in 16.078s (28/mon/2020) 28 | test('high load', () => { 29 | const mockCallback = jest.fn(); 30 | const data = {}; 31 | const start = new Date(); 32 | 33 | adobeDataLayer.addEventListener('carousel clicked', mockCallback); 34 | 35 | for (let i = 0; i < 1000; i++) { 36 | const pageId = '/content/mysite/en/products/crossfit' + i; 37 | const pageKey = 'page' + i; 38 | const page = { 39 | id: pageId, 40 | siteLanguage: 'en-us', 41 | siteCountry: 'US', 42 | pageType: 'product detail', 43 | pageName: 'pdp - crossfit zoom', 44 | pageCategory: 'women > shoes > athletic' 45 | }; 46 | const pushArg = { 47 | event: 'carousel clicked' 48 | }; 49 | data[pageKey] = page; 50 | pushArg[pageKey] = page; 51 | adobeDataLayer.push(pushArg); 52 | expect(adobeDataLayer.getState()).toStrictEqual(data); 53 | expect(mockCallback.mock.calls.length).toBe(i + 1); 54 | } 55 | 56 | expect(new Date() - start, 'to be smaller ms time than').toBeLessThan(60000); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/__tests__/scenarios/events.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | 13 | const testData = require('../testData'); 14 | const DataLayerManager = require('../../dataLayerManager'); 15 | const DataLayer = { Manager: DataLayerManager }; 16 | let adobeDataLayer; 17 | 18 | const clearDL = function() { 19 | beforeEach(() => { 20 | adobeDataLayer = []; 21 | DataLayer.Manager({ dataLayer: adobeDataLayer }); 22 | }); 23 | }; 24 | 25 | describe('Events', () => { 26 | clearDL(); 27 | 28 | test('push simple event', () => { 29 | adobeDataLayer.push(testData.carousel1click); 30 | expect(adobeDataLayer.getState()).toStrictEqual(testData.carousel1); 31 | }); 32 | 33 | test('check number of arguments in callback', () => { 34 | let calls = 0; 35 | 36 | adobeDataLayer.addEventListener('test', function() { calls = arguments.length; }); 37 | 38 | adobeDataLayer.push({ event: 'test' }); 39 | expect(calls, 'just one argument if no data is added').toStrictEqual(1); 40 | 41 | adobeDataLayer.push({ event: 'test', eventInfo: 'test' }); 42 | expect(calls, 'just one argument if no data is added').toStrictEqual(1); 43 | 44 | adobeDataLayer.push({ event: 'test', somekey: 'somedata' }); 45 | expect(calls, 'just one argument if data is added').toStrictEqual(1); 46 | }); 47 | 48 | test('check if eventInfo is passed to callback', () => { 49 | adobeDataLayer.addEventListener('test', function() { 50 | expect(arguments[0].eventInfo).toStrictEqual('test'); 51 | }); 52 | 53 | adobeDataLayer.push({ event: 'test', eventInfo: 'test' }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | const CONSTANTS = { 13 | /** 14 | * @typedef {String} ItemType 15 | **/ 16 | 17 | /** 18 | * Enumeration of data layer item types. 19 | * 20 | * @enum {ItemType} 21 | * @readonly 22 | */ 23 | itemType: { 24 | /** Represents an item of type data */ 25 | DATA: 'data', 26 | /** Represents an item of type function */ 27 | FCTN: 'fctn', 28 | /** Represents an item of type event */ 29 | EVENT: 'event', 30 | /** Represents an item of type listener on */ 31 | LISTENER_ON: 'listenerOn', 32 | /** Represents an item of type listener off */ 33 | LISTENER_OFF: 'listenerOff' 34 | }, 35 | 36 | /** 37 | * @typedef {String} DataLayerEvent 38 | **/ 39 | 40 | /** 41 | * Enumeration of data layer events. 42 | * 43 | * @enum {DataLayerEvent} 44 | * @readonly 45 | */ 46 | dataLayerEvent: { 47 | /** Represents an event triggered for any change in the data layer state */ 48 | CHANGE: 'adobeDataLayer:change', 49 | /** Represents an event triggered for any event push to the data layer */ 50 | EVENT: 'adobeDataLayer:event' 51 | }, 52 | 53 | /** 54 | * @typedef {String} ListenerScope 55 | **/ 56 | 57 | /** 58 | * Enumeration of listener scopes. 59 | * 60 | * @enum {ListenerScope} 61 | * @readonly 62 | */ 63 | listenerScope: { 64 | /** Past events only */ 65 | PAST: 'past', 66 | /** Future events only */ 67 | FUTURE: 'future', 68 | /** All events, past and future */ 69 | ALL: 'all' 70 | } 71 | }; 72 | 73 | module.exports = CONSTANTS; 74 | -------------------------------------------------------------------------------- /src/utils/customMerge.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | 13 | const { cloneDeepWith, mergeWith } = require('./mergeWith.js'); 14 | 15 | /** 16 | * Merges the source into the object and sets objects as 'undefined' if they are 'undefined' in the source object. 17 | * 18 | * @param {Object} object The object into which to merge. 19 | * @param {Object} source The source to merge with. 20 | * @returns {Object} The object into which source was merged in. 21 | */ 22 | module.exports = function(object, source) { 23 | const makeOmittingCloneDeepCustomizer = function(predicate) { 24 | return function omittingCloneDeepCustomizer(value) { 25 | if (value === Object(value)) { 26 | if (Array.isArray(value)) { 27 | return value.filter(item => !predicate(item)).map(item => cloneDeepWith(item, omittingCloneDeepCustomizer)); 28 | } 29 | 30 | const clone = {}; 31 | for (const subKey of Object.keys(value)) { 32 | if (!predicate(value[subKey])) { 33 | clone[subKey] = cloneDeepWith(value[subKey], omittingCloneDeepCustomizer); 34 | } 35 | } 36 | return clone; 37 | } 38 | return undefined; 39 | }; 40 | }; 41 | 42 | const customizer = function(_, srcValue) { 43 | if (typeof srcValue === 'undefined' || srcValue === null) { 44 | return null; 45 | } 46 | }; 47 | 48 | const omitDeep = function(value, predicate = (val) => !val) { 49 | return cloneDeepWith(value, makeOmittingCloneDeepCustomizer(predicate)); 50 | }; 51 | 52 | mergeWith(object, source, customizer); 53 | 54 | // Remove null or undefined objects 55 | object = omitDeep(object, v => v === null || v === undefined); 56 | 57 | return object; 58 | }; 59 | -------------------------------------------------------------------------------- /src/utils/listenerMatch.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | 13 | import { has } from './get.js'; 14 | 15 | const CONSTANTS = require('../constants'); 16 | const ancestorRemoved = require('./ancestorRemoved'); 17 | 18 | /** 19 | * Checks if the listener matches the item. 20 | * 21 | * @param {Listener} listener The listener. 22 | * @param {Item} item The item. 23 | * @returns {Boolean} true if listener matches the item, false otherwise. 24 | */ 25 | function listenerMatch(listener, item) { 26 | const event = listener.event; 27 | const itemConfig = item.config; 28 | let matches = false; 29 | 30 | if (item.type === CONSTANTS.itemType.DATA) { 31 | if (event === CONSTANTS.dataLayerEvent.CHANGE) { 32 | matches = selectorMatches(listener, item); 33 | } 34 | } else if (item.type === CONSTANTS.itemType.EVENT) { 35 | if (event === CONSTANTS.dataLayerEvent.EVENT || event === itemConfig.event) { 36 | matches = selectorMatches(listener, item); 37 | } 38 | if (item.data && event === CONSTANTS.dataLayerEvent.CHANGE) { 39 | matches = selectorMatches(listener, item); 40 | } 41 | } 42 | 43 | return matches; 44 | } 45 | 46 | /** 47 | * Checks if a listener has a selector that points to an object in the data payload of an item. 48 | * 49 | * @param {Listener} listener The listener to extract the selector from. 50 | * @param {Item} item The item. 51 | * @returns {Boolean} true if a selector is not provided or if the given selector is matching, false otherwise. 52 | * @private 53 | */ 54 | function selectorMatches(listener, item) { 55 | if (item.data && listener.path) { 56 | return has(item.data, listener.path) || ancestorRemoved(item.data, listener.path); 57 | } 58 | 59 | return true; 60 | } 61 | 62 | module.exports = listenerMatch; 63 | -------------------------------------------------------------------------------- /examples/js/datalayer.mocks.4.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | /* global console, window, dataLayer, CustomEvent */ 13 | (function() { 14 | 'use strict'; 15 | 16 | /* eslint no-console: "off" */ 17 | /* eslint no-unused-vars: "off" */ 18 | 19 | // Test case: scope = future -> console output should be: event3, event4 20 | 21 | window.adobeDataLayer = window.adobeDataLayer || []; 22 | 23 | 24 | 25 | // Example 1. If event listener has path filter then we return before / after value not the whole before / after state 26 | 27 | adobeDataLayer.addEventListener("test1", function(event, before, after) { 28 | console.log('LOG1: ', before, after); 29 | }, { path: 'component.image' }); 30 | adobeDataLayer.push({ 31 | event: "test1", 32 | component: { image: { id: 'image1' } } 33 | }); 34 | // LOG1: undefined, { id: "image1' } 35 | 36 | // Clear DL before next example 37 | adobeDataLayer.push({ component: null }); 38 | 39 | 40 | 41 | // Example 2. If we add event listener and only then push items to DL we get before / after state arguments in callback function. 42 | 43 | adobeDataLayer.addEventListener("test2", function(event, before, after) { 44 | console.log('LOG2: ', before, after); 45 | }); 46 | adobeDataLayer.push({ 47 | event: "test2", 48 | count: "one" 49 | }); 50 | // LOG2: {}, { count: "one" } 51 | 52 | // Clear DL before next example 53 | adobeDataLayer.push({ count: null }); 54 | 55 | 56 | 57 | // Example 3. If we add event listener then for all past items (items in DL) we will not get before / after arguments in callback function. 58 | 59 | adobeDataLayer.push({ 60 | event: "test3", 61 | count: "one" 62 | }); 63 | adobeDataLayer.addEventListener("test3", function(event, before, after) { 64 | console.log('LOG3: ', before, after); 65 | }); 66 | // LOG3: undefined, undefined 67 | 68 | // Clear DL before next example 69 | adobeDataLayer.push({ count: null }); 70 | 71 | 72 | 73 | // Example 4. If we have data in DL, then add event listener and then push items to DL we will get before / after state arguments in callback function only for items that are pushed after event listener was added. 74 | 75 | adobeDataLayer.push({ 76 | event: "test4", 77 | count: "one" 78 | }); 79 | adobeDataLayer.addEventListener("test4", function(event, before, after) { 80 | console.log('LOG4: ', before, after); 81 | }); 82 | adobeDataLayer.push({ 83 | event: "test4", 84 | count: "two" 85 | }); 86 | // LOG4: undefined, undefined 87 | // LOG4: {}, { count: "one" } 88 | 89 | // Clear DL before next example 90 | adobeDataLayer.push({ count: null }); 91 | 92 | })(); 93 | -------------------------------------------------------------------------------- /src/item.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | 13 | const dataMatchesContraints = require('./utils/dataMatchesContraints'); 14 | const ITEM_CONSTRAINTS = require('./itemConstraints'); 15 | const CONSTANTS = require('./constants'); 16 | 17 | const isEmpty = obj => [Object, Array].includes((obj || {}).constructor) && !Object.entries((obj || {})).length; 18 | function isPlainObject(obj) { 19 | if (typeof obj !== 'object' || obj === null) return false; 20 | let proto = obj; 21 | while (Object.getPrototypeOf(proto) !== null) { 22 | proto = Object.getPrototypeOf(proto); 23 | } 24 | return Object.getPrototypeOf(obj) === proto; 25 | } 26 | 27 | /** 28 | * Constructs a data layer item. 29 | * 30 | * @param {ItemConfig} itemConfig The data layer item configuration. 31 | * @param {Number} index The item index in the array of existing items. 32 | */ 33 | 34 | module.exports = function(itemConfig, index) { 35 | const _config = itemConfig; 36 | const _index = index; 37 | const _type = getType(); 38 | const _data = getData(); 39 | const _valid = !!_type; 40 | 41 | function getType() { 42 | return Object.keys(ITEM_CONSTRAINTS).find(key => dataMatchesContraints(_config, ITEM_CONSTRAINTS[key])) || 43 | (typeof _config === 'function' && CONSTANTS.itemType.FCTN) || 44 | (isPlainObject(_config) && CONSTANTS.itemType.DATA); 45 | } 46 | 47 | function getData() { 48 | const data = Object.keys(_config) 49 | .filter(key => !Object.keys(ITEM_CONSTRAINTS.event).includes(key)) 50 | .reduce((obj, key) => { 51 | obj[key] = _config[key]; 52 | return obj; 53 | }, {}); 54 | if (!isEmpty(data)) { 55 | return data; 56 | } 57 | } 58 | 59 | return { 60 | /** 61 | * Returns the item configuration. 62 | * 63 | * @returns {ItemConfig} The item configuration. 64 | */ 65 | config: _config, 66 | 67 | /** 68 | * Returns the item type. 69 | * 70 | * @returns {itemType} The item type. 71 | */ 72 | type: _type, 73 | 74 | /** 75 | * Returns the item data. 76 | * 77 | * @returns {DataConfig} The item data. 78 | */ 79 | data: _data, 80 | 81 | /** 82 | * Indicates whether the item is valid. 83 | * 84 | * @returns {Boolean} true if the item is valid, false otherwise. 85 | */ 86 | valid: _valid, 87 | 88 | /** 89 | * Returns the index of the item in the array of existing items (added with the standard Array.prototype.push()) 90 | * 91 | * @returns {Number} The index of the item in the array of existing items if it exists, -1 otherwise. 92 | */ 93 | index: _index 94 | }; 95 | }; 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Adobe Client Data Layer 2 | 3 | [![NPM version](https://badgen.net/npm/v/@adobe/adobe-client-data-layer)](https://www.npmjs.com/package/@adobe/adobe-client-data-layer) 4 | [![BundlePhobia](https://badgen.net/bundlephobia/minzip/@adobe/adobe-client-data-layer)](https://bundlephobia.com/result?p=@adobe/adobe-client-data-layer) 5 | [![LGTM](https://badgen.net/lgtm/grade/g/adobe/adobe-client-data-layer)](https://lgtm.com/projects/g/adobe/adobe-client-data-layer) 6 | [![CircleCI](https://badgen.net/circleci/github/adobe/adobe-client-data-layer)](https://app.circleci.com/pipelines/github/adobe/adobe-client-data-layer) 7 | [![Codecov](https://badgen.net/codecov/c/github/adobe/adobe-client-data-layer)](https://codecov.io/gh/adobe/adobe-client-data-layer) 8 | 9 | The Adobe Client Data Layer aims to reduce the effort to instrument websites by providing a standardized method to expose and access any kind of data for any script. 10 | 11 | ## Documentation 12 | 13 | * [Adobe Client Data Layer](https://github.com/adobe/adobe-client-data-layer/wiki) 14 | 15 | ## Consuming 16 | 17 | The best way to try out the Adobe Client Data Layer is to install the distributed npm package in your project build, by running: 18 | ``` 19 | npm install @adobe/adobe-client-data-layer 20 | ``` 21 | 22 | Locate the `/dist` folder in the installed package, which contains the built and minified javascript. 23 | 24 | This script can then be included in your page head, as follows: 25 | 26 | ```html 27 | 28 | ``` 29 | 30 | > **Note** - you can directly access the [minified version](https://unpkg.com/browse/@adobe/adobe-client-data-layer@2.0.1/dist/adobe-client-data-layer.min.js) of the data layer without downloading the sources and compiling them. 31 | 32 | ## Building / Testing 33 | 34 | First run the following commands: 35 | ``` 36 | npm install 37 | ``` 38 | 39 | Then choose from the following npm scripts: 40 | * `npm run dev` - generates the build in the `./dist` folder and runs a development server on `localhost:3000`. 41 | * `npm run build` - generates the build in the `./dist` folder. 42 | * `npm run test` - run the unit tests 43 | 44 | > **Note** - you can [get some stats](https://bundlephobia.com/result?p=@adobe/adobe-client-data-layer@2.0.1) (bundle size, download time) about the released version. 45 | 46 | ## Releasing 47 | 48 | Release can be triggered only as a Github action. There is no way to release package manually using npm scripts anymore. 49 | 50 | To release using Github action: 51 | 1. Go to the Github [Actions](https://github.com/adobe/adobe-client-data-layer/actions) tab. 52 | 2. Select "Release and publish to npm" and click "Run workflow". 53 | 3. Provide a new version. Patch, minor or major versions allowed, see [NPM Semantic Versioning](https://docs.npmjs.com/about-semantic-versioning). 54 | 55 | Release and publish Github action will: 56 | * increase the ACDL version accordingly, 57 | * commit release and push to Github repository, 58 | * create and push the Git release tag, 59 | * publish the npm package. 60 | 61 | ## Contributing 62 | 63 | Contributions are welcome! Read the [Contributing Guide](./.github/CONTRIBUTING.md) for more information. 64 | 65 | ## Licensing 66 | 67 | This project is licensed under the Apache V2 License. See [LICENSE](LICENSE) for more information. 68 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Adobe Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at Grp-opensourceoffice@adobe.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | 13 | const DataLayerManager = require('./dataLayerManager'); 14 | 15 | /** 16 | * Data Layer. 17 | * 18 | * @type {Object} 19 | */ 20 | const DataLayer = { 21 | Manager: DataLayerManager 22 | }; 23 | 24 | window.adobeDataLayer = window.adobeDataLayer || []; 25 | 26 | // If data layer has already been initialized, do not re-initialize. 27 | if (window.adobeDataLayer.version) { 28 | console.warn( 29 | `Adobe Client Data Layer v${window.adobeDataLayer.version} has already been imported/initialized on this page. You may be erroneously loading it a second time.` 30 | ); 31 | } else { 32 | DataLayer.Manager({ 33 | dataLayer: window.adobeDataLayer 34 | }); 35 | } 36 | 37 | /** 38 | * @typedef {Object} ListenerOnConfig 39 | * @property {String} on Name of the event to bind to. 40 | * @property {String} [path] Object key in the state to bind to. 41 | * @property {ListenerScope} [scope] Scope of the listener. 42 | * @property {Function} handler Handler to execute when the bound event is triggered. 43 | */ 44 | 45 | /** 46 | * @typedef {Object} ListenerOffConfig 47 | * @property {String} off Name of the event to unbind. 48 | * @property {String} [path] Object key in the state to bind to. 49 | * @property {ListenerScope} [scope] Scope of the listener. 50 | * @property {Function} [handler] Handler for a previously attached event to unbind. 51 | */ 52 | 53 | /** 54 | * @typedef {Object} DataConfig 55 | * @property {Object} data Data to be updated in the state. 56 | */ 57 | 58 | /** 59 | * @typedef {Object} EventConfig 60 | * @property {String} event Name of the event. 61 | * @property {Object} [eventInfo] Additional information to pass to the event handler. 62 | * @property {DataConfig.data} [data] Data to be updated in the state. 63 | */ 64 | 65 | /** 66 | * @typedef {DataConfig | EventConfig | ListenerOnConfig | ListenerOffConfig} ItemConfig 67 | */ 68 | 69 | /** 70 | * Triggered when there is change in the data layer state. 71 | * 72 | * @event DataLayerEvent.CHANGE 73 | * @type {Object} 74 | * @property {Object} data Data pushed that caused a change in the data layer state. 75 | */ 76 | 77 | /** 78 | * Triggered when an event is pushed to the data layer. 79 | * 80 | * @event DataLayerEvent.EVENT 81 | * @type {Object} 82 | * @property {String} name Name of the committed event. 83 | * @property {Object} eventInfo Additional information passed with the committed event. 84 | * @property {Object} data Data that was pushed alongside the event. 85 | */ 86 | 87 | /** 88 | * Triggered when an arbitrary event is pushed to the data layer. 89 | * 90 | * @event 91 | * @type {Object} 92 | * @property {String} name Name of the committed event. 93 | * @property {Object} eventInfo Additional information passed with the committed event. 94 | * @property {Object} data Data that was pushed alongside the event. 95 | */ 96 | 97 | module.exports = DataLayer; 98 | -------------------------------------------------------------------------------- /src/__tests__/scenarios/functions.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | 13 | const testData = require('../testData'); 14 | const DataLayerManager = require('../../dataLayerManager'); 15 | const DataLayer = { Manager: DataLayerManager }; 16 | let adobeDataLayer; 17 | 18 | const clearDL = function() { 19 | beforeEach(() => { 20 | adobeDataLayer = []; 21 | DataLayer.Manager({ dataLayer: adobeDataLayer }); 22 | }); 23 | }; 24 | 25 | const createEventListener = function(dl, eventName, callback) { 26 | dl.addEventListener(eventName, function(eventData) { 27 | expect(eventData, 'data layer object as an argument of callback').toEqual(eventData); 28 | callback(); 29 | }); 30 | }; 31 | 32 | describe('Functions', () => { 33 | describe('simple', () => { 34 | clearDL(); 35 | 36 | test('push simple function', () => { 37 | const mockCallback = jest.fn(); 38 | adobeDataLayer.push(mockCallback); 39 | expect(mockCallback.mock.calls.length).toBe(1); 40 | }); 41 | 42 | test('function adds event listener for adobeDataLayer:change', () => { 43 | const mockCallback = jest.fn(); 44 | const addEventListener = function(adl) { 45 | adl.addEventListener('adobeDataLayer:change', mockCallback); 46 | }; 47 | 48 | adobeDataLayer.push(testData.carousel1); 49 | adobeDataLayer.push(addEventListener); 50 | adobeDataLayer.push(testData.carousel2); 51 | 52 | expect(mockCallback.mock.calls.length, 'event triggered twice').toBe(2); 53 | }); 54 | 55 | test('function updates component in data layer state', () => { 56 | const updateCarousel = function(adl) { 57 | adl.push(testData.carousel1new); 58 | }; 59 | 60 | adobeDataLayer.push(testData.carousel1); 61 | expect(adobeDataLayer.getState(), 'carousel set to carousel1').toEqual(testData.carousel1); 62 | 63 | adobeDataLayer.push(updateCarousel); 64 | expect(adobeDataLayer.getState(), 'carousel set to carousel1new').toEqual(testData.carousel1new); 65 | }); 66 | }); 67 | 68 | test('nested anonymous functions', () => { 69 | const mockCallback1 = jest.fn(); 70 | const mockCallback2 = jest.fn(); 71 | const mockCallback3 = jest.fn(); 72 | 73 | adobeDataLayer.addEventListener('adobeDataLayer:event', function() { 74 | mockCallback1(); 75 | }); 76 | 77 | adobeDataLayer.push(testData.carousel1click); 78 | 79 | adobeDataLayer.push(function(dl) { 80 | createEventListener(dl, 'carousel clicked', mockCallback2, testData.carousel1click); 81 | 82 | dl.push(function(dl2) { 83 | createEventListener(dl2, 'viewed', mockCallback3, testData.carousel1viewed); 84 | 85 | dl2.push(function(dl3) { 86 | dl3.push(testData.carousel1click); 87 | }); 88 | }); 89 | 90 | adobeDataLayer.push(testData.carousel1viewed); 91 | }); 92 | 93 | DataLayer.Manager({ dataLayer: adobeDataLayer }); 94 | 95 | expect(mockCallback1.mock.calls.length, 'callback triggered 3 times').toBe(3); 96 | expect(mockCallback2.mock.calls.length, 'callback triggered 2 times').toBe(2); 97 | expect(mockCallback3.mock.calls.length, 'callback triggered 1 times').toBe(1); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /src/__tests__/testData.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | 13 | import { mergeWith } from '../utils/mergeWith'; 14 | const merge = (target, ...sources) => { 15 | return sources.reduce((acc, source) => { 16 | return mergeWith(acc, structuredClone(source)); 17 | }, target); 18 | }; 19 | 20 | const carousel1 = { 21 | component: { 22 | carousel: { 23 | carousel1: { 24 | id: '/content/mysite/en/home/jcr:content/root/carousel1', 25 | shownItems: [ 26 | 'item1', 'item2', 'item3', 'item4', 'item5' 27 | ], 28 | items: {} 29 | } 30 | } 31 | } 32 | }; 33 | 34 | const image1 = { 35 | component: { 36 | image: { 37 | image1: { 38 | src: '/content/image/test.jpg' 39 | } 40 | } 41 | } 42 | }; 43 | 44 | const testData = { 45 | 46 | // Pages 47 | 48 | page1: { 49 | page: { 50 | id: '/content/mysite/en/products/crossfit', 51 | siteLanguage: 'en-us', 52 | siteCountry: 'US', 53 | pageType: 'product detail', 54 | pageName: 'pdp - crossfit zoom', 55 | pageCategory: 'women > shoes > athletic' 56 | } 57 | }, 58 | page2: { 59 | page: { 60 | id: '/content/mysite/en/products/running', 61 | siteLanguage: 'en-us', 62 | siteCountry: 'US', 63 | pageType: 'product detail', 64 | pageName: 'pdp - running zoom', 65 | pageCategory: 'women > shoes > running' 66 | } 67 | }, 68 | 69 | componentNull: { 70 | component: null 71 | }, 72 | 73 | componentUndefined: { 74 | component: undefined 75 | }, 76 | 77 | // Carousel 1 78 | 79 | carousel1: carousel1, 80 | carousel1withUndefined: { 81 | component: { 82 | carousel: { 83 | carousel1: undefined 84 | } 85 | } 86 | }, 87 | carousel1withNull: { 88 | component: { 89 | carousel: { 90 | carousel1: null 91 | } 92 | } 93 | }, 94 | carousel1withNullAndUndefinedArrayItems: { 95 | component: { 96 | carousel: { 97 | carousel1: { 98 | id: '/content/mysite/en/home/jcr:content/root/carousel1', 99 | shownItems: [ 100 | 'item1', null, 'item3', undefined, 'item5' 101 | ], 102 | items: {} 103 | } 104 | } 105 | } 106 | }, 107 | carousel1withRemovedArrayItems: { 108 | component: { 109 | carousel: { 110 | carousel1: { 111 | id: '/content/mysite/en/home/jcr:content/root/carousel1', 112 | shownItems: [ 113 | 'item1', 'item3', 'item5' 114 | ], 115 | items: {} 116 | } 117 | } 118 | } 119 | }, 120 | carousel1empty: { 121 | component: { 122 | carousel: { 123 | } 124 | } 125 | }, 126 | carousel1new: merge({}, carousel1, { 127 | component: { 128 | carousel: { 129 | carousel1: { 130 | id: '/content/mysite/en/home/jcr:content/root/carousel1-new' 131 | } 132 | } 133 | } 134 | }), 135 | carousel1click: merge({}, carousel1, { 136 | event: 'carousel clicked' 137 | }), 138 | carousel1change: merge({}, carousel1, { 139 | event: 'adobeDataLayer:change' 140 | }), 141 | carousel1viewed: merge({}, carousel1, { 142 | event: 'viewed' 143 | }), 144 | carousel1oldId: merge({}, carousel1, { 145 | component: { 146 | carousel: { 147 | carousel1: { 148 | id: 'old' 149 | } 150 | } 151 | } 152 | }), 153 | carousel1newId: merge({}, carousel1, { 154 | component: { 155 | carousel: { 156 | carousel1: { 157 | id: 'new' 158 | } 159 | } 160 | } 161 | }), 162 | 163 | // Carousel 2 164 | 165 | carousel2: { 166 | component: { 167 | carousel: { 168 | carousel2: { 169 | id: '/content/mysite/en/home/jcr:content/root/carousel2', 170 | items: {} 171 | } 172 | } 173 | } 174 | }, 175 | carousel2withUndefined: { 176 | component: { 177 | carousel: { 178 | carousel2: undefined 179 | } 180 | } 181 | }, 182 | carousel2empty: { 183 | component: { 184 | carousel: { 185 | } 186 | } 187 | }, 188 | 189 | // Image 1 190 | 191 | image1: image1, 192 | image1change: merge({}, image1, { 193 | event: 'adobeDataLayer:change' 194 | }), 195 | image1viewed: merge({}, image1, { 196 | event: 'viewed' 197 | }) 198 | }; 199 | 200 | module.exports = testData; 201 | -------------------------------------------------------------------------------- /src/listenerManager.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | 13 | import { cloneDeepWith } from './utils/mergeWith'; 14 | 15 | const constants = require('./constants'); 16 | const listenerMatch = require('./utils/listenerMatch'); 17 | const indexOfListener = require('./utils/indexOfListener'); 18 | 19 | /** 20 | * Creates a listener manager. 21 | * 22 | * @param {Manager} dataLayerManager The data layer manager. 23 | * @returns {ListenerManager} A listener manager. 24 | */ 25 | module.exports = function(dataLayerManager) { 26 | const _listeners = {}; 27 | const _dataLayerManager = dataLayerManager; 28 | 29 | /** 30 | * Find index of listener in listeners object. 31 | */ 32 | const _indexOfListener = indexOfListener.bind(null, _listeners); 33 | 34 | const ListenerManager = { 35 | /** 36 | * Registers a listener. 37 | * 38 | * @function 39 | * @param {Listener} listener The listener to register. 40 | * @returns {Boolean} true if the listener was registered, false otherwise. 41 | */ 42 | register: function(listener) { 43 | const event = listener.event; 44 | 45 | if (Object.prototype.hasOwnProperty.call(_listeners, event)) { 46 | if (_indexOfListener(listener) === -1) { 47 | _listeners[listener.event].push(listener); 48 | return true; 49 | } 50 | } else { 51 | _listeners[listener.event] = [listener]; 52 | return true; 53 | } 54 | return false; 55 | }, 56 | /** 57 | * Unregisters a listener. 58 | * 59 | * @function 60 | * @param {Listener} listener The listener to unregister. 61 | */ 62 | unregister: function(listener) { 63 | const event = listener.event; 64 | 65 | if (Object.prototype.hasOwnProperty.call(_listeners, event)) { 66 | if (!(listener.handler || listener.scope || listener.path)) { 67 | _listeners[event] = []; 68 | } else { 69 | const index = _indexOfListener(listener); 70 | if (index > -1) { 71 | _listeners[event].splice(index, 1); 72 | } 73 | } 74 | } 75 | }, 76 | /** 77 | * Triggers listeners related to the passed item. 78 | * 79 | * @function 80 | * @param {Item} item Item to trigger listener for. 81 | */ 82 | triggerListeners: function(item) { 83 | const triggeredEvents = _getTriggeredEvents(item); 84 | triggeredEvents.forEach(function(event) { 85 | if (Object.prototype.hasOwnProperty.call(_listeners, event)) { 86 | for (const listener of _listeners[event]) { 87 | _callHandler(listener, item); 88 | } 89 | } 90 | }); 91 | }, 92 | /** 93 | * Triggers a single listener on the passed item. 94 | * 95 | * @function 96 | * @param {Listener} listener Listener to call. 97 | * @param {Item} item Item to call the listener with. 98 | */ 99 | triggerListener: function(listener, item) { 100 | _callHandler(listener, item); 101 | } 102 | }; 103 | 104 | /** 105 | * Calls the listener handler on the item if a match is found. 106 | * 107 | * @param {Listener} listener The listener. 108 | * @param {Item} item The item. 109 | * @private 110 | */ 111 | function _callHandler(listener, item) { 112 | if (listenerMatch(listener, item)) { 113 | const callbackArgs = [cloneDeepWith(item.config)]; 114 | listener.handler.apply(_dataLayerManager.getDataLayer(), callbackArgs); 115 | } 116 | } 117 | 118 | /** 119 | * Returns the names of the events that are triggered for this item. 120 | * 121 | * @param {Item} item The item. 122 | * @returns {Array} The names of the events that are triggered for this item. 123 | * @private 124 | */ 125 | function _getTriggeredEvents(item) { 126 | const triggeredEvents = []; 127 | 128 | switch (item.type) { 129 | case constants.itemType.DATA: { 130 | triggeredEvents.push(constants.dataLayerEvent.CHANGE); 131 | break; 132 | } 133 | case constants.itemType.EVENT: { 134 | triggeredEvents.push(constants.dataLayerEvent.EVENT); 135 | if (item.data) triggeredEvents.push(constants.dataLayerEvent.CHANGE); 136 | if (item.config.event !== constants.dataLayerEvent.CHANGE) { 137 | triggeredEvents.push(item.config.event); 138 | } 139 | break; 140 | } 141 | } 142 | return triggeredEvents; 143 | } 144 | 145 | return ListenerManager; 146 | }; 147 | -------------------------------------------------------------------------------- /src/__tests__/demo.js: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------------------------------------------- 2 | // Pushing data 3 | // ----------------------------------------------------------------------------------------------------------------- 4 | 5 | window.adobeDataLayer = window.adobeDataLayer || []; 6 | 7 | window.adobeDataLayer.getState(); 8 | 9 | window.adobeDataLayer.push({ 10 | component: { 11 | carousel: { 12 | carousel1: { 13 | id: '/content/mysite/en/home/jcr:content/root/carousel1', 14 | shownItems: [ 15 | 'item1', 'item2', 'item3' 16 | ] 17 | }, 18 | carousel2: { 19 | id: '/content/mysite/en/home/jcr:content/root/carousel2', 20 | items: {} 21 | }, 22 | carousel3: { 23 | id: '/content/mysite/en/home/jcr:content/root/carousel3', 24 | items: {} 25 | } 26 | } 27 | } 28 | }); 29 | 30 | // remove item 31 | 32 | window.adobeDataLayer.push({ 33 | component: { 34 | carousel: { 35 | carousel1: { 36 | id: '/content/mysite/en/home/jcr:content/root/carousel1', 37 | shownItems: [ 38 | 'item1', null, 'item3' 39 | ] 40 | }, 41 | carousel3: null 42 | } 43 | } 44 | }); 45 | 46 | console.log('Pushing component: ', window.adobeDataLayer.getState('component.carousel')); 47 | 48 | // getState() returns a copy of the state 49 | 50 | window.adobeDataLayer.getState().component.carousel.carousel2.id = 'new id'; 51 | 52 | window.adobeDataLayer.getState('component.carousel.carousel2.id'); 53 | 54 | // update an object 55 | 56 | window.adobeDataLayer.push({ 57 | component: { 58 | carousel: { 59 | carousel1: { 60 | shownItems: [ 61 | 'item1', 'item3-new' 62 | ] 63 | } 64 | } 65 | } 66 | }); 67 | 68 | window.adobeDataLayer.getState('component.carousel.carousel1.shownItems'); 69 | 70 | // ----------------------------------------------------------------------------------------------------------------- 71 | // Pushing a function 72 | // ----------------------------------------------------------------------------------------------------------------- 73 | 74 | window.adobeDataLayer.push(function(dl) { 75 | console.log('Pushing a function:'); 76 | console.log(dl.getState()); 77 | }); 78 | 79 | // ----------------------------------------------------------------------------------------------------------------- 80 | // Pushing an event 81 | // ----------------------------------------------------------------------------------------------------------------- 82 | 83 | window.adobeDataLayer.push({ 84 | event: 'clicked', 85 | eventInfo: { 86 | reference: 'component.carousel.carousel1' 87 | }, 88 | component: { 89 | carousel: { 90 | carousel1: { 91 | id: '/content/mysite/en/home/jcr:content/root/carousel1-new' 92 | } 93 | } 94 | } 95 | }); 96 | 97 | // ----------------------------------------------------------------------------------------------------------------- 98 | // Adding an event listener: scope 99 | // ----------------------------------------------------------------------------------------------------------------- 100 | 101 | const fct1 = function(event) { 102 | console.log('fct1'); 103 | console.log(event); 104 | }; 105 | 106 | window.adobeDataLayer.addEventListener('adobeDataLayer:change', fct1); 107 | 108 | window.adobeDataLayer.push({ 109 | component: { 110 | carousel: { 111 | carousel4: { 112 | id: '/content/mysite/en/home/jcr:content/root/carousel4', 113 | items: {} 114 | } 115 | } 116 | } 117 | }); 118 | 119 | const fct2 = function(event) { 120 | console.log('fct2'); 121 | console.log('this', this); 122 | console.log('event', event); 123 | }; 124 | 125 | window.adobeDataLayer.addEventListener('adobeDataLayer:change', fct2, { scope: 'future' }); 126 | 127 | window.adobeDataLayer.push({ 128 | component: { 129 | carousel: { 130 | carousel5: { 131 | id: '/content/mysite/en/home/jcr:content/root/carousel5', 132 | items: {} 133 | } 134 | } 135 | } 136 | }); 137 | 138 | // ----------------------------------------------------------------------------------------------------------------- 139 | // Adding an event listener: path 140 | // ----------------------------------------------------------------------------------------------------------------- 141 | 142 | const fct3 = function(event) { 143 | console.log('fct3'); 144 | console.log('this', this); 145 | console.log('event', event); 146 | }; 147 | 148 | window.adobeDataLayer.addEventListener('adobeDataLayer:change', fct3, { path: 'component.carousel.carousel5' }); 149 | 150 | window.adobeDataLayer.push({ 151 | component: { 152 | carousel: { 153 | carousel5: { 154 | id: '/content/mysite/en/home/jcr:content/root/carousel5-new', 155 | items: {} 156 | } 157 | } 158 | } 159 | }); 160 | 161 | // ----------------------------------------------------------------------------------------------------------------- 162 | // Removing an event listener 163 | // ----------------------------------------------------------------------------------------------------------------- 164 | 165 | window.adobeDataLayer.removeEventListener('adobeDataLayer:change'); 166 | 167 | window.adobeDataLayer.push({ 168 | component: { 169 | carousel: { 170 | carousel6: { 171 | id: '/content/mysite/en/home/jcr:content/root/carousel6', 172 | items: {} 173 | } 174 | } 175 | } 176 | }); 177 | -------------------------------------------------------------------------------- /src/__tests__/scenarios/data.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | import { mergeWith } from '../../utils/mergeWith'; 13 | 14 | const testData = require('../testData'); 15 | const DataLayerManager = require('../../dataLayerManager'); 16 | const DataLayer = { Manager: DataLayerManager }; 17 | const merge = (target, ...sources) => { 18 | return sources.reduce((acc, source) => { 19 | return mergeWith(acc, structuredClone(source)); 20 | }, target); 21 | }; 22 | let adobeDataLayer; 23 | 24 | const clearDL = function() { 25 | beforeEach(() => { 26 | adobeDataLayer = []; 27 | DataLayer.Manager({ dataLayer: adobeDataLayer }); 28 | }); 29 | }; 30 | 31 | describe('Data', () => { 32 | clearDL(); 33 | 34 | test('push page', () => { 35 | adobeDataLayer.push(testData.page1); 36 | expect(adobeDataLayer.getState(), 'page is in data layer after push').toStrictEqual(testData.page1); 37 | }); 38 | 39 | test('push data, override and remove', () => { 40 | adobeDataLayer.push({ test: 'foo' }); 41 | expect(adobeDataLayer.getState(), 'data pushed').toStrictEqual({ test: 'foo' }); 42 | 43 | adobeDataLayer.push({ test: 'bar' }); 44 | expect(adobeDataLayer.getState(), 'data overriden').toStrictEqual({ test: 'bar' }); 45 | 46 | adobeDataLayer.push({ test: null }); 47 | expect(adobeDataLayer.getState(), 'data removed').toStrictEqual({}); 48 | }); 49 | 50 | test('push components and override', () => { 51 | const twoCarousels = merge({}, testData.carousel1, testData.carousel2); 52 | const carousel1empty = merge({}, testData.carousel1empty, testData.carousel2); 53 | const carousel2empty = merge({}, testData.carousel1, testData.carousel2empty); 54 | const twoCarouselsEmpty = merge({}, testData.carousel1empty, testData.carousel2empty); 55 | 56 | adobeDataLayer.push(testData.carousel1); 57 | adobeDataLayer.push(testData.carousel1withNullAndUndefinedArrayItems); 58 | expect(adobeDataLayer.getState(), 'carousel 1 with removed items').toStrictEqual(testData.carousel1withRemovedArrayItems); 59 | 60 | adobeDataLayer.push(twoCarousels); 61 | expect(adobeDataLayer.getState(), 'carousel 1 with data, carousel 2 with data').toStrictEqual(twoCarousels); 62 | 63 | adobeDataLayer.push(testData.carousel1withUndefined); 64 | expect(adobeDataLayer.getState(), 'carousel 1 empty, carousel 2 with data').toStrictEqual(carousel1empty); 65 | 66 | adobeDataLayer.push(testData.carousel2withUndefined); 67 | expect(adobeDataLayer.getState(), 'carousel 1 empty, carousel 2 empty').toStrictEqual(twoCarouselsEmpty); 68 | 69 | adobeDataLayer.push(testData.carousel1); 70 | expect(adobeDataLayer.getState(), 'carousel 1 with data, carousel 2 empty').toStrictEqual(carousel2empty); 71 | 72 | adobeDataLayer.push(testData.carousel1withNull); 73 | expect(adobeDataLayer.getState(), 'carousel 1 empty, carousel 2 empty').toStrictEqual(twoCarouselsEmpty); 74 | 75 | adobeDataLayer.push(testData.carousel1); 76 | expect(adobeDataLayer.getState(), 'carousel 1 with data, carousel 2 empty').toStrictEqual(carousel2empty); 77 | }); 78 | 79 | test('push eventInfo without event', () => { 80 | adobeDataLayer.push({ eventInfo: 'test' }); 81 | 82 | expect(adobeDataLayer.getState(), 'no event info added').toStrictEqual({}); 83 | }); 84 | 85 | test('push invalid data type - string', () => { 86 | adobeDataLayer.push('test'); 87 | 88 | expect(adobeDataLayer.getState(), 'string is invalid data type and is not part of the state').toStrictEqual({}); 89 | }); 90 | 91 | test('push invalid data type - array of strings', () => { 92 | adobeDataLayer.push(['test1', 'test2']); 93 | 94 | expect(adobeDataLayer.getState(), 'string is invalid data type and is not part of the state').toStrictEqual({}); 95 | }); 96 | 97 | test('push initial data provided before data layer initialization', () => { 98 | adobeDataLayer = [testData.carousel1, testData.carousel2]; 99 | DataLayer.Manager({ dataLayer: adobeDataLayer }); 100 | 101 | expect(adobeDataLayer.getState(), 'all items are pushed to data layer state').toStrictEqual(merge({}, testData.carousel1, testData.carousel2)); 102 | }); 103 | 104 | test('invalid initial data triggers error', () => { 105 | // Catches console.error function which should be triggered by data layer during this test 106 | var consoleSpy = jest.spyOn(console, 'error').mockImplementation(); 107 | adobeDataLayer = ['test']; 108 | DataLayer.Manager({ dataLayer: adobeDataLayer }); 109 | 110 | expect(adobeDataLayer.getState(), 'initialization').toStrictEqual({}); 111 | expect(consoleSpy).toHaveBeenCalled(); 112 | // Restores console.error to default behaviour 113 | consoleSpy.mockRestore(); 114 | }); 115 | 116 | test('push on / off listeners is not allowed', () => { 117 | adobeDataLayer.push({ 118 | on: 'click', 119 | handler: jest.fn() 120 | }); 121 | adobeDataLayer.push({ 122 | off: 'click', 123 | handler: jest.fn() 124 | }); 125 | expect(adobeDataLayer.getState()).toStrictEqual({}); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for choosing to contribute to the Adobe Client Data Layer project, we really appreciate your time and effort! 😃🎊 4 | 5 | The following are a set of guidelines for contributing to the project. 6 | 7 | #### Contents 8 | 9 | * [Code of Conduct](#code-of-conduct) 10 | * [Ways of Contributing](#ways-of-contributing) 11 | * [Reporting Bugs](#reporting-bugs-) 🐛 12 | * [Contributing Code](#contributing-code-) 👾 13 | * [Reviewing Code](#reviewing-code-) 👀 14 | * [Documenting](#documenting-) 📜 15 | * [Issue Report Guidelines](#issue-report-guidelines) 16 | * [Contributor License Agreement](#contributor-license-agreement) 17 | * [Security Issues](#security-issues) 18 | 19 | ## Code of Conduct 20 | 21 | This project adheres to the Adobe [code of conduct](../CODE_OF_CONDUCT.md). By participating, 22 | you are expected to uphold this code. Please report unacceptable behavior to 23 | [Grp-opensourceoffice@adobe.com](mailto:Grp-opensourceoffice@adobe.com). 24 | 25 | ## Ways of Contributing 26 | 27 | There are many ways of contributing, from testing and reporting an issue to suggesting and coding full features. Below is a summary of some of the best ways to get involved. 28 | 29 | ### Reporting Bugs 🐛 30 | 31 | ##### Before Reporting a Bug 32 | 33 | * Have a quick search through the currently open [bug reports](https://github.com/adobe/adobe-client-data-layer/labels/bug) to see if the issue has already been reported. 34 | * Ensure that the issue is repeatable and that the actual behavior versus the expected results can be easily described. 35 | 36 | ##### Filing a Bug 37 | 38 | 1. Visit our [issue tracker on GitHub](https://github.com/adobe/adobe-client-data-layer/issues). 39 | 1. File a `New Issue`. 40 | 1. Ensure your issue follows the [issue report guidelines](#issue-report-guidelines). 41 | 1. Thanks for the report! The committers will get back to you in a timely manner, typically within one week. 42 | 43 | ### Contributing Code 👾 44 | 45 | High quality code is important to the project, and to keep it that way, all code submissions are reviewed by committers before being accepted. A close adherence to the guidelines below can help speed up the review process and increase the likelihood of the submission being accepted. 46 | 47 | ##### Before Contributing 48 | 49 | * Create a [bug report](#reporting-bugs-) issue summarizing the problem that you will be solving. This will help with early feedback and tracking. 50 | * Ensure you have [signed the Adobe Contributor License Agreement](http://opensource.adobe.com/cla.html). If you are an Adobe employee, you do not have to sign the CLA. 51 | 52 | ##### Contributing 53 | 54 | The project accepts contributions primarily using GitHub pull requests. This process: 55 | * Helps to maintain project quality 56 | * Engages the community in working towards commonly accepted solutions with peer review 57 | * Leads to a more meaningful and cleaner git history 58 | * Ensures sustainable code management 59 | 60 | Creating a pull request involves creating a fork of the project in your personal space, adding your new code in a branch and triggering a pull request. Check the GitHub [Using Pull Requests](https://help.github.com/articles/using-pull-requests) article on how to perform pull requests. 61 | 62 | Please base your pull request on the `master` branch and make sure to check you have incorporated or merged the latest changes! 63 | 64 | The title of the pull request typically matches that of the issue it fixes, see the [issue report guidelines](#issue-report-guidelines). 65 | Have a look at our [pull request template](./PULL_REQUEST_TEMPLATE.md) to see what is expected to be included in the pull request description. The same template is available when the pull request is triggered. 66 | 67 | ##### Your first contribution 68 | Would you would like to contribute to the project but don't have an issue in mind? Or are still fairly unfamiliar with the code? Then have a look at our [good first issues](https://github.com/adobe/adobe-client-data-layer/labels/good%20first%20issue), they are fairly simple starter issues that should only require a small amount of code and simple testing. 69 | 70 | ### Reviewing Code 👀 71 | 72 | Reviewing others' code contributions is another great way to contribute - more eyes on the code help to improve its overall quality. To review a pull request, check the [open pull requests](https://github.com/adobe/adobe-client-data-layer/pulls) for anything you can comment on. 73 | 74 | ### Documenting 📜 75 | 76 | We very much welcome issue reports or pull requests that improve our documentation pages. While the best effort is made to keep them error free, useful and up-to-date there are always things that could be improved. 77 | 78 | ## Issue Report Guidelines 79 | 80 | A well defined issue report will help in quickly understanding and replicating the problem faced, or the feature requested. Below are some guidelines on what to include when reporting an issue. 81 | 82 | ##### Title 83 | * **Descriptive** - Should be specific, well described and readable at a glance. 84 | * **Concise** - If the issue can't be easily described in a short title, then it is likely unfocused. 85 | * **Keyword-rich** - Including keywords can help with quickly finding the issue in the backlog. 86 | 87 | ##### Description 88 | 89 | See our [issue template](./ISSUE_TEMPLATE.md) for details on what is expected to be described. The same information is available when creating a new issue on GitHub. 90 | 91 | ##### Labels 92 | 93 | Once an issue is reported, the project committers will assign it a relevant label. You can see our [label list on GitHub](https://github.com/adobe/adobe-client-data-layer/labels) to better understand what each label means. 94 | 95 | ## Contributor License Agreement 96 | 97 | All third-party contributions to this project must be accompanied by a signed contributor 98 | license agreement. This gives Adobe permission to redistribute your contributions 99 | as part of the project. [Sign our CLA](https://opensource.adobe.com/cla.html). You 100 | only need to submit an Adobe CLA one time, so if you have submitted one previously, 101 | you are good to go! 102 | 103 | ## Security Issues 104 | 105 | Security issues shouldn't be reported on this issue tracker. Instead, [file an issue to our security experts](https://helpx.adobe.com/security/alertus.html). 106 | -------------------------------------------------------------------------------- /src/__tests__/scenarios/initialization.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | 13 | const testData = require('../testData'); 14 | const DataLayerManager = require('../../dataLayerManager'); 15 | const DataLayer = { Manager: DataLayerManager }; 16 | let adobeDataLayer; 17 | 18 | const createEventListener = function(dl, callback, options) { 19 | dl.addEventListener('adobeDataLayer:event', function(eventData) { 20 | expect(eventData, 'data layer object as an argument of callback').toEqual(testData.carousel1click); 21 | callback(); 22 | }, options); 23 | }; 24 | 25 | describe('Initialization', () => { 26 | describe('arguments', () => { 27 | test('empty array', () => { 28 | adobeDataLayer = []; 29 | DataLayer.Manager({ dataLayer: adobeDataLayer }); 30 | 31 | expect(adobeDataLayer.getState()).toEqual({}); 32 | }); 33 | 34 | test('array with data', () => { 35 | adobeDataLayer = [testData.carousel1]; 36 | DataLayer.Manager({ dataLayer: adobeDataLayer }); 37 | 38 | expect(adobeDataLayer.getState()).toEqual(testData.carousel1); 39 | }); 40 | 41 | test('wrong type', () => { 42 | adobeDataLayer = DataLayer.Manager({ dataLayer: {} }); 43 | 44 | expect(adobeDataLayer.getState()).toEqual({}); 45 | }); 46 | 47 | test('null', () => { 48 | adobeDataLayer = DataLayer.Manager(null); 49 | 50 | expect(adobeDataLayer.getState()).toEqual({}); 51 | }); 52 | }); 53 | 54 | describe('events', () => { 55 | beforeEach(() => { 56 | adobeDataLayer = []; 57 | }); 58 | 59 | test('scope past with early initialization', () => { 60 | const mockCallback = jest.fn(); 61 | DataLayer.Manager({ dataLayer: adobeDataLayer }); 62 | adobeDataLayer.push(function(dl) { createEventListener(dl, mockCallback, { scope: 'past' }); }); 63 | adobeDataLayer.push(testData.carousel1click); 64 | 65 | expect(mockCallback.mock.calls.length, 'callback not triggered').toBe(0); 66 | }); 67 | 68 | test('scope past with late initialization', () => { 69 | const mockCallback = jest.fn(); 70 | adobeDataLayer.push(function(dl) { createEventListener(dl, mockCallback, { scope: 'past' }); }); 71 | adobeDataLayer.push(testData.carousel1click); 72 | DataLayer.Manager({ dataLayer: adobeDataLayer }); 73 | 74 | expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(0); 75 | }); 76 | 77 | test('scope future with early initialization', () => { 78 | const mockCallback = jest.fn(); 79 | DataLayer.Manager({ dataLayer: adobeDataLayer }); 80 | adobeDataLayer.push(function(dl) { createEventListener(dl, mockCallback, { scope: 'future' }); }); 81 | adobeDataLayer.push(testData.carousel1click); 82 | 83 | expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); 84 | }); 85 | 86 | test('scope future with late initialization', () => { 87 | const mockCallback = jest.fn(); 88 | adobeDataLayer.push(function(dl) { createEventListener(dl, mockCallback, { scope: 'future' }); }); 89 | adobeDataLayer.push(testData.carousel1click); 90 | DataLayer.Manager({ dataLayer: adobeDataLayer }); 91 | 92 | expect(mockCallback.mock.calls.length, 'callback not triggered').toBe(1); 93 | }); 94 | }); 95 | 96 | describe('order', () => { 97 | beforeEach(() => { 98 | adobeDataLayer = []; 99 | }); 100 | 101 | test('listener > event > initialization', () => { 102 | const mockCallback = jest.fn(); 103 | adobeDataLayer.push(function(dl) { createEventListener(dl, mockCallback); }); 104 | adobeDataLayer.push(testData.carousel1click); 105 | DataLayer.Manager({ dataLayer: adobeDataLayer }); 106 | 107 | expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); 108 | }); 109 | 110 | test('event > listener > initialization', () => { 111 | const mockCallback = jest.fn(); 112 | adobeDataLayer.push(testData.carousel1click); 113 | adobeDataLayer.push(function(dl) { createEventListener(dl, mockCallback); }); 114 | DataLayer.Manager({ dataLayer: adobeDataLayer }); 115 | 116 | expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); 117 | }); 118 | 119 | test('listener > initialization > event', () => { 120 | const mockCallback = jest.fn(); 121 | adobeDataLayer.push(function(dl) { createEventListener(dl, mockCallback); }); 122 | DataLayer.Manager({ dataLayer: adobeDataLayer }); 123 | adobeDataLayer.push(testData.carousel1click); 124 | 125 | expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); 126 | }); 127 | 128 | test('event > initialization > listener', () => { 129 | const mockCallback = jest.fn(); 130 | adobeDataLayer.push(testData.carousel1click); 131 | DataLayer.Manager({ dataLayer: adobeDataLayer }); 132 | adobeDataLayer.push(function(dl) { createEventListener(dl, mockCallback); }); 133 | 134 | expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); 135 | }); 136 | 137 | test('initialization > event > listener', () => { 138 | const mockCallback = jest.fn(); 139 | DataLayer.Manager({ dataLayer: adobeDataLayer }); 140 | adobeDataLayer.push(testData.carousel1click); 141 | adobeDataLayer.push(function(dl) { createEventListener(dl, mockCallback); }); 142 | 143 | expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); 144 | }); 145 | 146 | test('initialization > listener > event', () => { 147 | const mockCallback = jest.fn(); 148 | DataLayer.Manager({ dataLayer: adobeDataLayer }); 149 | adobeDataLayer.push(function(dl) { createEventListener(dl, mockCallback); }); 150 | adobeDataLayer.push(testData.carousel1click); 151 | 152 | expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /src/__tests__/scenarios/utils.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | 13 | const testData = require('../testData'); 14 | const ITEM_CONSTRAINTS = require('../../itemConstraints'); 15 | const DataLayerManager = require('../../dataLayerManager'); 16 | const DataLayer = { Manager: DataLayerManager }; 17 | let adobeDataLayer; 18 | 19 | const ancestorRemoved = require('../../utils/ancestorRemoved'); 20 | const customMerge = require('../../utils/customMerge'); 21 | const dataMatchesContraints = require('../../utils/dataMatchesContraints'); 22 | const indexOfListener = require('../../utils/indexOfListener'); 23 | const listenerMatch = require('../../utils/listenerMatch'); 24 | 25 | const clearDL = function() { 26 | beforeEach(() => { 27 | adobeDataLayer = []; 28 | DataLayer.Manager({ dataLayer: adobeDataLayer }); 29 | }); 30 | }; 31 | 32 | describe('Utils', () => { 33 | clearDL(); 34 | 35 | describe('ancestorRemoved', () => { 36 | test('removed', () => { 37 | expect(ancestorRemoved(testData.componentNull, 'component.carousel')).toBeTruthy(); 38 | expect(ancestorRemoved(testData.componentNull, 'component.carousel.carousel1')).toBeTruthy(); 39 | }); 40 | test('not removed', () => { 41 | expect(ancestorRemoved(testData.carousel1, 'component.carousel')).toBeFalsy(); 42 | expect(ancestorRemoved(testData.carousel1, 'component.carousel.carousel1')).toBeFalsy(); 43 | }); 44 | }); 45 | 46 | describe('customMerge', () => { 47 | test('merges object', () => { 48 | const objectInitial = { prop1: 'foo' }; 49 | const objectSource = { prop2: 'bar' }; 50 | const objectFinal = { prop1: 'foo', prop2: 'bar' }; 51 | customMerge(objectInitial, objectSource); 52 | expect(objectInitial).toEqual(objectFinal); 53 | }); 54 | test('overrides with null and undefined', () => { 55 | const objectInitial = { prop1: 'foo', prop2: 'bar' }; 56 | const objectSource = { prop1: null, prop2: undefined }; 57 | const objectFinal = { prop1: null, prop2: null }; 58 | customMerge(objectInitial, objectSource); 59 | expect(objectInitial).toEqual(objectFinal); 60 | }); 61 | test('merge into null object', () => { 62 | const objectInitial = null; 63 | const objectSource = { prop1: 'foo' }; 64 | const objectFinal = null; 65 | customMerge(objectInitial, objectSource); 66 | expect(objectInitial).toEqual(objectFinal); 67 | }); 68 | }); 69 | 70 | describe('dataMatchesContraints', () => { 71 | test('event', () => { 72 | expect(dataMatchesContraints(testData.carousel1click, ITEM_CONSTRAINTS.event)).toBeTruthy(); 73 | }); 74 | test('listenerOn', () => { 75 | const listenerOn = { 76 | on: 'event', 77 | handler: () => {}, 78 | scope: 'future', 79 | path: 'component.carousel1' 80 | }; 81 | expect(dataMatchesContraints(listenerOn, ITEM_CONSTRAINTS.listenerOn)).toBeTruthy(); 82 | }); 83 | test('listenerOn with wrong scope (optional)', () => { 84 | const listenerOn = { 85 | on: 'event', 86 | handler: () => {}, 87 | scope: 'wrong', 88 | path: 'component.carousel1' 89 | }; 90 | expect(dataMatchesContraints(listenerOn, ITEM_CONSTRAINTS.listenerOn)).toBeFalsy(); 91 | }); 92 | test('listenerOn with wrong scope (not optional)', () => { 93 | const constraints = { 94 | scope: { 95 | type: 'string', 96 | values: ['past', 'future', 'all'] 97 | } 98 | }; 99 | const listenerOn = { 100 | on: 'event', 101 | handler: () => {}, 102 | scope: 'past' 103 | }; 104 | expect(dataMatchesContraints(listenerOn, constraints)).toBeTruthy(); 105 | }); 106 | test('listenerOff', () => { 107 | const listenerOff = { 108 | off: 'event', 109 | handler: () => {}, 110 | scope: 'future', 111 | path: 'component.carousel1' 112 | }; 113 | expect(dataMatchesContraints(listenerOff, ITEM_CONSTRAINTS.listenerOff)).toBeTruthy(); 114 | }); 115 | }); 116 | 117 | describe('indexOfListener', () => { 118 | test('indexOfListener', () => { 119 | const fct1 = jest.fn(); 120 | const fct2 = jest.fn(); 121 | const listener1 = { 122 | event: 'click', 123 | handler: fct1 124 | }; 125 | const listener2 = { 126 | event: 'click', 127 | handler: fct2 128 | }; 129 | const listener3 = { 130 | event: 'load', 131 | handler: fct1 132 | }; 133 | const listeners = { 134 | click: [listener1, listener2] 135 | }; 136 | expect(indexOfListener(listeners, listener2)).toBe(1); 137 | expect(indexOfListener(listeners, listener3)).toBe(-1); 138 | }); 139 | }); 140 | 141 | describe('listenerMatch', () => { 142 | test('event type', () => { 143 | const listener = { 144 | event: 'user loaded', 145 | handler: () => {}, 146 | scope: 'all', 147 | path: null 148 | }; 149 | const item = { 150 | config: { event: 'user loaded' }, 151 | type: 'event' 152 | }; 153 | expect(listenerMatch(listener, item)).toBeTruthy(); 154 | }); 155 | test('with correct path', () => { 156 | const listener = { 157 | event: 'viewed', 158 | handler: () => {}, 159 | scope: 'all', 160 | path: 'component.image.image1' 161 | }; 162 | const item = { 163 | config: testData.image1viewed, 164 | type: 'event', 165 | data: testData.image1 166 | }; 167 | expect(listenerMatch(listener, item)).toBeTruthy(); 168 | }); 169 | test('with incorrect path', () => { 170 | const listener = { 171 | event: 'viewed', 172 | handler: () => {}, 173 | scope: 'all', 174 | path: 'component.carousel' 175 | }; 176 | const item = { 177 | config: testData.image1viewed, 178 | type: 'event', 179 | data: testData.image1 180 | }; 181 | expect(listenerMatch(listener, item)).toBeFalsy(); 182 | }); 183 | test('wrong item type', () => { 184 | const listener = { 185 | event: 'user loaded', 186 | handler: () => {}, 187 | scope: 'all', 188 | path: null 189 | }; 190 | const item = { 191 | config: { event: 'user loaded' }, 192 | type: 'wrong' 193 | }; 194 | expect(listenerMatch(listener, item)).toBeFalsy(); 195 | }); 196 | test('item type == data', () => { 197 | const listener = { 198 | event: 'user loaded', 199 | handler: () => {} 200 | }; 201 | const item = { 202 | type: 'data' 203 | }; 204 | expect(listenerMatch(listener, item)).toBeFalsy(); 205 | }); 206 | }); 207 | }); 208 | -------------------------------------------------------------------------------- /src/dataLayerManager.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | 13 | import { get } from './utils/get.js'; 14 | 15 | const version = require('../version.json').version; 16 | const Item = require('./item'); 17 | const Listener = require('./listener'); 18 | const ListenerManager = require('./listenerManager'); 19 | const CONSTANTS = require('./constants'); 20 | const customMerge = require('./utils/customMerge'); 21 | 22 | /** 23 | * Manager 24 | * 25 | * @class Manager 26 | * @classdesc Data Layer manager that augments the passed data layer array and handles eventing. 27 | * @param {Object} config The Data Layer manager configuration. 28 | */ 29 | module.exports = function(config) { 30 | const _config = config || {}; 31 | let _dataLayer = []; 32 | let _preLoadedItems = []; 33 | let _state = {}; 34 | let _listenerManager; 35 | 36 | const DataLayerManager = { 37 | getState: function() { 38 | return _state; 39 | }, 40 | getDataLayer: function() { 41 | return _dataLayer; 42 | } 43 | }; 44 | 45 | _initialize(); 46 | _augment(); 47 | _processItems(); 48 | 49 | /** 50 | * Initializes the data layer. 51 | * 52 | * @private 53 | */ 54 | function _initialize() { 55 | if (!Array.isArray(_config.dataLayer)) { 56 | _config.dataLayer = []; 57 | } 58 | 59 | // Remove preloaded items from the data layer array and add those to the array of preloaded items 60 | _preLoadedItems = _config.dataLayer.splice(0, _config.dataLayer.length); 61 | _dataLayer = _config.dataLayer; 62 | _dataLayer.version = version; 63 | _state = {}; 64 | _listenerManager = ListenerManager(DataLayerManager); 65 | } 66 | 67 | /** 68 | * Updates the state with the item. 69 | * 70 | * @param {Item} item The item. 71 | * @private 72 | */ 73 | function _updateState(item) { 74 | _state = customMerge(_state, item.data); 75 | } 76 | 77 | /** 78 | * Augments the data layer Array Object, overriding: push() and adding getState(), addEventListener and removeEventListener. 79 | * 80 | * @private 81 | */ 82 | function _augment() { 83 | /** 84 | * Pushes one or more items to the data layer. 85 | * 86 | * @param {...ItemConfig} args The items to add to the data layer. 87 | * @returns {Number} The length of the data layer following push. 88 | */ 89 | _dataLayer.push = function(...args) { 90 | const pushArguments = args; 91 | const filteredArguments = args; 92 | 93 | Object.keys(pushArguments).forEach(function(key) { 94 | const itemConfig = pushArguments[key]; 95 | const item = Item(itemConfig); 96 | 97 | if (!item.valid) { 98 | _logInvalidItemError(item); 99 | delete filteredArguments[key]; 100 | } 101 | switch (item.type) { 102 | case CONSTANTS.itemType.DATA: 103 | case CONSTANTS.itemType.EVENT: { 104 | _processItem(item); 105 | break; 106 | } 107 | case CONSTANTS.itemType.FCTN: { 108 | delete filteredArguments[key]; 109 | _processItem(item); 110 | break; 111 | } 112 | case CONSTANTS.itemType.LISTENER_ON: 113 | case CONSTANTS.itemType.LISTENER_OFF: { 114 | delete filteredArguments[key]; 115 | } 116 | } 117 | }); 118 | 119 | if (filteredArguments[0]) { 120 | return Array.prototype.push.apply(this, filteredArguments); 121 | } 122 | }; 123 | 124 | /** 125 | * Returns a deep copy of the data layer state or of the object defined by the path. 126 | * 127 | * @param {Array|String} path The path of the property to get. 128 | * @returns {*} Returns a deep copy of the resolved value if a path is passed, a deep copy of the data layer state otherwise. 129 | */ 130 | _dataLayer.getState = function(path) { 131 | if (path) { 132 | return get(structuredClone(_state), path); 133 | } 134 | return structuredClone(_state); 135 | }; 136 | 137 | /** 138 | * Event listener callback. 139 | * 140 | * @callback eventListenerCallback A function that is called when the event of the specified type occurs. 141 | * @this {DataLayer} 142 | * @param {Object} event The event object pushed to the data layer that triggered the callback. 143 | */ 144 | 145 | /** 146 | * Sets up a function that will be called whenever the specified event is triggered. 147 | * 148 | * @param {String} type A case-sensitive string representing the event type to listen for. 149 | * @param {eventListenerCallback} callback A function that is called when the event of the specified type occurs. 150 | * @param {Object} [options] Optional characteristics of the event listener. 151 | * @param {String} [options.path] The path in the state object to filter the listening to. 152 | * @param {('past'|'future'|'all')} [options.scope] The timing to filter the listening to: 153 | * - {String} past The listener is triggered for past events only. 154 | * - {String} future The listener is triggered for future events only. 155 | * - {String} all The listener is triggered for both past and future events (default value). 156 | */ 157 | _dataLayer.addEventListener = function(type, callback, options) { 158 | const eventListenerItem = Item({ 159 | on: type, 160 | handler: callback, 161 | scope: options && options.scope, 162 | path: options && options.path 163 | }); 164 | 165 | _processItem(eventListenerItem); 166 | }; 167 | 168 | /** 169 | * Removes an event listener previously registered with addEventListener(). 170 | * 171 | * @param {String} type A case-sensitive string representing the event type to listen for. 172 | * @param {Function} [listener] Optional function that is to be removed. 173 | */ 174 | _dataLayer.removeEventListener = function(type, listener) { 175 | const eventListenerItem = Item({ 176 | off: type, 177 | handler: listener 178 | }); 179 | 180 | _processItem(eventListenerItem); 181 | }; 182 | } 183 | 184 | /** 185 | * Processes all items that already exist on the stack. 186 | * 187 | * @private 188 | */ 189 | function _processItems() { 190 | for (let i = 0; i < _preLoadedItems.length; i++) { 191 | _dataLayer.push(_preLoadedItems[i]); 192 | } 193 | } 194 | 195 | /** 196 | * Processes an item pushed to the stack. 197 | * 198 | * @param {Item} item The item to process. 199 | * @private 200 | */ 201 | function _processItem(item) { 202 | if (!item.valid) { 203 | _logInvalidItemError(item); 204 | return; 205 | } 206 | 207 | /** 208 | * Returns all items before the provided one. 209 | * 210 | * @param {Item} item The item. 211 | * @returns {Array} The items before. 212 | * @private 213 | */ 214 | function _getBefore(item) { 215 | if (!(_dataLayer.length === 0 || item.index > _dataLayer.length - 1)) { 216 | return _dataLayer.slice(0, item.index).map(itemConfig => Item(itemConfig)); 217 | } 218 | return []; 219 | } 220 | 221 | const typeProcessors = { 222 | data: function(item) { 223 | _updateState(item); 224 | _listenerManager.triggerListeners(item); 225 | }, 226 | fctn: function(item) { 227 | item.config.call(_dataLayer, _dataLayer); 228 | }, 229 | event: function(item) { 230 | if (item.data) { 231 | _updateState(item); 232 | } 233 | _listenerManager.triggerListeners(item); 234 | }, 235 | listenerOn: function(item) { 236 | const listener = Listener(item); 237 | switch (listener.scope) { 238 | case CONSTANTS.listenerScope.PAST: { 239 | for (const registeredItem of _getBefore(item)) { 240 | _listenerManager.triggerListener(listener, registeredItem); 241 | } 242 | break; 243 | } 244 | case CONSTANTS.listenerScope.FUTURE: { 245 | _listenerManager.register(listener); 246 | break; 247 | } 248 | case CONSTANTS.listenerScope.ALL: { 249 | const registered = _listenerManager.register(listener); 250 | if (registered) { 251 | for (const registeredItem of _getBefore(item)) { 252 | _listenerManager.triggerListener(listener, registeredItem); 253 | } 254 | } 255 | } 256 | } 257 | }, 258 | listenerOff: function(item) { 259 | _listenerManager.unregister(Listener(item)); 260 | } 261 | }; 262 | 263 | typeProcessors[item.type](item); 264 | } 265 | 266 | /** 267 | * Logs error for invalid item pushed to the data layer. 268 | * 269 | * @param {Item} item The invalid item. 270 | * @private 271 | */ 272 | function _logInvalidItemError(item) { 273 | const message = 'The following item cannot be handled by the data layer ' + 274 | 'because it does not have a valid format: ' + 275 | JSON.stringify(item.config); 276 | console.error(message); 277 | } 278 | 279 | return DataLayerManager; 280 | }; 281 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 Adobe 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/__tests__/scenarios/listeners.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | const testData = require('../testData'); 13 | const DataLayerManager = require('../../dataLayerManager'); 14 | const DataLayer = { Manager: DataLayerManager }; 15 | let adobeDataLayer; 16 | 17 | const clearDL = function() { 18 | beforeEach(() => { 19 | adobeDataLayer = []; 20 | DataLayer.Manager({ dataLayer: adobeDataLayer }); 21 | }); 22 | }; 23 | 24 | describe('Event listeners', () => { 25 | clearDL(); 26 | 27 | describe('types', () => { 28 | test('adobeDataLayer:change triggered by component push', () => { 29 | const mockCallback = jest.fn(); 30 | 31 | // edge case: unregister when no listener had been registered 32 | adobeDataLayer.removeEventListener('adobeDataLayer:change'); 33 | 34 | adobeDataLayer.addEventListener('adobeDataLayer:change', mockCallback); 35 | adobeDataLayer.push(testData.carousel1); 36 | expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); 37 | 38 | adobeDataLayer.removeEventListener('adobeDataLayer:change'); 39 | adobeDataLayer.push(testData.carousel2); 40 | expect(mockCallback.mock.calls.length, 'callback not triggered second time').toBe(1); 41 | }); 42 | 43 | test('adobeDataLayer:change triggered by event push', () => { 44 | const mockCallback = jest.fn(); 45 | 46 | adobeDataLayer.addEventListener('adobeDataLayer:change', mockCallback); 47 | adobeDataLayer.push(testData.carousel1click); 48 | expect(mockCallback.mock.calls.length).toBe(1); 49 | 50 | adobeDataLayer.removeEventListener('adobeDataLayer:change'); 51 | adobeDataLayer.push(testData.carousel1change); 52 | expect(mockCallback.mock.calls.length).toBe(1); 53 | }); 54 | 55 | test('adobeDataLayer:event triggered by event push', () => { 56 | const mockCallback = jest.fn(); 57 | 58 | adobeDataLayer.addEventListener('adobeDataLayer:event', mockCallback); 59 | adobeDataLayer.push(testData.carousel1click); 60 | expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); 61 | 62 | adobeDataLayer.removeEventListener('adobeDataLayer:event'); 63 | adobeDataLayer.push(testData.carousel1click); 64 | expect(mockCallback.mock.calls.length, 'callback not triggered second time').toBe(1); 65 | }); 66 | 67 | test('adobeDataLayer:change not triggered by event push', () => { 68 | const mockCallback = jest.fn(); 69 | 70 | adobeDataLayer.addEventListener('adobeDataLayer:change', mockCallback); 71 | adobeDataLayer.push({ 72 | event: 'page loaded' 73 | }); 74 | expect(mockCallback.mock.calls.length, 'callback not triggered').toBe(0); 75 | adobeDataLayer.push(testData.carousel1click); 76 | expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); 77 | }); 78 | 79 | test('custom event triggered by event push', () => { 80 | const mockCallback = jest.fn(); 81 | 82 | adobeDataLayer.addEventListener('carousel clicked', mockCallback); 83 | adobeDataLayer.push(testData.carousel1click); 84 | expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); 85 | 86 | adobeDataLayer.removeEventListener('carousel clicked'); 87 | adobeDataLayer.push(testData.carousel1click); 88 | expect(mockCallback.mock.calls.length, 'callback not triggered second time').toBe(1); 89 | }); 90 | }); 91 | 92 | describe('scopes', () => { 93 | test('past', () => { 94 | const mockCallback = jest.fn(); 95 | 96 | adobeDataLayer.push(testData.carousel1click); 97 | adobeDataLayer.addEventListener('carousel clicked', mockCallback, { scope: 'past' }); 98 | expect(mockCallback.mock.calls.length, 'callback triggered once').toBe(1); 99 | 100 | adobeDataLayer.push(testData.carousel1click); 101 | expect(mockCallback.mock.calls.length, 'callback not triggered second time').toBe(1); 102 | }); 103 | 104 | test('future', () => { 105 | const mockCallback = jest.fn(); 106 | 107 | adobeDataLayer.push(testData.carousel1click); 108 | adobeDataLayer.addEventListener('carousel clicked', mockCallback, { scope: 'future' }); 109 | expect(mockCallback.mock.calls.length, 'callback not triggered first time').toBe(0); 110 | 111 | adobeDataLayer.push(testData.carousel1click); 112 | expect(mockCallback.mock.calls.length, 'callback triggered only second time').toBe(1); 113 | }); 114 | 115 | test('all', () => { 116 | const mockCallback = jest.fn(); 117 | 118 | adobeDataLayer.push(testData.carousel1click); 119 | adobeDataLayer.addEventListener('carousel clicked', mockCallback, { scope: 'all' }); 120 | expect(mockCallback.mock.calls.length, 'callback triggered first time').toBe(1); 121 | 122 | adobeDataLayer.push(testData.carousel1click); 123 | expect(mockCallback.mock.calls.length, 'callback triggered second time').toBe(2); 124 | }); 125 | 126 | test('undefined (defaults to all)', () => { 127 | const mockCallback = jest.fn(); 128 | 129 | adobeDataLayer.push(testData.carousel1click); 130 | adobeDataLayer.addEventListener('carousel clicked', mockCallback); 131 | expect(mockCallback.mock.calls.length, 'callback triggered first time').toBe(1); 132 | 133 | adobeDataLayer.push(testData.carousel1click); 134 | expect(mockCallback.mock.calls.length, 'callback triggered second time').toBe(2); 135 | }); 136 | 137 | test('invalid', () => { 138 | const mockCallback = jest.fn(); 139 | adobeDataLayer.addEventListener('carousel clicked', mockCallback, { scope: 'invalid' }); 140 | adobeDataLayer.push(testData.carousel1click); 141 | expect(mockCallback.mock.calls.length).toBe(0); 142 | }); 143 | }); 144 | 145 | describe('duplications', () => { 146 | test('register a handler that has already been registered', () => { 147 | const mockCallback = jest.fn(); 148 | 149 | adobeDataLayer.push(testData.carousel1click); 150 | adobeDataLayer.addEventListener('carousel clicked', mockCallback); 151 | adobeDataLayer.addEventListener('carousel clicked', mockCallback); 152 | 153 | // only one listener is registered 154 | 155 | expect(mockCallback.mock.calls.length, 'callback triggered just once').toBe(1); 156 | adobeDataLayer.push(testData.carousel1click); 157 | expect(mockCallback.mock.calls.length, 'callback triggered just once (second time)').toBe(2); 158 | }); 159 | 160 | test('register a handler (with a static function) that has already been registered', () => { 161 | const mockCallback = jest.fn(); 162 | adobeDataLayer.addEventListener('carousel clicked', function() { 163 | mockCallback(); 164 | }); 165 | adobeDataLayer.addEventListener('carousel clicked', function() { 166 | mockCallback(); 167 | }); 168 | 169 | // both listeners are registered 170 | 171 | adobeDataLayer.push(testData.carousel1click); 172 | expect(mockCallback.mock.calls.length, 'callback triggered twice').toBe(2); 173 | }); 174 | }); 175 | 176 | describe('with path', () => { 177 | const mockCallback = jest.fn(); 178 | const changeEventArguments = ['adobeDataLayer:change', mockCallback, { path: 'component.image' }]; 179 | 180 | beforeEach(() => { 181 | mockCallback.mockClear(); 182 | }); 183 | 184 | test('adobeDataLayer:change triggers on component.image', () => { 185 | adobeDataLayer.addEventListener.apply(adobeDataLayer, changeEventArguments); 186 | adobeDataLayer.push(testData.carousel1); 187 | expect(mockCallback.mock.calls.length).toBe(0); 188 | 189 | adobeDataLayer.push(testData.image1); 190 | expect(mockCallback.mock.calls.length).toBe(1); 191 | }); 192 | 193 | test('adobeDataLayer:change triggers on component.image with data', () => { 194 | adobeDataLayer.addEventListener.apply(adobeDataLayer, changeEventArguments); 195 | adobeDataLayer.push(testData.carousel1change); 196 | expect(mockCallback.mock.calls.length).toBe(0); 197 | 198 | adobeDataLayer.push(testData.image1change); 199 | expect(mockCallback.mock.calls.length).toBe(1); 200 | }); 201 | 202 | test('event triggers when the ancestor is removed with null', () => { 203 | adobeDataLayer.addEventListener.apply(adobeDataLayer, changeEventArguments); 204 | adobeDataLayer.push(testData.componentNull); 205 | expect(mockCallback.mock.calls.length).toBe(1); 206 | }); 207 | 208 | test('event triggers when the ancestor is removed with undefined', () => { 209 | adobeDataLayer.addEventListener.apply(adobeDataLayer, changeEventArguments); 210 | adobeDataLayer.push(testData.componentUndefined); 211 | expect(mockCallback.mock.calls.length).toBe(1); 212 | }); 213 | 214 | test('event does not trigger when the ancestor does not exist', () => { 215 | const changeEventArguments1 = ['adobeDataLayer:change', mockCallback, { path: 'component1.image' }]; 216 | adobeDataLayer.addEventListener.apply(adobeDataLayer, changeEventArguments1); 217 | adobeDataLayer.push(testData.componentUndefined); 218 | expect(mockCallback.mock.calls.length).toBe(0); 219 | }); 220 | 221 | test('viewed event triggers on component.image', () => { 222 | adobeDataLayer.addEventListener('viewed', mockCallback, { path: 'component.image' }); 223 | adobeDataLayer.push(testData.carousel1viewed); 224 | expect(mockCallback.mock.calls.length).toBe(0); 225 | 226 | adobeDataLayer.push(testData.image1viewed); 227 | expect(mockCallback.mock.calls.length).toBe(1); 228 | }); 229 | 230 | test('viewed event does not trigger on a non existing object', () => { 231 | adobeDataLayer.addEventListener('viewed', mockCallback, { path: 'component.image.undefined' }); 232 | adobeDataLayer.push(testData.image1viewed); 233 | expect(mockCallback.mock.calls.length).toBe(0); 234 | }); 235 | 236 | test('custom event triggers on all components', () => { 237 | adobeDataLayer.push(testData.carousel1change); 238 | adobeDataLayer.push(testData.image1change); 239 | adobeDataLayer.addEventListener('adobeDataLayer:change', mockCallback, { path: 'component' }); 240 | adobeDataLayer.push(testData.image1change); 241 | expect(mockCallback.mock.calls.length).toBe(3); 242 | }); 243 | }); 244 | 245 | describe('unregister', () => { 246 | test('one handler', () => { 247 | const mockCallback = jest.fn(); 248 | 249 | adobeDataLayer.push(testData.carousel1click); 250 | adobeDataLayer.addEventListener('carousel clicked', mockCallback, { scope: 'all' }); 251 | expect(mockCallback.mock.calls.length).toBe(1); 252 | 253 | adobeDataLayer.removeEventListener('carousel clicked', mockCallback); 254 | adobeDataLayer.push(testData.carousel1click); 255 | expect(mockCallback.mock.calls.length).toBe(1); 256 | }); 257 | 258 | test('handler with an anonymous function', () => { 259 | const mockCallback = jest.fn(); 260 | 261 | adobeDataLayer.addEventListener('carousel clicked', function() { mockCallback(); }); 262 | adobeDataLayer.removeEventListener('carousel clicked', function() { mockCallback(); }); 263 | 264 | // an anonymous handler cannot be unregistered (similar to EventTarget.addEventListener()) 265 | 266 | adobeDataLayer.push(testData.carousel1click); 267 | expect(mockCallback.mock.calls.length).toBe(1); 268 | }); 269 | 270 | test('multiple handlers', () => { 271 | const mockCallback1 = jest.fn(); 272 | const mockCallback2 = jest.fn(); 273 | const userLoadedEvent = { event: 'user loaded' }; 274 | 275 | adobeDataLayer.addEventListener('user loaded', mockCallback1); 276 | adobeDataLayer.addEventListener('user loaded', mockCallback2); 277 | adobeDataLayer.push(userLoadedEvent); 278 | 279 | expect(mockCallback1.mock.calls.length).toBe(1); 280 | expect(mockCallback2.mock.calls.length).toBe(1); 281 | 282 | adobeDataLayer.removeEventListener('user loaded'); 283 | adobeDataLayer.push(userLoadedEvent); 284 | 285 | expect(mockCallback1.mock.calls.length).toBe(1); 286 | expect(mockCallback2.mock.calls.length).toBe(1); 287 | }); 288 | }); 289 | }); 290 | -------------------------------------------------------------------------------- /examples/js/datalayer.mocks.1.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Adobe. All rights reserved. 3 | This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. You may obtain a copy 5 | of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under 8 | the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | OF ANY KIND, either express or implied. See the License for the specific language 10 | governing permissions and limitations under the License. 11 | */ 12 | /* global console, window, dataLayer, CustomEvent */ 13 | (function() { 14 | 'use strict'; 15 | 16 | /* eslint no-console: "off" */ 17 | /* eslint no-unused-vars: "off" */ 18 | 19 | adobeDataLayer.push(function(adobeDataLayer) { 20 | 21 | // ----------------------------------------------------------------------------------------------------------------- 22 | // Test case 1: add data 23 | // ----------------------------------------------------------------------------------------------------------------- 24 | 25 | adobeDataLayer.push({ 26 | component: { 27 | carousel: { 28 | carousel1: { 29 | id: '/content/mysite/en/home/jcr:content/root/carousel1', 30 | items: {} 31 | } 32 | } 33 | } 34 | }); 35 | 36 | const id1 = adobeDataLayer.getState().component.carousel.carousel1.id; 37 | if (id1 !== '/content/mysite/en/home/jcr:content/root/carousel1') { 38 | console.error('FAILs: test case 1 "add data"'); 39 | } else { 40 | console.info('SUCCESS: test case 1 "add data"'); 41 | } 42 | 43 | // ----------------------------------------------------------------------------------------------------------------- 44 | // Test case 2: remove data 45 | // ----------------------------------------------------------------------------------------------------------------- 46 | 47 | adobeDataLayer.push({ 48 | component: { 49 | carousel: { 50 | carousel1: undefined 51 | } 52 | } 53 | }); 54 | 55 | if (adobeDataLayer.getState().component.carousel.carousel1) { 56 | console.error('FAILS: test case 2 "remove data"'); 57 | } else { 58 | console.info('SUCCESS: test case 2 "remove data"'); 59 | } 60 | 61 | // ----------------------------------------------------------------------------------------------------------------- 62 | // Test case 3: add event with data 63 | // ----------------------------------------------------------------------------------------------------------------- 64 | 65 | adobeDataLayer.push({ 66 | event: 'carousel clicked', 67 | component: { 68 | carousel: { 69 | carousel3: { 70 | id: '/content/mysite/en/home/jcr:content/root/carousel3', 71 | items: {} 72 | } 73 | } 74 | } 75 | }); 76 | 77 | const id3 = adobeDataLayer.getState().component.carousel.carousel3.id; 78 | if (id3 !== '/content/mysite/en/home/jcr:content/root/carousel3') { 79 | console.error('FAILS: test case 3 "add event with data"'); 80 | } else { 81 | console.info('SUCCESS: test case 3 "add event with data"'); 82 | } 83 | 84 | // ----------------------------------------------------------------------------------------------------------------- 85 | // test case 4: listener on: adobeDataLayer:change 86 | // ----------------------------------------------------------------------------------------------------------------- 87 | 88 | let success4 = false; 89 | 90 | adobeDataLayer.addEventListener('adobeDataLayer:change', function(event) { 91 | success4 = true; 92 | }); 93 | 94 | adobeDataLayer.push({ 95 | component: { 96 | carousel: { 97 | carousel4: { 98 | id: '/content/mysite/en/home/jcr:content/root/carousel4', 99 | items: {} 100 | } 101 | } 102 | } 103 | }); 104 | 105 | if (!success4) { 106 | console.error('FAILS: test case 4 "listener on: adobeDataLayer:change"'); 107 | } else { 108 | console.info('SUCCESS: test case 4 "listener on: adobeDataLayer:change"'); 109 | } 110 | 111 | // ----------------------------------------------------------------------------------------------------------------- 112 | // test case 5: listener on: adobeDataLayer:event 113 | // ----------------------------------------------------------------------------------------------------------------- 114 | 115 | let success5 = false; 116 | 117 | adobeDataLayer.addEventListener('adobeDataLayer:event', function(event) { 118 | success5 = true; 119 | }); 120 | 121 | adobeDataLayer.push({ 122 | event: 'adobeDataLayer:event', 123 | eventInfo: { 124 | reference: '/content/mysite/en/home/jcr:content/root/carousel5' 125 | } 126 | }); 127 | 128 | if (!success5) { 129 | console.error('FAILS: test case 5 "listener on: adobeDataLayer:event"'); 130 | } else { 131 | console.info('SUCCESS: test case 5 "listener on: adobeDataLayer:event"'); 132 | } 133 | 134 | // ----------------------------------------------------------------------------------------------------------------- 135 | // test case 6: listener on: custom event 136 | // ----------------------------------------------------------------------------------------------------------------- 137 | 138 | let success6 = false; 139 | 140 | adobeDataLayer.addEventListener('carousel clicked', function(event) { 141 | success6 = true; 142 | }); 143 | 144 | adobeDataLayer.push({ 145 | event: 'carousel clicked', 146 | eventInfo: { 147 | reference: '/content/mysite/en/home/jcr:content/root/carousel6' 148 | } 149 | }); 150 | 151 | if (!success6) { 152 | console.error('FAILS: test case 6 "listener on: custom event"'); 153 | } else { 154 | console.info('SUCCESS: test case 6 "listener on: custom event"'); 155 | } 156 | 157 | // ----------------------------------------------------------------------------------------------------------------- 158 | // test case 7: listener on: scope = past 159 | // ----------------------------------------------------------------------------------------------------------------- 160 | 161 | let success7a = false; 162 | 163 | adobeDataLayer.push({ 164 | event: 'carousel 7a clicked', 165 | eventInfo: { 166 | reference: '/content/mysite/en/home/jcr:content/root/carousel7a' 167 | } 168 | }); 169 | 170 | adobeDataLayer.addEventListener('carousel 7a clicked', function(event) { 171 | success7a = true; 172 | }, {scope: 'past'}); 173 | 174 | if (!success7a) { 175 | console.error('FAILS: test case 7a "listener on: scope = past"'); 176 | } else { 177 | console.info('SUCCESS: test case 7a "listener on: scope = past"'); 178 | } 179 | 180 | let success7b = true; 181 | 182 | adobeDataLayer.addEventListener('carousel 7b clicked', function(event) { 183 | success7b = false; 184 | }, {scope: 'past'}); 185 | 186 | adobeDataLayer.push({ 187 | event: 'carousel 7b clicked', 188 | eventInfo: { 189 | reference: '/content/mysite/en/home/jcr:content/root/carousel7b' 190 | } 191 | }); 192 | 193 | if (!success7b) { 194 | console.error('FAILS: test case 7b "listener on: scope = past"'); 195 | } else { 196 | console.info('SUCCESS: test case 7b "listener on: scope = past"'); 197 | } 198 | 199 | // ----------------------------------------------------------------------------------------------------------------- 200 | // test case 8: listener on: scope = future 201 | // ----------------------------------------------------------------------------------------------------------------- 202 | 203 | let success8a = true; 204 | 205 | adobeDataLayer.push({ 206 | event: 'carousel 8a clicked', 207 | eventInfo: { 208 | reference: '/content/mysite/en/home/jcr:content/root/carousel8a' 209 | } 210 | }); 211 | 212 | adobeDataLayer.addEventListener('carousel 8a clicked', function(event) { 213 | success8a = false; 214 | }, {scope: 'future'}); 215 | 216 | if (!success8a) { 217 | console.error('FAILS: test case 8a "listener on: scope = future"'); 218 | } else { 219 | console.info('SUCCESS: test case 8a "listener on: scope = future"'); 220 | } 221 | 222 | let success8b = false; 223 | 224 | adobeDataLayer.addEventListener('carousel 8b clicked', function(event) { 225 | success8b = true; 226 | }, {scope: 'future'}); 227 | 228 | adobeDataLayer.push({ 229 | event: 'carousel 8b clicked', 230 | eventInfo: { 231 | reference: '/content/mysite/en/home/jcr:content/root/carousel8b' 232 | } 233 | }); 234 | 235 | if (!success8b) { 236 | console.error('FAILS: test case 8b "listener on: scope = future"'); 237 | } else { 238 | console.info('SUCCESS: test case 8b "listener on: scope = future"'); 239 | } 240 | 241 | // ----------------------------------------------------------------------------------------------------------------- 242 | // test case 9: listener on: scope = all 243 | // ----------------------------------------------------------------------------------------------------------------- 244 | 245 | let success9a = false; 246 | 247 | adobeDataLayer.push({ 248 | event: 'carousel 9a clicked', 249 | eventInfo: { 250 | reference: '/content/mysite/en/home/jcr:content/root/carousel9a' 251 | } 252 | }); 253 | 254 | adobeDataLayer.addEventListener('carousel 9a clicked', function(event) { 255 | success9a = true; 256 | }, {scope: 'all'}); 257 | 258 | if (!success9a) { 259 | console.error('FAILS: test case 9a "listener on: scope = all"'); 260 | } else { 261 | console.info('SUCCESS: test case 9a "listener on: scope = all"'); 262 | } 263 | 264 | let success9b = false; 265 | 266 | adobeDataLayer.addEventListener('carousel 9b clicked', function(event) { 267 | success9b = true; 268 | }, {scope: 'all'}); 269 | 270 | adobeDataLayer.push({ 271 | event: 'carousel 9b clicked', 272 | eventInfo: { 273 | reference: '/content/mysite/en/home/jcr:content/root/carousel9b' 274 | } 275 | }); 276 | 277 | if (!success9b) { 278 | console.error('FAILS: test case 9b "listener on: scope = all"'); 279 | } else { 280 | console.info('SUCCESS: test case 9b "listener on: scope = all"'); 281 | } 282 | 283 | // ----------------------------------------------------------------------------------------------------------------- 284 | // test case 10: listener on: scope = undefined (default to 'future') 285 | // ----------------------------------------------------------------------------------------------------------------- 286 | 287 | let success10a = true; 288 | 289 | adobeDataLayer.push({ 290 | event: 'carousel 10a clicked', 291 | eventInfo: { 292 | reference: '/content/mysite/en/home/jcr:content/root/carousel10a' 293 | } 294 | }); 295 | 296 | adobeDataLayer.addEventListener('carousel 10a clicked', function(event) { 297 | success10a = false; 298 | }, {scope: 'future'}); 299 | 300 | if (!success10a) { 301 | console.error('FAILS: test case 10a "listener on: scope = undefined"'); 302 | } else { 303 | console.info('SUCCESS: test case 10a "listener on: scope = undefined"'); 304 | } 305 | 306 | let success10b = false; 307 | 308 | adobeDataLayer.addEventListener('carousel 10b clicked', function(event) { 309 | success10b = true; 310 | }); 311 | 312 | adobeDataLayer.push({ 313 | event: 'carousel 10b clicked', 314 | eventInfo: { 315 | reference: '/content/mysite/en/home/jcr:content/root/carousel10b' 316 | } 317 | }); 318 | 319 | if (!success10b) { 320 | console.error('FAILS: test case 10b "listener on: scope = undefined"'); 321 | } else { 322 | console.info('SUCCESS: test case 10b "listener on: scope = undefined"'); 323 | } 324 | 325 | // ----------------------------------------------------------------------------------------------------------------- 326 | // test case 11: listener off 327 | // ----------------------------------------------------------------------------------------------------------------- 328 | 329 | let success11 = true; 330 | 331 | adobeDataLayer.addEventListener('carousel 11a clicked', function(event) { 332 | success11 = false; 333 | }, {scope: 'future'}); 334 | 335 | adobeDataLayer.push({ 336 | event: 'carousel 11a clicked', 337 | eventInfo: { 338 | id: '/content/mysite/en/home/jcr:content/root/carousel11a' 339 | } 340 | }); 341 | 342 | // success11 should be: false: we force it to true: 343 | success11 = true; 344 | 345 | adobeDataLayer.removeEventListener('carousel 11a clicked'); 346 | 347 | adobeDataLayer.push({ 348 | event: 'carousel 11a clicked', 349 | eventInfo: { 350 | reference: '/content/mysite/en/home/jcr:content/root/carousel11a' 351 | } 352 | }); 353 | 354 | if (!success11) { 355 | console.error('FAILS: test case 11 "listener off"'); 356 | } else { 357 | console.log('SUCCESS: test case 11 "listener off"'); 358 | } 359 | }); 360 | 361 | })(); 362 | --------------------------------------------------------------------------------