From 996b9461723f5a2b6ed132aae92f03dcc6871dbd Mon Sep 17 00:00:00 2001 From: Serban Stancu Date: Mon, 7 Oct 2024 11:33:51 -0600 Subject: [PATCH 01/14] WIP --- .../createNotificationHandler.js | 19 +++++++++++++++- .../createOnDecisionHandler.js | 8 ++++++- .../handlers/createProcessInAppMessage.js | 20 +++++++++-------- .../handlers/createProcessPropositions.js | 22 +++++++++++++++---- .../handlers/injectCreateProposition.js | 2 ++ .../RulesEngine/createDecisionProvider.js | 17 ++++++++++++-- .../createEvaluableRulesetPayload.js | 1 + src/constants/eventType.js | 1 + src/constants/propositionEventType.js | 6 ++++- 9 files changed, 78 insertions(+), 18 deletions(-) diff --git a/src/components/Personalization/createNotificationHandler.js b/src/components/Personalization/createNotificationHandler.js index 7ab7c1354..2d7f1981f 100644 --- a/src/components/Personalization/createNotificationHandler.js +++ b/src/components/Personalization/createNotificationHandler.js @@ -10,6 +10,7 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ import { defer } from "../../utils/index.js"; +import { SUPPRESS } from "../../constants/eventType.js"; export default (collect, renderedPropositions) => { return (isRenderDecisions, isSendDisplayEvent, viewName) => { @@ -24,13 +25,29 @@ export default (collect, renderedPropositions) => { return renderedPropositionsDeferred.resolve; } - return (decisionsMeta) => { + return (suppressedPropositions, decisionsMeta) => { if (decisionsMeta.length > 0) { collect({ decisionsMeta, viewName, }); } + + if (suppressedPropositions && suppressedPropositions.length > 0) { + const suppressedMeta = suppressedPropositions.map( + ({ id, scope, scopeDetails }) => ({ + id, + scope, + scopeDetails, + }), + ); + + collect({ + decisionsMeta: suppressedMeta, + eventType: SUPPRESS, + viewName, + }); + } }; }; }; diff --git a/src/components/Personalization/createOnDecisionHandler.js b/src/components/Personalization/createOnDecisionHandler.js index 574f36d37..d410c2e54 100644 --- a/src/components/Personalization/createOnDecisionHandler.js +++ b/src/components/Personalization/createOnDecisionHandler.js @@ -30,13 +30,19 @@ export default ({ const { render, returnedPropositions } = processPropositions( propositionsToExecute, ); + debugger; const handleNotifications = notificationHandler( renderDecisions, sendDisplayEvent, viewName, ); - render().then(handleNotifications); + render().then( + handleNotifications.bind( + null, + returnedPropositions.filter((p) => p.isSuppressedDisplay), + ), + ); return Promise.resolve({ propositions: returnedPropositions, diff --git a/src/components/Personalization/handlers/createProcessInAppMessage.js b/src/components/Personalization/handlers/createProcessInAppMessage.js index 38d4f886a..c65eba67b 100644 --- a/src/components/Personalization/handlers/createProcessInAppMessage.js +++ b/src/components/Personalization/handlers/createProcessInAppMessage.js @@ -47,7 +47,7 @@ const isValidInAppMessage = (data, logger) => { }; export default ({ modules, logger }) => { - return (item) => { + return (item, firstItemInBatch) => { const data = item.getData(); const meta = { ...item.getProposition().getNotification() }; @@ -73,14 +73,16 @@ export default ({ modules, logger }) => { } return { - render: () => { - return modules[type]({ - ...data, - meta, - }); - }, - setRenderAttempted: true, - includeInNotification: true, + render: firstItemInBatch + ? () => + modules[type]({ + ...data, + meta, + }) + : null, + setRenderAttempted: firstItemInBatch, + setSuppressedDisplay: !firstItemInBatch, + includeInNotification: firstItemInBatch, }; }; }; diff --git a/src/components/Personalization/handlers/createProcessPropositions.js b/src/components/Personalization/handlers/createProcessPropositions.js index e50668ce2..fb87ab373 100644 --- a/src/components/Personalization/handlers/createProcessPropositions.js +++ b/src/components/Personalization/handlers/createProcessPropositions.js @@ -51,12 +51,12 @@ export default ({ schemaProcessors, logger }) => { return undefined; }); - const processItem = (item) => { + const processItem = (item, firstItemInBatch) => { const processor = schemaProcessors[item.getSchema()]; if (!processor) { return {}; } - return processor(item); + return processor(item, firstItemInBatch); }; const processItems = ({ @@ -65,6 +65,7 @@ export default ({ schemaProcessors, logger }) => { returnedDecisions: existingReturnedDecisions, items, proposition, + firstItemInBatch = false, }) => { let renderers = [...existingRenderers]; let returnedPropositions = [...existingReturnedPropositions]; @@ -75,6 +76,7 @@ export default ({ schemaProcessors, logger }) => { let atLeastOneWithNotification = false; let render; let setRenderAttempted; + let setSuppressedDisplay; let includeInNotification; let onlyRenderThis = false; let i = 0; @@ -82,8 +84,14 @@ export default ({ schemaProcessors, logger }) => { while (items.length > i) { item = items[i]; - ({ render, setRenderAttempted, includeInNotification, onlyRenderThis } = - processItem(item)); + ({ + render, + setRenderAttempted, + setSuppressedDisplay, + includeInNotification, + onlyRenderThis, + } = processItem(item, firstItemInBatch)); + if (onlyRenderThis) { returnedPropositions = []; returnedDecisions = []; @@ -99,6 +107,7 @@ export default ({ schemaProcessors, logger }) => { atLeastOneWithNotification = includeInNotification; break; } + if (render) { itemRenderers.push(wrapRenderWithLogging(render, item)); } @@ -126,6 +135,7 @@ export default ({ schemaProcessors, logger }) => { returnedDecisions, renderedItems, true, + setSuppressedDisplay, ); } if (nonRenderedItems.length > 0) { @@ -134,9 +144,11 @@ export default ({ schemaProcessors, logger }) => { returnedDecisions, nonRenderedItems, false, + setSuppressedDisplay, ); } + debugger; return { renderers, returnedPropositions, @@ -164,6 +176,7 @@ export default ({ schemaProcessors, logger }) => { returnedDecisions, items, proposition, + firstItemInBatch: i === 0, })); if (onlyRenderThis) { break; @@ -195,6 +208,7 @@ export default ({ schemaProcessors, logger }) => { false, ); }); + const render = () => { return Promise.all(renderers.map((renderer) => renderer())).then( (metas) => { diff --git a/src/components/Personalization/handlers/injectCreateProposition.js b/src/components/Personalization/handlers/injectCreateProposition.js index fdc2b85f5..74b66e023 100644 --- a/src/components/Personalization/handlers/injectCreateProposition.js +++ b/src/components/Personalization/handlers/injectCreateProposition.js @@ -90,12 +90,14 @@ export default ({ preprocess, isPageWideSurface }) => { decisions, includedItems, renderAttempted, + isSuppressedDisplay = false, ) { if (visibleInReturnedItems) { propositions.push({ ...payload, items: includedItems.map((i) => i.getOriginalItem()), renderAttempted, + isSuppressedDisplay, }); if (!renderAttempted) { decisions.push({ diff --git a/src/components/RulesEngine/createDecisionProvider.js b/src/components/RulesEngine/createDecisionProvider.js index 73e618a2f..245beef50 100644 --- a/src/components/RulesEngine/createDecisionProvider.js +++ b/src/components/RulesEngine/createDecisionProvider.js @@ -35,10 +35,23 @@ export default ({ eventRegistry }) => { } }; - const evaluate = (context = {}) => - Object.values(payloadsBasedOnActivityId) + const evaluate = (context = {}) => { + const sortedPayloadsBasedOnActivityId = Object.values( + payloadsBasedOnActivityId, + ).sort(({ rank: rankA }, { rank: rankB }) => { + if (rankA < rankB) { + return -1; + } + if (rankA > rankB) { + return 1; + } + return 0; + }); + + return sortedPayloadsBasedOnActivityId .map((payload) => payload.evaluate(context)) .filter((payload) => payload.items.length > 0); + }; const addPayloads = (personalizationPayloads) => { personalizationPayloads.forEach(addPayload); diff --git a/src/components/RulesEngine/createEvaluableRulesetPayload.js b/src/components/RulesEngine/createEvaluableRulesetPayload.js index bc7adbe6a..23cec655f 100644 --- a/src/components/RulesEngine/createEvaluableRulesetPayload.js +++ b/src/components/RulesEngine/createEvaluableRulesetPayload.js @@ -95,6 +95,7 @@ export default (payload, eventRegistry, decisionHistory) => { } return { + rank: payload?.scopeDetails?.rank || Infinity, evaluate, isEvaluable: items.length > 0, }; diff --git a/src/constants/eventType.js b/src/constants/eventType.js index e379a5198..1299715a3 100644 --- a/src/constants/eventType.js +++ b/src/constants/eventType.js @@ -14,4 +14,5 @@ export const DISPLAY = "decisioning.propositionDisplay"; export const INTERACT = "decisioning.propositionInteract"; export const TRIGGER = "decisioning.propositionTrigger"; export const DISMISS = "decisioning.propositionDismiss"; +export const SUPPRESS = "decisioning.propositionSuppressDisplay"; export const EVENT_TYPE_TRUE = 1; diff --git a/src/constants/propositionEventType.js b/src/constants/propositionEventType.js index 3b2358308..ca40fbd3f 100644 --- a/src/constants/propositionEventType.js +++ b/src/constants/propositionEventType.js @@ -10,13 +10,14 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import { DISPLAY, INTERACT, TRIGGER, DISMISS } from "./eventType.js"; +import { DISPLAY, INTERACT, TRIGGER, DISMISS, SUPPRESS } from "./eventType.js"; export const PropositionEventType = { DISPLAY: "display", INTERACT: "interact", TRIGGER: "trigger", DISMISS: "dismiss", + SUPPRESS: "suppressDisplay", }; const eventTypeToPropositionEventTypeMapping = { @@ -24,12 +25,15 @@ const eventTypeToPropositionEventTypeMapping = { [INTERACT]: PropositionEventType.INTERACT, [TRIGGER]: PropositionEventType.TRIGGER, [DISMISS]: PropositionEventType.DISMISS, + [SUPPRESS]: PropositionEventType.SUPPRESS, }; + const propositionEventTypeToEventTypeMapping = { [PropositionEventType.DISPLAY]: DISPLAY, [PropositionEventType.INTERACT]: INTERACT, [PropositionEventType.TRIGGER]: TRIGGER, [PropositionEventType.DISMISS]: DISMISS, + [PropositionEventType.SUPPRESS]: SUPPRESS, }; export const getPropositionEventType = (eventType) => From 9c27177b4d450d6006f1456083af8055ebc07409 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Mon, 7 Oct 2024 13:58:37 -0600 Subject: [PATCH 02/14] another approach --- .../createNotificationHandler.js | 47 ++++++++++--------- .../createOnDecisionHandler.js | 29 ++++++++---- .../handlers/createProcessInAppMessage.js | 19 ++++---- .../handlers/createProcessPropositions.js | 19 ++------ .../handlers/injectCreateProposition.js | 12 +++-- .../RulesEngine/createDecisionProvider.js | 10 +--- src/utils/createCollect.js | 4 ++ 7 files changed, 71 insertions(+), 69 deletions(-) diff --git a/src/components/Personalization/createNotificationHandler.js b/src/components/Personalization/createNotificationHandler.js index 2d7f1981f..73639832d 100644 --- a/src/components/Personalization/createNotificationHandler.js +++ b/src/components/Personalization/createNotificationHandler.js @@ -12,6 +12,11 @@ governing permissions and limitations under the License. import { defer } from "../../utils/index.js"; import { SUPPRESS } from "../../constants/eventType.js"; +const notificationDetail = (meta) => { + const { id, scope, scopeDetails } = meta; + return { id, scope, scopeDetails }; +}; + export default (collect, renderedPropositions) => { return (isRenderDecisions, isSendDisplayEvent, viewName) => { if (!isRenderDecisions) { @@ -25,29 +30,25 @@ export default (collect, renderedPropositions) => { return renderedPropositionsDeferred.resolve; } - return (suppressedPropositions, decisionsMeta) => { - if (decisionsMeta.length > 0) { - collect({ - decisionsMeta, - viewName, - }); - } - - if (suppressedPropositions && suppressedPropositions.length > 0) { - const suppressedMeta = suppressedPropositions.map( - ({ id, scope, scopeDetails }) => ({ - id, - scope, - scopeDetails, - }), - ); - - collect({ - decisionsMeta: suppressedMeta, - eventType: SUPPRESS, - viewName, - }); - } + return (decisionsMeta) => { + const decisionsMetaDisplay = decisionsMeta + .filter((meta) => !meta.shouldSuppressDisplay) + .map(notificationDetail); + + const decisionsMetaSuppressed = decisionsMeta + .filter((meta) => meta.shouldSuppressDisplay) + .map(notificationDetail); + + collect({ + decisionsMeta: decisionsMetaDisplay, + viewName, + }); + + collect({ + decisionsMeta: decisionsMetaSuppressed, + eventType: SUPPRESS, + viewName, + }); }; }; }; diff --git a/src/components/Personalization/createOnDecisionHandler.js b/src/components/Personalization/createOnDecisionHandler.js index d410c2e54..b35f6f34b 100644 --- a/src/components/Personalization/createOnDecisionHandler.js +++ b/src/components/Personalization/createOnDecisionHandler.js @@ -10,6 +10,22 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ +import { MESSAGE_IN_APP } from "../../constants/schema.js"; + +const createShouldSuppressDisplay = () => { + let count = 0; + return (proposition) => { + const { items = [] } = proposition; + + if (!items.some((item) => item.schema === MESSAGE_IN_APP)) { + return false; + } + count += 1; + + return count > 1; + }; +}; + export default ({ processPropositions, createProposition, @@ -23,26 +39,23 @@ export default ({ const { sendDisplayEvent = true } = personalization; const viewName = event ? event.getViewName() : undefined; + const shouldSuppressDisplay = createShouldSuppressDisplay(); + const propositionsToExecute = propositions.map((proposition) => - createProposition(proposition, true), + createProposition(proposition, true, shouldSuppressDisplay(proposition)), ); const { render, returnedPropositions } = processPropositions( propositionsToExecute, ); - debugger; const handleNotifications = notificationHandler( renderDecisions, sendDisplayEvent, viewName, ); - render().then( - handleNotifications.bind( - null, - returnedPropositions.filter((p) => p.isSuppressedDisplay), - ), - ); + + render().then(handleNotifications); return Promise.resolve({ propositions: returnedPropositions, diff --git a/src/components/Personalization/handlers/createProcessInAppMessage.js b/src/components/Personalization/handlers/createProcessInAppMessage.js index c65eba67b..82f951fb5 100644 --- a/src/components/Personalization/handlers/createProcessInAppMessage.js +++ b/src/components/Personalization/handlers/createProcessInAppMessage.js @@ -47,7 +47,7 @@ const isValidInAppMessage = (data, logger) => { }; export default ({ modules, logger }) => { - return (item, firstItemInBatch) => { + return (item) => { const data = item.getData(); const meta = { ...item.getProposition().getNotification() }; @@ -73,16 +73,13 @@ export default ({ modules, logger }) => { } return { - render: firstItemInBatch - ? () => - modules[type]({ - ...data, - meta, - }) - : null, - setRenderAttempted: firstItemInBatch, - setSuppressedDisplay: !firstItemInBatch, - includeInNotification: firstItemInBatch, + render: () => + modules[type]({ + ...data, + meta, + }), + setRenderAttempted: true, + includeInNotification: true, }; }; }; diff --git a/src/components/Personalization/handlers/createProcessPropositions.js b/src/components/Personalization/handlers/createProcessPropositions.js index fb87ab373..cd29ac408 100644 --- a/src/components/Personalization/handlers/createProcessPropositions.js +++ b/src/components/Personalization/handlers/createProcessPropositions.js @@ -51,12 +51,12 @@ export default ({ schemaProcessors, logger }) => { return undefined; }); - const processItem = (item, firstItemInBatch) => { + const processItem = (item) => { const processor = schemaProcessors[item.getSchema()]; if (!processor) { return {}; } - return processor(item, firstItemInBatch); + return processor(item); }; const processItems = ({ @@ -65,7 +65,6 @@ export default ({ schemaProcessors, logger }) => { returnedDecisions: existingReturnedDecisions, items, proposition, - firstItemInBatch = false, }) => { let renderers = [...existingRenderers]; let returnedPropositions = [...existingReturnedPropositions]; @@ -76,7 +75,6 @@ export default ({ schemaProcessors, logger }) => { let atLeastOneWithNotification = false; let render; let setRenderAttempted; - let setSuppressedDisplay; let includeInNotification; let onlyRenderThis = false; let i = 0; @@ -84,13 +82,8 @@ export default ({ schemaProcessors, logger }) => { while (items.length > i) { item = items[i]; - ({ - render, - setRenderAttempted, - setSuppressedDisplay, - includeInNotification, - onlyRenderThis, - } = processItem(item, firstItemInBatch)); + ({ render, setRenderAttempted, includeInNotification, onlyRenderThis } = + processItem(item)); if (onlyRenderThis) { returnedPropositions = []; @@ -135,7 +128,6 @@ export default ({ schemaProcessors, logger }) => { returnedDecisions, renderedItems, true, - setSuppressedDisplay, ); } if (nonRenderedItems.length > 0) { @@ -144,11 +136,9 @@ export default ({ schemaProcessors, logger }) => { returnedDecisions, nonRenderedItems, false, - setSuppressedDisplay, ); } - debugger; return { renderers, returnedPropositions, @@ -176,7 +166,6 @@ export default ({ schemaProcessors, logger }) => { returnedDecisions, items, proposition, - firstItemInBatch: i === 0, })); if (onlyRenderThis) { break; diff --git a/src/components/Personalization/handlers/injectCreateProposition.js b/src/components/Personalization/handlers/injectCreateProposition.js index 74b66e023..b5264549e 100644 --- a/src/components/Personalization/handlers/injectCreateProposition.js +++ b/src/components/Personalization/handlers/injectCreateProposition.js @@ -56,7 +56,11 @@ export default ({ preprocess, isPageWideSurface }) => { }; }; - return (payload, visibleInReturnedItems = true) => { + return ( + payload, + visibleInReturnedItems = true, + shouldSuppressDisplay = false, + ) => { const { id, scope, scopeDetails, items = [] } = payload; const { characteristics: { scopeType } = {} } = scopeDetails || {}; @@ -85,19 +89,21 @@ export default ({ preprocess, isPageWideSurface }) => { toJSON() { return payload; }, + shouldSuppressDisplay() { + return shouldSuppressDisplay; + }, addToReturnValues( propositions, decisions, includedItems, renderAttempted, - isSuppressedDisplay = false, ) { if (visibleInReturnedItems) { propositions.push({ ...payload, items: includedItems.map((i) => i.getOriginalItem()), renderAttempted, - isSuppressedDisplay, + shouldSuppressDisplay, }); if (!renderAttempted) { decisions.push({ diff --git a/src/components/RulesEngine/createDecisionProvider.js b/src/components/RulesEngine/createDecisionProvider.js index 245beef50..e0b3ed363 100644 --- a/src/components/RulesEngine/createDecisionProvider.js +++ b/src/components/RulesEngine/createDecisionProvider.js @@ -38,15 +38,7 @@ export default ({ eventRegistry }) => { const evaluate = (context = {}) => { const sortedPayloadsBasedOnActivityId = Object.values( payloadsBasedOnActivityId, - ).sort(({ rank: rankA }, { rank: rankB }) => { - if (rankA < rankB) { - return -1; - } - if (rankA > rankB) { - return 1; - } - return 0; - }); + ).sort(({ rank: rankA }, { rank: rankB }) => rankB - rankA); return sortedPayloadsBasedOnActivityId .map((payload) => payload.evaluate(context)) diff --git a/src/utils/createCollect.js b/src/utils/createCollect.js index f15fc6558..0c454dc42 100644 --- a/src/utils/createCollect.js +++ b/src/utils/createCollect.js @@ -23,6 +23,10 @@ export default ({ eventManager, mergeDecisionsMeta }) => { propositionEventTypes = [getPropositionEventType(eventType)], viewName, }) => { + if (!Array.isArray(decisionsMeta) || decisionsMeta.length === 0) { + return Promise.resolve(); + } + const event = eventManager.createEvent(); const data = { eventType }; From cc8914e7e1e77a83692fce6a193773cd62633148 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Mon, 7 Oct 2024 13:58:52 -0600 Subject: [PATCH 03/14] this too --- .../handlers/createProcessPropositions.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/Personalization/handlers/createProcessPropositions.js b/src/components/Personalization/handlers/createProcessPropositions.js index cd29ac408..829b2ceaf 100644 --- a/src/components/Personalization/handlers/createProcessPropositions.js +++ b/src/components/Personalization/handlers/createProcessPropositions.js @@ -59,6 +59,13 @@ export default ({ schemaProcessors, logger }) => { return processor(item); }; + const getNotification = (proposition) => { + return { + ...proposition.getNotification(), + shouldSuppressDisplay: proposition.shouldSuppressDisplay(), + }; + }; + const processItems = ({ renderers: existingRenderers, returnedPropositions: existingReturnedPropositions, @@ -116,11 +123,11 @@ export default ({ schemaProcessors, logger }) => { } if (itemRenderers.length > 0) { const meta = atLeastOneWithNotification - ? proposition.getNotification() + ? getNotification(proposition) : undefined; renderers.push(() => renderItems(itemRenderers, meta)); } else if (atLeastOneWithNotification) { - renderers.push(() => Promise.resolve(proposition.getNotification())); + renderers.push(() => Promise.resolve(getNotification(proposition))); } if (renderedItems.length > 0) { proposition.addToReturnValues( From 6436ae3757ea2388782378a80833fe74afa6a43b Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Mon, 7 Oct 2024 14:57:45 -0600 Subject: [PATCH 04/14] cleaner --- .../createNotificationHandler.js | 15 +------------- .../createOnDecisionHandler.js | 20 ++++++++++++++++++- .../handlers/createProcessPropositions.js | 11 ++-------- .../handlers/injectCreateProposition.js | 1 - 4 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/components/Personalization/createNotificationHandler.js b/src/components/Personalization/createNotificationHandler.js index 73639832d..ac499df58 100644 --- a/src/components/Personalization/createNotificationHandler.js +++ b/src/components/Personalization/createNotificationHandler.js @@ -12,11 +12,6 @@ governing permissions and limitations under the License. import { defer } from "../../utils/index.js"; import { SUPPRESS } from "../../constants/eventType.js"; -const notificationDetail = (meta) => { - const { id, scope, scopeDetails } = meta; - return { id, scope, scopeDetails }; -}; - export default (collect, renderedPropositions) => { return (isRenderDecisions, isSendDisplayEvent, viewName) => { if (!isRenderDecisions) { @@ -30,15 +25,7 @@ export default (collect, renderedPropositions) => { return renderedPropositionsDeferred.resolve; } - return (decisionsMeta) => { - const decisionsMetaDisplay = decisionsMeta - .filter((meta) => !meta.shouldSuppressDisplay) - .map(notificationDetail); - - const decisionsMetaSuppressed = decisionsMeta - .filter((meta) => meta.shouldSuppressDisplay) - .map(notificationDetail); - + return (decisionsMetaDisplay = [], decisionsMetaSuppressed = []) => { collect({ decisionsMeta: decisionsMetaDisplay, viewName, diff --git a/src/components/Personalization/createOnDecisionHandler.js b/src/components/Personalization/createOnDecisionHandler.js index b35f6f34b..6a4a204d1 100644 --- a/src/components/Personalization/createOnDecisionHandler.js +++ b/src/components/Personalization/createOnDecisionHandler.js @@ -55,7 +55,25 @@ export default ({ viewName, ); - render().then(handleNotifications); + const propositionsById = propositionsToExecute.reduce( + (tot, proposition) => { + tot[proposition.getId()] = proposition; + return tot; + }, + {}, + ); + + render().then((decisionsMeta) => { + const decisionsMetaDisplay = decisionsMeta.filter( + (meta) => !propositionsById[meta.id].shouldSuppressDisplay(), + ); + + const decisionsMetaSuppressed = decisionsMeta.filter((meta) => + propositionsById[meta.id].shouldSuppressDisplay(), + ); + + handleNotifications(decisionsMetaDisplay, decisionsMetaSuppressed); + }); return Promise.resolve({ propositions: returnedPropositions, diff --git a/src/components/Personalization/handlers/createProcessPropositions.js b/src/components/Personalization/handlers/createProcessPropositions.js index 829b2ceaf..cd29ac408 100644 --- a/src/components/Personalization/handlers/createProcessPropositions.js +++ b/src/components/Personalization/handlers/createProcessPropositions.js @@ -59,13 +59,6 @@ export default ({ schemaProcessors, logger }) => { return processor(item); }; - const getNotification = (proposition) => { - return { - ...proposition.getNotification(), - shouldSuppressDisplay: proposition.shouldSuppressDisplay(), - }; - }; - const processItems = ({ renderers: existingRenderers, returnedPropositions: existingReturnedPropositions, @@ -123,11 +116,11 @@ export default ({ schemaProcessors, logger }) => { } if (itemRenderers.length > 0) { const meta = atLeastOneWithNotification - ? getNotification(proposition) + ? proposition.getNotification() : undefined; renderers.push(() => renderItems(itemRenderers, meta)); } else if (atLeastOneWithNotification) { - renderers.push(() => Promise.resolve(getNotification(proposition))); + renderers.push(() => Promise.resolve(proposition.getNotification())); } if (renderedItems.length > 0) { proposition.addToReturnValues( diff --git a/src/components/Personalization/handlers/injectCreateProposition.js b/src/components/Personalization/handlers/injectCreateProposition.js index b5264549e..a91c0df06 100644 --- a/src/components/Personalization/handlers/injectCreateProposition.js +++ b/src/components/Personalization/handlers/injectCreateProposition.js @@ -103,7 +103,6 @@ export default ({ preprocess, isPageWideSurface }) => { ...payload, items: includedItems.map((i) => i.getOriginalItem()), renderAttempted, - shouldSuppressDisplay, }); if (!renderAttempted) { decisions.push({ From 57a146128b204b31533a7ecacb021bca71b6e2ab Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Mon, 7 Oct 2024 15:33:14 -0600 Subject: [PATCH 05/14] fix tests --- package.json | 2 +- .../createNotificationHandler.js | 28 +++++++++++++------ src/utils/createCollect.js | 4 --- .../createOnDecisionHandler.spec.js | 8 ++++-- 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 4b0835588..1050b2ac2 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "format": "prettier --write \"*.{html,js,cjs,mjs,jsx}\" \"{sandbox,src,test,scripts}/**/*.{html,js,cjs,mjs,jsx}\"", "test": "npm run test:unit && npm run test:scripts && npm run test:functional", "test:unit": "karma start karma.conf.cjs --single-run", - "test:unit:debug": "karma start --browsers=Chrome --single-run=false --debug", + "test:unit:debug": "karma start karma.conf.cjs --browsers=Chrome --single-run=false --debug", "test:unit:watch": "karma start", "test:unit:saucelabs:local": "karma start karma.saucelabs.conf.cjs --single-run", "test:unit:coverage": "karma start --single-run --reporters spec,coverage", diff --git a/src/components/Personalization/createNotificationHandler.js b/src/components/Personalization/createNotificationHandler.js index ac499df58..15c24c77c 100644 --- a/src/components/Personalization/createNotificationHandler.js +++ b/src/components/Personalization/createNotificationHandler.js @@ -26,16 +26,26 @@ export default (collect, renderedPropositions) => { } return (decisionsMetaDisplay = [], decisionsMetaSuppressed = []) => { - collect({ - decisionsMeta: decisionsMetaDisplay, - viewName, - }); + if ( + Array.isArray(decisionsMetaDisplay) && + decisionsMetaDisplay.length > 0 + ) { + collect({ + decisionsMeta: decisionsMetaDisplay, + viewName, + }); + } - collect({ - decisionsMeta: decisionsMetaSuppressed, - eventType: SUPPRESS, - viewName, - }); + if ( + Array.isArray(decisionsMetaSuppressed) && + decisionsMetaSuppressed.length > 0 + ) { + collect({ + decisionsMeta: decisionsMetaSuppressed, + eventType: SUPPRESS, + viewName, + }); + } }; }; }; diff --git a/src/utils/createCollect.js b/src/utils/createCollect.js index 0c454dc42..f15fc6558 100644 --- a/src/utils/createCollect.js +++ b/src/utils/createCollect.js @@ -23,10 +23,6 @@ export default ({ eventManager, mergeDecisionsMeta }) => { propositionEventTypes = [getPropositionEventType(eventType)], viewName, }) => { - if (!Array.isArray(decisionsMeta) || decisionsMeta.length === 0) { - return Promise.resolve(); - } - const event = eventManager.createEvent(); const data = { eventType }; diff --git a/test/unit/specs/components/Personalization/createOnDecisionHandler.spec.js b/test/unit/specs/components/Personalization/createOnDecisionHandler.spec.js index f16aa4301..ca4d37308 100644 --- a/test/unit/specs/components/Personalization/createOnDecisionHandler.spec.js +++ b/test/unit/specs/components/Personalization/createOnDecisionHandler.spec.js @@ -186,7 +186,11 @@ describe("Personalization::createOnDecisionHandler", () => { beforeEach(() => { render = jasmine .createSpy("render") - .and.returnValue(Promise.resolve([{ hi: true }])); + .and.returnValue( + Promise.resolve([ + { id: "1a3d874f-39ee-4310-bfa9-6559a10041a4", hi: true }, + ]), + ); collect = jasmine.createSpy("collect").and.returnValue(Promise.resolve()); processPropositions = jasmine .createSpy("processPropositions") @@ -237,7 +241,7 @@ describe("Personalization::createOnDecisionHandler", () => { expect(render).toHaveBeenCalledTimes(1); expect(collect).toHaveBeenCalledOnceWith({ - decisionsMeta: [{ hi: true }], + decisionsMeta: [{ id: "1a3d874f-39ee-4310-bfa9-6559a10041a4", hi: true }], viewName: "blippi", }); expect(renderedPropositions.concat).not.toHaveBeenCalled(); From adabc2da252facc053fe3cb09d230f287fe008e0 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Mon, 7 Oct 2024 16:35:04 -0600 Subject: [PATCH 06/14] fix bug --- .../handlers/createProcessInAppMessage.js | 14 +++++++++----- .../RulesEngine/createDecisionProvider.js | 2 +- .../handlers/createProcessInAppMessage.spec.js | 5 ++++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/components/Personalization/handlers/createProcessInAppMessage.js b/src/components/Personalization/handlers/createProcessInAppMessage.js index 82f951fb5..bb6cd21e7 100644 --- a/src/components/Personalization/handlers/createProcessInAppMessage.js +++ b/src/components/Personalization/handlers/createProcessInAppMessage.js @@ -50,6 +50,7 @@ export default ({ modules, logger }) => { return (item) => { const data = item.getData(); const meta = { ...item.getProposition().getNotification() }; + const shouldSuppressDisplay = item.getProposition().shouldSuppressDisplay(); if (!data) { logger.warn("Invalid in-app message data: undefined.", data); @@ -73,11 +74,14 @@ export default ({ modules, logger }) => { } return { - render: () => - modules[type]({ - ...data, - meta, - }), + render: () => { + return shouldSuppressDisplay + ? Promise.resolve() + : modules[type]({ + ...data, + meta, + }); + }, setRenderAttempted: true, includeInNotification: true, }; diff --git a/src/components/RulesEngine/createDecisionProvider.js b/src/components/RulesEngine/createDecisionProvider.js index e0b3ed363..c8a58c6b9 100644 --- a/src/components/RulesEngine/createDecisionProvider.js +++ b/src/components/RulesEngine/createDecisionProvider.js @@ -38,7 +38,7 @@ export default ({ eventRegistry }) => { const evaluate = (context = {}) => { const sortedPayloadsBasedOnActivityId = Object.values( payloadsBasedOnActivityId, - ).sort(({ rank: rankA }, { rank: rankB }) => rankB - rankA); + ).sort(({ rank: rankA }, { rank: rankB }) => rankA - rankB); return sortedPayloadsBasedOnActivityId .map((payload) => payload.evaluate(context)) diff --git a/test/unit/specs/components/Personalization/handlers/createProcessInAppMessage.spec.js b/test/unit/specs/components/Personalization/handlers/createProcessInAppMessage.spec.js index 394f75ea4..eb6445e25 100644 --- a/test/unit/specs/components/Personalization/handlers/createProcessInAppMessage.spec.js +++ b/test/unit/specs/components/Personalization/handlers/createProcessInAppMessage.spec.js @@ -25,7 +25,10 @@ describe("Personalization::handlers::createProcessInAppMessage", () => { return data; }, getProposition() { - return { getNotification: () => meta }; + return { + getNotification: () => meta, + shouldSuppressDisplay: () => false, + }; }, }; modules = { From 16efe79962ad64f9aba4464b9a2b05baf068f5e6 Mon Sep 17 00:00:00 2001 From: Serban Stancu Date: Tue, 8 Oct 2024 08:21:10 -0600 Subject: [PATCH 07/14] Small optimization. --- .../Personalization/handlers/createProcessInAppMessage.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/Personalization/handlers/createProcessInAppMessage.js b/src/components/Personalization/handlers/createProcessInAppMessage.js index bb6cd21e7..80a73bbf4 100644 --- a/src/components/Personalization/handlers/createProcessInAppMessage.js +++ b/src/components/Personalization/handlers/createProcessInAppMessage.js @@ -49,8 +49,9 @@ const isValidInAppMessage = (data, logger) => { export default ({ modules, logger }) => { return (item) => { const data = item.getData(); - const meta = { ...item.getProposition().getNotification() }; - const shouldSuppressDisplay = item.getProposition().shouldSuppressDisplay(); + const proposition = item.getProposition(); + const meta = { ...proposition.getNotification() }; + const shouldSuppressDisplay = proposition.shouldSuppressDisplay(); if (!data) { logger.warn("Invalid in-app message data: undefined.", data); @@ -76,7 +77,7 @@ export default ({ modules, logger }) => { return { render: () => { return shouldSuppressDisplay - ? Promise.resolve() + ? null : modules[type]({ ...data, meta, From 0dc5b5507ae3dbba1398dbbd77c7ea243f9460a0 Mon Sep 17 00:00:00 2001 From: Serban Stancu Date: Tue, 8 Oct 2024 08:21:32 -0600 Subject: [PATCH 08/14] Add propositionAction. --- src/components/Personalization/createNotificationHandler.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Personalization/createNotificationHandler.js b/src/components/Personalization/createNotificationHandler.js index 15c24c77c..cd5a6837d 100644 --- a/src/components/Personalization/createNotificationHandler.js +++ b/src/components/Personalization/createNotificationHandler.js @@ -43,6 +43,7 @@ export default (collect, renderedPropositions) => { collect({ decisionsMeta: decisionsMetaSuppressed, eventType: SUPPRESS, + propositionAction: { reason: "Conflict" }, viewName, }); } From ca45de703f168653c3497998869884cc6d52c7d0 Mon Sep 17 00:00:00 2001 From: Serban Stancu Date: Wed, 9 Oct 2024 17:36:48 -0600 Subject: [PATCH 09/14] Fix test count. --- scripts/specs/updatePackageVersion.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/specs/updatePackageVersion.spec.js b/scripts/specs/updatePackageVersion.spec.js index 177a3b05e..cfdd9aa7f 100644 --- a/scripts/specs/updatePackageVersion.spec.js +++ b/scripts/specs/updatePackageVersion.spec.js @@ -31,7 +31,7 @@ describe("updatePackageVersion", () => { expect(logger.info).toHaveBeenCalledOnceWith( "Updating package.json with version 1.2.3.", ); - expect(exec).toHaveBeenCalledTimes(4); + expect(exec).toHaveBeenCalledTimes(5); }); it("doesn't update the package version", async () => { From e8769a8e52dda1c24808ae7fa80f2996dbe10e41 Mon Sep 17 00:00:00 2001 From: Serban Stancu Date: Wed, 9 Oct 2024 17:37:16 -0600 Subject: [PATCH 10/14] Add option to run a specific test in watch mode. --- scripts/watchFunctionalTests.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/scripts/watchFunctionalTests.js b/scripts/watchFunctionalTests.js index e12046bb8..3a82128e5 100755 --- a/scripts/watchFunctionalTests.js +++ b/scripts/watchFunctionalTests.js @@ -36,6 +36,13 @@ program.addOption( .default("chrome"), ); +program.addOption( + new Option( + "--specsPath ", + "configures the test runner to run tests from the specified files", + ), +); + program.parse(); const argv = program.opts(); @@ -68,8 +75,14 @@ const effectByEventCode = { } else { firstBuildComplete = true; const testcafe = await createTestCafe(); - const liveRunner = testcafe.createLiveModeRunner(); - await liveRunner.browsers(argv.browsers).run(); + let liveRunner = testcafe.createLiveModeRunner(); + liveRunner = liveRunner.browsers(argv.browsers); + + if (argv.specsPath) { + liveRunner = await liveRunner.src(argv.specsPath); + } + + await liveRunner.run(); await testcafe.close(); } }, From cc29c4ceb79a0dbfc36a7409054471c08223f571 Mon Sep 17 00:00:00 2001 From: Serban Stancu Date: Wed, 9 Oct 2024 17:38:09 -0600 Subject: [PATCH 11/14] Add test for conflict resolution testing. --- .../Personalization/conflictResolutions.js | 251 ++++++++++++++++++ .../Personalization/conflictResolutions.js | 127 +++++++++ 2 files changed, 378 insertions(+) create mode 100644 test/functional/fixtures/Personalization/conflictResolutions.js create mode 100644 test/functional/specs/Personalization/conflictResolutions.js diff --git a/test/functional/fixtures/Personalization/conflictResolutions.js b/test/functional/fixtures/Personalization/conflictResolutions.js new file mode 100644 index 000000000..88a493ca0 --- /dev/null +++ b/test/functional/fixtures/Personalization/conflictResolutions.js @@ -0,0 +1,251 @@ +/* +Copyright 2024 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 inAppMessagesPropositions = { + requestId: "09ead9d7-b91f-41d6-9bb5-e7a5396315d6", + handle: [ + { + payload: [ + { + id: "03560676528201411421379778603411811904", + namespace: { + code: "ECID", + }, + }, + ], + type: "identity:result", + }, + { + payload: [ + { + id: "id2", + scope: "web://www.test.com/", + scopeDetails: { + rank: 2, + decisionProvider: "AJO", + correlationID: "3a034993-3bdb-4d8d-870f-0817e8949ebd-0", + characteristics: { + eventToken: + "eyJtZXNzYWdlRXhlY3V0aW9uIjp7Im1lc3NhZ2VFeGVjdXRpb25JRCI6IlVFOkluYm91bmQiLCJtZXNzYWdlSUQiOiIxYmQ4NWMwMC1iNGM5LTQ0NjQtYjIyMC1hYzY5ZTY5NGI1M2QiLCJtZXNzYWdlUHVibGljYXRpb25JRCI6IjNhMDM0OTkzLTNiZGItNGQ4ZC04NzBmLTA4MTdlODk0OWViZCIsIm1lc3NhZ2VUeXBlIjoibWFya2V0aW5nIiwiY2FtcGFpZ25JRCI6IjY4ODg5NDdjLTAyZDQtNDFhNi05YzZjLTE1YTU4ZTVlNzcyYyIsImNhbXBhaWduVmVyc2lvbklEIjoiNDU1NzE2MmQtNzgyMi00Zjg4LTkyZWItMmMxZmViZTFhMmViIiwiY2FtcGFpZ25BY3Rpb25JRCI6ImNhMzJlZjZmLWY3NTItNDA2Ny04YmFlLTE1NGY2MDAyN2E3ZSJ9LCJtZXNzYWdlUHJvZmlsZSI6eyJtZXNzYWdlUHJvZmlsZUlEIjoiYjBlM2M0YjctMWE3YS00NTljLWE2YzUtNTFlNDU0ZDVjYmRiIiwiY2hhbm5lbCI6eyJfaWQiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbHMvaW5BcHAiLCJfdHlwZSI6Imh0dHBzOi8vbnMuYWRvYmUuY29tL3hkbS9jaGFubmVsLXR5cGVzL2luQXBwIn19fQ==", + }, + activity: { + id: "6888947c-02d4-41a6-9c6c-15a58e5e772c#ca32ef6f-f752-4067-8bae-154f60027a7e", + matchedSurfaces: ["web://surface1/"], + }, + }, + items: [ + { + id: "e1090531-caee-4749-a738-680f393cf4a9", + schema: "https://ns.adobe.com/personalization/ruleset-item", + data: { + version: 1, + rules: [ + { + condition: { + definition: { + key: "~type", + matcher: "eq", + values: ["com.adobe.eventType.rulesEngine"], + }, + type: "matcher", + }, + consequences: [ + { + id: "f03980c9-5344-4e32-84c7-d74ffebf06d6", + type: "schema", + detail: { + id: "f03980c9-5344-4e32-84c7-d74ffebf06d6", + schema: + "https://ns.adobe.com/personalization/message/in-app", + data: { + content: + "Proposition 2", + contentType: "text/html", + remoteAssets: [ + "https://cdn.experience.adobe.net/solutions/cjm-inapp-editor/assets/InAppBlockImageDefault.aa849e19.svg", + ], + webParameters: { + "alloy-content-iframe": { + style: { + border: "none", + height: "100%", + width: "100%", + }, + params: { + enabled: true, + insertionMethod: "appendChild", + parentElement: "#alloy-messaging-container", + }, + }, + "alloy-messaging-container": { + style: { + backgroundColor: "#000000", + border: "none", + borderRadius: "15px", + height: "100vh", + overflow: "hidden", + position: "fixed", + width: "100%", + left: "50%", + transform: + "translateX(-50%) translateY(-50%)", + top: "50%", + }, + params: { + enabled: true, + insertionMethod: "appendChild", + parentElement: "body", + }, + }, + "alloy-overlay-container": { + style: { + position: "fixed", + top: "0", + left: "0", + width: "100%", + height: "100%", + background: "transparent", + opacity: 0.2, + backgroundColor: "#000000", + }, + params: { + enabled: true, + insertionMethod: "appendChild", + parentElement: "body", + }, + }, + }, + publishedDate: 1727568749, + }, + }, + }, + ], + }, + ], + }, + }, + ], + }, + { + id: "id1", + scope: "web://www.test.com/", + scopeDetails: { + rank: 1, + decisionProvider: "AJO", + correlationID: "d4f5d677-1123-4605-ad9e-86fead923220-0", + characteristics: { + eventToken: + "eyJtZXNzYWdlRXhlY3V0aW9uIjp7Im1lc3NhZ2VFeGVjdXRpb25JRCI6IlVFOkluYm91bmQiLCJtZXNzYWdlSUQiOiJiNmY1ODVmZS1mNWE1LTQ0NWUtYjU0OC0yOThlMmQ1MDFkZTYiLCJtZXNzYWdlUHVibGljYXRpb25JRCI6ImQ0ZjVkNjc3LTExMjMtNDYwNS1hZDllLTg2ZmVhZDkyMzIyMCIsIm1lc3NhZ2VUeXBlIjoibWFya2V0aW5nIiwiY2FtcGFpZ25JRCI6ImI3NWNkNDAyLTIwNzMtNDc5OC1hMDJjLTc0YmQyMDg0MmQyNyIsImNhbXBhaWduVmVyc2lvbklEIjoiMmExMDJjZDMtZmM0NC00ZDI0LWEzMWMtNzZhZTBmY2FjMWJhIiwiY2FtcGFpZ25BY3Rpb25JRCI6ImNhMzJlZjZmLWY3NTItNDA2Ny04YmFlLTE1NGY2MDAyN2E3ZSJ9LCJtZXNzYWdlUHJvZmlsZSI6eyJtZXNzYWdlUHJvZmlsZUlEIjoiNzEyZjYwN2UtNjNmNS00NGExLTg3ZDYtM2Y1NWQzYjVmNGM2IiwiY2hhbm5lbCI6eyJfaWQiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbHMvaW5BcHAiLCJfdHlwZSI6Imh0dHBzOi8vbnMuYWRvYmUuY29tL3hkbS9jaGFubmVsLXR5cGVzL2luQXBwIn19fQ==", + }, + activity: { + id: "b75cd402-2073-4798-a02c-74bd20842d27#ca32ef6f-f752-4067-8bae-154f60027a7e", + matchedSurfaces: ["web://surface1/"], + }, + }, + items: [ + { + id: "471af8f8-7806-47ce-885e-45fffe14108a", + schema: "https://ns.adobe.com/personalization/ruleset-item", + data: { + version: 1, + rules: [ + { + condition: { + definition: { + key: "~type", + matcher: "eq", + values: ["com.adobe.eventType.rulesEngine"], + }, + type: "matcher", + }, + consequences: [ + { + id: "613e9843-7291-4505-9cca-3f76caff6bec", + type: "schema", + detail: { + id: "613e9843-7291-4505-9cca-3f76caff6bec", + schema: + "https://ns.adobe.com/personalization/message/in-app", + data: { + content: + "Proposition 1", + contentType: "text/html", + remoteAssets: [ + "https://cdn.experience.adobe.net/solutions/cjm-inapp-editor/assets/InAppBlockImageDefault.aa849e19.svg", + ], + webParameters: { + "alloy-content-iframe": { + style: { + border: "none", + height: "100%", + width: "100%", + }, + params: { + enabled: true, + insertionMethod: "appendChild", + parentElement: "#alloy-messaging-container", + }, + }, + "alloy-messaging-container": { + style: { + backgroundColor: "#000000", + border: "none", + borderRadius: "15px", + height: "100vh", + overflow: "hidden", + position: "fixed", + width: "100%", + left: "50%", + transform: + "translateX(-50%) translateY(-50%)", + top: "50%", + }, + params: { + enabled: true, + insertionMethod: "appendChild", + parentElement: "body", + }, + }, + "alloy-overlay-container": { + style: { + position: "fixed", + top: "0", + left: "0", + width: "100%", + height: "100%", + background: "transparent", + opacity: 0.2, + backgroundColor: "#000000", + }, + params: { + enabled: true, + insertionMethod: "appendChild", + parentElement: "body", + }, + }, + }, + publishedDate: 1727568995, + }, + }, + }, + ], + }, + ], + }, + }, + ], + }, + ], + type: "personalization:decisions", + eventIndex: 0, + }, + ], +}; diff --git a/test/functional/specs/Personalization/conflictResolutions.js b/test/functional/specs/Personalization/conflictResolutions.js new file mode 100644 index 000000000..d45b19d0e --- /dev/null +++ b/test/functional/specs/Personalization/conflictResolutions.js @@ -0,0 +1,127 @@ +/* +Copyright 2024 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 { ClientFunction, RequestMock, t } from "testcafe"; +import createNetworkLogger from "../../helpers/networkLogger/index.js"; +import createFixture from "../../helpers/createFixture/index.js"; +import { inAppMessagesPropositions } from "../../fixtures/Personalization/conflictResolutions.js"; +import createAlloyProxy from "../../helpers/createAlloyProxy.js"; +import getBaseConfig from "../../helpers/getBaseConfig.js"; +import { + compose, + debugEnabled, +} from "../../helpers/constants/configParts/index.js"; + +const networkLogger = createNetworkLogger(); + +const organizationId = "5BFE274A5F6980A50A495C08@AdobeOrg"; +const dataStreamId = "ae47e1ea-7625-49b9-b69f-8ad372e46344"; + +const orgMainConfigMain = getBaseConfig(organizationId, dataStreamId); +const config = compose(orgMainConfigMain, debugEnabled); + +const firstInteractCallReturnsInAppMessagesMock = () => { + let i = 0; + return RequestMock() + .onRequestTo((request) => { + if (request.url.includes("/interact")) { + i += 1; + } + + const r = request.url.includes("/interact") && i === 2; + return r; + }) + .respond(JSON.stringify(inAppMessagesPropositions), 200, { + "access-control-allow-origin": "https://alloyio.com", + "access-control-allow-credentials": true, + "content-type": " application/json;charset=utf-8", + }); +}; + +createFixture({ + title: + "Conflict resoulution: show one in app message propositions sorted by rank", + requestHooks: [ + firstInteractCallReturnsInAppMessagesMock(), + networkLogger.edgeEndpointLogs, + ], +}); + +test("propositions are sorted ascending by rank", async () => { + const alloy = createAlloyProxy(); + await alloy.configure(config); + + // Sending an event for getting an Identity cookie. + await alloy.sendEvent(); + + await alloy.sendEvent({ + renderDecisions: true, + }); + + const result = await alloy.evaluateRulesets({ + renderDecisions: true, + }); + + // When we receive multiple In-App messages propositions, DecisioningEngine will sort them ascending by rank. + // Lower rank means higher priority. We need to diplay just one proposition (with the rank 1). There can be + // multiple propositions with the same rank but with different surfaces. + // The mock returns the propositions in descending order by rank. + // For the displayed propositions an event with the type decisioning.propositionDisplay is sent back to the edge. + // For the suppressed propositions an event with the type decisioning.propositionSuppressDisplay is sent back to the edge. + await t.expect(result.propositions[0].scopeDetails.rank).eql(1); + await t.expect(result.propositions[1].scopeDetails.rank).eql(2); + + const edgeRequest = networkLogger.edgeEndpointLogs.requests; + + const propositionDisplayEvent = edgeRequest[edgeRequest.length - 2]; + const propositionDisplayEventBody = JSON.parse( + propositionDisplayEvent.request.body, + ); + + await t + .expect(propositionDisplayEventBody.events[0].xdm.eventType) + .eql("decisioning.propositionDisplay"); + await t + .expect( + // eslint-disable-next-line no-underscore-dangle + propositionDisplayEventBody.events[0].xdm._experience.decisioning + .propositions[0].id, + ) + .eql("id1"); + + const propositionSuppressDisplayEvent = edgeRequest[edgeRequest.length - 1]; + const propositionSuppressDisplayEventBody = JSON.parse( + propositionSuppressDisplayEvent.request.body, + ); + + await t + .expect(propositionSuppressDisplayEventBody.events[0].xdm.eventType) + .eql("decisioning.propositionSuppressDisplay"); + await t + .expect( + // eslint-disable-next-line no-underscore-dangle + propositionSuppressDisplayEventBody.events[0].xdm._experience.decisioning + .propositions[0].id, + ) + .eql("id2"); + + // Check there is only one iframe in the DOM. + const iframesContent = await ClientFunction(() => { + const iframes = document.querySelectorAll("#alloy-content-iframe"); + const r = []; + iframes.forEach((i) => { + r.push(i.contentDocument.body.textContent); + }); + return r; + })(); + await t.expect(iframesContent.length).eql(1); + await t.expect(iframesContent[0]).eql("Proposition 1"); +}); From f647640579429605139c94aad293f4c793aa4aa4 Mon Sep 17 00:00:00 2001 From: Serban Stancu Date: Wed, 9 Oct 2024 17:39:41 -0600 Subject: [PATCH 12/14] Rename file. --- .../{conflictResolutions.js => conflictResolution.js} | 0 .../{conflictResolutions.js => conflictResolution.js} | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename test/functional/fixtures/Personalization/{conflictResolutions.js => conflictResolution.js} (100%) rename test/functional/specs/Personalization/{conflictResolutions.js => conflictResolution.js} (99%) diff --git a/test/functional/fixtures/Personalization/conflictResolutions.js b/test/functional/fixtures/Personalization/conflictResolution.js similarity index 100% rename from test/functional/fixtures/Personalization/conflictResolutions.js rename to test/functional/fixtures/Personalization/conflictResolution.js diff --git a/test/functional/specs/Personalization/conflictResolutions.js b/test/functional/specs/Personalization/conflictResolution.js similarity index 99% rename from test/functional/specs/Personalization/conflictResolutions.js rename to test/functional/specs/Personalization/conflictResolution.js index d45b19d0e..fe24c4357 100644 --- a/test/functional/specs/Personalization/conflictResolutions.js +++ b/test/functional/specs/Personalization/conflictResolution.js @@ -12,7 +12,7 @@ governing permissions and limitations under the License. import { ClientFunction, RequestMock, t } from "testcafe"; import createNetworkLogger from "../../helpers/networkLogger/index.js"; import createFixture from "../../helpers/createFixture/index.js"; -import { inAppMessagesPropositions } from "../../fixtures/Personalization/conflictResolutions.js"; +import { inAppMessagesPropositions } from "../../fixtures/Personalization/conflictResolution.js"; import createAlloyProxy from "../../helpers/createAlloyProxy.js"; import getBaseConfig from "../../helpers/getBaseConfig.js"; import { From 3eb772b72c84834a8654dd3f2f9bf273251ed884 Mon Sep 17 00:00:00 2001 From: Serban Stancu Date: Thu, 10 Oct 2024 13:00:41 -0600 Subject: [PATCH 13/14] Add comment. --- src/components/Personalization/createOnDecisionHandler.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/Personalization/createOnDecisionHandler.js b/src/components/Personalization/createOnDecisionHandler.js index 6a4a204d1..941260d3d 100644 --- a/src/components/Personalization/createOnDecisionHandler.js +++ b/src/components/Personalization/createOnDecisionHandler.js @@ -12,6 +12,10 @@ governing permissions and limitations under the License. import { MESSAGE_IN_APP } from "../../constants/schema.js"; +// When multiple In-App messages propositions are returned, we need to show only one +// of them (the one with lowest rank). This function keep track of the number of +// times it was called. It returns false for the first proposition that contains +// In-App messages items, true afterwards. const createShouldSuppressDisplay = () => { let count = 0; return (proposition) => { From d537f6f4a980089b67520db483edf310aa65f359 Mon Sep 17 00:00:00 2001 From: Serban Stancu Date: Mon, 28 Oct 2024 13:42:24 -0600 Subject: [PATCH 14/14] Use isNonEmptyArray utility. --- .../Personalization/createNotificationHandler.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/components/Personalization/createNotificationHandler.js b/src/components/Personalization/createNotificationHandler.js index cd5a6837d..c8f65333e 100644 --- a/src/components/Personalization/createNotificationHandler.js +++ b/src/components/Personalization/createNotificationHandler.js @@ -11,6 +11,7 @@ governing permissions and limitations under the License. */ import { defer } from "../../utils/index.js"; import { SUPPRESS } from "../../constants/eventType.js"; +import isNonEmptyArray from "../../utils/isNonEmptyArray.js"; export default (collect, renderedPropositions) => { return (isRenderDecisions, isSendDisplayEvent, viewName) => { @@ -26,20 +27,14 @@ export default (collect, renderedPropositions) => { } return (decisionsMetaDisplay = [], decisionsMetaSuppressed = []) => { - if ( - Array.isArray(decisionsMetaDisplay) && - decisionsMetaDisplay.length > 0 - ) { + if (isNonEmptyArray(decisionsMetaDisplay)) { collect({ decisionsMeta: decisionsMetaDisplay, viewName, }); } - if ( - Array.isArray(decisionsMetaSuppressed) && - decisionsMetaSuppressed.length > 0 - ) { + if (isNonEmptyArray(decisionsMetaSuppressed)) { collect({ decisionsMeta: decisionsMetaSuppressed, eventType: SUPPRESS,