From 22d1d819208eac40171c68e9f90ce27c6451bb50 Mon Sep 17 00:00:00 2001 From: Jon Snyder Date: Mon, 6 Feb 2023 12:52:20 -0700 Subject: [PATCH 01/20] WIP add handlers for each kind of activity --- .../Personalization/createFetchDataHandler.js | 20 +++++-- .../handlers/createCachingHandler.js | 17 ++++++ .../handlers/createDomActionHandler.js | 38 +++++++++++++ .../createMeasurementSchemaHandler.js | 11 ++++ .../handlers/createProposition.js | 53 +++++++++++++++++++ .../handlers/createRedirectHandler.js | 14 +++++ .../handlers/propositionHandler.js | 48 +++++++++++++++++ src/components/Personalization/index.js | 13 +---- 8 files changed, 198 insertions(+), 16 deletions(-) create mode 100644 src/components/Personalization/handlers/createCachingHandler.js create mode 100644 src/components/Personalization/handlers/createDomActionHandler.js create mode 100644 src/components/Personalization/handlers/createMeasurementSchemaHandler.js create mode 100644 src/components/Personalization/handlers/createProposition.js create mode 100644 src/components/Personalization/handlers/createRedirectHandler.js create mode 100644 src/components/Personalization/handlers/propositionHandler.js diff --git a/src/components/Personalization/createFetchDataHandler.js b/src/components/Personalization/createFetchDataHandler.js index df45aafc6..0d7436017 100644 --- a/src/components/Personalization/createFetchDataHandler.js +++ b/src/components/Personalization/createFetchDataHandler.js @@ -12,9 +12,12 @@ governing permissions and limitations under the License. export default ({ prehidingStyle, - responseHandler, + propositionHandler, hideContainers, - mergeQuery + mergeQuery, + renderHandler, + nonRenderHandler, + collect }) => { return ({ decisionsDeferred, personalizationDetails, event, onResponse }) => { if (personalizationDetails.isRenderDecisions()) { @@ -22,8 +25,17 @@ export default ({ } mergeQuery(event, personalizationDetails.createQueryDetails()); - onResponse(({ response }) => - responseHandler({ decisionsDeferred, personalizationDetails, response }) + onResponse(({ response }) => { + const handles = response.getPayloadsByType(DECISIONS_HANDLE); + const handler = personalizationDetails.isRenderDecisions() ? renderHandler : nonRenderHandler; + const viewName = personalizationDetails.getViewName(); + const sendDisplayNotification = decisionsMeta => { + if (decisionsMeta.length > 0) { + collect({ decisionsMeta, viewName }); + } + }; + + return propositionHandler({ handles, handler, viewName, decisionsDeferred, sendDisplayNotification}); ); }; }; diff --git a/src/components/Personalization/handlers/createCachingHandler.js b/src/components/Personalization/handlers/createCachingHandler.js new file mode 100644 index 000000000..c61a8622a --- /dev/null +++ b/src/components/Personalization/handlers/createCachingHandler.js @@ -0,0 +1,17 @@ +export default ({ next }) => args => { + const { proposition } = args; + const { + scopeDetails: { + characteristics: { + scopeType + } + } + } = proposition.getHandle(); + + if (scopeType === VIEW_SCOPE_TYPE) { + cache(); + } + + // this proposition may contain items that need to be rendered or cached by other handlers. + next(args); +}; diff --git a/src/components/Personalization/handlers/createDomActionHandler.js b/src/components/Personalization/handlers/createDomActionHandler.js new file mode 100644 index 000000000..94d0bad7e --- /dev/null +++ b/src/components/Personalization/handlers/createDomActionHandler.js @@ -0,0 +1,38 @@ +import { DOM_ACTION } from "../constants/schema"; + +export default ({ next }) => args => { + const { proposition, viewName } = args; + const { + scope, + scopeDetails: { + characteristics: { + scopeType + } + }, + items + } = proposition.getHandle(); + + if (scope === PAGE_WIDE_SCOPE || isPageWideSurface(scope) || + scopeType === VIEW_SCOPE_TYPE && scope === viewName) { + + if (items.some(({ schema }) => schema === DOM_ACTION )) { + //render(createItemRenderer(items)); + /* + const { schema, data } = item; + if (schema === DOM_ACTION) { + const execute = modules[type] + const remappedData = remapHeadOffers(data); + render(() => { + if (!execute) { + logger.error(`DOM action "${type}" not found`); + return; + } + return execute(); + }); + } +*/ + } + } + // this proposition may contain items that need to be rendered or cached by other handlers. + next(args); +}; diff --git a/src/components/Personalization/handlers/createMeasurementSchemaHandler.js b/src/components/Personalization/handlers/createMeasurementSchemaHandler.js new file mode 100644 index 000000000..128ae485d --- /dev/null +++ b/src/components/Personalization/handlers/createMeasurementSchemaHandler.js @@ -0,0 +1,11 @@ +import { MEASUREMENT_SCHEMA } from "../constants/schema"; + +export default ({ next }) => args => { + const { handle: { items } } = args; + + // 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(args); + } +}; diff --git a/src/components/Personalization/handlers/createProposition.js b/src/components/Personalization/handlers/createProposition.js new file mode 100644 index 000000000..32cd9cebf --- /dev/null +++ b/src/components/Personalization/handlers/createProposition.js @@ -0,0 +1,53 @@ +export default (handle) => { + + const { id, scope, scopeDetails } = handle; + + let renderers = []; + let redirectUrl; + let markedForCache = false; + let renderAttempted = false; + + return { + getHandle() { + return handle; + }, + redirect(url) { + renderAttempted = true; + redirectUrl = url; + }, + getRedirectUrl() { + return redirectUrl; + }, + cache() { + markedForCache = true; + }, + addRenderer(renderer) { + renderAttempted = true; + renderers.push(renderer); + }, + render() { + return Promise.all(renderers.map(renderer => renderer())); + }, + addToNotifications(notifications) { + if (renderAttempted) { + notifications.push({ id, scope, scopeDetails }); + } + }, + addToCache(cache) { + if (!markedForCache) { + return; + } + cache[scope] = cache[scope] || []; + cache[scope].push(handle); + }, + addToReturnedPropositions(propositions) { + propositions.push({ id, scope, scopeDetails, renderAttempted }); + }, + addToReturnedDecisions(decisions) { + if (!renderAttempted) { + decisions.push({ id, scope, scopeDetails }); + } + } + }; + +}; diff --git a/src/components/Personalization/handlers/createRedirectHandler.js b/src/components/Personalization/handlers/createRedirectHandler.js new file mode 100644 index 000000000..b67bb23c0 --- /dev/null +++ b/src/components/Personalization/handlers/createRedirectHandler.js @@ -0,0 +1,14 @@ +import { REDIRECT_ITEM } from "../constants/schema" + +export default ({ next }) => args => { + const { handle: { items }, redirect } = args; + + const redirectItem = find(items, ({ schema }) => schema === REDIRECT_ITEM); + if (redirectItem) { + const { data: { content } } = redirectItem; + redirect(content); + // On a redirect, nothing else needs to handle this. + } else { + next(args); + } +}; diff --git a/src/components/Personalization/handlers/propositionHandler.js b/src/components/Personalization/handlers/propositionHandler.js new file mode 100644 index 000000000..db1f6c26a --- /dev/null +++ b/src/components/Personalization/handlers/propositionHandler.js @@ -0,0 +1,48 @@ +// renderDecisions=true +// redirectHandler +// measurementSchemaHandler +// domActionHandler +// cachingHandler +// no-op + +// renderDecisions=false +// cachingHandler +// no-op + +import createProposition from "./createProposition"; + + +export default ({ handles, handler, viewName, decisionsDeferred, sendDisplayNotification }) => { + const propositions = handles.map(createProposition); + + for( let i = 0; i < propositions.length; i += 1) { + const proposition = propositions[i]; + handler({ proposition, viewName }); + const redirectUrl = proposition.getRedirectUrl(); + if (redirectUrl) { + return sendDisplayNotification([proposition.toNotification()]).then(() => { + window.location.replace(redirectUrl); + }); // TODO add error log message + } + }; + + Promise.all(propositions.map(proposition => proposition.render())).then(() => { + const notificationPropositions = []; + propositions.forEach(proposition => proposition.addToNotifications(notificationPropositions)); + sendDisplayNotification(notificationPropositions); + }); // TODO add error log message? + + const cachedPropositions = {}; + const returnedPropositions = []; + const returnedDecisions = []; + propositions.forEach(p => { + p.addToCache(cachedPropositions) + p.addToReturnedPropositions(returnedPropositions); + p.addtoReturnedDecisions(returnedDecisions); + }); + decisionsDeferred.resolve(cachedPropositions); + return { + propositions: returnedPropositions, + decisions: returnedDecisions + }; +} diff --git a/src/components/Personalization/index.js b/src/components/Personalization/index.js index cd4bb2549..6ab7bacbe 100644 --- a/src/components/Personalization/index.js +++ b/src/components/Personalization/index.js @@ -50,18 +50,7 @@ const createPersonalization = ({ config, logger, eventManager }) => { logger, executeActions }); - const handleRedirectDecisions = createRedirectHandler({ - collect, - window, - logger, - showContainers - }); - const autoRenderingHandler = createAutorenderingHandler({ - viewCache, - executeDecisions, - showContainers, - collect - }); + const applyPropositions = createApplyPropositions({ executeDecisions }); From efe6c1613eea721e2106c1f55057f52c4f2bfa8a Mon Sep 17 00:00:00 2001 From: Jon Snyder Date: Tue, 21 Feb 2023 12:13:27 -0700 Subject: [PATCH 02/20] Finalize personalization handlers --- .../Personalization/createFetchDataHandler.js | 7 +- .../handlers/createCachingHandler.js | 4 +- .../handlers/createDomActionHandler.js | 26 +++---- .../createMeasurementSchemaHandler.js | 3 +- .../handlers/createProposition.js | 4 +- .../handlers/createRedirectHandler.js | 8 ++- .../handlers/propositionHandler.js | 8 ++- src/components/Personalization/index.js | 33 +++++---- .../specs/Personalization/C6364798.js | 4 +- .../handlers/createRedirectHandler.spec.js | 72 +++++++++++++++++++ 10 files changed, 124 insertions(+), 45 deletions(-) create mode 100644 test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js diff --git a/src/components/Personalization/createFetchDataHandler.js b/src/components/Personalization/createFetchDataHandler.js index 0d7436017..2a159d524 100644 --- a/src/components/Personalization/createFetchDataHandler.js +++ b/src/components/Personalization/createFetchDataHandler.js @@ -9,6 +9,7 @@ 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. */ +const DECISIONS_HANDLE = "personalization:decisions"; export default ({ prehidingStyle, @@ -31,11 +32,13 @@ export default ({ const viewName = personalizationDetails.getViewName(); const sendDisplayNotification = decisionsMeta => { if (decisionsMeta.length > 0) { - collect({ decisionsMeta, viewName }); + return collect({ decisionsMeta, viewName }); + } else { + return Promise.resolve(); } }; return propositionHandler({ handles, handler, viewName, decisionsDeferred, sendDisplayNotification}); - ); + }); }; }; diff --git a/src/components/Personalization/handlers/createCachingHandler.js b/src/components/Personalization/handlers/createCachingHandler.js index c61a8622a..38b9e2b91 100644 --- a/src/components/Personalization/handlers/createCachingHandler.js +++ b/src/components/Personalization/handlers/createCachingHandler.js @@ -1,3 +1,5 @@ +import { VIEW_SCOPE_TYPE } from "../constants/scopeType"; + export default ({ next }) => args => { const { proposition } = args; const { @@ -9,7 +11,7 @@ export default ({ next }) => args => { } = proposition.getHandle(); if (scopeType === VIEW_SCOPE_TYPE) { - cache(); + proposition.cache(); } // this proposition may contain items that need to be rendered or cached by other handlers. diff --git a/src/components/Personalization/handlers/createDomActionHandler.js b/src/components/Personalization/handlers/createDomActionHandler.js index 94d0bad7e..88a43b075 100644 --- a/src/components/Personalization/handlers/createDomActionHandler.js +++ b/src/components/Personalization/handlers/createDomActionHandler.js @@ -1,6 +1,8 @@ -import { DOM_ACTION } from "../constants/schema"; +import { DEFAULT_CONTENT_ITEM, DOM_ACTION } from "../constants/schema"; +import PAGE_WIDE_SCOPE from "../../../constants/pageWideScope"; +import { VIEW_SCOPE_TYPE } from "../constants/scopeType" -export default ({ next }) => args => { +export default ({ next, executeDecisions, isPageWideSurface }) => args => { const { proposition, viewName } = args; const { scope, @@ -15,22 +17,10 @@ export default ({ next }) => args => { if (scope === PAGE_WIDE_SCOPE || isPageWideSurface(scope) || scopeType === VIEW_SCOPE_TYPE && scope === viewName) { - if (items.some(({ schema }) => schema === DOM_ACTION )) { - //render(createItemRenderer(items)); - /* - const { schema, data } = item; - if (schema === DOM_ACTION) { - const execute = modules[type] - const remappedData = remapHeadOffers(data); - render(() => { - if (!execute) { - logger.error(`DOM action "${type}" not found`); - return; - } - return execute(); - }); - } -*/ + if (items.some(({ schema }) => schema === DOM_ACTION || schema === DEFAULT_CONTENT_ITEM )) { + proposition.addRenderer(() => { + return executeDecisions([proposition.getHandle()]); + }); } } // this proposition may contain items that need to be rendered or cached by other handlers. diff --git a/src/components/Personalization/handlers/createMeasurementSchemaHandler.js b/src/components/Personalization/handlers/createMeasurementSchemaHandler.js index 128ae485d..83ae323c6 100644 --- a/src/components/Personalization/handlers/createMeasurementSchemaHandler.js +++ b/src/components/Personalization/handlers/createMeasurementSchemaHandler.js @@ -1,7 +1,8 @@ import { MEASUREMENT_SCHEMA } from "../constants/schema"; export default ({ next }) => args => { - const { handle: { items } } = args; + const { proposition } = args; + 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) diff --git a/src/components/Personalization/handlers/createProposition.js b/src/components/Personalization/handlers/createProposition.js index 32cd9cebf..e1a8ae13c 100644 --- a/src/components/Personalization/handlers/createProposition.js +++ b/src/components/Personalization/handlers/createProposition.js @@ -41,11 +41,11 @@ export default (handle) => { cache[scope].push(handle); }, addToReturnedPropositions(propositions) { - propositions.push({ id, scope, scopeDetails, renderAttempted }); + propositions.push({ ...handle, renderAttempted }); }, addToReturnedDecisions(decisions) { if (!renderAttempted) { - decisions.push({ id, scope, scopeDetails }); + decisions.push({ ...handle }); } } }; diff --git a/src/components/Personalization/handlers/createRedirectHandler.js b/src/components/Personalization/handlers/createRedirectHandler.js index b67bb23c0..5b7588c71 100644 --- a/src/components/Personalization/handlers/createRedirectHandler.js +++ b/src/components/Personalization/handlers/createRedirectHandler.js @@ -1,12 +1,14 @@ -import { REDIRECT_ITEM } from "../constants/schema" +import { REDIRECT_ITEM } from "../constants/schema"; +import { find } from "../../../utils"; export default ({ next }) => args => { - const { handle: { items }, redirect } = args; + const { proposition } = args; + const { items } = proposition.getHandle(); const redirectItem = find(items, ({ schema }) => schema === REDIRECT_ITEM); if (redirectItem) { const { data: { content } } = redirectItem; - redirect(content); + proposition.redirect(content); // On a redirect, nothing else needs to handle this. } else { next(args); diff --git a/src/components/Personalization/handlers/propositionHandler.js b/src/components/Personalization/handlers/propositionHandler.js index db1f6c26a..831b08920 100644 --- a/src/components/Personalization/handlers/propositionHandler.js +++ b/src/components/Personalization/handlers/propositionHandler.js @@ -1,4 +1,5 @@ // renderDecisions=true +// -------------------- // redirectHandler // measurementSchemaHandler // domActionHandler @@ -6,6 +7,7 @@ // no-op // renderDecisions=false +// --------------------- // cachingHandler // no-op @@ -20,7 +22,9 @@ export default ({ handles, handler, viewName, decisionsDeferred, sendDisplayNoti handler({ proposition, viewName }); const redirectUrl = proposition.getRedirectUrl(); if (redirectUrl) { - return sendDisplayNotification([proposition.toNotification()]).then(() => { + const notifications = []; + proposition.addToNotifications(notifications); + return sendDisplayNotification(notifications).then(() => { window.location.replace(redirectUrl); }); // TODO add error log message } @@ -38,7 +42,7 @@ export default ({ handles, handler, viewName, decisionsDeferred, sendDisplayNoti propositions.forEach(p => { p.addToCache(cachedPropositions) p.addToReturnedPropositions(returnedPropositions); - p.addtoReturnedDecisions(returnedDecisions); + p.addToReturnedDecisions(returnedDecisions); }); decisionsDeferred.resolve(cachedPropositions); return { diff --git a/src/components/Personalization/index.js b/src/components/Personalization/index.js index 6ab7bacbe..d20afb005 100644 --- a/src/components/Personalization/index.js +++ b/src/components/Personalization/index.js @@ -23,15 +23,16 @@ 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 propositionHandler from "./handlers/propositionHandler"; +import createRedirectHandler from "./handlers/createRedirectHandler"; +import createCachingHandler from "./handlers/createCachingHandler"; +import createDomActionHandler from "./handlers/createDomActionHandler"; +import createMeasurementSchemaHandler from "./handlers/createMeasurementSchemaHandler"; +import { isPageWideSurface } from "./utils/surfaceUtils"; const createPersonalization = ({ config, logger, eventManager }) => { const { targetMigrationEnabled, prehidingStyle } = config; @@ -54,19 +55,21 @@ const createPersonalization = ({ config, logger, eventManager }) => { const applyPropositions = createApplyPropositions({ executeDecisions }); - const nonRenderingHandler = createNonRenderingHandler({ viewCache }); - const responseHandler = createOnResponseHandler({ - autoRenderingHandler, - nonRenderingHandler, - groupDecisions, - handleRedirectDecisions, - showContainers - }); + + const noOpHandler = () => undefined; + const cachingHandler = createCachingHandler({ next: noOpHandler }); + const domActionHandler = createDomActionHandler({ next: cachingHandler, executeDecisions, isPageWideSurface }); + const measurementSchemaHandler = createMeasurementSchemaHandler({ next: domActionHandler }); + const redirectHandler = createRedirectHandler({ next: measurementSchemaHandler }); + const fetchDataHandler = createFetchDataHandler({ prehidingStyle, - responseHandler, + propositionHandler, hideContainers, - mergeQuery + mergeQuery, + renderHandler: redirectHandler, + nonRenderHandler: cachingHandler, + collect }); const onClickHandler = createOnClickHandler({ mergeDecisionsMeta, diff --git a/test/functional/specs/Personalization/C6364798.js b/test/functional/specs/Personalization/C6364798.js index 798a92a15..0167c0d4b 100644 --- a/test/functional/specs/Personalization/C6364798.js +++ b/test/functional/specs/Personalization/C6364798.js @@ -113,6 +113,8 @@ const simulatePageLoad = async alloy => { personalizationPayload, PAGE_WIDE_SCOPE ); + console.log(JSON.stringify(pageWideScopeDecisionsMeta, null, 2)); + await t.debug(); await t .expect( // eslint-disable-next-line no-underscore-dangle @@ -261,7 +263,7 @@ const simulateViewRerender = async (alloy, propositions) => { await t.expect(networkLogger.edgeEndpointLogs.requests.length).eql(4); }; -test("Test C6364798: applyPropositions should re-render SPA view without sending view notifications", async () => { +test.only("Test C6364798: applyPropositions should re-render SPA view without sending view notifications", async () => { const alloy = createAlloyProxy(); await alloy.configure(config); 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..7621c9178 --- /dev/null +++ b/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js @@ -0,0 +1,72 @@ +import createRedirectHandler from "../../../../../../src/components/Personalization/handlers/createRedirectHandler"; +import createProposition from "../../../../../../src/components/Personalization/handlers/createProposition"; + + +fdescribe("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, viewName: "myview" }); + 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"); + }); +}); From 3aeff9dbea9591c01ab84a53fe55fd793f1616df Mon Sep 17 00:00:00 2001 From: Jon Snyder Date: Tue, 15 Aug 2023 15:15:21 -0600 Subject: [PATCH 03/20] Add component tests with support for fetch, viewChange, and apply --- rollup.test.config.js | 1 + .../DataCollector/validateUserEventOptions.js | 4 +- .../createApplyPropositions.js | 15 +- .../Personalization/createComponent.js | 2 +- .../Personalization/createExecuteDecisions.js | 8 +- .../Personalization/createFetchDataHandler.js | 69 ++++- .../createPersonalizationDetails.js | 3 + .../Personalization/createViewCacheManager.js | 30 +- .../createViewChangeHandler.js | 34 ++- .../delivery/createImmediateNotifications.js | 9 + .../dom-actions/executeActions.js | 1 + .../handlers/createCachingHandler.js | 42 ++- .../handlers/createDomActionHandler.js | 48 ++- .../createMeasurementSchemaHandler.js | 2 +- .../handlers/createProposition.js | 44 +-- .../handlers/createPropositionHandler.js | 103 +++++++ .../handlers/createRedirectHandler.js | 2 +- .../handlers/propositionHandler.js | 52 ---- src/components/Personalization/index.js | 2 +- .../Personalization/utils/createAsyncArray.js | 20 ++ src/core/createEvent.js | 22 +- src/utils/deduplicateArray.js | 14 + src/utils/index.js | 1 + .../handlers/createRedirectHandler.spec.js | 2 +- .../responsesMock/eventResponses.js | 5 +- .../Personalization/topLevel/buildAlloy.js | 166 +++++++++++ .../Personalization/topLevel/buildMocks.js | 75 +++++ .../topLevel/cartViewDecisions.spec.js | 265 +++++++++++++++++ .../topLevel/mergedMetricDecisions.spec.js | 112 +++++++ .../topLevel/mixedPropositions.spec.js | 279 ++++++++++++++++++ ...eDecisionsWithDomActionSchemaItems.spec.js | 149 ++++++++++ .../topLevel/pageWideScopeDecisions.spec.js | 195 ++++++++++++ ...cisionsWithoutDomActionSchemaItems.spec.js | 96 ++++++ .../topLevel/productsViewDecisions.spec.js | 47 +++ .../redirectPageWideScopeDecision.spec.js | 66 +++++ .../Personalization/topLevel/resetMocks.js | 18 ++ .../topLevel/scopesFoo1Foo2Decisions.spec.js | 143 +++++++++ 37 files changed, 2030 insertions(+), 116 deletions(-) create mode 100644 src/components/Personalization/delivery/createImmediateNotifications.js create mode 100644 src/components/Personalization/handlers/createPropositionHandler.js delete mode 100644 src/components/Personalization/handlers/propositionHandler.js create mode 100644 src/components/Personalization/utils/createAsyncArray.js create mode 100644 src/utils/deduplicateArray.js create mode 100644 test/unit/specs/components/Personalization/topLevel/buildAlloy.js create mode 100644 test/unit/specs/components/Personalization/topLevel/buildMocks.js create mode 100644 test/unit/specs/components/Personalization/topLevel/cartViewDecisions.spec.js create mode 100644 test/unit/specs/components/Personalization/topLevel/mergedMetricDecisions.spec.js create mode 100644 test/unit/specs/components/Personalization/topLevel/mixedPropositions.spec.js create mode 100644 test/unit/specs/components/Personalization/topLevel/pageWideDecisionsWithDomActionSchemaItems.spec.js create mode 100644 test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisions.spec.js create mode 100644 test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisionsWithoutDomActionSchemaItems.spec.js create mode 100644 test/unit/specs/components/Personalization/topLevel/productsViewDecisions.spec.js create mode 100644 test/unit/specs/components/Personalization/topLevel/redirectPageWideScopeDecision.spec.js create mode 100644 test/unit/specs/components/Personalization/topLevel/resetMocks.js create mode 100644 test/unit/specs/components/Personalization/topLevel/scopesFoo1Foo2Decisions.spec.js 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/DataCollector/validateUserEventOptions.js b/src/components/DataCollector/validateUserEventOptions.js index 9ba6e320c..be5e82dbe 100644 --- a/src/components/DataCollector/validateUserEventOptions.js +++ b/src/components/DataCollector/validateUserEventOptions.js @@ -30,7 +30,9 @@ export default ({ options }) => { decisionScopes: arrayOf(string()).uniqueItems(), personalization: objectOf({ decisionScopes: arrayOf(string()).uniqueItems(), - surfaces: arrayOf(string()).uniqueItems() + surfaces: arrayOf(string()).uniqueItems(), + sendDisplayNotifications: boolean().default(true), + includePendingDisplayNotifications: boolean().default(false) }), datasetId: string(), mergeId: string(), diff --git a/src/components/Personalization/createApplyPropositions.js b/src/components/Personalization/createApplyPropositions.js index dfc840a6d..1c8b63981 100644 --- a/src/components/Personalization/createApplyPropositions.js +++ b/src/components/Personalization/createApplyPropositions.js @@ -18,7 +18,7 @@ import { EMPTY_PROPOSITIONS } from "./validateApplyPropositionsOptions"; export const SUPPORTED_SCHEMAS = [DOM_ACTION, HTML_CONTENT_ITEM]; -export default ({ executeDecisions }) => { +export default ({ propositionHandler, renderHandler }) => { const filterItemsPredicate = item => SUPPORTED_SCHEMAS.indexOf(item.schema) > -1; @@ -71,14 +71,21 @@ export default ({ executeDecisions }) => { .filter(proposition => isNonEmptyArray(proposition.items)); }; - const applyPropositions = ({ propositions, metadata }) => { + const applyPropositions = async ({ propositions, metadata }) => { const propositionsToExecute = preparePropositions({ propositions, metadata }); - return executeDecisions(propositionsToExecute).then(() => { - return composePersonalizationResultingObject(propositionsToExecute, true); + + const result = await propositionHandler({ + handles: propositionsToExecute, + handler: renderHandler, + viewName: undefined, + resolveDisplayNotification: () => undefined, + resolveRedirectNotification: () => undefined }); + delete result.decisions; + return result; }; return ({ propositions, metadata = {} }) => { diff --git a/src/components/Personalization/createComponent.js b/src/components/Personalization/createComponent.js index 9abd6110c..f6c7380f3 100644 --- a/src/components/Personalization/createComponent.js +++ b/src/components/Personalization/createComponent.js @@ -66,9 +66,9 @@ export default ({ if (personalizationDetails.shouldFetchData()) { const decisionsDeferred = defer(); - viewCache.storeViews(decisionsDeferred.promise); onRequestFailure(() => decisionsDeferred.reject()); + fetchDataHandler({ decisionsDeferred, personalizationDetails, diff --git a/src/components/Personalization/createExecuteDecisions.js b/src/components/Personalization/createExecuteDecisions.js index fff2ee8f2..e8d982753 100644 --- a/src/components/Personalization/createExecuteDecisions.js +++ b/src/components/Personalization/createExecuteDecisions.js @@ -40,6 +40,7 @@ const buildActions = decision => { const processMetas = (logger, actionResults) => { const results = flatMap(actionResults, identity); + console.log("ProcessMetas", JSON.stringify(results, null, 2)); const finalMetas = []; const set = new Set(); @@ -73,9 +74,14 @@ export default ({ modules, logger, executeActions }) => { return executeActions(actions, modules, logger); }); + return Promise.all(actionResultsPromises) - .then(results => processMetas(logger, results)) + .then(results => { + console.log("Results:", JSON.stringify(results, null, 2)); + return processMetas(logger, results); + }) .catch(error => { + console.log("Error:", error); logger.error(error); }); }; diff --git a/src/components/Personalization/createFetchDataHandler.js b/src/components/Personalization/createFetchDataHandler.js index 2a159d524..70dd1fd84 100644 --- a/src/components/Personalization/createFetchDataHandler.js +++ b/src/components/Personalization/createFetchDataHandler.js @@ -1,3 +1,6 @@ +import { VIEW_SCOPE_TYPE } from "./constants/scopeType"; +import isPageWideScope from "./utils/isPageWideScope"; + /* Copyright 2020 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); @@ -18,18 +21,59 @@ export default ({ mergeQuery, renderHandler, nonRenderHandler, - collect + collect, + getView }) => { - return ({ decisionsDeferred, personalizationDetails, event, onResponse }) => { + return ({ decisionsDeferred, personalizationDetails, event, onResponse, displayNotificationsDeferred }) => { if (personalizationDetails.isRenderDecisions()) { hideContainers(prehidingStyle); } mergeQuery(event, personalizationDetails.createQueryDetails()); - onResponse(({ response }) => { + onResponse(async ({ response }) => { const handles = response.getPayloadsByType(DECISIONS_HANDLE); - const handler = personalizationDetails.isRenderDecisions() ? renderHandler : nonRenderHandler; + + const viewTypeHandles = []; + const pageWideHandles = []; + const otherHandles = []; + handles.forEach(handle => { + const { + scope, + scopeDetails: { + characteristics: { + scopeType + } = {} + } = {} + } = handle; + if (isPageWideScope(scope)) { + pageWideHandles.push(handle); + } else if (scopeType === VIEW_SCOPE_TYPE) { + viewTypeHandles.push(handle); + } else { + otherHandles.push(handle); + } + }); + decisionsDeferred.resolve(viewTypeHandles); const viewName = personalizationDetails.getViewName(); + const propositionsToHandle = [ + ...(await getView(viewName)), + ...pageWideHandles + ]; + + const handler = personalizationDetails.isRenderDecisions() ? renderHandler : nonRenderHandler; + + /* + const resolveDisplayNotification = decisionsMeta => { + if (!personalizationDetails.isSendDisplayNotifications()) { + return displayNotificationsDeferred.resolve({ decisionsMeta, viewName }); + } + if (decisionsMeta.length > 0) { + displayNotificationsDeferred.resolve({ decisionsMeta, viewName }); + return collect({ decisionsMeta, viewName }); + } + return Promise.resolve(); + }; + */ const sendDisplayNotification = decisionsMeta => { if (decisionsMeta.length > 0) { return collect({ decisionsMeta, viewName }); @@ -38,7 +82,22 @@ export default ({ } }; - return propositionHandler({ handles, handler, viewName, decisionsDeferred, sendDisplayNotification}); + const { propositions, decisions } = propositionHandler({ + handles: propositionsToHandle, + handler, + viewName, + resolveDisplayNotification: sendDisplayNotification, + resolveRedirectNotification: sendDisplayNotification + }); + + otherHandles.forEach(handle => { + propositions.push({ + renderAttempted: false, + ...handle + }); + decisions.push(handle); + }); + return { propositions, decisions }; }); }; }; diff --git a/src/components/Personalization/createPersonalizationDetails.js b/src/components/Personalization/createPersonalizationDetails.js index c62402bff..8eaaa0c2c 100644 --- a/src/components/Personalization/createPersonalizationDetails.js +++ b/src/components/Personalization/createPersonalizationDetails.js @@ -51,6 +51,9 @@ export default ({ isRenderDecisions() { return renderDecisions; }, + isSendDisplayNotifications() { + return !!personalization.sendDisplayNotifications; + }, getViewName() { return viewName; }, diff --git a/src/components/Personalization/createViewCacheManager.js b/src/components/Personalization/createViewCacheManager.js index 8830c6b48..9d00eb5ab 100644 --- a/src/components/Personalization/createViewCacheManager.js +++ b/src/components/Personalization/createViewCacheManager.js @@ -13,17 +13,34 @@ governing permissions and limitations under the License. import { assign } from "../../utils"; import defer from "../../utils/defer"; +const createEmptyViewPropositions = viewName => { + return [{ + scope: viewName, + scopeDetails: { + characteristics: { + scopeType: "view" + } + } + }]; +}; + export default () => { let viewStorage; const viewStorageDeferred = defer(); - const storeViews = decisionsPromise => { - decisionsPromise - .then(decisions => { + const storeViews = viewTypeHandlesPromise => { + viewTypeHandlesPromise + .then(viewTypeHandles => { if (viewStorage === undefined) { viewStorage = {}; } - assign(viewStorage, decisions); + const newViewStorage = viewTypeHandles.reduce((acc, handle) => { + const { scope } = handle; + acc[scope] = acc[scope] || []; + acc[scope].push(handle); + return acc; + }, {}); + assign(viewStorage, newViewStorage); viewStorageDeferred.resolve(); }) .catch(() => { @@ -35,7 +52,10 @@ export default () => { }; const getView = viewName => { - return viewStorageDeferred.promise.then(() => viewStorage[viewName] || []); + return viewStorageDeferred.promise.then(() => + viewStorage[viewName] || + createEmptyViewPropositions(viewName) + ); }; const isInitialized = () => { diff --git a/src/components/Personalization/createViewChangeHandler.js b/src/components/Personalization/createViewChangeHandler.js index 8440abeff..985bb6a66 100644 --- a/src/components/Personalization/createViewChangeHandler.js +++ b/src/components/Personalization/createViewChangeHandler.js @@ -17,13 +17,40 @@ import { PropositionEventType } from "./constants/propositionEventType"; export default ({ mergeDecisionsMeta, collect, - executeDecisions, + renderHandler, + nonRenderHandler, + propositionHandler, viewCache }) => { - return ({ personalizationDetails, event, onResponse }) => { + return async ({ personalizationDetails, event, onResponse }) => { const viewName = personalizationDetails.getViewName(); - return viewCache.getView(viewName).then(viewDecisions => { + const viewDecisions = await viewCache.getView(viewName); + + const handler = personalizationDetails.isRenderDecisions() ? renderHandler : nonRenderHandler; + const sendDisplayNotification = decisionsMeta => { + mergeDecisionsMeta( + event, + decisionsMeta, + PropositionEventType.DISPLAY + ); + return new Promise(resolve => { + onResponse(resolve); + }); + }; + + const result = await propositionHandler({ + handles: viewDecisions, + handler, + viewName, + resolveDisplayNotification: sendDisplayNotification, + resolveRedirectNotification: sendDisplayNotification + }); + + onResponse(() => { + return result; + }); +/* 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 @@ -51,5 +78,6 @@ export default ({ }); return {}; }); +*/ }; }; diff --git a/src/components/Personalization/delivery/createImmediateNotifications.js b/src/components/Personalization/delivery/createImmediateNotifications.js new file mode 100644 index 000000000..645c7ae4a --- /dev/null +++ b/src/components/Personalization/delivery/createImmediateNotifications.js @@ -0,0 +1,9 @@ + +export default ({ collect }) => ({ decisionsMeta, viewName }) => { + if (decisionsMeta.length > 0) { + // TODO just add the code for collect here + return collect({ decisionsMeta, viewName }); + } else { + return Promise.resolve(); + } +}; diff --git a/src/components/Personalization/dom-actions/executeActions.js b/src/components/Personalization/dom-actions/executeActions.js index 3c7a3a346..5c8c5eefd 100644 --- a/src/components/Personalization/dom-actions/executeActions.js +++ b/src/components/Personalization/dom-actions/executeActions.js @@ -45,6 +45,7 @@ const executeAction = (logger, modules, type, args) => { return execute(...args); }; + const PREPROCESSORS = [remapHeadOffers, remapCustomCodeOffers]; const preprocess = action => diff --git a/src/components/Personalization/handlers/createCachingHandler.js b/src/components/Personalization/handlers/createCachingHandler.js index 38b9e2b91..96f800488 100644 --- a/src/components/Personalization/handlers/createCachingHandler.js +++ b/src/components/Personalization/handlers/createCachingHandler.js @@ -6,8 +6,8 @@ export default ({ next }) => args => { scopeDetails: { characteristics: { scopeType - } - } + } = {} + } = {} } = proposition.getHandle(); if (scopeType === VIEW_SCOPE_TYPE) { @@ -17,3 +17,41 @@ export default ({ next }) => args => { // this proposition may contain items that need to be rendered or cached by other handlers. next(args); }; + +/* +import { assign } from "../../utils"; + +export const createViewCacheManager = () => { + const viewStorage = {}; + let storeViewsCalledAtLeastOnce = false; + let previousStoreViewsComplete = Promise.resolve(); + + const storeViews = viewTypeHandlesPromise => { + storeViewsCalledAtLeastOnce = true; + previousStoreViewsComplete = previousStoreViewsComplete + .then(() => viewTypeHandlesPromise) + .then(viewTypeHandles => { + const decisions = viewTypeHandles.reduce((handle, memo) => { + const { scope } = handle; + memo[scope] = memo[scope] || []; + memo[scope].push(handle); + }, {}); + assign(viewStorage, decisions); + }) + .catch(() => {}); + }; + + const getView = viewName => { + return previousStoreViewsComplete.then(() => viewStorage[viewName] || []); + }; + + const isInitialized = () => { + return storeViewsCalledAtLeastOnce; + }; + return { + storeViews, + getView, + isInitialized + }; +}; +*/ diff --git a/src/components/Personalization/handlers/createDomActionHandler.js b/src/components/Personalization/handlers/createDomActionHandler.js index 88a43b075..42f4f4614 100644 --- a/src/components/Personalization/handlers/createDomActionHandler.js +++ b/src/components/Personalization/handlers/createDomActionHandler.js @@ -1,28 +1,52 @@ -import { DEFAULT_CONTENT_ITEM, DOM_ACTION } from "../constants/schema"; +import { DEFAULT_CONTENT_ITEM, DOM_ACTION, HTML_CONTENT_ITEM } from "../constants/schema"; import PAGE_WIDE_SCOPE from "../../../constants/pageWideScope"; import { VIEW_SCOPE_TYPE } from "../constants/scopeType" -export default ({ next, executeDecisions, isPageWideSurface }) => args => { +export default ({ next, isPageWideSurface, modules, storeClickMetrics }) => args => { const { proposition, viewName } = args; const { scope, scopeDetails: { characteristics: { scopeType - } - }, - items + } = {} + } = {}, + items = [] } = proposition.getHandle(); - if (scope === PAGE_WIDE_SCOPE || isPageWideSurface(scope) || + /*if (scope === PAGE_WIDE_SCOPE || isPageWideSurface(scope) || scopeType === VIEW_SCOPE_TYPE && scope === viewName) { + */ - if (items.some(({ schema }) => schema === DOM_ACTION || schema === DEFAULT_CONTENT_ITEM )) { - proposition.addRenderer(() => { - return executeDecisions([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. + proposition.addRenderer(index, () => { + storeClickMetrics({ selector, meta: proposition.getMeta() }); + }); + } else if (modules[type]) { + proposition.includeInDisplayNotification(); + proposition.addRenderer(index, () => { + return modules[type](data); + }); + } + } + if (schema === HTML_CONTENT_ITEM && type && selector && modules[type]) { + proposition.includeInDisplayNotification(); + proposition.addRenderer(index, () => { + return modules[type](data); + }); + } + }); + //} // this proposition may contain items that need to be rendered or cached by other handlers. next(args); }; diff --git a/src/components/Personalization/handlers/createMeasurementSchemaHandler.js b/src/components/Personalization/handlers/createMeasurementSchemaHandler.js index 83ae323c6..61462acfd 100644 --- a/src/components/Personalization/handlers/createMeasurementSchemaHandler.js +++ b/src/components/Personalization/handlers/createMeasurementSchemaHandler.js @@ -2,7 +2,7 @@ import { MEASUREMENT_SCHEMA } from "../constants/schema"; export default ({ next }) => args => { const { proposition } = args; - const { items } = proposition.getHandle(); + 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) diff --git a/src/components/Personalization/handlers/createProposition.js b/src/components/Personalization/handlers/createProposition.js index e1a8ae13c..15877c2f8 100644 --- a/src/components/Personalization/handlers/createProposition.js +++ b/src/components/Personalization/handlers/createProposition.js @@ -1,51 +1,55 @@ export default (handle) => { - const { id, scope, scopeDetails } = handle; + const { id, scope, scopeDetails, items = [] } = handle; let renderers = []; let redirectUrl; - let markedForCache = false; - let renderAttempted = false; + let includeInDisplayNotification = false; + let itemsRenderAttempted = new Array(items.length).map(() => false); return { getHandle() { return handle; }, + getMeta() { + return { id, scope, scopeDetails }; + }, redirect(url) { - renderAttempted = true; + includeInDisplayNotification = true; redirectUrl = url; }, getRedirectUrl() { return redirectUrl; }, - cache() { - markedForCache = true; - }, - addRenderer(renderer) { - renderAttempted = true; + addRenderer(itemIndex, renderer) { + itemsRenderAttempted[itemIndex] = true; renderers.push(renderer); }, + includeInDisplayNotification() { + includeInDisplayNotification = true; + }, render() { return Promise.all(renderers.map(renderer => renderer())); }, addToNotifications(notifications) { - if (renderAttempted) { + if (includeInDisplayNotification) { notifications.push({ id, scope, scopeDetails }); } }, - addToCache(cache) { - if (!markedForCache) { - return; - } - cache[scope] = cache[scope] || []; - cache[scope].push(handle); - }, addToReturnedPropositions(propositions) { - propositions.push({ ...handle, renderAttempted }); + const renderedItems = items.filter((item, index) => itemsRenderAttempted[index]); + if (renderedItems.length > 0) { + propositions.push({ ...handle, items: renderedItems, renderAttempted: true }); + } + const nonrenderedItems = items.filter((item, index) => !itemsRenderAttempted[index]); + if (nonrenderedItems.length > 0) { + propositions.push({ ...handle, items: nonrenderedItems, renderAttempted: false }); + } }, addToReturnedDecisions(decisions) { - if (!renderAttempted) { - decisions.push({ ...handle }); + const nonrenderedItems = items.filter((item, index) => !itemsRenderAttempted[index]); + if (nonrenderedItems.length > 0) { + decisions.push({ ...handle, items: nonrenderedItems }); } } }; diff --git a/src/components/Personalization/handlers/createPropositionHandler.js b/src/components/Personalization/handlers/createPropositionHandler.js new file mode 100644 index 000000000..8e1ab91bf --- /dev/null +++ b/src/components/Personalization/handlers/createPropositionHandler.js @@ -0,0 +1,103 @@ +/* +1. sendEvent where fetching from server +2. sendEvent where fetching from cache +3. applyPropositions where propositions passed in +4. applyPropositions where fetching view from cache + +Arguments: propositions, renderDecisions, sendNotifications, proposition source (server, cache, or passed in) +*/ + + +// applyPropositions (sendNotifications = false, renderDecision = true) +// ----------------- + + +// sendEvent SPA view change +// --------------------- +// redirectHandler +// measurementSchemaHandler +// domActionHandler +// no-op + + +// sendEvent with renderDecisions=true +// -------------------- +// redirectHandler +// measurementSchemaHandler +// domActionHandler +// cachingHandler +// no-op + +// sendEvent with renderDecisions=false +// --------------------- +// cachingHandler +// no-op + +import createProposition from "./createProposition"; + +/** + * Runs propositions through handlers and generates the return value for the + * sendEvent call + * + * @param {Object} options + * @param {Array} options.handles - the handles returned from experience edge of + * type "personalization.decisions" + * @param {Function} options.handler - the handler function to run on each + * handle + * @param {String} options.viewName - the name of the view + * @param {Function} options.resolveCache - If there is no redirect, this will + * be called once with the propositions that should be cached. This is resolved + * with an object with keys equal to the scope/viewName and values equal to an + * array of propositions. + * @param {Function} options.resolveDisplayNotification - If there is no + * redirect, this will be called once with the propositions to include in a + * display notification. + * @param {Function} options.resolveRedirectNotification - If there is a + * redirect, this will be called once with the propositions to include in a + * redirect notification. + * + * @returns {Object} - an object with keys "propositions" and "decisions". This + * is the return value for the sendEvent call, and is always returned + * synchronously. + */ +export default ({ window }) => ({ + handles, + handler, + viewName, + resolveDisplayNotification, + resolveRedirectNotification + }) => { + const propositions = handles.map(createProposition); + + for( let i = 0; i < propositions.length; i += 1) { + const proposition = propositions[i]; + handler({ proposition, viewName }); + 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 resolveRedirectNotification(displayNotificationPropositions).then(() => { + window.location.replace(redirectUrl); + }); // TODO add error log message + } + }; + + Promise.all(propositions.map(proposition => proposition.render())).then(() => { + const displayNotificationPropositions = []; + propositions.forEach(proposition => proposition.addToNotifications(displayNotificationPropositions)); + resolveDisplayNotification(displayNotificationPropositions); + }); // TODO add error log message? + + const returnedPropositions = []; + const returnedDecisions = []; + propositions.forEach(p => { + p.addToReturnedPropositions(returnedPropositions); + p.addToReturnedDecisions(returnedDecisions); + }); + return { + propositions: returnedPropositions, + decisions: returnedDecisions + }; +} diff --git a/src/components/Personalization/handlers/createRedirectHandler.js b/src/components/Personalization/handlers/createRedirectHandler.js index 5b7588c71..9ef3ff4a3 100644 --- a/src/components/Personalization/handlers/createRedirectHandler.js +++ b/src/components/Personalization/handlers/createRedirectHandler.js @@ -3,7 +3,7 @@ import { find } from "../../../utils"; export default ({ next }) => args => { const { proposition } = args; - const { items } = proposition.getHandle(); + const { items = [] } = proposition.getHandle() || {}; const redirectItem = find(items, ({ schema }) => schema === REDIRECT_ITEM); if (redirectItem) { diff --git a/src/components/Personalization/handlers/propositionHandler.js b/src/components/Personalization/handlers/propositionHandler.js deleted file mode 100644 index 831b08920..000000000 --- a/src/components/Personalization/handlers/propositionHandler.js +++ /dev/null @@ -1,52 +0,0 @@ -// renderDecisions=true -// -------------------- -// redirectHandler -// measurementSchemaHandler -// domActionHandler -// cachingHandler -// no-op - -// renderDecisions=false -// --------------------- -// cachingHandler -// no-op - -import createProposition from "./createProposition"; - - -export default ({ handles, handler, viewName, decisionsDeferred, sendDisplayNotification }) => { - const propositions = handles.map(createProposition); - - for( let i = 0; i < propositions.length; i += 1) { - const proposition = propositions[i]; - handler({ proposition, viewName }); - const redirectUrl = proposition.getRedirectUrl(); - if (redirectUrl) { - const notifications = []; - proposition.addToNotifications(notifications); - return sendDisplayNotification(notifications).then(() => { - window.location.replace(redirectUrl); - }); // TODO add error log message - } - }; - - Promise.all(propositions.map(proposition => proposition.render())).then(() => { - const notificationPropositions = []; - propositions.forEach(proposition => proposition.addToNotifications(notificationPropositions)); - sendDisplayNotification(notificationPropositions); - }); // TODO add error log message? - - const cachedPropositions = {}; - const returnedPropositions = []; - const returnedDecisions = []; - propositions.forEach(p => { - p.addToCache(cachedPropositions) - p.addToReturnedPropositions(returnedPropositions); - p.addToReturnedDecisions(returnedDecisions); - }); - decisionsDeferred.resolve(cachedPropositions); - return { - propositions: returnedPropositions, - decisions: returnedDecisions - }; -} diff --git a/src/components/Personalization/index.js b/src/components/Personalization/index.js index 61f3b7154..8d4261120 100644 --- a/src/components/Personalization/index.js +++ b/src/components/Personalization/index.js @@ -27,7 +27,7 @@ import createClickStorage from "./createClickStorage"; import createApplyPropositions from "./createApplyPropositions"; import createGetPageLocation from "./createGetPageLocation"; import createSetTargetMigration from "./createSetTargetMigration"; -import propositionHandler from "./handlers/propositionHandler"; +import propositionHandler from "./handlers/createPropositionHandler"; import createRedirectHandler from "./handlers/createRedirectHandler"; import createCachingHandler from "./handlers/createCachingHandler"; import createDomActionHandler from "./handlers/createDomActionHandler"; diff --git a/src/components/Personalization/utils/createAsyncArray.js b/src/components/Personalization/utils/createAsyncArray.js new file mode 100644 index 000000000..a594bd5b5 --- /dev/null +++ b/src/components/Personalization/utils/createAsyncArray.js @@ -0,0 +1,20 @@ + +export default () => { + let latest = Promise.resolve([]); + return { + add(promise) { + latest = latest.then(existingPropositions => { + return promise.then(newPropositions => { + return existingPropositions.concat(newPropositions); + }).catch(() => { + return existingPropositions; + }); + }); + }, + clear() { + const oldLatest = latest; + latest = Promise.resolve([]); + return oldLatest; + } + } +}; diff --git a/src/core/createEvent.js b/src/core/createEvent.js index 6bf98131e..8b59f0d7a 100644 --- a/src/core/createEvent.js +++ b/src/core/createEvent.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 { isEmptyObject, deepAssign } from "../utils"; +import { isEmptyObject, deepAssign, isNonEmptyArray, deduplicateArray } from "../utils"; export default () => { const content = {}; @@ -64,7 +64,25 @@ export default () => { } if (userXdm) { - event.mergeXdm(userXdm); + // Merge the userXDM propositions with the ones included via the display + // notification cache. + if (userXdm._experience && userXdm._experience.decisioning && + isNonEmptyArray(userXdm._experience.decisioning.propositions) && + content.xdm._experience && content.xdm._experience.decisioning && + isNonEmptyArray(content.xdm._experience.decisioning.propositions)) { + + const newPropositions = deduplicateArray( + [ + ...userXdm._experience.decisioning.propositions, + ...content.xdm._experience.decisioning.propositions + ], + (a, b) => a === b || a.id && b.id && a.id === b.id + ); + event.mergeXdm(userXdm); + content.xdm._experience.decisioning.propositions = newPropositions; + } else { + event.mergeXdm(userXdm); + } } if (userData) { diff --git a/src/utils/deduplicateArray.js b/src/utils/deduplicateArray.js new file mode 100644 index 000000000..468f24705 --- /dev/null +++ b/src/utils/deduplicateArray.js @@ -0,0 +1,14 @@ +const REFERENCE_EQUALITY = (a, b) => a === b; + +const findIndex = (array, item, isEqual) => { + for (let i = 0; i < array.length; i++) { + if (isEqual(array[i], item)) { + return i; + } + } + return -1; +}; + +export default (array, isEqual = REFERENCE_EQUALITY) => { + return array.filter((item, index) => findIndex(array, item, isEqual) === index); +} diff --git a/src/utils/index.js b/src/utils/index.js index 927e3a28b..c38851c2f 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -22,6 +22,7 @@ export { default as createLoggingCookieJar } from "./createLoggingCookieJar"; export { default as createTaskQueue } from "./createTaskQueue"; export { default as crc32 } from "./crc32"; export { default as defer } from "./defer"; +export { default as deduplicateArray } from "./deduplicateArray"; export { default as deepAssign } from "./deepAssign"; export { default as endsWith } from "./endsWith"; export { default as find } from "./find"; diff --git a/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js b/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js index 7621c9178..ef979cec9 100644 --- a/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js +++ b/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js @@ -2,7 +2,7 @@ import createRedirectHandler from "../../../../../../src/components/Personalizat import createProposition from "../../../../../../src/components/Personalization/handlers/createProposition"; -fdescribe("redirectHandler", () => { +describe("redirectHandler", () => { let next; let redirectHandler; 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..e6b920f47 --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/buildAlloy.js @@ -0,0 +1,166 @@ +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 createExecuteDecisions from "../../../../../../src/components/Personalization/createExecuteDecisions"; +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 createPropositionHandler from "../../../../../../src/components/Personalization/handlers/createPropositionHandler"; +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 { isPageWideSurface } from "../../../../../../src/components/Personalization/utils/surfaceUtils"; + +const createAction = renderFunc => ({ selector, prehidingSelector, content, meta }) => { + renderFunc(selector, content); + if (selector === "#error") { + return Promise.resolve({ meta, error: `Error while rendering ${content}` }); + } + return Promise.resolve({ meta }); +}; + +const createClick = store => ({ selector }, meta) => { + store({ selector, meta }); + return Promise.resolve(); +}; + +const noop = ({ meta }) => Promise.resolve({ meta }); + +const buildComponent = ({ + actions, + config, + logger, + eventManager, + getPageLocation, + window, + hideContainers, + showContainers, + executeActions +}) => { + + const initDomActionsModulesMocks = store => { + 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(); + const modules = initDomActionsModulesMocks(storeClickMetrics); + const executeDecisions = createExecuteDecisions({ + modules, + logger, + executeActions + }); + + + const noOpHandler = () => undefined; + const domActionHandler = createDomActionHandler({ next: noOpHandler, isPageWideSurface, modules, storeClickMetrics}); + const measurementSchemaHandler = createMeasurementSchemaHandler({ next: domActionHandler }); + const redirectHandler = createRedirectHandler({ next: measurementSchemaHandler }); + + const propositionHandler = createPropositionHandler({ window }); + const fetchDataHandler = createFetchDataHandler({ + prehidingStyle, + propositionHandler, + hideContainers, + mergeQuery, + renderHandler: redirectHandler, + nonRenderHandler: noOpHandler, + collect, + getView: viewCache.getView + }); + const onClickHandler = createOnClickHandler({ + mergeDecisionsMeta, + collectClicks, + getClickSelectors, + getClickMetasBySelector + }); + const viewChangeHandler = createViewChangeHandler({ + mergeDecisionsMeta, + collect, + renderHandler: redirectHandler, + nonRenderHandler: noOpHandler, + propositionHandler, + viewCache + }); + const applyPropositions = createApplyPropositions({ + propositionHandler, + renderHandler: redirectHandler + }); +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 }; + }, + async applyPropositions(args) { + return await 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..f78fa8935 --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/buildMocks.js @@ -0,0 +1,75 @@ +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 executeActions = (actions, modules) => { + return Promise.resolve(actions.map(action => { + const { type, meta } = action; + console.log("executeActions mock", JSON.stringify(action, null, 2)); + modules[type](action); + //console.log("executeActions mock", JSON.stringify(meta, null, 2)); + return { meta }; + })); + }; + + const config = { + targetMigrationEnabled: true, + prehidingStyle: "myprehidingstyle" + }; + const logger = jasmine.createSpyObj("logger", ["warn", "error"]); + 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, + executeActions + }; +} + 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..69217dc84 --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/cartViewDecisions.spec.js @@ -0,0 +1,265 @@ +import { + CART_VIEW_DECISIONS +} from "../responsesMock/eventResponses"; + +import buildMocks from "./buildMocks" +import buildAlloy from "./buildAlloy"; +import resetMocks from "./resetMocks"; + +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 + ); + + 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..59f88e7c6 --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/mergedMetricDecisions.spec.js @@ -0,0 +1,112 @@ +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..0b8f828ff --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/mixedPropositions.spec.js @@ -0,0 +1,279 @@ +import { + MIXED_PROPOSITIONS +} from "../responsesMock/eventResponses"; + +import buildMocks from "./buildMocks" +import buildAlloy from "./buildAlloy"; +import resetMocks from "./resetMocks"; + +describe("PersonalizationComponent", () => { + + fit("MIXED_PROPOSITIONS", async () => { + const mocks = buildMocks(MIXED_PROPOSITIONS); + const alloy = buildAlloy(mocks); + let { 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" + ] + } + } + }); + console.log("result1", JSON.stringify(result, null, 2)); + 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); + result = await alloy.applyPropositions({ + propositions: result.propositions, + metadata: { + home: { + selector: "#myhomeselector", + actionType: "appendHtml" + } + } + }); + console.log(JSON.stringify(result.propositions, null, 2)); + expect(result.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 + } + ]); + console.log(JSON.stringify(result.decisions, null, 2)); + expect(result.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..56ec696a9 --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/pageWideDecisionsWithDomActionSchemaItems.spec.js @@ -0,0 +1,149 @@ +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..0339b29e4 --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisions.spec.js @@ -0,0 +1,195 @@ +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..865ad5a64 --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisionsWithoutDomActionSchemaItems.spec.js @@ -0,0 +1,96 @@ +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..c4a56d8f2 --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/productsViewDecisions.spec.js @@ -0,0 +1,47 @@ +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..9efe3cc8e --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/redirectPageWideScopeDecision.spec.js @@ -0,0 +1,66 @@ +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, result } = 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" + ] + } + } + }); + expect(result).toEqual({ + "propositions": [] + }); + 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..3b85d409a --- /dev/null +++ b/test/unit/specs/components/Personalization/topLevel/scopesFoo1Foo2Decisions.spec.js @@ -0,0 +1,143 @@ +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(); + }); +}); From 63cdfb05e8bf4a27880ade4a6d3b4ab603bb60c9 Mon Sep 17 00:00:00 2001 From: Jon Snyder Date: Thu, 17 Aug 2023 14:52:22 -0600 Subject: [PATCH 04/20] Refactor handlePropositions to render --- .../createApplyPropositions.js | 22 +- .../Personalization/createComponent.js | 13 +- .../Personalization/createFetchDataHandler.js | 97 ++---- .../createPersonalizationDetails.js | 4 +- .../Personalization/createViewCacheManager.js | 87 +++-- .../createViewChangeHandler.js | 50 +-- .../Personalization/dom-actions/action.js | 5 +- .../dom-actions/initDomActionsModules.js | 9 +- .../handlers/createCachingHandler.js | 57 ---- .../handlers/createDomActionHandler.js | 75 ++--- .../handlers/createHtmlContentHandler.js | 81 +++++ .../createMeasurementSchemaHandler.js | 16 +- .../handlers/createProposition.js | 57 ---- .../handlers/createPropositionHandler.js | 103 ------ .../handlers/createRedirectHandler.js | 20 +- .../Personalization/handlers/createRender.js | 43 +++ .../Personalization/handlers/proposition.js | 125 +++++++ src/components/Personalization/index.js | 54 ++-- src/components/Personalization/utils/split.js | 29 ++ .../handlers/createRedirectHandler.spec.js | 60 ++-- .../Personalization/topLevel/buildAlloy.js | 102 +++--- .../Personalization/topLevel/buildMocks.js | 32 +- .../topLevel/cartViewDecisions.spec.js | 198 ++++++------ .../topLevel/mixedPropositions.spec.js | 306 +++++++++--------- .../redirectPageWideScopeDecision.spec.js | 51 ++- 25 files changed, 865 insertions(+), 831 deletions(-) delete mode 100644 src/components/Personalization/handlers/createCachingHandler.js create mode 100644 src/components/Personalization/handlers/createHtmlContentHandler.js delete mode 100644 src/components/Personalization/handlers/createProposition.js delete mode 100644 src/components/Personalization/handlers/createPropositionHandler.js create mode 100644 src/components/Personalization/handlers/createRender.js create mode 100644 src/components/Personalization/handlers/proposition.js create mode 100644 src/components/Personalization/utils/split.js diff --git a/src/components/Personalization/createApplyPropositions.js b/src/components/Personalization/createApplyPropositions.js index 1c8b63981..7cdd3f18a 100644 --- a/src/components/Personalization/createApplyPropositions.js +++ b/src/components/Personalization/createApplyPropositions.js @@ -10,15 +10,18 @@ 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]; -export default ({ propositionHandler, renderHandler }) => { +export default ({ render }) => { const filterItemsPredicate = item => SUPPORTED_SCHEMAS.indexOf(item.schema) > -1; @@ -75,17 +78,12 @@ export default ({ propositionHandler, renderHandler }) => { const propositionsToExecute = preparePropositions({ propositions, metadata - }); + }).map(proposition => createProposition(proposition, true)); - const result = await propositionHandler({ - handles: propositionsToExecute, - handler: renderHandler, - viewName: undefined, - resolveDisplayNotification: () => undefined, - resolveRedirectNotification: () => undefined - }); - delete result.decisions; - return result; + render(propositionsToExecute); + return { + propositions: buildReturnedPropositions(propositionsToExecute) + }; }; return ({ propositions, metadata = {} }) => { diff --git a/src/components/Personalization/createComponent.js b/src/components/Personalization/createComponent.js index f6c7380f3..73f7e6ae4 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(); - viewCache.storeViews(decisionsDeferred.promise); - onRequestFailure(() => decisionsDeferred.reject()); + const cacheUpdate = viewCache.createCacheUpdate( + personalizationDetails.getViewName() + ); + onRequestFailure(() => cacheUpdate.reject()); fetchDataHandler({ - decisionsDeferred, + cacheUpdate, personalizationDetails, event, onResponse diff --git a/src/components/Personalization/createFetchDataHandler.js b/src/components/Personalization/createFetchDataHandler.js index 70dd1fd84..4ff56b223 100644 --- a/src/components/Personalization/createFetchDataHandler.js +++ b/src/components/Personalization/createFetchDataHandler.js @@ -1,8 +1,5 @@ -import { VIEW_SCOPE_TYPE } from "./constants/scopeType"; -import isPageWideScope from "./utils/isPageWideScope"; - /* -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 @@ -12,19 +9,22 @@ 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 { + createProposition, + buildReturnedPropositions, + buildReturnedDecisions +} from "./handlers/proposition"; + const DECISIONS_HANDLE = "personalization:decisions"; export default ({ prehidingStyle, - propositionHandler, hideContainers, mergeQuery, - renderHandler, - nonRenderHandler, collect, - getView + render }) => { - return ({ decisionsDeferred, personalizationDetails, event, onResponse, displayNotificationsDeferred }) => { + return ({ cacheUpdate, personalizationDetails, event, onResponse }) => { if (personalizationDetails.isRenderDecisions()) { hideContainers(prehidingStyle); } @@ -32,72 +32,23 @@ export default ({ onResponse(async ({ response }) => { const handles = response.getPayloadsByType(DECISIONS_HANDLE); + const handlesToRender = cacheUpdate.update(handles); + const propositions = handlesToRender.map(createProposition); + if (personalizationDetails.isRenderDecisions()) { + render(propositions).then(decisionsMeta => { + if (decisionsMeta.length > 0) { + collect({ + decisionsMeta, + viewName: personalizationDetails.getViewName() + }); + } + }); + } - const viewTypeHandles = []; - const pageWideHandles = []; - const otherHandles = []; - handles.forEach(handle => { - const { - scope, - scopeDetails: { - characteristics: { - scopeType - } = {} - } = {} - } = handle; - if (isPageWideScope(scope)) { - pageWideHandles.push(handle); - } else if (scopeType === VIEW_SCOPE_TYPE) { - viewTypeHandles.push(handle); - } else { - otherHandles.push(handle); - } - }); - decisionsDeferred.resolve(viewTypeHandles); - const viewName = personalizationDetails.getViewName(); - const propositionsToHandle = [ - ...(await getView(viewName)), - ...pageWideHandles - ]; - - const handler = personalizationDetails.isRenderDecisions() ? renderHandler : nonRenderHandler; - - /* - const resolveDisplayNotification = decisionsMeta => { - if (!personalizationDetails.isSendDisplayNotifications()) { - return displayNotificationsDeferred.resolve({ decisionsMeta, viewName }); - } - if (decisionsMeta.length > 0) { - displayNotificationsDeferred.resolve({ decisionsMeta, viewName }); - return collect({ decisionsMeta, viewName }); - } - return Promise.resolve(); + return { + propositions: buildReturnedPropositions(propositions), + decisions: buildReturnedDecisions(propositions) }; - */ - const sendDisplayNotification = decisionsMeta => { - if (decisionsMeta.length > 0) { - return collect({ decisionsMeta, viewName }); - } else { - return Promise.resolve(); - } - }; - - const { propositions, decisions } = propositionHandler({ - handles: propositionsToHandle, - handler, - viewName, - resolveDisplayNotification: sendDisplayNotification, - resolveRedirectNotification: sendDisplayNotification - }); - - otherHandles.forEach(handle => { - propositions.push({ - renderAttempted: false, - ...handle - }); - decisions.push(handle); - }); - return { propositions, decisions }; }); }; }; diff --git a/src/components/Personalization/createPersonalizationDetails.js b/src/components/Personalization/createPersonalizationDetails.js index 8eaaa0c2c..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(); @@ -103,7 +103,7 @@ export default ({ }; }, isCacheInitialized() { - return viewCache.isInitialized(); + return isCacheInitialized; }, shouldFetchData() { return ( diff --git a/src/components/Personalization/createViewCacheManager.js b/src/components/Personalization/createViewCacheManager.js index 9d00eb5ab..58f5b8a4e 100644 --- a/src/components/Personalization/createViewCacheManager.js +++ b/src/components/Personalization/createViewCacheManager.js @@ -12,57 +12,82 @@ governing permissions and limitations under the License. import { assign } from "../../utils"; import defer from "../../utils/defer"; +import { VIEW_SCOPE_TYPE } from "./constants/scopeType"; const createEmptyViewPropositions = viewName => { - return [{ - scope: viewName, - scopeDetails: { - characteristics: { - scopeType: "view" + return [ + { + scope: viewName, + scopeDetails: { + characteristics: { + scopeType: "view" + } } } - }]; + ]; }; export default () => { - let viewStorage; - const viewStorageDeferred = defer(); + const viewStorage = {}; + let cacheUpdateCreatedAtLeastOnce = false; + let previousUpdateCacheComplete = Promise.resolve(); - const storeViews = viewTypeHandlesPromise => { - viewTypeHandlesPromise - .then(viewTypeHandles => { - if (viewStorage === undefined) { - viewStorage = {}; - } - const newViewStorage = viewTypeHandles.reduce((acc, handle) => { - const { scope } = handle; - acc[scope] = acc[scope] || []; - acc[scope].push(handle); - return acc; - }, {}); + // 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); - viewStorageDeferred.resolve(); }) - .catch(() => { - if (viewStorage === undefined) { - viewStorage = {}; + .catch(() => {}); + + return { + update(personalizationHandles) { + const newViewStorage = {}; + const otherHandles = []; + personalizationHandles.forEach(handle => { + const { + scope, + scopeDetails: { characteristics: { scopeType } = {} } = {} + } = handle; + if (scopeType === VIEW_SCOPE_TYPE) { + newViewStorage[scope] = newViewStorage[scope] || []; + newViewStorage[scope].push(handle); + } else { + otherHandles.push(handle); + } + }); + updateCacheDeferred.resolve(newViewStorage); + if (viewName) { + return [ + ...(newViewStorage[viewName] || + createEmptyViewPropositions(viewName)), + otherHandles + ]; } - viewStorageDeferred.resolve(); - }); + return otherHandles; + }, + error() { + updateCacheDeferred.reject(); + } + }; }; const getView = viewName => { - return viewStorageDeferred.promise.then(() => - viewStorage[viewName] || - createEmptyViewPropositions(viewName) + return previousUpdateCacheComplete.then( + () => viewStorage[viewName] || createEmptyViewPropositions(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 985bb6a66..c581030d7 100644 --- a/src/components/Personalization/createViewChangeHandler.js +++ b/src/components/Personalization/createViewChangeHandler.js @@ -10,47 +10,31 @@ 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, + createProposition +} from "./handlers/proposition"; -export default ({ - mergeDecisionsMeta, - collect, - renderHandler, - nonRenderHandler, - propositionHandler, - viewCache -}) => { +export default ({ mergeDecisionsMeta, render, viewCache }) => { return async ({ personalizationDetails, event, onResponse }) => { const viewName = personalizationDetails.getViewName(); - const viewDecisions = await viewCache.getView(viewName); - - const handler = personalizationDetails.isRenderDecisions() ? renderHandler : nonRenderHandler; - const sendDisplayNotification = decisionsMeta => { - mergeDecisionsMeta( - event, - decisionsMeta, - PropositionEventType.DISPLAY - ); - return new Promise(resolve => { - onResponse(resolve); - }); - }; - - const result = await propositionHandler({ - handles: viewDecisions, - handler, - viewName, - resolveDisplayNotification: sendDisplayNotification, - resolveRedirectNotification: sendDisplayNotification - }); + const viewHandles = await viewCache.getView(viewName); + const propositions = viewHandles.map(createProposition); + if (personalizationDetails.isRenderDecisions()) { + const decisionsMeta = await render(propositions); + mergeDecisionsMeta(event, decisionsMeta, PropositionEventType.DISPLAY); + } onResponse(() => { - return result; + 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 diff --git a/src/components/Personalization/dom-actions/action.js b/src/components/Personalization/dom-actions/action.js index 50cc3716d..8d693c5ad 100644 --- a/src/components/Personalization/dom-actions/action.js +++ b/src/components/Personalization/dom-actions/action.js @@ -35,7 +35,7 @@ const renderContent = (elements, content, renderFunc) => { export const createAction = renderFunc => { return settings => { - const { selector, prehidingSelector, content, meta } = settings; + const { selector, prehidingSelector, content } = settings; hideElements(prehidingSelector); @@ -45,13 +45,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/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/handlers/createCachingHandler.js b/src/components/Personalization/handlers/createCachingHandler.js deleted file mode 100644 index 96f800488..000000000 --- a/src/components/Personalization/handlers/createCachingHandler.js +++ /dev/null @@ -1,57 +0,0 @@ -import { VIEW_SCOPE_TYPE } from "../constants/scopeType"; - -export default ({ next }) => args => { - const { proposition } = args; - const { - scopeDetails: { - characteristics: { - scopeType - } = {} - } = {} - } = proposition.getHandle(); - - if (scopeType === VIEW_SCOPE_TYPE) { - proposition.cache(); - } - - // this proposition may contain items that need to be rendered or cached by other handlers. - next(args); -}; - -/* -import { assign } from "../../utils"; - -export const createViewCacheManager = () => { - const viewStorage = {}; - let storeViewsCalledAtLeastOnce = false; - let previousStoreViewsComplete = Promise.resolve(); - - const storeViews = viewTypeHandlesPromise => { - storeViewsCalledAtLeastOnce = true; - previousStoreViewsComplete = previousStoreViewsComplete - .then(() => viewTypeHandlesPromise) - .then(viewTypeHandles => { - const decisions = viewTypeHandles.reduce((handle, memo) => { - const { scope } = handle; - memo[scope] = memo[scope] || []; - memo[scope].push(handle); - }, {}); - assign(viewStorage, decisions); - }) - .catch(() => {}); - }; - - const getView = viewName => { - return previousStoreViewsComplete.then(() => viewStorage[viewName] || []); - }; - - const isInitialized = () => { - return storeViewsCalledAtLeastOnce; - }; - return { - storeViews, - getView, - isInitialized - }; -}; -*/ diff --git a/src/components/Personalization/handlers/createDomActionHandler.js b/src/components/Personalization/handlers/createDomActionHandler.js index 42f4f4614..dd8a0436a 100644 --- a/src/components/Personalization/handlers/createDomActionHandler.js +++ b/src/components/Personalization/handlers/createDomActionHandler.js @@ -1,52 +1,41 @@ -import { DEFAULT_CONTENT_ITEM, DOM_ACTION, HTML_CONTENT_ITEM } from "../constants/schema"; -import PAGE_WIDE_SCOPE from "../../../constants/pageWideScope"; -import { VIEW_SCOPE_TYPE } from "../constants/scopeType" +/* +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 -export default ({ next, isPageWideSurface, modules, storeClickMetrics }) => args => { - const { proposition, viewName } = args; - const { - scope, - scopeDetails: { - characteristics: { - scopeType - } = {} - } = {}, - items = [] - } = proposition.getHandle(); +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"; - /*if (scope === PAGE_WIDE_SCOPE || isPageWideSurface(scope) || - scopeType === VIEW_SCOPE_TYPE && scope === viewName) { - */ +export default ({ next, modules, storeClickMetrics }) => 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. - proposition.addRenderer(index, () => { - storeClickMetrics({ selector, meta: proposition.getMeta() }); - }); - } else if (modules[type]) { - proposition.includeInDisplayNotification(); - proposition.addRenderer(index, () => { - return modules[type](data); - }); - } - } - if (schema === HTML_CONTENT_ITEM && type && selector && modules[type]) { + 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. + proposition.addRenderer(index, () => { + storeClickMetrics({ selector, meta: proposition.getMeta() }); + }); + } else if (modules[type]) { proposition.includeInDisplayNotification(); proposition.addRenderer(index, () => { return modules[type](data); }); } - }); - //} - // this proposition may contain items that need to be rendered or cached by other handlers. - next(args); + } + }); + + next(proposition); }; diff --git a/src/components/Personalization/handlers/createHtmlContentHandler.js b/src/components/Personalization/handlers/createHtmlContentHandler.js new file mode 100644 index 000000000..9f09f475e --- /dev/null +++ b/src/components/Personalization/handlers/createHtmlContentHandler.js @@ -0,0 +1,81 @@ +/* +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 }) => 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(); + proposition.addRenderer(index, () => { + return modules[type](data); + }); + } + }); + + // 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); + } +}; + +/* +import { assign } from "../../utils"; + +export const createViewCacheManager = () => { + const viewStorage = {}; + let storeViewsCalledAtLeastOnce = false; + let previousStoreViewsComplete = Promise.resolve(); + + const storeViews = viewTypeHandlesPromise => { + storeViewsCalledAtLeastOnce = true; + previousStoreViewsComplete = previousStoreViewsComplete + .then(() => viewTypeHandlesPromise) + .then(viewTypeHandles => { + const decisions = viewTypeHandles.reduce((handle, memo) => { + const { scope } = handle; + memo[scope] = memo[scope] || []; + memo[scope].push(handle); + }, {}); + assign(viewStorage, decisions); + }) + .catch(() => {}); + }; + + const getView = viewName => { + return previousStoreViewsComplete.then(() => viewStorage[viewName] || []); + }; + + const isInitialized = () => { + return storeViewsCalledAtLeastOnce; + }; + return { + storeViews, + getView, + isInitialized + }; +}; +*/ diff --git a/src/components/Personalization/handlers/createMeasurementSchemaHandler.js b/src/components/Personalization/handlers/createMeasurementSchemaHandler.js index 61462acfd..b6ec90680 100644 --- a/src/components/Personalization/handlers/createMeasurementSchemaHandler.js +++ b/src/components/Personalization/handlers/createMeasurementSchemaHandler.js @@ -1,12 +1,22 @@ +/* +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 { MEASUREMENT_SCHEMA } from "../constants/schema"; -export default ({ next }) => args => { - const { proposition } = args; +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(args); + next(proposition); } }; diff --git a/src/components/Personalization/handlers/createProposition.js b/src/components/Personalization/handlers/createProposition.js deleted file mode 100644 index 15877c2f8..000000000 --- a/src/components/Personalization/handlers/createProposition.js +++ /dev/null @@ -1,57 +0,0 @@ -export default (handle) => { - - const { id, scope, scopeDetails, items = [] } = handle; - - let renderers = []; - let redirectUrl; - let includeInDisplayNotification = false; - let itemsRenderAttempted = new Array(items.length).map(() => false); - - return { - getHandle() { - return handle; - }, - getMeta() { - return { id, scope, scopeDetails }; - }, - redirect(url) { - includeInDisplayNotification = true; - redirectUrl = url; - }, - getRedirectUrl() { - return redirectUrl; - }, - addRenderer(itemIndex, renderer) { - itemsRenderAttempted[itemIndex] = true; - renderers.push(renderer); - }, - includeInDisplayNotification() { - includeInDisplayNotification = true; - }, - render() { - return Promise.all(renderers.map(renderer => renderer())); - }, - addToNotifications(notifications) { - if (includeInDisplayNotification) { - notifications.push({ id, scope, scopeDetails }); - } - }, - addToReturnedPropositions(propositions) { - const renderedItems = items.filter((item, index) => itemsRenderAttempted[index]); - if (renderedItems.length > 0) { - propositions.push({ ...handle, items: renderedItems, renderAttempted: true }); - } - const nonrenderedItems = items.filter((item, index) => !itemsRenderAttempted[index]); - if (nonrenderedItems.length > 0) { - propositions.push({ ...handle, items: nonrenderedItems, renderAttempted: false }); - } - }, - addToReturnedDecisions(decisions) { - const nonrenderedItems = items.filter((item, index) => !itemsRenderAttempted[index]); - if (nonrenderedItems.length > 0) { - decisions.push({ ...handle, items: nonrenderedItems }); - } - } - }; - -}; diff --git a/src/components/Personalization/handlers/createPropositionHandler.js b/src/components/Personalization/handlers/createPropositionHandler.js deleted file mode 100644 index 8e1ab91bf..000000000 --- a/src/components/Personalization/handlers/createPropositionHandler.js +++ /dev/null @@ -1,103 +0,0 @@ -/* -1. sendEvent where fetching from server -2. sendEvent where fetching from cache -3. applyPropositions where propositions passed in -4. applyPropositions where fetching view from cache - -Arguments: propositions, renderDecisions, sendNotifications, proposition source (server, cache, or passed in) -*/ - - -// applyPropositions (sendNotifications = false, renderDecision = true) -// ----------------- - - -// sendEvent SPA view change -// --------------------- -// redirectHandler -// measurementSchemaHandler -// domActionHandler -// no-op - - -// sendEvent with renderDecisions=true -// -------------------- -// redirectHandler -// measurementSchemaHandler -// domActionHandler -// cachingHandler -// no-op - -// sendEvent with renderDecisions=false -// --------------------- -// cachingHandler -// no-op - -import createProposition from "./createProposition"; - -/** - * Runs propositions through handlers and generates the return value for the - * sendEvent call - * - * @param {Object} options - * @param {Array} options.handles - the handles returned from experience edge of - * type "personalization.decisions" - * @param {Function} options.handler - the handler function to run on each - * handle - * @param {String} options.viewName - the name of the view - * @param {Function} options.resolveCache - If there is no redirect, this will - * be called once with the propositions that should be cached. This is resolved - * with an object with keys equal to the scope/viewName and values equal to an - * array of propositions. - * @param {Function} options.resolveDisplayNotification - If there is no - * redirect, this will be called once with the propositions to include in a - * display notification. - * @param {Function} options.resolveRedirectNotification - If there is a - * redirect, this will be called once with the propositions to include in a - * redirect notification. - * - * @returns {Object} - an object with keys "propositions" and "decisions". This - * is the return value for the sendEvent call, and is always returned - * synchronously. - */ -export default ({ window }) => ({ - handles, - handler, - viewName, - resolveDisplayNotification, - resolveRedirectNotification - }) => { - const propositions = handles.map(createProposition); - - for( let i = 0; i < propositions.length; i += 1) { - const proposition = propositions[i]; - handler({ proposition, viewName }); - 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 resolveRedirectNotification(displayNotificationPropositions).then(() => { - window.location.replace(redirectUrl); - }); // TODO add error log message - } - }; - - Promise.all(propositions.map(proposition => proposition.render())).then(() => { - const displayNotificationPropositions = []; - propositions.forEach(proposition => proposition.addToNotifications(displayNotificationPropositions)); - resolveDisplayNotification(displayNotificationPropositions); - }); // TODO add error log message? - - const returnedPropositions = []; - const returnedDecisions = []; - propositions.forEach(p => { - p.addToReturnedPropositions(returnedPropositions); - p.addToReturnedDecisions(returnedDecisions); - }); - return { - propositions: returnedPropositions, - decisions: returnedDecisions - }; -} diff --git a/src/components/Personalization/handlers/createRedirectHandler.js b/src/components/Personalization/handlers/createRedirectHandler.js index 9ef3ff4a3..eed92e493 100644 --- a/src/components/Personalization/handlers/createRedirectHandler.js +++ b/src/components/Personalization/handlers/createRedirectHandler.js @@ -1,16 +1,28 @@ +/* +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 { REDIRECT_ITEM } from "../constants/schema"; import { find } from "../../../utils"; -export default ({ next }) => args => { - const { proposition } = args; +export default ({ next }) => proposition => { const { items = [] } = proposition.getHandle() || {}; const redirectItem = find(items, ({ schema }) => schema === REDIRECT_ITEM); if (redirectItem) { - const { data: { content } } = redirectItem; + const { + data: { content } + } = redirectItem; proposition.redirect(content); // On a redirect, nothing else needs to handle this. } else { - next(args); + next(proposition); } }; diff --git a/src/components/Personalization/handlers/createRender.js b/src/components/Personalization/handlers/createRender.js new file mode 100644 index 000000000..9c2942d25 --- /dev/null +++ b/src/components/Personalization/handlers/createRender.js @@ -0,0 +1,43 @@ +/* +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 +}) => 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 []; + } + ); + } + } + + 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..95331e8a6 --- /dev/null +++ b/src/components/Personalization/handlers/proposition.js @@ -0,0 +1,125 @@ +/* +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 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).map(() => false); + + return { + getHandle() { + return handle; + }, + getMeta() { + return { id, scope, scopeDetails }; + }, + redirect(url) { + includeInDisplayNotification = true; + redirectUrl = url; + }, + getRedirectUrl() { + return redirectUrl; + }, + addRenderer(itemIndex, renderer) { + itemsRenderAttempted[itemIndex] = true; + renderers.push(renderer); + }, + includeInDisplayNotification() { + includeInDisplayNotification = true; + }, + excludeInReturnedPropositions() { + includeInReturnedPropositions = false; + }, + render(logger) { + return Promise.all( + renderers.map(renderer => { + try { + renderer(); + return true; + } catch (e) { + logger.error(e); + return false; + } + }) + ).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( + (item, index) => itemsRenderAttempted[index] + ); + if (renderedItems.length > 0) { + propositions.push({ + ...handle, + items: renderedItems, + renderAttempted: true + }); + } + const nonrenderedItems = items.filter( + (item, 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 8d4261120..220a618c1 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"; @@ -27,11 +26,11 @@ import createClickStorage from "./createClickStorage"; import createApplyPropositions from "./createApplyPropositions"; import createGetPageLocation from "./createGetPageLocation"; import createSetTargetMigration from "./createSetTargetMigration"; -import propositionHandler from "./handlers/createPropositionHandler"; import createRedirectHandler from "./handlers/createRedirectHandler"; -import createCachingHandler from "./handlers/createCachingHandler"; +import createHtmlContentHandler from "./handlers/createHtmlContentHandler"; import createDomActionHandler from "./handlers/createDomActionHandler"; import createMeasurementSchemaHandler from "./handlers/createMeasurementSchemaHandler"; +import createRender from "./handlers/createRender"; import { isPageWideSurface } from "./utils/surfaceUtils"; const createPersonalization = ({ config, logger, eventManager }) => { @@ -45,31 +44,38 @@ const createPersonalization = ({ config, logger, eventManager }) => { } = createClickStorage(); const getPageLocation = createGetPageLocation({ window }); const viewCache = createViewCacheManager(); - const modules = initDomActionsModules(storeClickMetrics); - const executeDecisions = createExecuteDecisions({ + const modules = initDomActionsModules(); + + const noOpHandler = () => undefined; + const domActionHandler = createDomActionHandler({ + next: noOpHandler, + isPageWideSurface, modules, - logger, - executeActions + storeClickMetrics }); - - const applyPropositions = createApplyPropositions({ - executeDecisions + const measurementSchemaHandler = createMeasurementSchemaHandler({ + next: domActionHandler + }); + const redirectHandler = createRedirectHandler({ + next: measurementSchemaHandler + }); + const htmlContentHandler = createHtmlContentHandler({ + next: redirectHandler, + modules }); - const noOpHandler = () => undefined; - const cachingHandler = createCachingHandler({ next: noOpHandler }); - const domActionHandler = createDomActionHandler({ next: cachingHandler, executeDecisions, isPageWideSurface }); - const measurementSchemaHandler = createMeasurementSchemaHandler({ next: domActionHandler }); - const redirectHandler = createRedirectHandler({ next: measurementSchemaHandler }); - + const render = createRender({ + handleChain: htmlContentHandler, + collect, + executeRedirect: url => window.location.replace(url), + logger + }); const fetchDataHandler = createFetchDataHandler({ prehidingStyle, - propositionHandler, hideContainers, mergeQuery, - renderHandler: redirectHandler, - nonRenderHandler: cachingHandler, - collect + collect, + render }); const onClickHandler = createOnClickHandler({ mergeDecisionsMeta, @@ -79,10 +85,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/src/components/Personalization/utils/split.js b/src/components/Personalization/utils/split.js new file mode 100644 index 000000000..2f58225ae --- /dev/null +++ b/src/components/Personalization/utils/split.js @@ -0,0 +1,29 @@ +/* +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 (array, ...predicates) => { + const results = predicates.map(() => []); + results.push([]); + + array.forEach(item => { + for (let i = 0; i < predicates.length; i += 1) { + if (predicates[i](item)) { + results[i].push(item); + return; + } + } + // else + results[predicates.length].push(item); + }); + + return results; +}; diff --git a/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js b/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js index ef979cec9..21e0ba1ad 100644 --- a/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js +++ b/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js @@ -1,9 +1,7 @@ import createRedirectHandler from "../../../../../../src/components/Personalization/handlers/createRedirectHandler"; -import createProposition from "../../../../../../src/components/Personalization/handlers/createProposition"; - +import { createProposition } from "../../../../../../src/components/Personalization/handlers/proposition"; describe("redirectHandler", () => { - let next; let redirectHandler; @@ -14,41 +12,43 @@ describe("redirectHandler", () => { it("works with real response", () => { const handle = { - "id": "AT:eyJhY3Rpdml0eUlkIjoiMTI3ODE5IiwiZXhwZXJpZW5jZUlkIjoiMCJ9", - "scope": "__view__", - "scopeDetails": { - "decisionProvider": "TGT", - "activity": { - "id": "127819" + id: "AT:eyJhY3Rpdml0eUlkIjoiMTI3ODE5IiwiZXhwZXJpZW5jZUlkIjoiMCJ9", + scope: "__view__", + scopeDetails: { + decisionProvider: "TGT", + activity: { + id: "127819" }, - "experience": { - "id": "0" + experience: { + id: "0" }, - "strategies": [ + strategies: [ { - "algorithmID": "0", - "trafficType": "0" + algorithmID: "0", + trafficType: "0" } ], - "characteristics": { - "eventToken": "8CwxglIqrTLmqP2m1r52VWqipfsIHvVzTQxHolz2IpTMromRrB5ztP5VMxjHbs7c6qPG9UF4rvQTJZniWgqbOw==" + characteristics: { + eventToken: + "8CwxglIqrTLmqP2m1r52VWqipfsIHvVzTQxHolz2IpTMromRrB5ztP5VMxjHbs7c6qPG9UF4rvQTJZniWgqbOw==" } }, - "items": [ + items: [ { - "id": "0", - "schema": "https://ns.adobe.com/personalization/redirect-item", - "meta": { + 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" + data: { + type: "redirect", + format: "text/uri-list", + content: + "https://alloyio.com/functional-test/alloyTestPage.html?redirectedTest=true&test=C205528" } } ] @@ -56,17 +56,23 @@ describe("redirectHandler", () => { const proposition = createProposition(handle); redirectHandler({ proposition, viewName: "myview" }); expect(next).not.toHaveBeenCalled(); - expect(proposition.getRedirectUrl()).toEqual("https://alloyio.com/functional-test/alloyTestPage.html?redirectedTest=true&test=C205528"); + 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"); + expect(propositions[0].id).toEqual( + "AT:eyJhY3Rpdml0eUlkIjoiMTI3ODE5IiwiZXhwZXJpZW5jZUlkIjoiMCJ9" + ); const notifications = []; proposition.addToNotifications(notifications); expect(notifications.length).toEqual(1); - expect(notifications[0].id).toEqual("AT:eyJhY3Rpdml0eUlkIjoiMTI3ODE5IiwiZXhwZXJpZW5jZUlkIjoiMCJ9"); + expect(notifications[0].id).toEqual( + "AT:eyJhY3Rpdml0eUlkIjoiMTI3ODE5IiwiZXhwZXJpZW5jZUlkIjoiMCJ9" + ); }); }); diff --git a/test/unit/specs/components/Personalization/topLevel/buildAlloy.js b/test/unit/specs/components/Personalization/topLevel/buildAlloy.js index e6b920f47..970381fa8 100644 --- a/test/unit/specs/components/Personalization/topLevel/buildAlloy.js +++ b/test/unit/specs/components/Personalization/topLevel/buildAlloy.js @@ -1,12 +1,25 @@ +/* +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 createExecuteDecisions from "../../../../../../src/components/Personalization/createExecuteDecisions"; 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 { + 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"; @@ -14,27 +27,21 @@ import createClickStorage from "../../../../../../src/components/Personalization import createApplyPropositions from "../../../../../../src/components/Personalization/createApplyPropositions"; import createSetTargetMigration from "../../../../../../src/components/Personalization/createSetTargetMigration"; import { createCallbackAggregator, assign } from "../../../../../../src/utils"; -import createPropositionHandler from "../../../../../../src/components/Personalization/handlers/createPropositionHandler"; +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"; -const createAction = renderFunc => ({ selector, prehidingSelector, content, meta }) => { +const createAction = renderFunc => ({ selector, content }) => { renderFunc(selector, content); if (selector === "#error") { - return Promise.resolve({ meta, error: `Error while rendering ${content}` }); + return Promise.reject(new Error(`Error while rendering ${content}`)); } - return Promise.resolve({ meta }); -}; - -const createClick = store => ({ selector }, meta) => { - store({ selector, meta }); return Promise.resolve(); }; -const noop = ({ meta }) => Promise.resolve({ meta }); - const buildComponent = ({ actions, config, @@ -43,11 +50,9 @@ const buildComponent = ({ getPageLocation, window, hideContainers, - showContainers, - executeActions + showContainers }) => { - - const initDomActionsModulesMocks = store => { + const initDomActionsModulesMocks = () => { return { setHtml: createAction(actions.setHtml), customCode: createAction(actions.prependHtml), @@ -77,29 +82,38 @@ const buildComponent = ({ } = createClickStorage(); const viewCache = createViewCacheManager(); - const modules = initDomActionsModulesMocks(storeClickMetrics); - const executeDecisions = createExecuteDecisions({ - modules, - logger, - executeActions - }); - + const modules = initDomActionsModulesMocks(); const noOpHandler = () => undefined; - const domActionHandler = createDomActionHandler({ next: noOpHandler, isPageWideSurface, modules, storeClickMetrics}); - const measurementSchemaHandler = createMeasurementSchemaHandler({ next: domActionHandler }); - const redirectHandler = createRedirectHandler({ next: measurementSchemaHandler }); + const domActionHandler = createDomActionHandler({ + next: noOpHandler, + isPageWideSurface, + modules, + storeClickMetrics + }); + const measurementSchemaHandler = createMeasurementSchemaHandler({ + next: domActionHandler + }); + const redirectHandler = createRedirectHandler({ + next: measurementSchemaHandler + }); + const fetchHandler = createHtmlContentHandler({ + next: redirectHandler, + modules + }); - const propositionHandler = createPropositionHandler({ window }); + const render = createRender({ + handleChain: fetchHandler, + collect, + executeRedirect: url => window.location.replace(url), + logger + }); const fetchDataHandler = createFetchDataHandler({ prehidingStyle, - propositionHandler, hideContainers, mergeQuery, - renderHandler: redirectHandler, - nonRenderHandler: noOpHandler, collect, - getView: viewCache.getView + render }); const onClickHandler = createOnClickHandler({ mergeDecisionsMeta, @@ -109,17 +123,13 @@ const buildComponent = ({ }); const viewChangeHandler = createViewChangeHandler({ mergeDecisionsMeta, - collect, - renderHandler: redirectHandler, - nonRenderHandler: noOpHandler, - propositionHandler, + render, viewCache }); const applyPropositions = createApplyPropositions({ - propositionHandler, - renderHandler: redirectHandler + render }); -const setTargetMigration = createSetTargetMigration({ + const setTargetMigration = createSetTargetMigration({ targetMigrationEnabled }); return createComponent({ @@ -137,11 +147,17 @@ const setTargetMigration = createSetTargetMigration({ }); }; -export default (mocks) => { +export default mocks => { const component = buildComponent(mocks); const { response } = mocks; return { - async sendEvent({ xdm, data, renderDecisions, decisionScopes, personalization }) { + async sendEvent({ + xdm, + data, + renderDecisions, + decisionScopes, + personalization + }) { const event = createEvent(); event.setUserXdm(xdm); event.setUserData(data); @@ -159,8 +175,8 @@ export default (mocks) => { event.finalize(); return { event, result }; }, - async applyPropositions(args) { - return await component.commands.applyPropositions.run(args); + 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 index f78fa8935..be2023ca3 100644 --- a/test/unit/specs/components/Personalization/topLevel/buildMocks.js +++ b/test/unit/specs/components/Personalization/topLevel/buildMocks.js @@ -1,3 +1,14 @@ +/* +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"; @@ -27,21 +38,14 @@ export default decisions => { "insertHtmlBefore" ]); - const executeActions = (actions, modules) => { - return Promise.resolve(actions.map(action => { - const { type, meta } = action; - console.log("executeActions mock", JSON.stringify(action, null, 2)); - modules[type](action); - //console.log("executeActions mock", JSON.stringify(meta, null, 2)); - return { meta }; - })); - }; - const config = { targetMigrationEnabled: true, prehidingStyle: "myprehidingstyle" }; - const logger = jasmine.createSpyObj("logger", ["warn", "error"]); + const logger = { + warn: spyOn(console, "warn").and.callThrough(), + error: spyOn(console, "error").and.callThrough() + }; const sendEvent = jasmine.createSpy("sendEvent"); const eventManager = { createEvent, @@ -68,8 +72,6 @@ export default decisions => { window, hideContainers, showContainers, - response, - executeActions + response }; -} - +}; diff --git a/test/unit/specs/components/Personalization/topLevel/cartViewDecisions.spec.js b/test/unit/specs/components/Personalization/topLevel/cartViewDecisions.spec.js index 69217dc84..6aa3faca5 100644 --- a/test/unit/specs/components/Personalization/topLevel/cartViewDecisions.spec.js +++ b/test/unit/specs/components/Personalization/topLevel/cartViewDecisions.spec.js @@ -1,13 +1,11 @@ -import { - CART_VIEW_DECISIONS -} from "../responsesMock/eventResponses"; +import { CART_VIEW_DECISIONS } from "../responsesMock/eventResponses"; -import buildMocks from "./buildMocks" +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); @@ -18,27 +16,23 @@ describe("PersonalizationComponent", () => { CART_VIEW_DECISIONS ); expect(event.toJSON()).toEqual({ - "query": { - "personalization": { - "schemas": [ + 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" - ] + decisionScopes: ["__view__"], + surfaces: ["web://example.com/home"] } } }); expect(result).toEqual({ - "propositions": [], - "decisions": [] + propositions: [], + decisions: [] }); expect(mocks.sendEvent).not.toHaveBeenCalled(); @@ -60,66 +54,66 @@ describe("PersonalizationComponent", () => { [] )); - expect(event.toJSON()).toEqual({ - "xdm": { - "_experience": { - "decisioning": { - "propositions": [ + xdm: { + _experience: { + decisioning: { + propositions: [ { - "id": "TNT:activity4:experience9", - "scope": "cart", - "scopeDetails": { - "blah": "test", - "characteristics": { - "scopeType": "view" + id: "TNT:activity4:experience9", + scope: "cart", + scopeDetails: { + blah: "test", + characteristics: { + scopeType: "view" } } } ], - "propositionEventType": { - "display": 1 + propositionEventType: { + display: 1 } } }, - "web": { - "webPageDetails": { - "viewName": "cart" + web: { + webPageDetails: { + viewName: "cart" } } } }); expect(result).toEqual({ - "propositions": [ + propositions: [ { - "renderAttempted": true, - "id": "TNT:activity4:experience9", - "scope": "cart", - "items": [ + 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: "#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/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" + schema: + "https://ns.adobe.com/personalization/default-content-item" } ], - "scopeDetails": { - "blah": "test", - "characteristics": { - "scopeType": "view" + scopeDetails: { + blah: "test", + characteristics: { + scopeType: "view" } } } @@ -157,94 +151,93 @@ describe("PersonalizationComponent", () => { CART_VIEW_DECISIONS ); + await flushPromiseChains(); + expect(event.toJSON()).toEqual({ - "query": { - "personalization": { - "schemas": [ + 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" - ] + decisionScopes: ["__view__"], + surfaces: ["web://example.com/home"] } }, - "xdm": { - "web": { - "webPageDetails": { - "viewName": "cart" + xdm: { + web: { + webPageDetails: { + viewName: "cart" } } } }); expect(result).toEqual({ - "propositions": [ + propositions: [ { - "renderAttempted": true, - "id": "TNT:activity4:experience9", - "scope": "cart", - "items": [ + 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: "#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/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" + schema: + "https://ns.adobe.com/personalization/default-content-item" } ], - "scopeDetails": { - "blah": "test", - "characteristics": { - "scopeType": "view" + scopeDetails: { + blah: "test", + characteristics: { + scopeType: "view" } } } ], - "decisions": [] + decisions: [] }); expect(mocks.sendEvent).toHaveBeenCalledWith({ - "xdm": { - "_experience": { - "decisioning": { - "propositions": [ + xdm: { + _experience: { + decisioning: { + propositions: [ { - "id": "TNT:activity4:experience9", - "scope": "cart", - "scopeDetails": { - "blah": "test", - "characteristics": { - "scopeType": "view" + id: "TNT:activity4:experience9", + scope: "cart", + scopeDetails: { + blah: "test", + characteristics: { + scopeType: "view" } } } ], - "propositionEventType": { - "display": 1 + propositionEventType: { + display: 1 } } }, - "eventType": "decisioning.propositionDisplay", - "web": { - "webPageDetails": { - "viewName": "cart" + eventType: "decisioning.propositionDisplay", + web: { + webPageDetails: { + viewName: "cart" } } } @@ -260,6 +253,5 @@ describe("PersonalizationComponent", () => { 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/mixedPropositions.spec.js b/test/unit/specs/components/Personalization/topLevel/mixedPropositions.spec.js index 0b8f828ff..96efe02cc 100644 --- a/test/unit/specs/components/Personalization/topLevel/mixedPropositions.spec.js +++ b/test/unit/specs/components/Personalization/topLevel/mixedPropositions.spec.js @@ -1,223 +1,217 @@ -import { - MIXED_PROPOSITIONS -} from "../responsesMock/eventResponses"; +import { MIXED_PROPOSITIONS } from "../responsesMock/eventResponses"; -import buildMocks from "./buildMocks" +import buildMocks from "./buildMocks"; import buildAlloy from "./buildAlloy"; import resetMocks from "./resetMocks"; describe("PersonalizationComponent", () => { - - fit("MIXED_PROPOSITIONS", async () => { + it("MIXED_PROPOSITIONS", async () => { const mocks = buildMocks(MIXED_PROPOSITIONS); const alloy = buildAlloy(mocks); - let { event, result } = await alloy.sendEvent( + const { event, result } = await alloy.sendEvent( { renderDecisions: true }, MIXED_PROPOSITIONS ); - expect(event.toJSON()).toEqual({ - "query": { - "personalization": { - "schemas": [ + 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" - ] + decisionScopes: ["__view__"], + surfaces: ["web://example.com/home"] } } }); - console.log("result1", JSON.stringify(result, null, 2)); - expect(result.propositions).toEqual(jasmine.arrayWithExactContents([ + expect(result.propositions).toEqual( + jasmine.arrayWithExactContents([ { - "renderAttempted": true, - "id": "AT:eyJhY3Rpdml0eUlkIjoiNDQyMzU4IiwiZXhwZXJpZW5jZUlkIjoiIn1=", - "scope": "__view__", - "items": [ + 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" + 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": [ + 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" + 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": [ + 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" + 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": [ + 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" + 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": [ + 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" + 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": [ + 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" + 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([ + ]) + ); + expect(result.decisions).toEqual( + jasmine.arrayWithExactContents([ { - "id": "AT:eyJhY3Rpdml0eUlkIjoiNDQyMzU4IiwiZXhwZXJpZW5jZUlkIjoiIn1=", - "scope": "home", - "items": [ + 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: "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: "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: "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: "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: "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: "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" + 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); - result = await alloy.applyPropositions({ + const applyPropositionsResult = await alloy.applyPropositions({ propositions: result.propositions, metadata: { home: { @@ -226,47 +220,45 @@ describe("PersonalizationComponent", () => { } } }); - console.log(JSON.stringify(result.propositions, null, 2)); - expect(result.propositions).toEqual([ + expect(applyPropositionsResult.propositions).toEqual([ { - "id": "AT:eyJhY3Rpdml0eUlkIjoiNDQyMzU4IiwiZXhwZXJpZW5jZUlkIjoiIn0=", - "scope": "home", - "items": [ + 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" + id: "442358", + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + type: "click", + format: "application/vnd.adobe.target.dom-action", + selector: "#root" } } ], - "renderAttempted": true, + renderAttempted: true, scopeDetails: undefined }, { - "id": "AT:eyJhY3Rpdml0eUlkIjoiNDQyMzU4IiwiZXhwZXJpZW5jZUlkIjoiIn1=", - "scope": "home", - "items": [ + 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" + 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, + renderAttempted: true, scopeDetails: undefined } ]); - console.log(JSON.stringify(result.decisions, null, 2)); - expect(result.decisions).toBeUndefined(); + expect(applyPropositionsResult.decisions).toBeUndefined(); expect(mocks.sendEvent).not.toHaveBeenCalled(); expect(mocks.actions.appendHtml).toHaveBeenCalledOnceWith( diff --git a/test/unit/specs/components/Personalization/topLevel/redirectPageWideScopeDecision.spec.js b/test/unit/specs/components/Personalization/topLevel/redirectPageWideScopeDecision.spec.js index 9efe3cc8e..92c6d984c 100644 --- a/test/unit/specs/components/Personalization/topLevel/redirectPageWideScopeDecision.spec.js +++ b/test/unit/specs/components/Personalization/topLevel/redirectPageWideScopeDecision.spec.js @@ -1,62 +1,53 @@ -import { - REDIRECT_PAGE_WIDE_SCOPE_DECISION -} from "../responsesMock/eventResponses"; +import { REDIRECT_PAGE_WIDE_SCOPE_DECISION } from "../responsesMock/eventResponses"; -import buildMocks from "./buildMocks" +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, result } = await alloy.sendEvent( + const { event } = await alloy.sendEvent( { renderDecisions: true }, REDIRECT_PAGE_WIDE_SCOPE_DECISION ); - expect(event.toJSON()).toEqual({ - "query": { - "personalization": { - "schemas": [ + 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" - ] + decisionScopes: ["__view__"], + surfaces: ["web://example.com/home"] } } }); - expect(result).toEqual({ - "propositions": [] - }); + // No expectation on the result value because the page will redirect soon. expect(mocks.sendEvent).toHaveBeenCalledWith({ - "xdm": { - "_experience": { - "decisioning": { - "propositions": [ + xdm: { + _experience: { + decisioning: { + propositions: [ { - "id": "TNT:activity15:experience1", - "scope": "__view__", - "scopeDetails": { - "blah": "test" + id: "TNT:activity15:experience1", + scope: "__view__", + scopeDetails: { + blah: "test" } } ], - "propositionEventType": { - "display": 1 + propositionEventType: { + display: 1 } } }, - "eventType": "decisioning.propositionDisplay" + eventType: "decisioning.propositionDisplay" } }); From 4c337ec9b2c6cc28f6683892d3f63156b007aeb1 Mon Sep 17 00:00:00 2001 From: Jon Snyder Date: Fri, 18 Aug 2023 16:09:30 -0600 Subject: [PATCH 05/20] Remove unused files --- .../createApplyPropositions.js | 2 +- .../createAutoRenderingHandler.js | 77 ------ .../Personalization/createExecuteDecisions.js | 88 ------- .../createNonRenderingHandler.js | 54 ---- .../createOnResponseHandler.js | 68 ----- .../Personalization/createRedirectHandler.js | 37 --- .../delivery/createImmediateNotifications.js | 9 - .../Personalization/dom-actions/action.js | 4 +- .../Personalization/dom-actions/click.js | 18 -- .../dom-actions/clicks/index.js | 15 -- .../dom-actions/executeActions.js | 73 ------ .../Personalization/dom-actions/index.js | 1 - .../Personalization/groupDecisions.js | 149 ----------- .../handlers/createDomActionHandler.js | 5 +- .../handlers/createHtmlContentHandler.js | 5 +- .../Personalization/handlers/createRender.js | 16 +- .../Personalization/handlers/proposition.js | 33 ++- .../composePersonalizationResultingObject.js | 25 -- .../Personalization/utils/createAsyncArray.js | 15 +- src/components/Personalization/utils/split.js | 29 --- src/core/createEvent.js | 20 +- src/utils/deduplicateArray.js | 6 +- .../createAutoRenderingHandler.spec.js | 239 ------------------ .../createExecuteDecisions.spec.js | 127 ---------- .../createNonRenderingHandler.spec.js | 86 ------- .../createOnResponseHandler.spec.js | 206 --------------- .../createRedirectHandler.spec.js | 75 ------ .../Personalization/dom-actions/click.spec.js | 27 -- .../dom-actions/executeActions.spec.js | 136 ---------- .../Personalization/groupDecisions.spec.js | 114 --------- .../topLevel/mergedMetricDecisions.spec.js | 101 ++++---- ...eDecisionsWithDomActionSchemaItems.spec.js | 127 +++++----- .../topLevel/pageWideScopeDecisions.spec.js | 201 ++++++++------- ...cisionsWithoutDomActionSchemaItems.spec.js | 85 +++---- .../topLevel/productsViewDecisions.spec.js | 27 +- .../topLevel/scopesFoo1Foo2Decisions.spec.js | 139 +++++----- ...posePersonalizationResultingObject.spec.js | 35 --- 37 files changed, 395 insertions(+), 2079 deletions(-) delete mode 100644 src/components/Personalization/createAutoRenderingHandler.js delete mode 100644 src/components/Personalization/createExecuteDecisions.js delete mode 100644 src/components/Personalization/createNonRenderingHandler.js delete mode 100644 src/components/Personalization/createOnResponseHandler.js delete mode 100644 src/components/Personalization/createRedirectHandler.js delete mode 100644 src/components/Personalization/delivery/createImmediateNotifications.js delete mode 100644 src/components/Personalization/dom-actions/click.js delete mode 100644 src/components/Personalization/dom-actions/clicks/index.js delete mode 100644 src/components/Personalization/dom-actions/executeActions.js delete mode 100644 src/components/Personalization/groupDecisions.js delete mode 100644 src/components/Personalization/utils/composePersonalizationResultingObject.js delete mode 100644 src/components/Personalization/utils/split.js delete mode 100644 test/unit/specs/components/Personalization/createAutoRenderingHandler.spec.js delete mode 100644 test/unit/specs/components/Personalization/createExecuteDecisions.spec.js delete mode 100644 test/unit/specs/components/Personalization/createNonRenderingHandler.spec.js delete mode 100644 test/unit/specs/components/Personalization/createOnResponseHandler.spec.js delete mode 100644 test/unit/specs/components/Personalization/createRedirectHandler.spec.js delete mode 100644 test/unit/specs/components/Personalization/dom-actions/click.spec.js delete mode 100644 test/unit/specs/components/Personalization/dom-actions/executeActions.spec.js delete mode 100644 test/unit/specs/components/Personalization/groupDecisions.spec.js delete mode 100644 test/unit/specs/components/Personalization/utils/composePersonalizationResultingObject.spec.js diff --git a/src/components/Personalization/createApplyPropositions.js b/src/components/Personalization/createApplyPropositions.js index 7cdd3f18a..51f5ee759 100644 --- a/src/components/Personalization/createApplyPropositions.js +++ b/src/components/Personalization/createApplyPropositions.js @@ -19,7 +19,7 @@ import { createProposition } from "./handlers/proposition"; -export const SUPPORTED_SCHEMAS = [DOM_ACTION, HTML_CONTENT_ITEM]; +const SUPPORTED_SCHEMAS = [DOM_ACTION, HTML_CONTENT_ITEM]; export default ({ render }) => { const filterItemsPredicate = item => 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/createExecuteDecisions.js b/src/components/Personalization/createExecuteDecisions.js deleted file mode 100644 index e8d982753..000000000 --- a/src/components/Personalization/createExecuteDecisions.js +++ /dev/null @@ -1,88 +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); - console.log("ProcessMetas", JSON.stringify(results, null, 2)); - 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 => { - console.log("Results:", JSON.stringify(results, null, 2)); - return processMetas(logger, results); - }) - .catch(error => { - console.log("Error:", error); - logger.error(error); - }); - }; -}; 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/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/delivery/createImmediateNotifications.js b/src/components/Personalization/delivery/createImmediateNotifications.js deleted file mode 100644 index 645c7ae4a..000000000 --- a/src/components/Personalization/delivery/createImmediateNotifications.js +++ /dev/null @@ -1,9 +0,0 @@ - -export default ({ collect }) => ({ decisionsMeta, viewName }) => { - if (decisionsMeta.length > 0) { - // TODO just add the code for collect here - return collect({ decisionsMeta, viewName }); - } else { - return Promise.resolve(); - } -}; diff --git a/src/components/Personalization/dom-actions/action.js b/src/components/Personalization/dom-actions/action.js index 8d693c5ad..6b9ab2281 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)); @@ -35,7 +34,7 @@ const renderContent = (elements, content, renderFunc) => { export const createAction = renderFunc => { return settings => { - const { selector, prehidingSelector, content } = settings; + const { selector, prehidingSelector, content, meta } = settings; hideElements(prehidingSelector); @@ -45,6 +44,7 @@ export const createAction = renderFunc => { () => { // if everything is OK, show elements showElements(prehidingSelector); + return; }, error => { // in case of awaiting timing or error, we need to remove the style tag diff --git a/src/components/Personalization/dom-actions/click.js b/src/components/Personalization/dom-actions/click.js deleted file mode 100644 index 7a5f7332f..000000000 --- a/src/components/Personalization/dom-actions/click.js +++ /dev/null @@ -1,18 +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. -*/ - -export default (settings, store) => { - const { selector, meta } = settings; - - store({ selector, meta }); - return Promise.resolve(); -}; 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/executeActions.js b/src/components/Personalization/dom-actions/executeActions.js deleted file mode 100644 index 5c8c5eefd..000000000 --- a/src/components/Personalization/dom-actions/executeActions.js +++ /dev/null @@ -1,73 +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/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 index dd8a0436a..1d2dbdaa7 100644 --- a/src/components/Personalization/handlers/createDomActionHandler.js +++ b/src/components/Personalization/handlers/createDomActionHandler.js @@ -11,7 +11,7 @@ governing permissions and limitations under the License. */ import { DEFAULT_CONTENT_ITEM, DOM_ACTION } from "../constants/schema"; -export default ({ next, modules, storeClickMetrics }) => proposition => { +export default ({ next, modules, storeClickMetrics, preprocess }) => proposition => { const { items = [] } = proposition.getHandle(); items.forEach((item, index) => { @@ -30,8 +30,9 @@ export default ({ next, modules, storeClickMetrics }) => proposition => { }); } else if (modules[type]) { proposition.includeInDisplayNotification(); + const processedData = preprocess(data); proposition.addRenderer(index, () => { - return modules[type](data); + return modules[type](processedData); }); } } diff --git a/src/components/Personalization/handlers/createHtmlContentHandler.js b/src/components/Personalization/handlers/createHtmlContentHandler.js index 9f09f475e..bc0fab6e9 100644 --- a/src/components/Personalization/handlers/createHtmlContentHandler.js +++ b/src/components/Personalization/handlers/createHtmlContentHandler.js @@ -13,7 +13,7 @@ import { HTML_CONTENT_ITEM } from "../constants/schema"; import { VIEW_SCOPE_TYPE } from "../constants/scopeType"; import isPageWideScope from "../utils/isPageWideScope"; -export default ({ next, modules }) => proposition => { +export default ({ next, modules, preprocess }) => proposition => { const { scope, scopeDetails: { characteristics: { scopeType } = {} } = {}, @@ -25,8 +25,9 @@ export default ({ next, modules }) => proposition => { 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](data); + return modules[type](preprocessedData); }); } }); diff --git a/src/components/Personalization/handlers/createRender.js b/src/components/Personalization/handlers/createRender.js index 9c2942d25..1ef0ad97d 100644 --- a/src/components/Personalization/handlers/createRender.js +++ b/src/components/Personalization/handlers/createRender.js @@ -1,3 +1,5 @@ +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"); @@ -13,7 +15,8 @@ export default ({ handleChain, collect, executeRedirect, - logger + logger, + showContainers }) => propositions => { for (let i = 0; i < propositions.length; i += 1) { const proposition = propositions[i]; @@ -24,14 +27,17 @@ export default ({ 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( - () => { + 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); + }); } } diff --git a/src/components/Personalization/handlers/proposition.js b/src/components/Personalization/handlers/proposition.js index 95331e8a6..ae43ea4bb 100644 --- a/src/components/Personalization/handlers/proposition.js +++ b/src/components/Personalization/handlers/proposition.js @@ -9,6 +9,29 @@ 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. */ + +const renderWithLogging = async (renderer, logger) => { + try { + await renderer(); + if (logger.enabled) { + const details = JSON.stringify(processedAction); + logger.info(`Action ${details} executed.`); + } + return true; + } catch (e) { + if (logger.enabled) { + const details = JSON.stringify(processedAction); + 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; @@ -44,15 +67,7 @@ export const createProposition = (handle, isApplyPropositions = false) => { }, render(logger) { return Promise.all( - renderers.map(renderer => { - try { - renderer(); - return true; - } catch (e) { - logger.error(e); - return false; - } - }) + renderers.map(renderer => renderWithLogging(renderer, logger)) ).then(successes => { const notifications = []; // as long as at least one renderer succeeds, we want to add the notification diff --git a/src/components/Personalization/utils/composePersonalizationResultingObject.js b/src/components/Personalization/utils/composePersonalizationResultingObject.js deleted file mode 100644 index ad1a0e98b..000000000 --- a/src/components/Personalization/utils/composePersonalizationResultingObject.js +++ /dev/null @@ -1,25 +0,0 @@ -/* -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 addRenderAttemptedToDecisions from "./addRenderAttemptedToDecisions"; - -export default (decisions = [], renderDecisions) => { - const resultingObject = { - propositions: addRenderAttemptedToDecisions({ - decisions, - renderAttempted: renderDecisions - }) - }; - if (!renderDecisions) { - resultingObject.decisions = decisions; - } - return resultingObject; -}; diff --git a/src/components/Personalization/utils/createAsyncArray.js b/src/components/Personalization/utils/createAsyncArray.js index a594bd5b5..7a42d2fec 100644 --- a/src/components/Personalization/utils/createAsyncArray.js +++ b/src/components/Personalization/utils/createAsyncArray.js @@ -1,14 +1,15 @@ - export default () => { let latest = Promise.resolve([]); return { add(promise) { latest = latest.then(existingPropositions => { - return promise.then(newPropositions => { - return existingPropositions.concat(newPropositions); - }).catch(() => { - return existingPropositions; - }); + return promise + .then(newPropositions => { + return existingPropositions.concat(newPropositions); + }) + .catch(() => { + return existingPropositions; + }); }); }, clear() { @@ -16,5 +17,5 @@ export default () => { latest = Promise.resolve([]); return oldLatest; } - } + }; }; diff --git a/src/components/Personalization/utils/split.js b/src/components/Personalization/utils/split.js deleted file mode 100644 index 2f58225ae..000000000 --- a/src/components/Personalization/utils/split.js +++ /dev/null @@ -1,29 +0,0 @@ -/* -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 (array, ...predicates) => { - const results = predicates.map(() => []); - results.push([]); - - array.forEach(item => { - for (let i = 0; i < predicates.length; i += 1) { - if (predicates[i](item)) { - results[i].push(item); - return; - } - } - // else - results[predicates.length].push(item); - }); - - return results; -}; diff --git a/src/core/createEvent.js b/src/core/createEvent.js index 8b59f0d7a..798c63b16 100644 --- a/src/core/createEvent.js +++ b/src/core/createEvent.js @@ -10,7 +10,12 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import { isEmptyObject, deepAssign, isNonEmptyArray, deduplicateArray } from "../utils"; +import { + isEmptyObject, + deepAssign, + isNonEmptyArray, + deduplicateArray +} from "../utils"; export default () => { const content = {}; @@ -66,17 +71,20 @@ export default () => { if (userXdm) { // Merge the userXDM propositions with the ones included via the display // notification cache. - if (userXdm._experience && userXdm._experience.decisioning && + if ( + userXdm._experience && + userXdm._experience.decisioning && isNonEmptyArray(userXdm._experience.decisioning.propositions) && - content.xdm._experience && content.xdm._experience.decisioning && - isNonEmptyArray(content.xdm._experience.decisioning.propositions)) { - + content.xdm._experience && + content.xdm._experience.decisioning && + isNonEmptyArray(content.xdm._experience.decisioning.propositions) + ) { const newPropositions = deduplicateArray( [ ...userXdm._experience.decisioning.propositions, ...content.xdm._experience.decisioning.propositions ], - (a, b) => a === b || a.id && b.id && a.id === b.id + (a, b) => a === b || (a.id && b.id && a.id === b.id) ); event.mergeXdm(userXdm); content.xdm._experience.decisioning.propositions = newPropositions; diff --git a/src/utils/deduplicateArray.js b/src/utils/deduplicateArray.js index 468f24705..e4fd4beca 100644 --- a/src/utils/deduplicateArray.js +++ b/src/utils/deduplicateArray.js @@ -10,5 +10,7 @@ const findIndex = (array, item, isEqual) => { }; export default (array, isEqual = REFERENCE_EQUALITY) => { - return array.filter((item, index) => findIndex(array, item, isEqual) === index); -} + return array.filter( + (item, index) => findIndex(array, item, isEqual) === index + ); +}; 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/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/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/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/dom-actions/click.spec.js b/test/unit/specs/components/Personalization/dom-actions/click.spec.js deleted file mode 100644 index cb8090965..000000000 --- a/test/unit/specs/components/Personalization/dom-actions/click.spec.js +++ /dev/null @@ -1,27 +0,0 @@ -/* -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 { initDomActionsModules } from "../../../../../../src/components/Personalization/dom-actions"; - -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 }; - - click(settings, store); - - expect(store).toHaveBeenCalledWith({ selector, meta }); - }); -}); 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/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/topLevel/mergedMetricDecisions.spec.js b/test/unit/specs/components/Personalization/topLevel/mergedMetricDecisions.spec.js index 59f88e7c6..3f77d60be 100644 --- a/test/unit/specs/components/Personalization/topLevel/mergedMetricDecisions.spec.js +++ b/test/unit/specs/components/Personalization/topLevel/mergedMetricDecisions.spec.js @@ -1,12 +1,9 @@ -import { - MERGED_METRIC_DECISIONS -} from "../responsesMock/eventResponses"; +import { MERGED_METRIC_DECISIONS } from "../responsesMock/eventResponses"; -import buildMocks from "./buildMocks" +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); @@ -17,88 +14,86 @@ describe("PersonalizationComponent", () => { MERGED_METRIC_DECISIONS ); expect(event.toJSON()).toEqual({ - "query": { - "personalization": { - "schemas": [ + 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" - ] + decisionScopes: ["__view__"], + surfaces: ["web://example.com/home"] } } }); expect(result).toEqual({ - "propositions": [ + propositions: [ { - "renderAttempted": false, - "id": "TNT:activity6:experience1", - "scope": "testScope", - "items": [ + 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" + 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/default-content-item" }, { - "schema": "https://ns.adobe.com/personalization/measurement", - "data": { - "type": "click", - "format": "application/vnd.adobe.target.metric" + schema: "https://ns.adobe.com/personalization/measurement", + data: { + type: "click", + format: "application/vnd.adobe.target.metric" } } ], - "scopeDetails": { - "eventTokens": { - "display": "displayToken1", - "click": "clickToken1" + scopeDetails: { + eventTokens: { + display: "displayToken1", + click: "clickToken1" } } } ], - "decisions": [ + decisions: [ { - "id": "TNT:activity6:experience1", - "scope": "testScope", - "items": [ + 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" + 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/default-content-item" }, { - "schema": "https://ns.adobe.com/personalization/measurement", - "data": { - "type": "click", - "format": "application/vnd.adobe.target.metric" + schema: "https://ns.adobe.com/personalization/measurement", + data: { + type: "click", + format: "application/vnd.adobe.target.metric" } } ], - "scopeDetails": { - "eventTokens": { - "display": "displayToken1", - "click": "clickToken1" + scopeDetails: { + eventTokens: { + display: "displayToken1", + click: "clickToken1" } } } diff --git a/test/unit/specs/components/Personalization/topLevel/pageWideDecisionsWithDomActionSchemaItems.spec.js b/test/unit/specs/components/Personalization/topLevel/pageWideDecisionsWithDomActionSchemaItems.spec.js index 56ec696a9..3d936ca45 100644 --- a/test/unit/specs/components/Personalization/topLevel/pageWideDecisionsWithDomActionSchemaItems.spec.js +++ b/test/unit/specs/components/Personalization/topLevel/pageWideDecisionsWithDomActionSchemaItems.spec.js @@ -1,12 +1,9 @@ -import { - PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS -} from "../responsesMock/eventResponses"; +import { PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS } from "../responsesMock/eventResponses"; -import buildMocks from "./buildMocks" +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); @@ -16,114 +13,112 @@ describe("PersonalizationComponent", () => { }, PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS ); - expect(event.toJSON()).toEqual({ - "query": { - "personalization": { - "schemas": [ + 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" - ] + decisionScopes: ["__view__"], + surfaces: ["web://example.com/home"] } } }); expect(result).toEqual({ - "propositions": [ + propositions: [ { - "renderAttempted": true, - "id": "TNT:activity1:experience1", - "scope": "__view__", - "items": [ + 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: "#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/dom-action", + data: { + type: "setHtml", + selector: "#foo2", + content: "
here is a target activity
" } }, { - "schema": "https://ns.adobe.com/personalization/default-content-item" + schema: + "https://ns.adobe.com/personalization/default-content-item" } ], - "scopeDetails": { - "blah": "test" + scopeDetails: { + blah: "test" } }, { - "renderAttempted": true, - "id": "AJO:campaign1:message1", - "scope": "web://alloy.test.com/test/page/1", - "items": [ + 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: "#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/dom-action", + data: { + type: "setHtml", + selector: "#foo2", + content: "
here is a target activity
" } }, { - "schema": "https://ns.adobe.com/personalization/default-content-item" + schema: + "https://ns.adobe.com/personalization/default-content-item" } ], - "scopeDetails": { - "decisionProvider": "AJO" + scopeDetails: { + decisionProvider: "AJO" } } ], - "decisions": [] + decisions: [] }); expect(mocks.sendEvent).toHaveBeenCalledWith({ - "xdm": { - "_experience": { - "decisioning": { - "propositions": [ + xdm: { + _experience: { + decisioning: { + propositions: [ { - "id": "TNT:activity1:experience1", - "scope": "__view__", - "scopeDetails": { - "blah": "test" + id: "TNT:activity1:experience1", + scope: "__view__", + scopeDetails: { + blah: "test" } }, { - "id": "AJO:campaign1:message1", - "scope": "web://alloy.test.com/test/page/1", - "scopeDetails": { - "decisionProvider": "AJO" + id: "AJO:campaign1:message1", + scope: "web://alloy.test.com/test/page/1", + scopeDetails: { + decisionProvider: "AJO" } } ], - "propositionEventType": { - "display": 1 + propositionEventType: { + display: 1 } } }, - "eventType": "decisioning.propositionDisplay" + eventType: "decisioning.propositionDisplay" } }); expect(mocks.actions.setHtml).toHaveBeenCalledWith( diff --git a/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisions.spec.js b/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisions.spec.js index 0339b29e4..d4d9fec2c 100644 --- a/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisions.spec.js +++ b/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisions.spec.js @@ -1,12 +1,9 @@ -import { - PAGE_WIDE_SCOPE_DECISIONS -} from "../responsesMock/eventResponses"; +import { PAGE_WIDE_SCOPE_DECISIONS } from "../responsesMock/eventResponses"; -import buildMocks from "./buildMocks" +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); @@ -17,159 +14,161 @@ describe("PersonalizationComponent", () => { PAGE_WIDE_SCOPE_DECISIONS ); expect(event.toJSON()).toEqual({ - "query": { - "personalization": { - "schemas": [ + 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" - ] + decisionScopes: ["__view__"], + surfaces: ["web://example.com/home"] } } }); - expect(result.propositions).toEqual(jasmine.arrayWithExactContents([ + expect(result.propositions).toEqual( + jasmine.arrayWithExactContents([ { - "renderAttempted": true, - "id": "TNT:activity1:experience1", - "scope": "__view__", - "items": [ + 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: "#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/dom-action", + data: { + type: "setHtml", + selector: "#foo2", + content: "
here is a target activity
" } }, { - "schema": "https://ns.adobe.com/personalization/default-content-item" + schema: + "https://ns.adobe.com/personalization/default-content-item" } ], - "scopeDetails": { - "blah": "test" + scopeDetails: { + blah: "test" } }, { - "renderAttempted": true, - "id": "AJO:campaign1:message1", - "scope": "web://alloy.test.com/test/page/1", - "items": [ + 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: "#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/dom-action", + data: { + type: "setHtml", + selector: "#foo2", + content: "
here is a target activity
" } }, { - "schema": "https://ns.adobe.com/personalization/default-content-item" + schema: + "https://ns.adobe.com/personalization/default-content-item" } ], - "scopeDetails": { - "decisionProvider": "AJO" + scopeDetails: { + decisionProvider: "AJO" } }, { - "renderAttempted": false, - "id": "TNT:activity1:experience1", - "scope": "__view__", - "items": [ + 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: "A", + content: "Banner A ...." } }, { - "schema": "https://ns.adove.com/experience/item", - "data": { - "id": "B", - "content": "Banner B ...." + schema: "https://ns.adove.com/experience/item", + data: { + id: "B", + content: "Banner B ...." } } ], - "scopeDetails": { - "blah": "test" + 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 ...." + ]) + ); + 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" } - ], - "scopeDetails": { - "blah": "test" } - } - ])); + ]) + ); expect(mocks.sendEvent).toHaveBeenCalledWith({ - "xdm": { - "_experience": { - "decisioning": { - "propositions": [ + xdm: { + _experience: { + decisioning: { + propositions: [ { - "id": "TNT:activity1:experience1", - "scope": "__view__", - "scopeDetails": { - "blah": "test" + id: "TNT:activity1:experience1", + scope: "__view__", + scopeDetails: { + blah: "test" } }, { - "id": "AJO:campaign1:message1", - "scope": "web://alloy.test.com/test/page/1", - "scopeDetails": { - "decisionProvider": "AJO" + id: "AJO:campaign1:message1", + scope: "web://alloy.test.com/test/page/1", + scopeDetails: { + decisionProvider: "AJO" } } ], - "propositionEventType": { - "display": 1 + propositionEventType: { + display: 1 } } }, - "eventType": "decisioning.propositionDisplay" + eventType: "decisioning.propositionDisplay" } }); expect(mocks.actions.setHtml).toHaveBeenCalledWith( diff --git a/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisionsWithoutDomActionSchemaItems.spec.js b/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisionsWithoutDomActionSchemaItems.spec.js index 865ad5a64..fc96d24bd 100644 --- a/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisionsWithoutDomActionSchemaItems.spec.js +++ b/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisionsWithoutDomActionSchemaItems.spec.js @@ -1,14 +1,13 @@ -import { - PAGE_WIDE_SCOPE_DECISIONS_WITHOUT_DOM_ACTION_SCHEMA_ITEMS -} from "../responsesMock/eventResponses"; +import { PAGE_WIDE_SCOPE_DECISIONS_WITHOUT_DOM_ACTION_SCHEMA_ITEMS } from "../responsesMock/eventResponses"; -import buildMocks from "./buildMocks" +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 mocks = buildMocks( + PAGE_WIDE_SCOPE_DECISIONS_WITHOUT_DOM_ACTION_SCHEMA_ITEMS + ); const alloy = buildAlloy(mocks); const { event, result } = await alloy.sendEvent( { @@ -16,74 +15,70 @@ describe("PersonalizationComponent", () => { }, PAGE_WIDE_SCOPE_DECISIONS_WITHOUT_DOM_ACTION_SCHEMA_ITEMS ); - expect(event.toJSON()).toEqual({ - "query": { - "personalization": { - "schemas": [ + 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" - ] + decisionScopes: ["__view__"], + surfaces: ["web://example.com/home"] } } }); expect(result).toEqual({ - "propositions": [ + propositions: [ { - "renderAttempted": false, - "id": "TNT:activity1:experience1", - "scope": "__view__", - "items": [ + 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: "A", + content: "Banner A ...." } }, { - "schema": "https://ns.adove.com/experience/item", - "data": { - "id": "B", - "content": "Banner B ...." + schema: "https://ns.adove.com/experience/item", + data: { + id: "B", + content: "Banner B ...." } } ], - "scopeDetails": { - "blah": "test" + scopeDetails: { + blah: "test" } } ], - "decisions": [ + decisions: [ { - "id": "TNT:activity1:experience1", - "scope": "__view__", - "items": [ + 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: "A", + content: "Banner A ...." } }, { - "schema": "https://ns.adove.com/experience/item", - "data": { - "id": "B", - "content": "Banner B ...." + schema: "https://ns.adove.com/experience/item", + data: { + id: "B", + content: "Banner B ...." } } ], - "scopeDetails": { - "blah": "test" + scopeDetails: { + blah: "test" } } ] diff --git a/test/unit/specs/components/Personalization/topLevel/productsViewDecisions.spec.js b/test/unit/specs/components/Personalization/topLevel/productsViewDecisions.spec.js index c4a56d8f2..0f7c72002 100644 --- a/test/unit/specs/components/Personalization/topLevel/productsViewDecisions.spec.js +++ b/test/unit/specs/components/Personalization/topLevel/productsViewDecisions.spec.js @@ -1,12 +1,9 @@ -import { - PRODUCTS_VIEW_DECISIONS -} from "../responsesMock/eventResponses"; +import { PRODUCTS_VIEW_DECISIONS } from "../responsesMock/eventResponses"; -import buildMocks from "./buildMocks" +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); @@ -16,28 +13,24 @@ describe("PersonalizationComponent", () => { }, PRODUCTS_VIEW_DECISIONS ); - expect(event.toJSON()).toEqual({ - "query": { - "personalization": { - "schemas": [ + 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" - ] + decisionScopes: ["__view__"], + surfaces: ["web://example.com/home"] } } }); expect(result).toEqual({ - "propositions": [], - "decisions": [] + propositions: [], + decisions: [] }); expect(mocks.sendEvent).not.toHaveBeenCalled(); diff --git a/test/unit/specs/components/Personalization/topLevel/scopesFoo1Foo2Decisions.spec.js b/test/unit/specs/components/Personalization/topLevel/scopesFoo1Foo2Decisions.spec.js index 3b85d409a..cf5e76d12 100644 --- a/test/unit/specs/components/Personalization/topLevel/scopesFoo1Foo2Decisions.spec.js +++ b/test/unit/specs/components/Personalization/topLevel/scopesFoo1Foo2Decisions.spec.js @@ -1,12 +1,9 @@ -import { - SCOPES_FOO1_FOO2_DECISIONS -} from "../responsesMock/eventResponses"; +import { SCOPES_FOO1_FOO2_DECISIONS } from "../responsesMock/eventResponses"; -import buildMocks from "./buildMocks" +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); @@ -16,119 +13,115 @@ describe("PersonalizationComponent", () => { }, SCOPES_FOO1_FOO2_DECISIONS ); - expect(event.toJSON()).toEqual({ - "query": { - "personalization": { - "schemas": [ + 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" - ] + decisionScopes: ["__view__"], + surfaces: ["web://example.com/home"] } } }); expect(result).toEqual({ - "propositions": [ + propositions: [ { - "renderAttempted": false, - "id": "TNT:ABC:A", - "scope": "Foo1", - "items": [ + 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: "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: "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" + 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" + scopeDetails: { + blah: "test" } }, { - "renderAttempted": false, - "id": "TNT:ABC:A", - "scope": "Foo2", - "items": [ + renderAttempted: false, + id: "TNT:ABC:A", + scope: "Foo2", + items: [ { - "schema": "https://ns.adove.com/experience/item", - "data": { - "id": "A", - "content": "Banner A ...." + schema: "https://ns.adove.com/experience/item", + data: { + id: "A", + content: "Banner A ...." } } ] } ], - "decisions": [ + decisions: [ { - "id": "TNT:ABC:A", - "scope": "Foo1", - "items": [ + 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: "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: "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" + 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" + scopeDetails: { + blah: "test" } }, { - "id": "TNT:ABC:A", - "scope": "Foo2", - "items": [ + id: "TNT:ABC:A", + scope: "Foo2", + items: [ { - "schema": "https://ns.adove.com/experience/item", - "data": { - "id": "A", - "content": "Banner A ...." + schema: "https://ns.adove.com/experience/item", + data: { + id: "A", + content: "Banner A ...." } } ] 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); - }); -}); From f6483766c7499cf134ac666264de5ee350b25cde Mon Sep 17 00:00:00 2001 From: Jon Snyder Date: Fri, 18 Aug 2023 17:59:20 -0600 Subject: [PATCH 06/20] fix some tests --- .../Personalization/createComponent.js | 2 +- .../Personalization/createFetchDataHandler.js | 2 +- .../Personalization/createViewCacheManager.js | 4 +- .../createViewChangeHandler.js | 29 ---- .../Personalization/handlers/proposition.js | 12 +- .../Personalization/createComponent.spec.js | 2 +- .../createFetchDataHandler.spec.js | 124 +++++++++------- .../createViewCacheManager.spec.js | 132 +++++++++++------- .../createViewChangeHandler.spec.js | 62 ++++---- .../dom-actions/initDomActionsModules.spec.js | 2 - .../handlers/createRedirectHandler.spec.js | 2 +- .../Personalization/topLevel/buildAlloy.js | 11 +- 12 files changed, 209 insertions(+), 175 deletions(-) diff --git a/src/components/Personalization/createComponent.js b/src/components/Personalization/createComponent.js index 73f7e6ae4..226db3def 100644 --- a/src/components/Personalization/createComponent.js +++ b/src/components/Personalization/createComponent.js @@ -68,7 +68,7 @@ export default ({ const cacheUpdate = viewCache.createCacheUpdate( personalizationDetails.getViewName() ); - onRequestFailure(() => cacheUpdate.reject()); + onRequestFailure(() => cacheUpdate.cancel()); fetchDataHandler({ cacheUpdate, diff --git a/src/components/Personalization/createFetchDataHandler.js b/src/components/Personalization/createFetchDataHandler.js index 4ff56b223..22c1fa217 100644 --- a/src/components/Personalization/createFetchDataHandler.js +++ b/src/components/Personalization/createFetchDataHandler.js @@ -30,7 +30,7 @@ export default ({ } mergeQuery(event, personalizationDetails.createQueryDetails()); - onResponse(async ({ response }) => { + onResponse(({ response }) => { const handles = response.getPayloadsByType(DECISIONS_HANDLE); const handlesToRender = cacheUpdate.update(handles); const propositions = handlesToRender.map(createProposition); diff --git a/src/components/Personalization/createViewCacheManager.js b/src/components/Personalization/createViewCacheManager.js index 58f5b8a4e..2629098a0 100644 --- a/src/components/Personalization/createViewCacheManager.js +++ b/src/components/Personalization/createViewCacheManager.js @@ -65,12 +65,12 @@ export default () => { return [ ...(newViewStorage[viewName] || createEmptyViewPropositions(viewName)), - otherHandles + ...otherHandles ]; } return otherHandles; }, - error() { + cancel() { updateCacheDeferred.reject(); } }; diff --git a/src/components/Personalization/createViewChangeHandler.js b/src/components/Personalization/createViewChangeHandler.js index c581030d7..2c50a0a1f 100644 --- a/src/components/Personalization/createViewChangeHandler.js +++ b/src/components/Personalization/createViewChangeHandler.js @@ -34,34 +34,5 @@ export default ({ mergeDecisionsMeta, render, viewCache }) => { 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); - }); - }); - } - - onResponse(() => { - return composePersonalizationResultingObject(viewDecisions, false); - }); - return {}; - }); -*/ }; }; diff --git a/src/components/Personalization/handlers/proposition.js b/src/components/Personalization/handlers/proposition.js index ae43ea4bb..e37f033d9 100644 --- a/src/components/Personalization/handlers/proposition.js +++ b/src/components/Personalization/handlers/proposition.js @@ -39,7 +39,10 @@ export const createProposition = (handle, isApplyPropositions = false) => { let redirectUrl; let includeInDisplayNotification = false; let includeInReturnedPropositions = true; - const itemsRenderAttempted = new Array(items.length).map(() => false); + const itemsRenderAttempted = new Array(items.length); + for (let i = 0; i < items.length; i += 1) { + itemsRenderAttempted[i] = false; + } return { getHandle() { @@ -50,6 +53,9 @@ export const createProposition = (handle, isApplyPropositions = false) => { }, redirect(url) { includeInDisplayNotification = true; + itemsRenderAttempted.forEach((_, index) => { + itemsRenderAttempted[index] = true; + }); redirectUrl = url; }, getRedirectUrl() { @@ -86,7 +92,7 @@ export const createProposition = (handle, isApplyPropositions = false) => { addToReturnedPropositions(propositions) { if (includeInReturnedPropositions) { const renderedItems = items.filter( - (item, index) => itemsRenderAttempted[index] + (_, index) => itemsRenderAttempted[index] ); if (renderedItems.length > 0) { propositions.push({ @@ -96,7 +102,7 @@ export const createProposition = (handle, isApplyPropositions = false) => { }); } const nonrenderedItems = items.filter( - (item, index) => !itemsRenderAttempted[index] + (_, index) => !itemsRenderAttempted[index] ); if (nonrenderedItems.length > 0) { propositions.push({ diff --git a/test/unit/specs/components/Personalization/createComponent.spec.js b/test/unit/specs/components/Personalization/createComponent.spec.js index fcb8d92d3..5b4d28100 100644 --- a/test/unit/specs/components/Personalization/createComponent.spec.js +++ b/test/unit/specs/components/Personalization/createComponent.spec.js @@ -56,7 +56,7 @@ describe("Personalization", () => { mergeQuery = jasmine.createSpy("mergeQuery"); viewCache = jasmine.createSpyObj("viewCache", [ "isInitialized", - "storeViews" + "createCacheUpdate" ]); setTargetMigration = jasmine.createSpy("setTargetMigration"); diff --git a/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js b/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js index d35788f0d..ceed93fce 100644 --- a/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js +++ b/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js @@ -10,87 +10,113 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ +import { de } from "date-fns/locale"; import createFetchDataHandler from "../../../../../src/components/Personalization/createFetchDataHandler"; +import flushPromiseChains from "../../../helpers/flushPromiseChains"; describe("Personalization::createFetchDataHandler", () => { - let responseHandler; + + let prehidingStyle; 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"; + 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, 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([{ id: "handle1", items: ["item1"] }]); + const result = returnResponse(); + expect(result).toEqual({ + propositions: [{ id: "handle1", items: ["item1"], renderAttempted: true }], + decisions: [] + }); + await flushPromiseChains(); + 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/createViewCacheManager.spec.js b/test/unit/specs/components/Personalization/createViewCacheManager.spec.js index 0c7e2ec89..bfe9d24f7 100644 --- a/test/unit/specs/components/Personalization/createViewCacheManager.spec.js +++ b/test/unit/specs/components/Personalization/createViewCacheManager.spec.js @@ -11,74 +11,104 @@ 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", () => { + it("stores and gets the decisions based on a viewName", async () => { const viewCacheManager = createViewCacheManager(); - const decisionsDeferred = defer(); - viewCacheManager.storeViews(decisionsDeferred.promise); - decisionsDeferred.resolve(viewDecisions); + const cacheUpdate = viewCacheManager.createCacheUpdate("home"); + const resultingHandles = cacheUpdate.update(viewHandles); + expect(resultingHandles).toEqual([ + viewHandles[0], + viewHandles[1], + viewHandles[3] + ]); + + const homeViews = await viewCacheManager.getView("home"); + expect(homeViews).toEqual([ + viewHandles[0], + viewHandles[1] + ]); + + const cartViews = await viewCacheManager.getView("cart"); + expect(cartViews).toEqual([ + viewHandles[2] + ]); - return Promise.all([ - expectAsync(viewCacheManager.getView(cartView)).toBeResolvedTo( - viewDecisions[cartView] - ), - expectAsync(viewCacheManager.getView(homeView)).toBeResolvedTo( - viewDecisions[homeView] - ) + const otherViews = await viewCacheManager.getView("other"); + expect(otherViews).toEqual([ + { + scope: "other", + scopeDetails: { + characteristics: { + scopeType: "view" + } + } + } ]); }); - it("gets an empty array if there is no decisions for a specific view", () => { + it("should be no views when decisions deferred is rejected", async () => { const viewCacheManager = createViewCacheManager(); - const decisionsDeferred = defer(); + const cacheUpdate = viewCacheManager.createCacheUpdate("home"); + cacheUpdate.cancel(); - viewCacheManager.storeViews(decisionsDeferred.promise); - decisionsDeferred.resolve(viewDecisions); - - return Promise.all([ - expectAsync(viewCacheManager.getView(productsView)).toBeResolvedTo([]) + const homeViews = await viewCacheManager.getView("home"); + expect(homeViews).toEqual([ + { + scope: "home", + scopeDetails: { + characteristics: { + scopeType: "view" + } + } + } ]); }); - it("should be no views when decisions deferred is rejected", () => { + it("should not be initialized when first created", () => { const viewCacheManager = createViewCacheManager(); - const decisionsDeferred = defer(); - - viewCacheManager.storeViews(decisionsDeferred.promise); - decisionsDeferred.reject(); + 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(); + 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..3bb3e95c7 100644 --- a/test/unit/specs/components/Personalization/createViewChangeHandler.spec.js +++ b/test/unit/specs/components/Personalization/createViewChangeHandler.spec.js @@ -15,59 +15,58 @@ import { PropositionEventType } from "../../../../../src/components/Personalizat import { CART_VIEW_DECISIONS } from "./responsesMock/eventResponses"; 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)); + 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 +115,5 @@ describe("Personalization::createViewChangeHandler", () => { expect(executeDecisions).toHaveBeenCalledWith([]); expect(collect).toHaveBeenCalled(); }); + */ }); 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/handlers/createRedirectHandler.spec.js b/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js index 21e0ba1ad..c7dc10f52 100644 --- a/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js +++ b/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js @@ -54,7 +54,7 @@ describe("redirectHandler", () => { ] }; const proposition = createProposition(handle); - redirectHandler({ proposition, viewName: "myview" }); + redirectHandler(proposition); expect(next).not.toHaveBeenCalled(); expect(proposition.getRedirectUrl()).toEqual( "https://alloyio.com/functional-test/alloyTestPage.html?redirectedTest=true&test=C205528" diff --git a/test/unit/specs/components/Personalization/topLevel/buildAlloy.js b/test/unit/specs/components/Personalization/topLevel/buildAlloy.js index 970381fa8..3f726446c 100644 --- a/test/unit/specs/components/Personalization/topLevel/buildAlloy.js +++ b/test/unit/specs/components/Personalization/topLevel/buildAlloy.js @@ -85,11 +85,13 @@ const buildComponent = ({ const modules = initDomActionsModulesMocks(); const noOpHandler = () => undefined; + const preprocess = action => action; const domActionHandler = createDomActionHandler({ next: noOpHandler, isPageWideSurface, modules, - storeClickMetrics + storeClickMetrics, + preprocess }); const measurementSchemaHandler = createMeasurementSchemaHandler({ next: domActionHandler @@ -97,13 +99,14 @@ const buildComponent = ({ const redirectHandler = createRedirectHandler({ next: measurementSchemaHandler }); - const fetchHandler = createHtmlContentHandler({ + const htmlContentHandler = createHtmlContentHandler({ next: redirectHandler, - modules + modules, + preprocess }); const render = createRender({ - handleChain: fetchHandler, + handleChain: htmlContentHandler, collect, executeRedirect: url => window.location.replace(url), logger From 1cc8e88c2ad785b568a8538b25264fdf29866437 Mon Sep 17 00:00:00 2001 From: Jon Snyder Date: Mon, 21 Aug 2023 12:43:42 -0600 Subject: [PATCH 07/20] Update all unit tests --- .../createApplyPropositions.js | 10 +-- .../createApplyPropositions.spec.js | 90 +++++++------------ .../Personalization/createComponent.spec.js | 11 ++- .../createPersonalizationDetails.spec.js | 37 +++----- 4 files changed, 52 insertions(+), 96 deletions(-) diff --git a/src/components/Personalization/createApplyPropositions.js b/src/components/Personalization/createApplyPropositions.js index 51f5ee759..63181c17a 100644 --- a/src/components/Personalization/createApplyPropositions.js +++ b/src/components/Personalization/createApplyPropositions.js @@ -13,7 +13,6 @@ governing permissions and limitations under the License. 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 @@ -74,7 +73,7 @@ export default ({ render }) => { .filter(proposition => isNonEmptyArray(proposition.items)); }; - const applyPropositions = async ({ propositions, metadata }) => { + return async ({ propositions, metadata = {} }) => { const propositionsToExecute = preparePropositions({ propositions, metadata @@ -85,11 +84,4 @@ export default ({ render }) => { propositions: buildReturnedPropositions(propositionsToExecute) }; }; - - return ({ propositions, metadata = {} }) => { - if (isNonEmptyArray(propositions)) { - return applyPropositions({ propositions, metadata }); - } - return Promise.resolve(EMPTY_PROPOSITIONS); - }; }; diff --git a/test/unit/specs/components/Personalization/createApplyPropositions.spec.js b/test/unit/specs/components/Personalization/createApplyPropositions.spec.js index 0f75b7f9e..e7486bd0b 100644 --- a/test/unit/specs/components/Personalization/createApplyPropositions.spec.js +++ b/test/unit/specs/components/Personalization/createApplyPropositions.spec.js @@ -26,34 +26,34 @@ 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({ propositions: [] }).then(result => { expect(result).toEqual({ propositions: [] }); - expect(executeDecisions).toHaveBeenCalledTimes(0); + 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,16 +62,13 @@ describe("Personalization::createApplyPropositions", () => { }); const applyPropositions = createApplyPropositions({ - executeDecisions + render }); return applyPropositions({ propositions: PAGE_WIDE_SCOPE_DECISIONS }).then(result => { - expect(executeDecisions).toHaveBeenCalledTimes(1); - expect(executeDecisions.calls.first().args[0]).toEqual( - expectedExecuteDecisionsPropositions - ); + expect(render).toHaveBeenCalledTimes(1); const expectedScopes = expectedExecuteDecisionsPropositions.map( proposition => proposition.scope @@ -86,22 +83,18 @@ describe("Personalization::createApplyPropositions", () => { }); 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({ propositions: MIXED_PROPOSITIONS, metadata: METADATA - }).then(() => { - const executedPropositions = executeDecisions.calls.first().args[0]; - expect(executedPropositions.length).toEqual(3); - executedPropositions.forEach(proposition => { + }).then(({ propositions }) => { + expect(propositions.length).toEqual(4); + propositions.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") { @@ -110,7 +103,7 @@ describe("Personalization::createApplyPropositions", () => { expect(proposition.items[0].data.type).toEqual("setHtml"); } }); - expect(executeDecisions).toHaveBeenCalledTimes(1); + expect(render).toHaveBeenCalledTimes(1); }); }); @@ -142,11 +135,7 @@ describe("Personalization::createApplyPropositions", () => { } ]; - executeDecisions.and.returnValue(Promise.resolve(MIXED_PROPOSITIONS)); - - const applyPropositions = createApplyPropositions({ - executeDecisions - }); + const applyPropositions = createApplyPropositions({ render }); return applyPropositions({ propositions @@ -159,16 +148,12 @@ describe("Personalization::createApplyPropositions", () => { }); 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({ propositions: MIXED_PROPOSITIONS }).then(result => { - expect(result.propositions.length).toEqual(2); + expect(result.propositions.length).toEqual(3); result.propositions.forEach(proposition => { expect(proposition.renderAttempted).toBeTrue(); }); @@ -176,14 +161,12 @@ describe("Personalization::createApplyPropositions", () => { }); it("it should ignore propositions with __view__ scope that have already been rendered", () => { - executeDecisions.and.returnValue(Promise.resolve(MIXED_PROPOSITIONS)); - - const applyPropositions = createApplyPropositions({ - executeDecisions - }); + const applyPropositions = createApplyPropositions({ render }); + const propositions = JSON.parse(JSON.stringify(MIXED_PROPOSITIONS)); + propositions[4].renderAttempted = true; return applyPropositions({ - propositions: MIXED_PROPOSITIONS + propositions }).then(result => { expect(result.propositions.length).toEqual(2); result.propositions.forEach(proposition => { @@ -198,20 +181,15 @@ describe("Personalization::createApplyPropositions", () => { }); 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({ propositions: MIXED_PROPOSITIONS - }).then(() => { - const executedPropositions = executeDecisions.calls.first().args[0]; - expect(executedPropositions.length).toEqual(2); - executedPropositions.forEach(proposition => { + }).then(({ propositions }) => { + expect(propositions.length).toEqual(3); + propositions.forEach(proposition => { expect(proposition.items.length).toEqual(1); proposition.items.forEach(item => { expect(expectedItemIds.indexOf(item.id) > -1); @@ -221,11 +199,7 @@ describe("Personalization::createApplyPropositions", () => { }); 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({ @@ -243,7 +217,7 @@ describe("Personalization::createApplyPropositions", () => { expect(proposition).not.toBe(original); } }); - expect(numReturnedPropositions).toEqual(3); + expect(numReturnedPropositions).toEqual(4); }); }); }); diff --git a/test/unit/specs/components/Personalization/createComponent.spec.js b/test/unit/specs/components/Personalization/createComponent.spec.js index 5b4d28100..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({ @@ -58,6 +59,8 @@ describe("Personalization", () => { "isInitialized", "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/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); From 520a0c8e272f77b8742c03c65d477a5fd9846b95 Mon Sep 17 00:00:00 2001 From: Jon Snyder Date: Tue, 22 Aug 2023 15:34:29 -0600 Subject: [PATCH 08/20] Fix functional tests, and fix issues found when running functional tests --- .../createApplyPropositions.js | 3 +- .../Personalization/createFetchDataHandler.js | 4 +- .../Personalization/createViewCacheManager.js | 43 ++--- .../createViewChangeHandler.js | 34 ++-- .../Personalization/dom-actions/action.js | 5 +- .../dom-actions/createPreprocess.js | 19 +++ .../handlers/createDomActionHandler.js | 12 +- .../handlers/createHtmlContentHandler.js | 38 ----- .../Personalization/handlers/proposition.js | 59 ++++--- src/components/Personalization/index.js | 16 +- .../specs/Personalization/C205528.js | 2 +- .../specs/Personalization/C6364798.js | 78 ++++----- .../specs/Personalization/C782718.js | 57 ++++--- test/functional/specs/Privacy/IAB/C224677.js | 10 +- .../createApplyPropositions.spec.js | 148 +++++++++--------- .../createFetchDataHandler.spec.js | 22 ++- .../createViewCacheManager.spec.js | 35 +++-- .../createViewChangeHandler.spec.js | 7 +- .../Personalization/topLevel/buildAlloy.js | 3 +- 19 files changed, 317 insertions(+), 278 deletions(-) create mode 100644 src/components/Personalization/dom-actions/createPreprocess.js diff --git a/src/components/Personalization/createApplyPropositions.js b/src/components/Personalization/createApplyPropositions.js index 63181c17a..15b00afb1 100644 --- a/src/components/Personalization/createApplyPropositions.js +++ b/src/components/Personalization/createApplyPropositions.js @@ -73,13 +73,14 @@ export default ({ render }) => { .filter(proposition => isNonEmptyArray(proposition.items)); }; - return async ({ propositions, metadata = {} }) => { + return ({ propositions, metadata = {} }) => { const propositionsToExecute = preparePropositions({ propositions, metadata }).map(proposition => createProposition(proposition, true)); render(propositionsToExecute); + return { propositions: buildReturnedPropositions(propositionsToExecute) }; diff --git a/src/components/Personalization/createFetchDataHandler.js b/src/components/Personalization/createFetchDataHandler.js index 22c1fa217..6bf75f953 100644 --- a/src/components/Personalization/createFetchDataHandler.js +++ b/src/components/Personalization/createFetchDataHandler.js @@ -10,7 +10,6 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ import { - createProposition, buildReturnedPropositions, buildReturnedDecisions } from "./handlers/proposition"; @@ -32,8 +31,7 @@ export default ({ onResponse(({ response }) => { const handles = response.getPayloadsByType(DECISIONS_HANDLE); - const handlesToRender = cacheUpdate.update(handles); - const propositions = handlesToRender.map(createProposition); + const propositions = cacheUpdate.update(handles); if (personalizationDetails.isRenderDecisions()) { render(propositions).then(decisionsMeta => { if (decisionsMeta.length > 0) { diff --git a/src/components/Personalization/createViewCacheManager.js b/src/components/Personalization/createViewCacheManager.js index 2629098a0..6f6a8a394 100644 --- a/src/components/Personalization/createViewCacheManager.js +++ b/src/components/Personalization/createViewCacheManager.js @@ -14,23 +14,29 @@ import { assign } from "../../utils"; import defer from "../../utils/defer"; import { VIEW_SCOPE_TYPE } from "./constants/scopeType"; -const createEmptyViewPropositions = viewName => { - return [ - { +export default ({ createProposition }) => { + const viewStorage = {}; + let cacheUpdateCreatedAtLeastOnce = false; + let previousUpdateCacheComplete = Promise.resolve(); + + 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" } } - } - ]; -}; - -export default () => { - const viewStorage = {}; - let cacheUpdateCreatedAtLeastOnce = false; - let previousUpdateCacheComplete = Promise.resolve(); + }); + emptyViewProposition.includeInDisplayNotification(); + emptyViewProposition.excludeInReturnedPropositions(); + return [emptyViewProposition]; + }; // This should be called before making the request to experience edge. const createCacheUpdate = viewName => { @@ -47,7 +53,7 @@ export default () => { return { update(personalizationHandles) { const newViewStorage = {}; - const otherHandles = []; + const otherPropositions = []; personalizationHandles.forEach(handle => { const { scope, @@ -57,18 +63,17 @@ export default () => { newViewStorage[scope] = newViewStorage[scope] || []; newViewStorage[scope].push(handle); } else { - otherHandles.push(handle); + otherPropositions.push(createProposition(handle)); } }); updateCacheDeferred.resolve(newViewStorage); if (viewName) { return [ - ...(newViewStorage[viewName] || - createEmptyViewPropositions(viewName)), - ...otherHandles + ...getViewPropositions(newViewStorage, viewName), + ...otherPropositions ]; } - return otherHandles; + return otherPropositions; }, cancel() { updateCacheDeferred.reject(); @@ -77,8 +82,8 @@ export default () => { }; const getView = viewName => { - return previousUpdateCacheComplete.then( - () => viewStorage[viewName] || createEmptyViewPropositions(viewName) + return previousUpdateCacheComplete.then(() => + getViewPropositions(viewStorage, viewName) ); }; diff --git a/src/components/Personalization/createViewChangeHandler.js b/src/components/Personalization/createViewChangeHandler.js index 2c50a0a1f..500732be7 100644 --- a/src/components/Personalization/createViewChangeHandler.js +++ b/src/components/Personalization/createViewChangeHandler.js @@ -13,26 +13,30 @@ governing permissions and limitations under the License. import { PropositionEventType } from "./constants/propositionEventType"; import { buildReturnedPropositions, - buildReturnedDecisions, - createProposition + buildReturnedDecisions } from "./handlers/proposition"; export default ({ mergeDecisionsMeta, render, viewCache }) => { - return async ({ personalizationDetails, event, onResponse }) => { + return ({ personalizationDetails, event, onResponse }) => { const viewName = personalizationDetails.getViewName(); - const viewHandles = await viewCache.getView(viewName); - const propositions = viewHandles.map(createProposition); + return viewCache + .getView(viewName) + .then(propositions => { + onResponse(() => { + return { + propositions: buildReturnedPropositions(propositions), + decisions: buildReturnedDecisions(propositions) + }; + }); - if (personalizationDetails.isRenderDecisions()) { - const decisionsMeta = await render(propositions); - mergeDecisionsMeta(event, decisionsMeta, PropositionEventType.DISPLAY); - } - onResponse(() => { - return { - propositions: buildReturnedPropositions(propositions), - decisions: buildReturnedDecisions(propositions) - }; - }); + if (personalizationDetails.isRenderDecisions()) { + return render(propositions); + } + return Promise.resolve([]); + }) + .then(decisionsMeta => { + mergeDecisionsMeta(event, decisionsMeta, PropositionEventType.DISPLAY); + }); }; }; diff --git a/src/components/Personalization/dom-actions/action.js b/src/components/Personalization/dom-actions/action.js index 6b9ab2281..28667d370 100644 --- a/src/components/Personalization/dom-actions/action.js +++ b/src/components/Personalization/dom-actions/action.js @@ -33,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); @@ -44,7 +44,6 @@ export const createAction = renderFunc => { () => { // if everything is OK, show elements showElements(prehidingSelector); - return; }, error => { // in case of awaiting timing or error, we need to remove the style tag diff --git a/src/components/Personalization/dom-actions/createPreprocess.js b/src/components/Personalization/dom-actions/createPreprocess.js new file mode 100644 index 000000000..ae094c2e7 --- /dev/null +++ b/src/components/Personalization/dom-actions/createPreprocess.js @@ -0,0 +1,19 @@ +/* +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 { assign } from "../../../utils"; + +export default preprocessors => action => { + return preprocessors.reduce( + (processed, fn) => assign(processed, fn(processed)), + action + ); +}; diff --git a/src/components/Personalization/handlers/createDomActionHandler.js b/src/components/Personalization/handlers/createDomActionHandler.js index 1d2dbdaa7..a27b6c48e 100644 --- a/src/components/Personalization/handlers/createDomActionHandler.js +++ b/src/components/Personalization/handlers/createDomActionHandler.js @@ -11,7 +11,12 @@ governing permissions and limitations under the License. */ import { DEFAULT_CONTENT_ITEM, DOM_ACTION } from "../constants/schema"; -export default ({ next, modules, storeClickMetrics, preprocess }) => proposition => { +export default ({ + next, + modules, + storeClickMetrics, + preprocess +}) => proposition => { const { items = [] } = proposition.getHandle(); items.forEach((item, index) => { @@ -25,9 +30,8 @@ export default ({ next, modules, storeClickMetrics, preprocess }) => proposition if (type === "click") { // Do not record the click proposition in display notification. // Store it for later. - proposition.addRenderer(index, () => { - storeClickMetrics({ selector, meta: proposition.getMeta() }); - }); + storeClickMetrics({ selector, meta: proposition.getItemMeta(index) }); + proposition.addRenderer(index, () => undefined); } else if (modules[type]) { proposition.includeInDisplayNotification(); const processedData = preprocess(data); diff --git a/src/components/Personalization/handlers/createHtmlContentHandler.js b/src/components/Personalization/handlers/createHtmlContentHandler.js index bc0fab6e9..47f3d028d 100644 --- a/src/components/Personalization/handlers/createHtmlContentHandler.js +++ b/src/components/Personalization/handlers/createHtmlContentHandler.js @@ -42,41 +42,3 @@ export default ({ next, modules, preprocess }) => proposition => { next(proposition); } }; - -/* -import { assign } from "../../utils"; - -export const createViewCacheManager = () => { - const viewStorage = {}; - let storeViewsCalledAtLeastOnce = false; - let previousStoreViewsComplete = Promise.resolve(); - - const storeViews = viewTypeHandlesPromise => { - storeViewsCalledAtLeastOnce = true; - previousStoreViewsComplete = previousStoreViewsComplete - .then(() => viewTypeHandlesPromise) - .then(viewTypeHandles => { - const decisions = viewTypeHandles.reduce((handle, memo) => { - const { scope } = handle; - memo[scope] = memo[scope] || []; - memo[scope].push(handle); - }, {}); - assign(viewStorage, decisions); - }) - .catch(() => {}); - }; - - const getView = viewName => { - return previousStoreViewsComplete.then(() => viewStorage[viewName] || []); - }; - - const isInitialized = () => { - return storeViewsCalledAtLeastOnce; - }; - return { - storeViews, - getView, - isInitialized - }; -}; -*/ diff --git a/src/components/Personalization/handlers/proposition.js b/src/components/Personalization/handlers/proposition.js index e37f033d9..c0e29babf 100644 --- a/src/components/Personalization/handlers/proposition.js +++ b/src/components/Personalization/handlers/proposition.js @@ -10,27 +10,28 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -const renderWithLogging = async (renderer, logger) => { - try { - await renderer(); - if (logger.enabled) { - const details = JSON.stringify(processedAction); - logger.info(`Action ${details} executed.`); - } - return true; - } catch (e) { - if (logger.enabled) { - const details = JSON.stringify(processedAction); - const { message, stack } = error; - const errorMessage = `Failed to execute action ${details}. ${message} ${ - stack ? `\n ${stack}` : "" - }`; - logger.error(errorMessage); - } - return false; - } -} - +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; @@ -48,8 +49,14 @@ export const createProposition = (handle, isApplyPropositions = false) => { getHandle() { return handle; }, - getMeta() { - return { id, scope, scopeDetails }; + 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; @@ -63,7 +70,7 @@ export const createProposition = (handle, isApplyPropositions = false) => { }, addRenderer(itemIndex, renderer) { itemsRenderAttempted[itemIndex] = true; - renderers.push(renderer); + renderers.push([itemIndex, renderer]); }, includeInDisplayNotification() { includeInDisplayNotification = true; @@ -73,7 +80,9 @@ export const createProposition = (handle, isApplyPropositions = false) => { }, render(logger) { return Promise.all( - renderers.map(renderer => renderWithLogging(renderer, logger)) + 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 diff --git a/src/components/Personalization/index.js b/src/components/Personalization/index.js index 220a618c1..310648fe4 100644 --- a/src/components/Personalization/index.js +++ b/src/components/Personalization/index.js @@ -31,7 +31,10 @@ import createHtmlContentHandler from "./handlers/createHtmlContentHandler"; import createDomActionHandler from "./handlers/createDomActionHandler"; import createMeasurementSchemaHandler from "./handlers/createMeasurementSchemaHandler"; import createRender from "./handlers/createRender"; -import { isPageWideSurface } from "./utils/surfaceUtils"; +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,15 +46,17 @@ const createPersonalization = ({ config, logger, eventManager }) => { storeClickMetrics } = createClickStorage(); const getPageLocation = createGetPageLocation({ window }); - const viewCache = createViewCacheManager(); + const viewCache = createViewCacheManager({ createProposition }); const modules = initDomActionsModules(); + const preprocess = createPreprocess([remapHeadOffers, remapCustomCodeOffers]); + const noOpHandler = () => undefined; const domActionHandler = createDomActionHandler({ next: noOpHandler, - isPageWideSurface, modules, - storeClickMetrics + storeClickMetrics, + preprocess }); const measurementSchemaHandler = createMeasurementSchemaHandler({ next: domActionHandler @@ -61,7 +66,8 @@ const createPersonalization = ({ config, logger, eventManager }) => { }); const htmlContentHandler = createHtmlContentHandler({ next: redirectHandler, - modules + modules, + preprocess }); const render = createRender({ 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 b864ff3cd..37d468032 100644 --- a/test/functional/specs/Personalization/C6364798.js +++ b/test/functional/specs/Personalization/C6364798.js @@ -124,26 +124,14 @@ const simulatePageLoad = async alloy => { personalizationPayload, PAGE_WIDE_SCOPE ); - console.log(JSON.stringify(pageWideScopeDecisionsMeta, null, 2)); - await t.debug(); - 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" @@ -151,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 ); @@ -183,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); @@ -232,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) => { @@ -271,10 +279,10 @@ 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.only("Test C6364798: applyPropositions should re-render SPA view without sending view notifications", async () => { +test("Test C6364798: applyPropositions should re-render SPA view without sending view notifications", async () => { const alloy = createAlloyProxy(); await alloy.configure(config); 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 e7486bd0b..0d3963560 100644 --- a/test/unit/specs/components/Personalization/createApplyPropositions.spec.js +++ b/test/unit/specs/components/Personalization/createApplyPropositions.spec.js @@ -45,12 +45,11 @@ describe("Personalization::createApplyPropositions", () => { render }); - return applyPropositions({ + const result = applyPropositions({ propositions: [] - }).then(result => { - expect(result).toEqual({ propositions: [] }); - expect(render).toHaveBeenCalledOnceWith([]); }); + expect(result).toEqual({ propositions: [] }); + expect(render).toHaveBeenCalledOnceWith([]); }); it("it should apply user-provided dom-action schema propositions", () => { @@ -65,20 +64,20 @@ describe("Personalization::createApplyPropositions", () => { render }); - return applyPropositions({ + const result = applyPropositions({ propositions: PAGE_WIDE_SCOPE_DECISIONS - }).then(result => { - 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); - }); + 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); }); }); @@ -87,24 +86,24 @@ describe("Personalization::createApplyPropositions", () => { render }); - return applyPropositions({ + const { propositions } = applyPropositions({ propositions: MIXED_PROPOSITIONS, metadata: METADATA - }).then(({ propositions }) => { - 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); }); + + 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", () => { @@ -137,26 +136,25 @@ describe("Personalization::createApplyPropositions", () => { const applyPropositions = createApplyPropositions({ render }); - 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", () => { const applyPropositions = createApplyPropositions({ render }); - return applyPropositions({ + const result = applyPropositions({ propositions: MIXED_PROPOSITIONS - }).then(result => { - expect(result.propositions.length).toEqual(3); - result.propositions.forEach(proposition => { - expect(proposition.renderAttempted).toBeTrue(); - }); + }); + expect(result.propositions.length).toEqual(3); + result.propositions.forEach(proposition => { + expect(proposition.renderAttempted).toBeTrue(); }); }); @@ -165,18 +163,17 @@ describe("Personalization::createApplyPropositions", () => { const propositions = JSON.parse(JSON.stringify(MIXED_PROPOSITIONS)); propositions[4].renderAttempted = true; - return applyPropositions({ + const result = applyPropositions({ 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"); + } }); }); @@ -185,15 +182,14 @@ describe("Personalization::createApplyPropositions", () => { const applyPropositions = createApplyPropositions({ render }); - return applyPropositions({ + const { propositions } = applyPropositions({ propositions: MIXED_PROPOSITIONS - }).then(({ propositions }) => { - expect(propositions.length).toEqual(3); - propositions.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); }); }); }); @@ -202,22 +198,22 @@ describe("Personalization::createApplyPropositions", () => { 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(4); }); + + 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/createFetchDataHandler.spec.js b/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js index ceed93fce..d9d1828e7 100644 --- a/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js +++ b/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js @@ -10,12 +10,11 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import { de } from "date-fns/locale"; import createFetchDataHandler from "../../../../../src/components/Personalization/createFetchDataHandler"; +import { createProposition } from "../../../../../src/components/Personalization/handlers/proposition"; import flushPromiseChains from "../../../helpers/flushPromiseChains"; describe("Personalization::createFetchDataHandler", () => { - let prehidingStyle; let hideContainers; let mergeQuery; @@ -67,7 +66,7 @@ describe("Personalization::createFetchDataHandler", () => { 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); @@ -104,16 +103,25 @@ describe("Personalization::createFetchDataHandler", () => { return Promise.resolve(decisionsMeta); }; run(); - response.getPayloadsByType.and.returnValue([{ id: "handle1" }, { id: "handle2" }]); - cacheUpdate.update.and.returnValue([{ id: "handle1", items: ["item1"] }]); + 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 }], + propositions: [ + { id: "handle1", items: ["item1"], renderAttempted: true } + ], decisions: [] }); await flushPromiseChains(); expect(collect).toHaveBeenCalledOnceWith({ - decisionsMeta: [{ id: "handle1", scope: undefined, scopeDetails: undefined }], + decisionsMeta: [ + { id: "handle1", scope: undefined, scopeDetails: undefined } + ], viewName: "myviewname" }); }); diff --git a/test/unit/specs/components/Personalization/createViewCacheManager.spec.js b/test/unit/specs/components/Personalization/createViewCacheManager.spec.js index bfe9d24f7..a5cf73290 100644 --- a/test/unit/specs/components/Personalization/createViewCacheManager.spec.js +++ b/test/unit/specs/components/Personalization/createViewCacheManager.spec.js @@ -13,7 +13,6 @@ governing permissions and limitations under the License. import createViewCacheManager from "../../../../../src/components/Personalization/createViewCacheManager"; describe("Personalization::createCacheManager", () => { - const viewHandles = [ { id: "foo1", @@ -48,30 +47,42 @@ describe("Personalization::createCacheManager", () => { } ]; + let createProposition; + + 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(); + const viewCacheManager = createViewCacheManager({ createProposition }); const cacheUpdate = viewCacheManager.createCacheUpdate("home"); const resultingHandles = cacheUpdate.update(viewHandles); - expect(resultingHandles).toEqual([ + expect(resultingHandles.map(h => h.getHandle())).toEqual([ viewHandles[0], viewHandles[1], viewHandles[3] ]); const homeViews = await viewCacheManager.getView("home"); - expect(homeViews).toEqual([ + expect(homeViews.map(h => h.getHandle())).toEqual([ viewHandles[0], viewHandles[1] ]); const cartViews = await viewCacheManager.getView("cart"); - expect(cartViews).toEqual([ - viewHandles[2] - ]); + expect(cartViews.map(h => h.getHandle())).toEqual([viewHandles[2]]); const otherViews = await viewCacheManager.getView("other"); - expect(otherViews).toEqual([ + expect(otherViews.map(h => h.getHandle())).toEqual([ { scope: "other", scopeDetails: { @@ -84,12 +95,12 @@ describe("Personalization::createCacheManager", () => { }); it("should be no views when decisions deferred is rejected", async () => { - const viewCacheManager = createViewCacheManager(); + const viewCacheManager = createViewCacheManager({ createProposition }); const cacheUpdate = viewCacheManager.createCacheUpdate("home"); cacheUpdate.cancel(); const homeViews = await viewCacheManager.getView("home"); - expect(homeViews).toEqual([ + expect(homeViews.map(h => h.getHandle())).toEqual([ { scope: "home", scopeDetails: { @@ -102,12 +113,12 @@ describe("Personalization::createCacheManager", () => { }); it("should not be initialized when first created", () => { - const viewCacheManager = createViewCacheManager(); + const viewCacheManager = createViewCacheManager({ createProposition }); expect(viewCacheManager.isInitialized()).toBe(false); }); it("should be initialized when first cache update is created", () => { - const viewCacheManager = createViewCacheManager(); + 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 3bb3e95c7..2d070c070 100644 --- a/test/unit/specs/components/Personalization/createViewChangeHandler.spec.js +++ b/test/unit/specs/components/Personalization/createViewChangeHandler.spec.js @@ -13,6 +13,7 @@ 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 mergeDecisionsMeta; @@ -51,7 +52,9 @@ describe("Personalization::createViewChangeHandler", () => { }; it("should trigger render if renderDecisions is true", async () => { - viewCache.getView.and.returnValue(Promise.resolve(CART_VIEW_DECISIONS)); + 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")); @@ -66,7 +69,7 @@ describe("Personalization::createViewChangeHandler", () => { ); 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) diff --git a/test/unit/specs/components/Personalization/topLevel/buildAlloy.js b/test/unit/specs/components/Personalization/topLevel/buildAlloy.js index 3f726446c..661d1e7a9 100644 --- a/test/unit/specs/components/Personalization/topLevel/buildAlloy.js +++ b/test/unit/specs/components/Personalization/topLevel/buildAlloy.js @@ -33,6 +33,7 @@ import createMeasurementSchemaHandler from "../../../../../../src/components/Per 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); @@ -81,7 +82,7 @@ const buildComponent = ({ storeClickMetrics } = createClickStorage(); - const viewCache = createViewCacheManager(); + const viewCache = createViewCacheManager({ createProposition }); const modules = initDomActionsModulesMocks(); const noOpHandler = () => undefined; From 8a97a0bc0eaf998a3858f7d6ab40816759b43d14 Mon Sep 17 00:00:00 2001 From: Jon Snyder Date: Wed, 23 Aug 2023 09:50:47 -0600 Subject: [PATCH 09/20] Remove fetch related features, only including refactor and combining notifications --- .../DataCollector/validateUserEventOptions.js | 4 +-- .../createViewChangeHandler.js | 35 ++++++++++--------- .../Personalization/utils/createAsyncArray.js | 21 ----------- src/core/createEvent.js | 30 ++-------------- src/utils/deduplicateArray.js | 16 --------- src/utils/index.js | 1 - 6 files changed, 21 insertions(+), 86 deletions(-) delete mode 100644 src/components/Personalization/utils/createAsyncArray.js delete mode 100644 src/utils/deduplicateArray.js diff --git a/src/components/DataCollector/validateUserEventOptions.js b/src/components/DataCollector/validateUserEventOptions.js index be5e82dbe..9ba6e320c 100644 --- a/src/components/DataCollector/validateUserEventOptions.js +++ b/src/components/DataCollector/validateUserEventOptions.js @@ -30,9 +30,7 @@ export default ({ options }) => { decisionScopes: arrayOf(string()).uniqueItems(), personalization: objectOf({ decisionScopes: arrayOf(string()).uniqueItems(), - surfaces: arrayOf(string()).uniqueItems(), - sendDisplayNotifications: boolean().default(true), - includePendingDisplayNotifications: boolean().default(false) + surfaces: arrayOf(string()).uniqueItems() }), datasetId: string(), mergeId: string(), diff --git a/src/components/Personalization/createViewChangeHandler.js b/src/components/Personalization/createViewChangeHandler.js index 500732be7..aa7394731 100644 --- a/src/components/Personalization/createViewChangeHandler.js +++ b/src/components/Personalization/createViewChangeHandler.js @@ -20,23 +20,24 @@ export default ({ mergeDecisionsMeta, render, viewCache }) => { return ({ personalizationDetails, event, onResponse }) => { const viewName = personalizationDetails.getViewName(); - return viewCache - .getView(viewName) - .then(propositions => { - onResponse(() => { - return { - propositions: buildReturnedPropositions(propositions), - decisions: buildReturnedDecisions(propositions) - }; - }); - - if (personalizationDetails.isRenderDecisions()) { - return render(propositions); - } - return Promise.resolve([]); - }) - .then(decisionsMeta => { - mergeDecisionsMeta(event, decisionsMeta, PropositionEventType.DISPLAY); + return viewCache.getView(viewName).then(propositions => { + onResponse(() => { + return { + propositions: buildReturnedPropositions(propositions), + decisions: buildReturnedDecisions(propositions) + }; }); + + if (personalizationDetails.isRenderDecisions()) { + return render(propositions).then(decisionsMeta => { + mergeDecisionsMeta( + event, + decisionsMeta, + PropositionEventType.DISPLAY + ); + }); + } + return Promise.resolve(); + }); }; }; diff --git a/src/components/Personalization/utils/createAsyncArray.js b/src/components/Personalization/utils/createAsyncArray.js deleted file mode 100644 index 7a42d2fec..000000000 --- a/src/components/Personalization/utils/createAsyncArray.js +++ /dev/null @@ -1,21 +0,0 @@ -export default () => { - let latest = Promise.resolve([]); - return { - add(promise) { - latest = latest.then(existingPropositions => { - return promise - .then(newPropositions => { - return existingPropositions.concat(newPropositions); - }) - .catch(() => { - return existingPropositions; - }); - }); - }, - clear() { - const oldLatest = latest; - latest = Promise.resolve([]); - return oldLatest; - } - }; -}; diff --git a/src/core/createEvent.js b/src/core/createEvent.js index 798c63b16..6bf98131e 100644 --- a/src/core/createEvent.js +++ b/src/core/createEvent.js @@ -10,12 +10,7 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import { - isEmptyObject, - deepAssign, - isNonEmptyArray, - deduplicateArray -} from "../utils"; +import { isEmptyObject, deepAssign } from "../utils"; export default () => { const content = {}; @@ -69,28 +64,7 @@ export default () => { } if (userXdm) { - // Merge the userXDM propositions with the ones included via the display - // notification cache. - if ( - userXdm._experience && - userXdm._experience.decisioning && - isNonEmptyArray(userXdm._experience.decisioning.propositions) && - content.xdm._experience && - content.xdm._experience.decisioning && - isNonEmptyArray(content.xdm._experience.decisioning.propositions) - ) { - const newPropositions = deduplicateArray( - [ - ...userXdm._experience.decisioning.propositions, - ...content.xdm._experience.decisioning.propositions - ], - (a, b) => a === b || (a.id && b.id && a.id === b.id) - ); - event.mergeXdm(userXdm); - content.xdm._experience.decisioning.propositions = newPropositions; - } else { - event.mergeXdm(userXdm); - } + event.mergeXdm(userXdm); } if (userData) { diff --git a/src/utils/deduplicateArray.js b/src/utils/deduplicateArray.js deleted file mode 100644 index e4fd4beca..000000000 --- a/src/utils/deduplicateArray.js +++ /dev/null @@ -1,16 +0,0 @@ -const REFERENCE_EQUALITY = (a, b) => a === b; - -const findIndex = (array, item, isEqual) => { - for (let i = 0; i < array.length; i++) { - if (isEqual(array[i], item)) { - return i; - } - } - return -1; -}; - -export default (array, isEqual = REFERENCE_EQUALITY) => { - return array.filter( - (item, index) => findIndex(array, item, isEqual) === index - ); -}; diff --git a/src/utils/index.js b/src/utils/index.js index c38851c2f..927e3a28b 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -22,7 +22,6 @@ export { default as createLoggingCookieJar } from "./createLoggingCookieJar"; export { default as createTaskQueue } from "./createTaskQueue"; export { default as crc32 } from "./crc32"; export { default as defer } from "./defer"; -export { default as deduplicateArray } from "./deduplicateArray"; export { default as deepAssign } from "./deepAssign"; export { default as endsWith } from "./endsWith"; export { default as find } from "./find"; From 76038af69df61f5ba23e948f39266b498c5758c0 Mon Sep 17 00:00:00 2001 From: Jon Snyder Date: Tue, 29 Aug 2023 08:12:46 -0600 Subject: [PATCH 10/20] Add unit tests for new handler files --- .../dom-actions/createPreprocess.spec.js | 40 +++ .../handlers/createDomActionHandler.spec.js | 164 ++++++++++++ .../handlers/createHtmlContentHandler.spec.js | 110 ++++++++ .../createMeasurementSchemaHandler.spec.js | 35 +++ .../handlers/createRedirectHandler.spec.js | 48 ++++ .../handlers/createRender.spec.js | 87 ++++++ .../handlers/proposition.spec.js | 250 ++++++++++++++++++ 7 files changed, 734 insertions(+) create mode 100644 test/unit/specs/components/Personalization/dom-actions/createPreprocess.spec.js create mode 100644 test/unit/specs/components/Personalization/handlers/createDomActionHandler.spec.js create mode 100644 test/unit/specs/components/Personalization/handlers/createHtmlContentHandler.spec.js create mode 100644 test/unit/specs/components/Personalization/handlers/createMeasurementSchemaHandler.spec.js create mode 100644 test/unit/specs/components/Personalization/handlers/createRender.spec.js create mode 100644 test/unit/specs/components/Personalization/handlers/proposition.spec.js 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/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 index c7dc10f52..b7fa3a280 100644 --- a/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js +++ b/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js @@ -75,4 +75,52 @@ describe("redirectHandler", () => { "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"]); + }); + }); +}); From 443745b55e7cfe7da8345622fed2a2f5b46ae5f8 Mon Sep 17 00:00:00 2001 From: Jon Snyder Date: Tue, 29 Aug 2023 08:16:48 -0600 Subject: [PATCH 11/20] Revert "Remove fetch related features, only including refactor and combining notifications" This reverts commit 8a97a0bc0eaf998a3858f7d6ab40816759b43d14. --- .../DataCollector/validateUserEventOptions.js | 4 ++- .../createViewChangeHandler.js | 35 +++++++++---------- .../Personalization/utils/createAsyncArray.js | 21 +++++++++++ src/core/createEvent.js | 30 ++++++++++++++-- src/utils/deduplicateArray.js | 16 +++++++++ src/utils/index.js | 1 + 6 files changed, 86 insertions(+), 21 deletions(-) create mode 100644 src/components/Personalization/utils/createAsyncArray.js create mode 100644 src/utils/deduplicateArray.js diff --git a/src/components/DataCollector/validateUserEventOptions.js b/src/components/DataCollector/validateUserEventOptions.js index 9ba6e320c..be5e82dbe 100644 --- a/src/components/DataCollector/validateUserEventOptions.js +++ b/src/components/DataCollector/validateUserEventOptions.js @@ -30,7 +30,9 @@ export default ({ options }) => { decisionScopes: arrayOf(string()).uniqueItems(), personalization: objectOf({ decisionScopes: arrayOf(string()).uniqueItems(), - surfaces: arrayOf(string()).uniqueItems() + surfaces: arrayOf(string()).uniqueItems(), + sendDisplayNotifications: boolean().default(true), + includePendingDisplayNotifications: boolean().default(false) }), datasetId: string(), mergeId: string(), diff --git a/src/components/Personalization/createViewChangeHandler.js b/src/components/Personalization/createViewChangeHandler.js index aa7394731..500732be7 100644 --- a/src/components/Personalization/createViewChangeHandler.js +++ b/src/components/Personalization/createViewChangeHandler.js @@ -20,24 +20,23 @@ export default ({ mergeDecisionsMeta, render, viewCache }) => { return ({ personalizationDetails, event, onResponse }) => { const viewName = personalizationDetails.getViewName(); - return viewCache.getView(viewName).then(propositions => { - onResponse(() => { - return { - propositions: buildReturnedPropositions(propositions), - decisions: buildReturnedDecisions(propositions) - }; - }); - - if (personalizationDetails.isRenderDecisions()) { - return render(propositions).then(decisionsMeta => { - mergeDecisionsMeta( - event, - decisionsMeta, - PropositionEventType.DISPLAY - ); + return viewCache + .getView(viewName) + .then(propositions => { + onResponse(() => { + return { + propositions: buildReturnedPropositions(propositions), + decisions: buildReturnedDecisions(propositions) + }; }); - } - return Promise.resolve(); - }); + + if (personalizationDetails.isRenderDecisions()) { + return render(propositions); + } + return Promise.resolve([]); + }) + .then(decisionsMeta => { + mergeDecisionsMeta(event, decisionsMeta, PropositionEventType.DISPLAY); + }); }; }; diff --git a/src/components/Personalization/utils/createAsyncArray.js b/src/components/Personalization/utils/createAsyncArray.js new file mode 100644 index 000000000..7a42d2fec --- /dev/null +++ b/src/components/Personalization/utils/createAsyncArray.js @@ -0,0 +1,21 @@ +export default () => { + let latest = Promise.resolve([]); + return { + add(promise) { + latest = latest.then(existingPropositions => { + return promise + .then(newPropositions => { + return existingPropositions.concat(newPropositions); + }) + .catch(() => { + return existingPropositions; + }); + }); + }, + clear() { + const oldLatest = latest; + latest = Promise.resolve([]); + return oldLatest; + } + }; +}; diff --git a/src/core/createEvent.js b/src/core/createEvent.js index 6bf98131e..798c63b16 100644 --- a/src/core/createEvent.js +++ b/src/core/createEvent.js @@ -10,7 +10,12 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import { isEmptyObject, deepAssign } from "../utils"; +import { + isEmptyObject, + deepAssign, + isNonEmptyArray, + deduplicateArray +} from "../utils"; export default () => { const content = {}; @@ -64,7 +69,28 @@ export default () => { } if (userXdm) { - event.mergeXdm(userXdm); + // Merge the userXDM propositions with the ones included via the display + // notification cache. + if ( + userXdm._experience && + userXdm._experience.decisioning && + isNonEmptyArray(userXdm._experience.decisioning.propositions) && + content.xdm._experience && + content.xdm._experience.decisioning && + isNonEmptyArray(content.xdm._experience.decisioning.propositions) + ) { + const newPropositions = deduplicateArray( + [ + ...userXdm._experience.decisioning.propositions, + ...content.xdm._experience.decisioning.propositions + ], + (a, b) => a === b || (a.id && b.id && a.id === b.id) + ); + event.mergeXdm(userXdm); + content.xdm._experience.decisioning.propositions = newPropositions; + } else { + event.mergeXdm(userXdm); + } } if (userData) { diff --git a/src/utils/deduplicateArray.js b/src/utils/deduplicateArray.js new file mode 100644 index 000000000..e4fd4beca --- /dev/null +++ b/src/utils/deduplicateArray.js @@ -0,0 +1,16 @@ +const REFERENCE_EQUALITY = (a, b) => a === b; + +const findIndex = (array, item, isEqual) => { + for (let i = 0; i < array.length; i++) { + if (isEqual(array[i], item)) { + return i; + } + } + return -1; +}; + +export default (array, isEqual = REFERENCE_EQUALITY) => { + return array.filter( + (item, index) => findIndex(array, item, isEqual) === index + ); +}; diff --git a/src/utils/index.js b/src/utils/index.js index 927e3a28b..c38851c2f 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -22,6 +22,7 @@ export { default as createLoggingCookieJar } from "./createLoggingCookieJar"; export { default as createTaskQueue } from "./createTaskQueue"; export { default as crc32 } from "./crc32"; export { default as defer } from "./defer"; +export { default as deduplicateArray } from "./deduplicateArray"; export { default as deepAssign } from "./deepAssign"; export { default as endsWith } from "./endsWith"; export { default as find } from "./find"; From aa0474a9270652776a0e083853a3b1d4b520a393 Mon Sep 17 00:00:00 2001 From: Jon Snyder Date: Tue, 29 Aug 2023 15:13:52 -0600 Subject: [PATCH 12/20] Save display notifications and send them at the bottom of page hit --- .../createApplyPropositions.js | 4 +- .../Personalization/createComponent.js | 32 +++++++++----- .../Personalization/createFetchDataHandler.js | 29 ++++++++---- .../createPendingNotificationsHandler.js | 20 +++++++++ .../createPersonalizationDetails.js | 3 ++ src/components/Personalization/index.js | 13 +++++- .../Personalization/utils/createAsyncArray.js | 18 +++++++- src/core/createEvent.js | 44 +++++++++---------- .../createApplyPropositions.spec.js | 39 ++++++++++++---- .../createFetchDataHandler.spec.js | 13 +++++- .../createPendingNotificationsHandler.spec.js | 34 ++++++++++++++ .../Personalization/topLevel/buildAlloy.js | 18 ++++++-- .../utils/createAsyncArray.spec.js | 35 +++++++++++++++ .../unit/specs/utils/deduplicateArray.spec.js | 33 ++++++++++++++ 14 files changed, 273 insertions(+), 62 deletions(-) create mode 100644 src/components/Personalization/createPendingNotificationsHandler.js create mode 100644 test/unit/specs/components/Personalization/createPendingNotificationsHandler.spec.js create mode 100644 test/unit/specs/components/Personalization/utils/createAsyncArray.spec.js create mode 100644 test/unit/specs/utils/deduplicateArray.spec.js diff --git a/src/components/Personalization/createApplyPropositions.js b/src/components/Personalization/createApplyPropositions.js index 15b00afb1..3a209f122 100644 --- a/src/components/Personalization/createApplyPropositions.js +++ b/src/components/Personalization/createApplyPropositions.js @@ -20,7 +20,7 @@ import { const SUPPORTED_SCHEMAS = [DOM_ACTION, HTML_CONTENT_ITEM]; -export default ({ render }) => { +export default ({ render, pendingDisplayNotifications }) => { const filterItemsPredicate = item => SUPPORTED_SCHEMAS.indexOf(item.schema) > -1; @@ -79,7 +79,7 @@ export default ({ render }) => { metadata }).map(proposition => createProposition(proposition, true)); - render(propositionsToExecute); + pendingDisplayNotifications.concat(render(propositionsToExecute)); return { propositions: buildReturnedPropositions(propositionsToExecute) diff --git a/src/components/Personalization/createComponent.js b/src/components/Personalization/createComponent.js index 226db3def..f7d305715 100644 --- a/src/components/Personalization/createComponent.js +++ b/src/components/Personalization/createComponent.js @@ -26,7 +26,8 @@ export default ({ viewCache, showContainers, applyPropositions, - setTargetMigration + setTargetMigration, + pendingNotificationsHandler }) => { return { lifecycle: { @@ -51,7 +52,7 @@ export default ({ // If we are in authoring mode we disable personalization mergeQuery(event, { enabled: false }); - return; + return Promise.resolve(); } const personalizationDetails = createPersonalizationDetails({ @@ -64,6 +65,11 @@ export default ({ logger }); + const handlerPromises = []; + if (personalizationDetails.shouldAddPendingDisplayNotifications()) { + handlerPromises.push(pendingNotificationsHandler({ event })); + } + if (personalizationDetails.shouldFetchData()) { const cacheUpdate = viewCache.createCacheUpdate( personalizationDetails.getViewName() @@ -76,18 +82,20 @@ export default ({ event, onResponse }); - return; - } - - if (personalizationDetails.shouldUseCachedData()) { + } else if (personalizationDetails.shouldUseCachedData()) { // eslint-disable-next-line consistent-return - return viewChangeHandler({ - personalizationDetails, - event, - onResponse, - onRequestFailure - }); + handlerPromises.push( + viewChangeHandler({ + personalizationDetails, + event, + onResponse, + onRequestFailure + }) + ); } + // We can wait for personalization to be applied and for + // the fetch data request to complete in parallel. + return Promise.all(handlerPromises); }, onClick({ event, clickedElement }) { onClickHandler({ event, clickedElement }); diff --git a/src/components/Personalization/createFetchDataHandler.js b/src/components/Personalization/createFetchDataHandler.js index 6bf75f953..05c27b088 100644 --- a/src/components/Personalization/createFetchDataHandler.js +++ b/src/components/Personalization/createFetchDataHandler.js @@ -9,6 +9,7 @@ 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 { defer } from "../../utils"; import { buildReturnedPropositions, buildReturnedDecisions @@ -21,7 +22,8 @@ export default ({ hideContainers, mergeQuery, collect, - render + render, + pendingDisplayNotifications }) => { return ({ cacheUpdate, personalizationDetails, event, onResponse }) => { if (personalizationDetails.isRenderDecisions()) { @@ -29,18 +31,27 @@ export default ({ } mergeQuery(event, personalizationDetails.createQueryDetails()); + let handleNotifications; + if (personalizationDetails.isSendDisplayNotifications()) { + handleNotifications = decisionsMeta => { + if (decisionsMeta.length > 0) { + collect({ + decisionsMeta, + viewName: personalizationDetails.getViewName() + }); + } + }; + } else { + const displayNotificationsDeferred = defer(); + pendingDisplayNotifications.concat(displayNotificationsDeferred.promise); + handleNotifications = displayNotificationsDeferred.resolve; + } + onResponse(({ response }) => { const handles = response.getPayloadsByType(DECISIONS_HANDLE); const propositions = cacheUpdate.update(handles); if (personalizationDetails.isRenderDecisions()) { - render(propositions).then(decisionsMeta => { - if (decisionsMeta.length > 0) { - collect({ - decisionsMeta, - viewName: personalizationDetails.getViewName() - }); - } - }); + render(propositions).then(handleNotifications); } return { diff --git a/src/components/Personalization/createPendingNotificationsHandler.js b/src/components/Personalization/createPendingNotificationsHandler.js new file mode 100644 index 000000000..c161a8024 --- /dev/null +++ b/src/components/Personalization/createPendingNotificationsHandler.js @@ -0,0 +1,20 @@ +/* +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 { PropositionEventType } from "./constants/propositionEventType"; + +export default ({ pendingDisplayNotifications, mergeDecisionsMeta }) => ({ + event +}) => { + return pendingDisplayNotifications.clear().then(decisionsMeta => { + mergeDecisionsMeta(event, decisionsMeta, PropositionEventType.DISPLAY); + }); +}; diff --git a/src/components/Personalization/createPersonalizationDetails.js b/src/components/Personalization/createPersonalizationDetails.js index b43e05440..5188e469b 100644 --- a/src/components/Personalization/createPersonalizationDetails.js +++ b/src/components/Personalization/createPersonalizationDetails.js @@ -54,6 +54,9 @@ export default ({ isSendDisplayNotifications() { return !!personalization.sendDisplayNotifications; }, + shouldAddPendingDisplayNotifications() { + return !!personalization.includePendingDisplayNotifications; + }, getViewName() { return viewName; }, diff --git a/src/components/Personalization/index.js b/src/components/Personalization/index.js index 310648fe4..4a72914c3 100644 --- a/src/components/Personalization/index.js +++ b/src/components/Personalization/index.js @@ -35,6 +35,8 @@ import remapCustomCodeOffers from "./dom-actions/remapCustomCodeOffers"; import remapHeadOffers from "./dom-actions/remapHeadOffers"; import createPreprocess from "./dom-actions/createPreprocess"; import { createProposition } from "./handlers/proposition"; +import createAsyncArray from "./utils/createAsyncArray"; +import createPendingNotificationsHandler from "./createPendingNotificationsHandler"; const createPersonalization = ({ config, logger, eventManager }) => { const { targetMigrationEnabled, prehidingStyle } = config; @@ -76,12 +78,18 @@ const createPersonalization = ({ config, logger, eventManager }) => { executeRedirect: url => window.location.replace(url), logger }); + const pendingDisplayNotifications = createAsyncArray(); + const pendingNotificationsHandler = createPendingNotificationsHandler({ + pendingDisplayNotifications, + mergeDecisionsMeta + }); const fetchDataHandler = createFetchDataHandler({ prehidingStyle, hideContainers, mergeQuery, collect, - render + render, + pendingDisplayNotifications }); const onClickHandler = createOnClickHandler({ mergeDecisionsMeta, @@ -111,7 +119,8 @@ const createPersonalization = ({ config, logger, eventManager }) => { viewCache, showContainers, applyPropositions, - setTargetMigration + setTargetMigration, + pendingNotificationsHandler }); }; diff --git a/src/components/Personalization/utils/createAsyncArray.js b/src/components/Personalization/utils/createAsyncArray.js index 7a42d2fec..c736b8853 100644 --- a/src/components/Personalization/utils/createAsyncArray.js +++ b/src/components/Personalization/utils/createAsyncArray.js @@ -1,7 +1,18 @@ +/* +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 () => { let latest = Promise.resolve([]); return { - add(promise) { + concat(promise) { latest = latest.then(existingPropositions => { return promise .then(newPropositions => { @@ -12,6 +23,11 @@ export default () => { }); }); }, + /** + * Clears the saved propositions, waiting until the next propositions are resolved and available. + * + * @returns {Promise} A promise that resolves to the latest propositions. + */ clear() { const oldLatest = latest; latest = Promise.resolve([]); diff --git a/src/core/createEvent.js b/src/core/createEvent.js index 798c63b16..146a71790 100644 --- a/src/core/createEvent.js +++ b/src/core/createEvent.js @@ -17,6 +17,19 @@ import { deduplicateArray } from "../utils"; +const getXdmPropositions = xdm => { + return xdm && + // eslint-disable-next-line no-underscore-dangle + xdm._experience && + // eslint-disable-next-line no-underscore-dangle + xdm._experience.decisioning && + // eslint-disable-next-line no-underscore-dangle + isNonEmptyArray(xdm._experience.decisioning.propositions) + ? // eslint-disable-next-line no-underscore-dangle + xdm._experience.decisioning.propositions + : []; +}; + export default () => { const content = {}; let userXdm; @@ -68,29 +81,16 @@ export default () => { return; } + const newPropositions = deduplicateArray( + [...getXdmPropositions(userXdm), ...getXdmPropositions(content.xdm)], + (a, b) => a === b || (a.id && b.id && a.id === b.id) + ); if (userXdm) { - // Merge the userXDM propositions with the ones included via the display - // notification cache. - if ( - userXdm._experience && - userXdm._experience.decisioning && - isNonEmptyArray(userXdm._experience.decisioning.propositions) && - content.xdm._experience && - content.xdm._experience.decisioning && - isNonEmptyArray(content.xdm._experience.decisioning.propositions) - ) { - const newPropositions = deduplicateArray( - [ - ...userXdm._experience.decisioning.propositions, - ...content.xdm._experience.decisioning.propositions - ], - (a, b) => a === b || (a.id && b.id && a.id === b.id) - ); - event.mergeXdm(userXdm); - content.xdm._experience.decisioning.propositions = newPropositions; - } else { - event.mergeXdm(userXdm); - } + event.mergeXdm(userXdm); + } + if (newPropositions.length > 0) { + // eslint-disable-next-line no-underscore-dangle + content.xdm._experience.decisioning.propositions = newPropositions; } if (userData) { diff --git a/test/unit/specs/components/Personalization/createApplyPropositions.spec.js b/test/unit/specs/components/Personalization/createApplyPropositions.spec.js index 0d3963560..e34126cd3 100644 --- a/test/unit/specs/components/Personalization/createApplyPropositions.spec.js +++ b/test/unit/specs/components/Personalization/createApplyPropositions.spec.js @@ -27,6 +27,7 @@ const METADATA = { describe("Personalization::createApplyPropositions", () => { let render; + let pendingDisplayNotifications; beforeEach(() => { render = jasmine.createSpy("render"); @@ -38,11 +39,16 @@ describe("Personalization::createApplyPropositions", () => { }); }); }); + pendingDisplayNotifications = jasmine.createSpyObj( + "pendingDisplayNotifications", + ["concat"] + ); }); it("it should return an empty propositions promise if propositions is empty array", () => { const applyPropositions = createApplyPropositions({ - render + render, + pendingDisplayNotifications }); const result = applyPropositions({ @@ -61,7 +67,8 @@ describe("Personalization::createApplyPropositions", () => { }); const applyPropositions = createApplyPropositions({ - render + render, + pendingDisplayNotifications }); const result = applyPropositions({ @@ -83,7 +90,8 @@ describe("Personalization::createApplyPropositions", () => { it("it should merge metadata with propositions that have html-content-item schema", () => { const applyPropositions = createApplyPropositions({ - render + render, + pendingDisplayNotifications }); const { propositions } = applyPropositions({ @@ -134,7 +142,10 @@ describe("Personalization::createApplyPropositions", () => { } ]; - const applyPropositions = createApplyPropositions({ render }); + const applyPropositions = createApplyPropositions({ + render, + pendingDisplayNotifications + }); const result = applyPropositions({ propositions @@ -147,7 +158,10 @@ describe("Personalization::createApplyPropositions", () => { }); it("it should return renderAttempted = true on resulting propositions", () => { - const applyPropositions = createApplyPropositions({ render }); + const applyPropositions = createApplyPropositions({ + render, + pendingDisplayNotifications + }); const result = applyPropositions({ propositions: MIXED_PROPOSITIONS @@ -159,7 +173,10 @@ describe("Personalization::createApplyPropositions", () => { }); it("it should ignore propositions with __view__ scope that have already been rendered", () => { - const applyPropositions = createApplyPropositions({ render }); + const applyPropositions = createApplyPropositions({ + render, + pendingDisplayNotifications + }); const propositions = JSON.parse(JSON.stringify(MIXED_PROPOSITIONS)); propositions[4].renderAttempted = true; @@ -180,7 +197,10 @@ describe("Personalization::createApplyPropositions", () => { it("it should ignore items with unsupported schemas", () => { const expectedItemIds = ["442358", "442359"]; - const applyPropositions = createApplyPropositions({ render }); + const applyPropositions = createApplyPropositions({ + render, + pendingDisplayNotifications + }); const { propositions } = applyPropositions({ propositions: MIXED_PROPOSITIONS @@ -195,7 +215,10 @@ describe("Personalization::createApplyPropositions", () => { }); it("it should not mutate original propositions", () => { - const applyPropositions = createApplyPropositions({ render }); + const applyPropositions = createApplyPropositions({ + render, + pendingDisplayNotifications + }); const originalPropositions = clone(MIXED_PROPOSITIONS); const result = applyPropositions({ diff --git a/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js b/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js index d9d1828e7..72704a08f 100644 --- a/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js +++ b/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js @@ -20,6 +20,7 @@ describe("Personalization::createFetchDataHandler", () => { let mergeQuery; let collect; let render; + let pendingDisplayNotifications; let cacheUpdate; let personalizationDetails; @@ -34,13 +35,20 @@ describe("Personalization::createFetchDataHandler", () => { mergeQuery = jasmine.createSpy("mergeQuery"); collect = jasmine.createSpy("collect"); render = jasmine.createSpy("render"); + pendingDisplayNotifications = jasmine.createSpyObj( + "pendingDisplayNotifications", + ["concat"] + ); + cacheUpdate = jasmine.createSpyObj("cacheUpdate", ["update"]); personalizationDetails = jasmine.createSpyObj("personalizationDetails", [ "isRenderDecisions", "createQueryDetails", - "getViewName" + "getViewName", + "isSendDisplayNotifications" ]); personalizationDetails.createQueryDetails.and.returnValue("myquerydetails"); + personalizationDetails.isSendDisplayNotifications.and.returnValue(true); event = "myevent"; onResponse = jasmine.createSpy(); response = jasmine.createSpyObj("response", ["getPayloadsByType"]); @@ -52,7 +60,8 @@ describe("Personalization::createFetchDataHandler", () => { hideContainers, mergeQuery, collect, - render + render, + pendingDisplayNotifications }); fetchDataHandler({ cacheUpdate, diff --git a/test/unit/specs/components/Personalization/createPendingNotificationsHandler.spec.js b/test/unit/specs/components/Personalization/createPendingNotificationsHandler.spec.js new file mode 100644 index 000000000..b45029987 --- /dev/null +++ b/test/unit/specs/components/Personalization/createPendingNotificationsHandler.spec.js @@ -0,0 +1,34 @@ +import createPendingNotificationsHandler from "../../../../../src/components/Personalization/createPendingNotificationsHandler"; + +describe("Personalization::createPendingNotificationsHandler", () => { + let pendingDisplayNotifications; + let mergeDecisionsMeta; + let event; + let pendingNotificationsHandler; + + beforeEach(() => { + pendingDisplayNotifications = jasmine.createSpyObj( + "pendingDisplayNotifications", + ["clear"] + ); + mergeDecisionsMeta = jasmine.createSpy("mergeDecisionsMeta"); + event = "myevent"; + pendingNotificationsHandler = createPendingNotificationsHandler({ + pendingDisplayNotifications, + mergeDecisionsMeta + }); + }); + + it("should clear pending notifications and merge decisions meta", () => { + pendingDisplayNotifications.clear.and.returnValue( + Promise.resolve(["mymeta1", "mymeta2"]) + ); + return pendingNotificationsHandler({ event }).then(() => { + expect(mergeDecisionsMeta).toHaveBeenCalledOnceWith( + event, + ["mymeta1", "mymeta2"], + "display" + ); + }); + }); +}); diff --git a/test/unit/specs/components/Personalization/topLevel/buildAlloy.js b/test/unit/specs/components/Personalization/topLevel/buildAlloy.js index 661d1e7a9..85543ff01 100644 --- a/test/unit/specs/components/Personalization/topLevel/buildAlloy.js +++ b/test/unit/specs/components/Personalization/topLevel/buildAlloy.js @@ -34,6 +34,8 @@ import createRedirectHandler from "../../../../../../src/components/Personalizat import createHtmlContentHandler from "../../../../../../src/components/Personalization/handlers/createHtmlContentHandler"; import { isPageWideSurface } from "../../../../../../src/components/Personalization/utils/surfaceUtils"; import { createProposition } from "../../../../../../src/components/Personalization/handlers/proposition"; +import createAsyncArray from "../../../../../../src/components/Personalization/utils/createAsyncArray"; +import createPendingNotificationsHandler from "../../../../../../src/components/Personalization/createPendingNotificationsHandler"; const createAction = renderFunc => ({ selector, content }) => { renderFunc(selector, content); @@ -112,12 +114,18 @@ const buildComponent = ({ executeRedirect: url => window.location.replace(url), logger }); + const pendingDisplayNotifications = createAsyncArray(); + const pendingNotificationsHandler = createPendingNotificationsHandler({ + pendingDisplayNotifications, + mergeDecisionsMeta + }); const fetchDataHandler = createFetchDataHandler({ prehidingStyle, hideContainers, mergeQuery, collect, - render + render, + pendingDisplayNotifications }); const onClickHandler = createOnClickHandler({ mergeDecisionsMeta, @@ -131,7 +139,8 @@ const buildComponent = ({ viewCache }); const applyPropositions = createApplyPropositions({ - render + render, + pendingDisplayNotifications }); const setTargetMigration = createSetTargetMigration({ targetMigrationEnabled @@ -147,7 +156,8 @@ const buildComponent = ({ viewCache, showContainers, applyPropositions, - setTargetMigration + setTargetMigration, + pendingNotificationsHandler }); }; @@ -170,7 +180,7 @@ export default mocks => { event, renderDecisions, decisionScopes, - personalization, + personalization: personalization || { sendDisplayNotifications: true }, onResponse: callbacks.add }); const results = await callbacks.call({ response }); diff --git a/test/unit/specs/components/Personalization/utils/createAsyncArray.spec.js b/test/unit/specs/components/Personalization/utils/createAsyncArray.spec.js new file mode 100644 index 000000000..c7557a5ae --- /dev/null +++ b/test/unit/specs/components/Personalization/utils/createAsyncArray.spec.js @@ -0,0 +1,35 @@ +import createAsyncArray from "../../../../../../src/components/Personalization/utils/createAsyncArray"; +import { defer } from "../../../../../../src/utils"; +import flushPromiseChains from "../../../../helpers/flushPromiseChains"; + +describe("Personalization::utils::createAsyncArray", () => { + it("should start with an empty array", async () => { + const asyncArray = createAsyncArray(); + expect(await asyncArray.clear()).toEqual([]); + }); + + it("should add items to the array, and clear the items", async () => { + const asyncArray = createAsyncArray(); + await asyncArray.concat(Promise.resolve(["myitem1"])); + expect(await asyncArray.clear()).toEqual(["myitem1"]); + expect(await asyncArray.clear()).toEqual([]); + }); + + it("should add multiple arrays", async () => { + const asyncArray = createAsyncArray(); + await asyncArray.concat(Promise.resolve(["myitem1"])); + await asyncArray.concat(Promise.resolve(["myitem2"])); + expect(await asyncArray.clear()).toEqual(["myitem1", "myitem2"]); + }); + + it("should wait for items while clearing the array", async () => { + const asyncArray = createAsyncArray(); + const deferred = defer(); + asyncArray.concat(deferred.promise); + const clearPromise = asyncArray.clear(); + await flushPromiseChains(); + expectAsync(clearPromise).toBePending(); + deferred.resolve(["myitem1"]); + expect(await clearPromise).toEqual(["myitem1"]); + }); +}); diff --git a/test/unit/specs/utils/deduplicateArray.spec.js b/test/unit/specs/utils/deduplicateArray.spec.js new file mode 100644 index 000000000..f94b954a7 --- /dev/null +++ b/test/unit/specs/utils/deduplicateArray.spec.js @@ -0,0 +1,33 @@ +import { deduplicateArray } from "../../../../src/utils"; + +describe("deduplicateArray", () => { + it("should return an empty array if input is empty", () => { + expect(deduplicateArray([])).toEqual([]); + }); + + it("should return an array with one item if input has one item", () => { + const input = [1]; + expect(deduplicateArray(input)).toEqual(input); + }); + + it("should return an array with one item if input has two equal items", () => { + const input = [1, 1]; + expect(deduplicateArray(input)).toEqual([1]); + }); + + it("should return an array with two items if input has two different items", () => { + const input = [1, 2]; + expect(deduplicateArray(input)).toEqual(input); + }); + + it("should return an array with two items if input has three items with two equal items", () => { + const input = [1, 1, 2]; + expect(deduplicateArray(input)).toEqual([1, 2]); + }); + + it("should accept a custom equality function", () => { + const input = [{ id: 1 }, { id: 1 }, { id: 2 }]; + const isEqual = (a, b) => a.id === b.id; + expect(deduplicateArray(input, isEqual)).toEqual([{ id: 1 }, { id: 2 }]); + }); +}); From 0ca03c8dfdbe4b899830ef8c01847fcc92080e5c Mon Sep 17 00:00:00 2001 From: Jon Snyder Date: Thu, 31 Aug 2023 12:23:13 -0600 Subject: [PATCH 13/20] Fix default value for sendDisplayNotifications --- .../DataCollector/validateUserEventOptions.js | 2 +- src/components/Personalization/index.js | 3 +- src/core/createEvent.js | 11 +++++- .../specs/Personalization/C782718.js | 1 + test/unit/specs/core/createEvent.spec.js | 39 +++++++++++++++++++ 5 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/components/DataCollector/validateUserEventOptions.js b/src/components/DataCollector/validateUserEventOptions.js index be5e82dbe..dbf672052 100644 --- a/src/components/DataCollector/validateUserEventOptions.js +++ b/src/components/DataCollector/validateUserEventOptions.js @@ -33,7 +33,7 @@ export default ({ options }) => { surfaces: arrayOf(string()).uniqueItems(), sendDisplayNotifications: boolean().default(true), includePendingDisplayNotifications: boolean().default(false) - }), + }).default({ sendDisplayNotifications: true }), datasetId: string(), mergeId: string(), edgeConfigOverrides: validateConfigOverride diff --git a/src/components/Personalization/index.js b/src/components/Personalization/index.js index 4a72914c3..67416f6b8 100644 --- a/src/components/Personalization/index.js +++ b/src/components/Personalization/index.js @@ -103,7 +103,8 @@ const createPersonalization = ({ config, logger, eventManager }) => { viewCache }); const applyPropositions = createApplyPropositions({ - render + render, + pendingDisplayNotifications }); const setTargetMigration = createSetTargetMigration({ targetMigrationEnabled diff --git a/src/core/createEvent.js b/src/core/createEvent.js index 146a71790..33474069d 100644 --- a/src/core/createEvent.js +++ b/src/core/createEvent.js @@ -83,10 +83,17 @@ export default () => { const newPropositions = deduplicateArray( [...getXdmPropositions(userXdm), ...getXdmPropositions(content.xdm)], - (a, b) => a === b || (a.id && b.id && a.id === b.id) + (a, b) => + a === b || + (a.id && + b.id && + a.id === b.id && + a.scope && + b.scope && + a.scope === b.scope) ); if (userXdm) { - event.mergeXdm(userXdm); + this.mergeXdm(userXdm); } if (newPropositions.length > 0) { // eslint-disable-next-line no-underscore-dangle diff --git a/test/functional/specs/Personalization/C782718.js b/test/functional/specs/Personalization/C782718.js index ddfb48a20..feacf87e5 100644 --- a/test/functional/specs/Personalization/C782718.js +++ b/test/functional/specs/Personalization/C782718.js @@ -123,6 +123,7 @@ const simulatePageLoad = async alloy => { personalizationPayload, PAGE_WIDE_SCOPE ); + await t .expect( // eslint-disable-next-line no-underscore-dangle diff --git a/test/unit/specs/core/createEvent.spec.js b/test/unit/specs/core/createEvent.spec.js index 2ca57f2dc..e81cd7b45 100644 --- a/test/unit/specs/core/createEvent.spec.js +++ b/test/unit/specs/core/createEvent.spec.js @@ -360,4 +360,43 @@ describe("createEvent", () => { }); }); }); + + it("deduplicates propositions by id", () => { + const subject = createEvent(); + subject.mergeXdm({ + _experience: { + decisioning: { + propositions: [ + { id: "1", scope: "a" }, + { id: "2", scope: "a" } + ] + } + } + }); + subject.setUserXdm({ + _experience: { + decisioning: { + propositions: [ + { id: "2", scope: "a" }, + { id: "3", scope: "a" }, + { id: "3", scope: "a" } + ] + } + } + }); + subject.finalize(); + expect(subject.toJSON()).toEqual({ + xdm: { + _experience: { + decisioning: { + propositions: [ + { id: "2", scope: "a" }, + { id: "3", scope: "a" }, + { id: "1", scope: "a" } + ] + } + } + } + }); + }); }); From 9a9835d03fd4f6efd99c49501489a7d7fa4b8c3e Mon Sep 17 00:00:00 2001 From: Jon Snyder Date: Thu, 31 Aug 2023 13:26:45 -0600 Subject: [PATCH 14/20] Add viewName option to applyPropositions --- src/components/DataCollector/index.js | 6 +- .../DataCollector/validateApplyResponse.js | 5 +- .../createApplyPropositions.js | 26 +++++--- src/components/Personalization/index.js | 3 +- .../validateApplyPropositionsOptions.js | 7 ++- src/core/createEventManager.js | 5 +- .../createApplyPropositions.spec.js | 61 ++++++++++++++----- .../Personalization/topLevel/buildAlloy.js | 3 +- .../validateApplyPropositionsOptions.spec.js | 4 +- 9 files changed, 85 insertions(+), 35 deletions(-) diff --git a/src/components/DataCollector/index.js b/src/components/DataCollector/index.js index 78c93c6a2..ecd052e86 100644 --- a/src/components/DataCollector/index.js +++ b/src/components/DataCollector/index.js @@ -89,7 +89,8 @@ const createDataCollector = ({ eventManager, logger }) => { const { renderDecisions = false, responseHeaders = {}, - responseBody = { handle: [] } + responseBody = { handle: [] }, + personalization } = options; const event = eventManager.createEvent(); @@ -97,7 +98,8 @@ const createDataCollector = ({ eventManager, logger }) => { return eventManager.applyResponse(event, { renderDecisions, responseHeaders, - responseBody + responseBody, + personalization }); } } diff --git a/src/components/DataCollector/validateApplyResponse.js b/src/components/DataCollector/validateApplyResponse.js index a7f411836..614dad21e 100644 --- a/src/components/DataCollector/validateApplyResponse.js +++ b/src/components/DataCollector/validateApplyResponse.js @@ -29,7 +29,10 @@ export default ({ options }) => { payload: anything().required() }) ).required() - }).required() + }).required(), + personalization: objectOf({ + sendDisplayNotifications: boolean().default(true) + }).default({ sendDisplayNotifications: true }) }).noUnknownFields(); return validator(options); diff --git a/src/components/Personalization/createApplyPropositions.js b/src/components/Personalization/createApplyPropositions.js index 3a209f122..63c93d01d 100644 --- a/src/components/Personalization/createApplyPropositions.js +++ b/src/components/Personalization/createApplyPropositions.js @@ -20,7 +20,7 @@ import { const SUPPORTED_SCHEMAS = [DOM_ACTION, HTML_CONTENT_ITEM]; -export default ({ render, pendingDisplayNotifications }) => { +export default ({ render, pendingDisplayNotifications, viewCache }) => { const filterItemsPredicate = item => SUPPORTED_SCHEMAS.indexOf(item.schema) > -1; @@ -73,16 +73,28 @@ export default ({ render, pendingDisplayNotifications }) => { .filter(proposition => isNonEmptyArray(proposition.items)); }; - return ({ propositions, metadata = {} }) => { - const propositionsToExecute = preparePropositions({ + return ({ propositions = [], metadata = {}, viewName }) => { + let propositionsToExecute = preparePropositions({ propositions, metadata }).map(proposition => createProposition(proposition, true)); - pendingDisplayNotifications.concat(render(propositionsToExecute)); + return Promise.resolve() + .then(() => { + if (viewName) { + return viewCache.getView(viewName); + } + return []; + }) + .then(additionalPropositions => { + propositionsToExecute = propositionsToExecute.concat( + additionalPropositions + ); + pendingDisplayNotifications.concat(render(propositionsToExecute)); - return { - propositions: buildReturnedPropositions(propositionsToExecute) - }; + return { + propositions: buildReturnedPropositions(propositionsToExecute) + }; + }); }; }; diff --git a/src/components/Personalization/index.js b/src/components/Personalization/index.js index 67416f6b8..3e847c383 100644 --- a/src/components/Personalization/index.js +++ b/src/components/Personalization/index.js @@ -104,7 +104,8 @@ const createPersonalization = ({ config, logger, eventManager }) => { }); const applyPropositions = createApplyPropositions({ render, - pendingDisplayNotifications + pendingDisplayNotifications, + viewCache }); const setTargetMigration = createSetTargetMigration({ targetMigrationEnabled diff --git a/src/components/Personalization/validateApplyPropositionsOptions.js b/src/components/Personalization/validateApplyPropositionsOptions.js index 96141a922..fee5cdb02 100644 --- a/src/components/Personalization/validateApplyPropositionsOptions.js +++ b/src/components/Personalization/validateApplyPropositionsOptions.js @@ -9,14 +9,15 @@ 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 { anything, objectOf, arrayOf } from "../../utils/validation"; +import { anything, objectOf, arrayOf, string } from "../../utils/validation"; export const EMPTY_PROPOSITIONS = { propositions: [] }; export default ({ logger, options }) => { const applyPropositionsOptionsValidator = objectOf({ - propositions: arrayOf(objectOf(anything())).nonEmpty(), - metadata: objectOf(anything()) + propositions: arrayOf(objectOf(anything())), + metadata: objectOf(anything()), + viewName: string() }).required(); try { diff --git a/src/core/createEventManager.js b/src/core/createEventManager.js index 2435b7c15..c07f79f57 100644 --- a/src/core/createEventManager.js +++ b/src/core/createEventManager.js @@ -122,7 +122,8 @@ export default ({ const { renderDecisions = false, responseHeaders = {}, - responseBody = { handle: [] } + responseBody = { handle: [] }, + personalization } = options; const payload = createDataCollectionRequestPayload(); @@ -134,7 +135,7 @@ export default ({ event, renderDecisions, decisionScopes: [PAGE_WIDE_SCOPE], - personalization: {}, + personalization, onResponse: onResponseCallbackAggregator.add, onRequestFailure: noop }) diff --git a/test/unit/specs/components/Personalization/createApplyPropositions.spec.js b/test/unit/specs/components/Personalization/createApplyPropositions.spec.js index e34126cd3..3e6b379b5 100644 --- a/test/unit/specs/components/Personalization/createApplyPropositions.spec.js +++ b/test/unit/specs/components/Personalization/createApplyPropositions.spec.js @@ -17,6 +17,7 @@ import { } from "./responsesMock/eventResponses"; import createApplyPropositions from "../../../../../src/components/Personalization/createApplyPropositions"; import clone from "../../../../../src/utils/clone"; +import { createProposition } from "../../../../../src/components/Personalization/handlers/proposition"; const METADATA = { home: { @@ -28,6 +29,7 @@ const METADATA = { describe("Personalization::createApplyPropositions", () => { let render; let pendingDisplayNotifications; + let viewCache; beforeEach(() => { render = jasmine.createSpy("render"); @@ -43,22 +45,24 @@ describe("Personalization::createApplyPropositions", () => { "pendingDisplayNotifications", ["concat"] ); + viewCache = jasmine.createSpyObj("viewCache", ["getView"]); + viewCache.getView.and.returnValue(Promise.resolve([])); }); - it("it should return an empty propositions promise if propositions is empty array", () => { + it("it should return an empty propositions promise if propositions is empty array", async () => { const applyPropositions = createApplyPropositions({ render, pendingDisplayNotifications }); - const result = applyPropositions({ + const result = await applyPropositions({ propositions: [] }); expect(result).toEqual({ propositions: [] }); expect(render).toHaveBeenCalledOnceWith([]); }); - it("it should apply user-provided dom-action schema propositions", () => { + it("it should apply user-provided dom-action schema propositions", async () => { const expectedExecuteDecisionsPropositions = clone( PAGE_WIDE_SCOPE_DECISIONS ).map(proposition => { @@ -71,7 +75,7 @@ describe("Personalization::createApplyPropositions", () => { pendingDisplayNotifications }); - const result = applyPropositions({ + const result = await applyPropositions({ propositions: PAGE_WIDE_SCOPE_DECISIONS }); @@ -88,13 +92,13 @@ describe("Personalization::createApplyPropositions", () => { }); }); - it("it should merge metadata with propositions that have html-content-item schema", () => { + it("it should merge metadata with propositions that have html-content-item schema", async () => { const applyPropositions = createApplyPropositions({ render, pendingDisplayNotifications }); - const { propositions } = applyPropositions({ + const { propositions } = await applyPropositions({ propositions: MIXED_PROPOSITIONS, metadata: METADATA }); @@ -114,7 +118,7 @@ describe("Personalization::createApplyPropositions", () => { expect(render).toHaveBeenCalledTimes(1); }); - it("it should drop items with html-content-item schema when there is no metadata", () => { + it("it should drop items with html-content-item schema when there is no metadata", async () => { const propositions = [ { id: "AT:eyJhY3Rpdml0eUlkIjoiNDQyMzU4IiwiZXhwZXJpZW5jZUlkIjoiIn1=", @@ -147,7 +151,7 @@ describe("Personalization::createApplyPropositions", () => { pendingDisplayNotifications }); - const result = applyPropositions({ + const result = await applyPropositions({ propositions }); @@ -157,13 +161,13 @@ describe("Personalization::createApplyPropositions", () => { expect(result.propositions[0].renderAttempted).toBeTrue(); }); - it("it should return renderAttempted = true on resulting propositions", () => { + it("it should return renderAttempted = true on resulting propositions", async () => { const applyPropositions = createApplyPropositions({ render, pendingDisplayNotifications }); - const result = applyPropositions({ + const result = await applyPropositions({ propositions: MIXED_PROPOSITIONS }); expect(result.propositions.length).toEqual(3); @@ -172,7 +176,7 @@ describe("Personalization::createApplyPropositions", () => { }); }); - it("it should ignore propositions with __view__ scope that have already been rendered", () => { + it("it should ignore propositions with __view__ scope that have already been rendered", async () => { const applyPropositions = createApplyPropositions({ render, pendingDisplayNotifications @@ -180,7 +184,7 @@ describe("Personalization::createApplyPropositions", () => { const propositions = JSON.parse(JSON.stringify(MIXED_PROPOSITIONS)); propositions[4].renderAttempted = true; - const result = applyPropositions({ + const result = await applyPropositions({ propositions }); expect(result.propositions.length).toEqual(2); @@ -194,7 +198,7 @@ describe("Personalization::createApplyPropositions", () => { }); }); - it("it should ignore items with unsupported schemas", () => { + it("it should ignore items with unsupported schemas", async () => { const expectedItemIds = ["442358", "442359"]; const applyPropositions = createApplyPropositions({ @@ -202,7 +206,7 @@ describe("Personalization::createApplyPropositions", () => { pendingDisplayNotifications }); - const { propositions } = applyPropositions({ + const { propositions } = await applyPropositions({ propositions: MIXED_PROPOSITIONS }); expect(propositions.length).toEqual(3); @@ -214,14 +218,14 @@ describe("Personalization::createApplyPropositions", () => { }); }); - it("it should not mutate original propositions", () => { + it("it should not mutate original propositions", async () => { const applyPropositions = createApplyPropositions({ render, pendingDisplayNotifications }); const originalPropositions = clone(MIXED_PROPOSITIONS); - const result = applyPropositions({ + const result = await applyPropositions({ propositions: originalPropositions, metadata: METADATA }); @@ -239,4 +243,29 @@ describe("Personalization::createApplyPropositions", () => { }); expect(numReturnedPropositions).toEqual(4); }); + + it("concats viewName propositions", async () => { + viewCache.getView.and.returnValue( + Promise.resolve([ + createProposition({ id: "myViewNameProp1", items: [{}] }) + ]) + ); + const applyPropositions = createApplyPropositions({ + render, + pendingDisplayNotifications, + viewCache + }); + const result = await applyPropositions({ + viewName: "myViewName" + }); + expect(result).toEqual({ + propositions: [ + { + id: "myViewNameProp1", + items: [{}], + renderAttempted: true + } + ] + }); + }); }); diff --git a/test/unit/specs/components/Personalization/topLevel/buildAlloy.js b/test/unit/specs/components/Personalization/topLevel/buildAlloy.js index 85543ff01..740f2b7f4 100644 --- a/test/unit/specs/components/Personalization/topLevel/buildAlloy.js +++ b/test/unit/specs/components/Personalization/topLevel/buildAlloy.js @@ -140,7 +140,8 @@ const buildComponent = ({ }); const applyPropositions = createApplyPropositions({ render, - pendingDisplayNotifications + pendingDisplayNotifications, + viewCache }); const setTargetMigration = createSetTargetMigration({ targetMigrationEnabled diff --git a/test/unit/specs/components/Personalization/validateApplyPropositionsOptions.spec.js b/test/unit/specs/components/Personalization/validateApplyPropositionsOptions.spec.js index 90f4a333e..52558c967 100644 --- a/test/unit/specs/components/Personalization/validateApplyPropositionsOptions.spec.js +++ b/test/unit/specs/components/Personalization/validateApplyPropositionsOptions.spec.js @@ -82,7 +82,7 @@ describe("Personalization::validateApplyPropositionsOptions", () => { expect(result).not.toEqual(EMPTY_PROPOSITIONS); }); - it("it should log a warning when propositions is empty array", () => { + it("it should not log a warning when propositions is empty array", () => { const result = validateApplyPropositionsOptions({ logger, options: { @@ -90,7 +90,7 @@ describe("Personalization::validateApplyPropositionsOptions", () => { } }); - expect(loggerSpy).toHaveBeenCalled(); + expect(loggerSpy).not.toHaveBeenCalled(); expect(result).toEqual(EMPTY_PROPOSITIONS); }); From daea723d1e1de703a928f2b5756da3f208687c46 Mon Sep 17 00:00:00 2001 From: Jon Snyder Date: Fri, 1 Sep 2023 08:56:36 -0600 Subject: [PATCH 15/20] Add showContainers call after rendering in fetch data handler --- src/components/Personalization/createFetchDataHandler.js | 2 ++ src/components/Personalization/index.js | 1 + .../components/Personalization/createFetchDataHandler.spec.js | 4 ++++ .../specs/components/Personalization/topLevel/buildAlloy.js | 1 + 4 files changed, 8 insertions(+) diff --git a/src/components/Personalization/createFetchDataHandler.js b/src/components/Personalization/createFetchDataHandler.js index 6bf75f953..8bb1a0ef8 100644 --- a/src/components/Personalization/createFetchDataHandler.js +++ b/src/components/Personalization/createFetchDataHandler.js @@ -18,6 +18,7 @@ const DECISIONS_HANDLE = "personalization:decisions"; export default ({ prehidingStyle, + showContainers, hideContainers, mergeQuery, collect, @@ -34,6 +35,7 @@ export default ({ const propositions = cacheUpdate.update(handles); if (personalizationDetails.isRenderDecisions()) { render(propositions).then(decisionsMeta => { + showContainers(); if (decisionsMeta.length > 0) { collect({ decisionsMeta, diff --git a/src/components/Personalization/index.js b/src/components/Personalization/index.js index 310648fe4..5a75c68a9 100644 --- a/src/components/Personalization/index.js +++ b/src/components/Personalization/index.js @@ -78,6 +78,7 @@ const createPersonalization = ({ config, logger, eventManager }) => { }); const fetchDataHandler = createFetchDataHandler({ prehidingStyle, + showContainers, hideContainers, mergeQuery, collect, diff --git a/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js b/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js index d9d1828e7..94b0d106e 100644 --- a/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js +++ b/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js @@ -16,6 +16,7 @@ import flushPromiseChains from "../../../helpers/flushPromiseChains"; describe("Personalization::createFetchDataHandler", () => { let prehidingStyle; + let showContainers; let hideContainers; let mergeQuery; let collect; @@ -30,6 +31,7 @@ describe("Personalization::createFetchDataHandler", () => { beforeEach(() => { prehidingStyle = "myprehidingstyle"; + showContainers = jasmine.createSpy("showContainers"); hideContainers = jasmine.createSpy("hideContainers"); mergeQuery = jasmine.createSpy("mergeQuery"); collect = jasmine.createSpy("collect"); @@ -49,6 +51,7 @@ describe("Personalization::createFetchDataHandler", () => { const run = () => { const fetchDataHandler = createFetchDataHandler({ prehidingStyle, + showContainers, hideContainers, mergeQuery, collect, @@ -118,6 +121,7 @@ describe("Personalization::createFetchDataHandler", () => { decisions: [] }); await flushPromiseChains(); + expect(showContainers).toHaveBeenCalled(); expect(collect).toHaveBeenCalledOnceWith({ decisionsMeta: [ { id: "handle1", scope: undefined, scopeDetails: undefined } diff --git a/test/unit/specs/components/Personalization/topLevel/buildAlloy.js b/test/unit/specs/components/Personalization/topLevel/buildAlloy.js index 661d1e7a9..be2d9a204 100644 --- a/test/unit/specs/components/Personalization/topLevel/buildAlloy.js +++ b/test/unit/specs/components/Personalization/topLevel/buildAlloy.js @@ -114,6 +114,7 @@ const buildComponent = ({ }); const fetchDataHandler = createFetchDataHandler({ prehidingStyle, + showContainers, hideContainers, mergeQuery, collect, From 8c56124ec710b20cbcb2643bb0ab9f6f465fb3f0 Mon Sep 17 00:00:00 2001 From: Jon Snyder Date: Wed, 13 Sep 2023 20:21:18 -0600 Subject: [PATCH 16/20] Change proposition to be immutable. Render based on schema. Group by scope type. --- .../createApplyPropositions.js | 18 +- .../Personalization/createFetchDataHandler.js | 35 +++- .../Personalization/createViewCacheManager.js | 34 +--- .../createViewChangeHandler.js | 29 +-- .../handlers/createDomActionHandler.js | 7 + .../handlers/createHtmlContentHandler.js | 44 ---- .../handlers/createProcessDomAction.js | 35 ++++ .../handlers/createProcessHtmlContent.js | 31 +++ .../handlers/createProcessPropositions.js | 154 ++++++++++++++ .../handlers/createProcessRedirect.js | 29 +++ .../Personalization/handlers/createRender.js | 49 ----- .../handlers/injectCreateProposition.js | 91 +++++++++ .../handlers/processDefaultContent.js | 14 ++ .../Personalization/handlers/proposition.js | 155 -------------- src/components/Personalization/index.js | 49 ++--- .../createApplyPropositions.spec.js | 4 +- .../createFetchDataHandler.spec.js | 3 +- .../createViewCacheManager.spec.js | 2 +- .../createViewChangeHandler.spec.js | 4 +- .../handlers/createHtmlContentHandler.spec.js | 2 +- .../handlers/createProcessDomAction.spec.js | 67 +++++++ .../handlers/createProcessHtmlContent.spec.js | 57 ++++++ .../createProcessPropositions.spec.js | 189 ++++++++++++++++++ .../handlers/createProcessRedirect.spec.js | 68 +++++++ .../handlers/createRedirectHandler.spec.js | 3 +- .../handlers/createRender.spec.js | 4 +- .../handlers/createRenderDomAction.spec.js | 0 .../handlers/injectCreateProposition.spec.js | 73 +++++++ .../handlers/processDefaultContent.spec.js | 10 + .../handlers/proposition.spec.js | 8 +- .../Personalization/topLevel/buildAlloy.js | 66 +++--- .../topLevel/mixedPropositions.spec.js | 2 + .../topLevel/scopesFoo1Foo2Decisions.spec.js | 1 + 33 files changed, 946 insertions(+), 391 deletions(-) delete mode 100644 src/components/Personalization/handlers/createHtmlContentHandler.js create mode 100644 src/components/Personalization/handlers/createProcessDomAction.js create mode 100644 src/components/Personalization/handlers/createProcessHtmlContent.js create mode 100644 src/components/Personalization/handlers/createProcessPropositions.js create mode 100644 src/components/Personalization/handlers/createProcessRedirect.js delete mode 100644 src/components/Personalization/handlers/createRender.js create mode 100644 src/components/Personalization/handlers/injectCreateProposition.js create mode 100644 src/components/Personalization/handlers/processDefaultContent.js delete mode 100644 src/components/Personalization/handlers/proposition.js create mode 100644 test/unit/specs/components/Personalization/handlers/createProcessDomAction.spec.js create mode 100644 test/unit/specs/components/Personalization/handlers/createProcessHtmlContent.spec.js create mode 100644 test/unit/specs/components/Personalization/handlers/createProcessPropositions.spec.js create mode 100644 test/unit/specs/components/Personalization/handlers/createProcessRedirect.spec.js create mode 100644 test/unit/specs/components/Personalization/handlers/createRenderDomAction.spec.js create mode 100644 test/unit/specs/components/Personalization/handlers/injectCreateProposition.spec.js create mode 100644 test/unit/specs/components/Personalization/handlers/processDefaultContent.spec.js diff --git a/src/components/Personalization/createApplyPropositions.js b/src/components/Personalization/createApplyPropositions.js index 63c93d01d..1c614a48c 100644 --- a/src/components/Personalization/createApplyPropositions.js +++ b/src/components/Personalization/createApplyPropositions.js @@ -13,14 +13,10 @@ governing permissions and limitations under the License. import { isNonEmptyArray, isObject } from "../../utils"; import { DOM_ACTION, HTML_CONTENT_ITEM } from "./constants/schema"; import PAGE_WIDE_SCOPE from "../../constants/pageWideScope"; -import { - buildReturnedPropositions, - createProposition -} from "./handlers/proposition"; const SUPPORTED_SCHEMAS = [DOM_ACTION, HTML_CONTENT_ITEM]; -export default ({ render, pendingDisplayNotifications, viewCache }) => { +export default ({ processPropositions, createProposition, pendingDisplayNotifications, viewCache }) => { const filterItemsPredicate = item => SUPPORTED_SCHEMAS.indexOf(item.schema) > -1; @@ -77,7 +73,7 @@ export default ({ render, pendingDisplayNotifications, viewCache }) => { let propositionsToExecute = preparePropositions({ propositions, metadata - }).map(proposition => createProposition(proposition, true)); + }).map(proposition => createProposition(proposition)); return Promise.resolve() .then(() => { @@ -87,13 +83,13 @@ export default ({ render, pendingDisplayNotifications, viewCache }) => { return []; }) .then(additionalPropositions => { - propositionsToExecute = propositionsToExecute.concat( - additionalPropositions - ); - pendingDisplayNotifications.concat(render(propositionsToExecute)); + const { render, returnedPropositions } = + processPropositions([ ...propositionsToExecute, ...additionalPropositions]); + + pendingDisplayNotifications.concat(render()); return { - propositions: buildReturnedPropositions(propositionsToExecute) + propositions: returnedPropositions }; }); }; diff --git a/src/components/Personalization/createFetchDataHandler.js b/src/components/Personalization/createFetchDataHandler.js index 7f7c81978..fae2473a1 100644 --- a/src/components/Personalization/createFetchDataHandler.js +++ b/src/components/Personalization/createFetchDataHandler.js @@ -9,11 +9,7 @@ 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 { defer } from "../../utils"; -import { - buildReturnedPropositions, - buildReturnedDecisions -} from "./handlers/proposition"; +import { defer, groupBy } from "../../utils"; const DECISIONS_HANDLE = "personalization:decisions"; @@ -23,7 +19,8 @@ export default ({ hideContainers, mergeQuery, collect, - render, + processPropositions, + createProposition, pendingDisplayNotifications }) => { return ({ cacheUpdate, personalizationDetails, event, onResponse }) => { @@ -50,17 +47,35 @@ export default ({ onResponse(({ response }) => { const handles = response.getPayloadsByType(DECISIONS_HANDLE); - const propositions = cacheUpdate.update(handles); + const propositions = handles.map(handle => createProposition(handle)); + const { + page: pagePropositions = [], + view: viewPropositions = [], + proposition: nonRenderedPropositions = [] + } = groupBy(propositions, p => p.getScopeType()); + + const currentViewPropositions = cacheUpdate.update(viewPropositions); + + let render, returnedPropositions, returnedDecisions; + if (personalizationDetails.isRenderDecisions()) { - render(propositions).then(decisionsMeta => { + ({ render, returnedPropositions, returnedDecisions } = + processPropositions([...pagePropositions, ...currentViewPropositions], nonRenderedPropositions)); + render().then(decisionsMeta => { showContainers(); handleNotifications(decisionsMeta); + }).catch(e => { + showContainers(); + throw e; }); + } else { + ({ returnedPropositions, returnedDecisions } = + processPropositions([], [...pagePropositions, ...currentViewPropositions, ...nonRenderedPropositions])); } return { - propositions: buildReturnedPropositions(propositions), - decisions: buildReturnedDecisions(propositions) + propositions: returnedPropositions, + decisions: returnedDecisions }; }); }; diff --git a/src/components/Personalization/createViewCacheManager.js b/src/components/Personalization/createViewCacheManager.js index 6f6a8a394..6367a0d34 100644 --- a/src/components/Personalization/createViewCacheManager.js +++ b/src/components/Personalization/createViewCacheManager.js @@ -10,11 +10,11 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import { assign } from "../../utils"; +import { assign, groupBy } from "../../utils"; import defer from "../../utils/defer"; -import { VIEW_SCOPE_TYPE } from "./constants/scopeType"; export default ({ createProposition }) => { + const viewStorage = {}; let cacheUpdateCreatedAtLeastOnce = false; let previousUpdateCacheComplete = Promise.resolve(); @@ -22,7 +22,7 @@ export default ({ createProposition }) => { const getViewPropositions = (currentViewStorage, viewName) => { const viewPropositions = currentViewStorage[viewName]; if (viewPropositions && viewPropositions.length > 0) { - return viewPropositions.map(createProposition); + return viewPropositions; } const emptyViewProposition = createProposition({ @@ -32,9 +32,7 @@ export default ({ createProposition }) => { scopeType: "view" } } - }); - emptyViewProposition.includeInDisplayNotification(); - emptyViewProposition.excludeInReturnedPropositions(); + }, false); return [emptyViewProposition]; }; @@ -51,29 +49,13 @@ export default ({ createProposition }) => { .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)); - } - }); + update(viewPropositions) { + const newViewStorage = groupBy(viewPropositions, proposition => proposition.getScope()); updateCacheDeferred.resolve(newViewStorage); if (viewName) { - return [ - ...getViewPropositions(newViewStorage, viewName), - ...otherPropositions - ]; + return getViewPropositions(newViewStorage, viewName) } - return otherPropositions; + return []; }, cancel() { updateCacheDeferred.reject(); diff --git a/src/components/Personalization/createViewChangeHandler.js b/src/components/Personalization/createViewChangeHandler.js index 500732be7..06ec8fe5b 100644 --- a/src/components/Personalization/createViewChangeHandler.js +++ b/src/components/Personalization/createViewChangeHandler.js @@ -11,29 +11,30 @@ governing permissions and limitations under the License. */ import { PropositionEventType } from "./constants/propositionEventType"; -import { - buildReturnedPropositions, - buildReturnedDecisions -} from "./handlers/proposition"; -export default ({ mergeDecisionsMeta, render, viewCache }) => { +export default ({ mergeDecisionsMeta, processPropositions, viewCache }) => { return ({ personalizationDetails, event, onResponse }) => { + let returnedPropositions, returnedDecisions; const viewName = personalizationDetails.getViewName(); + onResponse(() => { + return { + propositions: returnedPropositions, + decisions: returnedDecisions + }; + }); + return viewCache .getView(viewName) .then(propositions => { - onResponse(() => { - return { - propositions: buildReturnedPropositions(propositions), - decisions: buildReturnedDecisions(propositions) - }; - }); - + let render; if (personalizationDetails.isRenderDecisions()) { - return render(propositions); + ({ render, returnedPropositions, returnedDecisions } = processPropositions(propositions)); + return render(); + } else { + ({ returnedPropositions, returnedDecisions } = processPropositions([], propositions)); + return []; } - return Promise.resolve([]); }) .then(decisionsMeta => { mergeDecisionsMeta(event, decisionsMeta, PropositionEventType.DISPLAY); diff --git a/src/components/Personalization/handlers/createDomActionHandler.js b/src/components/Personalization/handlers/createDomActionHandler.js index a27b6c48e..b4b085da0 100644 --- a/src/components/Personalization/handlers/createDomActionHandler.js +++ b/src/components/Personalization/handlers/createDomActionHandler.js @@ -44,3 +44,10 @@ export default ({ next(proposition); }; + + +/* +1. Anything scope type view we put in the view storage. +2. Anything page wide scope we try to render. +3. Anything else we only render if its Dom-action schema +*/ diff --git a/src/components/Personalization/handlers/createHtmlContentHandler.js b/src/components/Personalization/handlers/createHtmlContentHandler.js deleted file mode 100644 index 47f3d028d..000000000 --- a/src/components/Personalization/handlers/createHtmlContentHandler.js +++ /dev/null @@ -1,44 +0,0 @@ -/* -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/handlers/createProcessDomAction.js b/src/components/Personalization/handlers/createProcessDomAction.js new file mode 100644 index 000000000..46af90c9c --- /dev/null +++ b/src/components/Personalization/handlers/createProcessDomAction.js @@ -0,0 +1,35 @@ +/* +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 ({ modules, logger, storeClickMetrics }) => item => { + const { type, selector } = item.getData() || {}; + + if (!type) { + logger.warn("Invalid DOM action data: missing type.", item.getData()); + return {}; + } + + if (type === "click") { + if (!selector) { + logger.warn("Invalid DOM action data: missing selector.", item.getData()); + return {}; + } + storeClickMetrics({ selector, meta: item.getMeta() }); + return { setRenderAttempted: true, includeInNotification: false }; + } + + if (!modules[type]) { + logger.warn("Invalid DOM action data: unknown type.", item.getData()); + return {}; + } + + return { render: () => modules[type](item.getData()), setRenderAttempted: true, includeInNotification: true }; +}; diff --git a/src/components/Personalization/handlers/createProcessHtmlContent.js b/src/components/Personalization/handlers/createProcessHtmlContent.js new file mode 100644 index 000000000..45abd187b --- /dev/null +++ b/src/components/Personalization/handlers/createProcessHtmlContent.js @@ -0,0 +1,31 @@ +/* +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 ({ modules, logger }) => (item) => { + const { type, selector } = item.getData() || {}; + + if (!selector || !type) { + return {}; + } + + if (!modules[type]) { + logger.warn("Invalid HTML content data", item.getData()); + return {}; + } + + return { + render: () => { + modules[type](item.getData()); + }, + setRenderAttempted: true, + includeInNotification: true + }; +}; diff --git a/src/components/Personalization/handlers/createProcessPropositions.js b/src/components/Personalization/handlers/createProcessPropositions.js new file mode 100644 index 000000000..186d99d5e --- /dev/null +++ b/src/components/Personalization/handlers/createProcessPropositions.js @@ -0,0 +1,154 @@ +/* +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 ({ schemaProcessors, logger}) => { + + const wrapRenderWithLogging = (render, item) => () => { + return Promise.resolve() + .then(render) + .then(() => { + if (logger.enabled) { + logger.info(`Action ${item.toString()} executed.`); + } + return true; + }) + .catch(error => { + if (logger.enabled) { + const { message, stack } = error; + const errorMessage = `Failed to execute action ${item.toString()}. ${message} ${stack}`; + logger.error(errorMessage); + } + return false; + }); + }; + + const renderItems = (renderers, meta) => + Promise.all(renderers.map(renderer => renderer())) + .then(successes => { + // as long as at least one renderer succeeds, we want to add the notification + // to the display notifications + if (!successes.includes(true)) { + return undefined; + } + return meta; + }); + + const processItem = (item) => { + const processor = schemaProcessors[item.getSchema()]; + if (!processor) { + return {}; + } + return processor(item); + }; + + const processItems = ({ + renderers: existingRenderers, + returnedPropositions: existingReturnedPropositions, + returnedDecisions: existingReturnedDecisions, + items, + proposition + }) => { + let renderers = [...existingRenderers]; + let returnedPropositions = [...existingReturnedPropositions]; + let returnedDecisions = [...existingReturnedDecisions]; + let renderedItems = []; + let nonRenderedItems = []; + let itemRenderers = []; + let atLeastOneWithNotification = false; + let render; + let setRenderAttempted; + let includeInNotification; + let onlyRenderThis = false; + let i = 0; + let item; + + while (items.length > i) { + item = items[i]; + ({ render, setRenderAttempted, includeInNotification, onlyRenderThis } = processItem(item)); + if (onlyRenderThis) { + returnedPropositions = []; + returnedDecisions = []; + if (setRenderAttempted) { + renderedItems = [item]; + nonRenderedItems = []; + } else { + renderedItems = []; + nonRenderedItems = [item]; + } + renderers = []; + itemRenderers = [render]; + atLeastOneWithNotification = includeInNotification; + break; + } + if (render) { + itemRenderers.push(wrapRenderWithLogging(render, item)); + } + if (includeInNotification) { + atLeastOneWithNotification = true; + } + if (setRenderAttempted) { + renderedItems.push(item); + } else { + nonRenderedItems.push(item); + } + i += 1; + } + if (itemRenderers.length > 0) { + const meta = atLeastOneWithNotification ? proposition.getNotification() : undefined; + renderers.push(() => renderItems(itemRenderers, meta)); + } else if (atLeastOneWithNotification) { + renderers.push(() => proposition.getNotification()); + } + if (renderedItems.length > 0) { + proposition.addToReturnValues(returnedPropositions, returnedDecisions, renderedItems, true); + } + if (nonRenderedItems.length > 0) { + proposition.addToReturnValues(returnedPropositions, returnedDecisions, nonRenderedItems, false); + } + + return { renderers, returnedPropositions, returnedDecisions, onlyRenderThis }; + }; + + return (renderPropositions, nonRenderPropositions = []) => { + let renderers = []; + let returnedPropositions = []; + let returnedDecisions = []; + let onlyRenderThis; + let i = 0; + let proposition, items; + + while (renderPropositions.length > i) { + proposition = renderPropositions[i]; + items = proposition.getItems(); + ({ renderers, returnedPropositions, returnedDecisions, onlyRenderThis } = + processItems({ renderers, returnedPropositions, returnedDecisions, items, proposition })); + if (onlyRenderThis) { + renderPropositions.forEach((p, index) => { + if (index !== i) { + p.addToReturnValues(returnedPropositions, returnedDecisions, p.getItems(), false); + } + }); + break; + } + i += 1; + } + + nonRenderPropositions.forEach(p => { + p.addToReturnValues(returnedPropositions, returnedDecisions, p.getItems(), false); + }); + const render = () => { + return Promise.all(renderers.map(renderer => renderer())) + .then(metas => metas.filter(meta => meta)); + }; + return { returnedPropositions, returnedDecisions, render}; + }; +}; diff --git a/src/components/Personalization/handlers/createProcessRedirect.js b/src/components/Personalization/handlers/createProcessRedirect.js new file mode 100644 index 000000000..fbb3801e2 --- /dev/null +++ b/src/components/Personalization/handlers/createProcessRedirect.js @@ -0,0 +1,29 @@ +/* +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 ({ logger, executeRedirect, collect }) => item => { + const { content } = item.getData() || {}; + + if (!content) { + logger.warn("Invalid Redirect data", item.getData()); + return {}; + } + + const render = () => { + return collect({ decisionsMeta: [item.getMeta()] }) + .then(() => { + executeRedirect(content); + // We've already sent the display notification, so don't return anything + }); + } + + return { render, setRenderAttempted: true, onlyRenderThis: true }; +}; diff --git a/src/components/Personalization/handlers/createRender.js b/src/components/Personalization/handlers/createRender.js deleted file mode 100644 index 1ef0ad97d..000000000 --- a/src/components/Personalization/handlers/createRender.js +++ /dev/null @@ -1,49 +0,0 @@ -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/injectCreateProposition.js b/src/components/Personalization/handlers/injectCreateProposition.js new file mode 100644 index 000000000..65f1cd76c --- /dev/null +++ b/src/components/Personalization/handlers/injectCreateProposition.js @@ -0,0 +1,91 @@ +/* +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 PAGE_WIDE_SCOPE from "../../../constants/pageWideScope"; + +export default ({ preprocess, isPageWideSurface }) => { + + const createItem = (item, meta) => { + const { schema, data, characteristics: { trackingLabel } = {} } = item; + + const processedData = preprocess(data); + + if (trackingLabel) { + meta.trackingLabel = trackingLabel; + } + + return { + getSchema() { + return schema; + }, + getData() { + return processedData; + }, + getMeta() { + return meta; + }, + getOriginalItem() { + return item; + }, + toString() { + return JSON.stringify(item); + }, + toJSON() { + return item; + } + }; + }; + + return (payload, visibleInReturnedItems = true) => { + const { id, scope, scopeDetails, items = [] } = payload; + const { characteristics: { scopeType } = {} } = scopeDetails || {}; + + return { + getScope() { + return scope; + }, + getScopeType() { + if (scope === PAGE_WIDE_SCOPE || isPageWideSurface(scope)) { + return "page"; + } + if (scopeType === "view") { + return "view"; + } + return "proposition"; + }, + getItems() { + return items.map(item => createItem(item, { id, scope, scopeDetails })); + }, + getNotification() { + return { id, scope, scopeDetails }; + }, + toJSON() { + return payload; + }, + addToReturnValues(propositions, decisions, includedItems, renderAttempted) { + if (visibleInReturnedItems) { + propositions.push({ + ...payload, + items: includedItems.map(i => i.getOriginalItem()), + renderAttempted + }); + if (!renderAttempted) { + decisions.push({ + ...payload, + items: includedItems.map(i => i.getOriginalItem()) + }); + } + } + } + }; + }; +}; diff --git a/src/components/Personalization/handlers/processDefaultContent.js b/src/components/Personalization/handlers/processDefaultContent.js new file mode 100644 index 000000000..d697ea123 --- /dev/null +++ b/src/components/Personalization/handlers/processDefaultContent.js @@ -0,0 +1,14 @@ +/* +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 () => { + return { setRenderAttempted: true, includeInNotification: true }; +}; diff --git a/src/components/Personalization/handlers/proposition.js b/src/components/Personalization/handlers/proposition.js deleted file mode 100644 index c0e29babf..000000000 --- a/src/components/Personalization/handlers/proposition.js +++ /dev/null @@ -1,155 +0,0 @@ -/* -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 f309b6b8c..0b2efd52c 100644 --- a/src/components/Personalization/index.js +++ b/src/components/Personalization/index.js @@ -30,13 +30,16 @@ 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 createRender from "./handlers/createProcessPropositions"; import remapCustomCodeOffers from "./dom-actions/remapCustomCodeOffers"; import remapHeadOffers from "./dom-actions/remapHeadOffers"; import createPreprocess from "./dom-actions/createPreprocess"; -import { createProposition } from "./handlers/proposition"; +import injectCreateProposition, { createProposition } from "./handlers/injectCreateProposition"; import createAsyncArray from "./utils/createAsyncArray"; import createPendingNotificationsHandler from "./createPendingNotificationsHandler"; +import * as schema from "./constants/schema"; +import processDefaultContent from "./handlers/processDefaultContent"; +import { isPageWideSurface } from "./utils/surfaceUtils"; const createPersonalization = ({ config, logger, eventManager }) => { const { targetMigrationEnabled, prehidingStyle } = config; @@ -52,32 +55,20 @@ const createPersonalization = ({ config, logger, eventManager }) => { const modules = initDomActionsModules(); const preprocess = createPreprocess([remapHeadOffers, remapCustomCodeOffers]); + const createProposition = injectCreateProposition({ preprocess, isPageWideSurface }); - const noOpHandler = () => undefined; - const domActionHandler = createDomActionHandler({ - next: noOpHandler, - modules, - storeClickMetrics, - preprocess - }); - const measurementSchemaHandler = createMeasurementSchemaHandler({ - next: domActionHandler - }); - const redirectHandler = createRedirectHandler({ - next: measurementSchemaHandler - }); - const htmlContentHandler = createHtmlContentHandler({ - next: redirectHandler, - modules, - preprocess - }); + const schemaProcessors = { + [schema.DEFAULT_CONTENT_ITEM]: processDefaultContent, + [schema.DOM_ACTION]: createProcessDomAction({ modules, logger, storeClickMetrics }), + [schema.HTML_CONTENT_ITEM]: createProcessHtmlContent({ modules, logger }), + [schema.REDIRECT_ITEM]: createProcessRedirect({ logger, executeRedirect: url => window.location.replace(url) }) + }; - const render = createRender({ - handleChain: htmlContentHandler, - collect, - executeRedirect: url => window.location.replace(url), - logger + const processPropositions = createProcessPropositions({ + schemaProcessors, + logger, }); + const pendingDisplayNotifications = createAsyncArray(); const pendingNotificationsHandler = createPendingNotificationsHandler({ pendingDisplayNotifications, @@ -89,7 +80,8 @@ const createPersonalization = ({ config, logger, eventManager }) => { hideContainers, mergeQuery, collect, - render, + processPropositions, + createProposition, pendingDisplayNotifications }); const onClickHandler = createOnClickHandler({ @@ -100,11 +92,12 @@ const createPersonalization = ({ config, logger, eventManager }) => { }); const viewChangeHandler = createViewChangeHandler({ mergeDecisionsMeta, - render, + processPropositions, viewCache }); const applyPropositions = createApplyPropositions({ - render, + processProposition, + createProposition, pendingDisplayNotifications, viewCache }); diff --git a/test/unit/specs/components/Personalization/createApplyPropositions.spec.js b/test/unit/specs/components/Personalization/createApplyPropositions.spec.js index 3e6b379b5..ab0904a73 100644 --- a/test/unit/specs/components/Personalization/createApplyPropositions.spec.js +++ b/test/unit/specs/components/Personalization/createApplyPropositions.spec.js @@ -17,7 +17,7 @@ import { } from "./responsesMock/eventResponses"; import createApplyPropositions from "../../../../../src/components/Personalization/createApplyPropositions"; import clone from "../../../../../src/utils/clone"; -import { createProposition } from "../../../../../src/components/Personalization/handlers/proposition"; +//import { createProposition } from "../../../../../src/components/Personalization/handlers/proposition"; const METADATA = { home: { @@ -26,7 +26,7 @@ const METADATA = { } }; -describe("Personalization::createApplyPropositions", () => { +xdescribe("Personalization::createApplyPropositions", () => { let render; let pendingDisplayNotifications; let viewCache; diff --git a/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js b/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js index 41f68a7d5..9678938bb 100644 --- a/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js +++ b/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js @@ -11,10 +11,9 @@ 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", () => { +xdescribe("Personalization::createFetchDataHandler", () => { let prehidingStyle; let showContainers; let hideContainers; diff --git a/test/unit/specs/components/Personalization/createViewCacheManager.spec.js b/test/unit/specs/components/Personalization/createViewCacheManager.spec.js index a5cf73290..238e8b3b8 100644 --- a/test/unit/specs/components/Personalization/createViewCacheManager.spec.js +++ b/test/unit/specs/components/Personalization/createViewCacheManager.spec.js @@ -12,7 +12,7 @@ governing permissions and limitations under the License. import createViewCacheManager from "../../../../../src/components/Personalization/createViewCacheManager"; -describe("Personalization::createCacheManager", () => { +xdescribe("Personalization::createCacheManager", () => { const viewHandles = [ { id: "foo1", diff --git a/test/unit/specs/components/Personalization/createViewChangeHandler.spec.js b/test/unit/specs/components/Personalization/createViewChangeHandler.spec.js index 2d070c070..b27cf6abd 100644 --- a/test/unit/specs/components/Personalization/createViewChangeHandler.spec.js +++ b/test/unit/specs/components/Personalization/createViewChangeHandler.spec.js @@ -13,9 +13,9 @@ 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", () => { + +xdescribe("Personalization::createViewChangeHandler", () => { let mergeDecisionsMeta; let render; let viewCache; diff --git a/test/unit/specs/components/Personalization/handlers/createHtmlContentHandler.spec.js b/test/unit/specs/components/Personalization/handlers/createHtmlContentHandler.spec.js index 42e808047..f7387b148 100644 --- a/test/unit/specs/components/Personalization/handlers/createHtmlContentHandler.spec.js +++ b/test/unit/specs/components/Personalization/handlers/createHtmlContentHandler.spec.js @@ -1,6 +1,6 @@ import createHtmlContentHandler from "../../../../../../src/components/Personalization/handlers/createHtmlContentHandler"; -describe("Personalization::handlers::createHtmlContentHandler", () => { +xdescribe("Personalization::handlers::createHtmlContentHandler", () => { let next; let modules; let action1; diff --git a/test/unit/specs/components/Personalization/handlers/createProcessDomAction.spec.js b/test/unit/specs/components/Personalization/handlers/createProcessDomAction.spec.js new file mode 100644 index 000000000..cff5ac29c --- /dev/null +++ b/test/unit/specs/components/Personalization/handlers/createProcessDomAction.spec.js @@ -0,0 +1,67 @@ +import createProcessDomAction from "../../../../../../src/components/Personalization/handlers/createProcessDomAction"; + +describe("createProcessDomAction", () => { + let item; + let data; + let meta; + let modules; + let logger; + let storeClickMetrics; + let processDomAction; + + beforeEach(() => { + item = { + getData() { return data; }, + getMeta() { return meta; } + }; + modules = { + typeA: jasmine.createSpy("typeA"), + typeB: jasmine.createSpy("typeB") + }; + logger = jasmine.createSpyObj("logger", ["warn"]); + storeClickMetrics = jasmine.createSpy("storeClickMetrics"); + + processDomAction = createProcessDomAction({ modules, logger, storeClickMetrics }); + }); + + it("returns an empty object if the item has no data, and logs missing type", () => { + data = undefined; + expect(processDomAction(item)).toEqual({}); + expect(logger.warn).toHaveBeenCalledWith("Invalid DOM action data: missing type.", undefined); + }); + + it("returns an empty object if the item has no type, and logs missing type", () => { + data = {}; + expect(processDomAction(item)).toEqual({}); + expect(logger.warn).toHaveBeenCalledWith("Invalid DOM action data: missing type.", {}); + }); + + it("returns an empty object if the item has an unknown type, and logs unknown type", () => { + data = { type: "typeC" }; + expect(processDomAction(item)).toEqual({}); + expect(logger.warn).toHaveBeenCalledWith("Invalid DOM action data: unknown type.", { type: "typeC" }); + }); + + it("returns an empty object if the item has no selector for a click type, and logs missing selector", () => { + data = { type: "click" }; + expect(processDomAction(item)).toEqual({}); + expect(logger.warn).toHaveBeenCalledWith("Invalid DOM action data: missing selector.", { type: "click" }); + }); + + it("handles a click type", () => { + data = { type: "click", selector: ".selector" }; + meta = "mymetavalue"; + expect(processDomAction(item)).toEqual({ setRenderAttempted: true, includeInNotification: false }); + expect(storeClickMetrics).toHaveBeenCalledWith({ selector: ".selector", meta: "mymetavalue" }); + }); + + it("handles a non-click known type", () => { + data = { type: "typeA", a: "b" }; + const result = processDomAction(item); + expect(result).toEqual({ render: jasmine.any(Function), setRenderAttempted: true, includeInNotification: true }); + expect(modules.typeA).not.toHaveBeenCalled(); + result.render(); + expect(modules.typeA).toHaveBeenCalledWith({ type: "typeA", a: "b" }); + }); + +}); diff --git a/test/unit/specs/components/Personalization/handlers/createProcessHtmlContent.spec.js b/test/unit/specs/components/Personalization/handlers/createProcessHtmlContent.spec.js new file mode 100644 index 000000000..efd3c3e2f --- /dev/null +++ b/test/unit/specs/components/Personalization/handlers/createProcessHtmlContent.spec.js @@ -0,0 +1,57 @@ +import createProcessHtmlContent from "../../../../../../src/components/Personalization/handlers/createProcessHtmlContent"; + +describe("createProcessHtmlContent", () => { + + let modules; + let logger; + let item; + let data; + let processHtmlContent; + + beforeEach(() => { + modules = { + typeA: jasmine.createSpy("typeA"), + typeB: jasmine.createSpy("typeB") + }; + logger = jasmine.createSpyObj("logger", ["warn"]); + item = { + getData() { return data; } + }; + + processHtmlContent = createProcessHtmlContent({ modules, logger }); + }); + + it("returns an empty object if the item has no data", () => { + data = undefined; + expect(processHtmlContent(item)).toEqual({}); + expect(logger.warn).not.toHaveBeenCalled(); + }); + + it("returns an empty object if the item has no type", () => { + data = {selector: ".myselector"}; + expect(processHtmlContent(item)).toEqual({}); + expect(logger.warn).not.toHaveBeenCalled(); + }); + + it("returns an empty object if the item has no selector", () => { + data = {type: "mytype"}; + expect(processHtmlContent(item)).toEqual({}); + expect(logger.warn).not.toHaveBeenCalled(); + }); + + it("returns an empty object if the item has an unknown type, and logs unknown type", () => { + data = { type: "typeC", selector: ".myselector", content: "mycontent" }; + expect(processHtmlContent(item)).toEqual({}); + expect(logger.warn).toHaveBeenCalledWith("Invalid HTML content data", { type: "typeC", selector: ".myselector", content: "mycontent" }); + }); + + it("handles a known type", () => { + data = { type: "typeA", selector: ".myselector", content: "mycontent" }; + const result = processHtmlContent(item); + expect(result).toEqual({ render: jasmine.any(Function), setRenderAttempted: true, includeInNotification: true }); + expect(modules.typeA).not.toHaveBeenCalled(); + result.render(); + expect(modules.typeA).toHaveBeenCalledWith({ type: "typeA", selector: ".myselector", content: "mycontent" }); + }); + +}); diff --git a/test/unit/specs/components/Personalization/handlers/createProcessPropositions.spec.js b/test/unit/specs/components/Personalization/handlers/createProcessPropositions.spec.js new file mode 100644 index 000000000..1a690f682 --- /dev/null +++ b/test/unit/specs/components/Personalization/handlers/createProcessPropositions.spec.js @@ -0,0 +1,189 @@ +import createProcessPropositions from "../../../../../../src/components/Personalization/handlers/createProcessPropositions"; +import injectCreateProposition from "../../../../../../src/components/Personalization/handlers/injectCreateProposition"; + +describe("createProcessPropositions", () => { + let schemaProcessors; + let logger; + let createProposition; + let processPropositions; + + let render; + let always; + let noNotification; + let never; + let noRender; + let redirect; + + beforeEach(() => { + render = jasmine.createSpy("render"); + always = item => ({ render: () => render(item.getData()), setRenderAttempted: true, includeInNotification: true }); + noNotification = item => ({ render: () => render(item.getData()), setRenderAttempted: true, includeInNotification: false }); + never = () => ({}); + noRender = () => ({ setRenderAttempted: true, includeInNotification: true }); + redirect = item => ({ render: () => render(item.getData()), setRenderAttempted: true, onlyRenderThis: true}); + + schemaProcessors = { always, noNotification, never, noRender, redirect }; + logger = jasmine.createSpyObj("logger", ["info", "error"]); + processPropositions = createProcessPropositions({ schemaProcessors, logger }); + createProposition = injectCreateProposition({ preprocess: data => data }); + }); + + it("handles no propositions", async () => { + const result = processPropositions([]); + expect(result).toEqual({ render: jasmine.any(Function), returnedPropositions: [], returnedDecisions: [] }); + await expectAsync(result.render()).toBeResolvedTo([]); + }); + + it("processes a proposition with an always item", async () => { + const prop1 = createProposition({ id: "always1", scope: "myscope", scopeDetails: { a: 1 }, items: [ { schema: "always", data: "mydata" }] }); + const result = processPropositions([prop1]); + expect(result).toEqual({ render: jasmine.any(Function), returnedPropositions: [{ + id: "always1", + scope: "myscope", + scopeDetails: { a: 1 }, + items: [ { schema: "always", data: "mydata" }], + renderAttempted: true + }], returnedDecisions: [] }); + expect(render).not.toHaveBeenCalled(); + await expectAsync(result.render()).toBeResolvedTo([{ + id: "always1", + scope: "myscope", + scopeDetails: { a: 1 } + }]); + expect(render).toHaveBeenCalledWith("mydata"); + }); + + it("processes a proposition with a noNotification item", async () => { + const prop1 = createProposition({ id: "noNotification1", scope: "myscope", scopeDetails: { a: 1 }, items: [ { schema: "noNotification", data: "mydata" }] }); + const result = processPropositions([prop1]); + expect(result).toEqual({ render: jasmine.any(Function), returnedPropositions: [{ + id: "noNotification1", + scope: "myscope", + scopeDetails: { a: 1 }, + items: [ { schema: "noNotification", data: "mydata" }], + renderAttempted: true + }], returnedDecisions: [] }); + expect(render).not.toHaveBeenCalled(); + await expectAsync(result.render()).toBeResolvedTo([]); + expect(render).toHaveBeenCalledWith("mydata"); + }); + + it("processes a proposition with a never item", async () => { + const prop1 = createProposition({ id: "never1", scope: "myscope", scopeDetails: { a: 1 }, items: [ { schema: "never", data: "mydata" }] }); + const result = processPropositions([prop1]); + expect(result).toEqual({ render: jasmine.any(Function), returnedPropositions: [{ + id: "never1", + scope: "myscope", + scopeDetails: { a: 1 }, + items: [ { schema: "never", data: "mydata" }], + renderAttempted: false + }], returnedDecisions: [{ + id: "never1", + scope: "myscope", + scopeDetails: { a: 1 }, + items: [ { schema: "never", data: "mydata" }] + }]}); + await expectAsync(result.render()).toBeResolvedTo([]); + expect(render).not.toHaveBeenCalled(); + }); + + it("processes a proposition with a noRender item", async () => { + const prop1 = createProposition({ id: "noRender1", scope: "myscope", scopeDetails: { a: 1 }, items: [ { schema: "noRender", data: "mydata" }] }); + const result = processPropositions([prop1]); + expect(result).toEqual({ render: jasmine.any(Function), returnedPropositions: [{ + id: "noRender1", + scope: "myscope", + scopeDetails: { a: 1 }, + items: [ { schema: "noRender", data: "mydata" }], + renderAttempted: true + }], returnedDecisions: [] }); + await expectAsync(result.render()).toBeResolvedTo([{ + id: "noRender1", + scope: "myscope", + scopeDetails: { a: 1 } + }]); + expect(render).not.toHaveBeenCalled(); + }); + + it("processes a proposition with a redirect item", async () => { + const prop1 = createProposition({ id: "redirect1", scope: "myscope", scopeDetails: { a: 1 }, items: [ { schema: "redirect", data: "mydata" }] }); + const result = processPropositions([prop1]); + expect(result).toEqual({ render: jasmine.any(Function), returnedPropositions: [{ + id: "redirect1", + scope: "myscope", + scopeDetails: { a: 1 }, + items: [ { schema: "redirect", data: "mydata" }], + renderAttempted: true + }], returnedDecisions: [] }); + expect(render).not.toHaveBeenCalled(); + await expectAsync(result.render()).toBeResolvedTo([]); + expect(render).toHaveBeenCalledWith("mydata"); + }); + + it("doesn't render other propositions if one has a redirect", async () => { + const prop1 = createProposition({ id: "always1", scope: "myscope", scopeDetails: { a: 1 }, items: [ { schema: "always", data: "mydata1" }] }); + const prop2 = createProposition({ id: "redirect2", scope: "myscope", scopeDetails: { a: 2 }, items: [ { schema: "redirect", data: "mydata2" }] }); + const prop3 = createProposition({ id: "always3", scope: "myscope", scopeDetails: { a: 3 }, items: [ { schema: "always", data: "mydata3" }] }); + const result = processPropositions([prop1, prop2, prop3]); + expect(result).toEqual({ + render: jasmine.any(Function), + returnedPropositions: [ + { + id: "redirect2", + scope: "myscope", + scopeDetails: { a: 2 }, + items: [ { schema: "redirect", data: "mydata2" }], + renderAttempted: true + },{ + id: "always1", + scope: "myscope", + scopeDetails: { a: 1 }, + items: [ { schema: "always", data: "mydata1" }], + renderAttempted: false + },{ + id: "always3", + scope: "myscope", + scopeDetails: { a: 3 }, + items: [ { schema: "always", data: "mydata3" }], + renderAttempted: false + } + ], + returnedDecisions: [ + { + id: "always1", + scope: "myscope", + scopeDetails: { a: 1 }, + items: [ { schema: "always", data: "mydata1" }] + },{ + id: "always3", + scope: "myscope", + scopeDetails: { a: 3 }, + items: [ { schema: "always", data: "mydata3" }] + } + ] + }); + expect(render).not.toHaveBeenCalled(); + await expectAsync(result.render()).toBeResolvedTo([]); + expect(render).toHaveBeenCalledWith("mydata2"); + }); + + it("processes nonRenderPropositions", async () => { + const prop1 = createProposition({ id: "always1", scope: "myscope", scopeDetails: { a: 1 }, items: [ { schema: "always", data: "mydata" }] }); + const result = processPropositions([], [prop1]); + expect(result).toEqual({ render: jasmine.any(Function), returnedPropositions: [{ + id: "always1", + scope: "myscope", + scopeDetails: { a: 1 }, + items: [ { schema: "always", data: "mydata" }], + renderAttempted: false + }], returnedDecisions: [{ + id: "always1", + scope: "myscope", + scopeDetails: { a: 1 }, + items: [ { schema: "always", data: "mydata" }] + }]}); + await expectAsync(result.render()).toBeResolvedTo([]); + expect(render).not.toHaveBeenCalled(); + }); + +}); diff --git a/test/unit/specs/components/Personalization/handlers/createProcessRedirect.spec.js b/test/unit/specs/components/Personalization/handlers/createProcessRedirect.spec.js new file mode 100644 index 000000000..92ad441a0 --- /dev/null +++ b/test/unit/specs/components/Personalization/handlers/createProcessRedirect.spec.js @@ -0,0 +1,68 @@ +import { defer } from "../../../../../../src/utils"; +import flushPromiseChains from "../../../../helpers/flushPromiseChains"; +import createProcessRedirect from "../../../../../../src/components/Personalization/handlers/createProcessRedirect"; + +describe("createProcessRedirect", () => { + + let logger; + let executeRedirect; + let collect; + let collectDefer; + let item; + let data; + let meta; + + let processRedirect; + + beforeEach(() => { + logger = jasmine.createSpyObj("logger", ["warn"]); + executeRedirect = jasmine.createSpy("executeRedirect"); + collectDefer = defer(); + collect = jasmine.createSpy("collect").and.returnValue(collectDefer.promise); + item = { + getData() { return data; }, + getMeta() { return meta; } + }; + + processRedirect = createProcessRedirect({ logger, executeRedirect, collect }); + }); + + it("returns an empty object if the item has no data", () => { + data = undefined; + expect(processRedirect(item)).toEqual({}); + expect(logger.warn).toHaveBeenCalledWith("Invalid Redirect data", undefined); + }); + + it("returns an empty object if the item has no content", () => { + data = {a: 1}; + expect(processRedirect(item)).toEqual({}); + expect(logger.warn).toHaveBeenCalledWith("Invalid Redirect data", {a: 1}); + }); + + it("redirects", async () => { + data = { content: "mycontent" }; + meta = "mymetavalue"; + const result = processRedirect(item); + expect(result).toEqual({ render: jasmine.any(Function), setRenderAttempted: true, onlyRenderThis: true }); + expect(collect).not.toHaveBeenCalled(); + expect(executeRedirect).not.toHaveBeenCalled(); + const renderPromise = result.render(); + await flushPromiseChains(); + expect(collect).toHaveBeenCalledWith({ decisionsMeta: ["mymetavalue"] }); + expect(executeRedirect).not.toHaveBeenCalled(); + collectDefer.resolve(); + await flushPromiseChains(); + expect(executeRedirect).toHaveBeenCalledWith("mycontent"); + expect(await renderPromise).toBeUndefined(); + }); + + it("doesn't eat the exception", async () => { + data = { content: "mycontent" }; + meta = "mymetavalue"; + const result = processRedirect(item); + const renderPromise = result.render(); + collectDefer.reject("myerror"); + await expectAsync(renderPromise).toBeRejectedWith("myerror"); + }); + +}); diff --git a/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js b/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js index b7fa3a280..b12422226 100644 --- a/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js +++ b/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js @@ -1,7 +1,6 @@ import createRedirectHandler from "../../../../../../src/components/Personalization/handlers/createRedirectHandler"; -import { createProposition } from "../../../../../../src/components/Personalization/handlers/proposition"; -describe("redirectHandler", () => { +xdescribe("redirectHandler", () => { let next; let redirectHandler; diff --git a/test/unit/specs/components/Personalization/handlers/createRender.spec.js b/test/unit/specs/components/Personalization/handlers/createRender.spec.js index c191c568e..27476ef55 100644 --- a/test/unit/specs/components/Personalization/handlers/createRender.spec.js +++ b/test/unit/specs/components/Personalization/handlers/createRender.spec.js @@ -1,6 +1,6 @@ -import createRender from "../../../../../../src/components/Personalization/handlers/createRender"; +import createProcessPropositions from "../../../../../../src/components/Personalization/handlers/createProcessPropositions"; -describe("Personalization::handlers::createRender", () => { +xdescribe("Personalization::handlers::createRender", () => { let handleChain; let collect; let executeRedirect; diff --git a/test/unit/specs/components/Personalization/handlers/createRenderDomAction.spec.js b/test/unit/specs/components/Personalization/handlers/createRenderDomAction.spec.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/specs/components/Personalization/handlers/injectCreateProposition.spec.js b/test/unit/specs/components/Personalization/handlers/injectCreateProposition.spec.js new file mode 100644 index 000000000..2c8cd1c52 --- /dev/null +++ b/test/unit/specs/components/Personalization/handlers/injectCreateProposition.spec.js @@ -0,0 +1,73 @@ +import injectCreateProposition from "../../../../../../src/components/Personalization/handlers/injectCreateProposition"; + +describe("injectCreateProposition", () => { + const preprocess = data => `preprocessed ${data}`; + const isPageWideSurface = scope => scope === "__surface__"; + const createProposition = injectCreateProposition({ preprocess, isPageWideSurface }); + + it("creates a proposition from nothing", () => { + const proposition = createProposition({}); + + expect(proposition.getScope()).toBeUndefined(); + expect(proposition.getScopeType()).toEqual("proposition"); + expect(proposition.getItems()).toEqual([]); + expect(proposition.getNotification()).toEqual({ + id: undefined, + scope: undefined, + scopeDetails: undefined + }); + expect(proposition.toJSON()).toEqual({}); + }); + + it("creates a full proposition", () => { + const proposition = createProposition({ + id: "id", + scope: "scope", + scopeDetails: { characteristics: { scopeType: "view" } }, + items: [ + { + schema: "schema", + data: "data", + characteristics: { trackingLabel: "trackingLabel" } + } + ] + }); + + expect(proposition.getScope()).toEqual("scope"); + expect(proposition.getScopeType()).toEqual("view"); + const item = proposition.getItems()[0]; + expect(item.getSchema()).toEqual("schema"); + expect(item.getData()).toEqual("preprocessed data"); + expect(item.getMeta()).toEqual({ + id: "id", + scope: "scope", + scopeDetails: { characteristics: { scopeType: "view" } }, + trackingLabel: "trackingLabel" + }); + expect(item.getOriginalItem()).toEqual({ + schema: "schema", + data: "data", + characteristics: { trackingLabel: "trackingLabel" } + }); + expect(proposition.getNotification()).toEqual({ + id: "id", + scope: "scope", + scopeDetails: { characteristics: { scopeType: "view" } } + }); + }); + + it("creates a page wide surface proposition", () => { + const proposition = createProposition({ + scope: "__surface__" + }); + expect(proposition.getScopeType()).toEqual("page"); + }); + + it("creates a page wide scope proposition", () => { + const proposition = createProposition({ + scope: "__view__" + }); + expect(proposition.getScopeType()).toEqual("page"); + }); + +}); diff --git a/test/unit/specs/components/Personalization/handlers/processDefaultContent.spec.js b/test/unit/specs/components/Personalization/handlers/processDefaultContent.spec.js new file mode 100644 index 000000000..e6f8d8519 --- /dev/null +++ b/test/unit/specs/components/Personalization/handlers/processDefaultContent.spec.js @@ -0,0 +1,10 @@ +import processDefaultContent from "../../../../../../src/components/Personalization/handlers/processDefaultContent"; + +describe("processDefaultContent", () => { + + it("always renders the default content", () => { + const result = processDefaultContent(); + expect(result).toEqual({ setRenderAttempted: true, includeInNotification: true }); + }); + +}); diff --git a/test/unit/specs/components/Personalization/handlers/proposition.spec.js b/test/unit/specs/components/Personalization/handlers/proposition.spec.js index b501990e0..a769ffcee 100644 --- a/test/unit/specs/components/Personalization/handlers/proposition.spec.js +++ b/test/unit/specs/components/Personalization/handlers/proposition.spec.js @@ -1,10 +1,4 @@ -import { - createProposition, - buildReturnedDecisions, - buildReturnedPropositions -} from "../../../../../../src/components/Personalization/handlers/proposition"; - -describe("Personalization::handlers", () => { +xdescribe("Personalization::handlers", () => { describe("createProposition", () => { it("returns the handle", () => { const handle = { id: "id", scope: "scope", scopeDetails: "scopeDetails" }; diff --git a/test/unit/specs/components/Personalization/topLevel/buildAlloy.js b/test/unit/specs/components/Personalization/topLevel/buildAlloy.js index 0cd721768..867baad18 100644 --- a/test/unit/specs/components/Personalization/topLevel/buildAlloy.js +++ b/test/unit/specs/components/Personalization/topLevel/buildAlloy.js @@ -27,15 +27,16 @@ import createClickStorage from "../../../../../../src/components/Personalization 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"; +import injectCreateProposition from "../../../../../../src/components/Personalization/handlers/injectCreateProposition"; +import createProcessPropositions from "../../../../../../src/components/Personalization/handlers/createProcessPropositions"; import createAsyncArray from "../../../../../../src/components/Personalization/utils/createAsyncArray"; import createPendingNotificationsHandler from "../../../../../../src/components/Personalization/createPendingNotificationsHandler"; +import * as schema from "../../../../../../src/components/Personalization/constants/schema"; +import createProcessDomAction from "../../../../../../src/components/Personalization/handlers/createProcessDomAction"; +import createProcessHtmlContent from "../../../../../../src/components/Personalization/handlers/createProcessHtmlContent"; +import createProcessRedirect from "../../../../../../src/components/Personalization/handlers/createProcessRedirect"; +import processDefaultContent from "../../../../../../src/components/Personalization/handlers/processDefaultContent"; +import { isPageWideSurface } from "../../../../../../src/components/Personalization/utils/surfaceUtils"; const createAction = renderFunc => ({ selector, content }) => { renderFunc(selector, content); @@ -84,36 +85,29 @@ const buildComponent = ({ storeClickMetrics } = createClickStorage(); + const preprocess = action => action; + const createProposition = injectCreateProposition({ preprocess, isPageWideSurface }); + 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 schemaProcessors = { + [schema.DEFAULT_CONTENT_ITEM]: processDefaultContent, + [schema.DOM_ACTION]: createProcessDomAction({ modules, logger, storeClickMetrics }), + [schema.HTML_CONTENT_ITEM]: createProcessHtmlContent({ modules, logger }), + [schema.REDIRECT_ITEM]: createProcessRedirect({ + logger, + executeRedirect: url => window.location.replace(url), + collect + }) + }; + + const processPropositions = createProcessPropositions({ + schemaProcessors, + logger, }); + const pendingDisplayNotifications = createAsyncArray(); const pendingNotificationsHandler = createPendingNotificationsHandler({ pendingDisplayNotifications, @@ -125,7 +119,8 @@ const buildComponent = ({ hideContainers, mergeQuery, collect, - render, + processPropositions, + createProposition, pendingDisplayNotifications }); const onClickHandler = createOnClickHandler({ @@ -136,11 +131,12 @@ const buildComponent = ({ }); const viewChangeHandler = createViewChangeHandler({ mergeDecisionsMeta, - render, + processPropositions, viewCache }); const applyPropositions = createApplyPropositions({ - render, + processPropositions, + createProposition, pendingDisplayNotifications, viewCache }); diff --git a/test/unit/specs/components/Personalization/topLevel/mixedPropositions.spec.js b/test/unit/specs/components/Personalization/topLevel/mixedPropositions.spec.js index 96efe02cc..d378ca581 100644 --- a/test/unit/specs/components/Personalization/topLevel/mixedPropositions.spec.js +++ b/test/unit/specs/components/Personalization/topLevel/mixedPropositions.spec.js @@ -3,6 +3,7 @@ import { MIXED_PROPOSITIONS } from "../responsesMock/eventResponses"; import buildMocks from "./buildMocks"; import buildAlloy from "./buildAlloy"; import resetMocks from "./resetMocks"; +import flushPromiseChains from "../../../../helpers/flushPromiseChains"; describe("PersonalizationComponent", () => { it("MIXED_PROPOSITIONS", async () => { @@ -260,6 +261,7 @@ describe("PersonalizationComponent", () => { ]); expect(applyPropositionsResult.decisions).toBeUndefined(); + await flushPromiseChains(); expect(mocks.sendEvent).not.toHaveBeenCalled(); expect(mocks.actions.appendHtml).toHaveBeenCalledOnceWith( "#myhomeselector", diff --git a/test/unit/specs/components/Personalization/topLevel/scopesFoo1Foo2Decisions.spec.js b/test/unit/specs/components/Personalization/topLevel/scopesFoo1Foo2Decisions.spec.js index cf5e76d12..fad11f532 100644 --- a/test/unit/specs/components/Personalization/topLevel/scopesFoo1Foo2Decisions.spec.js +++ b/test/unit/specs/components/Personalization/topLevel/scopesFoo1Foo2Decisions.spec.js @@ -28,6 +28,7 @@ describe("PersonalizationComponent", () => { } } }); + expect(result).toEqual({ propositions: [ { From e9b5acfdb19e187d92002c2a0eb5fdadd888e1ca Mon Sep 17 00:00:00 2001 From: Jon Snyder Date: Wed, 13 Sep 2023 21:36:05 -0600 Subject: [PATCH 17/20] Remove unused code, fix unit tests for personalization handlers --- .../createApplyPropositions.js | 15 +- .../Personalization/createFetchDataHandler.js | 40 ++- .../Personalization/createViewCacheManager.js | 24 +- .../createViewChangeHandler.js | 17 +- .../handlers/createDomActionHandler.js | 53 ---- .../createMeasurementSchemaHandler.js | 22 -- .../handlers/createProcessDomAction.js | 6 +- .../handlers/createProcessHtmlContent.js | 2 +- .../handlers/createProcessPropositions.js | 92 ++++-- .../handlers/createProcessRedirect.js | 11 +- .../handlers/createRedirectHandler.js | 28 -- .../handlers/injectCreateProposition.js | 8 +- src/components/Personalization/index.js | 33 ++- src/utils/deduplicateArray.js | 2 +- .../createApplyPropositions.spec.js | 86 ++---- .../createFetchDataHandler.spec.js | 48 ++-- .../createViewCacheManager.spec.js | 69 ++--- .../createViewChangeHandler.spec.js | 77 ++---- .../handlers/createDomActionHandler.spec.js | 164 ----------- .../handlers/createHtmlContentHandler.spec.js | 110 -------- .../createMeasurementSchemaHandler.spec.js | 35 --- .../handlers/createProcessDomAction.spec.js | 55 +++- .../handlers/createProcessHtmlContent.spec.js | 28 +- .../createProcessPropositions.spec.js | 261 +++++++++++++----- .../handlers/createProcessRedirect.spec.js | 35 ++- .../handlers/createRedirectHandler.spec.js | 125 --------- .../handlers/createRender.spec.js | 87 ------ .../handlers/createRenderDomAction.spec.js | 0 .../handlers/injectCreateProposition.spec.js | 6 +- .../handlers/processDefaultContent.spec.js | 7 +- .../handlers/proposition.spec.js | 244 ---------------- .../Personalization/topLevel/buildAlloy.js | 16 +- 32 files changed, 566 insertions(+), 1240 deletions(-) delete mode 100644 src/components/Personalization/handlers/createDomActionHandler.js delete mode 100644 src/components/Personalization/handlers/createMeasurementSchemaHandler.js delete mode 100644 src/components/Personalization/handlers/createRedirectHandler.js delete mode 100644 test/unit/specs/components/Personalization/handlers/createDomActionHandler.spec.js delete mode 100644 test/unit/specs/components/Personalization/handlers/createHtmlContentHandler.spec.js delete mode 100644 test/unit/specs/components/Personalization/handlers/createMeasurementSchemaHandler.spec.js delete mode 100644 test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js delete mode 100644 test/unit/specs/components/Personalization/handlers/createRender.spec.js delete mode 100644 test/unit/specs/components/Personalization/handlers/createRenderDomAction.spec.js delete mode 100644 test/unit/specs/components/Personalization/handlers/proposition.spec.js diff --git a/src/components/Personalization/createApplyPropositions.js b/src/components/Personalization/createApplyPropositions.js index 1c614a48c..7da67eb01 100644 --- a/src/components/Personalization/createApplyPropositions.js +++ b/src/components/Personalization/createApplyPropositions.js @@ -16,7 +16,12 @@ import PAGE_WIDE_SCOPE from "../../constants/pageWideScope"; const SUPPORTED_SCHEMAS = [DOM_ACTION, HTML_CONTENT_ITEM]; -export default ({ processPropositions, createProposition, pendingDisplayNotifications, viewCache }) => { +export default ({ + processPropositions, + createProposition, + pendingDisplayNotifications, + viewCache +}) => { const filterItemsPredicate = item => SUPPORTED_SCHEMAS.indexOf(item.schema) > -1; @@ -70,7 +75,7 @@ export default ({ processPropositions, createProposition, pendingDisplayNotifica }; return ({ propositions = [], metadata = {}, viewName }) => { - let propositionsToExecute = preparePropositions({ + const propositionsToExecute = preparePropositions({ propositions, metadata }).map(proposition => createProposition(proposition)); @@ -83,8 +88,10 @@ export default ({ processPropositions, createProposition, pendingDisplayNotifica return []; }) .then(additionalPropositions => { - const { render, returnedPropositions } = - processPropositions([ ...propositionsToExecute, ...additionalPropositions]); + const { render, returnedPropositions } = processPropositions([ + ...propositionsToExecute, + ...additionalPropositions + ]); pendingDisplayNotifications.concat(render()); diff --git a/src/components/Personalization/createFetchDataHandler.js b/src/components/Personalization/createFetchDataHandler.js index fae2473a1..e2b9609fe 100644 --- a/src/components/Personalization/createFetchDataHandler.js +++ b/src/components/Personalization/createFetchDataHandler.js @@ -56,21 +56,37 @@ export default ({ const currentViewPropositions = cacheUpdate.update(viewPropositions); - let render, returnedPropositions, returnedDecisions; + let render; + let returnedPropositions; + let returnedDecisions; if (personalizationDetails.isRenderDecisions()) { - ({ render, returnedPropositions, returnedDecisions } = - processPropositions([...pagePropositions, ...currentViewPropositions], nonRenderedPropositions)); - render().then(decisionsMeta => { - showContainers(); - handleNotifications(decisionsMeta); - }).catch(e => { - showContainers(); - throw e; - }); + ({ + render, + returnedPropositions, + returnedDecisions + } = processPropositions( + [...pagePropositions, ...currentViewPropositions], + nonRenderedPropositions + )); + render() + .then(decisionsMeta => { + showContainers(); + handleNotifications(decisionsMeta); + }) + .catch(e => { + showContainers(); + throw e; + }); } else { - ({ returnedPropositions, returnedDecisions } = - processPropositions([], [...pagePropositions, ...currentViewPropositions, ...nonRenderedPropositions])); + ({ returnedPropositions, returnedDecisions } = processPropositions( + [], + [ + ...pagePropositions, + ...currentViewPropositions, + ...nonRenderedPropositions + ] + )); } return { diff --git a/src/components/Personalization/createViewCacheManager.js b/src/components/Personalization/createViewCacheManager.js index 6367a0d34..22a751975 100644 --- a/src/components/Personalization/createViewCacheManager.js +++ b/src/components/Personalization/createViewCacheManager.js @@ -14,7 +14,6 @@ import { assign, groupBy } from "../../utils"; import defer from "../../utils/defer"; export default ({ createProposition }) => { - const viewStorage = {}; let cacheUpdateCreatedAtLeastOnce = false; let previousUpdateCacheComplete = Promise.resolve(); @@ -25,14 +24,17 @@ export default ({ createProposition }) => { return viewPropositions; } - const emptyViewProposition = createProposition({ - scope: viewName, - scopeDetails: { - characteristics: { - scopeType: "view" + const emptyViewProposition = createProposition( + { + scope: viewName, + scopeDetails: { + characteristics: { + scopeType: "view" + } } - } - }, false); + }, + false + ); return [emptyViewProposition]; }; @@ -50,10 +52,12 @@ export default ({ createProposition }) => { return { update(viewPropositions) { - const newViewStorage = groupBy(viewPropositions, proposition => proposition.getScope()); + const newViewStorage = groupBy(viewPropositions, proposition => + proposition.getScope() + ); updateCacheDeferred.resolve(newViewStorage); if (viewName) { - return getViewPropositions(newViewStorage, viewName) + return getViewPropositions(newViewStorage, viewName); } return []; }, diff --git a/src/components/Personalization/createViewChangeHandler.js b/src/components/Personalization/createViewChangeHandler.js index 06ec8fe5b..2f68cd8eb 100644 --- a/src/components/Personalization/createViewChangeHandler.js +++ b/src/components/Personalization/createViewChangeHandler.js @@ -14,7 +14,8 @@ import { PropositionEventType } from "./constants/propositionEventType"; export default ({ mergeDecisionsMeta, processPropositions, viewCache }) => { return ({ personalizationDetails, event, onResponse }) => { - let returnedPropositions, returnedDecisions; + let returnedPropositions; + let returnedDecisions; const viewName = personalizationDetails.getViewName(); onResponse(() => { @@ -29,12 +30,18 @@ export default ({ mergeDecisionsMeta, processPropositions, viewCache }) => { .then(propositions => { let render; if (personalizationDetails.isRenderDecisions()) { - ({ render, returnedPropositions, returnedDecisions } = processPropositions(propositions)); + ({ + render, + returnedPropositions, + returnedDecisions + } = processPropositions(propositions)); return render(); - } else { - ({ returnedPropositions, returnedDecisions } = processPropositions([], propositions)); - return []; } + ({ returnedPropositions, returnedDecisions } = processPropositions( + [], + propositions + )); + return []; }) .then(decisionsMeta => { mergeDecisionsMeta(event, decisionsMeta, PropositionEventType.DISPLAY); diff --git a/src/components/Personalization/handlers/createDomActionHandler.js b/src/components/Personalization/handlers/createDomActionHandler.js deleted file mode 100644 index b4b085da0..000000000 --- a/src/components/Personalization/handlers/createDomActionHandler.js +++ /dev/null @@ -1,53 +0,0 @@ -/* -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); -}; - - -/* -1. Anything scope type view we put in the view storage. -2. Anything page wide scope we try to render. -3. Anything else we only render if its Dom-action schema -*/ diff --git a/src/components/Personalization/handlers/createMeasurementSchemaHandler.js b/src/components/Personalization/handlers/createMeasurementSchemaHandler.js deleted file mode 100644 index b6ec90680..000000000 --- a/src/components/Personalization/handlers/createMeasurementSchemaHandler.js +++ /dev/null @@ -1,22 +0,0 @@ -/* -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 { MEASUREMENT_SCHEMA } from "../constants/schema"; - -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); - } -}; diff --git a/src/components/Personalization/handlers/createProcessDomAction.js b/src/components/Personalization/handlers/createProcessDomAction.js index 46af90c9c..dd825a16b 100644 --- a/src/components/Personalization/handlers/createProcessDomAction.js +++ b/src/components/Personalization/handlers/createProcessDomAction.js @@ -31,5 +31,9 @@ export default ({ modules, logger, storeClickMetrics }) => item => { return {}; } - return { render: () => modules[type](item.getData()), setRenderAttempted: true, includeInNotification: true }; + return { + render: () => modules[type](item.getData()), + setRenderAttempted: true, + includeInNotification: true + }; }; diff --git a/src/components/Personalization/handlers/createProcessHtmlContent.js b/src/components/Personalization/handlers/createProcessHtmlContent.js index 45abd187b..a19fcba2f 100644 --- a/src/components/Personalization/handlers/createProcessHtmlContent.js +++ b/src/components/Personalization/handlers/createProcessHtmlContent.js @@ -9,7 +9,7 @@ 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. */ -export default ({ modules, logger }) => (item) => { +export default ({ modules, logger }) => item => { const { type, selector } = item.getData() || {}; if (!selector || !type) { diff --git a/src/components/Personalization/handlers/createProcessPropositions.js b/src/components/Personalization/handlers/createProcessPropositions.js index 186d99d5e..d58b494a6 100644 --- a/src/components/Personalization/handlers/createProcessPropositions.js +++ b/src/components/Personalization/handlers/createProcessPropositions.js @@ -10,8 +10,7 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -export default ({ schemaProcessors, logger}) => { - +export default ({ schemaProcessors, logger }) => { const wrapRenderWithLogging = (render, item) => () => { return Promise.resolve() .then(render) @@ -32,8 +31,7 @@ export default ({ schemaProcessors, logger}) => { }; const renderItems = (renderers, meta) => - Promise.all(renderers.map(renderer => renderer())) - .then(successes => { + Promise.all(renderers.map(renderer => renderer())).then(successes => { // as long as at least one renderer succeeds, we want to add the notification // to the display notifications if (!successes.includes(true)) { @@ -42,7 +40,7 @@ export default ({ schemaProcessors, logger}) => { return meta; }); - const processItem = (item) => { + const processItem = item => { const processor = schemaProcessors[item.getSchema()]; if (!processor) { return {}; @@ -73,7 +71,12 @@ export default ({ schemaProcessors, logger}) => { while (items.length > i) { item = items[i]; - ({ render, setRenderAttempted, includeInNotification, onlyRenderThis } = processItem(item)); + ({ + render, + setRenderAttempted, + includeInNotification, + onlyRenderThis + } = processItem(item)); if (onlyRenderThis) { returnedPropositions = []; returnedDecisions = []; @@ -103,19 +106,36 @@ export default ({ schemaProcessors, logger}) => { i += 1; } if (itemRenderers.length > 0) { - const meta = atLeastOneWithNotification ? proposition.getNotification() : undefined; + const meta = atLeastOneWithNotification + ? proposition.getNotification() + : undefined; renderers.push(() => renderItems(itemRenderers, meta)); } else if (atLeastOneWithNotification) { renderers.push(() => proposition.getNotification()); } if (renderedItems.length > 0) { - proposition.addToReturnValues(returnedPropositions, returnedDecisions, renderedItems, true); + proposition.addToReturnValues( + returnedPropositions, + returnedDecisions, + renderedItems, + true + ); } if (nonRenderedItems.length > 0) { - proposition.addToReturnValues(returnedPropositions, returnedDecisions, nonRenderedItems, false); + proposition.addToReturnValues( + returnedPropositions, + returnedDecisions, + nonRenderedItems, + false + ); } - return { renderers, returnedPropositions, returnedDecisions, onlyRenderThis }; + return { + renderers, + returnedPropositions, + returnedDecisions, + onlyRenderThis + }; }; return (renderPropositions, nonRenderPropositions = []) => { @@ -124,31 +144,59 @@ export default ({ schemaProcessors, logger}) => { let returnedDecisions = []; let onlyRenderThis; let i = 0; - let proposition, items; + let proposition; + let items; while (renderPropositions.length > i) { proposition = renderPropositions[i]; items = proposition.getItems(); - ({ renderers, returnedPropositions, returnedDecisions, onlyRenderThis } = - processItems({ renderers, returnedPropositions, returnedDecisions, items, proposition })); + ({ + renderers, + returnedPropositions, + returnedDecisions, + onlyRenderThis + } = processItems({ + renderers, + returnedPropositions, + returnedDecisions, + items, + proposition + })); if (onlyRenderThis) { - renderPropositions.forEach((p, index) => { - if (index !== i) { - p.addToReturnValues(returnedPropositions, returnedDecisions, p.getItems(), false); - } - }); break; } i += 1; } + if (onlyRenderThis) { + // if onlyRenderThis is true, that means returnedPropositions and returnedDecisions + // only contains the proposition that triggered only rendering this. We need to + // add the other propositions to the returnedPropositions and returnedDecisions. + renderPropositions.forEach((p, index) => { + if (index !== i) { + p.addToReturnValues( + returnedPropositions, + returnedDecisions, + p.getItems(), + false + ); + } + }); + } + nonRenderPropositions.forEach(p => { - p.addToReturnValues(returnedPropositions, returnedDecisions, p.getItems(), false); + p.addToReturnValues( + returnedPropositions, + returnedDecisions, + p.getItems(), + false + ); }); const render = () => { - return Promise.all(renderers.map(renderer => renderer())) - .then(metas => metas.filter(meta => meta)); + return Promise.all(renderers.map(renderer => renderer())).then(metas => + metas.filter(meta => meta) + ); }; - return { returnedPropositions, returnedDecisions, render}; + return { returnedPropositions, returnedDecisions, render }; }; }; diff --git a/src/components/Personalization/handlers/createProcessRedirect.js b/src/components/Personalization/handlers/createProcessRedirect.js index fbb3801e2..4f03aa5ae 100644 --- a/src/components/Personalization/handlers/createProcessRedirect.js +++ b/src/components/Personalization/handlers/createProcessRedirect.js @@ -18,12 +18,11 @@ export default ({ logger, executeRedirect, collect }) => item => { } const render = () => { - return collect({ decisionsMeta: [item.getMeta()] }) - .then(() => { - executeRedirect(content); - // We've already sent the display notification, so don't return anything - }); - } + return collect({ decisionsMeta: [item.getMeta()] }).then(() => { + executeRedirect(content); + // We've already sent the display notification, so don't return anything + }); + }; return { render, setRenderAttempted: true, onlyRenderThis: true }; }; diff --git a/src/components/Personalization/handlers/createRedirectHandler.js b/src/components/Personalization/handlers/createRedirectHandler.js deleted file mode 100644 index eed92e493..000000000 --- a/src/components/Personalization/handlers/createRedirectHandler.js +++ /dev/null @@ -1,28 +0,0 @@ -/* -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 { REDIRECT_ITEM } from "../constants/schema"; -import { find } from "../../../utils"; - -export default ({ next }) => proposition => { - const { items = [] } = proposition.getHandle() || {}; - - 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/injectCreateProposition.js b/src/components/Personalization/handlers/injectCreateProposition.js index 65f1cd76c..fc63f82f3 100644 --- a/src/components/Personalization/handlers/injectCreateProposition.js +++ b/src/components/Personalization/handlers/injectCreateProposition.js @@ -13,7 +13,6 @@ governing permissions and limitations under the License. import PAGE_WIDE_SCOPE from "../../../constants/pageWideScope"; export default ({ preprocess, isPageWideSurface }) => { - const createItem = (item, meta) => { const { schema, data, characteristics: { trackingLabel } = {} } = item; @@ -71,7 +70,12 @@ export default ({ preprocess, isPageWideSurface }) => { toJSON() { return payload; }, - addToReturnValues(propositions, decisions, includedItems, renderAttempted) { + addToReturnValues( + propositions, + decisions, + includedItems, + renderAttempted + ) { if (visibleInReturnedItems) { propositions.push({ ...payload, diff --git a/src/components/Personalization/index.js b/src/components/Personalization/index.js index 0b2efd52c..a0b9ffcd2 100644 --- a/src/components/Personalization/index.js +++ b/src/components/Personalization/index.js @@ -26,20 +26,19 @@ import createClickStorage from "./createClickStorage"; 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/createProcessPropositions"; import remapCustomCodeOffers from "./dom-actions/remapCustomCodeOffers"; import remapHeadOffers from "./dom-actions/remapHeadOffers"; import createPreprocess from "./dom-actions/createPreprocess"; -import injectCreateProposition, { createProposition } from "./handlers/injectCreateProposition"; +import injectCreateProposition from "./handlers/injectCreateProposition"; import createAsyncArray from "./utils/createAsyncArray"; import createPendingNotificationsHandler from "./createPendingNotificationsHandler"; import * as schema from "./constants/schema"; import processDefaultContent from "./handlers/processDefaultContent"; import { isPageWideSurface } from "./utils/surfaceUtils"; +import createProcessDomAction from "./handlers/createProcessDomAction"; +import createProcessHtmlContent from "./handlers/createProcessHtmlContent"; +import createProcessRedirect from "./handlers/createProcessRedirect"; +import createProcessPropositions from "./handlers/createProcessPropositions"; const createPersonalization = ({ config, logger, eventManager }) => { const { targetMigrationEnabled, prehidingStyle } = config; @@ -51,22 +50,32 @@ const createPersonalization = ({ config, logger, eventManager }) => { storeClickMetrics } = createClickStorage(); const getPageLocation = createGetPageLocation({ window }); - const viewCache = createViewCacheManager({ createProposition }); const modules = initDomActionsModules(); const preprocess = createPreprocess([remapHeadOffers, remapCustomCodeOffers]); - const createProposition = injectCreateProposition({ preprocess, isPageWideSurface }); + const createProposition = injectCreateProposition({ + preprocess, + isPageWideSurface + }); + const viewCache = createViewCacheManager({ createProposition }); const schemaProcessors = { [schema.DEFAULT_CONTENT_ITEM]: processDefaultContent, - [schema.DOM_ACTION]: createProcessDomAction({ modules, logger, storeClickMetrics }), + [schema.DOM_ACTION]: createProcessDomAction({ + modules, + logger, + storeClickMetrics + }), [schema.HTML_CONTENT_ITEM]: createProcessHtmlContent({ modules, logger }), - [schema.REDIRECT_ITEM]: createProcessRedirect({ logger, executeRedirect: url => window.location.replace(url) }) + [schema.REDIRECT_ITEM]: createProcessRedirect({ + logger, + executeRedirect: url => window.location.replace(url) + }) }; const processPropositions = createProcessPropositions({ schemaProcessors, - logger, + logger }); const pendingDisplayNotifications = createAsyncArray(); @@ -96,7 +105,7 @@ const createPersonalization = ({ config, logger, eventManager }) => { viewCache }); const applyPropositions = createApplyPropositions({ - processProposition, + processPropositions, createProposition, pendingDisplayNotifications, viewCache diff --git a/src/utils/deduplicateArray.js b/src/utils/deduplicateArray.js index e4fd4beca..02c2eaf5a 100644 --- a/src/utils/deduplicateArray.js +++ b/src/utils/deduplicateArray.js @@ -1,7 +1,7 @@ const REFERENCE_EQUALITY = (a, b) => a === b; const findIndex = (array, item, isEqual) => { - for (let i = 0; i < array.length; i++) { + for (let i = 0; i < array.length; i += 1) { if (isEqual(array[i], item)) { return i; } diff --git a/test/unit/specs/components/Personalization/createApplyPropositions.spec.js b/test/unit/specs/components/Personalization/createApplyPropositions.spec.js index ab0904a73..db4f9a110 100644 --- a/test/unit/specs/components/Personalization/createApplyPropositions.spec.js +++ b/test/unit/specs/components/Personalization/createApplyPropositions.spec.js @@ -10,14 +10,13 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import "jasmine-expect"; import { MIXED_PROPOSITIONS, PAGE_WIDE_SCOPE_DECISIONS } from "./responsesMock/eventResponses"; import createApplyPropositions from "../../../../../src/components/Personalization/createApplyPropositions"; import clone from "../../../../../src/utils/clone"; -//import { createProposition } from "../../../../../src/components/Personalization/handlers/proposition"; +import injectCreateProposition from "../../../../../src/components/Personalization/handlers/injectCreateProposition"; const METADATA = { home: { @@ -26,40 +25,50 @@ const METADATA = { } }; -xdescribe("Personalization::createApplyPropositions", () => { - let render; +describe("Personalization::createApplyPropositions", () => { + let processPropositions; + let createProposition; let pendingDisplayNotifications; let viewCache; + let applyPropositions; + let render; beforeEach(() => { + processPropositions = jasmine.createSpy("processPropositions"); + processPropositions.and.callFake(propositions => { + const returnedPropositions = propositions.map(proposition => ({ + ...proposition.toJSON(), + renderAttempted: true + })); + return { returnedPropositions, render }; + }); render = jasmine.createSpy("render"); - render.and.callFake(propositions => { - propositions.forEach(proposition => { - const { items = [] } = proposition.getHandle(); - items.forEach((_, i) => { - proposition.addRenderer(i, () => undefined); - }); - }); + render.and.callFake(() => Promise.resolve("notifications")); + createProposition = injectCreateProposition({ + preprocess: data => data, + isPageWideSurface: () => false }); + pendingDisplayNotifications = jasmine.createSpyObj( "pendingDisplayNotifications", ["concat"] ); viewCache = jasmine.createSpyObj("viewCache", ["getView"]); viewCache.getView.and.returnValue(Promise.resolve([])); + applyPropositions = createApplyPropositions({ + processPropositions, + createProposition, + pendingDisplayNotifications, + viewCache + }); }); it("it should return an empty propositions promise if propositions is empty array", async () => { - const applyPropositions = createApplyPropositions({ - render, - pendingDisplayNotifications - }); - const result = await applyPropositions({ propositions: [] }); expect(result).toEqual({ propositions: [] }); - expect(render).toHaveBeenCalledOnceWith([]); + expect(processPropositions).toHaveBeenCalledOnceWith([]); }); it("it should apply user-provided dom-action schema propositions", async () => { @@ -70,16 +79,11 @@ xdescribe("Personalization::createApplyPropositions", () => { return proposition; }); - const applyPropositions = createApplyPropositions({ - render, - pendingDisplayNotifications - }); - const result = await applyPropositions({ propositions: PAGE_WIDE_SCOPE_DECISIONS }); - expect(render).toHaveBeenCalledTimes(1); + expect(processPropositions).toHaveBeenCalledTimes(1); const expectedScopes = expectedExecuteDecisionsPropositions.map( proposition => proposition.scope @@ -93,11 +97,6 @@ xdescribe("Personalization::createApplyPropositions", () => { }); it("it should merge metadata with propositions that have html-content-item schema", async () => { - const applyPropositions = createApplyPropositions({ - render, - pendingDisplayNotifications - }); - const { propositions } = await applyPropositions({ propositions: MIXED_PROPOSITIONS, metadata: METADATA @@ -115,7 +114,7 @@ xdescribe("Personalization::createApplyPropositions", () => { expect(proposition.items[0].data.type).toEqual("setHtml"); } }); - expect(render).toHaveBeenCalledTimes(1); + expect(processPropositions).toHaveBeenCalledTimes(1); }); it("it should drop items with html-content-item schema when there is no metadata", async () => { @@ -146,11 +145,6 @@ xdescribe("Personalization::createApplyPropositions", () => { } ]; - const applyPropositions = createApplyPropositions({ - render, - pendingDisplayNotifications - }); - const result = await applyPropositions({ propositions }); @@ -162,11 +156,6 @@ xdescribe("Personalization::createApplyPropositions", () => { }); it("it should return renderAttempted = true on resulting propositions", async () => { - const applyPropositions = createApplyPropositions({ - render, - pendingDisplayNotifications - }); - const result = await applyPropositions({ propositions: MIXED_PROPOSITIONS }); @@ -177,10 +166,6 @@ xdescribe("Personalization::createApplyPropositions", () => { }); it("it should ignore propositions with __view__ scope that have already been rendered", async () => { - const applyPropositions = createApplyPropositions({ - render, - pendingDisplayNotifications - }); const propositions = JSON.parse(JSON.stringify(MIXED_PROPOSITIONS)); propositions[4].renderAttempted = true; @@ -201,11 +186,6 @@ xdescribe("Personalization::createApplyPropositions", () => { it("it should ignore items with unsupported schemas", async () => { const expectedItemIds = ["442358", "442359"]; - const applyPropositions = createApplyPropositions({ - render, - pendingDisplayNotifications - }); - const { propositions } = await applyPropositions({ propositions: MIXED_PROPOSITIONS }); @@ -219,11 +199,6 @@ xdescribe("Personalization::createApplyPropositions", () => { }); it("it should not mutate original propositions", async () => { - const applyPropositions = createApplyPropositions({ - render, - pendingDisplayNotifications - }); - const originalPropositions = clone(MIXED_PROPOSITIONS); const result = await applyPropositions({ propositions: originalPropositions, @@ -250,11 +225,6 @@ xdescribe("Personalization::createApplyPropositions", () => { createProposition({ id: "myViewNameProp1", items: [{}] }) ]) ); - const applyPropositions = createApplyPropositions({ - render, - pendingDisplayNotifications, - viewCache - }); const result = await applyPropositions({ viewName: "myViewName" }); diff --git a/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js b/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js index 9678938bb..74559a5a8 100644 --- a/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js +++ b/test/unit/specs/components/Personalization/createFetchDataHandler.spec.js @@ -11,15 +11,17 @@ governing permissions and limitations under the License. */ import createFetchDataHandler from "../../../../../src/components/Personalization/createFetchDataHandler"; +import injectCreateProposition from "../../../../../src/components/Personalization/handlers/injectCreateProposition"; import flushPromiseChains from "../../../helpers/flushPromiseChains"; -xdescribe("Personalization::createFetchDataHandler", () => { +describe("Personalization::createFetchDataHandler", () => { let prehidingStyle; let showContainers; let hideContainers; let mergeQuery; let collect; - let render; + let processPropositions; + let createProposition; let pendingDisplayNotifications; let cacheUpdate; @@ -35,7 +37,11 @@ xdescribe("Personalization::createFetchDataHandler", () => { hideContainers = jasmine.createSpy("hideContainers"); mergeQuery = jasmine.createSpy("mergeQuery"); collect = jasmine.createSpy("collect"); - render = jasmine.createSpy("render"); + processPropositions = jasmine.createSpy("processPropositions"); + createProposition = injectCreateProposition({ + preprocess: data => data, + isPageWideSurface: () => false + }); pendingDisplayNotifications = jasmine.createSpyObj( "pendingDisplayNotifications", ["concat"] @@ -62,7 +68,8 @@ xdescribe("Personalization::createFetchDataHandler", () => { hideContainers, mergeQuery, collect, - render, + processPropositions, + createProposition, pendingDisplayNotifications }); fetchDataHandler({ @@ -96,6 +103,10 @@ xdescribe("Personalization::createFetchDataHandler", () => { run(); response.getPayloadsByType.and.returnValue([]); cacheUpdate.update.and.returnValue([]); + processPropositions.and.returnValue({ + returnedPropositions: [], + returnedDecisions: [] + }); const result = returnResponse(); expect(result).toEqual({ propositions: [], @@ -106,21 +117,24 @@ xdescribe("Personalization::createFetchDataHandler", () => { 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); + processPropositions = () => { + return { + render: () => Promise.resolve([{ id: "handle1" }]), + returnedPropositions: [ + { id: "handle1", items: ["item1"], renderAttempted: true } + ], + returnedDecisions: [] + }; }; run(); response.getPayloadsByType.and.returnValue([ - { id: "handle1" }, + { + id: "handle1", + scopeDetails: { characteristics: { scopeType: "view" } } + }, { id: "handle2" } ]); - cacheUpdate.update.and.returnValue([ - createProposition({ id: "handle1", items: ["item1"] }) - ]); + cacheUpdate.update.and.returnValue([createProposition({ id: "handle1" })]); const result = returnResponse(); expect(result).toEqual({ propositions: [ @@ -131,12 +145,8 @@ xdescribe("Personalization::createFetchDataHandler", () => { await flushPromiseChains(); expect(showContainers).toHaveBeenCalled(); expect(collect).toHaveBeenCalledOnceWith({ - decisionsMeta: [ - { id: "handle1", scope: undefined, scopeDetails: undefined } - ], + decisionsMeta: [{ id: "handle1" }], viewName: "myviewname" }); }); - - // TODO - test the rest of the functionality }); diff --git a/test/unit/specs/components/Personalization/createViewCacheManager.spec.js b/test/unit/specs/components/Personalization/createViewCacheManager.spec.js index 238e8b3b8..a9cd02209 100644 --- a/test/unit/specs/components/Personalization/createViewCacheManager.spec.js +++ b/test/unit/specs/components/Personalization/createViewCacheManager.spec.js @@ -12,34 +12,19 @@ governing permissions and limitations under the License. import createViewCacheManager from "../../../../../src/components/Personalization/createViewCacheManager"; -xdescribe("Personalization::createCacheManager", () => { +describe("Personalization::createCacheManager", () => { const viewHandles = [ { id: "foo1", - scope: "home", - scopeDetails: { - characteristics: { - scopeType: "view" - } - } + scope: "home" }, { id: "foo2", - scope: "home", - scopeDetails: { - characteristics: { - scopeType: "view" - } - } + scope: "home" }, { id: "foo3", - scope: "cart", - scopeDetails: { - characteristics: { - scopeType: "view" - } - } + scope: "cart" }, { id: "foo4", @@ -48,50 +33,38 @@ xdescribe("Personalization::createCacheManager", () => { ]; let createProposition; + let propositions; beforeEach(() => { createProposition = viewHandle => { - const proposition = jasmine.createSpyObj("proposition", [ - "includeInDisplayNotification", - "excludeInReturnedPropositions", - "getHandle" - ]); - proposition.getHandle.and.returnValue(viewHandle); - return proposition; + const { scope } = viewHandle; + return { + getScope() { + return scope; + }, + toJSON() { + return viewHandle; + } + }; }; + propositions = viewHandles.map(createProposition); }); it("stores and gets the decisions based on a viewName", async () => { const viewCacheManager = createViewCacheManager({ createProposition }); const cacheUpdate = viewCacheManager.createCacheUpdate("home"); - const resultingHandles = cacheUpdate.update(viewHandles); - expect(resultingHandles.map(h => h.getHandle())).toEqual([ - viewHandles[0], - viewHandles[1], - viewHandles[3] - ]); + const resultingHandles = cacheUpdate.update(propositions); + expect(resultingHandles).toEqual([propositions[0], propositions[1]]); const homeViews = await viewCacheManager.getView("home"); - expect(homeViews.map(h => h.getHandle())).toEqual([ - viewHandles[0], - viewHandles[1] - ]); + expect(homeViews).toEqual([propositions[0], propositions[1]]); const cartViews = await viewCacheManager.getView("cart"); - expect(cartViews.map(h => h.getHandle())).toEqual([viewHandles[2]]); + expect(cartViews).toEqual([propositions[2]]); const otherViews = await viewCacheManager.getView("other"); - expect(otherViews.map(h => h.getHandle())).toEqual([ - { - scope: "other", - scopeDetails: { - characteristics: { - scopeType: "view" - } - } - } - ]); + expect(otherViews).toEqual([propositions[3]]); }); it("should be no views when decisions deferred is rejected", async () => { @@ -100,7 +73,7 @@ xdescribe("Personalization::createCacheManager", () => { cacheUpdate.cancel(); const homeViews = await viewCacheManager.getView("home"); - expect(homeViews.map(h => h.getHandle())).toEqual([ + expect(homeViews.map(h => h.toJSON())).toEqual([ { scope: "home", scopeDetails: { diff --git a/test/unit/specs/components/Personalization/createViewChangeHandler.spec.js b/test/unit/specs/components/Personalization/createViewChangeHandler.spec.js index b27cf6abd..5cf1c7aa8 100644 --- a/test/unit/specs/components/Personalization/createViewChangeHandler.spec.js +++ b/test/unit/specs/components/Personalization/createViewChangeHandler.spec.js @@ -13,20 +13,22 @@ 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 injectCreateProposition from "../../../../../src/components/Personalization/handlers/injectCreateProposition"; - -xdescribe("Personalization::createViewChangeHandler", () => { +describe("Personalization::createViewChangeHandler", () => { let mergeDecisionsMeta; - let render; + let processPropositions; let viewCache; let personalizationDetails; let event; let onResponse; + let createProposition; + beforeEach(() => { mergeDecisionsMeta = jasmine.createSpy("mergeDecisionsMeta"); - render = jasmine.createSpy("render"); + processPropositions = jasmine.createSpy("processPropositions"); viewCache = jasmine.createSpyObj("viewCache", ["getView"]); personalizationDetails = jasmine.createSpyObj("personalizationDetails", [ @@ -35,12 +37,17 @@ xdescribe("Personalization::createViewChangeHandler", () => { ]); event = "myevent"; onResponse = jasmine.createSpy(); + + createProposition = injectCreateProposition({ + preprocess: data => data, + isPageWideSurface: () => false + }); }); const run = async () => { const viewChangeHandler = createViewChangeHandler({ mergeDecisionsMeta, - render, + processPropositions, viewCache }); await viewChangeHandler({ @@ -53,15 +60,19 @@ xdescribe("Personalization::createViewChangeHandler", () => { it("should trigger render if renderDecisions is true", async () => { viewCache.getView.and.returnValue( - Promise.resolve(CART_VIEW_DECISIONS.map(createProposition)) + Promise.resolve(CART_VIEW_DECISIONS.map(p => createProposition(p))) ); personalizationDetails.isRenderDecisions.and.returnValue(true); personalizationDetails.getViewName.and.returnValue("cart"); - render.and.returnValue(Promise.resolve("decisionMeta")); + processPropositions.and.returnValue({ + render: () => Promise.resolve("decisionMeta"), + returnedPropositions: [], + returnedDecisions: CART_VIEW_DECISIONS + }); const result = await run(); - expect(render).toHaveBeenCalledTimes(1); + expect(processPropositions).toHaveBeenCalledTimes(1); expect(mergeDecisionsMeta).toHaveBeenCalledWith( "myevent", "decisionMeta", @@ -69,54 +80,4 @@ xdescribe("Personalization::createViewChangeHandler", () => { ); 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) - }; - viewCache.getView.and.returnValue(cartViewPromise); - personalizationDetails.isRenderDecisions.and.returnValue(false); - personalizationDetails.getViewName.and.returnValue("cart"); - - const viewChangeHandler = createViewChangeHandler({ - executeDecisions, - viewCache, - showContainers - }); - - viewChangeHandler({ - event, - personalizationDetails, - onResponse - }); - expect(executeDecisions).not.toHaveBeenCalled(); - expect(collect).not.toHaveBeenCalled(); - }); - - it("at onResponse it should trigger collect call when no decisions in cache", () => { - const cartViewPromise = { - then: callback => callback([]) - }; - - viewCache.getView.and.returnValue(cartViewPromise); - executeDecisions.and.returnValue(cartViewPromise); - personalizationDetails.isRenderDecisions.and.returnValue(true); - personalizationDetails.getViewName.and.returnValue("cart"); - - const viewChangeHandler = createViewChangeHandler({ - mergeDecisionsMeta, - collect, - executeDecisions, - viewCache - }); - - viewChangeHandler({ - event, - personalizationDetails, - onResponse - }); - expect(executeDecisions).toHaveBeenCalledWith([]); - expect(collect).toHaveBeenCalled(); - }); - */ }); diff --git a/test/unit/specs/components/Personalization/handlers/createDomActionHandler.spec.js b/test/unit/specs/components/Personalization/handlers/createDomActionHandler.spec.js deleted file mode 100644 index f72718f12..000000000 --- a/test/unit/specs/components/Personalization/handlers/createDomActionHandler.spec.js +++ /dev/null @@ -1,164 +0,0 @@ -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 deleted file mode 100644 index f7387b148..000000000 --- a/test/unit/specs/components/Personalization/handlers/createHtmlContentHandler.spec.js +++ /dev/null @@ -1,110 +0,0 @@ -import createHtmlContentHandler from "../../../../../../src/components/Personalization/handlers/createHtmlContentHandler"; - -xdescribe("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 deleted file mode 100644 index d583b5dd4..000000000 --- a/test/unit/specs/components/Personalization/handlers/createMeasurementSchemaHandler.spec.js +++ /dev/null @@ -1,35 +0,0 @@ -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/createProcessDomAction.spec.js b/test/unit/specs/components/Personalization/handlers/createProcessDomAction.spec.js index cff5ac29c..ffaaa2170 100644 --- a/test/unit/specs/components/Personalization/handlers/createProcessDomAction.spec.js +++ b/test/unit/specs/components/Personalization/handlers/createProcessDomAction.spec.js @@ -11,8 +11,12 @@ describe("createProcessDomAction", () => { beforeEach(() => { item = { - getData() { return data; }, - getMeta() { return meta; } + getData() { + return data; + }, + getMeta() { + return meta; + } }; modules = { typeA: jasmine.createSpy("typeA"), @@ -21,47 +25,76 @@ describe("createProcessDomAction", () => { logger = jasmine.createSpyObj("logger", ["warn"]); storeClickMetrics = jasmine.createSpy("storeClickMetrics"); - processDomAction = createProcessDomAction({ modules, logger, storeClickMetrics }); + processDomAction = createProcessDomAction({ + modules, + logger, + storeClickMetrics + }); }); it("returns an empty object if the item has no data, and logs missing type", () => { data = undefined; expect(processDomAction(item)).toEqual({}); - expect(logger.warn).toHaveBeenCalledWith("Invalid DOM action data: missing type.", undefined); + expect(logger.warn).toHaveBeenCalledWith( + "Invalid DOM action data: missing type.", + undefined + ); }); it("returns an empty object if the item has no type, and logs missing type", () => { data = {}; expect(processDomAction(item)).toEqual({}); - expect(logger.warn).toHaveBeenCalledWith("Invalid DOM action data: missing type.", {}); + expect(logger.warn).toHaveBeenCalledWith( + "Invalid DOM action data: missing type.", + {} + ); }); it("returns an empty object if the item has an unknown type, and logs unknown type", () => { data = { type: "typeC" }; expect(processDomAction(item)).toEqual({}); - expect(logger.warn).toHaveBeenCalledWith("Invalid DOM action data: unknown type.", { type: "typeC" }); + expect(logger.warn).toHaveBeenCalledWith( + "Invalid DOM action data: unknown type.", + { + type: "typeC" + } + ); }); it("returns an empty object if the item has no selector for a click type, and logs missing selector", () => { data = { type: "click" }; expect(processDomAction(item)).toEqual({}); - expect(logger.warn).toHaveBeenCalledWith("Invalid DOM action data: missing selector.", { type: "click" }); + expect(logger.warn).toHaveBeenCalledWith( + "Invalid DOM action data: missing selector.", + { + type: "click" + } + ); }); it("handles a click type", () => { data = { type: "click", selector: ".selector" }; meta = "mymetavalue"; - expect(processDomAction(item)).toEqual({ setRenderAttempted: true, includeInNotification: false }); - expect(storeClickMetrics).toHaveBeenCalledWith({ selector: ".selector", meta: "mymetavalue" }); + expect(processDomAction(item)).toEqual({ + setRenderAttempted: true, + includeInNotification: false + }); + expect(storeClickMetrics).toHaveBeenCalledWith({ + selector: ".selector", + meta: "mymetavalue" + }); }); it("handles a non-click known type", () => { data = { type: "typeA", a: "b" }; const result = processDomAction(item); - expect(result).toEqual({ render: jasmine.any(Function), setRenderAttempted: true, includeInNotification: true }); + expect(result).toEqual({ + render: jasmine.any(Function), + setRenderAttempted: true, + includeInNotification: true + }); expect(modules.typeA).not.toHaveBeenCalled(); result.render(); expect(modules.typeA).toHaveBeenCalledWith({ type: "typeA", a: "b" }); }); - }); diff --git a/test/unit/specs/components/Personalization/handlers/createProcessHtmlContent.spec.js b/test/unit/specs/components/Personalization/handlers/createProcessHtmlContent.spec.js index efd3c3e2f..cef24684e 100644 --- a/test/unit/specs/components/Personalization/handlers/createProcessHtmlContent.spec.js +++ b/test/unit/specs/components/Personalization/handlers/createProcessHtmlContent.spec.js @@ -1,7 +1,6 @@ import createProcessHtmlContent from "../../../../../../src/components/Personalization/handlers/createProcessHtmlContent"; describe("createProcessHtmlContent", () => { - let modules; let logger; let item; @@ -15,7 +14,9 @@ describe("createProcessHtmlContent", () => { }; logger = jasmine.createSpyObj("logger", ["warn"]); item = { - getData() { return data; } + getData() { + return data; + } }; processHtmlContent = createProcessHtmlContent({ modules, logger }); @@ -28,13 +29,13 @@ describe("createProcessHtmlContent", () => { }); it("returns an empty object if the item has no type", () => { - data = {selector: ".myselector"}; + data = { selector: ".myselector" }; expect(processHtmlContent(item)).toEqual({}); expect(logger.warn).not.toHaveBeenCalled(); }); it("returns an empty object if the item has no selector", () => { - data = {type: "mytype"}; + data = { type: "mytype" }; expect(processHtmlContent(item)).toEqual({}); expect(logger.warn).not.toHaveBeenCalled(); }); @@ -42,16 +43,27 @@ describe("createProcessHtmlContent", () => { it("returns an empty object if the item has an unknown type, and logs unknown type", () => { data = { type: "typeC", selector: ".myselector", content: "mycontent" }; expect(processHtmlContent(item)).toEqual({}); - expect(logger.warn).toHaveBeenCalledWith("Invalid HTML content data", { type: "typeC", selector: ".myselector", content: "mycontent" }); + expect(logger.warn).toHaveBeenCalledWith("Invalid HTML content data", { + type: "typeC", + selector: ".myselector", + content: "mycontent" + }); }); it("handles a known type", () => { data = { type: "typeA", selector: ".myselector", content: "mycontent" }; const result = processHtmlContent(item); - expect(result).toEqual({ render: jasmine.any(Function), setRenderAttempted: true, includeInNotification: true }); + expect(result).toEqual({ + render: jasmine.any(Function), + setRenderAttempted: true, + includeInNotification: true + }); expect(modules.typeA).not.toHaveBeenCalled(); result.render(); - expect(modules.typeA).toHaveBeenCalledWith({ type: "typeA", selector: ".myselector", content: "mycontent" }); + expect(modules.typeA).toHaveBeenCalledWith({ + type: "typeA", + selector: ".myselector", + content: "mycontent" + }); }); - }); diff --git a/test/unit/specs/components/Personalization/handlers/createProcessPropositions.spec.js b/test/unit/specs/components/Personalization/handlers/createProcessPropositions.spec.js index 1a690f682..10c726577 100644 --- a/test/unit/specs/components/Personalization/handlers/createProcessPropositions.spec.js +++ b/test/unit/specs/components/Personalization/handlers/createProcessPropositions.spec.js @@ -16,114 +16,212 @@ describe("createProcessPropositions", () => { beforeEach(() => { render = jasmine.createSpy("render"); - always = item => ({ render: () => render(item.getData()), setRenderAttempted: true, includeInNotification: true }); - noNotification = item => ({ render: () => render(item.getData()), setRenderAttempted: true, includeInNotification: false }); + always = item => ({ + render: () => render(item.getData()), + setRenderAttempted: true, + includeInNotification: true + }); + noNotification = item => ({ + render: () => render(item.getData()), + setRenderAttempted: true, + includeInNotification: false + }); never = () => ({}); - noRender = () => ({ setRenderAttempted: true, includeInNotification: true }); - redirect = item => ({ render: () => render(item.getData()), setRenderAttempted: true, onlyRenderThis: true}); + noRender = () => ({ + setRenderAttempted: true, + includeInNotification: true + }); + redirect = item => ({ + render: () => render(item.getData()), + setRenderAttempted: true, + onlyRenderThis: true + }); schemaProcessors = { always, noNotification, never, noRender, redirect }; logger = jasmine.createSpyObj("logger", ["info", "error"]); - processPropositions = createProcessPropositions({ schemaProcessors, logger }); + processPropositions = createProcessPropositions({ + schemaProcessors, + logger + }); createProposition = injectCreateProposition({ preprocess: data => data }); }); it("handles no propositions", async () => { const result = processPropositions([]); - expect(result).toEqual({ render: jasmine.any(Function), returnedPropositions: [], returnedDecisions: [] }); + expect(result).toEqual({ + render: jasmine.any(Function), + returnedPropositions: [], + returnedDecisions: [] + }); await expectAsync(result.render()).toBeResolvedTo([]); }); it("processes a proposition with an always item", async () => { - const prop1 = createProposition({ id: "always1", scope: "myscope", scopeDetails: { a: 1 }, items: [ { schema: "always", data: "mydata" }] }); - const result = processPropositions([prop1]); - expect(result).toEqual({ render: jasmine.any(Function), returnedPropositions: [{ + const prop1 = createProposition({ id: "always1", scope: "myscope", scopeDetails: { a: 1 }, - items: [ { schema: "always", data: "mydata" }], - renderAttempted: true - }], returnedDecisions: [] }); + items: [{ schema: "always", data: "mydata" }] + }); + const result = processPropositions([prop1]); + expect(result).toEqual({ + render: jasmine.any(Function), + returnedPropositions: [ + { + id: "always1", + scope: "myscope", + scopeDetails: { a: 1 }, + items: [{ schema: "always", data: "mydata" }], + renderAttempted: true + } + ], + returnedDecisions: [] + }); expect(render).not.toHaveBeenCalled(); - await expectAsync(result.render()).toBeResolvedTo([{ - id: "always1", - scope: "myscope", - scopeDetails: { a: 1 } - }]); + await expectAsync(result.render()).toBeResolvedTo([ + { + id: "always1", + scope: "myscope", + scopeDetails: { a: 1 } + } + ]); expect(render).toHaveBeenCalledWith("mydata"); }); it("processes a proposition with a noNotification item", async () => { - const prop1 = createProposition({ id: "noNotification1", scope: "myscope", scopeDetails: { a: 1 }, items: [ { schema: "noNotification", data: "mydata" }] }); - const result = processPropositions([prop1]); - expect(result).toEqual({ render: jasmine.any(Function), returnedPropositions: [{ + const prop1 = createProposition({ id: "noNotification1", scope: "myscope", scopeDetails: { a: 1 }, - items: [ { schema: "noNotification", data: "mydata" }], - renderAttempted: true - }], returnedDecisions: [] }); + items: [{ schema: "noNotification", data: "mydata" }] + }); + const result = processPropositions([prop1]); + expect(result).toEqual({ + render: jasmine.any(Function), + returnedPropositions: [ + { + id: "noNotification1", + scope: "myscope", + scopeDetails: { a: 1 }, + items: [{ schema: "noNotification", data: "mydata" }], + renderAttempted: true + } + ], + returnedDecisions: [] + }); expect(render).not.toHaveBeenCalled(); await expectAsync(result.render()).toBeResolvedTo([]); expect(render).toHaveBeenCalledWith("mydata"); }); it("processes a proposition with a never item", async () => { - const prop1 = createProposition({ id: "never1", scope: "myscope", scopeDetails: { a: 1 }, items: [ { schema: "never", data: "mydata" }] }); - const result = processPropositions([prop1]); - expect(result).toEqual({ render: jasmine.any(Function), returnedPropositions: [{ - id: "never1", - scope: "myscope", - scopeDetails: { a: 1 }, - items: [ { schema: "never", data: "mydata" }], - renderAttempted: false - }], returnedDecisions: [{ + const prop1 = createProposition({ id: "never1", scope: "myscope", scopeDetails: { a: 1 }, - items: [ { schema: "never", data: "mydata" }] - }]}); + items: [{ schema: "never", data: "mydata" }] + }); + const result = processPropositions([prop1]); + expect(result).toEqual({ + render: jasmine.any(Function), + returnedPropositions: [ + { + id: "never1", + scope: "myscope", + scopeDetails: { a: 1 }, + items: [{ schema: "never", data: "mydata" }], + renderAttempted: false + } + ], + returnedDecisions: [ + { + id: "never1", + scope: "myscope", + scopeDetails: { a: 1 }, + items: [{ schema: "never", data: "mydata" }] + } + ] + }); await expectAsync(result.render()).toBeResolvedTo([]); expect(render).not.toHaveBeenCalled(); }); it("processes a proposition with a noRender item", async () => { - const prop1 = createProposition({ id: "noRender1", scope: "myscope", scopeDetails: { a: 1 }, items: [ { schema: "noRender", data: "mydata" }] }); - const result = processPropositions([prop1]); - expect(result).toEqual({ render: jasmine.any(Function), returnedPropositions: [{ + const prop1 = createProposition({ id: "noRender1", scope: "myscope", scopeDetails: { a: 1 }, - items: [ { schema: "noRender", data: "mydata" }], - renderAttempted: true - }], returnedDecisions: [] }); - await expectAsync(result.render()).toBeResolvedTo([{ - id: "noRender1", - scope: "myscope", - scopeDetails: { a: 1 } - }]); + items: [{ schema: "noRender", data: "mydata" }] + }); + const result = processPropositions([prop1]); + expect(result).toEqual({ + render: jasmine.any(Function), + returnedPropositions: [ + { + id: "noRender1", + scope: "myscope", + scopeDetails: { a: 1 }, + items: [{ schema: "noRender", data: "mydata" }], + renderAttempted: true + } + ], + returnedDecisions: [] + }); + await expectAsync(result.render()).toBeResolvedTo([ + { + id: "noRender1", + scope: "myscope", + scopeDetails: { a: 1 } + } + ]); expect(render).not.toHaveBeenCalled(); }); it("processes a proposition with a redirect item", async () => { - const prop1 = createProposition({ id: "redirect1", scope: "myscope", scopeDetails: { a: 1 }, items: [ { schema: "redirect", data: "mydata" }] }); - const result = processPropositions([prop1]); - expect(result).toEqual({ render: jasmine.any(Function), returnedPropositions: [{ + const prop1 = createProposition({ id: "redirect1", scope: "myscope", scopeDetails: { a: 1 }, - items: [ { schema: "redirect", data: "mydata" }], - renderAttempted: true - }], returnedDecisions: [] }); + items: [{ schema: "redirect", data: "mydata" }] + }); + const result = processPropositions([prop1]); + expect(result).toEqual({ + render: jasmine.any(Function), + returnedPropositions: [ + { + id: "redirect1", + scope: "myscope", + scopeDetails: { a: 1 }, + items: [{ schema: "redirect", data: "mydata" }], + renderAttempted: true + } + ], + returnedDecisions: [] + }); expect(render).not.toHaveBeenCalled(); await expectAsync(result.render()).toBeResolvedTo([]); expect(render).toHaveBeenCalledWith("mydata"); }); it("doesn't render other propositions if one has a redirect", async () => { - const prop1 = createProposition({ id: "always1", scope: "myscope", scopeDetails: { a: 1 }, items: [ { schema: "always", data: "mydata1" }] }); - const prop2 = createProposition({ id: "redirect2", scope: "myscope", scopeDetails: { a: 2 }, items: [ { schema: "redirect", data: "mydata2" }] }); - const prop3 = createProposition({ id: "always3", scope: "myscope", scopeDetails: { a: 3 }, items: [ { schema: "always", data: "mydata3" }] }); + const prop1 = createProposition({ + id: "always1", + scope: "myscope", + scopeDetails: { a: 1 }, + items: [{ schema: "always", data: "mydata1" }] + }); + const prop2 = createProposition({ + id: "redirect2", + scope: "myscope", + scopeDetails: { a: 2 }, + items: [{ schema: "redirect", data: "mydata2" }] + }); + const prop3 = createProposition({ + id: "always3", + scope: "myscope", + scopeDetails: { a: 3 }, + items: [{ schema: "always", data: "mydata3" }] + }); const result = processPropositions([prop1, prop2, prop3]); expect(result).toEqual({ render: jasmine.any(Function), @@ -132,19 +230,21 @@ describe("createProcessPropositions", () => { id: "redirect2", scope: "myscope", scopeDetails: { a: 2 }, - items: [ { schema: "redirect", data: "mydata2" }], + items: [{ schema: "redirect", data: "mydata2" }], renderAttempted: true - },{ + }, + { id: "always1", scope: "myscope", scopeDetails: { a: 1 }, - items: [ { schema: "always", data: "mydata1" }], + items: [{ schema: "always", data: "mydata1" }], renderAttempted: false - },{ + }, + { id: "always3", scope: "myscope", scopeDetails: { a: 3 }, - items: [ { schema: "always", data: "mydata3" }], + items: [{ schema: "always", data: "mydata3" }], renderAttempted: false } ], @@ -153,12 +253,13 @@ describe("createProcessPropositions", () => { id: "always1", scope: "myscope", scopeDetails: { a: 1 }, - items: [ { schema: "always", data: "mydata1" }] - },{ + items: [{ schema: "always", data: "mydata1" }] + }, + { id: "always3", scope: "myscope", scopeDetails: { a: 3 }, - items: [ { schema: "always", data: "mydata3" }] + items: [{ schema: "always", data: "mydata3" }] } ] }); @@ -168,22 +269,34 @@ describe("createProcessPropositions", () => { }); it("processes nonRenderPropositions", async () => { - const prop1 = createProposition({ id: "always1", scope: "myscope", scopeDetails: { a: 1 }, items: [ { schema: "always", data: "mydata" }] }); - const result = processPropositions([], [prop1]); - expect(result).toEqual({ render: jasmine.any(Function), returnedPropositions: [{ + const prop1 = createProposition({ id: "always1", scope: "myscope", scopeDetails: { a: 1 }, - items: [ { schema: "always", data: "mydata" }], - renderAttempted: false - }], returnedDecisions: [{ - id: "always1", - scope: "myscope", - scopeDetails: { a: 1 }, - items: [ { schema: "always", data: "mydata" }] - }]}); + items: [{ schema: "always", data: "mydata" }] + }); + const result = processPropositions([], [prop1]); + expect(result).toEqual({ + render: jasmine.any(Function), + returnedPropositions: [ + { + id: "always1", + scope: "myscope", + scopeDetails: { a: 1 }, + items: [{ schema: "always", data: "mydata" }], + renderAttempted: false + } + ], + returnedDecisions: [ + { + id: "always1", + scope: "myscope", + scopeDetails: { a: 1 }, + items: [{ schema: "always", data: "mydata" }] + } + ] + }); await expectAsync(result.render()).toBeResolvedTo([]); expect(render).not.toHaveBeenCalled(); }); - }); diff --git a/test/unit/specs/components/Personalization/handlers/createProcessRedirect.spec.js b/test/unit/specs/components/Personalization/handlers/createProcessRedirect.spec.js index 92ad441a0..f69b91f7d 100644 --- a/test/unit/specs/components/Personalization/handlers/createProcessRedirect.spec.js +++ b/test/unit/specs/components/Personalization/handlers/createProcessRedirect.spec.js @@ -3,7 +3,6 @@ import flushPromiseChains from "../../../../helpers/flushPromiseChains"; import createProcessRedirect from "../../../../../../src/components/Personalization/handlers/createProcessRedirect"; describe("createProcessRedirect", () => { - let logger; let executeRedirect; let collect; @@ -18,32 +17,49 @@ describe("createProcessRedirect", () => { logger = jasmine.createSpyObj("logger", ["warn"]); executeRedirect = jasmine.createSpy("executeRedirect"); collectDefer = defer(); - collect = jasmine.createSpy("collect").and.returnValue(collectDefer.promise); + collect = jasmine + .createSpy("collect") + .and.returnValue(collectDefer.promise); item = { - getData() { return data; }, - getMeta() { return meta; } + getData() { + return data; + }, + getMeta() { + return meta; + } }; - processRedirect = createProcessRedirect({ logger, executeRedirect, collect }); + processRedirect = createProcessRedirect({ + logger, + executeRedirect, + collect + }); }); it("returns an empty object if the item has no data", () => { data = undefined; expect(processRedirect(item)).toEqual({}); - expect(logger.warn).toHaveBeenCalledWith("Invalid Redirect data", undefined); + expect(logger.warn).toHaveBeenCalledWith( + "Invalid Redirect data", + undefined + ); }); it("returns an empty object if the item has no content", () => { - data = {a: 1}; + data = { a: 1 }; expect(processRedirect(item)).toEqual({}); - expect(logger.warn).toHaveBeenCalledWith("Invalid Redirect data", {a: 1}); + expect(logger.warn).toHaveBeenCalledWith("Invalid Redirect data", { a: 1 }); }); it("redirects", async () => { data = { content: "mycontent" }; meta = "mymetavalue"; const result = processRedirect(item); - expect(result).toEqual({ render: jasmine.any(Function), setRenderAttempted: true, onlyRenderThis: true }); + expect(result).toEqual({ + render: jasmine.any(Function), + setRenderAttempted: true, + onlyRenderThis: true + }); expect(collect).not.toHaveBeenCalled(); expect(executeRedirect).not.toHaveBeenCalled(); const renderPromise = result.render(); @@ -64,5 +80,4 @@ describe("createProcessRedirect", () => { collectDefer.reject("myerror"); await expectAsync(renderPromise).toBeRejectedWith("myerror"); }); - }); diff --git a/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js b/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js deleted file mode 100644 index b12422226..000000000 --- a/test/unit/specs/components/Personalization/handlers/createRedirectHandler.spec.js +++ /dev/null @@ -1,125 +0,0 @@ -import createRedirectHandler from "../../../../../../src/components/Personalization/handlers/createRedirectHandler"; - -xdescribe("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 deleted file mode 100644 index 27476ef55..000000000 --- a/test/unit/specs/components/Personalization/handlers/createRender.spec.js +++ /dev/null @@ -1,87 +0,0 @@ -import createProcessPropositions from "../../../../../../src/components/Personalization/handlers/createProcessPropositions"; - -xdescribe("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/createRenderDomAction.spec.js b/test/unit/specs/components/Personalization/handlers/createRenderDomAction.spec.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/unit/specs/components/Personalization/handlers/injectCreateProposition.spec.js b/test/unit/specs/components/Personalization/handlers/injectCreateProposition.spec.js index 2c8cd1c52..6266137f0 100644 --- a/test/unit/specs/components/Personalization/handlers/injectCreateProposition.spec.js +++ b/test/unit/specs/components/Personalization/handlers/injectCreateProposition.spec.js @@ -3,7 +3,10 @@ import injectCreateProposition from "../../../../../../src/components/Personaliz describe("injectCreateProposition", () => { const preprocess = data => `preprocessed ${data}`; const isPageWideSurface = scope => scope === "__surface__"; - const createProposition = injectCreateProposition({ preprocess, isPageWideSurface }); + const createProposition = injectCreateProposition({ + preprocess, + isPageWideSurface + }); it("creates a proposition from nothing", () => { const proposition = createProposition({}); @@ -69,5 +72,4 @@ describe("injectCreateProposition", () => { }); expect(proposition.getScopeType()).toEqual("page"); }); - }); diff --git a/test/unit/specs/components/Personalization/handlers/processDefaultContent.spec.js b/test/unit/specs/components/Personalization/handlers/processDefaultContent.spec.js index e6f8d8519..998661639 100644 --- a/test/unit/specs/components/Personalization/handlers/processDefaultContent.spec.js +++ b/test/unit/specs/components/Personalization/handlers/processDefaultContent.spec.js @@ -1,10 +1,11 @@ import processDefaultContent from "../../../../../../src/components/Personalization/handlers/processDefaultContent"; describe("processDefaultContent", () => { - it("always renders the default content", () => { const result = processDefaultContent(); - expect(result).toEqual({ setRenderAttempted: true, includeInNotification: true }); + expect(result).toEqual({ + setRenderAttempted: true, + includeInNotification: true + }); }); - }); diff --git a/test/unit/specs/components/Personalization/handlers/proposition.spec.js b/test/unit/specs/components/Personalization/handlers/proposition.spec.js deleted file mode 100644 index a769ffcee..000000000 --- a/test/unit/specs/components/Personalization/handlers/proposition.spec.js +++ /dev/null @@ -1,244 +0,0 @@ -xdescribe("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/topLevel/buildAlloy.js b/test/unit/specs/components/Personalization/topLevel/buildAlloy.js index 867baad18..30e589563 100644 --- a/test/unit/specs/components/Personalization/topLevel/buildAlloy.js +++ b/test/unit/specs/components/Personalization/topLevel/buildAlloy.js @@ -86,26 +86,32 @@ const buildComponent = ({ } = createClickStorage(); const preprocess = action => action; - const createProposition = injectCreateProposition({ preprocess, isPageWideSurface }); + const createProposition = injectCreateProposition({ + preprocess, + isPageWideSurface + }); const viewCache = createViewCacheManager({ createProposition }); const modules = initDomActionsModulesMocks(); - const schemaProcessors = { [schema.DEFAULT_CONTENT_ITEM]: processDefaultContent, - [schema.DOM_ACTION]: createProcessDomAction({ modules, logger, storeClickMetrics }), + [schema.DOM_ACTION]: createProcessDomAction({ + modules, + logger, + storeClickMetrics + }), [schema.HTML_CONTENT_ITEM]: createProcessHtmlContent({ modules, logger }), [schema.REDIRECT_ITEM]: createProcessRedirect({ logger, executeRedirect: url => window.location.replace(url), collect - }) + }) }; const processPropositions = createProcessPropositions({ schemaProcessors, - logger, + logger }); const pendingDisplayNotifications = createAsyncArray(); From 9b664c03a52748bffd4b07ecad94ebc38411f6f7 Mon Sep 17 00:00:00 2001 From: Jon Snyder Date: Wed, 13 Sep 2023 21:37:01 -0600 Subject: [PATCH 18/20] Add license header --- src/utils/deduplicateArray.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/utils/deduplicateArray.js b/src/utils/deduplicateArray.js index 02c2eaf5a..05e71ab85 100644 --- a/src/utils/deduplicateArray.js +++ b/src/utils/deduplicateArray.js @@ -1,3 +1,14 @@ +/* +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 REFERENCE_EQUALITY = (a, b) => a === b; const findIndex = (array, item, isEqual) => { From 50dba9cbe4e22db153f2babe90a8e403f5817893 Mon Sep 17 00:00:00 2001 From: Jon Snyder Date: Wed, 13 Sep 2023 22:39:41 -0600 Subject: [PATCH 19/20] Fix bugs found running functional tests --- .../Personalization/createViewCacheManager.js | 8 +++++++- .../Personalization/dom-actions/createPreprocess.js | 3 +++ .../Personalization/dom-actions/remapHeadOffers.js | 4 ++++ src/components/Personalization/index.js | 3 ++- test/functional/specs/Personalization/C6364798.js | 4 ++-- test/functional/specs/Personalization/C782718.js | 4 ++-- .../Personalization/createViewCacheManager.spec.js | 10 ++++++++-- 7 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/components/Personalization/createViewCacheManager.js b/src/components/Personalization/createViewCacheManager.js index 22a751975..ac18adacf 100644 --- a/src/components/Personalization/createViewCacheManager.js +++ b/src/components/Personalization/createViewCacheManager.js @@ -12,6 +12,7 @@ governing permissions and limitations under the License. import { assign, groupBy } from "../../utils"; import defer from "../../utils/defer"; +import { DEFAULT_CONTENT_ITEM } from "./constants/schema"; export default ({ createProposition }) => { const viewStorage = {}; @@ -31,7 +32,12 @@ export default ({ createProposition }) => { characteristics: { scopeType: "view" } - } + }, + items: [ + { + schema: DEFAULT_CONTENT_ITEM + } + ] }, false ); diff --git a/src/components/Personalization/dom-actions/createPreprocess.js b/src/components/Personalization/dom-actions/createPreprocess.js index ae094c2e7..8bb87127d 100644 --- a/src/components/Personalization/dom-actions/createPreprocess.js +++ b/src/components/Personalization/dom-actions/createPreprocess.js @@ -12,6 +12,9 @@ governing permissions and limitations under the License. import { assign } from "../../../utils"; export default preprocessors => action => { + if (!action) { + return action; + } return preprocessors.reduce( (processed, fn) => assign(processed, fn(processed)), action diff --git a/src/components/Personalization/dom-actions/remapHeadOffers.js b/src/components/Personalization/dom-actions/remapHeadOffers.js index 55b2be517..29341cf3e 100644 --- a/src/components/Personalization/dom-actions/remapHeadOffers.js +++ b/src/components/Personalization/dom-actions/remapHeadOffers.js @@ -40,6 +40,10 @@ export default action => { return result; } + if (selector == null) { + return result; + } + const container = selectNodesWithEq(selector); if (!is(container[0], HEAD)) { return result; diff --git a/src/components/Personalization/index.js b/src/components/Personalization/index.js index a0b9ffcd2..67da84169 100644 --- a/src/components/Personalization/index.js +++ b/src/components/Personalization/index.js @@ -69,7 +69,8 @@ const createPersonalization = ({ config, logger, eventManager }) => { [schema.HTML_CONTENT_ITEM]: createProcessHtmlContent({ modules, logger }), [schema.REDIRECT_ITEM]: createProcessRedirect({ logger, - executeRedirect: url => window.location.replace(url) + executeRedirect: url => window.location.replace(url), + collect }) }; diff --git a/test/functional/specs/Personalization/C6364798.js b/test/functional/specs/Personalization/C6364798.js index 37d468032..698893992 100644 --- a/test/functional/specs/Personalization/C6364798.js +++ b/test/functional/specs/Personalization/C6364798.js @@ -129,7 +129,7 @@ const simulatePageLoad = async alloy => { .expect( // eslint-disable-next-line no-underscore-dangle notificationRequestBody.events[0].xdm._experience.decisioning - .propositions[1] + .propositions[0] ) .eql(pageWideScopeDecisionsMeta[0]); const productsViewDecisionsMeta = getDecisionsMetaByScope( @@ -140,7 +140,7 @@ const simulatePageLoad = async alloy => { .expect( // eslint-disable-next-line no-underscore-dangle notificationRequestBody.events[0].xdm._experience.decisioning - .propositions[0] + .propositions[1] ) .eql(productsViewDecisionsMeta[0]); await t diff --git a/test/functional/specs/Personalization/C782718.js b/test/functional/specs/Personalization/C782718.js index feacf87e5..272164a5c 100644 --- a/test/functional/specs/Personalization/C782718.js +++ b/test/functional/specs/Personalization/C782718.js @@ -128,7 +128,7 @@ const simulatePageLoad = async alloy => { .expect( // eslint-disable-next-line no-underscore-dangle notificationRequestBody.events[0].xdm._experience.decisioning - .propositions[1] + .propositions[0] ) .eql(pageWideScopeDecisionsMeta[0]); const productsViewDecisionsMeta = getDecisionsMetaByScope( @@ -139,7 +139,7 @@ const simulatePageLoad = async alloy => { .expect( // eslint-disable-next-line no-underscore-dangle notificationRequestBody.events[0].xdm._experience.decisioning - .propositions[0] + .propositions[1] ) .eql(productsViewDecisionsMeta[0]); await t diff --git a/test/unit/specs/components/Personalization/createViewCacheManager.spec.js b/test/unit/specs/components/Personalization/createViewCacheManager.spec.js index a9cd02209..fcbf24cd0 100644 --- a/test/unit/specs/components/Personalization/createViewCacheManager.spec.js +++ b/test/unit/specs/components/Personalization/createViewCacheManager.spec.js @@ -10,9 +10,10 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ +import { DEFAULT_CONTENT_ITEM } from "../../../../../src/components/Personalization/constants/schema"; import createViewCacheManager from "../../../../../src/components/Personalization/createViewCacheManager"; -describe("Personalization::createCacheManager", () => { +describe("Personalization::createViewCacheManager", () => { const viewHandles = [ { id: "foo1", @@ -80,7 +81,12 @@ describe("Personalization::createCacheManager", () => { characteristics: { scopeType: "view" } - } + }, + items: [ + { + schema: DEFAULT_CONTENT_ITEM + } + ] } ]); }); From ca330320872539075641d75ece3800d593da44d6 Mon Sep 17 00:00:00 2001 From: Jon Snyder Date: Wed, 20 Sep 2023 11:18:23 -0600 Subject: [PATCH 20/20] Update scope lowercase logic to only apply to the view cache. --- src/components/Personalization/createViewCacheManager.js | 7 +++++-- .../Personalization/handlers/injectCreateProposition.js | 2 +- src/core/createEvent.js | 2 +- test/functional/specs/Personalization/C6364798.js | 9 ++------- test/functional/specs/Personalization/C782718.js | 4 ++-- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/components/Personalization/createViewCacheManager.js b/src/components/Personalization/createViewCacheManager.js index ac18adacf..896726907 100644 --- a/src/components/Personalization/createViewCacheManager.js +++ b/src/components/Personalization/createViewCacheManager.js @@ -20,7 +20,7 @@ export default ({ createProposition }) => { let previousUpdateCacheComplete = Promise.resolve(); const getViewPropositions = (currentViewStorage, viewName) => { - const viewPropositions = currentViewStorage[viewName]; + const viewPropositions = currentViewStorage[viewName.toLowerCase()]; if (viewPropositions && viewPropositions.length > 0) { return viewPropositions; } @@ -58,9 +58,12 @@ export default ({ createProposition }) => { return { update(viewPropositions) { - const newViewStorage = groupBy(viewPropositions, proposition => + const viewPropositionsWithScope = viewPropositions.filter(proposition => proposition.getScope() ); + const newViewStorage = groupBy(viewPropositionsWithScope, proposition => + proposition.getScope().toLowerCase() + ); updateCacheDeferred.resolve(newViewStorage); if (viewName) { return getViewPropositions(newViewStorage, viewName); diff --git a/src/components/Personalization/handlers/injectCreateProposition.js b/src/components/Personalization/handlers/injectCreateProposition.js index 36dae1a24..f94219d44 100644 --- a/src/components/Personalization/handlers/injectCreateProposition.js +++ b/src/components/Personalization/handlers/injectCreateProposition.js @@ -53,7 +53,7 @@ export default ({ preprocess, isPageWideSurface }) => { if (!scope) { return scope; } - return scope.toLowerCase(); + return scope; }, getScopeType() { if (scope === PAGE_WIDE_SCOPE || isPageWideSurface(scope)) { diff --git a/src/core/createEvent.js b/src/core/createEvent.js index 5144aa768..84f1a50a3 100644 --- a/src/core/createEvent.js +++ b/src/core/createEvent.js @@ -157,7 +157,7 @@ export default () => { return undefined; } - return userXdm.web.webPageDetails.viewName.toLowerCase(); + return userXdm.web.webPageDetails.viewName; }, toJSON() { if (!isFinalized) { diff --git a/test/functional/specs/Personalization/C6364798.js b/test/functional/specs/Personalization/C6364798.js index fe5e9cd8a..47a8f1135 100644 --- a/test/functional/specs/Personalization/C6364798.js +++ b/test/functional/specs/Personalization/C6364798.js @@ -175,7 +175,7 @@ const simulateViewChange = async (alloy, personalizationPayload) => { xdm: { web: { webPageDetails: { - viewName: "cart" + viewName: "Cart" } } } @@ -223,7 +223,7 @@ const simulateViewChangeForNonExistingView = async alloy => { eventType: "noviewoffers", web: { webPageDetails: { - viewName: "noview" + viewName: "noView" } } } @@ -256,11 +256,6 @@ const simulateViewChangeForNonExistingView = async alloy => { noViewViewChangeRequestBody.events[0].xdm.web.webPageDetails.viewName ) .eql("noView"); - await t - .expect( - noViewViewChangeRequestBody.events[0].xdm.web.webPageDetails.viewName - ) - .eql("noview"); await t .expect(noViewViewChangeRequestBody.events[0].xdm.eventType) .eql("noviewoffers"); diff --git a/test/functional/specs/Personalization/C782718.js b/test/functional/specs/Personalization/C782718.js index c905ac14e..272164a5c 100644 --- a/test/functional/specs/Personalization/C782718.js +++ b/test/functional/specs/Personalization/C782718.js @@ -211,7 +211,7 @@ const simulateViewChangeForNonExistingView = async alloy => { eventType: "noviewoffers", web: { webPageDetails: { - viewName: "noview" + viewName: "noView" } } } @@ -232,7 +232,7 @@ const simulateViewChangeForNonExistingView = async alloy => { .expect( noViewViewChangeRequestBody.events[0].xdm.web.webPageDetails.viewName ) - .eql("noview"); + .eql("noView"); await t // eslint-disable-next-line no-underscore-dangle .expect(noViewViewChangeRequestBody.events[0].xdm._experience.decisioning)