├── 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 | [](https://www.npmjs.com/package/@adobe/adobe-client-data-layer)
4 | [](https://bundlephobia.com/result?p=@adobe/adobe-client-data-layer)
5 | [](https://lgtm.com/projects/g/adobe/adobe-client-data-layer)
6 | [](https://app.circleci.com/pipelines/github/adobe/adobe-client-data-layer)
7 | [](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 |
--------------------------------------------------------------------------------