diff --git a/rollup.test.config.js b/rollup.test.config.js index 79ae3600b..33abab56f 100644 --- a/rollup.test.config.js +++ b/rollup.test.config.js @@ -48,6 +48,7 @@ if (argv.reporters && argv.reporters.split(",").includes("coverage")) { module.exports = { output: { + sourcemap: true, format: "iife", // Allow non-IE browsers and IE11 // document.documentMode was added in IE8, and is specific to IE. diff --git a/src/components/Personalization/createApplyPropositions.js b/src/components/Personalization/createApplyPropositions.js index dfc840a6d..15b00afb1 100644 --- a/src/components/Personalization/createApplyPropositions.js +++ b/src/components/Personalization/createApplyPropositions.js @@ -10,15 +10,17 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import composePersonalizationResultingObject from "./utils/composePersonalizationResultingObject"; import { isNonEmptyArray, isObject } from "../../utils"; import { DOM_ACTION, HTML_CONTENT_ITEM } from "./constants/schema"; import PAGE_WIDE_SCOPE from "../../constants/pageWideScope"; -import { EMPTY_PROPOSITIONS } from "./validateApplyPropositionsOptions"; +import { + buildReturnedPropositions, + createProposition +} from "./handlers/proposition"; -export const SUPPORTED_SCHEMAS = [DOM_ACTION, HTML_CONTENT_ITEM]; +const SUPPORTED_SCHEMAS = [DOM_ACTION, HTML_CONTENT_ITEM]; -export default ({ executeDecisions }) => { +export default ({ render }) => { const filterItemsPredicate = item => SUPPORTED_SCHEMAS.indexOf(item.schema) > -1; @@ -71,20 +73,16 @@ export default ({ executeDecisions }) => { .filter(proposition => isNonEmptyArray(proposition.items)); }; - const applyPropositions = ({ propositions, metadata }) => { + return ({ propositions, metadata = {} }) => { const propositionsToExecute = preparePropositions({ propositions, metadata - }); - return executeDecisions(propositionsToExecute).then(() => { - return composePersonalizationResultingObject(propositionsToExecute, true); - }); - }; + }).map(proposition => createProposition(proposition, true)); - return ({ propositions, metadata = {} }) => { - if (isNonEmptyArray(propositions)) { - return applyPropositions({ propositions, metadata }); - } - return Promise.resolve(EMPTY_PROPOSITIONS); + render(propositionsToExecute); + + return { + propositions: buildReturnedPropositions(propositionsToExecute) + }; }; }; diff --git a/src/components/Personalization/createAutoRenderingHandler.js b/src/components/Personalization/createAutoRenderingHandler.js deleted file mode 100644 index 0e21c0954..000000000 --- a/src/components/Personalization/createAutoRenderingHandler.js +++ /dev/null @@ -1,77 +0,0 @@ -/* -Copyright 2021 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ - -import addRenderAttemptedToDecisions from "./utils/addRenderAttemptedToDecisions"; -import isNonEmptyArray from "../../utils/isNonEmptyArray"; - -const getPropositions = ({ viewCache, viewName, pageWideScopeDecisions }) => { - if (!viewName) { - return { pageWideScopeDecisions, viewPropositions: [] }; - } - - return viewCache.getView(viewName).then(viewPropositions => { - return { pageWideScopeDecisions, viewPropositions }; - }); -}; - -export default ({ viewCache, executeDecisions, showContainers, collect }) => { - return ({ viewName, pageWideScopeDecisions, nonAutoRenderableDecisions }) => { - return Promise.resolve(pageWideScopeDecisions) - .then(propositions => - getPropositions({ - viewCache, - viewName, - executeDecisions, - pageWideScopeDecisions: propositions - }) - ) - .then(propositions => { - executeDecisions(propositions.pageWideScopeDecisions).then( - decisionsMeta => { - if (isNonEmptyArray(decisionsMeta)) { - collect({ decisionsMeta }); - } - } - ); - - if (viewName) { - executeDecisions(propositions.viewPropositions).then( - decisionsMeta => { - collect({ decisionsMeta, viewName }); - } - ); - } - - showContainers(); - - return [ - ...propositions.pageWideScopeDecisions, - ...propositions.viewPropositions - ]; - }) - .then(renderablePropositions => { - return { - decisions: [...nonAutoRenderableDecisions], - propositions: [ - ...addRenderAttemptedToDecisions({ - decisions: renderablePropositions, - renderAttempted: true - }), - ...addRenderAttemptedToDecisions({ - decisions: nonAutoRenderableDecisions, - renderAttempted: false - }) - ] - }; - }); - }; -}; diff --git a/src/components/Personalization/createComponent.js b/src/components/Personalization/createComponent.js index 9abd6110c..226db3def 100644 --- a/src/components/Personalization/createComponent.js +++ b/src/components/Personalization/createComponent.js @@ -10,7 +10,7 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import { noop, defer } from "../../utils"; +import { noop } from "../../utils"; import createPersonalizationDetails from "./createPersonalizationDetails"; import { AUTHORING_ENABLED } from "./constants/loggerMessage"; import validateApplyPropositionsOptions from "./validateApplyPropositionsOptions"; @@ -60,17 +60,18 @@ export default ({ decisionScopes, personalization, event, - viewCache, + isCacheInitialized: viewCache.isInitialized(), logger }); if (personalizationDetails.shouldFetchData()) { - const decisionsDeferred = defer(); + const cacheUpdate = viewCache.createCacheUpdate( + personalizationDetails.getViewName() + ); + onRequestFailure(() => cacheUpdate.cancel()); - viewCache.storeViews(decisionsDeferred.promise); - onRequestFailure(() => decisionsDeferred.reject()); fetchDataHandler({ - decisionsDeferred, + cacheUpdate, personalizationDetails, event, onResponse diff --git a/src/components/Personalization/createExecuteDecisions.js b/src/components/Personalization/createExecuteDecisions.js deleted file mode 100644 index fff2ee8f2..000000000 --- a/src/components/Personalization/createExecuteDecisions.js +++ /dev/null @@ -1,82 +0,0 @@ -/* -Copyright 2020 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ - -import { assign, flatMap } from "../../utils"; - -const DEFAULT_ACTION_TYPE = "defaultContent"; - -const identity = item => item; - -const getItemMeta = (item, decisionMeta) => - item.characteristics && item.characteristics.trackingLabel - ? assign( - { trackingLabel: item.characteristics.trackingLabel }, - decisionMeta - ) - : decisionMeta; - -const buildActions = decision => { - const decisionMeta = { - id: decision.id, - scope: decision.scope, - scopeDetails: decision.scopeDetails - }; - - return decision.items.map(item => - assign({ type: DEFAULT_ACTION_TYPE }, item.data, { - meta: getItemMeta(item, decisionMeta) - }) - ); -}; - -const processMetas = (logger, actionResults) => { - const results = flatMap(actionResults, identity); - const finalMetas = []; - const set = new Set(); - - results.forEach(item => { - // for click actions we don't return an item - if (!item) { - return; - } - if (item.error) { - logger.warn(item); - return; - } - - const { meta } = item; - - if (set.has(meta.id)) { - return; - } - - set.add(meta.id); - finalMetas.push(meta); - }); - - return finalMetas; -}; - -export default ({ modules, logger, executeActions }) => { - return decisions => { - const actionResultsPromises = decisions.map(decision => { - const actions = buildActions(decision); - - return executeActions(actions, modules, logger); - }); - return Promise.all(actionResultsPromises) - .then(results => processMetas(logger, results)) - .catch(error => { - logger.error(error); - }); - }; -}; diff --git a/src/components/Personalization/createFetchDataHandler.js b/src/components/Personalization/createFetchDataHandler.js index df45aafc6..8bb1a0ef8 100644 --- a/src/components/Personalization/createFetchDataHandler.js +++ b/src/components/Personalization/createFetchDataHandler.js @@ -1,5 +1,5 @@ /* -Copyright 2020 Adobe. All rights reserved. +Copyright 2023 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -9,21 +9,46 @@ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTA OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ +import { + buildReturnedPropositions, + buildReturnedDecisions +} from "./handlers/proposition"; + +const DECISIONS_HANDLE = "personalization:decisions"; export default ({ prehidingStyle, - responseHandler, + showContainers, hideContainers, - mergeQuery + mergeQuery, + collect, + render }) => { - return ({ decisionsDeferred, personalizationDetails, event, onResponse }) => { + return ({ cacheUpdate, personalizationDetails, event, onResponse }) => { if (personalizationDetails.isRenderDecisions()) { hideContainers(prehidingStyle); } mergeQuery(event, personalizationDetails.createQueryDetails()); - onResponse(({ response }) => - responseHandler({ decisionsDeferred, personalizationDetails, response }) - ); + onResponse(({ response }) => { + const handles = response.getPayloadsByType(DECISIONS_HANDLE); + const propositions = cacheUpdate.update(handles); + if (personalizationDetails.isRenderDecisions()) { + render(propositions).then(decisionsMeta => { + showContainers(); + if (decisionsMeta.length > 0) { + collect({ + decisionsMeta, + viewName: personalizationDetails.getViewName() + }); + } + }); + } + + return { + propositions: buildReturnedPropositions(propositions), + decisions: buildReturnedDecisions(propositions) + }; + }); }; }; diff --git a/src/components/Personalization/createNonRenderingHandler.js b/src/components/Personalization/createNonRenderingHandler.js deleted file mode 100644 index 3c9c61fb5..000000000 --- a/src/components/Personalization/createNonRenderingHandler.js +++ /dev/null @@ -1,54 +0,0 @@ -/* -Copyright 2021 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ - -import addRenderAttemptedToDecisions from "./utils/addRenderAttemptedToDecisions"; - -const getViewPropositions = ({ viewCache, viewName, propositions }) => { - if (!viewName) { - return propositions; - } - - return viewCache - .getView(viewName) - .then(viewPropositions => [...viewPropositions, ...propositions]); -}; - -const buildFinalResult = ({ propositions }) => { - return { - decisions: propositions, - propositions: addRenderAttemptedToDecisions({ - decisions: propositions, - renderAttempted: false - }) - }; -}; - -export default ({ viewCache }) => { - return ({ - viewName, - redirectDecisions, - pageWideScopeDecisions, - nonAutoRenderableDecisions - }) => { - const propositions = [ - ...redirectDecisions, - ...pageWideScopeDecisions, - ...nonAutoRenderableDecisions - ]; - - return Promise.resolve(propositions) - .then(items => - getViewPropositions({ viewCache, viewName, propositions: items }) - ) - .then(items => buildFinalResult({ propositions: items })); - }; -}; diff --git a/src/components/Personalization/createOnResponseHandler.js b/src/components/Personalization/createOnResponseHandler.js deleted file mode 100644 index 29901c0af..000000000 --- a/src/components/Personalization/createOnResponseHandler.js +++ /dev/null @@ -1,68 +0,0 @@ -/* -Copyright 2020 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ -import isNonEmptyArray from "../../utils/isNonEmptyArray"; - -const DECISIONS_HANDLE = "personalization:decisions"; - -export default ({ - autoRenderingHandler, - nonRenderingHandler, - groupDecisions, - handleRedirectDecisions, - showContainers -}) => { - return ({ decisionsDeferred, personalizationDetails, response }) => { - const unprocessedDecisions = response.getPayloadsByType(DECISIONS_HANDLE); - const viewName = personalizationDetails.getViewName(); - - // if personalization payload is empty return empty decisions array - if (unprocessedDecisions.length === 0) { - showContainers(); - decisionsDeferred.resolve({}); - return { - decisions: [], - propositions: [] - }; - } - - const { - redirectDecisions, - pageWideScopeDecisions, - viewDecisions, - nonAutoRenderableDecisions - } = groupDecisions(unprocessedDecisions); - - if ( - personalizationDetails.isRenderDecisions() && - isNonEmptyArray(redirectDecisions) - ) { - decisionsDeferred.resolve({}); - return handleRedirectDecisions(redirectDecisions); - } - // save decisions for views in local cache - decisionsDeferred.resolve(viewDecisions); - - if (personalizationDetails.isRenderDecisions()) { - return autoRenderingHandler({ - viewName, - pageWideScopeDecisions, - nonAutoRenderableDecisions - }); - } - return nonRenderingHandler({ - viewName, - redirectDecisions, - pageWideScopeDecisions, - nonAutoRenderableDecisions - }); - }; -}; diff --git a/src/components/Personalization/createPersonalizationDetails.js b/src/components/Personalization/createPersonalizationDetails.js index c62402bff..b43e05440 100644 --- a/src/components/Personalization/createPersonalizationDetails.js +++ b/src/components/Personalization/createPersonalizationDetails.js @@ -43,7 +43,7 @@ export default ({ decisionScopes, personalization, event, - viewCache, + isCacheInitialized, logger }) => { const viewName = event.getViewName(); @@ -51,6 +51,9 @@ export default ({ isRenderDecisions() { return renderDecisions; }, + isSendDisplayNotifications() { + return !!personalization.sendDisplayNotifications; + }, getViewName() { return viewName; }, @@ -100,7 +103,7 @@ export default ({ }; }, isCacheInitialized() { - return viewCache.isInitialized(); + return isCacheInitialized; }, shouldFetchData() { return ( diff --git a/src/components/Personalization/createRedirectHandler.js b/src/components/Personalization/createRedirectHandler.js deleted file mode 100644 index e4eca0376..000000000 --- a/src/components/Personalization/createRedirectHandler.js +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright 2021 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ - -import { REDIRECT_EXECUTION_ERROR } from "./constants/loggerMessage"; - -const getRedirectDetails = redirectDecisions => { - const decision = redirectDecisions[0]; - const { items, id, scope, scopeDetails } = decision; - const { content } = items[0].data; - - return { content, decisions: [{ id, scope, scopeDetails }] }; -}; - -export default ({ collect, window, logger, showContainers }) => { - return redirectDecisions => { - const { content, decisions } = getRedirectDetails(redirectDecisions); - const documentMayUnload = true; - - return collect({ decisionsMeta: decisions, documentMayUnload }) - .then(() => { - window.location.replace(content); - }) - .catch(() => { - showContainers(); - logger.warn(REDIRECT_EXECUTION_ERROR); - }); - }; -}; diff --git a/src/components/Personalization/createViewCacheManager.js b/src/components/Personalization/createViewCacheManager.js index 8830c6b48..6f6a8a394 100644 --- a/src/components/Personalization/createViewCacheManager.js +++ b/src/components/Personalization/createViewCacheManager.js @@ -12,37 +12,87 @@ governing permissions and limitations under the License. import { assign } from "../../utils"; import defer from "../../utils/defer"; +import { VIEW_SCOPE_TYPE } from "./constants/scopeType"; -export default () => { - let viewStorage; - const viewStorageDeferred = defer(); +export default ({ createProposition }) => { + const viewStorage = {}; + let cacheUpdateCreatedAtLeastOnce = false; + let previousUpdateCacheComplete = Promise.resolve(); - const storeViews = decisionsPromise => { - decisionsPromise - .then(decisions => { - if (viewStorage === undefined) { - viewStorage = {}; + const getViewPropositions = (currentViewStorage, viewName) => { + const viewPropositions = currentViewStorage[viewName]; + if (viewPropositions && viewPropositions.length > 0) { + return viewPropositions.map(createProposition); + } + + const emptyViewProposition = createProposition({ + scope: viewName, + scopeDetails: { + characteristics: { + scopeType: "view" } - assign(viewStorage, decisions); - viewStorageDeferred.resolve(); + } + }); + emptyViewProposition.includeInDisplayNotification(); + emptyViewProposition.excludeInReturnedPropositions(); + return [emptyViewProposition]; + }; + + // This should be called before making the request to experience edge. + const createCacheUpdate = viewName => { + const updateCacheDeferred = defer(); + + cacheUpdateCreatedAtLeastOnce = true; + previousUpdateCacheComplete = previousUpdateCacheComplete + .then(() => updateCacheDeferred.promise) + .then(newViewStorage => { + assign(viewStorage, newViewStorage); }) - .catch(() => { - if (viewStorage === undefined) { - viewStorage = {}; + .catch(() => {}); + + return { + update(personalizationHandles) { + const newViewStorage = {}; + const otherPropositions = []; + personalizationHandles.forEach(handle => { + const { + scope, + scopeDetails: { characteristics: { scopeType } = {} } = {} + } = handle; + if (scopeType === VIEW_SCOPE_TYPE) { + newViewStorage[scope] = newViewStorage[scope] || []; + newViewStorage[scope].push(handle); + } else { + otherPropositions.push(createProposition(handle)); + } + }); + updateCacheDeferred.resolve(newViewStorage); + if (viewName) { + return [ + ...getViewPropositions(newViewStorage, viewName), + ...otherPropositions + ]; } - viewStorageDeferred.resolve(); - }); + return otherPropositions; + }, + cancel() { + updateCacheDeferred.reject(); + } + }; }; const getView = viewName => { - return viewStorageDeferred.promise.then(() => viewStorage[viewName] || []); + return previousUpdateCacheComplete.then(() => + getViewPropositions(viewStorage, viewName) + ); }; const isInitialized = () => { - return !(viewStorage === undefined); + return cacheUpdateCreatedAtLeastOnce; }; + return { - storeViews, + createCacheUpdate, getView, isInitialized }; diff --git a/src/components/Personalization/createViewChangeHandler.js b/src/components/Personalization/createViewChangeHandler.js index 8440abeff..aa7394731 100644 --- a/src/components/Personalization/createViewChangeHandler.js +++ b/src/components/Personalization/createViewChangeHandler.js @@ -10,46 +10,34 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import composePersonalizationResultingObject from "./utils/composePersonalizationResultingObject"; -import { isNonEmptyArray } from "../../utils"; import { PropositionEventType } from "./constants/propositionEventType"; +import { + buildReturnedPropositions, + buildReturnedDecisions +} from "./handlers/proposition"; -export default ({ - mergeDecisionsMeta, - collect, - executeDecisions, - viewCache -}) => { +export default ({ mergeDecisionsMeta, render, viewCache }) => { return ({ personalizationDetails, event, onResponse }) => { const viewName = personalizationDetails.getViewName(); - return viewCache.getView(viewName).then(viewDecisions => { + return viewCache.getView(viewName).then(propositions => { + onResponse(() => { + return { + propositions: buildReturnedPropositions(propositions), + decisions: buildReturnedDecisions(propositions) + }; + }); + if (personalizationDetails.isRenderDecisions()) { - return executeDecisions(viewDecisions).then(decisionsMeta => { - // if there are decisions to be rendered we render them and attach the result in experience.decisions.propositions - if (isNonEmptyArray(decisionsMeta)) { - mergeDecisionsMeta( - event, - decisionsMeta, - PropositionEventType.DISPLAY - ); - onResponse(() => { - return composePersonalizationResultingObject(viewDecisions, true); - }); - return; - } - // if there are no decisions in cache for this view, we will send a empty notification - onResponse(() => { - collect({ decisionsMeta: [], viewName }); - return composePersonalizationResultingObject(viewDecisions, true); - }); + return render(propositions).then(decisionsMeta => { + mergeDecisionsMeta( + event, + decisionsMeta, + PropositionEventType.DISPLAY + ); }); } - - onResponse(() => { - return composePersonalizationResultingObject(viewDecisions, false); - }); - return {}; + return Promise.resolve(); }); }; }; diff --git a/src/components/Personalization/dom-actions/action.js b/src/components/Personalization/dom-actions/action.js index 50cc3716d..28667d370 100644 --- a/src/components/Personalization/dom-actions/action.js +++ b/src/components/Personalization/dom-actions/action.js @@ -25,7 +25,6 @@ export { default as setStyles } from "./setStyles"; export { default as setAttributes } from "./setAttributes"; export { default as swapImage } from "./swapImage"; export { default as rearrangeChildren } from "./rearrangeChildren"; -export { default as click } from "./click"; const renderContent = (elements, content, renderFunc) => { const executions = elements.map(element => renderFunc(element, content)); @@ -34,8 +33,8 @@ const renderContent = (elements, content, renderFunc) => { }; export const createAction = renderFunc => { - return settings => { - const { selector, prehidingSelector, content, meta } = settings; + return itemData => { + const { selector, prehidingSelector, content } = itemData; hideElements(prehidingSelector); @@ -45,13 +44,12 @@ export const createAction = renderFunc => { () => { // if everything is OK, show elements showElements(prehidingSelector); - return { meta }; }, error => { // in case of awaiting timing or error, we need to remove the style tag // hence showing the pre-hidden elements showElements(prehidingSelector); - return { meta, error }; + throw error; } ); }; diff --git a/src/components/Personalization/dom-actions/clicks/index.js b/src/components/Personalization/dom-actions/clicks/index.js deleted file mode 100644 index a1ebd0b9e..000000000 --- a/src/components/Personalization/dom-actions/clicks/index.js +++ /dev/null @@ -1,15 +0,0 @@ -/* -Copyright 2019 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ - -import collectClicks from "./collectClicks"; - -export default collectClicks; diff --git a/src/components/Personalization/dom-actions/click.js b/src/components/Personalization/dom-actions/createPreprocess.js similarity index 70% rename from src/components/Personalization/dom-actions/click.js rename to src/components/Personalization/dom-actions/createPreprocess.js index 7a5f7332f..ae094c2e7 100644 --- a/src/components/Personalization/dom-actions/click.js +++ b/src/components/Personalization/dom-actions/createPreprocess.js @@ -1,5 +1,5 @@ /* -Copyright 2019 Adobe. All rights reserved. +Copyright 2023 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -9,10 +9,11 @@ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTA OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ +import { assign } from "../../../utils"; -export default (settings, store) => { - const { selector, meta } = settings; - - store({ selector, meta }); - return Promise.resolve(); +export default preprocessors => action => { + return preprocessors.reduce( + (processed, fn) => assign(processed, fn(processed)), + action + ); }; diff --git a/src/components/Personalization/dom-actions/executeActions.js b/src/components/Personalization/dom-actions/executeActions.js deleted file mode 100644 index 3c7a3a346..000000000 --- a/src/components/Personalization/dom-actions/executeActions.js +++ /dev/null @@ -1,72 +0,0 @@ -/* -Copyright 2019 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ - -import remapHeadOffers from "./remapHeadOffers"; -import { assign } from "../../../utils"; -import remapCustomCodeOffers from "./remapCustomCodeOffers"; - -const logActionError = (logger, action, error) => { - if (logger.enabled) { - const details = JSON.stringify(action); - const { message, stack } = error; - const errorMessage = `Failed to execute action ${details}. ${message} ${ - stack ? `\n ${stack}` : "" - }`; - - logger.error(errorMessage); - } -}; - -const logActionCompleted = (logger, action) => { - if (logger.enabled) { - const details = JSON.stringify(action); - - logger.info(`Action ${details} executed.`); - } -}; - -const executeAction = (logger, modules, type, args) => { - const execute = modules[type]; - - if (!execute) { - const error = new Error(`DOM action "${type}" not found`); - logActionError(logger, args[0], error); - throw error; - } - return execute(...args); -}; - -const PREPROCESSORS = [remapHeadOffers, remapCustomCodeOffers]; - -const preprocess = action => - PREPROCESSORS.reduce( - (processed, fn) => assign(processed, fn(processed)), - action - ); - -export default (actions, modules, logger) => { - const actionPromises = actions.map(action => { - const processedAction = preprocess(action); - const { type } = processedAction; - - return executeAction(logger, modules, type, [processedAction]) - .then(result => { - logActionCompleted(logger, processedAction); - return result; - }) - .catch(error => { - logActionError(logger, processedAction, error); - throw error; - }); - }); - return Promise.all(actionPromises); -}; diff --git a/src/components/Personalization/dom-actions/index.js b/src/components/Personalization/dom-actions/index.js index 763784428..59f99e605 100644 --- a/src/components/Personalization/dom-actions/index.js +++ b/src/components/Personalization/dom-actions/index.js @@ -11,4 +11,3 @@ governing permissions and limitations under the License. */ export { default as initDomActionsModules } from "./initDomActionsModules"; -export { default as executeActions } from "./executeActions"; diff --git a/src/components/Personalization/dom-actions/initDomActionsModules.js b/src/components/Personalization/dom-actions/initDomActionsModules.js index 92710d8ce..7cdf223fb 100644 --- a/src/components/Personalization/dom-actions/initDomActionsModules.js +++ b/src/components/Personalization/dom-actions/initDomActionsModules.js @@ -23,11 +23,10 @@ import { appendHtml, prependHtml, insertHtmlAfter, - insertHtmlBefore, - click + insertHtmlBefore } from "./action"; -export default store => { +export default () => { return { setHtml: createAction(setHtml), customCode: createAction(prependHtml), @@ -43,8 +42,6 @@ export default store => { insertBefore: createAction(insertHtmlBefore), replaceHtml: createAction(replaceHtml), prependHtml: createAction(prependHtml), - appendHtml: createAction(appendHtml), - click: settings => click(settings, store), - defaultContent: settings => Promise.resolve({ meta: settings.meta }) + appendHtml: createAction(appendHtml) }; }; diff --git a/src/components/Personalization/groupDecisions.js b/src/components/Personalization/groupDecisions.js deleted file mode 100644 index 73528dbe4..000000000 --- a/src/components/Personalization/groupDecisions.js +++ /dev/null @@ -1,149 +0,0 @@ -/* -Copyright 2020 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ - -import { isNonEmptyArray, includes } from "../../utils"; -import isPageWideScope from "./utils/isPageWideScope"; -import { - DOM_ACTION, - REDIRECT_ITEM, - DEFAULT_CONTENT_ITEM, - MEASUREMENT_SCHEMA -} from "./constants/schema"; -import { VIEW_SCOPE_TYPE } from "./constants/scopeType"; - -const splitItems = (items, schemas) => { - const matched = []; - const nonMatched = []; - - items.forEach(item => { - if (includes(schemas, item.schema)) { - matched.push(item); - } else { - nonMatched.push(item); - } - }); - - return [matched, nonMatched]; -}; - -const createDecision = (decision, items) => { - return { - id: decision.id, - scope: decision.scope, - items, - scopeDetails: decision.scopeDetails - }; -}; - -const splitMergedMetricDecisions = decisions => { - const matchedDecisions = decisions.filter(decision => { - const { items = [] } = decision; - return items.some(item => item.schema === MEASUREMENT_SCHEMA); - }); - const unmatchedDecisions = decisions.filter( - decision => !includes(matchedDecisions, decision) - ); - return { matchedDecisions, unmatchedDecisions }; -}; - -const splitDecisions = (decisions, ...schemas) => { - const matchedDecisions = []; - const unmatchedDecisions = []; - - decisions.forEach(decision => { - const { items = [] } = decision; - const [matchedItems, nonMatchedItems] = splitItems(items, schemas); - - if (isNonEmptyArray(matchedItems)) { - matchedDecisions.push(createDecision(decision, matchedItems)); - } - - if (isNonEmptyArray(nonMatchedItems)) { - unmatchedDecisions.push(createDecision(decision, nonMatchedItems)); - } - }); - return { matchedDecisions, unmatchedDecisions }; -}; - -const appendScopeDecision = (scopeDecisions, decision) => { - if (!scopeDecisions[decision.scope]) { - scopeDecisions[decision.scope] = []; - } - scopeDecisions[decision.scope].push(decision); -}; - -const isViewScope = scopeDetails => - scopeDetails.characteristics && - scopeDetails.characteristics.scopeType && - scopeDetails.characteristics.scopeType === VIEW_SCOPE_TYPE; - -const extractDecisionsByScope = decisions => { - const pageWideScopeDecisions = []; - const nonPageWideScopeDecisions = []; - const viewScopeDecisions = {}; - - if (isNonEmptyArray(decisions)) { - decisions.forEach(decision => { - if (isPageWideScope(decision.scope)) { - pageWideScopeDecisions.push(decision); - } else if (isViewScope(decision.scopeDetails)) { - appendScopeDecision(viewScopeDecisions, decision); - } else { - nonPageWideScopeDecisions.push(decision); - } - }); - } - - return { - pageWideScopeDecisions, - nonPageWideScopeDecisions, - viewScopeDecisions - }; -}; - -const groupDecisions = unprocessedDecisions => { - // split redirect decisions - const decisionsGroupedByRedirectItemSchema = splitDecisions( - unprocessedDecisions, - REDIRECT_ITEM - ); - // split merged measurement decisions - const mergedMetricDecisions = splitMergedMetricDecisions( - decisionsGroupedByRedirectItemSchema.unmatchedDecisions - ); - // split renderable decisions - const decisionsGroupedByRenderableSchemas = splitDecisions( - mergedMetricDecisions.unmatchedDecisions, - DOM_ACTION, - DEFAULT_CONTENT_ITEM - ); - // group renderable decisions by scope - const { - pageWideScopeDecisions, - nonPageWideScopeDecisions, - viewScopeDecisions - } = extractDecisionsByScope( - decisionsGroupedByRenderableSchemas.matchedDecisions - ); - - return { - redirectDecisions: decisionsGroupedByRedirectItemSchema.matchedDecisions, - pageWideScopeDecisions, - viewDecisions: viewScopeDecisions, - nonAutoRenderableDecisions: [ - ...mergedMetricDecisions.matchedDecisions, - ...decisionsGroupedByRenderableSchemas.unmatchedDecisions, - ...nonPageWideScopeDecisions - ] - }; -}; -export default groupDecisions; diff --git a/src/components/Personalization/handlers/createDomActionHandler.js b/src/components/Personalization/handlers/createDomActionHandler.js new file mode 100644 index 000000000..a27b6c48e --- /dev/null +++ b/src/components/Personalization/handlers/createDomActionHandler.js @@ -0,0 +1,46 @@ +/* +Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +import { DEFAULT_CONTENT_ITEM, DOM_ACTION } from "../constants/schema"; + +export default ({ + next, + modules, + storeClickMetrics, + preprocess +}) => proposition => { + const { items = [] } = proposition.getHandle(); + + items.forEach((item, index) => { + const { schema, data } = item; + if (schema === DEFAULT_CONTENT_ITEM) { + proposition.includeInDisplayNotification(); + proposition.addRenderer(index, () => undefined); + } + const { type, selector } = data || {}; + if (schema === DOM_ACTION && type && selector) { + if (type === "click") { + // Do not record the click proposition in display notification. + // Store it for later. + storeClickMetrics({ selector, meta: proposition.getItemMeta(index) }); + proposition.addRenderer(index, () => undefined); + } else if (modules[type]) { + proposition.includeInDisplayNotification(); + const processedData = preprocess(data); + proposition.addRenderer(index, () => { + return modules[type](processedData); + }); + } + } + }); + + next(proposition); +}; diff --git a/src/components/Personalization/handlers/createHtmlContentHandler.js b/src/components/Personalization/handlers/createHtmlContentHandler.js new file mode 100644 index 000000000..47f3d028d --- /dev/null +++ b/src/components/Personalization/handlers/createHtmlContentHandler.js @@ -0,0 +1,44 @@ +/* +Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +import { HTML_CONTENT_ITEM } from "../constants/schema"; +import { VIEW_SCOPE_TYPE } from "../constants/scopeType"; +import isPageWideScope from "../utils/isPageWideScope"; + +export default ({ next, modules, preprocess }) => proposition => { + const { + scope, + scopeDetails: { characteristics: { scopeType } = {} } = {}, + items = [] + } = proposition.getHandle(); + + items.forEach((item, index) => { + const { schema, data } = item; + const { type, selector } = data || {}; + if (schema === HTML_CONTENT_ITEM && type && selector && modules[type]) { + proposition.includeInDisplayNotification(); + const preprocessedData = preprocess(data); + proposition.addRenderer(index, () => { + return modules[type](preprocessedData); + }); + } + }); + + // only continue processing if it is a view scope proposition + // or if it is a page wide proposition. + if ( + scopeType === VIEW_SCOPE_TYPE || + isPageWideScope(scope) || + proposition.isApplyPropositions() + ) { + next(proposition); + } +}; diff --git a/src/components/Personalization/utils/composePersonalizationResultingObject.js b/src/components/Personalization/handlers/createMeasurementSchemaHandler.js similarity index 61% rename from src/components/Personalization/utils/composePersonalizationResultingObject.js rename to src/components/Personalization/handlers/createMeasurementSchemaHandler.js index ad1a0e98b..b6ec90680 100644 --- a/src/components/Personalization/utils/composePersonalizationResultingObject.js +++ b/src/components/Personalization/handlers/createMeasurementSchemaHandler.js @@ -9,17 +9,14 @@ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTA OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import addRenderAttemptedToDecisions from "./addRenderAttemptedToDecisions"; +import { MEASUREMENT_SCHEMA } from "../constants/schema"; -export default (decisions = [], renderDecisions) => { - const resultingObject = { - propositions: addRenderAttemptedToDecisions({ - decisions, - renderAttempted: renderDecisions - }) - }; - if (!renderDecisions) { - resultingObject.decisions = decisions; +export default ({ next }) => proposition => { + const { items = [] } = proposition.getHandle(); + + // If there is a measurement schema in the item list, + // just return the whole proposition unrendered. (i.e. do not call next) + if (!items.some(item => item.schema === MEASUREMENT_SCHEMA)) { + next(proposition); } - return resultingObject; }; diff --git a/test/unit/specs/components/Personalization/dom-actions/click.spec.js b/src/components/Personalization/handlers/createRedirectHandler.js similarity index 53% rename from test/unit/specs/components/Personalization/dom-actions/click.spec.js rename to src/components/Personalization/handlers/createRedirectHandler.js index cb8090965..eed92e493 100644 --- a/test/unit/specs/components/Personalization/dom-actions/click.spec.js +++ b/src/components/Personalization/handlers/createRedirectHandler.js @@ -9,19 +9,20 @@ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTA OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import { initDomActionsModules } from "../../../../../../src/components/Personalization/dom-actions"; +import { REDIRECT_ITEM } from "../constants/schema"; +import { find } from "../../../utils"; -describe("Personalization::actions::click", () => { - it("should set click tracking attribute", () => { - const store = jasmine.createSpy(); - const modules = initDomActionsModules(store); - const { click } = modules; - const selector = "#click"; - const meta = { a: 1 }; - const settings = { selector, meta }; +export default ({ next }) => proposition => { + const { items = [] } = proposition.getHandle() || {}; - click(settings, store); - - expect(store).toHaveBeenCalledWith({ selector, meta }); - }); -}); + const redirectItem = find(items, ({ schema }) => schema === REDIRECT_ITEM); + if (redirectItem) { + const { + data: { content } + } = redirectItem; + proposition.redirect(content); + // On a redirect, nothing else needs to handle this. + } else { + next(proposition); + } +}; diff --git a/src/components/Personalization/handlers/createRender.js b/src/components/Personalization/handlers/createRender.js new file mode 100644 index 000000000..1ef0ad97d --- /dev/null +++ b/src/components/Personalization/handlers/createRender.js @@ -0,0 +1,49 @@ +import { REDIRECT_EXECUTION_ERROR } from "../constants/loggerMessage"; + +/* +Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +export default ({ + handleChain, + collect, + executeRedirect, + logger, + showContainers +}) => propositions => { + for (let i = 0; i < propositions.length; i += 1) { + const proposition = propositions[i]; + handleChain(proposition); + const redirectUrl = proposition.getRedirectUrl(); + if (redirectUrl) { + const displayNotificationPropositions = []; + proposition.addToNotifications(displayNotificationPropositions); + // no return value because we are redirecting. i.e. the sendEvent promise will + // never resolve anyways so no need to generate the return value. + return collect({ decisionsMeta: displayNotificationPropositions }) + .then(() => { + executeRedirect(redirectUrl); + // This code should never be reached because we are redirecting, but in case + // it does we return an empty array of notifications to match the return type. + return []; + }) + .catch(() => { + showContainers(); + logger.warn(REDIRECT_EXECUTION_ERROR); + }); + } + } + + return Promise.all( + propositions.map(proposition => proposition.render(logger)) + ).then(notifications => { + return notifications.filter(notification => notification); + }); +}; diff --git a/src/components/Personalization/handlers/proposition.js b/src/components/Personalization/handlers/proposition.js new file mode 100644 index 000000000..c0e29babf --- /dev/null +++ b/src/components/Personalization/handlers/proposition.js @@ -0,0 +1,155 @@ +/* +Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const renderWithLogging = (renderer, item, logger) => { + return Promise.resolve() + .then(renderer) + .then(() => { + if (logger.enabled) { + const details = JSON.stringify(item); + logger.info(`Action ${details} executed.`); + } + return true; + }) + .catch(error => { + if (logger.enabled) { + const details = JSON.stringify(item); + const { message, stack } = error; + const errorMessage = `Failed to execute action ${details}. ${message} ${ + stack ? `\n ${stack}` : "" + }`; + logger.error(errorMessage); + } + return false; + }); +}; + +export const createProposition = (handle, isApplyPropositions = false) => { + const { id, scope, scopeDetails, items = [] } = handle; + + const renderers = []; + let redirectUrl; + let includeInDisplayNotification = false; + let includeInReturnedPropositions = true; + const itemsRenderAttempted = new Array(items.length); + for (let i = 0; i < items.length; i += 1) { + itemsRenderAttempted[i] = false; + } + + return { + getHandle() { + return handle; + }, + getItemMeta(i) { + const item = items[i]; + const meta = { id, scope, scopeDetails }; + if (item.characteristics && item.characteristics.trackingLabel) { + meta.trackingLabel = item.characteristics.trackingLabel; + } + + return meta; + }, + redirect(url) { + includeInDisplayNotification = true; + itemsRenderAttempted.forEach((_, index) => { + itemsRenderAttempted[index] = true; + }); + redirectUrl = url; + }, + getRedirectUrl() { + return redirectUrl; + }, + addRenderer(itemIndex, renderer) { + itemsRenderAttempted[itemIndex] = true; + renderers.push([itemIndex, renderer]); + }, + includeInDisplayNotification() { + includeInDisplayNotification = true; + }, + excludeInReturnedPropositions() { + includeInReturnedPropositions = false; + }, + render(logger) { + return Promise.all( + renderers.map(([itemIndex, renderer]) => + renderWithLogging(renderer, items[itemIndex], logger) + ) + ).then(successes => { + const notifications = []; + // as long as at least one renderer succeeds, we want to add the notification + // to the display notifications + if (successes.length === 0 || successes.includes(true)) { + this.addToNotifications(notifications); + } + return notifications[0]; + }); + }, + addToNotifications(notifications) { + if (includeInDisplayNotification) { + notifications.push({ id, scope, scopeDetails }); + } + }, + addToReturnedPropositions(propositions) { + if (includeInReturnedPropositions) { + const renderedItems = items.filter( + (_, index) => itemsRenderAttempted[index] + ); + if (renderedItems.length > 0) { + propositions.push({ + ...handle, + items: renderedItems, + renderAttempted: true + }); + } + const nonrenderedItems = items.filter( + (_, index) => !itemsRenderAttempted[index] + ); + if (nonrenderedItems.length > 0) { + propositions.push({ + ...handle, + items: nonrenderedItems, + renderAttempted: false + }); + } + } + }, + addToReturnedDecisions(decisions) { + if (includeInReturnedPropositions) { + const nonrenderedItems = items.filter( + (item, index) => !itemsRenderAttempted[index] + ); + if (nonrenderedItems.length > 0) { + decisions.push({ ...handle, items: nonrenderedItems }); + } + } + }, + isApplyPropositions() { + return isApplyPropositions; + } + }; +}; + +export const buildReturnedPropositions = propositions => { + const returnedPropositions = []; + propositions.forEach(p => { + p.addToReturnedPropositions(returnedPropositions); + }); + return returnedPropositions; +}; + +export const buildReturnedDecisions = propositions => { + const returnedDecisions = []; + propositions.forEach(p => { + p.addToReturnedDecisions(returnedDecisions); + }); + return returnedDecisions; +}; diff --git a/src/components/Personalization/index.js b/src/components/Personalization/index.js index 14c92f196..5a75c68a9 100644 --- a/src/components/Personalization/index.js +++ b/src/components/Personalization/index.js @@ -12,9 +12,8 @@ governing permissions and limitations under the License. import { string, boolean, objectOf } from "../../utils/validation"; import createComponent from "./createComponent"; -import { initDomActionsModules, executeActions } from "./dom-actions"; +import { initDomActionsModules } from "./dom-actions"; import createCollect from "./createCollect"; -import createExecuteDecisions from "./createExecuteDecisions"; import { hideContainers, showContainers } from "./flicker"; import createFetchDataHandler from "./createFetchDataHandler"; import collectClicks from "./dom-actions/clicks/collectClicks"; @@ -23,15 +22,19 @@ import { mergeDecisionsMeta, mergeQuery } from "./event"; import createOnClickHandler from "./createOnClickHandler"; import createViewCacheManager from "./createViewCacheManager"; import createViewChangeHandler from "./createViewChangeHandler"; -import groupDecisions from "./groupDecisions"; -import createOnResponseHandler from "./createOnResponseHandler"; import createClickStorage from "./createClickStorage"; -import createRedirectHandler from "./createRedirectHandler"; -import createAutorenderingHandler from "./createAutoRenderingHandler"; -import createNonRenderingHandler from "./createNonRenderingHandler"; import createApplyPropositions from "./createApplyPropositions"; import createGetPageLocation from "./createGetPageLocation"; import createSetTargetMigration from "./createSetTargetMigration"; +import createRedirectHandler from "./handlers/createRedirectHandler"; +import createHtmlContentHandler from "./handlers/createHtmlContentHandler"; +import createDomActionHandler from "./handlers/createDomActionHandler"; +import createMeasurementSchemaHandler from "./handlers/createMeasurementSchemaHandler"; +import createRender from "./handlers/createRender"; +import remapCustomCodeOffers from "./dom-actions/remapCustomCodeOffers"; +import remapHeadOffers from "./dom-actions/remapHeadOffers"; +import createPreprocess from "./dom-actions/createPreprocess"; +import { createProposition } from "./handlers/proposition"; const createPersonalization = ({ config, logger, eventManager }) => { const { targetMigrationEnabled, prehidingStyle } = config; @@ -43,41 +46,43 @@ const createPersonalization = ({ config, logger, eventManager }) => { storeClickMetrics } = createClickStorage(); const getPageLocation = createGetPageLocation({ window }); - const viewCache = createViewCacheManager(); - const modules = initDomActionsModules(storeClickMetrics); - const executeDecisions = createExecuteDecisions({ + const viewCache = createViewCacheManager({ createProposition }); + const modules = initDomActionsModules(); + + const preprocess = createPreprocess([remapHeadOffers, remapCustomCodeOffers]); + + const noOpHandler = () => undefined; + const domActionHandler = createDomActionHandler({ + next: noOpHandler, modules, - logger, - executeActions + storeClickMetrics, + preprocess }); - const handleRedirectDecisions = createRedirectHandler({ - collect, - window, - logger, - showContainers + const measurementSchemaHandler = createMeasurementSchemaHandler({ + next: domActionHandler }); - const autoRenderingHandler = createAutorenderingHandler({ - viewCache, - executeDecisions, - showContainers, - collect + const redirectHandler = createRedirectHandler({ + next: measurementSchemaHandler }); - const applyPropositions = createApplyPropositions({ - executeDecisions + const htmlContentHandler = createHtmlContentHandler({ + next: redirectHandler, + modules, + preprocess }); - const nonRenderingHandler = createNonRenderingHandler({ viewCache }); - const responseHandler = createOnResponseHandler({ - autoRenderingHandler, - nonRenderingHandler, - groupDecisions, - handleRedirectDecisions, - showContainers + + const render = createRender({ + handleChain: htmlContentHandler, + collect, + executeRedirect: url => window.location.replace(url), + logger }); const fetchDataHandler = createFetchDataHandler({ prehidingStyle, - responseHandler, + showContainers, hideContainers, - mergeQuery + mergeQuery, + collect, + render }); const onClickHandler = createOnClickHandler({ mergeDecisionsMeta, @@ -87,10 +92,12 @@ const createPersonalization = ({ config, logger, eventManager }) => { }); const viewChangeHandler = createViewChangeHandler({ mergeDecisionsMeta, - collect, - executeDecisions, + render, viewCache }); + const applyPropositions = createApplyPropositions({ + render + }); const setTargetMigration = createSetTargetMigration({ targetMigrationEnabled }); diff --git a/test/functional/specs/Personalization/C205528.js b/test/functional/specs/Personalization/C205528.js index 4e5beab79..6c4a4736f 100644 --- a/test/functional/specs/Personalization/C205528.js +++ b/test/functional/specs/Personalization/C205528.js @@ -49,6 +49,6 @@ test("Test C205528: A redirect offer should redirect the page to the URL in the } catch (e) { // an exception will be thrown because a redirect will be executed within the Alloy Client Function } finally { - await t.expect(redirectLogger.requests.length).eql(1); + await t.expect(redirectLogger.count(() => true)).eql(1); } }); diff --git a/test/functional/specs/Personalization/C6364798.js b/test/functional/specs/Personalization/C6364798.js index 4611d20ac..37d468032 100644 --- a/test/functional/specs/Personalization/C6364798.js +++ b/test/functional/specs/Personalization/C6364798.js @@ -124,24 +124,14 @@ const simulatePageLoad = async alloy => { personalizationPayload, PAGE_WIDE_SCOPE ); - await t - .expect( - // eslint-disable-next-line no-underscore-dangle - notificationRequestBody.events[0].xdm._experience.decisioning.propositions - ) - .eql(pageWideScopeDecisionsMeta); + await t .expect( // eslint-disable-next-line no-underscore-dangle notificationRequestBody.events[0].xdm._experience.decisioning - .propositionEventType.display + .propositions[1] ) - .eql(1); - // notification for view rendered decisions - const viewNotificationRequest = networkLogger.edgeEndpointLogs.requests[2]; - const viewNotificationRequestBody = JSON.parse( - viewNotificationRequest.request.body - ); + .eql(pageWideScopeDecisionsMeta[0]); const productsViewDecisionsMeta = getDecisionsMetaByScope( personalizationPayload, "products" @@ -149,17 +139,26 @@ const simulatePageLoad = async alloy => { await t .expect( // eslint-disable-next-line no-underscore-dangle - viewNotificationRequestBody.events[0].xdm._experience.decisioning - .propositions + notificationRequestBody.events[0].xdm._experience.decisioning + .propositions[0] + ) + .eql(productsViewDecisionsMeta[0]); + await t + .expect( + // eslint-disable-next-line no-underscore-dangle + notificationRequestBody.events[0].xdm._experience.decisioning.propositions + .length ) - .eql(productsViewDecisionsMeta); + .eql(2); + await t .expect( // eslint-disable-next-line no-underscore-dangle - viewNotificationRequestBody.events[0].xdm._experience.decisioning + notificationRequestBody.events[0].xdm._experience.decisioning .propositionEventType.display ) .eql(1); + const allPropositionsWereRendered = resultingObject.propositions.every( proposition => proposition.renderAttempted ); @@ -181,7 +180,7 @@ const simulateViewChange = async (alloy, personalizationPayload) => { } } }); - const viewChangeRequest = networkLogger.edgeEndpointLogs.requests[3]; + const viewChangeRequest = networkLogger.edgeEndpointLogs.requests[2]; const viewChangeRequestBody = JSON.parse(viewChangeRequest.request.body); // assert that no personalization query was attached to the request await t.expect(viewChangeRequestBody.events[0].query).eql(undefined); @@ -230,30 +229,41 @@ const simulateViewChangeForNonExistingView = async alloy => { } }); - const noViewViewChangeRequest = networkLogger.edgeEndpointLogs.requests[4]; + const noViewViewChangeRequest = networkLogger.edgeEndpointLogs.requests[3]; const noViewViewChangeRequestBody = JSON.parse( noViewViewChangeRequest.request.body ); // assert that no personalization query was attached to the request await t.expect(noViewViewChangeRequestBody.events[0].query).eql(undefined); - // assert that a notification call with xdm.web.webPageDetails.viewName and no personalization meta is sent - await flushPromiseChains(); - const noViewNotificationRequest = networkLogger.edgeEndpointLogs.requests[5]; - const noViewNotificationRequestBody = JSON.parse( - noViewNotificationRequest.request.body - ); await t - .expect(noViewNotificationRequestBody.events[0].xdm.eventType) - .eql("decisioning.propositionDisplay"); + .expect( + // eslint-disable-next-line no-underscore-dangle + noViewViewChangeRequestBody.events[0].xdm._experience.decisioning + .propositions + ) + .eql([ + { + scope: "noView", + scopeDetails: { + characteristics: { + scopeType: "view" + } + } + } + ]); + await t + .expect( + noViewViewChangeRequestBody.events[0].xdm.web.webPageDetails.viewName + ) + .eql("noView"); await t .expect( - noViewNotificationRequestBody.events[0].xdm.web.webPageDetails.viewName + noViewViewChangeRequestBody.events[0].xdm.web.webPageDetails.viewName ) .eql("noView"); await t - // eslint-disable-next-line no-underscore-dangle - .expect(noViewNotificationRequestBody.events[0].xdm._experience) - .eql(undefined); + .expect(noViewViewChangeRequestBody.events[0].xdm.eventType) + .eql("noviewoffers"); }; const simulateViewRerender = async (alloy, propositions) => { @@ -269,7 +279,7 @@ const simulateViewRerender = async (alloy, propositions) => { .expect(applyPropositionsResult.propositions.length) .eql(propositions.length); // make sure no new network requests are sent - applyPropositions is a client-side only command. - await t.expect(networkLogger.edgeEndpointLogs.requests.length).eql(4); + await t.expect(networkLogger.edgeEndpointLogs.requests.length).eql(3); }; test("Test C6364798: applyPropositions should re-render SPA view without sending view notifications", async () => { diff --git a/test/functional/specs/Personalization/C782718.js b/test/functional/specs/Personalization/C782718.js index 0a9b504cd..ddfb48a20 100644 --- a/test/functional/specs/Personalization/C782718.js +++ b/test/functional/specs/Personalization/C782718.js @@ -123,24 +123,13 @@ const simulatePageLoad = async alloy => { personalizationPayload, PAGE_WIDE_SCOPE ); - await t - .expect( - // eslint-disable-next-line no-underscore-dangle - notificationRequestBody.events[0].xdm._experience.decisioning.propositions - ) - .eql(pageWideScopeDecisionsMeta); await t .expect( // eslint-disable-next-line no-underscore-dangle notificationRequestBody.events[0].xdm._experience.decisioning - .propositionEventType.display + .propositions[1] ) - .eql(1); - // notification for view rendered decisions - const viewNotificationRequest = networkLogger.edgeEndpointLogs.requests[2]; - const viewNotificationRequestBody = JSON.parse( - viewNotificationRequest.request.body - ); + .eql(pageWideScopeDecisionsMeta[0]); const productsViewDecisionsMeta = getDecisionsMetaByScope( personalizationPayload, "products" @@ -148,14 +137,14 @@ const simulatePageLoad = async alloy => { await t .expect( // eslint-disable-next-line no-underscore-dangle - viewNotificationRequestBody.events[0].xdm._experience.decisioning - .propositions + notificationRequestBody.events[0].xdm._experience.decisioning + .propositions[0] ) - .eql(productsViewDecisionsMeta); + .eql(productsViewDecisionsMeta[0]); await t .expect( // eslint-disable-next-line no-underscore-dangle - viewNotificationRequestBody.events[0].xdm._experience.decisioning + notificationRequestBody.events[0].xdm._experience.decisioning .propositionEventType.display ) .eql(1); @@ -180,7 +169,7 @@ const simulateViewChange = async (alloy, personalizationPayload) => { } } }); - const viewChangeRequest = networkLogger.edgeEndpointLogs.requests[3]; + const viewChangeRequest = networkLogger.edgeEndpointLogs.requests[2]; const viewChangeRequestBody = JSON.parse(viewChangeRequest.request.body); // assert that no personalization query was attached to the request await t.expect(viewChangeRequestBody.events[0].query).eql(undefined); @@ -227,30 +216,40 @@ const simulateViewChangeForNonExistingView = async alloy => { } }); - const noViewViewChangeRequest = networkLogger.edgeEndpointLogs.requests[4]; + const noViewViewChangeRequest = networkLogger.edgeEndpointLogs.requests[3]; const noViewViewChangeRequestBody = JSON.parse( noViewViewChangeRequest.request.body ); // assert that no personalization query was attached to the request await t.expect(noViewViewChangeRequestBody.events[0].query).eql(undefined); // assert that a notification call with xdm.web.webPageDetails.viewName and no personalization meta is sent - await flushPromiseChains(); - const noViewNotificationRequest = networkLogger.edgeEndpointLogs.requests[5]; - const noViewNotificationRequestBody = JSON.parse( - noViewNotificationRequest.request.body - ); + await t - .expect(noViewNotificationRequestBody.events[0].xdm.eventType) - .eql("decisioning.propositionDisplay"); + .expect(noViewViewChangeRequestBody.events[0].xdm.eventType) + .eql("noviewoffers"); await t .expect( - noViewNotificationRequestBody.events[0].xdm.web.webPageDetails.viewName + noViewViewChangeRequestBody.events[0].xdm.web.webPageDetails.viewName ) .eql("noView"); await t // eslint-disable-next-line no-underscore-dangle - .expect(noViewNotificationRequestBody.events[0].xdm._experience) - .eql(undefined); + .expect(noViewViewChangeRequestBody.events[0].xdm._experience.decisioning) + .eql({ + propositions: [ + { + scope: "noView", + scopeDetails: { + characteristics: { + scopeType: "view" + } + } + } + ], + propositionEventType: { + display: 1 + } + }); }; test("Test C782718: SPA support with auto-rendering and view notifications", async () => { diff --git a/test/functional/specs/Privacy/IAB/C224677.js b/test/functional/specs/Privacy/IAB/C224677.js index 151c23b3e..9fb9c89b7 100644 --- a/test/functional/specs/Privacy/IAB/C224677.js +++ b/test/functional/specs/Privacy/IAB/C224677.js @@ -11,7 +11,6 @@ governing permissions and limitations under the License. */ import { t } from "testcafe"; import createNetworkLogger from "../../../helpers/networkLogger"; -import { responseStatus } from "../../../helpers/assertions/index"; import createFixture from "../../../helpers/createFixture"; import createResponse from "../../../helpers/createResponse"; import getResponseBody from "../../../helpers/networkLogger/getResponseBody"; @@ -82,5 +81,12 @@ test("Test C224677: Call setConsent when purpose 10 is FALSE", async () => { .notContains("https://ns.adobe.com/aep/errors/EXEG-0301-200"); await t.expect(networkLogger.edgeEndpointLogs.requests.length).eql(1); - await responseStatus(networkLogger.edgeEndpointLogs.requests, 200); + await t + .expect( + networkLogger.edgeEndpointLogs.count( + ({ response: { statusCode } }) => + statusCode === 200 || statusCode === 207 + ) + ) + .eql(1); }); diff --git a/test/unit/specs/components/Personalization/createApplyPropositions.spec.js b/test/unit/specs/components/Personalization/createApplyPropositions.spec.js index 0f75b7f9e..0d3963560 100644 --- a/test/unit/specs/components/Personalization/createApplyPropositions.spec.js +++ b/test/unit/specs/components/Personalization/createApplyPropositions.spec.js @@ -26,34 +26,33 @@ const METADATA = { }; describe("Personalization::createApplyPropositions", () => { - let executeDecisions; + let render; beforeEach(() => { - executeDecisions = jasmine.createSpy("executeDecisions"); + render = jasmine.createSpy("render"); + render.and.callFake(propositions => { + propositions.forEach(proposition => { + const { items = [] } = proposition.getHandle(); + items.forEach((_, i) => { + proposition.addRenderer(i, () => undefined); + }); + }); + }); }); it("it should return an empty propositions promise if propositions is empty array", () => { - executeDecisions.and.returnValue( - Promise.resolve(PAGE_WIDE_SCOPE_DECISIONS) - ); - const applyPropositions = createApplyPropositions({ - executeDecisions + render }); - return applyPropositions({ + const result = applyPropositions({ propositions: [] - }).then(result => { - expect(result).toEqual({ propositions: [] }); - expect(executeDecisions).toHaveBeenCalledTimes(0); }); + expect(result).toEqual({ propositions: [] }); + expect(render).toHaveBeenCalledOnceWith([]); }); it("it should apply user-provided dom-action schema propositions", () => { - executeDecisions.and.returnValue( - Promise.resolve(PAGE_WIDE_SCOPE_DECISIONS) - ); - const expectedExecuteDecisionsPropositions = clone( PAGE_WIDE_SCOPE_DECISIONS ).map(proposition => { @@ -62,56 +61,49 @@ describe("Personalization::createApplyPropositions", () => { }); const applyPropositions = createApplyPropositions({ - executeDecisions + render }); - return applyPropositions({ + const result = applyPropositions({ propositions: PAGE_WIDE_SCOPE_DECISIONS - }).then(result => { - expect(executeDecisions).toHaveBeenCalledTimes(1); - expect(executeDecisions.calls.first().args[0]).toEqual( - expectedExecuteDecisionsPropositions - ); + }); - const expectedScopes = expectedExecuteDecisionsPropositions.map( - proposition => proposition.scope - ); - result.propositions.forEach(proposition => { - expect(proposition.renderAttempted).toBeTrue(); - expect(expectedScopes).toContain(proposition.scope); - expect(proposition.items).toBeArrayOfObjects(); - expect(proposition.items.length).toEqual(2); - }); + expect(render).toHaveBeenCalledTimes(1); + + const expectedScopes = expectedExecuteDecisionsPropositions.map( + proposition => proposition.scope + ); + result.propositions.forEach(proposition => { + expect(proposition.renderAttempted).toBeTrue(); + expect(expectedScopes).toContain(proposition.scope); + expect(proposition.items).toBeArrayOfObjects(); + expect(proposition.items.length).toEqual(2); }); }); it("it should merge metadata with propositions that have html-content-item schema", () => { - executeDecisions.and.returnValue(Promise.resolve(MIXED_PROPOSITIONS)); - const applyPropositions = createApplyPropositions({ - executeDecisions + render }); - return applyPropositions({ + const { propositions } = applyPropositions({ propositions: MIXED_PROPOSITIONS, metadata: METADATA - }).then(() => { - const executedPropositions = executeDecisions.calls.first().args[0]; - expect(executedPropositions.length).toEqual(3); - executedPropositions.forEach(proposition => { - expect(proposition.items.length).toEqual(1); - if (proposition.items[0].id === "442358") { - expect(proposition.scope).toEqual("home"); - expect(proposition.items[0].data.selector).toEqual("#root"); - expect(proposition.items[0].data.type).toEqual("click"); - } else if (proposition.items[0].id === "442359") { - expect(proposition.scope).toEqual("home"); - expect(proposition.items[0].data.selector).toEqual("#home-item1"); - expect(proposition.items[0].data.type).toEqual("setHtml"); - } - }); - expect(executeDecisions).toHaveBeenCalledTimes(1); }); + + expect(propositions.length).toEqual(4); + propositions.forEach(proposition => { + expect(proposition.items.length).toEqual(1); + if (proposition.items[0].id === "442358") { + expect(proposition.items[0].data.selector).toEqual("#root"); + expect(proposition.items[0].data.type).toEqual("click"); + } else if (proposition.items[0].id === "442359") { + expect(proposition.scope).toEqual("home"); + expect(proposition.items[0].data.selector).toEqual("#home-item1"); + expect(proposition.items[0].data.type).toEqual("setHtml"); + } + }); + expect(render).toHaveBeenCalledTimes(1); }); it("it should drop items with html-content-item schema when there is no metadata", () => { @@ -142,108 +134,86 @@ describe("Personalization::createApplyPropositions", () => { } ]; - executeDecisions.and.returnValue(Promise.resolve(MIXED_PROPOSITIONS)); + const applyPropositions = createApplyPropositions({ render }); - const applyPropositions = createApplyPropositions({ - executeDecisions - }); - - return applyPropositions({ + const result = applyPropositions({ propositions - }).then(result => { - expect(result.propositions.length).toEqual(1); - expect(result.propositions[0].items.length).toEqual(1); - expect(result.propositions[0].items[0].id).toEqual("442358"); - expect(result.propositions[0].renderAttempted).toBeTrue(); }); + + expect(result.propositions.length).toEqual(1); + expect(result.propositions[0].items.length).toEqual(1); + expect(result.propositions[0].items[0].id).toEqual("442358"); + expect(result.propositions[0].renderAttempted).toBeTrue(); }); it("it should return renderAttempted = true on resulting propositions", () => { - executeDecisions.and.returnValue(Promise.resolve(MIXED_PROPOSITIONS)); - - const applyPropositions = createApplyPropositions({ - executeDecisions - }); + const applyPropositions = createApplyPropositions({ render }); - return applyPropositions({ + const result = applyPropositions({ propositions: MIXED_PROPOSITIONS - }).then(result => { - expect(result.propositions.length).toEqual(2); - result.propositions.forEach(proposition => { - expect(proposition.renderAttempted).toBeTrue(); - }); + }); + expect(result.propositions.length).toEqual(3); + result.propositions.forEach(proposition => { + expect(proposition.renderAttempted).toBeTrue(); }); }); it("it should ignore propositions with __view__ scope that have already been rendered", () => { - executeDecisions.and.returnValue(Promise.resolve(MIXED_PROPOSITIONS)); + const applyPropositions = createApplyPropositions({ render }); + const propositions = JSON.parse(JSON.stringify(MIXED_PROPOSITIONS)); + propositions[4].renderAttempted = true; - const applyPropositions = createApplyPropositions({ - executeDecisions + const result = applyPropositions({ + propositions }); - - return applyPropositions({ - propositions: MIXED_PROPOSITIONS - }).then(result => { - expect(result.propositions.length).toEqual(2); - result.propositions.forEach(proposition => { - expect(proposition.renderAttempted).toBeTrue(); - if (proposition.scope === "__view__") { - expect(proposition.items[0].id).not.toEqual("442358"); - } else { - expect(proposition.scope).toEqual("home"); - } - }); + expect(result.propositions.length).toEqual(2); + result.propositions.forEach(proposition => { + expect(proposition.renderAttempted).toBeTrue(); + if (proposition.scope === "__view__") { + expect(proposition.items[0].id).not.toEqual("442358"); + } else { + expect(proposition.scope).toEqual("home"); + } }); }); it("it should ignore items with unsupported schemas", () => { - executeDecisions.and.returnValue(Promise.resolve(MIXED_PROPOSITIONS)); - const expectedItemIds = ["442358", "442359"]; - const applyPropositions = createApplyPropositions({ - executeDecisions - }); + const applyPropositions = createApplyPropositions({ render }); - return applyPropositions({ + const { propositions } = applyPropositions({ propositions: MIXED_PROPOSITIONS - }).then(() => { - const executedPropositions = executeDecisions.calls.first().args[0]; - expect(executedPropositions.length).toEqual(2); - executedPropositions.forEach(proposition => { - expect(proposition.items.length).toEqual(1); - proposition.items.forEach(item => { - expect(expectedItemIds.indexOf(item.id) > -1); - }); + }); + expect(propositions.length).toEqual(3); + propositions.forEach(proposition => { + expect(proposition.items.length).toEqual(1); + proposition.items.forEach(item => { + expect(expectedItemIds.indexOf(item.id) > -1); }); }); }); it("it should not mutate original propositions", () => { - executeDecisions.and.returnValue(Promise.resolve(MIXED_PROPOSITIONS)); - - const applyPropositions = createApplyPropositions({ - executeDecisions - }); + const applyPropositions = createApplyPropositions({ render }); const originalPropositions = clone(MIXED_PROPOSITIONS); - return applyPropositions({ + const result = applyPropositions({ propositions: originalPropositions, metadata: METADATA - }).then(result => { - let numReturnedPropositions = 0; - expect(originalPropositions).toEqual(MIXED_PROPOSITIONS); - result.propositions.forEach(proposition => { - const [original] = originalPropositions.filter( - originalProposition => originalProposition.id === proposition.id - ); - if (original) { - numReturnedPropositions += 1; - expect(proposition).not.toBe(original); - } - }); - expect(numReturnedPropositions).toEqual(3); }); + + let numReturnedPropositions = 0; + expect(originalPropositions).toEqual(MIXED_PROPOSITIONS); + result.propositions.forEach(proposition => { + const [original] = originalPropositions.filter( + originalProposition => originalProposition.id === proposition.id + ); + if (original) { + numReturnedPropositions += 1; + expect(proposition).not.toBe(original); + } + }); + expect(numReturnedPropositions).toEqual(4); }); }); diff --git a/test/unit/specs/components/Personalization/createAutoRenderingHandler.spec.js b/test/unit/specs/components/Personalization/createAutoRenderingHandler.spec.js deleted file mode 100644 index e6756d060..000000000 --- a/test/unit/specs/components/Personalization/createAutoRenderingHandler.spec.js +++ /dev/null @@ -1,239 +0,0 @@ -/* -Copyright 2021 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ - -import { - CART_VIEW_DECISIONS, - PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS, - SCOPES_FOO1_FOO2_DECISIONS -} from "./responsesMock/eventResponses"; -import createAutoRenderingHandler from "../../../../../src/components/Personalization/createAutoRenderingHandler"; -import isPageWideScope from "../../../../../src/components/Personalization/utils/isPageWideScope"; - -describe("Personalization::createAutoRenderingHandler", () => { - let viewCache; - let executeDecisions; - let showContainers; - let collect; - let pageWideScopeDecisions; - let nonAutoRenderableDecisions; - - beforeEach(() => { - showContainers = jasmine.createSpy("showContainers"); - viewCache = jasmine.createSpyObj("viewCache", ["getView"]); - collect = jasmine.createSpy("collect"); - executeDecisions = jasmine.createSpy("executeDecisions"); - pageWideScopeDecisions = PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS; - nonAutoRenderableDecisions = SCOPES_FOO1_FOO2_DECISIONS; - }); - - it("it should fetch decisions from cache when viewName is present", () => { - const viewName = "cart"; - const executeViewDecisionsPromise = { - then: callback => callback(CART_VIEW_DECISIONS) - }; - const executePageWideScopeDecisionsPromise = { - then: callback => - callback(PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS) - }; - - viewCache.getView.and.returnValue(Promise.resolve(CART_VIEW_DECISIONS)); - const autorenderingHandler = createAutoRenderingHandler({ - viewCache, - executeDecisions, - showContainers, - collect - }); - - executeDecisions.and.returnValues( - executePageWideScopeDecisionsPromise, - executeViewDecisionsPromise - ); - - return autorenderingHandler({ - viewName, - pageWideScopeDecisions, - nonAutoRenderableDecisions - }).then(result => { - expect(viewCache.getView).toHaveBeenCalledWith("cart"); - expect(executeDecisions).toHaveBeenCalledTimes(2); - expect(executeDecisions.calls.all()[0].args[0]).toEqual( - PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS - ); - expect(executeDecisions.calls.all()[1].args[0]).toEqual( - CART_VIEW_DECISIONS - ); - - result.decisions.forEach(decision => { - expect(decision.renderAttempted).toBeUndefined(); - }); - - result.propositions.forEach(proposition => { - if ( - isPageWideScope(proposition.scope) || - proposition.scope === viewName - ) { - expect(proposition.renderAttempted).toEqual(true); - } else { - expect(proposition.renderAttempted).toEqual(false); - } - }); - - expect(collect).toHaveBeenCalledTimes(2); - expect(collect.calls.all()[0].args[0]).toEqual({ - decisionsMeta: PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS - }); - expect(collect.calls.all()[1].args[0]).toEqual({ - decisionsMeta: CART_VIEW_DECISIONS, - viewName - }); - expect(showContainers).toHaveBeenCalled(); - }); - }); - - it("it should execute page wide scope decisions when no viewName", () => { - const viewName = undefined; - const executePageWideScopeDecisionsPromise = { - then: callback => - callback(PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS) - }; - executeDecisions.and.returnValue(executePageWideScopeDecisionsPromise); - - const autorenderingHandler = createAutoRenderingHandler({ - viewCache, - executeDecisions, - collect, - showContainers - }); - - return autorenderingHandler({ - viewName, - pageWideScopeDecisions, - nonAutoRenderableDecisions - }).then(result => { - result.decisions.forEach(decision => { - expect(decision.renderAttempted).toBeUndefined(); - }); - - result.propositions.forEach(proposition => { - if (isPageWideScope(proposition.scope)) { - expect(proposition.renderAttempted).toEqual(true); - } else { - expect(proposition.renderAttempted).toEqual(false); - } - }); - expect(viewCache.getView).not.toHaveBeenCalled(); - expect(executeDecisions).toHaveBeenCalledTimes(1); - expect(executeDecisions).toHaveBeenCalledWith(pageWideScopeDecisions); - expect(collect).toHaveBeenCalledTimes(1); - expect(collect).toHaveBeenCalledWith({ - decisionsMeta: pageWideScopeDecisions - }); - expect(showContainers).toHaveBeenCalled(); - }); - }); - - it("it calls collect if cache returns empty array for a view", () => { - const viewName = "cart"; - const promise = { - then: callback => callback([]) - }; - const executePageWideScopeDecisionsPromise = { - then: callback => - callback(PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS) - }; - const executeViewDecisionsPromise = { - then: callback => callback([]) - }; - viewCache.getView.and.returnValue(promise); - executeDecisions.and.returnValues( - executePageWideScopeDecisionsPromise, - executeViewDecisionsPromise - ); - - const autorenderingHandler = createAutoRenderingHandler({ - viewCache, - executeDecisions, - showContainers, - collect - }); - - return autorenderingHandler({ - viewName, - pageWideScopeDecisions, - nonAutoRenderableDecisions - }).then(result => { - expect(viewCache.getView).toHaveBeenCalledWith("cart"); - expect(executeDecisions).toHaveBeenCalledWith(pageWideScopeDecisions); - expect(showContainers).toHaveBeenCalled(); - expect(collect).toHaveBeenCalledTimes(2); - expect(collect.calls.all()[1].args[0]).toEqual({ - decisionsMeta: [], - viewName - }); - result.decisions.forEach(decision => { - expect(decision.renderAttempted).toBeUndefined(); - }); - - result.propositions.forEach(proposition => { - if ( - isPageWideScope(proposition.scope) || - proposition.scope === viewName - ) { - expect(proposition.renderAttempted).toEqual(true); - } else { - expect(proposition.renderAttempted).toEqual(false); - } - }); - }); - }); - - it("it shouldn't call collect when no pageWideScopeDecisions and no viewName", () => { - const viewName = undefined; - - const executePageWideScopeDecisionsPromise = { - then: callback => callback([]) - }; - - executeDecisions.and.returnValue(executePageWideScopeDecisionsPromise); - - const autorenderingHandler = createAutoRenderingHandler({ - viewCache, - executeDecisions, - showContainers, - collect - }); - - return autorenderingHandler({ - viewName, - pageWideScopeDecisions, - nonAutoRenderableDecisions - }).then(result => { - expect(viewCache.getView).not.toHaveBeenCalled(); - expect(showContainers).toHaveBeenCalled(); - expect(collect).not.toHaveBeenCalled(); - result.decisions.forEach(decision => { - expect(decision.renderAttempted).toBeUndefined(); - }); - - result.propositions.forEach(proposition => { - if ( - isPageWideScope(proposition.scope) || - proposition.scope === viewName - ) { - expect(proposition.renderAttempted).toEqual(true); - } else { - expect(proposition.renderAttempted).toEqual(false); - } - }); - }); - }); -}); diff --git a/test/unit/specs/components/Personalization/createComponent.spec.js b/test/unit/specs/components/Personalization/createComponent.spec.js index fcb8d92d3..e674d3943 100644 --- a/test/unit/specs/components/Personalization/createComponent.spec.js +++ b/test/unit/specs/components/Personalization/createComponent.spec.js @@ -24,6 +24,7 @@ describe("Personalization", () => { let event; let personalizationComponent; let setTargetMigration; + let cacheUpdate; const build = () => { personalizationComponent = createComponent({ @@ -56,8 +57,10 @@ describe("Personalization", () => { mergeQuery = jasmine.createSpy("mergeQuery"); viewCache = jasmine.createSpyObj("viewCache", [ "isInitialized", - "storeViews" + "createCacheUpdate" ]); + cacheUpdate = jasmine.createSpyObj("cacheUpdate", ["update", "cancel"]); + viewCache.createCacheUpdate.and.returnValue(cacheUpdate); setTargetMigration = jasmine.createSpy("setTargetMigration"); build(); @@ -85,7 +88,7 @@ describe("Personalization", () => { expect(viewChangeHandler).not.toHaveBeenCalled(); expect(onClickHandler).not.toHaveBeenCalled(); expect(showContainers).not.toHaveBeenCalled(); - expect(viewCache.storeViews).not.toHaveBeenCalled(); + expect(viewCache.createCacheUpdate).not.toHaveBeenCalled(); }); it("should trigger pageLoad if there are decisionScopes", () => { @@ -104,7 +107,7 @@ describe("Personalization", () => { expect(viewChangeHandler).not.toHaveBeenCalled(); expect(mergeQuery).not.toHaveBeenCalled(); expect(onClickHandler).not.toHaveBeenCalled(); - expect(viewCache.storeViews).toHaveBeenCalled(); + expect(viewCache.createCacheUpdate).toHaveBeenCalled(); }); it("should trigger pageLoad if cache is not initialized", () => { const renderDecisions = false; @@ -124,7 +127,7 @@ describe("Personalization", () => { expect(viewChangeHandler).not.toHaveBeenCalled(); expect(mergeQuery).not.toHaveBeenCalled(); expect(onClickHandler).not.toHaveBeenCalled(); - expect(viewCache.storeViews).toHaveBeenCalled(); + expect(viewCache.createCacheUpdate).toHaveBeenCalled(); }); it("should trigger viewHandler if cache is initialized and viewName is provided", () => { const renderDecisions = false; @@ -145,7 +148,7 @@ describe("Personalization", () => { expect(viewChangeHandler).toHaveBeenCalled(); expect(mergeQuery).not.toHaveBeenCalled(); expect(onClickHandler).not.toHaveBeenCalled(); - expect(viewCache.storeViews).not.toHaveBeenCalled(); + expect(viewCache.createCacheUpdate).not.toHaveBeenCalled(); }); it("should trigger onClickHandler at onClick", () => { personalizationComponent.lifecycle.onClick({ event }); diff --git a/test/unit/specs/components/Personalization/createExecuteDecisions.spec.js b/test/unit/specs/components/Personalization/createExecuteDecisions.spec.js deleted file mode 100644 index 31759f179..000000000 --- a/test/unit/specs/components/Personalization/createExecuteDecisions.spec.js +++ /dev/null @@ -1,127 +0,0 @@ -/* -Copyright 2020 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ - -import createExecuteDecisions from "../../../../../src/components/Personalization/createExecuteDecisions"; - -describe("Personalization::createExecuteDecisions", () => { - let logger; - let executeActions; - let collect; - - const decisions = [ - { - id: 1, - scope: "foo", - scopeDetails: { - test: "blah1" - }, - items: [ - { - schema: "https://ns.adobe.com/personalization/dom-action", - data: { - type: "setHtml", - selector: "#foo", - content: "
Hola Mundo
" - } - } - ] - }, - { - id: 5, - scope: "__view__", - scopeDetails: { - test: "blah2" - }, - items: [ - { - schema: "https://ns.adobe.com/personalization/dom-action", - data: { - type: "setHtml", - selector: "#foo2", - content: "
offer 2
" - } - } - ] - } - ]; - const expectedAction = [ - { - type: "setHtml", - selector: "#foo", - content: "
Hola Mundo
", - meta: { - id: decisions[0].id, - scope: "foo", - scopeDetails: { - test: "blah1" - } - } - } - ]; - const metas = [ - { - id: decisions[0].id, - scope: decisions[0].scope, - scopeDetails: decisions[0].scopeDetails - }, - { - id: decisions[1].id, - scope: decisions[1].scope, - scopeDetails: decisions[1].scopeDetails - } - ]; - const modules = { - foo() {} - }; - - beforeEach(() => { - logger = jasmine.createSpyObj("logger", ["info", "warn", "error"]); - collect = jasmine.createSpy(); - executeActions = jasmine.createSpy(); - }); - - it("should trigger executeActions when provided with an array of actions", () => { - executeActions.and.returnValues( - [{ meta: metas[0] }, { meta: metas[0] }], - [{ meta: metas[1], error: "could not render this item" }] - ); - const executeDecisions = createExecuteDecisions({ - modules, - logger, - executeActions - }); - return executeDecisions(decisions).then(() => { - expect(executeActions).toHaveBeenCalledWith( - expectedAction, - modules, - logger - ); - expect(logger.warn).toHaveBeenCalledWith({ - meta: metas[1], - error: "could not render this item" - }); - }); - }); - - it("shouldn't trigger executeActions when provided with empty array of actions", () => { - executeActions.and.callThrough(); - const executeDecisions = createExecuteDecisions({ - modules, - logger, - executeActions, - collect - }); - return executeDecisions([]).then(() => { - expect(executeActions).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js b/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js index d35788f0d..94b0d106e 100644 --- a/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js +++ b/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js @@ -11,86 +11,124 @@ governing permissions and limitations under the License. */ import createFetchDataHandler from "../../../../../src/components/Personalization/createFetchDataHandler"; +import { createProposition } from "../../../../../src/components/Personalization/handlers/proposition"; +import flushPromiseChains from "../../../helpers/flushPromiseChains"; describe("Personalization::createFetchDataHandler", () => { - let responseHandler; + let prehidingStyle; + let showContainers; let hideContainers; let mergeQuery; + let collect; + let render; + + let cacheUpdate; let personalizationDetails; - let decisionsDeferred; - const config = { - prehidingStyle: "body {opacity:0;}" - }; - let onResponse = jasmine.createSpy(); - const event = {}; + let event; + let onResponse; + let response; beforeEach(() => { - response = jasmine.createSpyObj("response", ["getPayloadsByType"]); - responseHandler = jasmine.createSpy(); - mergeQuery = jasmine.createSpy(); + prehidingStyle = "myprehidingstyle"; + showContainers = jasmine.createSpy("showContainers"); + hideContainers = jasmine.createSpy("hideContainers"); + mergeQuery = jasmine.createSpy("mergeQuery"); + collect = jasmine.createSpy("collect"); + render = jasmine.createSpy("render"); + cacheUpdate = jasmine.createSpyObj("cacheUpdate", ["update"]); personalizationDetails = jasmine.createSpyObj("personalizationDetails", [ "isRenderDecisions", - "createQueryDetails" + "createQueryDetails", + "getViewName" ]); - hideContainers = jasmine.createSpy("hideContainers"); - decisionsDeferred = jasmine.createSpyObj("decisionsDeferred", ["reject"]); + personalizationDetails.createQueryDetails.and.returnValue("myquerydetails"); + event = "myevent"; + onResponse = jasmine.createSpy(); + response = jasmine.createSpyObj("response", ["getPayloadsByType"]); }); - it("should hide containers if renderDecisions is true", () => { + const run = () => { const fetchDataHandler = createFetchDataHandler({ - config, - responseHandler, + prehidingStyle, + showContainers, hideContainers, - mergeQuery + mergeQuery, + collect, + render }); - personalizationDetails.isRenderDecisions.and.returnValue(true); - fetchDataHandler({ - decisionsDeferred, + cacheUpdate, personalizationDetails, event, onResponse }); + }; + + const returnResponse = () => { + expect(onResponse).toHaveBeenCalledTimes(1); + const callback = onResponse.calls.argsFor(0)[0]; + return callback({ response }); + }; + + it("should hide containers if renderDecisions is true", () => { + personalizationDetails.isRenderDecisions.and.returnValue(true); + run(); expect(hideContainers).toHaveBeenCalled(); }); + it("shouldn't hide containers if renderDecisions is false", () => { - const fetchDataHandler = createFetchDataHandler({ - config, - responseHandler, - hideContainers, - mergeQuery - }); personalizationDetails.isRenderDecisions.and.returnValue(false); - fetchDataHandler({ - decisionsDeferred, - personalizationDetails, - event, - onResponse - }); - + run(); expect(hideContainers).not.toHaveBeenCalled(); }); it("should trigger responseHandler at onResponse", () => { - const fetchDataHandler = createFetchDataHandler({ - config, - responseHandler, - hideContainers, - mergeQuery - }); personalizationDetails.isRenderDecisions.and.returnValue(false); - onResponse = callback => { - callback(response); - }; - fetchDataHandler({ - decisionsDeferred, - personalizationDetails, - event, - onResponse + run(); + response.getPayloadsByType.and.returnValue([]); + cacheUpdate.update.and.returnValue([]); + const result = returnResponse(); + expect(result).toEqual({ + propositions: [], + decisions: [] }); + }); - expect(hideContainers).not.toHaveBeenCalled(); - expect(responseHandler).toHaveBeenCalled(); + it("should render decisions", async () => { + personalizationDetails.isRenderDecisions.and.returnValue(true); + personalizationDetails.getViewName.and.returnValue("myviewname"); + render = propositions => { + propositions[0].addRenderer(0, () => {}); + propositions[0].includeInDisplayNotification(); + const decisionsMeta = []; + propositions[0].addToNotifications(decisionsMeta); + return Promise.resolve(decisionsMeta); + }; + run(); + response.getPayloadsByType.and.returnValue([ + { id: "handle1" }, + { id: "handle2" } + ]); + cacheUpdate.update.and.returnValue([ + createProposition({ id: "handle1", items: ["item1"] }) + ]); + const result = returnResponse(); + expect(result).toEqual({ + propositions: [ + { id: "handle1", items: ["item1"], renderAttempted: true } + ], + decisions: [] + }); + await flushPromiseChains(); + expect(showContainers).toHaveBeenCalled(); + expect(collect).toHaveBeenCalledOnceWith({ + decisionsMeta: [ + { id: "handle1", scope: undefined, scopeDetails: undefined } + ], + viewName: "myviewname" + }); }); + + // TODO - test the rest of the functionality }); diff --git a/test/unit/specs/components/Personalization/createNonRenderingHandler.spec.js b/test/unit/specs/components/Personalization/createNonRenderingHandler.spec.js deleted file mode 100644 index bc54c8646..000000000 --- a/test/unit/specs/components/Personalization/createNonRenderingHandler.spec.js +++ /dev/null @@ -1,86 +0,0 @@ -/* -Copyright 2021 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ - -import { - CART_VIEW_DECISIONS, - PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS, - REDIRECT_PAGE_WIDE_SCOPE_DECISION, - SCOPES_FOO1_FOO2_DECISIONS -} from "./responsesMock/eventResponses"; -import createNonRenderingHandler from "../../../../../src/components/Personalization/createNonRenderingHandler"; - -describe("Personalization::createNonRenderingHandler", () => { - let viewCache; - let pageWideScopeDecisions; - let nonAutoRenderableDecisions; - let cartViewDecisions; - let redirectDecisions; - - beforeEach(() => { - redirectDecisions = REDIRECT_PAGE_WIDE_SCOPE_DECISION; - cartViewDecisions = CART_VIEW_DECISIONS; - pageWideScopeDecisions = PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS; - nonAutoRenderableDecisions = SCOPES_FOO1_FOO2_DECISIONS; - viewCache = jasmine.createSpyObj("viewCache", ["getView"]); - }); - - it("it should fetch decisions from cache when viewName is present", () => { - const viewName = "cart"; - const promise = { - then: callback => callback(cartViewDecisions) - }; - viewCache.getView.and.returnValue(promise); - - const nonRenderingHandler = createNonRenderingHandler({ - viewCache - }); - - return nonRenderingHandler({ - viewName, - redirectDecisions, - pageWideScopeDecisions, - nonAutoRenderableDecisions - }).then(result => { - expect(viewCache.getView).toHaveBeenCalledWith("cart"); - expect(result.decisions.length).toBe(6); - result.decisions.forEach(decision => { - expect(decision.renderAttempted).toBeUndefined(); - }); - result.propositions.forEach(proposition => { - expect(proposition.renderAttempted).toEqual(false); - }); - }); - }); - - it("it should not trigger viewCache when no viewName", () => { - const viewName = undefined; - const nonRenderingHandler = createNonRenderingHandler({ - viewCache - }); - - return nonRenderingHandler({ - viewName, - redirectDecisions, - pageWideScopeDecisions, - nonAutoRenderableDecisions - }).then(result => { - expect(viewCache.getView).not.toHaveBeenCalled(); - expect(result.decisions.length).toBe(5); - result.decisions.forEach(decision => { - expect(decision.renderAttempted).toBeUndefined(); - }); - result.propositions.forEach(proposition => { - expect(proposition.renderAttempted).toEqual(false); - }); - }); - }); -}); diff --git a/test/unit/specs/components/Personalization/createOnResponseHandler.spec.js b/test/unit/specs/components/Personalization/createOnResponseHandler.spec.js deleted file mode 100644 index c70da3deb..000000000 --- a/test/unit/specs/components/Personalization/createOnResponseHandler.spec.js +++ /dev/null @@ -1,206 +0,0 @@ -/* -Copyright 2020 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ - -import { - CART_VIEW_DECISIONS, - PAGE_WIDE_SCOPE_DECISIONS, - PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS, - PAGE_WIDE_SCOPE_DECISIONS_WITHOUT_DOM_ACTION_SCHEMA_ITEMS, - PRODUCTS_VIEW_DECISIONS, - REDIRECT_PAGE_WIDE_SCOPE_DECISION -} from "./responsesMock/eventResponses"; -import createOnResponseHandler from "../../../../../src/components/Personalization/createOnResponseHandler"; - -describe("Personalization::onResponseHandler", () => { - const nonDomActionDecisions = PAGE_WIDE_SCOPE_DECISIONS_WITHOUT_DOM_ACTION_SCHEMA_ITEMS; - const unprocessedDecisions = [ - ...PAGE_WIDE_SCOPE_DECISIONS, - ...CART_VIEW_DECISIONS, - ...PRODUCTS_VIEW_DECISIONS - ]; - const pageWideScopeDecisions = PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS; - - let groupDecisions; - let autoRenderingHandler; - let nonRenderingHandler; - let showContainers; - let response; - let personalizationDetails; - let decisionsDeferred; - let handleRedirectDecisions; - - beforeEach(() => { - response = jasmine.createSpyObj("response", ["getPayloadsByType"]); - personalizationDetails = jasmine.createSpyObj("personalizationDetails", [ - "isRenderDecisions", - "getViewName" - ]); - groupDecisions = jasmine.createSpy(); - decisionsDeferred = jasmine.createSpyObj("decisionsDeferred", [ - "defer", - "reject", - "resolve" - ]); - autoRenderingHandler = jasmine.createSpy("autoRenderingHandler"); - showContainers = jasmine.createSpy("showContainers"); - nonRenderingHandler = jasmine.createSpy("nonRenderingHandler"); - handleRedirectDecisions = jasmine.createSpy("handleRedirectDecisions"); - }); - - it("should trigger autoRenderingHandler when renderDecisions is true", () => { - const nonPageWideScopeDecisions = { - cart: CART_VIEW_DECISIONS, - products: PRODUCTS_VIEW_DECISIONS - }; - groupDecisions.and.returnValues({ - redirectDecisions: [], - pageWideScopeDecisions, - viewDecisions: nonPageWideScopeDecisions, - nonAutoRenderableDecisions: nonDomActionDecisions - }); - - response.getPayloadsByType.and.returnValue(unprocessedDecisions); - personalizationDetails.isRenderDecisions.and.returnValue(true); - personalizationDetails.getViewName.and.returnValue(undefined); - const onResponse = createOnResponseHandler({ - window, - groupDecisions, - nonRenderingHandler, - autoRenderingHandler, - handleRedirectDecisions, - showContainers - }); - - onResponse({ - decisionsDeferred, - personalizationDetails, - response - }); - expect(decisionsDeferred.resolve).toHaveBeenCalledWith( - nonPageWideScopeDecisions - ); - expect(autoRenderingHandler).toHaveBeenCalledWith({ - viewName: undefined, - pageWideScopeDecisions, - nonAutoRenderableDecisions: nonDomActionDecisions - }); - }); - it("should trigger nonRenderingHandler when renderDecisions is false", () => { - const nonPageWideScopeDecisions = { - products: PRODUCTS_VIEW_DECISIONS - }; - groupDecisions.and.returnValues({ - redirectDecisions: [], - pageWideScopeDecisions, - viewDecisions: nonPageWideScopeDecisions, - nonAutoRenderableDecisions: nonDomActionDecisions - }); - - response.getPayloadsByType.and.returnValue(unprocessedDecisions); - personalizationDetails.isRenderDecisions.and.returnValue(false); - personalizationDetails.getViewName.and.returnValue(undefined); - const onResponse = createOnResponseHandler({ - window, - groupDecisions, - nonRenderingHandler, - autoRenderingHandler, - handleRedirectDecisions, - showContainers - }); - - onResponse({ - decisionsDeferred, - personalizationDetails, - response - }); - expect(decisionsDeferred.resolve).toHaveBeenCalledWith( - nonPageWideScopeDecisions - ); - expect(nonRenderingHandler).toHaveBeenCalledWith({ - viewName: undefined, - redirectDecisions: [], - pageWideScopeDecisions, - nonAutoRenderableDecisions: nonDomActionDecisions - }); - }); - - it("should trigger showContainers if personalizationDetails payload is empty and return empty array", () => { - const expectedResult = { - decisions: [], - propositions: [] - }; - response.getPayloadsByType.and.returnValue([]); - personalizationDetails.isRenderDecisions.and.returnValue(false); - personalizationDetails.getViewName.and.returnValue("cart"); - - const onResponse = createOnResponseHandler({ - window, - groupDecisions, - nonRenderingHandler, - autoRenderingHandler, - handleRedirectDecisions, - showContainers - }); - const result = onResponse({ - decisionsDeferred, - personalizationDetails, - response - }); - expect(decisionsDeferred.resolve).toHaveBeenCalledWith({}); - expect(showContainers).toHaveBeenCalled(); - expect(groupDecisions).not.toHaveBeenCalled(); - expect(nonRenderingHandler).not.toHaveBeenCalled(); - expect(autoRenderingHandler).not.toHaveBeenCalled(); - expect(result).toEqual(expectedResult); - }); - - it("should trigger redirect handler when renderDecisions is true and there are redirectDecisions", () => { - const payload = [ - ...PAGE_WIDE_SCOPE_DECISIONS, - ...CART_VIEW_DECISIONS, - ...PRODUCTS_VIEW_DECISIONS, - ...REDIRECT_PAGE_WIDE_SCOPE_DECISION - ]; - const nonPageWideScopeDecisions = { - cart: CART_VIEW_DECISIONS, - products: PRODUCTS_VIEW_DECISIONS - }; - groupDecisions.and.returnValues({ - redirectDecisions: REDIRECT_PAGE_WIDE_SCOPE_DECISION, - pageWideScopeDecisions, - viewDecisions: nonPageWideScopeDecisions, - nonAutoRenderableDecisions: nonDomActionDecisions - }); - - response.getPayloadsByType.and.returnValue(payload); - personalizationDetails.isRenderDecisions.and.returnValue(true); - personalizationDetails.getViewName.and.returnValue("cart"); - const onResponse = createOnResponseHandler({ - window, - groupDecisions, - nonRenderingHandler, - autoRenderingHandler, - handleRedirectDecisions, - showContainers - }); - const result = onResponse({ - decisionsDeferred, - personalizationDetails, - response - }); - expect(decisionsDeferred.resolve).toHaveBeenCalledWith({}); - expect(showContainers).not.toHaveBeenCalled(); - expect(nonRenderingHandler).not.toHaveBeenCalled(); - expect(autoRenderingHandler).not.toHaveBeenCalled(); - expect(result).toEqual(undefined); - }); -}); diff --git a/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js b/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js index 6ebe0fb65..c3671c583 100644 --- a/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js +++ b/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js @@ -31,12 +31,10 @@ describe("Personalization::createPersonalizationDetails", () => { const getPageLocation = createGetPageLocation({ window }); let event; - let viewCache; let logger; beforeEach(() => { event = jasmine.createSpyObj("event", ["getViewName"]); - viewCache = jasmine.createSpyObj("viewCache", ["getView", "isInitialized"]); logger = jasmine.createSpyObj("logger", ["info", "warn", "error"]); }); @@ -51,10 +49,9 @@ describe("Personalization::createPersonalizationDetails", () => { decisionScopes, personalization, event, - viewCache, + isCacheInitialized: false, logger }); - viewCache.isInitialized.and.returnValue(false); const expectedDecisionScopes = [PAGE_WIDE_SCOPE]; const expectedQueryDetails = { schemas: [ @@ -89,10 +86,9 @@ describe("Personalization::createPersonalizationDetails", () => { decisionScopes, personalization, event, - viewCache, + isCacheInitialized: false, logger }); - viewCache.isInitialized.and.returnValue(false); const expectedDecisionScopes = [PAGE_WIDE_SCOPE]; const expectedQueryDetails = { schemas: [ @@ -127,10 +123,9 @@ describe("Personalization::createPersonalizationDetails", () => { decisionScopes, personalization, event, - viewCache, + isCacheInitialized: false, logger }); - viewCache.isInitialized.and.returnValue(false); const expectedDecisionScopes = ["test1", "__view__"]; const expectedQueryDetails = { schemas: [ @@ -165,10 +160,9 @@ describe("Personalization::createPersonalizationDetails", () => { personalization, decisionScopes, event, - viewCache, + isCacheInitialized: true, logger }); - viewCache.isInitialized.and.returnValue(true); const expectedDecisionScopes = ["test1"]; const expectedQueryDetails = { schemas: [ @@ -204,10 +198,9 @@ describe("Personalization::createPersonalizationDetails", () => { decisionScopes, personalization, event, - viewCache, + isCacheInitialized: true, logger }); - viewCache.isInitialized.and.returnValue(true); const expectedDecisionScopes = []; const expectedQueryDetails = { schemas: [ @@ -244,10 +237,9 @@ describe("Personalization::createPersonalizationDetails", () => { decisionScopes, personalization, event, - viewCache, + isCacheInitialized: true, logger }); - viewCache.isInitialized.and.returnValue(true); const expectedDecisionScopes = ["test1", "test2"]; const expectedQueryDetails = { @@ -281,10 +273,9 @@ describe("Personalization::createPersonalizationDetails", () => { decisionScopes, personalization, event, - viewCache, + isCacheInitialized: true, logger }); - viewCache.isInitialized.and.returnValue(true); expect(personalizationDetails.isRenderDecisions()).toEqual(true); expect(personalizationDetails.hasScopes()).toEqual(false); @@ -305,10 +296,9 @@ describe("Personalization::createPersonalizationDetails", () => { decisionScopes, personalization, event, - viewCache, + isCacheInitialized: true, logger }); - viewCache.isInitialized.and.returnValue(true); expect(personalizationDetails.isRenderDecisions()).toEqual(false); expect(personalizationDetails.hasScopes()).toEqual(false); @@ -329,10 +319,9 @@ describe("Personalization::createPersonalizationDetails", () => { personalization, decisionScopes, event, - viewCache, + isCacheInitialized: true, logger }); - viewCache.isInitialized.and.returnValue(true); expect(personalizationDetails.isRenderDecisions()).toEqual(true); expect(personalizationDetails.hasScopes()).toEqual(false); @@ -353,10 +342,9 @@ describe("Personalization::createPersonalizationDetails", () => { personalization, decisionScopes, event, - viewCache, + isCacheInitialized: true, logger }); - viewCache.isInitialized.and.returnValue(true); expect(personalizationDetails.isRenderDecisions()).toEqual(false); expect(personalizationDetails.hasScopes()).toEqual(false); @@ -379,10 +367,9 @@ describe("Personalization::createPersonalizationDetails", () => { decisionScopes, personalization, event, - viewCache, + isCacheInitialized: true, logger }); - viewCache.isInitialized.and.returnValue(true); const expectedDecisionScopes = ["__view__"]; const expectedQueryDetails = { @@ -417,7 +404,7 @@ describe("Personalization::createPersonalizationDetails", () => { decisionScopes, personalization, event, - viewCache, + isCacheInitialized: false, logger }); expect(personalizationDetails.isRenderDecisions()).toEqual(true); diff --git a/test/unit/specs/components/Personalization/createRedirectHandler.spec.js b/test/unit/specs/components/Personalization/createRedirectHandler.spec.js deleted file mode 100644 index 0bad6680e..000000000 --- a/test/unit/specs/components/Personalization/createRedirectHandler.spec.js +++ /dev/null @@ -1,75 +0,0 @@ -/* -Copyright 2021 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ - -import { REDIRECT_PAGE_WIDE_SCOPE_DECISION } from "./responsesMock/eventResponses"; -import createRedirectHandler from "../../../../../src/components/Personalization/createRedirectHandler"; - -describe("Personalization::createRedirectDecisionHandler", () => { - let collect; - let showContainers; - let logger; - - const documentMayUnload = true; - const decisions = REDIRECT_PAGE_WIDE_SCOPE_DECISION; - const decisionsMeta = [ - { - id: decisions[0].id, - scope: decisions[0].scope, - scopeDetails: decisions[0].scopeDetails - } - ]; - const replace = jasmine.createSpy(); - - const window = { - location: { replace } - }; - - beforeEach(() => { - collect = jasmine.createSpy().and.returnValue(Promise.resolve()); - logger = jasmine.createSpyObj("logger", ["warn"]); - showContainers = jasmine.createSpy("showContainers"); - }); - - it("should trigger collect before redirect", () => { - const handleRedirectDecisions = createRedirectHandler({ - collect, - window, - logger, - showContainers - }); - return handleRedirectDecisions(decisions).then(() => { - expect(collect).toHaveBeenCalledWith({ - decisionsMeta, - documentMayUnload - }); - expect(replace).toHaveBeenCalledWith(decisions[0].items[0].data.content); - }); - }); - it("should trigger showContainers and logger when redirect fails", () => { - replace.and.throwError("Malformed url"); - - const handleRedirectDecisions = createRedirectHandler({ - collect, - window, - logger, - showContainers - }); - return handleRedirectDecisions(decisions).then(() => { - expect(collect).toHaveBeenCalledWith({ - decisionsMeta, - documentMayUnload - }); - expect(showContainers).toHaveBeenCalled(); - expect(logger.warn).toHaveBeenCalled(); - }); - }); -}); diff --git a/test/unit/specs/components/Personalization/createViewCacheManager.spec.js b/test/unit/specs/components/Personalization/createViewCacheManager.spec.js index 0c7e2ec89..a5cf73290 100644 --- a/test/unit/specs/components/Personalization/createViewCacheManager.spec.js +++ b/test/unit/specs/components/Personalization/createViewCacheManager.spec.js @@ -11,74 +11,115 @@ governing permissions and limitations under the License. */ import createViewCacheManager from "../../../../../src/components/Personalization/createViewCacheManager"; -import { defer } from "../../../../../src/utils"; describe("Personalization::createCacheManager", () => { - const cartView = "cart"; - const homeView = "home"; - const productsView = "products"; - const viewDecisions = { - home: [ - { - id: "foo1", - items: [], - scope: "home" - }, - { - id: "foo2", - items: [], - scope: "home" + const viewHandles = [ + { + id: "foo1", + scope: "home", + scopeDetails: { + characteristics: { + scopeType: "view" + } } - ], - cart: [ - { - id: "foo3", - items: [], - scope: "cart" + }, + { + id: "foo2", + scope: "home", + scopeDetails: { + characteristics: { + scopeType: "view" + } + } + }, + { + id: "foo3", + scope: "cart", + scopeDetails: { + characteristics: { + scopeType: "view" + } } - ] - }; + }, + { + id: "foo4", + scope: "other" + } + ]; - it("stores and gets the decisions based on a viewName", () => { - const viewCacheManager = createViewCacheManager(); - const decisionsDeferred = defer(); + let createProposition; - viewCacheManager.storeViews(decisionsDeferred.promise); - decisionsDeferred.resolve(viewDecisions); + beforeEach(() => { + createProposition = viewHandle => { + const proposition = jasmine.createSpyObj("proposition", [ + "includeInDisplayNotification", + "excludeInReturnedPropositions", + "getHandle" + ]); + proposition.getHandle.and.returnValue(viewHandle); + return proposition; + }; + }); + + it("stores and gets the decisions based on a viewName", async () => { + const viewCacheManager = createViewCacheManager({ createProposition }); - return Promise.all([ - expectAsync(viewCacheManager.getView(cartView)).toBeResolvedTo( - viewDecisions[cartView] - ), - expectAsync(viewCacheManager.getView(homeView)).toBeResolvedTo( - viewDecisions[homeView] - ) + const cacheUpdate = viewCacheManager.createCacheUpdate("home"); + const resultingHandles = cacheUpdate.update(viewHandles); + expect(resultingHandles.map(h => h.getHandle())).toEqual([ + viewHandles[0], + viewHandles[1], + viewHandles[3] ]); - }); - it("gets an empty array if there is no decisions for a specific view", () => { - const viewCacheManager = createViewCacheManager(); - const decisionsDeferred = defer(); + const homeViews = await viewCacheManager.getView("home"); + expect(homeViews.map(h => h.getHandle())).toEqual([ + viewHandles[0], + viewHandles[1] + ]); - viewCacheManager.storeViews(decisionsDeferred.promise); - decisionsDeferred.resolve(viewDecisions); + const cartViews = await viewCacheManager.getView("cart"); + expect(cartViews.map(h => h.getHandle())).toEqual([viewHandles[2]]); - return Promise.all([ - expectAsync(viewCacheManager.getView(productsView)).toBeResolvedTo([]) + const otherViews = await viewCacheManager.getView("other"); + expect(otherViews.map(h => h.getHandle())).toEqual([ + { + scope: "other", + scopeDetails: { + characteristics: { + scopeType: "view" + } + } + } ]); }); - it("should be no views when decisions deferred is rejected", () => { - const viewCacheManager = createViewCacheManager(); - const decisionsDeferred = defer(); + it("should be no views when decisions deferred is rejected", async () => { + const viewCacheManager = createViewCacheManager({ createProposition }); + const cacheUpdate = viewCacheManager.createCacheUpdate("home"); + cacheUpdate.cancel(); + + const homeViews = await viewCacheManager.getView("home"); + expect(homeViews.map(h => h.getHandle())).toEqual([ + { + scope: "home", + scopeDetails: { + characteristics: { + scopeType: "view" + } + } + } + ]); + }); - viewCacheManager.storeViews(decisionsDeferred.promise); - decisionsDeferred.reject(); + it("should not be initialized when first created", () => { + const viewCacheManager = createViewCacheManager({ createProposition }); + expect(viewCacheManager.isInitialized()).toBe(false); + }); - return expectAsync(viewCacheManager.getView("cart")) - .toBeResolvedTo([]) - .then(() => { - expect(viewCacheManager.isInitialized()).toBeTrue(); - }); + it("should be initialized when first cache update is created", () => { + const viewCacheManager = createViewCacheManager({ createProposition }); + viewCacheManager.createCacheUpdate("home"); + expect(viewCacheManager.isInitialized()).toBe(true); }); }); diff --git a/test/unit/specs/components/Personalization/createViewChangeHandler.spec.js b/test/unit/specs/components/Personalization/createViewChangeHandler.spec.js index 134fd4d79..2d070c070 100644 --- a/test/unit/specs/components/Personalization/createViewChangeHandler.spec.js +++ b/test/unit/specs/components/Personalization/createViewChangeHandler.spec.js @@ -13,61 +13,63 @@ governing permissions and limitations under the License. import createViewChangeHandler from "../../../../../src/components/Personalization/createViewChangeHandler"; import { PropositionEventType } from "../../../../../src/components/Personalization/constants/propositionEventType"; import { CART_VIEW_DECISIONS } from "./responsesMock/eventResponses"; +import { createProposition } from "../../../../../src/components/Personalization/handlers/proposition"; describe("Personalization::createViewChangeHandler", () => { - let personalizationDetails; + let mergeDecisionsMeta; + let render; let viewCache; - const event = {}; - const onResponse = callback => callback(); - let executeDecisions; - let showContainers; - let mergeDecisionsMeta; - let collect; + let personalizationDetails; + let event; + let onResponse; beforeEach(() => { + mergeDecisionsMeta = jasmine.createSpy("mergeDecisionsMeta"); + render = jasmine.createSpy("render"); + viewCache = jasmine.createSpyObj("viewCache", ["getView"]); + personalizationDetails = jasmine.createSpyObj("personalizationDetails", [ "isRenderDecisions", "getViewName" ]); - viewCache = jasmine.createSpyObj("viewCache", ["getView"]); - executeDecisions = jasmine.createSpy("executeDecisions"); - showContainers = jasmine.createSpy("showContainers"); - mergeDecisionsMeta = jasmine.createSpy("mergeDecisionsMeta"); - collect = jasmine.createSpy("collect"); + event = "myevent"; + onResponse = jasmine.createSpy(); }); - it("should trigger executeDecisions if renderDecisions is true", () => { - const cartViewPromise = { - then: callback => callback(CART_VIEW_DECISIONS) - }; - - viewCache.getView.and.returnValue(cartViewPromise); - executeDecisions.and.returnValue(cartViewPromise); - personalizationDetails.isRenderDecisions.and.returnValue(true); - personalizationDetails.getViewName.and.returnValue("cart"); - + const run = async () => { const viewChangeHandler = createViewChangeHandler({ mergeDecisionsMeta, - collect, - executeDecisions, + render, viewCache }); - - viewChangeHandler({ + await viewChangeHandler({ event, personalizationDetails, onResponse }); - expect(executeDecisions).toHaveBeenCalledWith(CART_VIEW_DECISIONS); + return onResponse.calls.argsFor(0)[0](); + }; + + it("should trigger render if renderDecisions is true", async () => { + viewCache.getView.and.returnValue( + Promise.resolve(CART_VIEW_DECISIONS.map(createProposition)) + ); + personalizationDetails.isRenderDecisions.and.returnValue(true); + personalizationDetails.getViewName.and.returnValue("cart"); + render.and.returnValue(Promise.resolve("decisionMeta")); + + const result = await run(); + + expect(render).toHaveBeenCalledTimes(1); expect(mergeDecisionsMeta).toHaveBeenCalledWith( - event, - CART_VIEW_DECISIONS, + "myevent", + "decisionMeta", PropositionEventType.DISPLAY ); - expect(collect).not.toHaveBeenCalled(); + expect(result.decisions).toEqual(CART_VIEW_DECISIONS); }); - + /* it("should not trigger executeDecisions when render decisions is false", () => { const cartViewPromise = { then: callback => callback(CART_VIEW_DECISIONS) @@ -116,4 +118,5 @@ describe("Personalization::createViewChangeHandler", () => { expect(executeDecisions).toHaveBeenCalledWith([]); expect(collect).toHaveBeenCalled(); }); + */ }); diff --git a/test/unit/specs/components/Personalization/dom-actions/createPreprocess.spec.js b/test/unit/specs/components/Personalization/dom-actions/createPreprocess.spec.js new file mode 100644 index 000000000..41a07e3d1 --- /dev/null +++ b/test/unit/specs/components/Personalization/dom-actions/createPreprocess.spec.js @@ -0,0 +1,40 @@ +import createPreprocess from "../../../../../../src/components/Personalization/dom-actions/createPreprocess"; + +describe("Personalization::dom-actions::createPreprocess", () => { + let preprocessor1; + let preprocessor2; + let preprocess; + beforeEach(() => { + preprocessor1 = jasmine.createSpy("preprocessor1"); + preprocessor2 = jasmine.createSpy("preprocessor2"); + preprocess = createPreprocess([preprocessor1, preprocessor2]); + }); + + it("handles an empty action", () => { + expect(preprocess({})).toEqual({}); + }); + + it("passes the data through", () => { + preprocessor1.and.callFake(data => data); + preprocessor2.and.callFake(data => data); + expect(preprocess({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 }); + }); + + it("passes the data through when the preprocessor returns undefined", () => { + preprocessor1.and.callFake(() => undefined); + preprocessor2.and.callFake(() => undefined); + expect(preprocess({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 }); + }); + + it("updates the data", () => { + preprocessor1.and.callFake(() => ({ c: 3 })); + preprocessor2.and.callFake(() => ({ d: 4 })); + expect(preprocess({ a: 1, b: 2 })).toEqual({ a: 1, b: 2, c: 3, d: 4 }); + }); + + it("updates the data2", () => { + preprocessor1.and.callFake(data => ({ ...data, c: 3 })); + preprocessor2.and.callFake(data => ({ ...data, d: 4 })); + expect(preprocess({ a: 1, b: 2 })).toEqual({ a: 1, b: 2, c: 3, d: 4 }); + }); +}); diff --git a/test/unit/specs/components/Personalization/dom-actions/executeActions.spec.js b/test/unit/specs/components/Personalization/dom-actions/executeActions.spec.js deleted file mode 100644 index 029dcaba3..000000000 --- a/test/unit/specs/components/Personalization/dom-actions/executeActions.spec.js +++ /dev/null @@ -1,136 +0,0 @@ -/* -Copyright 2019 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ - -import executeActions from "../../../../../../src/components/Personalization/dom-actions/executeActions"; - -describe("Personalization::executeActions", () => { - it("should execute actions", () => { - const actionSpy = jasmine.createSpy().and.returnValue(Promise.resolve(1)); - const logger = jasmine.createSpyObj("logger", ["error", "info"]); - logger.enabled = true; - const actions = [{ type: "foo" }]; - const modules = { - foo: actionSpy - }; - - return executeActions(actions, modules, logger).then(result => { - expect(result).toEqual([1]); - expect(actionSpy).toHaveBeenCalled(); - expect(logger.info.calls.count()).toEqual(1); - expect(logger.error).not.toHaveBeenCalled(); - }); - }); - - it("should preprocess actions", () => { - const customCodeActionSpy = jasmine - .createSpy("customCodeActionSpy") - .and.returnValue(Promise.resolve(9)); - - const setHtmlActionSpy = jasmine - .createSpy("setHtmlActionSpy") - .and.returnValue(Promise.resolve(1)); - const appendHtmlActionSpy = jasmine - .createSpy("appendHtmlActionSpy") - .and.returnValue(Promise.resolve(2)); - const logger = jasmine.createSpyObj("logger", ["error", "info"]); - logger.enabled = true; - const actions = [ - { - type: "setHtml", - selector: "head", - content: - '

Unsupported tag content

' - }, - { - type: "customCode", - selector: "BODY > *:eq(0)", - content: "
superfluous
" - } - ]; - const modules = { - setHtml: setHtmlActionSpy, - appendHtml: appendHtmlActionSpy, - customCode: customCodeActionSpy - }; - - return executeActions(actions, modules, logger).then(result => { - expect(result).toEqual([2, 9]); - expect(setHtmlActionSpy).not.toHaveBeenCalled(); - expect(appendHtmlActionSpy).toHaveBeenCalledOnceWith( - jasmine.objectContaining({ - type: "appendHtml", - selector: "head", - content: '' - }) - ); - expect(logger.info.calls.count()).toEqual(2); - expect(logger.error).not.toHaveBeenCalled(); - - expect(customCodeActionSpy).toHaveBeenCalledOnceWith( - jasmine.objectContaining({ - type: "customCode", - selector: "BODY", - content: "
superfluous
" - }) - ); - }); - }); - - it("should not invoke logger.info when logger is not enabled", () => { - const actionSpy = jasmine.createSpy().and.returnValue(Promise.resolve(1)); - const logger = jasmine.createSpyObj("logger", ["error", "info"]); - logger.enabled = false; - const actions = [{ type: "foo" }]; - const modules = { - foo: actionSpy - }; - return executeActions(actions, modules, logger).then(result => { - expect(result).toEqual([1]); - expect(actionSpy).toHaveBeenCalled(); - expect(logger.info.calls.count()).toEqual(0); - expect(logger.error).not.toHaveBeenCalled(); - }); - }); - - it("should throw error when execute actions fails", () => { - const logger = jasmine.createSpyObj("logger", ["error", "info"]); - logger.enabled = true; - const actions = [{ type: "foo" }]; - const modules = { - foo: jasmine.createSpy().and.throwError("foo's error") - }; - - expect(() => executeActions(actions, modules, logger)).toThrowError(); - }); - - it("should log nothing when there are no actions", () => { - const logger = jasmine.createSpyObj("logger", ["error", "info"]); - const actions = []; - const modules = {}; - - return executeActions(actions, modules, logger).then(result => { - expect(result).toEqual([]); - expect(logger.info).not.toHaveBeenCalled(); - expect(logger.error).not.toHaveBeenCalled(); - }); - }); - - it("should throw error when there are no actions types", () => { - const logger = jasmine.createSpyObj("logger", ["error", "info"]); - logger.enabled = true; - const actions = [{ type: "foo1" }]; - const modules = { - foo: () => {} - }; - expect(() => executeActions(actions, modules, logger)).toThrowError(); - }); -}); diff --git a/test/unit/specs/components/Personalization/dom-actions/initDomActionsModules.spec.js b/test/unit/specs/components/Personalization/dom-actions/initDomActionsModules.spec.js index a308c7fca..6f671a9e2 100644 --- a/test/unit/specs/components/Personalization/dom-actions/initDomActionsModules.spec.js +++ b/test/unit/specs/components/Personalization/dom-actions/initDomActionsModules.spec.js @@ -31,8 +31,6 @@ const buildSet = () => { result.add("replaceHtml"); result.add("prependHtml"); result.add("appendHtml"); - result.add("click"); - result.add("defaultContent"); return result; }; diff --git a/test/unit/specs/components/Personalization/groupDecisions.spec.js b/test/unit/specs/components/Personalization/groupDecisions.spec.js deleted file mode 100644 index fe07dedda..000000000 --- a/test/unit/specs/components/Personalization/groupDecisions.spec.js +++ /dev/null @@ -1,114 +0,0 @@ -/* -Copyright 2020 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ - -import { - PAGE_WIDE_SCOPE_DECISIONS, - PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS, - PAGE_WIDE_SCOPE_DECISIONS_WITHOUT_DOM_ACTION_SCHEMA_ITEMS, - CART_VIEW_DECISIONS, - REDIRECT_PAGE_WIDE_SCOPE_DECISION, - PRODUCTS_VIEW_DECISIONS, - MERGED_METRIC_DECISIONS -} from "./responsesMock/eventResponses"; -import groupDecisions from "../../../../../src/components/Personalization/groupDecisions"; - -let cartDecisions; -let productDecisions; -let mergedDecisions; - -beforeEach(() => { - cartDecisions = PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS.concat( - CART_VIEW_DECISIONS - ); - productDecisions = PAGE_WIDE_SCOPE_DECISIONS.concat( - REDIRECT_PAGE_WIDE_SCOPE_DECISION - ).concat(PRODUCTS_VIEW_DECISIONS); - mergedDecisions = productDecisions.concat(MERGED_METRIC_DECISIONS); -}); - -describe("Personalization::groupDecisions", () => { - it("extracts decisions by scope", () => { - const { - redirectDecisions, - pageWideScopeDecisions, - viewDecisions, - nonAutoRenderableDecisions - } = groupDecisions(cartDecisions); - - expect(pageWideScopeDecisions).toEqual( - PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS - ); - expect(viewDecisions).toEqual({ cart: CART_VIEW_DECISIONS }); - expect(nonAutoRenderableDecisions).toEqual([]); - expect(redirectDecisions).toEqual([]); - }); - - it("extracts decisions", () => { - const expectedViewDecisions = { - products: PRODUCTS_VIEW_DECISIONS - }; - const { - redirectDecisions, - pageWideScopeDecisions, - viewDecisions, - nonAutoRenderableDecisions - } = groupDecisions(productDecisions); - - expect(nonAutoRenderableDecisions).toEqual( - PAGE_WIDE_SCOPE_DECISIONS_WITHOUT_DOM_ACTION_SCHEMA_ITEMS - ); - expect(pageWideScopeDecisions).toEqual( - PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS - ); - expect(redirectDecisions).toEqual(REDIRECT_PAGE_WIDE_SCOPE_DECISION); - expect(viewDecisions).toEqual(expectedViewDecisions); - }); - - it("extracts merged decisions", () => { - const expectedViewDecisions = { - products: PRODUCTS_VIEW_DECISIONS - }; - const { - redirectDecisions, - pageWideScopeDecisions, - viewDecisions, - nonAutoRenderableDecisions - } = groupDecisions(mergedDecisions); - - expect(nonAutoRenderableDecisions).toEqual( - MERGED_METRIC_DECISIONS.concat( - PAGE_WIDE_SCOPE_DECISIONS_WITHOUT_DOM_ACTION_SCHEMA_ITEMS - ) - ); - expect(pageWideScopeDecisions).toEqual( - PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS - ); - expect(redirectDecisions).toEqual(REDIRECT_PAGE_WIDE_SCOPE_DECISION); - expect(viewDecisions).toEqual(expectedViewDecisions); - }); - - it("extracts empty when no decisions", () => { - const decisions = []; - - const { - redirectDecisions, - pageWideScopeDecisions, - viewDecisions, - nonAutoRenderableDecisions - } = groupDecisions(decisions); - - expect(nonAutoRenderableDecisions).toEqual([]); - expect(pageWideScopeDecisions).toEqual([]); - expect(redirectDecisions).toEqual([]); - expect(viewDecisions).toEqual({}); - }); -}); diff --git a/test/unit/specs/components/Personalization/handlers/createDomActionHandler.spec.js b/test/unit/specs/components/Personalization/handlers/createDomActionHandler.spec.js new file mode 100644 index 000000000..f72718f12 --- /dev/null +++ b/test/unit/specs/components/Personalization/handlers/createDomActionHandler.spec.js @@ -0,0 +1,164 @@ +import createDomActionHandler from "../../../../../../src/components/Personalization/handlers/createDomActionHandler"; + +describe("Personalization::handlers::createDomActionHandler", () => { + let next; + let modules; + let storeClickMetrics; + let preprocess; + let action1; + let action2; + let handler; + + let proposition; + let handle; + + beforeEach(() => { + next = jasmine.createSpy("next"); + action1 = jasmine.createSpy("action1"); + action2 = jasmine.createSpy("action2"); + modules = { action1, action2 }; + storeClickMetrics = jasmine.createSpy("storeClickMetrics"); + preprocess = jasmine.createSpy("preprocess"); + preprocess.and.returnValue("preprocessed"); + handler = createDomActionHandler({ + next, + modules, + storeClickMetrics, + preprocess + }); + proposition = jasmine.createSpyObj("proposition1", [ + "getHandle", + "includeInDisplayNotification", + "addRenderer", + "getItemMeta" + ]); + proposition.getHandle.and.callFake(() => handle); + proposition.getItemMeta.and.callFake(index => `meta${index}`); + }); + + it("handles an empty proposition", () => { + handle = {}; + handler(proposition); + expect(next).toHaveBeenCalledOnceWith(proposition); + expect(action1).not.toHaveBeenCalled(); + expect(action2).not.toHaveBeenCalled(); + expect(storeClickMetrics).not.toHaveBeenCalled(); + expect(proposition.addRenderer).not.toHaveBeenCalled(); + expect(proposition.includeInDisplayNotification).not.toHaveBeenCalled(); + expect(preprocess).not.toHaveBeenCalled(); + }); + + it("handles an empty set of items", () => { + handle = { items: [] }; + handler(proposition); + expect(next).toHaveBeenCalledOnceWith(proposition); + expect(action1).not.toHaveBeenCalled(); + expect(action2).not.toHaveBeenCalled(); + expect(storeClickMetrics).not.toHaveBeenCalled(); + expect(proposition.addRenderer).not.toHaveBeenCalled(); + expect(proposition.includeInDisplayNotification).not.toHaveBeenCalled(); + expect(preprocess).not.toHaveBeenCalled(); + }); + + it("handles an item with an unknown schema", () => { + handle = { items: [{ schema: "unknown" }] }; + handler(proposition); + expect(next).toHaveBeenCalledOnceWith(proposition); + expect(action1).not.toHaveBeenCalled(); + expect(action2).not.toHaveBeenCalled(); + expect(storeClickMetrics).not.toHaveBeenCalled(); + expect(proposition.addRenderer).not.toHaveBeenCalled(); + expect(proposition.includeInDisplayNotification).not.toHaveBeenCalled(); + expect(preprocess).not.toHaveBeenCalled(); + }); + + it("handles a default content item", () => { + handle = { + items: [ + { schema: "https://ns.adobe.com/personalization/default-content-item" } + ] + }; + handler(proposition); + expect(next).toHaveBeenCalledOnceWith(proposition); + expect(proposition.includeInDisplayNotification).toHaveBeenCalledOnceWith(); + expect(proposition.addRenderer).toHaveBeenCalledOnceWith( + 0, + jasmine.any(Function) + ); + proposition.addRenderer.calls.argsFor(0)[1](); + expect(action1).not.toHaveBeenCalled(); + expect(action2).not.toHaveBeenCalled(); + expect(preprocess).not.toHaveBeenCalled(); + }); + + it("handles a click item", () => { + handle = { + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + type: "click", + selector: "#myselector" + } + } + ] + }; + handler(proposition); + expect(next).toHaveBeenCalledOnceWith(proposition); + + expect(storeClickMetrics).toHaveBeenCalledOnceWith({ + selector: "#myselector", + meta: "meta0" + }); + expect(proposition.addRenderer).toHaveBeenCalledOnceWith( + 0, + jasmine.any(Function) + ); + proposition.addRenderer.calls.argsFor(0)[1](); + expect(action1).not.toHaveBeenCalled(); + expect(action2).not.toHaveBeenCalled(); + expect(preprocess).not.toHaveBeenCalled(); + }); + + it("handles a dom action item", () => { + handle = { + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + type: "action1", + selector: "#myselector" + } + } + ] + }; + handler(proposition); + expect(next).toHaveBeenCalledOnceWith(proposition); + expect(proposition.includeInDisplayNotification).toHaveBeenCalledOnceWith(); + expect(proposition.addRenderer).toHaveBeenCalledOnceWith( + 0, + jasmine.any(Function) + ); + proposition.addRenderer.calls.argsFor(0)[1](); + expect(action1).toHaveBeenCalledOnceWith("preprocessed"); + expect(action2).not.toHaveBeenCalled(); + }); + + it("handles an unknown dom action item", () => { + handle = { + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + type: "unknown", + selector: "#myselector" + } + } + ] + }; + handler(proposition); + expect(next).toHaveBeenCalledOnceWith(proposition); + expect(proposition.addRenderer).not.toHaveBeenCalled(); + expect(preprocess).not.toHaveBeenCalled(); + }); +}); diff --git a/test/unit/specs/components/Personalization/handlers/createHtmlContentHandler.spec.js b/test/unit/specs/components/Personalization/handlers/createHtmlContentHandler.spec.js new file mode 100644 index 000000000..42e808047 --- /dev/null +++ b/test/unit/specs/components/Personalization/handlers/createHtmlContentHandler.spec.js @@ -0,0 +1,110 @@ +import createHtmlContentHandler from "../../../../../../src/components/Personalization/handlers/createHtmlContentHandler"; + +describe("Personalization::handlers::createHtmlContentHandler", () => { + let next; + let modules; + let action1; + let action2; + let preprocess; + let proposition; + let handle; + let handler; + + beforeEach(() => { + next = jasmine.createSpy("next"); + action1 = jasmine.createSpy("action1"); + action2 = jasmine.createSpy("action2"); + modules = { action1, action2 }; + preprocess = jasmine.createSpy("preprocess"); + preprocess.and.returnValue("preprocessed"); + proposition = jasmine.createSpyObj("proposition1", [ + "getHandle", + "includeInDisplayNotification", + "addRenderer", + "isApplyPropositions" + ]); + proposition.getHandle.and.callFake(() => handle); + handler = createHtmlContentHandler({ + next, + modules, + preprocess + }); + }); + + it("handles an empty proposition", () => { + handle = {}; + handler(proposition); + expect(next).not.toHaveBeenCalled(); + expect(action1).not.toHaveBeenCalled(); + expect(action2).not.toHaveBeenCalled(); + expect(proposition.addRenderer).not.toHaveBeenCalled(); + expect(proposition.includeInDisplayNotification).not.toHaveBeenCalled(); + expect(preprocess).not.toHaveBeenCalled(); + }); + + it("does not filter a view scope type", () => { + handle = { scopeDetails: { characteristics: { scopeType: "view" } } }; + handler(proposition); + expect(next).toHaveBeenCalledOnceWith(proposition); + }); + + it("does not filter a page wide scope", () => { + handle = { scope: "__view__" }; + handler(proposition); + expect(next).toHaveBeenCalledOnceWith(proposition); + }); + + it("does not filter an apply propositions call", () => { + handle = {}; + proposition.isApplyPropositions.and.returnValue(true); + handler(proposition); + expect(next).toHaveBeenCalledOnceWith(proposition); + }); + + it("handles a HTML content item", () => { + handle = { + items: [ + { + schema: "https://ns.adobe.com/personalization/html-content-item", + data: { type: "action1", selector: "selector1" } + } + ] + }; + handler(proposition); + expect(proposition.addRenderer).toHaveBeenCalledOnceWith( + 0, + jasmine.any(Function) + ); + expect(proposition.includeInDisplayNotification).toHaveBeenCalledOnceWith(); + proposition.addRenderer.calls.argsFor(0)[1](); + expect(action1).toHaveBeenCalledOnceWith("preprocessed"); + }); + + it("does not handle an HTML content item with an unknown type", () => { + handle = { + items: [ + { + schema: "https://ns.adobe.com/personalization/html-content-item", + data: { type: "unknown", selector: "selector1" } + } + ] + }; + handler(proposition); + expect(proposition.addRenderer).not.toHaveBeenCalled(); + expect(proposition.includeInDisplayNotification).not.toHaveBeenCalled(); + }); + + it("does not handle an HTML content item without a selector", () => { + handle = { + items: [ + { + schema: "https://ns.adobe.com/personalization/html-content-item", + data: { type: "action1" } + } + ] + }; + handler(proposition); + expect(proposition.addRenderer).not.toHaveBeenCalled(); + expect(proposition.includeInDisplayNotification).not.toHaveBeenCalled(); + }); +}); diff --git a/test/unit/specs/components/Personalization/handlers/createMeasurementSchemaHandler.spec.js b/test/unit/specs/components/Personalization/handlers/createMeasurementSchemaHandler.spec.js new file mode 100644 index 000000000..d583b5dd4 --- /dev/null +++ b/test/unit/specs/components/Personalization/handlers/createMeasurementSchemaHandler.spec.js @@ -0,0 +1,35 @@ +import createMeasurementSchemaHandler from "../../../../../../src/components/Personalization/handlers/createMeasurementSchemaHandler"; + +describe("Personalization::handlers::createMeasurementSchemaHandler", () => { + let next; + let proposition; + let handle; + let handler; + + beforeEach(() => { + next = jasmine.createSpy("next"); + proposition = jasmine.createSpyObj("proposition", ["getHandle"]); + proposition.getHandle.and.callFake(() => handle); + handler = createMeasurementSchemaHandler({ next }); + }); + + it("handles an empty proposition", () => { + handle = {}; + handler(proposition); + expect(next).toHaveBeenCalledOnceWith(proposition); + }); + + it("handles an empty set of items", () => { + handle = { items: [] }; + handler(proposition); + expect(next).toHaveBeenCalledOnceWith(proposition); + }); + + it("does not pass on a proposition with a measurment schema", () => { + handle = { + items: [{ schema: "https://ns.adobe.com/personalization/measurement" }] + }; + handler(proposition); + expect(next).not.toHaveBeenCalled(); + }); +}); diff --git a/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js b/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js new file mode 100644 index 000000000..b7fa3a280 --- /dev/null +++ b/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js @@ -0,0 +1,126 @@ +import createRedirectHandler from "../../../../../../src/components/Personalization/handlers/createRedirectHandler"; +import { createProposition } from "../../../../../../src/components/Personalization/handlers/proposition"; + +describe("redirectHandler", () => { + let next; + let redirectHandler; + + beforeEach(() => { + next = jasmine.createSpy("next"); + redirectHandler = createRedirectHandler({ next }); + }); + + it("works with real response", () => { + const handle = { + id: "AT:eyJhY3Rpdml0eUlkIjoiMTI3ODE5IiwiZXhwZXJpZW5jZUlkIjoiMCJ9", + scope: "__view__", + scopeDetails: { + decisionProvider: "TGT", + activity: { + id: "127819" + }, + experience: { + id: "0" + }, + strategies: [ + { + algorithmID: "0", + trafficType: "0" + } + ], + characteristics: { + eventToken: + "8CwxglIqrTLmqP2m1r52VWqipfsIHvVzTQxHolz2IpTMromRrB5ztP5VMxjHbs7c6qPG9UF4rvQTJZniWgqbOw==" + } + }, + items: [ + { + id: "0", + schema: "https://ns.adobe.com/personalization/redirect-item", + meta: { + "experience.id": "0", + "activity.id": "127819", + "offer.name": "Default Content", + "activity.name": "Functional:C205528", + "offer.id": "0" + }, + data: { + type: "redirect", + format: "text/uri-list", + content: + "https://alloyio.com/functional-test/alloyTestPage.html?redirectedTest=true&test=C205528" + } + } + ] + }; + const proposition = createProposition(handle); + redirectHandler(proposition); + expect(next).not.toHaveBeenCalled(); + expect(proposition.getRedirectUrl()).toEqual( + "https://alloyio.com/functional-test/alloyTestPage.html?redirectedTest=true&test=C205528" + ); + + const propositions = []; + proposition.addToReturnedPropositions(propositions); + expect(propositions.length).toEqual(1); + expect(propositions[0].renderAttempted).toBeTrue(); + expect(propositions[0].id).toEqual( + "AT:eyJhY3Rpdml0eUlkIjoiMTI3ODE5IiwiZXhwZXJpZW5jZUlkIjoiMCJ9" + ); + + const notifications = []; + proposition.addToNotifications(notifications); + expect(notifications.length).toEqual(1); + expect(notifications[0].id).toEqual( + "AT:eyJhY3Rpdml0eUlkIjoiMTI3ODE5IiwiZXhwZXJpZW5jZUlkIjoiMCJ9" + ); + }); + + it("passes through non-redirect propositions", () => { + const handle = { + id: "AT:eyJhY3Rpdml0eUlkIjoiMTI3ODE5IiwiZXhwZXJpZW5jZUlkIjoiMCJ9", + scope: "__view__", + scopeDetails: { + decisionProvider: "TGT", + activity: { + id: "127819" + }, + experience: { + id: "0" + }, + strategies: [ + { + algorithmID: "0", + trafficType: "0" + } + ], + characteristics: { + eventToken: + "8CwxglIqrTLmqP2m1r52VWqipfsIHvVzTQxHolz2IpTMromRrB5ztP5VMxjHbs7c6qPG9UF4rvQTJZniWgqbOw==" + } + }, + items: [ + { + id: "0", + schema: "https://ns.adobe.com/personalization/html-content-item", + meta: { + "experience.id": "0", + "activity.id": "127819", + "offer.name": "Default Content", + "activity.name": "Functional:C205528", + "offer.id": "0" + }, + data: { + type: "html", + format: "text/html", + content: "

Some custom content for the home page

" + } + } + ] + }; + const proposition = createProposition(handle); + redirectHandler(proposition); + expect(next).toHaveBeenCalledOnceWith(proposition); + expect(proposition.getRedirectUrl()).toBeUndefined(); + }); +}); diff --git a/test/unit/specs/components/Personalization/handlers/createRender.spec.js b/test/unit/specs/components/Personalization/handlers/createRender.spec.js new file mode 100644 index 000000000..c191c568e --- /dev/null +++ b/test/unit/specs/components/Personalization/handlers/createRender.spec.js @@ -0,0 +1,87 @@ +import createRender from "../../../../../../src/components/Personalization/handlers/createRender"; + +describe("Personalization::handlers::createRender", () => { + let handleChain; + let collect; + let executeRedirect; + let logger; + let showContainers; + + let proposition1; + let proposition2; + + let render; + + beforeEach(() => { + handleChain = jasmine.createSpy("handleChain"); + collect = jasmine.createSpy("collect"); + executeRedirect = jasmine.createSpy("executeRedirect"); + logger = jasmine.createSpyObj("logger", ["warn"]); + showContainers = jasmine.createSpy("showContainers"); + proposition1 = jasmine.createSpyObj("proposition1", [ + "getRedirectUrl", + "addToNotifications", + "render" + ]); + proposition2 = jasmine.createSpyObj("proposition2", [ + "getRedirectUrl", + "addToNotifications", + "render" + ]); + render = createRender({ + handleChain, + collect, + executeRedirect, + logger, + showContainers + }); + }); + + it("does nothing with an empty array", async () => { + const returnValue = await render([]); + expect(handleChain).not.toHaveBeenCalled(); + expect(collect).not.toHaveBeenCalled(); + expect(executeRedirect).not.toHaveBeenCalled(); + expect(logger.warn).not.toHaveBeenCalled(); + expect(showContainers).not.toHaveBeenCalled(); + expect(returnValue).toEqual([]); + }); + + it("returns notifications", async () => { + proposition1.render.and.returnValue("rendered1"); + proposition2.render.and.returnValue("rendered2"); + const returnValue = await render([proposition1, proposition2]); + expect(handleChain).toHaveBeenCalledWith(proposition1); + expect(handleChain).toHaveBeenCalledWith(proposition2); + expect(returnValue).toEqual(["rendered1", "rendered2"]); + }); + + it("returns empty notifications", async () => { + const returnValue = await render([proposition1, proposition2]); + expect(returnValue).toEqual([]); + }); + + it("handles a redirect", async () => { + proposition1.getRedirectUrl.and.returnValue("redirect1"); + collect.and.returnValue(Promise.resolve()); + proposition1.addToNotifications.and.callFake(array => { + array.push("notification1"); + }); + await render([proposition1, proposition2]); + expect(executeRedirect).toHaveBeenCalledWith("redirect1"); + expect(collect).toHaveBeenCalledOnceWith({ + decisionsMeta: ["notification1"] + }); + expect(showContainers).not.toHaveBeenCalled(); + expect(logger.warn).not.toHaveBeenCalled(); + }); + + it("handles an error in a redirect", async () => { + proposition1.getRedirectUrl.and.returnValue("redirect1"); + collect.and.returnValue(Promise.resolve()); + executeRedirect.and.throwError("error1"); + await render([proposition1, proposition2]); + expect(showContainers).toHaveBeenCalledOnceWith(); + expect(logger.warn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/test/unit/specs/components/Personalization/handlers/proposition.spec.js b/test/unit/specs/components/Personalization/handlers/proposition.spec.js new file mode 100644 index 000000000..b501990e0 --- /dev/null +++ b/test/unit/specs/components/Personalization/handlers/proposition.spec.js @@ -0,0 +1,250 @@ +import { + createProposition, + buildReturnedDecisions, + buildReturnedPropositions +} from "../../../../../../src/components/Personalization/handlers/proposition"; + +describe("Personalization::handlers", () => { + describe("createProposition", () => { + it("returns the handle", () => { + const handle = { id: "id", scope: "scope", scopeDetails: "scopeDetails" }; + const proposition = createProposition(handle); + expect(proposition.getHandle()).toEqual(handle); + }); + it("is okay with an empty handle", () => { + const proposition = createProposition({}); + expect(proposition.getHandle()).toEqual({}); + }); + it("returns the item meta", () => { + const handle = { + id: "id", + scope: "scope", + scopeDetails: "scopeDetails", + other: "other", + items: [{}] + }; + const proposition = createProposition(handle); + expect(proposition.getItemMeta(0)).toEqual({ + id: "id", + scope: "scope", + scopeDetails: "scopeDetails" + }); + }); + it("extracts the trackingLabel in the item meta", () => { + const handle = { + id: "id", + scope: "scope", + scopeDetails: "scopeDetails", + items: [ + { characteristics: { trackingLabel: "trackingLabel1" } }, + { characteristics: { trackingLabel: "trackingLabel2" } } + ] + }; + const proposition = createProposition(handle); + expect(proposition.getItemMeta(1)).toEqual({ + id: "id", + scope: "scope", + scopeDetails: "scopeDetails", + trackingLabel: "trackingLabel2" + }); + }); + it("saves the redirect", () => { + const proposition = createProposition({ + id: "id1", + items: [{ a: 1 }, { b: 2 }] + }); + proposition.redirect("redirectUrl"); + expect(proposition.getRedirectUrl()).toEqual("redirectUrl"); + }); + it("includes the redirect in the notifications", () => { + const proposition = createProposition({ + id: "id1", + items: [{ a: 1 }, { b: 2 }] + }); + proposition.redirect("redirectUrl"); + const notifications = []; + proposition.addToNotifications(notifications); + expect(notifications).toEqual([ + { id: "id1", scope: undefined, scopeDetails: undefined } + ]); + }); + it("includes the redirect in the returned propositions", () => { + const proposition = createProposition({ + id: "id1", + items: [{ a: 1 }, { b: 2 }] + }); + proposition.redirect("redirectUrl"); + const propositions = []; + proposition.addToReturnedPropositions(propositions); + expect(propositions).toEqual([ + { id: "id1", items: [{ a: 1 }, { b: 2 }], renderAttempted: true } + ]); + }); + it("doesn't include the redirect in the returned decisions", () => { + const proposition = createProposition({ + id: "id1", + items: [{ a: 1 }, { b: 2 }] + }); + proposition.redirect("redirectUrl"); + const decisions = []; + proposition.addToReturnedDecisions(decisions); + expect(decisions).toEqual([]); + }); + it("returns undefined for the redirect URL when it is not set", () => { + const proposition = createProposition({ + id: "id1", + items: [{ a: 1 }, { b: 2 }] + }); + expect(proposition.getRedirectUrl()).toBeUndefined(); + }); + it("includes the proposition in the returned propositions when not rendered", () => { + const proposition = createProposition({ + id: "id1", + items: [{ a: 1 }, { b: 2 }] + }); + const propositions = []; + proposition.addToReturnedPropositions(propositions); + expect(propositions).toEqual([ + { id: "id1", items: [{ a: 1 }, { b: 2 }], renderAttempted: false } + ]); + }); + it("includes the proposition in the returned decisions when not rendered", () => { + const proposition = createProposition({ + id: "id1", + items: [{ a: 1 }, { b: 2 }] + }); + const decisions = []; + proposition.addToReturnedDecisions(decisions); + expect(decisions).toEqual([{ id: "id1", items: [{ a: 1 }, { b: 2 }] }]); + }); + it("does not include the notification if it isn't rendered", () => { + const proposition = createProposition({ + id: "id1", + items: [{ a: 1 }, { b: 2 }] + }); + const notifications = []; + proposition.addToNotifications(notifications); + expect(notifications).toEqual([]); + }); + it("handles a completely rendered item", async () => { + const proposition = createProposition({ + id: "id1", + items: [{ a: 1 }, { b: 2 }] + }); + proposition.includeInDisplayNotification(); + proposition.addRenderer(0, () => {}); + proposition.addRenderer(1, () => {}); + + const notification = await proposition.render({ enabled: false }); + expect(notification).toEqual({ + id: "id1", + scope: undefined, + scopeDetails: undefined + }); + const propositions = []; + proposition.addToReturnedPropositions(propositions); + expect(propositions).toEqual([ + { id: "id1", items: [{ a: 1 }, { b: 2 }], renderAttempted: true } + ]); + const decisions = []; + proposition.addToReturnedDecisions(decisions); + expect(decisions).toEqual([]); + }); + it("handles a partially rendered item", async () => { + const proposition = createProposition({ + id: "id1", + items: [{ a: 1 }, { b: 2 }] + }); + proposition.includeInDisplayNotification(); + proposition.addRenderer(0, () => {}); + + const notification = await proposition.render({ enabled: false }); + expect(notification).toEqual({ + id: "id1", + scope: undefined, + scopeDetails: undefined + }); + const propositions = []; + proposition.addToReturnedPropositions(propositions); + expect(propositions).toEqual([ + { id: "id1", items: [{ a: 1 }], renderAttempted: true }, + { id: "id1", items: [{ b: 2 }], renderAttempted: false } + ]); + const decisions = []; + proposition.addToReturnedDecisions(decisions); + expect(decisions).toEqual([{ id: "id1", items: [{ b: 2 }] }]); + }); + it("renders items", async () => { + const logger = jasmine.createSpyObj("logger", ["info", "warn"]); + logger.enabled = true; + const renderer1 = jasmine.createSpy("renderer1"); + const renderer2 = jasmine.createSpy("renderer2"); + const proposition = createProposition({ + id: "id1", + items: [{ a: 1 }, { b: 2 }] + }); + proposition.includeInDisplayNotification(); + proposition.addRenderer(0, renderer1); + proposition.addRenderer(1, renderer2); + await proposition.render(logger); + expect(renderer1).toHaveBeenCalledTimes(1); + expect(renderer2).toHaveBeenCalledTimes(1); + expect(logger.info).toHaveBeenCalledWith(`Action {"a":1} executed.`); + expect(logger.info).toHaveBeenCalledWith(`Action {"b":2} executed.`); + }); + }); + + describe("buildReturnedDecisions", () => { + let p1; + let p2; + let p3; + + beforeEach(() => { + p1 = jasmine.createSpyObj("p1", ["addToReturnedDecisions"]); + p2 = jasmine.createSpyObj("p2", ["addToReturnedDecisions"]); + p3 = jasmine.createSpyObj("p3", ["addToReturnedDecisions"]); + }); + + it("returns empty array when no propositions", () => { + const returnedDecisions = buildReturnedDecisions([]); + expect(returnedDecisions).toEqual([]); + }); + it("returns added decisions", () => { + p1.addToReturnedDecisions.and.callFake(array => { + array.push("decision1"); + }); + p3.addToReturnedDecisions.and.callFake(array => { + array.push("decision3"); + }); + const returnedDecisions = buildReturnedDecisions([p1, p2, p3]); + expect(returnedDecisions).toEqual(["decision1", "decision3"]); + }); + }); + + describe("buildReturnedPropositions", () => { + let p1; + let p2; + let p3; + + beforeEach(() => { + p1 = jasmine.createSpyObj("p1", ["addToReturnedPropositions"]); + p2 = jasmine.createSpyObj("p2", ["addToReturnedPropositions"]); + p3 = jasmine.createSpyObj("p3", ["addToReturnedPropositions"]); + }); + + it("returns empty array when no propositions", () => { + const returnedPropositions = buildReturnedPropositions([]); + expect(returnedPropositions).toEqual([]); + }); + it("returns added propositions", () => { + p1.addToReturnedPropositions.and.callFake(array => { + array.push("proposition1"); + }); + p3.addToReturnedPropositions.and.callFake(array => { + array.push("proposition3"); + }); + const returnedPropositions = buildReturnedPropositions([p1, p2, p3]); + expect(returnedPropositions).toEqual(["proposition1", "proposition3"]); + }); + }); +}); diff --git a/test/unit/specs/components/Personalization/responsesMock/eventResponses.js b/test/unit/specs/components/Personalization/responsesMock/eventResponses.js index 329b340c5..9482fb28c 100644 --- a/test/unit/specs/components/Personalization/responsesMock/eventResponses.js +++ b/test/unit/specs/components/Personalization/responsesMock/eventResponses.js @@ -374,8 +374,7 @@ export const MIXED_PROPOSITIONS = [ id: "1202449" } } - ], - renderAttempted: false + ] }, { id: "AT:eyJhY3Rpdml0eUlkIjoiMTQxNjY0IiwiZXhwZXJpZW5jZUlkIjoiMCJ9", @@ -401,7 +400,6 @@ export const MIXED_PROPOSITIONS = [ { id: "AT:eyJhY3Rpdml0eUlkIjoiNDQyMzU4IiwiZXhwZXJpZW5jZUlkIjoiIn1=", scope: "__view__", - renderAttempted: true, items: [ { id: "442358", @@ -417,7 +415,6 @@ export const MIXED_PROPOSITIONS = [ { id: "AT:eyJhY3Rpdml0eUlkIjoiNDQyMzU4IiwiZXhwZXJpZW5jZUlkIjoiIn2=", scope: "__view__", - renderAttempted: false, items: [ { id: "442379", diff --git a/test/unit/specs/components/Personalization/topLevel/buildAlloy.js b/test/unit/specs/components/Personalization/topLevel/buildAlloy.js new file mode 100644 index 000000000..be2d9a204 --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/buildAlloy.js @@ -0,0 +1,187 @@ +/* +Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +import createEvent from "../../../../../../src/core/createEvent"; +import flushPromiseChains from "../../../../helpers/flushPromiseChains"; +import createComponent from "../../../../../../src/components/Personalization/createComponent"; +import createCollect from "../../../../../../src/components/Personalization/createCollect"; +import createFetchDataHandler from "../../../../../../src/components/Personalization/createFetchDataHandler"; +import collectClicks from "../../../../../../src/components/Personalization/dom-actions/clicks/collectClicks"; +import isAuthoringModeEnabled from "../../../../../../src/components/Personalization/utils/isAuthoringModeEnabled"; +import { + mergeDecisionsMeta, + mergeQuery +} from "../../../../../../src/components/Personalization/event"; +import createOnClickHandler from "../../../../../../src/components/Personalization/createOnClickHandler"; +import createViewCacheManager from "../../../../../../src/components/Personalization/createViewCacheManager"; +import createViewChangeHandler from "../../../../../../src/components/Personalization/createViewChangeHandler"; +import createClickStorage from "../../../../../../src/components/Personalization/createClickStorage"; +import createApplyPropositions from "../../../../../../src/components/Personalization/createApplyPropositions"; +import createSetTargetMigration from "../../../../../../src/components/Personalization/createSetTargetMigration"; +import { createCallbackAggregator, assign } from "../../../../../../src/utils"; +import createRender from "../../../../../../src/components/Personalization/handlers/createRender"; +import createDomActionHandler from "../../../../../../src/components/Personalization/handlers/createDomActionHandler"; +import createMeasurementSchemaHandler from "../../../../../../src/components/Personalization/handlers/createMeasurementSchemaHandler"; +import createRedirectHandler from "../../../../../../src/components/Personalization/handlers/createRedirectHandler"; +import createHtmlContentHandler from "../../../../../../src/components/Personalization/handlers/createHtmlContentHandler"; +import { isPageWideSurface } from "../../../../../../src/components/Personalization/utils/surfaceUtils"; +import { createProposition } from "../../../../../../src/components/Personalization/handlers/proposition"; + +const createAction = renderFunc => ({ selector, content }) => { + renderFunc(selector, content); + if (selector === "#error") { + return Promise.reject(new Error(`Error while rendering ${content}`)); + } + return Promise.resolve(); +}; + +const buildComponent = ({ + actions, + config, + logger, + eventManager, + getPageLocation, + window, + hideContainers, + showContainers +}) => { + const initDomActionsModulesMocks = () => { + return { + setHtml: createAction(actions.setHtml), + customCode: createAction(actions.prependHtml), + setText: createAction(actions.setText), + setAttribute: createAction(actions.setAttributes), + setImageSource: createAction(actions.swapImage), + setStyle: createAction(actions.setStyles), + move: createAction(actions.setStyles), + resize: createAction(actions.setStyles), + rearrange: createAction(actions.rearrangeChildren), + remove: createAction(actions.removeNode), + insertAfter: createAction(actions.insertHtmlAfter), + insertBefore: createAction(actions.insertHtmlBefore), + replaceHtml: createAction(actions.replaceHtml), + appendHtml: createAction(actions.appendHtml), + prependHtml: createAction(actions.prependHtml) + }; + }; + + const { targetMigrationEnabled, prehidingStyle } = config; + const collect = createCollect({ eventManager, mergeDecisionsMeta }); + + const { + getClickMetasBySelector, + getClickSelectors, + storeClickMetrics + } = createClickStorage(); + + const viewCache = createViewCacheManager({ createProposition }); + const modules = initDomActionsModulesMocks(); + + const noOpHandler = () => undefined; + const preprocess = action => action; + const domActionHandler = createDomActionHandler({ + next: noOpHandler, + isPageWideSurface, + modules, + storeClickMetrics, + preprocess + }); + const measurementSchemaHandler = createMeasurementSchemaHandler({ + next: domActionHandler + }); + const redirectHandler = createRedirectHandler({ + next: measurementSchemaHandler + }); + const htmlContentHandler = createHtmlContentHandler({ + next: redirectHandler, + modules, + preprocess + }); + + const render = createRender({ + handleChain: htmlContentHandler, + collect, + executeRedirect: url => window.location.replace(url), + logger + }); + const fetchDataHandler = createFetchDataHandler({ + prehidingStyle, + showContainers, + hideContainers, + mergeQuery, + collect, + render + }); + const onClickHandler = createOnClickHandler({ + mergeDecisionsMeta, + collectClicks, + getClickSelectors, + getClickMetasBySelector + }); + const viewChangeHandler = createViewChangeHandler({ + mergeDecisionsMeta, + render, + viewCache + }); + const applyPropositions = createApplyPropositions({ + render + }); + const setTargetMigration = createSetTargetMigration({ + targetMigrationEnabled + }); + return createComponent({ + getPageLocation, + logger, + fetchDataHandler, + viewChangeHandler, + onClickHandler, + isAuthoringModeEnabled, + mergeQuery, + viewCache, + showContainers, + applyPropositions, + setTargetMigration + }); +}; + +export default mocks => { + const component = buildComponent(mocks); + const { response } = mocks; + return { + async sendEvent({ + xdm, + data, + renderDecisions, + decisionScopes, + personalization + }) { + const event = createEvent(); + event.setUserXdm(xdm); + event.setUserData(data); + const callbacks = createCallbackAggregator(); + await component.lifecycle.onBeforeEvent({ + event, + renderDecisions, + decisionScopes, + personalization, + onResponse: callbacks.add + }); + const results = await callbacks.call({ response }); + const result = assign({}, ...results); + await flushPromiseChains(); + event.finalize(); + return { event, result }; + }, + applyPropositions(args) { + return component.commands.applyPropositions.run(args); + } + }; +}; diff --git a/test/unit/specs/components/Personalization/topLevel/buildMocks.js b/test/unit/specs/components/Personalization/topLevel/buildMocks.js new file mode 100644 index 000000000..be2023ca3 --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/buildMocks.js @@ -0,0 +1,77 @@ +/* +Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +import createEvent from "../../../../../../src/core/createEvent"; +import createResponse from "../../../../../functional/helpers/createResponse"; + +export default decisions => { + const response = createResponse({ + content: { + handle: decisions.map(payload => ({ + type: "personalization:decisions", + payload + })) + } + }); + + const actions = jasmine.createSpyObj("actions", [ + "createAction", + "setHtml", + "setText", + "setAttributes", + "swapImage", + "setStyles", + "rearrangeChildren", + "removeNode", + "replaceHtml", + "appendHtml", + "prependHtml", + "insertHtmlAfter", + "insertHtmlBefore" + ]); + + const config = { + targetMigrationEnabled: true, + prehidingStyle: "myprehidingstyle" + }; + const logger = { + warn: spyOn(console, "warn").and.callThrough(), + error: spyOn(console, "error").and.callThrough() + }; + const sendEvent = jasmine.createSpy("sendEvent"); + const eventManager = { + createEvent, + async sendEvent(event) { + event.finalize(); + sendEvent(event.toJSON()); + return Promise.resolve(); + } + }; + const getPageLocation = () => new URL("http://example.com/home"); + const window = { + location: jasmine.createSpyObj("location", ["replace"]) + }; + const hideContainers = jasmine.createSpy("hideContainers"); + const showContainers = jasmine.createSpy("showContainers"); + + return { + actions, + config, + logger, + sendEvent, + eventManager, + getPageLocation, + window, + hideContainers, + showContainers, + response + }; +}; diff --git a/test/unit/specs/components/Personalization/topLevel/cartViewDecisions.spec.js b/test/unit/specs/components/Personalization/topLevel/cartViewDecisions.spec.js new file mode 100644 index 000000000..6aa3faca5 --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/cartViewDecisions.spec.js @@ -0,0 +1,257 @@ +import { CART_VIEW_DECISIONS } from "../responsesMock/eventResponses"; + +import buildMocks from "./buildMocks"; +import buildAlloy from "./buildAlloy"; +import resetMocks from "./resetMocks"; +import flushPromiseChains from "../../../../helpers/flushPromiseChains"; + +describe("PersonalizationComponent", () => { + it("CART_VIEW_DECISIONS", async () => { + const mocks = buildMocks(CART_VIEW_DECISIONS); + const alloy = buildAlloy(mocks); + let { event, result } = await alloy.sendEvent( + { + renderDecisions: true + }, + CART_VIEW_DECISIONS + ); + expect(event.toJSON()).toEqual({ + query: { + personalization: { + schemas: [ + "https://ns.adobe.com/personalization/default-content-item", + "https://ns.adobe.com/personalization/html-content-item", + "https://ns.adobe.com/personalization/json-content-item", + "https://ns.adobe.com/personalization/redirect-item", + "https://ns.adobe.com/personalization/dom-action" + ], + decisionScopes: ["__view__"], + surfaces: ["web://example.com/home"] + } + } + }); + expect(result).toEqual({ + propositions: [], + decisions: [] + }); + expect(mocks.sendEvent).not.toHaveBeenCalled(); + + expect(mocks.logger.warn).not.toHaveBeenCalled(); + expect(mocks.logger.error).not.toHaveBeenCalled(); + + resetMocks(mocks); + ({ event, result } = await alloy.sendEvent( + { + renderDecisions: true, + xdm: { + web: { + webPageDetails: { + viewName: "cart" + } + } + } + }, + [] + )); + + expect(event.toJSON()).toEqual({ + xdm: { + _experience: { + decisioning: { + propositions: [ + { + id: "TNT:activity4:experience9", + scope: "cart", + scopeDetails: { + blah: "test", + characteristics: { + scopeType: "view" + } + } + } + ], + propositionEventType: { + display: 1 + } + } + }, + web: { + webPageDetails: { + viewName: "cart" + } + } + } + }); + expect(result).toEqual({ + propositions: [ + { + renderAttempted: true, + id: "TNT:activity4:experience9", + scope: "cart", + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + type: "setHtml", + selector: "#foo", + content: "
welcome to cart view
" + } + }, + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + type: "setHtml", + selector: "#foo2", + content: "
here is a target activity for cart view
" + } + }, + { + schema: + "https://ns.adobe.com/personalization/default-content-item" + } + ], + scopeDetails: { + blah: "test", + characteristics: { + scopeType: "view" + } + } + } + ], + decisions: [] + }); + expect(mocks.sendEvent).not.toHaveBeenCalled(); + expect(mocks.actions.setHtml).toHaveBeenCalledWith( + "#foo", + "
welcome to cart view
" + ); + expect(mocks.actions.setHtml).toHaveBeenCalledWith( + "#foo2", + "
here is a target activity for cart view
" + ); + expect(mocks.actions.setHtml).toHaveBeenCalledTimes(2); + expect(mocks.logger.warn).not.toHaveBeenCalled(); + expect(mocks.logger.error).not.toHaveBeenCalled(); + }); + + it("CART_VIEW_DECISIONS 2", async () => { + const mocks = buildMocks(CART_VIEW_DECISIONS); + const alloy = buildAlloy(mocks); + const { event, result } = await alloy.sendEvent( + { + renderDecisions: true, + xdm: { + web: { + webPageDetails: { + viewName: "cart" + } + } + } + }, + CART_VIEW_DECISIONS + ); + + await flushPromiseChains(); + + expect(event.toJSON()).toEqual({ + query: { + personalization: { + schemas: [ + "https://ns.adobe.com/personalization/default-content-item", + "https://ns.adobe.com/personalization/html-content-item", + "https://ns.adobe.com/personalization/json-content-item", + "https://ns.adobe.com/personalization/redirect-item", + "https://ns.adobe.com/personalization/dom-action" + ], + decisionScopes: ["__view__"], + surfaces: ["web://example.com/home"] + } + }, + xdm: { + web: { + webPageDetails: { + viewName: "cart" + } + } + } + }); + expect(result).toEqual({ + propositions: [ + { + renderAttempted: true, + id: "TNT:activity4:experience9", + scope: "cart", + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + type: "setHtml", + selector: "#foo", + content: "
welcome to cart view
" + } + }, + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + type: "setHtml", + selector: "#foo2", + content: "
here is a target activity for cart view
" + } + }, + { + schema: + "https://ns.adobe.com/personalization/default-content-item" + } + ], + scopeDetails: { + blah: "test", + characteristics: { + scopeType: "view" + } + } + } + ], + decisions: [] + }); + expect(mocks.sendEvent).toHaveBeenCalledWith({ + xdm: { + _experience: { + decisioning: { + propositions: [ + { + id: "TNT:activity4:experience9", + scope: "cart", + scopeDetails: { + blah: "test", + characteristics: { + scopeType: "view" + } + } + } + ], + propositionEventType: { + display: 1 + } + } + }, + eventType: "decisioning.propositionDisplay", + web: { + webPageDetails: { + viewName: "cart" + } + } + } + }); + expect(mocks.actions.setHtml).toHaveBeenCalledWith( + "#foo", + "
welcome to cart view
" + ); + expect(mocks.actions.setHtml).toHaveBeenCalledWith( + "#foo2", + "
here is a target activity for cart view
" + ); + expect(mocks.actions.setHtml).toHaveBeenCalledTimes(2); + expect(mocks.logger.warn).not.toHaveBeenCalled(); + expect(mocks.logger.error).not.toHaveBeenCalled(); + }); +}); diff --git a/test/unit/specs/components/Personalization/topLevel/mergedMetricDecisions.spec.js b/test/unit/specs/components/Personalization/topLevel/mergedMetricDecisions.spec.js new file mode 100644 index 000000000..3f77d60be --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/mergedMetricDecisions.spec.js @@ -0,0 +1,107 @@ +import { MERGED_METRIC_DECISIONS } from "../responsesMock/eventResponses"; + +import buildMocks from "./buildMocks"; +import buildAlloy from "./buildAlloy"; + +describe("PersonalizationComponent", () => { + it("MERGED_METRIC_DECISIONS", async () => { + const mocks = buildMocks(MERGED_METRIC_DECISIONS); + const alloy = buildAlloy(mocks); + const { event, result } = await alloy.sendEvent( + { + renderDecisions: true + }, + MERGED_METRIC_DECISIONS + ); + expect(event.toJSON()).toEqual({ + query: { + personalization: { + schemas: [ + "https://ns.adobe.com/personalization/default-content-item", + "https://ns.adobe.com/personalization/html-content-item", + "https://ns.adobe.com/personalization/json-content-item", + "https://ns.adobe.com/personalization/redirect-item", + "https://ns.adobe.com/personalization/dom-action" + ], + decisionScopes: ["__view__"], + surfaces: ["web://example.com/home"] + } + } + }); + expect(result).toEqual({ + propositions: [ + { + renderAttempted: false, + id: "TNT:activity6:experience1", + scope: "testScope", + items: [ + { + id: "0", + schema: "https://ns.adobe.com/personalization/html-content-item", + data: { + id: "0", + format: "text/html", + content: "testScope content1" + } + }, + { + schema: + "https://ns.adobe.com/personalization/default-content-item" + }, + { + schema: "https://ns.adobe.com/personalization/measurement", + data: { + type: "click", + format: "application/vnd.adobe.target.metric" + } + } + ], + scopeDetails: { + eventTokens: { + display: "displayToken1", + click: "clickToken1" + } + } + } + ], + decisions: [ + { + id: "TNT:activity6:experience1", + scope: "testScope", + items: [ + { + id: "0", + schema: "https://ns.adobe.com/personalization/html-content-item", + data: { + id: "0", + format: "text/html", + content: "testScope content1" + } + }, + { + schema: + "https://ns.adobe.com/personalization/default-content-item" + }, + { + schema: "https://ns.adobe.com/personalization/measurement", + data: { + type: "click", + format: "application/vnd.adobe.target.metric" + } + } + ], + scopeDetails: { + eventTokens: { + display: "displayToken1", + click: "clickToken1" + } + } + } + ] + }); + expect(mocks.sendEvent).not.toHaveBeenCalled(); + + expect(mocks.logger.warn).not.toHaveBeenCalled(); + expect(mocks.logger.error).not.toHaveBeenCalled(); + }); +}); diff --git a/test/unit/specs/components/Personalization/topLevel/mixedPropositions.spec.js b/test/unit/specs/components/Personalization/topLevel/mixedPropositions.spec.js new file mode 100644 index 000000000..96efe02cc --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/mixedPropositions.spec.js @@ -0,0 +1,271 @@ +import { MIXED_PROPOSITIONS } from "../responsesMock/eventResponses"; + +import buildMocks from "./buildMocks"; +import buildAlloy from "./buildAlloy"; +import resetMocks from "./resetMocks"; + +describe("PersonalizationComponent", () => { + it("MIXED_PROPOSITIONS", async () => { + const mocks = buildMocks(MIXED_PROPOSITIONS); + const alloy = buildAlloy(mocks); + const { event, result } = await alloy.sendEvent( + { + renderDecisions: true + }, + MIXED_PROPOSITIONS + ); + expect(event.toJSON()).toEqual({ + query: { + personalization: { + schemas: [ + "https://ns.adobe.com/personalization/default-content-item", + "https://ns.adobe.com/personalization/html-content-item", + "https://ns.adobe.com/personalization/json-content-item", + "https://ns.adobe.com/personalization/redirect-item", + "https://ns.adobe.com/personalization/dom-action" + ], + decisionScopes: ["__view__"], + surfaces: ["web://example.com/home"] + } + } + }); + expect(result.propositions).toEqual( + jasmine.arrayWithExactContents([ + { + renderAttempted: true, + id: "AT:eyJhY3Rpdml0eUlkIjoiNDQyMzU4IiwiZXhwZXJpZW5jZUlkIjoiIn1=", + scope: "__view__", + items: [ + { + id: "442358", + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + type: "click", + format: "application/vnd.adobe.target.dom-action", + selector: "#root" + } + } + ] + }, + { + renderAttempted: true, + id: "AT:eyJhY3Rpdml0eUlkIjoiNDQyMzU4IiwiZXhwZXJpZW5jZUlkIjoiIn2=", + scope: "__view__", + items: [ + { + id: "442379", + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + type: "click", + format: "application/vnd.adobe.target.dom-action", + selector: "#root" + } + } + ] + }, + { + renderAttempted: false, + id: "AT:eyJhY3Rpdml0eUlkIjoiNDQyMzU4IiwiZXhwZXJpZW5jZUlkIjoiIn1=", + scope: "home", + items: [ + { + id: "442359", + schema: "https://ns.adobe.com/personalization/html-content-item", + data: { + content: "

Some custom content for the home page

", + format: "text/html", + id: "1202448" + } + } + ] + }, + { + renderAttempted: false, + id: "AT:eyJhY3Rpdml0eUlkIjoiNDQyMzU4IiwiZXhwZXJpZW5jZUlkIjoiIn1=", + scope: "home", + items: [ + { + id: "442360", + schema: "https://ns.adobe.com/personalization/json-content-item", + data: { + content: "{'field1': 'custom content'}", + format: "text/javascript", + id: "1202449" + } + } + ] + }, + { + renderAttempted: false, + id: "AT:eyJhY3Rpdml0eUlkIjoiMTQxNjY0IiwiZXhwZXJpZW5jZUlkIjoiMCJ9", + scope: "home", + items: [ + { + id: "xcore:personalized-offer:134ce877e13a04ca", + etag: "4", + schema: + "https://ns.adobe.com/experience/offer-management/content-component-html", + data: { + id: "xcore:personalized-offer:134ce877e13a04ca", + format: "text/html", + language: ["en-us"], + content: "

An html offer from Offer Decisioning

", + characteristics: { + testing: "true" + } + } + } + ] + }, + { + renderAttempted: false, + id: "AT:eyJhY3Rpdml0eUlkIjoiNDQyMzU4IiwiZXhwZXJpZW5jZUlkIjoiIn0=", + scope: "home", + items: [ + { + id: "442358", + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + type: "click", + format: "application/vnd.adobe.target.dom-action", + selector: "#root" + } + } + ] + } + ]) + ); + expect(result.decisions).toEqual( + jasmine.arrayWithExactContents([ + { + id: "AT:eyJhY3Rpdml0eUlkIjoiNDQyMzU4IiwiZXhwZXJpZW5jZUlkIjoiIn1=", + scope: "home", + items: [ + { + id: "442359", + schema: "https://ns.adobe.com/personalization/html-content-item", + data: { + content: "

Some custom content for the home page

", + format: "text/html", + id: "1202448" + } + } + ] + }, + { + id: "AT:eyJhY3Rpdml0eUlkIjoiNDQyMzU4IiwiZXhwZXJpZW5jZUlkIjoiIn1=", + scope: "home", + items: [ + { + id: "442360", + schema: "https://ns.adobe.com/personalization/json-content-item", + data: { + content: "{'field1': 'custom content'}", + format: "text/javascript", + id: "1202449" + } + } + ] + }, + { + id: "AT:eyJhY3Rpdml0eUlkIjoiMTQxNjY0IiwiZXhwZXJpZW5jZUlkIjoiMCJ9", + scope: "home", + items: [ + { + id: "xcore:personalized-offer:134ce877e13a04ca", + etag: "4", + schema: + "https://ns.adobe.com/experience/offer-management/content-component-html", + data: { + id: "xcore:personalized-offer:134ce877e13a04ca", + format: "text/html", + language: ["en-us"], + content: "

An html offer from Offer Decisioning

", + characteristics: { + testing: "true" + } + } + } + ] + }, + { + id: "AT:eyJhY3Rpdml0eUlkIjoiNDQyMzU4IiwiZXhwZXJpZW5jZUlkIjoiIn0=", + scope: "home", + items: [ + { + id: "442358", + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + type: "click", + format: "application/vnd.adobe.target.dom-action", + selector: "#root" + } + } + ] + } + ]) + ); + expect(mocks.sendEvent).not.toHaveBeenCalled(); + + expect(mocks.logger.warn).not.toHaveBeenCalled(); + expect(mocks.logger.error).not.toHaveBeenCalled(); + + resetMocks(mocks); + const applyPropositionsResult = await alloy.applyPropositions({ + propositions: result.propositions, + metadata: { + home: { + selector: "#myhomeselector", + actionType: "appendHtml" + } + } + }); + expect(applyPropositionsResult.propositions).toEqual([ + { + id: "AT:eyJhY3Rpdml0eUlkIjoiNDQyMzU4IiwiZXhwZXJpZW5jZUlkIjoiIn0=", + scope: "home", + items: [ + { + id: "442358", + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + type: "click", + format: "application/vnd.adobe.target.dom-action", + selector: "#root" + } + } + ], + renderAttempted: true, + scopeDetails: undefined + }, + { + id: "AT:eyJhY3Rpdml0eUlkIjoiNDQyMzU4IiwiZXhwZXJpZW5jZUlkIjoiIn1=", + scope: "home", + items: [ + { + id: "442359", + schema: "https://ns.adobe.com/personalization/html-content-item", + data: { + content: "

Some custom content for the home page

", + format: "text/html", + id: "1202448", + selector: "#myhomeselector", + type: "appendHtml" + } + } + ], + renderAttempted: true, + scopeDetails: undefined + } + ]); + expect(applyPropositionsResult.decisions).toBeUndefined(); + + expect(mocks.sendEvent).not.toHaveBeenCalled(); + expect(mocks.actions.appendHtml).toHaveBeenCalledOnceWith( + "#myhomeselector", + "

Some custom content for the home page

" + ); + expect(mocks.logger.warn).not.toHaveBeenCalled(); + expect(mocks.logger.error).not.toHaveBeenCalled(); + }); +}); diff --git a/test/unit/specs/components/Personalization/topLevel/pageWideDecisionsWithDomActionSchemaItems.spec.js b/test/unit/specs/components/Personalization/topLevel/pageWideDecisionsWithDomActionSchemaItems.spec.js new file mode 100644 index 000000000..3d936ca45 --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/pageWideDecisionsWithDomActionSchemaItems.spec.js @@ -0,0 +1,144 @@ +import { PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS } from "../responsesMock/eventResponses"; + +import buildMocks from "./buildMocks"; +import buildAlloy from "./buildAlloy"; + +describe("PersonalizationComponent", () => { + it("PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS", async () => { + const mocks = buildMocks(PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS); + const alloy = buildAlloy(mocks); + const { event, result } = await alloy.sendEvent( + { + renderDecisions: true + }, + PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS + ); + expect(event.toJSON()).toEqual({ + query: { + personalization: { + schemas: [ + "https://ns.adobe.com/personalization/default-content-item", + "https://ns.adobe.com/personalization/html-content-item", + "https://ns.adobe.com/personalization/json-content-item", + "https://ns.adobe.com/personalization/redirect-item", + "https://ns.adobe.com/personalization/dom-action" + ], + decisionScopes: ["__view__"], + surfaces: ["web://example.com/home"] + } + } + }); + expect(result).toEqual({ + propositions: [ + { + renderAttempted: true, + id: "TNT:activity1:experience1", + scope: "__view__", + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + type: "setHtml", + selector: "#foo", + content: "
Hola Mundo
" + } + }, + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + type: "setHtml", + selector: "#foo2", + content: "
here is a target activity
" + } + }, + { + schema: + "https://ns.adobe.com/personalization/default-content-item" + } + ], + scopeDetails: { + blah: "test" + } + }, + { + renderAttempted: true, + id: "AJO:campaign1:message1", + scope: "web://alloy.test.com/test/page/1", + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + type: "setHtml", + selector: "#foo", + content: "
Hola Mundo
" + } + }, + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + type: "setHtml", + selector: "#foo2", + content: "
here is a target activity
" + } + }, + { + schema: + "https://ns.adobe.com/personalization/default-content-item" + } + ], + scopeDetails: { + decisionProvider: "AJO" + } + } + ], + decisions: [] + }); + expect(mocks.sendEvent).toHaveBeenCalledWith({ + xdm: { + _experience: { + decisioning: { + propositions: [ + { + id: "TNT:activity1:experience1", + scope: "__view__", + scopeDetails: { + blah: "test" + } + }, + { + id: "AJO:campaign1:message1", + scope: "web://alloy.test.com/test/page/1", + scopeDetails: { + decisionProvider: "AJO" + } + } + ], + propositionEventType: { + display: 1 + } + } + }, + eventType: "decisioning.propositionDisplay" + } + }); + expect(mocks.actions.setHtml).toHaveBeenCalledWith( + "#foo", + "
Hola Mundo
" + ); + expect(mocks.actions.setHtml).toHaveBeenCalledWith( + "#foo2", + "
here is a target activity
" + ); + expect(mocks.actions.setHtml).toHaveBeenCalledWith( + "#foo", + "
Hola Mundo
" + ); + expect(mocks.actions.setHtml).toHaveBeenCalledWith( + "#foo2", + "
here is a target activity
" + ); + expect(mocks.actions.setHtml).toHaveBeenCalledTimes(4); + expect(mocks.logger.warn).not.toHaveBeenCalled(); + expect(mocks.logger.error).not.toHaveBeenCalled(); + }); +}); diff --git a/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisions.spec.js b/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisions.spec.js new file mode 100644 index 000000000..d4d9fec2c --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisions.spec.js @@ -0,0 +1,194 @@ +import { PAGE_WIDE_SCOPE_DECISIONS } from "../responsesMock/eventResponses"; + +import buildMocks from "./buildMocks"; +import buildAlloy from "./buildAlloy"; + +describe("PersonalizationComponent", () => { + it("PAGE_WIDE_SCOPE_DECISIONS", async () => { + const mocks = buildMocks(PAGE_WIDE_SCOPE_DECISIONS); + const alloy = buildAlloy(mocks); + const { event, result } = await alloy.sendEvent( + { + renderDecisions: true + }, + PAGE_WIDE_SCOPE_DECISIONS + ); + expect(event.toJSON()).toEqual({ + query: { + personalization: { + schemas: [ + "https://ns.adobe.com/personalization/default-content-item", + "https://ns.adobe.com/personalization/html-content-item", + "https://ns.adobe.com/personalization/json-content-item", + "https://ns.adobe.com/personalization/redirect-item", + "https://ns.adobe.com/personalization/dom-action" + ], + decisionScopes: ["__view__"], + surfaces: ["web://example.com/home"] + } + } + }); + expect(result.propositions).toEqual( + jasmine.arrayWithExactContents([ + { + renderAttempted: true, + id: "TNT:activity1:experience1", + scope: "__view__", + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + type: "setHtml", + selector: "#foo", + content: "
Hola Mundo
" + } + }, + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + type: "setHtml", + selector: "#foo2", + content: "
here is a target activity
" + } + }, + { + schema: + "https://ns.adobe.com/personalization/default-content-item" + } + ], + scopeDetails: { + blah: "test" + } + }, + { + renderAttempted: true, + id: "AJO:campaign1:message1", + scope: "web://alloy.test.com/test/page/1", + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + type: "setHtml", + selector: "#foo", + content: "
Hola Mundo
" + } + }, + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + type: "setHtml", + selector: "#foo2", + content: "
here is a target activity
" + } + }, + { + schema: + "https://ns.adobe.com/personalization/default-content-item" + } + ], + scopeDetails: { + decisionProvider: "AJO" + } + }, + { + renderAttempted: false, + id: "TNT:activity1:experience1", + scope: "__view__", + items: [ + { + schema: "https://ns.adove.com/experience/item", + data: { + id: "A", + content: "Banner A ...." + } + }, + { + schema: "https://ns.adove.com/experience/item", + data: { + id: "B", + content: "Banner B ...." + } + } + ], + scopeDetails: { + blah: "test" + } + } + ]) + ); + expect(result.decisions).toEqual( + jasmine.arrayWithExactContents([ + { + id: "TNT:activity1:experience1", + scope: "__view__", + items: [ + { + schema: "https://ns.adove.com/experience/item", + data: { + id: "A", + content: "Banner A ...." + } + }, + { + schema: "https://ns.adove.com/experience/item", + data: { + id: "B", + content: "Banner B ...." + } + } + ], + scopeDetails: { + blah: "test" + } + } + ]) + ); + expect(mocks.sendEvent).toHaveBeenCalledWith({ + xdm: { + _experience: { + decisioning: { + propositions: [ + { + id: "TNT:activity1:experience1", + scope: "__view__", + scopeDetails: { + blah: "test" + } + }, + { + id: "AJO:campaign1:message1", + scope: "web://alloy.test.com/test/page/1", + scopeDetails: { + decisionProvider: "AJO" + } + } + ], + propositionEventType: { + display: 1 + } + } + }, + eventType: "decisioning.propositionDisplay" + } + }); + expect(mocks.actions.setHtml).toHaveBeenCalledWith( + "#foo", + "
Hola Mundo
" + ); + expect(mocks.actions.setHtml).toHaveBeenCalledWith( + "#foo2", + "
here is a target activity
" + ); + expect(mocks.actions.setHtml).toHaveBeenCalledWith( + "#foo", + "
Hola Mundo
" + ); + expect(mocks.actions.setHtml).toHaveBeenCalledWith( + "#foo2", + "
here is a target activity
" + ); + expect(mocks.actions.setHtml).toHaveBeenCalledTimes(4); + expect(mocks.logger.warn).not.toHaveBeenCalled(); + expect(mocks.logger.error).not.toHaveBeenCalled(); + }); +}); diff --git a/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisionsWithoutDomActionSchemaItems.spec.js b/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisionsWithoutDomActionSchemaItems.spec.js new file mode 100644 index 000000000..fc96d24bd --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisionsWithoutDomActionSchemaItems.spec.js @@ -0,0 +1,91 @@ +import { PAGE_WIDE_SCOPE_DECISIONS_WITHOUT_DOM_ACTION_SCHEMA_ITEMS } from "../responsesMock/eventResponses"; + +import buildMocks from "./buildMocks"; +import buildAlloy from "./buildAlloy"; + +describe("PersonalizationComponent", () => { + it("PAGE_WIDE_SCOPE_DECISIONS_WITHOUT_DOM_ACTION_SCHEMA_ITEMS", async () => { + const mocks = buildMocks( + PAGE_WIDE_SCOPE_DECISIONS_WITHOUT_DOM_ACTION_SCHEMA_ITEMS + ); + const alloy = buildAlloy(mocks); + const { event, result } = await alloy.sendEvent( + { + renderDecisions: true + }, + PAGE_WIDE_SCOPE_DECISIONS_WITHOUT_DOM_ACTION_SCHEMA_ITEMS + ); + expect(event.toJSON()).toEqual({ + query: { + personalization: { + schemas: [ + "https://ns.adobe.com/personalization/default-content-item", + "https://ns.adobe.com/personalization/html-content-item", + "https://ns.adobe.com/personalization/json-content-item", + "https://ns.adobe.com/personalization/redirect-item", + "https://ns.adobe.com/personalization/dom-action" + ], + decisionScopes: ["__view__"], + surfaces: ["web://example.com/home"] + } + } + }); + expect(result).toEqual({ + propositions: [ + { + renderAttempted: false, + id: "TNT:activity1:experience1", + scope: "__view__", + items: [ + { + schema: "https://ns.adove.com/experience/item", + data: { + id: "A", + content: "Banner A ...." + } + }, + { + schema: "https://ns.adove.com/experience/item", + data: { + id: "B", + content: "Banner B ...." + } + } + ], + scopeDetails: { + blah: "test" + } + } + ], + decisions: [ + { + id: "TNT:activity1:experience1", + scope: "__view__", + items: [ + { + schema: "https://ns.adove.com/experience/item", + data: { + id: "A", + content: "Banner A ...." + } + }, + { + schema: "https://ns.adove.com/experience/item", + data: { + id: "B", + content: "Banner B ...." + } + } + ], + scopeDetails: { + blah: "test" + } + } + ] + }); + expect(mocks.sendEvent).not.toHaveBeenCalled(); + + expect(mocks.logger.warn).not.toHaveBeenCalled(); + expect(mocks.logger.error).not.toHaveBeenCalled(); + }); +}); diff --git a/test/unit/specs/components/Personalization/topLevel/productsViewDecisions.spec.js b/test/unit/specs/components/Personalization/topLevel/productsViewDecisions.spec.js new file mode 100644 index 000000000..0f7c72002 --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/productsViewDecisions.spec.js @@ -0,0 +1,40 @@ +import { PRODUCTS_VIEW_DECISIONS } from "../responsesMock/eventResponses"; + +import buildMocks from "./buildMocks"; +import buildAlloy from "./buildAlloy"; + +describe("PersonalizationComponent", () => { + it("PRODUCTS_VIEW_DECISIONS", async () => { + const mocks = buildMocks(PRODUCTS_VIEW_DECISIONS); + const alloy = buildAlloy(mocks); + const { event, result } = await alloy.sendEvent( + { + renderDecisions: true + }, + PRODUCTS_VIEW_DECISIONS + ); + expect(event.toJSON()).toEqual({ + query: { + personalization: { + schemas: [ + "https://ns.adobe.com/personalization/default-content-item", + "https://ns.adobe.com/personalization/html-content-item", + "https://ns.adobe.com/personalization/json-content-item", + "https://ns.adobe.com/personalization/redirect-item", + "https://ns.adobe.com/personalization/dom-action" + ], + decisionScopes: ["__view__"], + surfaces: ["web://example.com/home"] + } + } + }); + expect(result).toEqual({ + propositions: [], + decisions: [] + }); + expect(mocks.sendEvent).not.toHaveBeenCalled(); + + expect(mocks.logger.warn).not.toHaveBeenCalled(); + expect(mocks.logger.error).not.toHaveBeenCalled(); + }); +}); diff --git a/test/unit/specs/components/Personalization/topLevel/redirectPageWideScopeDecision.spec.js b/test/unit/specs/components/Personalization/topLevel/redirectPageWideScopeDecision.spec.js new file mode 100644 index 000000000..92c6d984c --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/redirectPageWideScopeDecision.spec.js @@ -0,0 +1,57 @@ +import { REDIRECT_PAGE_WIDE_SCOPE_DECISION } from "../responsesMock/eventResponses"; + +import buildMocks from "./buildMocks"; +import buildAlloy from "./buildAlloy"; + +describe("PersonalizationComponent", () => { + it("REDIRECT_PAGE_WIDE_SCOPE_DECISION", async () => { + const mocks = buildMocks(REDIRECT_PAGE_WIDE_SCOPE_DECISION); + const alloy = buildAlloy(mocks); + const { event } = await alloy.sendEvent( + { + renderDecisions: true + }, + REDIRECT_PAGE_WIDE_SCOPE_DECISION + ); + expect(event.toJSON()).toEqual({ + query: { + personalization: { + schemas: [ + "https://ns.adobe.com/personalization/default-content-item", + "https://ns.adobe.com/personalization/html-content-item", + "https://ns.adobe.com/personalization/json-content-item", + "https://ns.adobe.com/personalization/redirect-item", + "https://ns.adobe.com/personalization/dom-action" + ], + decisionScopes: ["__view__"], + surfaces: ["web://example.com/home"] + } + } + }); + // No expectation on the result value because the page will redirect soon. + expect(mocks.sendEvent).toHaveBeenCalledWith({ + xdm: { + _experience: { + decisioning: { + propositions: [ + { + id: "TNT:activity15:experience1", + scope: "__view__", + scopeDetails: { + blah: "test" + } + } + ], + propositionEventType: { + display: 1 + } + } + }, + eventType: "decisioning.propositionDisplay" + } + }); + + expect(mocks.logger.warn).not.toHaveBeenCalled(); + expect(mocks.logger.error).not.toHaveBeenCalled(); + }); +}); diff --git a/test/unit/specs/components/Personalization/topLevel/resetMocks.js b/test/unit/specs/components/Personalization/topLevel/resetMocks.js new file mode 100644 index 000000000..202d917ae --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/resetMocks.js @@ -0,0 +1,18 @@ +export default ({ + actions, + logger, + sendEvent, + window, + hideContainers, + showContainers +}) => { + Object.keys(actions).forEach(key => { + actions[key].calls.reset(); + }); + logger.warn.calls.reset(); + logger.error.calls.reset(); + sendEvent.calls.reset(); + window.location.replace.calls.reset(); + hideContainers.calls.reset(); + showContainers.calls.reset(); +}; diff --git a/test/unit/specs/components/Personalization/topLevel/scopesFoo1Foo2Decisions.spec.js b/test/unit/specs/components/Personalization/topLevel/scopesFoo1Foo2Decisions.spec.js new file mode 100644 index 000000000..cf5e76d12 --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/scopesFoo1Foo2Decisions.spec.js @@ -0,0 +1,136 @@ +import { SCOPES_FOO1_FOO2_DECISIONS } from "../responsesMock/eventResponses"; + +import buildMocks from "./buildMocks"; +import buildAlloy from "./buildAlloy"; + +describe("PersonalizationComponent", () => { + it("SCOPES_FOO1_FOO2_DECISIONS", async () => { + const mocks = buildMocks(SCOPES_FOO1_FOO2_DECISIONS); + const alloy = buildAlloy(mocks); + const { event, result } = await alloy.sendEvent( + { + renderDecisions: true + }, + SCOPES_FOO1_FOO2_DECISIONS + ); + expect(event.toJSON()).toEqual({ + query: { + personalization: { + schemas: [ + "https://ns.adobe.com/personalization/default-content-item", + "https://ns.adobe.com/personalization/html-content-item", + "https://ns.adobe.com/personalization/json-content-item", + "https://ns.adobe.com/personalization/redirect-item", + "https://ns.adobe.com/personalization/dom-action" + ], + decisionScopes: ["__view__"], + surfaces: ["web://example.com/home"] + } + } + }); + expect(result).toEqual({ + propositions: [ + { + renderAttempted: false, + id: "TNT:ABC:A", + scope: "Foo1", + items: [ + { + schema: "https://ns.adove.com/experience/item-article", + data: { + id: "1", + url: "https://foo.com/article/1", + thumbnailUrl: "https://foo.com/image/1?size=400x300" + } + }, + { + schema: "https://ns.adove.com/experience/item-article", + data: { + id: "2", + url: "https://foo.com/article/2", + thumbnailUrl: "https://foo.com/image/2?size=400x300" + } + }, + { + schema: "https://ns.adove.com/experience/item-article", + data: { + id: "3", + url: "https://foo.com/article/3", + thumbnailUrl: "https://foo.com/image/3?size=400x300" + } + } + ], + scopeDetails: { + blah: "test" + } + }, + { + renderAttempted: false, + id: "TNT:ABC:A", + scope: "Foo2", + items: [ + { + schema: "https://ns.adove.com/experience/item", + data: { + id: "A", + content: "Banner A ...." + } + } + ] + } + ], + decisions: [ + { + id: "TNT:ABC:A", + scope: "Foo1", + items: [ + { + schema: "https://ns.adove.com/experience/item-article", + data: { + id: "1", + url: "https://foo.com/article/1", + thumbnailUrl: "https://foo.com/image/1?size=400x300" + } + }, + { + schema: "https://ns.adove.com/experience/item-article", + data: { + id: "2", + url: "https://foo.com/article/2", + thumbnailUrl: "https://foo.com/image/2?size=400x300" + } + }, + { + schema: "https://ns.adove.com/experience/item-article", + data: { + id: "3", + url: "https://foo.com/article/3", + thumbnailUrl: "https://foo.com/image/3?size=400x300" + } + } + ], + scopeDetails: { + blah: "test" + } + }, + { + id: "TNT:ABC:A", + scope: "Foo2", + items: [ + { + schema: "https://ns.adove.com/experience/item", + data: { + id: "A", + content: "Banner A ...." + } + } + ] + } + ] + }); + expect(mocks.sendEvent).not.toHaveBeenCalled(); + + expect(mocks.logger.warn).not.toHaveBeenCalled(); + expect(mocks.logger.error).not.toHaveBeenCalled(); + }); +}); diff --git a/test/unit/specs/components/Personalization/utils/composePersonalizationResultingObject.spec.js b/test/unit/specs/components/Personalization/utils/composePersonalizationResultingObject.spec.js deleted file mode 100644 index bb4eac352..000000000 --- a/test/unit/specs/components/Personalization/utils/composePersonalizationResultingObject.spec.js +++ /dev/null @@ -1,35 +0,0 @@ -/* -Copyright 2022 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ - -import composePersonalizationResultingObject from "../../../../../../src/components/Personalization/utils/composePersonalizationResultingObject"; - -describe("Personalization::composePersonalizationResultingObject", () => { - const decisions = [ - { - blah: "123" - }, - { - blah: "345" - } - ]; - it("adds a renderAttempted flag if renderDecisions is true", () => { - const result = composePersonalizationResultingObject(decisions, true); - expect(result.propositions[0].renderAttempted).toEqual(true); - expect(result.decisions).toBeUndefined(); - }); - - it("returns decisions without renderAttempted flag and propositions with renderAttempted false when render decisions is false", () => { - const result = composePersonalizationResultingObject(decisions, false); - expect(result.propositions[0].renderAttempted).toEqual(false); - expect(result.decisions).toEqual(decisions); - }); -});