From 936ed3203b6969c50f6ce4b63da6ce3f2d6fa5b1 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Mon, 10 Apr 2023 17:19:56 -0600 Subject: [PATCH 01/66] messaging action modules --- .../Personalization/constants/schema.js | 2 + .../createApplyPropositions.js | 12 ++- .../Personalization/createExecuteDecisions.js | 7 +- .../Personalization/createModules.js | 10 ++ .../Personalization/createModulesProvider.js | 21 ++++ .../createPersonalizationDetails.js | 4 +- .../Personalization/createPreprocessors.js | 9 ++ .../Personalization/dom-actions/index.js | 1 - .../{dom-actions => }/executeActions.js | 28 +++-- .../Personalization/groupDecisions.js | 4 +- .../actions/displayBanner.js | 40 +++++++ .../actions/displayModal.js | 102 ++++++++++++++++++ .../initMessagingActionsModules.js | 11 ++ .../in-app-message-actions/utils.js | 21 ++++ src/components/Personalization/index.js | 16 ++- test/functional/specs/Migration/helper.js | 1 + test/functional/specs/Visitor/C35448.js | 1 + .../createExecuteDecisions.spec.js | 25 +++-- .../Personalization/createModules.spec.js | 43 ++++++++ .../createModulesProvider.spec.js | 91 ++++++++++++++++ .../createPersonalizationDetails.spec.js | 14 ++- .../createPreprocessors.spec.js | 0 .../{dom-actions => }/executeActions.spec.js | 56 +++++++--- .../actions/displayBanner.spec.js | 1 + .../actions/displayModal.spec.js | 1 + .../initMessagingActionsModules.spec.js | 32 ++++++ .../in-app-message-actions/utils.spec.js | 1 + 27 files changed, 501 insertions(+), 53 deletions(-) create mode 100644 src/components/Personalization/createModules.js create mode 100644 src/components/Personalization/createModulesProvider.js create mode 100644 src/components/Personalization/createPreprocessors.js rename src/components/Personalization/{dom-actions => }/executeActions.js (75%) create mode 100644 src/components/Personalization/in-app-message-actions/actions/displayBanner.js create mode 100644 src/components/Personalization/in-app-message-actions/actions/displayModal.js create mode 100644 src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js create mode 100644 src/components/Personalization/in-app-message-actions/utils.js create mode 100644 test/unit/specs/components/Personalization/createModules.spec.js create mode 100644 test/unit/specs/components/Personalization/createModulesProvider.spec.js create mode 100644 test/unit/specs/components/Personalization/createPreprocessors.spec.js rename test/unit/specs/components/Personalization/{dom-actions => }/executeActions.spec.js (77%) create mode 100644 test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js create mode 100644 test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js create mode 100644 test/unit/specs/components/Personalization/in-app-message-actions/initMessagingActionsModules.spec.js create mode 100644 test/unit/specs/components/Personalization/in-app-message-actions/utils.spec.js diff --git a/src/components/Personalization/constants/schema.js b/src/components/Personalization/constants/schema.js index 223e625fa..4e59afdaf 100644 --- a/src/components/Personalization/constants/schema.js +++ b/src/components/Personalization/constants/schema.js @@ -19,5 +19,7 @@ export const JSON_CONTENT_ITEM = "https://ns.adobe.com/personalization/json-content-item"; export const REDIRECT_ITEM = "https://ns.adobe.com/personalization/redirect-item"; +export const IN_APP_MESSAGE = + "https://ns.adobe.com/personalization/in-app-message"; export const MEASUREMENT_SCHEMA = "https://ns.adobe.com/personalization/measurement"; diff --git a/src/components/Personalization/createApplyPropositions.js b/src/components/Personalization/createApplyPropositions.js index dfc840a6d..8657b55d8 100644 --- a/src/components/Personalization/createApplyPropositions.js +++ b/src/components/Personalization/createApplyPropositions.js @@ -12,11 +12,19 @@ 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 { + DOM_ACTION, + HTML_CONTENT_ITEM, + IN_APP_MESSAGE +} from "./constants/schema"; import PAGE_WIDE_SCOPE from "../../constants/pageWideScope"; import { EMPTY_PROPOSITIONS } from "./validateApplyPropositionsOptions"; -export const SUPPORTED_SCHEMAS = [DOM_ACTION, HTML_CONTENT_ITEM]; +export const SUPPORTED_SCHEMAS = [ + DOM_ACTION, + HTML_CONTENT_ITEM, + IN_APP_MESSAGE +]; export default ({ executeDecisions }) => { const filterItemsPredicate = item => diff --git a/src/components/Personalization/createExecuteDecisions.js b/src/components/Personalization/createExecuteDecisions.js index fff2ee8f2..02caa436a 100644 --- a/src/components/Personalization/createExecuteDecisions.js +++ b/src/components/Personalization/createExecuteDecisions.js @@ -33,7 +33,8 @@ const buildActions = decision => { return decision.items.map(item => assign({ type: DEFAULT_ACTION_TYPE }, item.data, { - meta: getItemMeta(item, decisionMeta) + schema: item.schema, + meta: getItemMeta(item, { ...decisionMeta }) }) ); }; @@ -66,12 +67,12 @@ const processMetas = (logger, actionResults) => { return finalMetas; }; -export default ({ modules, logger, executeActions }) => { +export default ({ modulesProvider, logger, executeActions }) => { return decisions => { const actionResultsPromises = decisions.map(decision => { const actions = buildActions(decision); - return executeActions(actions, modules, logger); + return executeActions(actions, modulesProvider, logger); }); return Promise.all(actionResultsPromises) .then(results => processMetas(logger, results)) diff --git a/src/components/Personalization/createModules.js b/src/components/Personalization/createModules.js new file mode 100644 index 000000000..a4548d7b9 --- /dev/null +++ b/src/components/Personalization/createModules.js @@ -0,0 +1,10 @@ +import { DOM_ACTION, IN_APP_MESSAGE } from "./constants/schema"; +import { initDomActionsModules } from "./dom-actions"; +import initMessagingActionsModules from "./in-app-message-actions/initMessagingActionsModules"; + +export default storeClickMetrics => { + return { + [DOM_ACTION]: initDomActionsModules(storeClickMetrics), + [IN_APP_MESSAGE]: initMessagingActionsModules(storeClickMetrics) + }; +}; diff --git a/src/components/Personalization/createModulesProvider.js b/src/components/Personalization/createModulesProvider.js new file mode 100644 index 000000000..d9fe825ff --- /dev/null +++ b/src/components/Personalization/createModulesProvider.js @@ -0,0 +1,21 @@ +import createPreprocessors from "./createPreprocessors"; + +export default ({ modules, preprocessors = createPreprocessors() }) => { + const getModules = schema => { + return { + getAction: type => { + if (!modules[schema]) { + return undefined; + } + + return modules[schema][type]; + }, + getPreprocessors: () => preprocessors[schema], + getSchema: () => schema + }; + }; + + return { + getModules + }; +}; diff --git a/src/components/Personalization/createPersonalizationDetails.js b/src/components/Personalization/createPersonalizationDetails.js index c62402bff..952d22f5a 100644 --- a/src/components/Personalization/createPersonalizationDetails.js +++ b/src/components/Personalization/createPersonalizationDetails.js @@ -17,6 +17,7 @@ import { DEFAULT_CONTENT_ITEM, DOM_ACTION, HTML_CONTENT_ITEM, + IN_APP_MESSAGE, JSON_CONTENT_ITEM, REDIRECT_ITEM } from "./constants/schema"; @@ -86,7 +87,8 @@ export default ({ DEFAULT_CONTENT_ITEM, HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, - REDIRECT_ITEM + REDIRECT_ITEM, + IN_APP_MESSAGE ]; if (includes(scopes, PAGE_WIDE_SCOPE)) { diff --git a/src/components/Personalization/createPreprocessors.js b/src/components/Personalization/createPreprocessors.js new file mode 100644 index 000000000..18ce13e00 --- /dev/null +++ b/src/components/Personalization/createPreprocessors.js @@ -0,0 +1,9 @@ +import { DOM_ACTION } from "./constants/schema"; +import remapHeadOffers from "./dom-actions/remapHeadOffers"; +import remapCustomCodeOffers from "./dom-actions/remapCustomCodeOffers"; + +export default () => { + return { + [DOM_ACTION]: [remapHeadOffers, remapCustomCodeOffers] + }; +}; diff --git a/src/components/Personalization/dom-actions/index.js b/src/components/Personalization/dom-actions/index.js index 763784428..59f99e605 100644 --- a/src/components/Personalization/dom-actions/index.js +++ b/src/components/Personalization/dom-actions/index.js @@ -11,4 +11,3 @@ governing permissions and limitations under the License. */ export { default as initDomActionsModules } from "./initDomActionsModules"; -export { default as executeActions } from "./executeActions"; diff --git a/src/components/Personalization/dom-actions/executeActions.js b/src/components/Personalization/executeActions.js similarity index 75% rename from src/components/Personalization/dom-actions/executeActions.js rename to src/components/Personalization/executeActions.js index 3c7a3a346..3d512c6fd 100644 --- a/src/components/Personalization/dom-actions/executeActions.js +++ b/src/components/Personalization/executeActions.js @@ -10,9 +10,7 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import remapHeadOffers from "./remapHeadOffers"; -import { assign } from "../../../utils"; -import remapCustomCodeOffers from "./remapCustomCodeOffers"; +import { assign } from "../../utils"; const logActionError = (logger, action, error) => { if (logger.enabled) { @@ -35,27 +33,35 @@ const logActionCompleted = (logger, action) => { }; const executeAction = (logger, modules, type, args) => { - const execute = modules[type]; + const execute = modules.getAction(type); if (!execute) { - const error = new Error(`DOM action "${type}" not found`); + const error = new Error( + `Action "${type}" not found for schema ${modules.getSchema()}` + ); logActionError(logger, args[0], error); throw error; } return execute(...args); }; -const PREPROCESSORS = [remapHeadOffers, remapCustomCodeOffers]; +const preprocess = (preprocessors, action) => { + if (!(preprocessors instanceof Array) || preprocessors.length === 0) { + return action; + } -const preprocess = action => - PREPROCESSORS.reduce( + return preprocessors.reduce( (processed, fn) => assign(processed, fn(processed)), action ); - -export default (actions, modules, logger) => { +}; +export default (actions, modulesProvider, logger) => { const actionPromises = actions.map(action => { - const processedAction = preprocess(action); + const { schema } = action; + + const modules = modulesProvider.getModules(schema); + + const processedAction = preprocess(modules.getPreprocessors(), action); const { type } = processedAction; return executeAction(logger, modules, type, [processedAction]) diff --git a/src/components/Personalization/groupDecisions.js b/src/components/Personalization/groupDecisions.js index 73528dbe4..17485f649 100644 --- a/src/components/Personalization/groupDecisions.js +++ b/src/components/Personalization/groupDecisions.js @@ -16,7 +16,8 @@ import { DOM_ACTION, REDIRECT_ITEM, DEFAULT_CONTENT_ITEM, - MEASUREMENT_SCHEMA + MEASUREMENT_SCHEMA, + IN_APP_MESSAGE } from "./constants/schema"; import { VIEW_SCOPE_TYPE } from "./constants/scopeType"; @@ -124,6 +125,7 @@ const groupDecisions = unprocessedDecisions => { const decisionsGroupedByRenderableSchemas = splitDecisions( mergedMetricDecisions.unmatchedDecisions, DOM_ACTION, + IN_APP_MESSAGE, DEFAULT_CONTENT_ITEM ); // group renderable decisions by scope diff --git a/src/components/Personalization/in-app-message-actions/actions/displayBanner.js b/src/components/Personalization/in-app-message-actions/actions/displayBanner.js new file mode 100644 index 000000000..30a2a7fdc --- /dev/null +++ b/src/components/Personalization/in-app-message-actions/actions/displayBanner.js @@ -0,0 +1,40 @@ +import { addStyle, removeElements } from "../utils"; + +const STYLE_TAG_ID = "alloy-messaging-banner-styles"; +const BANNER_CSS_CLASSNAME = "alloy-banner"; + +const showBanner = ({ background, content }) => { + removeElements(BANNER_CSS_CLASSNAME); + + addStyle( + STYLE_TAG_ID, + `.alloy-banner { + display: flex; + justify-content: center; + padding: 10px; + background: ${background}; + } + .alloy-banner-content { + }` + ); + + const banner = document.createElement("div"); + banner.className = BANNER_CSS_CLASSNAME; + + const bannerContent = document.createElement("div"); + bannerContent.className = "alloy-banner-content"; + bannerContent.innerHTML = content; + banner.appendChild(bannerContent); + + document.body.prepend(banner); +}; + +export default settings => { + return new Promise(resolve => { + const { meta } = settings; + + showBanner(settings); + + resolve({ meta }); + }); +}; diff --git a/src/components/Personalization/in-app-message-actions/actions/displayModal.js b/src/components/Personalization/in-app-message-actions/actions/displayModal.js new file mode 100644 index 000000000..25f427125 --- /dev/null +++ b/src/components/Personalization/in-app-message-actions/actions/displayModal.js @@ -0,0 +1,102 @@ +import { addStyle, removeElements } from "../utils"; + +const STYLE_TAG_ID = "alloy-messaging-modal-styles"; +const MODAL_CSS_CLASSNAME = "alloy-modal"; + +const closeModal = () => { + removeElements(MODAL_CSS_CLASSNAME); +}; +const showModal = ({ buttons = [], content }) => { + removeElements(MODAL_CSS_CLASSNAME); + + addStyle( + STYLE_TAG_ID, + `.alloy-modal { + display: none; + width: 100%; + height: 100%; + position: fixed; + left: 0; + top: 0; + background: rgba(0, 0, 0, 0.3); + z-index: 999; + } + .alloy-modal--show { display: flex; } + .alloy-align-center { + justify-content: center; + } + .alloy-align-vertical { + align-items: center; + } + .alloy-modal-container { + position: relative; + width: 100%; + max-width: 600px; + max-height: 800px; + padding: 20px; + margin: 12px; + background: #fff; + } + .alloy-modal-content { + margin: 15px 0; + vertical-align: top; + text-align: left; + } + .alloy-modal-close--x { + font-size: 30px; + position: absolute; + top: 3px; + right: 10px; + } + .alloy-modal-close--x:hover { + cursor: pointer; + } + .alloy-modal-buttons button { + margin-right: 5px; + }` + ); + + const modal = document.createElement("div"); + modal.className = `${MODAL_CSS_CLASSNAME} alloy-align-center alloy-align-vertical alloy-modal--show`; + + const modalContainer = document.createElement("div"); + modalContainer.className = "alloy-modal-container"; + + const closeButton = document.createElement("a"); + closeButton.className = "alloy-modal-close alloy-modal-close--x"; + closeButton.innerText = "✕"; + closeButton.addEventListener("click", closeModal); + closeButton.setAttribute("aria-hidden", "true"); + + const modalContent = document.createElement("div"); + modalContent.className = "alloy-modal-content"; + modalContent.innerHTML = content; + + const modalButtons = document.createElement("div"); + modalButtons.className = "alloy-modal-buttons"; + + buttons.forEach(buttonDetails => { + const button = document.createElement("button"); + button.className = "alloy_modal_button alloy-modal-close"; + button.innerText = buttonDetails.title; + button.addEventListener("click", closeModal); + modalButtons.appendChild(button); + }); + + modalContainer.appendChild(closeButton); + modalContainer.appendChild(modalContent); + modalContainer.appendChild(modalButtons); + + modal.appendChild(modalContainer); + document.body.append(modal); +}; + +export default settings => { + return new Promise(resolve => { + const { meta } = settings; + + showModal(settings); + + resolve({ meta }); + }); +}; diff --git a/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js b/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js new file mode 100644 index 000000000..4beea254f --- /dev/null +++ b/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js @@ -0,0 +1,11 @@ +/* eslint-disable no-unused-vars */ + +import displayModal from "./actions/displayModal"; +import displayBanner from "./actions/displayBanner"; + +export default store => { + return { + modal: settings => displayModal(settings), + banner: settings => displayBanner(settings) + }; +}; diff --git a/src/components/Personalization/in-app-message-actions/utils.js b/src/components/Personalization/in-app-message-actions/utils.js new file mode 100644 index 000000000..24b92fb69 --- /dev/null +++ b/src/components/Personalization/in-app-message-actions/utils.js @@ -0,0 +1,21 @@ +export const addStyle = (styleTagId, cssText) => { + const existingStyle = document.getElementById(styleTagId); + if (existingStyle) { + existingStyle.remove(); + } + + const styles = document.createElement("style"); + styles.id = styleTagId; + + styles.appendChild(document.createTextNode(cssText)); + document.head.appendChild(styles); +}; + +export const removeElements = cssClassName => { + [...document.getElementsByClassName(cssClassName)].forEach(element => { + if (!element) { + return; + } + element.remove(); + }); +}; diff --git a/src/components/Personalization/index.js b/src/components/Personalization/index.js index cd4bb2549..afda29bbe 100644 --- a/src/components/Personalization/index.js +++ b/src/components/Personalization/index.js @@ -10,9 +10,8 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import { string, boolean } from "../../utils/validation"; +import { boolean, string } from "../../utils/validation"; import createComponent from "./createComponent"; -import { initDomActionsModules, executeActions } from "./dom-actions"; import createCollect from "./createCollect"; import createExecuteDecisions from "./createExecuteDecisions"; import { hideContainers, showContainers } from "./flicker"; @@ -32,6 +31,10 @@ import createNonRenderingHandler from "./createNonRenderingHandler"; import createApplyPropositions from "./createApplyPropositions"; import createGetPageLocation from "./createGetPageLocation"; import createSetTargetMigration from "./createSetTargetMigration"; +import createModulesProvider from "./createModulesProvider"; +import executeActions from "./executeActions"; +import createModules from "./createModules"; +import createPreprocessors from "./createPreprocessors"; const createPersonalization = ({ config, logger, eventManager }) => { const { targetMigrationEnabled, prehidingStyle } = config; @@ -44,9 +47,14 @@ const createPersonalization = ({ config, logger, eventManager }) => { } = createClickStorage(); const getPageLocation = createGetPageLocation({ window }); const viewCache = createViewCacheManager(); - const modules = initDomActionsModules(storeClickMetrics); + + const modulesProvider = createModulesProvider({ + modules: createModules(storeClickMetrics), + preprocessors: createPreprocessors() + }); + const executeDecisions = createExecuteDecisions({ - modules, + modulesProvider, logger, executeActions }); diff --git a/test/functional/specs/Migration/helper.js b/test/functional/specs/Migration/helper.js index 72ea49b01..17a5d7c8a 100644 --- a/test/functional/specs/Migration/helper.js +++ b/test/functional/specs/Migration/helper.js @@ -79,6 +79,7 @@ export const fetchMboxOffer = ClientFunction( success(response) { return response; }, + // eslint-disable-next-line no-console error: console.error }); } diff --git a/test/functional/specs/Visitor/C35448.js b/test/functional/specs/Visitor/C35448.js index 7c8e93b20..5d0accd58 100644 --- a/test/functional/specs/Visitor/C35448.js +++ b/test/functional/specs/Visitor/C35448.js @@ -31,6 +31,7 @@ const visitorReady = ClientFunction(() => { }); const injectVisitor = ClientFunction( () => { + // eslint-disable-next-line no-console console.log(REMOTE_VISITOR_LIBRARY_URL); const s = document.createElement("script"); s.src = REMOTE_VISITOR_LIBRARY_URL; diff --git a/test/unit/specs/components/Personalization/createExecuteDecisions.spec.js b/test/unit/specs/components/Personalization/createExecuteDecisions.spec.js index 31759f179..53c95c8a3 100644 --- a/test/unit/specs/components/Personalization/createExecuteDecisions.spec.js +++ b/test/unit/specs/components/Personalization/createExecuteDecisions.spec.js @@ -11,11 +11,12 @@ governing permissions and limitations under the License. */ import createExecuteDecisions from "../../../../../src/components/Personalization/createExecuteDecisions"; +import { DOM_ACTION } from "../../../../../src/components/Personalization/constants/schema"; +import createModulesProvider from "../../../../../src/components/Personalization/createModulesProvider"; describe("Personalization::createExecuteDecisions", () => { let logger; let executeActions; - let collect; const decisions = [ { @@ -56,6 +57,7 @@ describe("Personalization::createExecuteDecisions", () => { const expectedAction = [ { type: "setHtml", + schema: DOM_ACTION, selector: "#foo", content: "
Hola Mundo
", meta: { @@ -79,13 +81,17 @@ describe("Personalization::createExecuteDecisions", () => { scopeDetails: decisions[1].scopeDetails } ]; - const modules = { - foo() {} - }; + + const modulesProvider = createModulesProvider({ + modules: { + [DOM_ACTION]: { + foo() {} + } + } + }); beforeEach(() => { logger = jasmine.createSpyObj("logger", ["info", "warn", "error"]); - collect = jasmine.createSpy(); executeActions = jasmine.createSpy(); }); @@ -95,14 +101,14 @@ describe("Personalization::createExecuteDecisions", () => { [{ meta: metas[1], error: "could not render this item" }] ); const executeDecisions = createExecuteDecisions({ - modules, + modulesProvider, logger, executeActions }); return executeDecisions(decisions).then(() => { expect(executeActions).toHaveBeenCalledWith( expectedAction, - modules, + modulesProvider, logger ); expect(logger.warn).toHaveBeenCalledWith({ @@ -115,10 +121,9 @@ describe("Personalization::createExecuteDecisions", () => { it("shouldn't trigger executeActions when provided with empty array of actions", () => { executeActions.and.callThrough(); const executeDecisions = createExecuteDecisions({ - modules, + modulesProvider, logger, - executeActions, - collect + executeActions }); return executeDecisions([]).then(() => { expect(executeActions).not.toHaveBeenCalled(); diff --git a/test/unit/specs/components/Personalization/createModules.spec.js b/test/unit/specs/components/Personalization/createModules.spec.js new file mode 100644 index 000000000..9bde44fbb --- /dev/null +++ b/test/unit/specs/components/Personalization/createModules.spec.js @@ -0,0 +1,43 @@ +import createModules from "../../../../../src/components/Personalization/createModules"; +import { + DOM_ACTION, + IN_APP_MESSAGE +} from "../../../../../src/components/Personalization/constants/schema"; + +describe("createModules", () => { + it("has dom-action modules", () => { + const modules = createModules(() => undefined); + + expect(modules[DOM_ACTION]).toEqual({ + setHtml: jasmine.any(Function), + customCode: jasmine.any(Function), + setText: jasmine.any(Function), + setAttribute: jasmine.any(Function), + setImageSource: jasmine.any(Function), + setStyle: jasmine.any(Function), + move: jasmine.any(Function), + resize: jasmine.any(Function), + rearrange: jasmine.any(Function), + remove: jasmine.any(Function), + insertAfter: jasmine.any(Function), + insertBefore: jasmine.any(Function), + replaceHtml: jasmine.any(Function), + prependHtml: jasmine.any(Function), + appendHtml: jasmine.any(Function), + click: jasmine.any(Function), + defaultContent: jasmine.any(Function) + }); + + expect(Object.keys(modules[DOM_ACTION]).length).toEqual(17); + }); + it("has in-app-message modules", () => { + const modules = createModules(() => undefined); + + expect(modules[IN_APP_MESSAGE]).toEqual({ + modal: jasmine.any(Function), + banner: jasmine.any(Function) + }); + + expect(Object.keys(modules[IN_APP_MESSAGE]).length).toEqual(2); + }); +}); diff --git a/test/unit/specs/components/Personalization/createModulesProvider.spec.js b/test/unit/specs/components/Personalization/createModulesProvider.spec.js new file mode 100644 index 000000000..33f641e88 --- /dev/null +++ b/test/unit/specs/components/Personalization/createModulesProvider.spec.js @@ -0,0 +1,91 @@ +import { DOM_ACTION } from "@adobe/alloy/libEs5/components/Personalization/constants/schema"; +import createModulesProvider from "../../../../../src/components/Personalization/createModulesProvider"; +import createPreprocessors from "../../../../../src/components/Personalization/createPreprocessors"; + +describe("createModulesProvider", () => { + let modulesProvider; + beforeEach(() => { + modulesProvider = createModulesProvider({ + modules: { + something: { + eat: () => undefined, + sleep: () => undefined, + exercise: () => undefined + }, + superfluous: { + bend: () => undefined, + crease: () => undefined, + fold: () => undefined + } + }, + preprocessors: { + something: [() => undefined], + superfluous: [() => undefined, () => undefined, () => undefined] + } + }); + }); + + it("has schema property", () => { + expect(modulesProvider.getModules("something").getSchema()).toEqual( + "something" + ); + }); + + it("has 'something' module actions", () => { + expect(modulesProvider.getModules("something").getAction("eat")).toEqual( + jasmine.any(Function) + ); + expect(modulesProvider.getModules("something").getAction("sleep")).toEqual( + jasmine.any(Function) + ); + expect( + modulesProvider.getModules("something").getAction("exercise") + ).toEqual(jasmine.any(Function)); + }); + + it("has 'superfluous' module actions", () => { + expect(modulesProvider.getModules("superfluous").getAction("bend")).toEqual( + jasmine.any(Function) + ); + expect( + modulesProvider.getModules("superfluous").getAction("crease") + ).toEqual(jasmine.any(Function)); + expect(modulesProvider.getModules("superfluous").getAction("fold")).toEqual( + jasmine.any(Function) + ); + }); + it("does not have 'moo' module actions", () => { + expect(modulesProvider.getModules("moo").getAction("a")).not.toBeDefined(); + }); + + it("has 'something' preprocessors", () => { + expect(modulesProvider.getModules("something").getPreprocessors()).toEqual([ + jasmine.any(Function) + ]); + }); + it("has 'superfluous' preprocessors", () => { + expect( + modulesProvider.getModules("superfluous").getPreprocessors() + ).toEqual([ + jasmine.any(Function), + jasmine.any(Function), + jasmine.any(Function) + ]); + }); + + it("has default preprocessors if none specified", () => { + const provider = createModulesProvider({ + modules: { + something: { + eat: () => undefined, + sleep: () => undefined, + exercise: () => undefined + } + } + }); + + expect(provider.getModules(DOM_ACTION).getPreprocessors()).toEqual( + createPreprocessors()[DOM_ACTION] + ); + }); +}); diff --git a/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js b/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js index 6ebe0fb65..be2b41557 100644 --- a/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js +++ b/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js @@ -16,6 +16,7 @@ import { DEFAULT_CONTENT_ITEM, DOM_ACTION, HTML_CONTENT_ITEM, + IN_APP_MESSAGE, JSON_CONTENT_ITEM, REDIRECT_ITEM } from "../../../../../src/components/Personalization/constants/schema"; @@ -62,6 +63,7 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, + IN_APP_MESSAGE, DOM_ACTION ], decisionScopes: expectedDecisionScopes, @@ -100,6 +102,7 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, + IN_APP_MESSAGE, DOM_ACTION ], decisionScopes: expectedDecisionScopes, @@ -138,6 +141,7 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, + IN_APP_MESSAGE, DOM_ACTION ], decisionScopes: expectedDecisionScopes, @@ -175,7 +179,8 @@ describe("Personalization::createPersonalizationDetails", () => { DEFAULT_CONTENT_ITEM, HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, - REDIRECT_ITEM + REDIRECT_ITEM, + IN_APP_MESSAGE ], decisionScopes: expectedDecisionScopes, surfaces: [] @@ -214,7 +219,8 @@ describe("Personalization::createPersonalizationDetails", () => { DEFAULT_CONTENT_ITEM, HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, - REDIRECT_ITEM + REDIRECT_ITEM, + IN_APP_MESSAGE ], decisionScopes: expectedDecisionScopes, surfaces: ["web://test1.com/"] @@ -255,7 +261,8 @@ describe("Personalization::createPersonalizationDetails", () => { DEFAULT_CONTENT_ITEM, HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, - REDIRECT_ITEM + REDIRECT_ITEM, + IN_APP_MESSAGE ], decisionScopes: expectedDecisionScopes, surfaces: ["web://test1.com/"] @@ -391,6 +398,7 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, + IN_APP_MESSAGE, DOM_ACTION ], decisionScopes: expectedDecisionScopes, diff --git a/test/unit/specs/components/Personalization/createPreprocessors.spec.js b/test/unit/specs/components/Personalization/createPreprocessors.spec.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/specs/components/Personalization/dom-actions/executeActions.spec.js b/test/unit/specs/components/Personalization/executeActions.spec.js similarity index 77% rename from test/unit/specs/components/Personalization/dom-actions/executeActions.spec.js rename to test/unit/specs/components/Personalization/executeActions.spec.js index 029dcaba3..16bcb075f 100644 --- a/test/unit/specs/components/Personalization/dom-actions/executeActions.spec.js +++ b/test/unit/specs/components/Personalization/executeActions.spec.js @@ -10,19 +10,28 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import executeActions from "../../../../../../src/components/Personalization/dom-actions/executeActions"; +import { DOM_ACTION } from "@adobe/alloy/libEs5/components/Personalization/constants/schema"; +import executeActions from "../../../../../src/components/Personalization/executeActions"; +import createModulesProvider from "../../../../../src/components/Personalization/createModulesProvider"; 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 - }; + const actions = [{ type: "foo", schema: DOM_ACTION }]; - return executeActions(actions, modules, logger).then(result => { + return executeActions( + actions, + createModulesProvider({ + modules: { + [DOM_ACTION]: { + foo: actionSpy + } + } + }), + logger + ).then(result => { expect(result).toEqual([1]); expect(actionSpy).toHaveBeenCalled(); expect(logger.info.calls.count()).toEqual(1); @@ -46,23 +55,30 @@ describe("Personalization::executeActions", () => { const actions = [ { type: "setHtml", + schema: DOM_ACTION, selector: "head", content: '

Unsupported tag content

' }, { type: "customCode", + schema: DOM_ACTION, selector: "BODY > *:eq(0)", content: "
superfluous
" } ]; - const modules = { - setHtml: setHtmlActionSpy, - appendHtml: appendHtmlActionSpy, - customCode: customCodeActionSpy - }; - return executeActions(actions, modules, logger).then(result => { + const modulesProvider = createModulesProvider({ + modules: { + [DOM_ACTION]: { + setHtml: setHtmlActionSpy, + appendHtml: appendHtmlActionSpy, + customCode: customCodeActionSpy + } + } + }); + + return executeActions(actions, modulesProvider, logger).then(result => { expect(result).toEqual([2, 9]); expect(setHtmlActionSpy).not.toHaveBeenCalled(); expect(appendHtmlActionSpy).toHaveBeenCalledOnceWith( @@ -89,11 +105,17 @@ describe("Personalization::executeActions", () => { 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 => { + const actions = [{ type: "foo", schema: DOM_ACTION }]; + + const modulesProvider = createModulesProvider({ + modules: { + [DOM_ACTION]: { + foo: actionSpy + } + } + }); + + return executeActions(actions, modulesProvider, logger).then(result => { expect(result).toEqual([1]); expect(actionSpy).toHaveBeenCalled(); expect(logger.info.calls.count()).toEqual(0); diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js new file mode 100644 index 000000000..70b786d12 --- /dev/null +++ b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js @@ -0,0 +1 @@ +// TODO diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js new file mode 100644 index 000000000..70b786d12 --- /dev/null +++ b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js @@ -0,0 +1 @@ +// TODO diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/initMessagingActionsModules.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/initMessagingActionsModules.spec.js new file mode 100644 index 000000000..ab92659d6 --- /dev/null +++ b/test/unit/specs/components/Personalization/in-app-message-actions/initMessagingActionsModules.spec.js @@ -0,0 +1,32 @@ +/* +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 initMessagingActionsModules from "../../../../../../src/components/Personalization/in-app-message-actions/initMessagingActionsModules"; +import createModules from "../../../../../../src/components/Personalization/createModules"; +import { IN_APP_MESSAGE } from "../../../../../../src/components/Personalization/constants/schema"; + +describe("Personalization::turbine::initMessagingActionsModules", () => { + const modules = createModules(() => undefined); + const expectedModules = modules[IN_APP_MESSAGE]; + + it("should have all the required modules", () => { + const messagingActionsModules = initMessagingActionsModules(() => {}); + + expect(Object.keys(messagingActionsModules).length).toEqual( + Object.keys(expectedModules).length + ); + + Object.keys(expectedModules).forEach(key => { + expect(messagingActionsModules[key]).toEqual(jasmine.any(Function)); + }); + }); +}); diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/utils.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/utils.spec.js new file mode 100644 index 000000000..70b786d12 --- /dev/null +++ b/test/unit/specs/components/Personalization/in-app-message-actions/utils.spec.js @@ -0,0 +1 @@ +// TODO From 5a5b61b5a2ae05dcff2a7de7c7e1dc203c5afa6e Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Tue, 11 Apr 2023 10:35:20 -0600 Subject: [PATCH 02/66] banner test --- .../actions/displayBanner.js | 2 + .../actions/displayBanner.spec.js | 42 ++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/components/Personalization/in-app-message-actions/actions/displayBanner.js b/src/components/Personalization/in-app-message-actions/actions/displayBanner.js index 30a2a7fdc..95f010c08 100644 --- a/src/components/Personalization/in-app-message-actions/actions/displayBanner.js +++ b/src/components/Personalization/in-app-message-actions/actions/displayBanner.js @@ -1,6 +1,7 @@ import { addStyle, removeElements } from "../utils"; const STYLE_TAG_ID = "alloy-messaging-banner-styles"; +const ELEMENT_TAG_ID = "alloy-messaging-banner"; const BANNER_CSS_CLASSNAME = "alloy-banner"; const showBanner = ({ background, content }) => { @@ -19,6 +20,7 @@ const showBanner = ({ background, content }) => { ); const banner = document.createElement("div"); + banner.id = ELEMENT_TAG_ID; banner.className = BANNER_CSS_CLASSNAME; const bannerContent = document.createElement("div"); diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js index 70b786d12..99e71613f 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js @@ -1 +1,41 @@ -// TODO +import displayBanner from "../../../../../../../src/components/Personalization/in-app-message-actions/actions/displayBanner"; +import { createNode } from "../../../../../../../src/utils/dom"; +import { DIV } from "../../../../../../../src/constants/tagName"; + +describe("Personalization::IAM:banner", () => { + it("inserts banner into dom", async () => { + const something = createNode( + DIV, + { className: "something" }, + { + innerHTML: + "

Amet cillum consectetur elit cupidatat voluptate nisi duis et occaecat enim pariatur.

" + } + ); + + document.body.prepend(something); + + await displayBanner({ + type: "banner", + position: "top", + closeButton: false, + background: "#00a0fe", + content: + "FLASH SALE!! 50% off everything, 24 hours only!" + }); + + const banner = document.querySelector("div#alloy-messaging-banner"); + const bannerStyle = document.querySelector( + "style#alloy-messaging-banner-styles" + ); + + expect(banner).not.toBeNull(); + expect(bannerStyle).not.toBeNull(); + + expect(banner.parentNode).toEqual(document.body); + expect(bannerStyle.parentNode).toEqual(document.head); + + expect(banner.previousElementSibling).toBeNull(); + expect(banner.nextElementSibling).toEqual(something); + }); +}); From bc28a587e35143052a3d878fc756f564938aa26c Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Tue, 11 Apr 2023 10:49:01 -0600 Subject: [PATCH 03/66] modal test --- .../actions/displayModal.js | 2 + .../actions/displayModal.spec.js | 61 ++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/components/Personalization/in-app-message-actions/actions/displayModal.js b/src/components/Personalization/in-app-message-actions/actions/displayModal.js index 25f427125..d125975b1 100644 --- a/src/components/Personalization/in-app-message-actions/actions/displayModal.js +++ b/src/components/Personalization/in-app-message-actions/actions/displayModal.js @@ -1,6 +1,7 @@ import { addStyle, removeElements } from "../utils"; const STYLE_TAG_ID = "alloy-messaging-modal-styles"; +const ELEMENT_TAG_ID = "alloy-messaging-modal"; const MODAL_CSS_CLASSNAME = "alloy-modal"; const closeModal = () => { @@ -57,6 +58,7 @@ const showModal = ({ buttons = [], content }) => { ); const modal = document.createElement("div"); + modal.id = ELEMENT_TAG_ID; modal.className = `${MODAL_CSS_CLASSNAME} alloy-align-center alloy-align-vertical alloy-modal--show`; const modalContainer = document.createElement("div"); diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js index 70b786d12..e30224918 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js @@ -1 +1,60 @@ -// TODO +import { createNode } from "../../../../../../../src/utils/dom"; +import { DIV } from "../../../../../../../src/constants/tagName"; +import displayModal from "../../../../../../../src/components/Personalization/in-app-message-actions/actions/displayModal"; + +describe("Personalization::IAM:modal", () => { + it("inserts banner into dom", async () => { + const something = createNode( + DIV, + { className: "something" }, + { + innerHTML: + "

Amet cillum consectetur elit cupidatat voluptate nisi duis et occaecat enim pariatur.

" + } + ); + + document.body.append(something); + + await displayModal({ + type: "modal", + horizontalAlign: "center", + verticalAlign: "center", + closeButton: true, + dimBackground: true, + content: + "

Special offer, don't delay!

", + buttons: [ + { + title: "Yes please!" + }, + { + title: "No, thanks" + } + ] + }); + + const modal = document.querySelector("div#alloy-messaging-modal"); + const modalStyle = document.querySelector( + "style#alloy-messaging-modal-styles" + ); + + expect(modal).not.toBeNull(); + expect(modalStyle).not.toBeNull(); + + expect(modal.parentNode).toEqual(document.body); + expect(modalStyle.parentNode).toEqual(document.head); + + expect(modal.previousElementSibling).toEqual(something); + expect(modal.nextElementSibling).toBeNull(); + + expect(modal.querySelector(".alloy-modal-content").innerText).toEqual( + "Special offer, don't delay!" + ); + + const buttons = modal.querySelector(".alloy-modal-buttons"); + + expect(buttons.childElementCount).toEqual(2); + expect(buttons.children[0].innerText).toEqual("Yes please!"); + expect(buttons.children[1].innerText).toEqual("No, thanks"); + }); +}); From f19788b0b70fbdf41b10d30137188ab0c5220fa0 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Thu, 13 Apr 2023 16:49:07 -0600 Subject: [PATCH 04/66] decisioning engine component --- package-lock.json | 79 +++ package.json | 2 + src/components/DataCollector/index.js | 4 + .../DataCollector/validateApplyResponse.js | 1 + .../DataCollector/validateUserEventOptions.js | 1 + .../DecisioningEngine/createApplyResponse.js | 9 + .../createDecisionProvider.js | 32 ++ .../createEvaluableRulesetPayload.js | 38 ++ .../DecisioningEngine/createEventRegistry.js | 37 ++ .../createOnResponseHandler.js | 24 + src/components/DecisioningEngine/index.js | 50 ++ .../Personalization/constants/handle.js | 1 + .../Personalization/constants/schema.js | 3 + .../Personalization/createComponent.js | 8 + .../createOnResponseHandler.js | 7 +- src/components/Personalization/index.js | 1 + src/core/componentCreators.js | 4 +- src/core/createEvent.js | 13 + src/core/createEventManager.js | 4 + src/core/createLifecycle.js | 5 +- src/utils/flattenArray.js | 19 + src/utils/flattenObject.js | 24 + test/functional/specs/Migration/helper.js | 1 + test/functional/specs/Visitor/C35448.js | 1 - .../components/DataCollector/index.spec.js | 4 + .../createApplyResponse.spec.js | 51 ++ .../createDecisionProvider.spec.js | 452 ++++++++++++++++++ .../createEvaluableRulesetPayload.spec.js | 277 +++++++++++ .../createEventRegistry.spec.js | 122 +++++ .../createOnResponseHandler.spec.js | 342 +++++++++++++ .../specs/core/createEventManager.spec.js | 1 + test/unit/specs/utils/flattenArray.spec.js | 62 +++ test/unit/specs/utils/flattenObject.spec.js | 105 ++++ 33 files changed, 1778 insertions(+), 6 deletions(-) create mode 100644 src/components/DecisioningEngine/createApplyResponse.js create mode 100644 src/components/DecisioningEngine/createDecisionProvider.js create mode 100644 src/components/DecisioningEngine/createEvaluableRulesetPayload.js create mode 100644 src/components/DecisioningEngine/createEventRegistry.js create mode 100644 src/components/DecisioningEngine/createOnResponseHandler.js create mode 100644 src/components/DecisioningEngine/index.js create mode 100644 src/components/Personalization/constants/handle.js create mode 100644 src/utils/flattenArray.js create mode 100644 src/utils/flattenObject.js create mode 100644 test/unit/specs/components/DecisioningEngine/createApplyResponse.spec.js create mode 100644 test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js create mode 100644 test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js create mode 100644 test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js create mode 100644 test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js create mode 100644 test/unit/specs/utils/flattenArray.spec.js create mode 100644 test/unit/specs/utils/flattenObject.spec.js diff --git a/package-lock.json b/package-lock.json index 9da66983c..8591fc509 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "2.15.0", "license": "Apache-2.0", "dependencies": { + "@adobe/aep-rules-engine": "file:../aepsdk-rulesengine-javascript", "@adobe/reactor-cookie": "^1.0.0", "@adobe/reactor-load-script": "^1.1.1", "@adobe/reactor-object-assign": "^1.0.0", @@ -79,6 +80,48 @@ "yargs": "^16.2.0" } }, + "../aepsdk-rulesengine-javascript": { + "name": "@adobe/aep-rules-engine", + "version": "1.0.0", + "license": "Apache-2.0", + "devDependencies": { + "@babel/core": "^7.21.3", + "@babel/preset-env": "^7.20.2", + "@babel/preset-typescript": "^7.21.0", + "@lwc/eslint-plugin-lwc": "^1.6.2", + "@rollup/plugin-babel": "^6.0.3", + "@rollup/plugin-node-resolve": "^15.0.1", + "@rollup/plugin-replace": "^5.0.2", + "@rollup/plugin-terser": "^0.4.0", + "@types/jest": "^29.5.0", + "@typescript-eslint/eslint-plugin": "^5.57.0", + "@typescript-eslint/parser": "^5.57.0", + "babel-jest": "^29.5.0", + "eslint": "^8.36.0", + "eslint-config-airbnb-typescript": "^17.0.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jest": "^27.2.1", + "eslint-plugin-n": "^15.6.1", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-promise": "^6.1.1", + "handlebars": "^4.7.7", + "husky": "^8.0.0", + "jest": "^29.5.0", + "prettier": "^2.8.4", + "pretty-quick": "^3.1.3", + "rimraf": "^4.4.0", + "rollup": "^3.19.1", + "rollup-plugin-cleanup": "^3.2.1", + "rollup-plugin-typescript2": "^0.34.1", + "staged-git-files": "^1.3.0", + "typescript": "^5.0.2" + } + }, + "node_modules/@adobe/aep-rules-engine": { + "resolved": "../aepsdk-rulesengine-javascript", + "link": true + }, "node_modules/@adobe/alloy": { "version": "2.15.0", "resolved": "https://registry.npmjs.org/@adobe/alloy/-/alloy-2.15.0.tgz", @@ -15216,6 +15259,42 @@ } }, "dependencies": { + "@adobe/aep-rules-engine": { + "version": "file:../aepsdk-rulesengine-javascript", + "requires": { + "@babel/core": "^7.21.3", + "@babel/preset-env": "^7.20.2", + "@babel/preset-typescript": "^7.21.0", + "@lwc/eslint-plugin-lwc": "^1.6.2", + "@rollup/plugin-babel": "^6.0.3", + "@rollup/plugin-node-resolve": "^15.0.1", + "@rollup/plugin-replace": "^5.0.2", + "@rollup/plugin-terser": "^0.4.0", + "@types/jest": "^29.5.0", + "@typescript-eslint/eslint-plugin": "^5.57.0", + "@typescript-eslint/parser": "^5.57.0", + "babel-jest": "^29.5.0", + "eslint": "^8.36.0", + "eslint-config-airbnb-typescript": "^17.0.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jest": "^27.2.1", + "eslint-plugin-n": "^15.6.1", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-promise": "^6.1.1", + "handlebars": "^4.7.7", + "husky": "^8.0.0", + "jest": "^29.5.0", + "prettier": "^2.8.4", + "pretty-quick": "^3.1.3", + "rimraf": "^4.4.0", + "rollup": "^3.19.1", + "rollup-plugin-cleanup": "^3.2.1", + "rollup-plugin-typescript2": "^0.34.1", + "staged-git-files": "^1.3.0", + "typescript": "^5.0.2" + } + }, "@adobe/alloy": { "version": "2.15.0", "resolved": "https://registry.npmjs.org/@adobe/alloy/-/alloy-2.15.0.tgz", diff --git a/package.json b/package.json index 5f5167734..c089417d5 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "format": "prettier --write \"*.{html,js}\" \"{sandbox,src,test,scripts}/**/*.{html,js}\"", "test": "npm run test:unit && npm run test:scripts && npm run test:functional", "test:unit": "karma start --single-run", + "testdebug": "karma start --browsers=Chrome --single-run=false --debug", "test:unit:watch": "karma start", "test:unit:saucelabs:local": "karma start karma.saucelabs.conf.js --single-run", "test:unit:coverage": "karma start --single-run --reporters spec,coverage", @@ -56,6 +57,7 @@ } ], "dependencies": { + "@adobe/aep-rules-engine": "file:../aepsdk-rulesengine-javascript", "@adobe/reactor-cookie": "^1.0.0", "@adobe/reactor-load-script": "^1.1.1", "@adobe/reactor-object-assign": "^1.0.0", diff --git a/src/components/DataCollector/index.js b/src/components/DataCollector/index.js index 8e302c8bd..5e995b932 100644 --- a/src/components/DataCollector/index.js +++ b/src/components/DataCollector/index.js @@ -29,6 +29,7 @@ const createDataCollector = ({ eventManager }) => { type, mergeId, renderDecisions = false, + decisionContext = {}, decisionScopes = [], // Note: this option will soon be deprecated, please use personalization.decisionScopes instead personalization = {}, datasetId @@ -64,6 +65,7 @@ const createDataCollector = ({ eventManager }) => { return eventManager.sendEvent(event, { renderDecisions, + decisionContext, decisionScopes, personalization }); @@ -77,6 +79,7 @@ const createDataCollector = ({ eventManager }) => { run: options => { const { renderDecisions = false, + decisionContext = {}, responseHeaders = {}, responseBody = { handle: [] } } = options; @@ -85,6 +88,7 @@ const createDataCollector = ({ eventManager }) => { return eventManager.applyResponse(event, { renderDecisions, + decisionContext, responseHeaders, responseBody }); diff --git a/src/components/DataCollector/validateApplyResponse.js b/src/components/DataCollector/validateApplyResponse.js index 95207d37a..81e1f6ed7 100644 --- a/src/components/DataCollector/validateApplyResponse.js +++ b/src/components/DataCollector/validateApplyResponse.js @@ -10,6 +10,7 @@ import { export default ({ options }) => { const validator = objectOf({ renderDecisions: boolean(), + decisionContext: objectOf({}), responseHeaders: mapOfValues(string().required()), responseBody: objectOf({ handle: arrayOf( diff --git a/src/components/DataCollector/validateUserEventOptions.js b/src/components/DataCollector/validateUserEventOptions.js index 70ec0be4e..8f84b0586 100644 --- a/src/components/DataCollector/validateUserEventOptions.js +++ b/src/components/DataCollector/validateUserEventOptions.js @@ -27,6 +27,7 @@ export default ({ options }) => { data: objectOf({}), documentUnloading: boolean(), renderDecisions: boolean(), + decisionContext: objectOf({}), decisionScopes: arrayOf(string()).uniqueItems(), personalization: objectOf({ decisionScopes: arrayOf(string()).uniqueItems(), diff --git a/src/components/DecisioningEngine/createApplyResponse.js b/src/components/DecisioningEngine/createApplyResponse.js new file mode 100644 index 000000000..b7f18dc5e --- /dev/null +++ b/src/components/DecisioningEngine/createApplyResponse.js @@ -0,0 +1,9 @@ +export default lifecycle => { + return ({ viewName, propositions = [] }) => { + if (propositions.length > 0 && lifecycle) { + lifecycle.onDecision({ viewName, propositions }); + } + + return { propositions }; + }; +}; diff --git a/src/components/DecisioningEngine/createDecisionProvider.js b/src/components/DecisioningEngine/createDecisionProvider.js new file mode 100644 index 000000000..7b40e7fd7 --- /dev/null +++ b/src/components/DecisioningEngine/createDecisionProvider.js @@ -0,0 +1,32 @@ +import createEvaluableRulesetPayload from "./createEvaluableRulesetPayload"; + +export default () => { + const payloads = {}; + + const addPayload = payload => { + if (!payload.id) { + return; + } + + const evaluableRulesetPayload = createEvaluableRulesetPayload(payload); + + if (evaluableRulesetPayload.isEvaluable) { + payloads[payload.id] = evaluableRulesetPayload; + } + }; + + const evaluate = (context = {}) => + Object.values(payloads) + .map(payload => payload.evaluate(context)) + .filter(payload => payload.items.length > 0); + + const addPayloads = personalizationPayloads => { + personalizationPayloads.forEach(addPayload); + }; + + return { + addPayload, + addPayloads, + evaluate + }; +}; diff --git a/src/components/DecisioningEngine/createEvaluableRulesetPayload.js b/src/components/DecisioningEngine/createEvaluableRulesetPayload.js new file mode 100644 index 000000000..8ec248117 --- /dev/null +++ b/src/components/DecisioningEngine/createEvaluableRulesetPayload.js @@ -0,0 +1,38 @@ +import RulesEngine from "@adobe/aep-rules-engine"; +import { JSON_RULESET_ITEM } from "../Personalization/constants/schema"; +import flattenArray from "../../utils/flattenArray"; + +export default payload => { + const items = []; + + const addItem = item => { + const { data = {} } = item; + const { content } = data; + + if (!content) { + return; + } + + items.push(RulesEngine(JSON.parse(content))); + }; + + const evaluate = context => { + return { + ...payload, + items: flattenArray(items.map(item => item.execute(context))).map( + item => item.detail + ) + }; + }; + + if (Array.isArray(payload.items)) { + payload.items + .filter(item => item.schema === JSON_RULESET_ITEM) + .forEach(addItem); + } + + return { + evaluate, + isEvaluable: items.length > 0 + }; +}; diff --git a/src/components/DecisioningEngine/createEventRegistry.js b/src/components/DecisioningEngine/createEventRegistry.js new file mode 100644 index 000000000..839f60cdf --- /dev/null +++ b/src/components/DecisioningEngine/createEventRegistry.js @@ -0,0 +1,37 @@ +export default () => { + const events = {}; + const rememberEvent = event => { + const { xdm = {} } = event.getContent(); + const { eventType = "", _experience } = xdm; + + if ( + !eventType || + !_experience || + typeof _experience !== "object" || + eventType === "" + ) { + return; + } + + const { decisioning = {} } = _experience; + const { propositions = [] } = decisioning; + + propositions.forEach(proposition => { + let count = 0; + const existingEvent = events[proposition.id]; + if (existingEvent) { + count = existingEvent.count; + } + + events[proposition.id] = { + event: { id: proposition.id, type: eventType }, + timestamp: new Date().getTime(), + count: count + 1 + }; + }); + }; + + const getEvent = eventId => events[eventId]; + + return { rememberEvent, getEvent, toJSON: () => events }; +}; diff --git a/src/components/DecisioningEngine/createOnResponseHandler.js b/src/components/DecisioningEngine/createOnResponseHandler.js new file mode 100644 index 000000000..436340210 --- /dev/null +++ b/src/components/DecisioningEngine/createOnResponseHandler.js @@ -0,0 +1,24 @@ +import { PERSONALIZATION_DECISIONS_HANDLE } from "../Personalization/constants/handle"; +import flattenObject from "../../utils/flattenObject"; + +export default ({ + decisionProvider, + applyResponse, + event, + decisionContext +}) => { + const context = { + ...flattenObject(event.getContent()), + ...decisionContext + }; + + const viewName = event.getViewName(); + + return ({ response }) => { + decisionProvider.addPayloads( + response.getPayloadsByType(PERSONALIZATION_DECISIONS_HANDLE) + ); + const propositions = decisionProvider.evaluate(context); + applyResponse({ viewName, propositions }); + }; +}; diff --git a/src/components/DecisioningEngine/index.js b/src/components/DecisioningEngine/index.js new file mode 100644 index 000000000..9af7c4bad --- /dev/null +++ b/src/components/DecisioningEngine/index.js @@ -0,0 +1,50 @@ +import { noop } from "../../utils"; +import createOnResponseHandler from "./createOnResponseHandler"; +import createDecisionProvider from "./createDecisionProvider"; +import createApplyResponse from "./createApplyResponse"; +import createEventRegistry from "./createEventRegistry"; + +const createDecisioningEngine = () => { + const eventRegistry = createEventRegistry(); + let applyResponse = createApplyResponse(); + const decisionProvider = createDecisionProvider(); + + return { + lifecycle: { + onComponentsRegistered(tools) { + applyResponse = createApplyResponse(tools.lifecycle); + }, + onBeforeEvent({ + event, + renderDecisions, + decisionContext = {}, + onResponse = noop + }) { + if (renderDecisions) { + onResponse( + createOnResponseHandler({ + decisionProvider, + applyResponse, + event, + decisionContext + }) + ); + return; + } + + eventRegistry.rememberEvent(event); + } + }, + commands: { + renderDecisions: { + run: decisionContext => + applyResponse({ + propositions: decisionProvider.evaluate(decisionContext) + }) + } + } + }; +}; + +createDecisioningEngine.namespace = "DecisioningEngine"; +export default createDecisioningEngine; diff --git a/src/components/Personalization/constants/handle.js b/src/components/Personalization/constants/handle.js new file mode 100644 index 000000000..a37beb080 --- /dev/null +++ b/src/components/Personalization/constants/handle.js @@ -0,0 +1 @@ +export const PERSONALIZATION_DECISIONS_HANDLE = "personalization:decisions"; diff --git a/src/components/Personalization/constants/schema.js b/src/components/Personalization/constants/schema.js index 223e625fa..ffebe3663 100644 --- a/src/components/Personalization/constants/schema.js +++ b/src/components/Personalization/constants/schema.js @@ -17,6 +17,9 @@ export const HTML_CONTENT_ITEM = "https://ns.adobe.com/personalization/html-content-item"; export const JSON_CONTENT_ITEM = "https://ns.adobe.com/personalization/json-content-item"; +export const JSON_RULESET_ITEM = + "https://ns.adobe.com/personalization/json-ruleset-item"; + export const REDIRECT_ITEM = "https://ns.adobe.com/personalization/redirect-item"; export const MEASUREMENT_SCHEMA = diff --git a/src/components/Personalization/createComponent.js b/src/components/Personalization/createComponent.js index 9abd6110c..d5e1f7a90 100644 --- a/src/components/Personalization/createComponent.js +++ b/src/components/Personalization/createComponent.js @@ -19,6 +19,7 @@ export default ({ getPageLocation, logger, fetchDataHandler, + autoRenderingHandler, viewChangeHandler, onClickHandler, isAuthoringModeEnabled, @@ -30,6 +31,13 @@ export default ({ }) => { return { lifecycle: { + onDecision({ viewName, propositions }) { + autoRenderingHandler({ + viewName, + pageWideScopeDecisions: propositions, + nonAutoRenderableDecisions: [] + }); + }, onBeforeRequest({ request }) { setTargetMigration(request); return Promise.resolve(); diff --git a/src/components/Personalization/createOnResponseHandler.js b/src/components/Personalization/createOnResponseHandler.js index 29901c0af..3a89480e5 100644 --- a/src/components/Personalization/createOnResponseHandler.js +++ b/src/components/Personalization/createOnResponseHandler.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. */ import isNonEmptyArray from "../../utils/isNonEmptyArray"; - -const DECISIONS_HANDLE = "personalization:decisions"; +import { PERSONALIZATION_DECISIONS_HANDLE } from "./constants/handle"; export default ({ autoRenderingHandler, @@ -21,7 +20,9 @@ export default ({ showContainers }) => { return ({ decisionsDeferred, personalizationDetails, response }) => { - const unprocessedDecisions = response.getPayloadsByType(DECISIONS_HANDLE); + const unprocessedDecisions = response.getPayloadsByType( + PERSONALIZATION_DECISIONS_HANDLE + ); const viewName = personalizationDetails.getViewName(); // if personalization payload is empty return empty decisions array diff --git a/src/components/Personalization/index.js b/src/components/Personalization/index.js index cd4bb2549..1d89aee4f 100644 --- a/src/components/Personalization/index.js +++ b/src/components/Personalization/index.js @@ -98,6 +98,7 @@ const createPersonalization = ({ config, logger, eventManager }) => { getPageLocation, logger, fetchDataHandler, + autoRenderingHandler, viewChangeHandler, onClickHandler, isAuthoringModeEnabled, diff --git a/src/core/componentCreators.js b/src/core/componentCreators.js index f86d2ce22..290bba0a7 100644 --- a/src/core/componentCreators.js +++ b/src/core/componentCreators.js @@ -21,6 +21,7 @@ import createContext from "../components/Context"; import createPrivacy from "../components/Privacy"; import createEventMerge from "../components/EventMerge"; import createLibraryInfo from "../components/LibraryInfo"; +import createDecisioningEngine from "../components/DecisioningEngine"; import createMachineLearning from "../components/MachineLearning"; // TODO: Register the Components here statically for now. They might be registered differently. @@ -35,5 +36,6 @@ export default [ createPrivacy, createEventMerge, createLibraryInfo, - createMachineLearning + createMachineLearning, + createDecisioningEngine ]; diff --git a/src/core/createEvent.js b/src/core/createEvent.js index 6bf98131e..10a0f3b69 100644 --- a/src/core/createEvent.js +++ b/src/core/createEvent.js @@ -29,6 +29,19 @@ export default () => { }; const event = { + getContent() { + const currentContent = JSON.parse(JSON.stringify(content)); + + if (userXdm) { + deepAssign(currentContent, { xdm: userXdm }); + } + + if (userData) { + deepAssign(currentContent, { data: userData }); + } + + return currentContent; + }, setUserXdm(value) { throwIfEventFinalized("setUserXdm"); userXdm = value; diff --git a/src/core/createEventManager.js b/src/core/createEventManager.js index f3d92fcf5..6ee8edba8 100644 --- a/src/core/createEventManager.js +++ b/src/core/createEventManager.js @@ -51,6 +51,7 @@ export default ({ const { renderDecisions = false, decisionScopes, + decisionContext, personalization } = options; const payload = createDataCollectionRequestPayload(); @@ -62,6 +63,7 @@ export default ({ .onBeforeEvent({ event, renderDecisions, + decisionContext, decisionScopes, personalization, onResponse: onResponseCallbackAggregator.add, @@ -109,6 +111,7 @@ export default ({ applyResponse(event, options = {}) { const { renderDecisions = false, + decisionContext = {}, responseHeaders = {}, responseBody = { handle: [] } } = options; @@ -121,6 +124,7 @@ export default ({ .onBeforeEvent({ event, renderDecisions, + decisionContext, decisionScopes: [PAGE_WIDE_SCOPE], personalization: {}, onResponse: onResponseCallbackAggregator.add, diff --git a/src/core/createLifecycle.js b/src/core/createLifecycle.js index cba59d97e..7cb520438 100644 --- a/src/core/createLifecycle.js +++ b/src/core/createLifecycle.js @@ -29,7 +29,10 @@ const hookNames = [ // { response } is passed as the parameter) "onRequestFailure", // A user clicked on an element. - "onClick" + "onClick", + // Called by DecisioningEngine when a ruleset is satisfied with a list of + // propositions + "onDecision" ]; const createHook = (componentRegistry, hookName) => { diff --git a/src/utils/flattenArray.js b/src/utils/flattenArray.js new file mode 100644 index 000000000..5e87b9dec --- /dev/null +++ b/src/utils/flattenArray.js @@ -0,0 +1,19 @@ +const flattenArray = (items = []) => { + const flat = []; + + if (!Array.isArray(items)) { + return items; + } + + items.forEach(item => { + if (Array.isArray(item)) { + flat.push(...flattenArray(item)); + } else { + flat.push(item); + } + }); + + return flat; +}; + +export default flattenArray; diff --git a/src/utils/flattenObject.js b/src/utils/flattenObject.js new file mode 100644 index 000000000..20c2c64c7 --- /dev/null +++ b/src/utils/flattenObject.js @@ -0,0 +1,24 @@ +const isPlainObject = obj => + obj !== null && + typeof obj === "object" && + Object.getPrototypeOf(obj) === Object.prototype; + +const flattenObject = (obj, result = {}, keys = []) => { + Object.keys(obj).forEach(key => { + if (isPlainObject(obj[key]) || Array.isArray(obj[key])) { + flattenObject(obj[key], result, [...keys, key]); + } else { + result[[...keys, key].join(".")] = obj[key]; + } + }); + + return result; +}; + +export default obj => { + if (!isPlainObject(obj)) { + return obj; + } + + return flattenObject(obj); +}; diff --git a/test/functional/specs/Migration/helper.js b/test/functional/specs/Migration/helper.js index 72ea49b01..17a5d7c8a 100644 --- a/test/functional/specs/Migration/helper.js +++ b/test/functional/specs/Migration/helper.js @@ -79,6 +79,7 @@ export const fetchMboxOffer = ClientFunction( success(response) { return response; }, + // eslint-disable-next-line no-console error: console.error }); } diff --git a/test/functional/specs/Visitor/C35448.js b/test/functional/specs/Visitor/C35448.js index 7c8e93b20..f685c0312 100644 --- a/test/functional/specs/Visitor/C35448.js +++ b/test/functional/specs/Visitor/C35448.js @@ -31,7 +31,6 @@ const visitorReady = ClientFunction(() => { }); const injectVisitor = ClientFunction( () => { - console.log(REMOTE_VISITOR_LIBRARY_URL); const s = document.createElement("script"); s.src = REMOTE_VISITOR_LIBRARY_URL; document.body.appendChild(s); diff --git a/test/unit/specs/components/DataCollector/index.spec.js b/test/unit/specs/components/DataCollector/index.spec.js index 8d5ad8774..45c64dda5 100644 --- a/test/unit/specs/components/DataCollector/index.spec.js +++ b/test/unit/specs/components/DataCollector/index.spec.js @@ -60,6 +60,7 @@ describe("Event Command", () => { expect(event.setUserData).toHaveBeenCalledWith(data); expect(eventManager.sendEvent).toHaveBeenCalledWith(event, { renderDecisions: true, + decisionContext: {}, decisionScopes: [], personalization: {} }); @@ -79,6 +80,7 @@ describe("Event Command", () => { return sendEventCommand.run(options).then(result => { expect(eventManager.sendEvent).toHaveBeenCalledWith(event, { renderDecisions: true, + decisionContext: {}, decisionScopes: ["Foo1"], personalization: { decisionScopes: ["Foo2"] @@ -99,6 +101,7 @@ describe("Event Command", () => { return sendEventCommand.run(options).then(result => { expect(eventManager.sendEvent).toHaveBeenCalledWith(event, { renderDecisions: true, + decisionContext: {}, decisionScopes: [], personalization: { surfaces: ["Foo1", "Foo2"] @@ -118,6 +121,7 @@ describe("Event Command", () => { return sendEventCommand.run({}).then(() => { expect(eventManager.sendEvent).toHaveBeenCalledWith(event, { renderDecisions: false, + decisionContext: {}, decisionScopes: [], personalization: {} }); diff --git a/test/unit/specs/components/DecisioningEngine/createApplyResponse.spec.js b/test/unit/specs/components/DecisioningEngine/createApplyResponse.spec.js new file mode 100644 index 000000000..23d9655a8 --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/createApplyResponse.spec.js @@ -0,0 +1,51 @@ +import createApplyResponse from "../../../../../src/components/DecisioningEngine/createApplyResponse"; + +describe("DecisioningEngine:createApplyResponse", () => { + const proposition = { + id: "AT:eyJhY3Rpdml0eUlkIjoiMTQxMDY0IiwiZXhwZXJpZW5jZUlkIjoiMCJ9", + scope: "__view__", + items: [] + }; + + it("calls lifecycle.onDecision with propositions", () => { + const lifecycle = jasmine.createSpyObj("lifecycle", { + onDecision: Promise.resolve() + }); + + const applyResponse = createApplyResponse(lifecycle); + + applyResponse({ propositions: [proposition] }); + + expect(lifecycle.onDecision).toHaveBeenCalledWith({ + viewName: undefined, + propositions: [proposition] + }); + }); + + it("calls lifecycle.onDecision with viewName", () => { + const lifecycle = jasmine.createSpyObj("lifecycle", { + onDecision: Promise.resolve() + }); + + const applyResponse = createApplyResponse(lifecycle); + + applyResponse({ viewName: "oh hai", propositions: [proposition] }); + + expect(lifecycle.onDecision).toHaveBeenCalledWith({ + viewName: "oh hai", + propositions: [proposition] + }); + }); + + it("does not call lifecycle.onDecision if no propositions", () => { + const lifecycle = jasmine.createSpyObj("lifecycle", { + onDecision: Promise.resolve() + }); + + const applyResponse = createApplyResponse(lifecycle); + + applyResponse({ propositions: [] }); + + expect(lifecycle.onDecision).not.toHaveBeenCalled(); + }); +}); diff --git a/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js b/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js new file mode 100644 index 000000000..a2b80d15b --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js @@ -0,0 +1,452 @@ +import createDecisionProvider from "../../../../../src/components/DecisioningEngine/createDecisionProvider"; + +describe("DecisioningEngine:createDecisionProvider", () => { + let decisionProvider; + + beforeEach(() => { + decisionProvider = createDecisionProvider(); + decisionProvider.addPayloads([ + { + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: "abc" + }, + strategies: [ + { + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" + } + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" + }, + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" + }, + id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", + items: [ + { + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", + schema: "https://ns.adobe.com/personalization/json-ruleset-item", + data: { + content: JSON.stringify({ + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [ + { + definition: { + conditions: [ + { + definition: { + key: "color", + matcher: "eq", + values: ["orange", "blue"] + }, + type: "matcher" + }, + { + definition: { + key: "action", + matcher: "eq", + values: ["lipstick"] + }, + type: "matcher" + } + ], + logic: "and" + }, + type: "group" + } + ], + logic: "and" + }, + type: "group" + }, + consequences: [ + { + type: "item", + detail: { + schema: + "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" + }, + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }, + { + type: "item", + detail: { + schema: + "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + } + ] + } + ] + }) + } + } + ], + scope: "web://mywebsite.com" + }, + { + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: "abc" + }, + strategies: [ + { + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" + } + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" + }, + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" + }, + id: "3d5d69cd-acde-4eca-b43b-a54574b67bb0", + items: [ + { + id: "5229f502-38d6-40c3-9a3a-b5b1a6adc441", + schema: "https://ns.adobe.com/personalization/json-ruleset-item", + data: { + content: JSON.stringify({ + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [ + { + definition: { + conditions: [ + { + definition: { + key: "xdm.web.webPageDetails.viewName", + matcher: "eq", + values: ["home"] + }, + type: "matcher" + } + ], + logic: "and" + }, + type: "group" + } + ], + logic: "and" + }, + type: "group" + }, + consequences: [ + { + type: "item", + detail: { + schema: + "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "div#spa #spa-content h3", + type: "setHtml", + content: "i can haz?", + prehidingSelector: "div#spa #spa-content h3" + }, + id: "8a0d7a45-70fb-4845-a093-2133b5744c8d" + }, + id: "8a0d7a45-70fb-4845-a093-2133b5744c8d" + }, + { + type: "item", + detail: { + schema: + "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "div#spa #spa-content p", + type: "setHtml", + content: "ALL YOUR BASE ARE BELONG TO US", + prehidingSelector: "div#spa #spa-content p" + }, + id: "a44af51a-e073-4e8c-92e1-84ac28210043" + }, + id: "a44af51a-e073-4e8c-92e1-84ac28210043" + } + ] + } + ] + }) + } + } + ], + scope: "web://mywebsite.com" + } + ]); + }); + it("returns a single payload with items that qualify", () => { + expect( + decisionProvider.evaluate({ color: "blue", action: "lipstick" }) + ).toEqual([ + { + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: "abc" + }, + strategies: [ + { + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" + } + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" + }, + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" + }, + id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" + }, + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }, + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + } + ], + scope: "web://mywebsite.com" + } + ]); + }); + it("returns a different single payload with items that qualify", () => { + expect( + decisionProvider.evaluate({ "xdm.web.webPageDetails.viewName": "home" }) + ).toEqual([ + { + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: "abc" + }, + strategies: [ + { + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" + } + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" + }, + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" + }, + id: "3d5d69cd-acde-4eca-b43b-a54574b67bb0", + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "div#spa #spa-content h3", + type: "setHtml", + content: "i can haz?", + prehidingSelector: "div#spa #spa-content h3" + }, + id: "8a0d7a45-70fb-4845-a093-2133b5744c8d" + }, + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "div#spa #spa-content p", + type: "setHtml", + content: "ALL YOUR BASE ARE BELONG TO US", + prehidingSelector: "div#spa #spa-content p" + }, + id: "a44af51a-e073-4e8c-92e1-84ac28210043" + } + ], + scope: "web://mywebsite.com" + } + ]); + }); + it("returns two payloads with items that qualify", () => { + expect( + decisionProvider.evaluate({ + color: "blue", + action: "lipstick", + "xdm.web.webPageDetails.viewName": "home" + }) + ).toEqual([ + { + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: "abc" + }, + strategies: [ + { + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" + } + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" + }, + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" + }, + id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" + }, + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }, + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + } + ], + scope: "web://mywebsite.com" + }, + { + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: "abc" + }, + strategies: [ + { + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" + } + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" + }, + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" + }, + id: "3d5d69cd-acde-4eca-b43b-a54574b67bb0", + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "div#spa #spa-content h3", + type: "setHtml", + content: "i can haz?", + prehidingSelector: "div#spa #spa-content h3" + }, + id: "8a0d7a45-70fb-4845-a093-2133b5744c8d" + }, + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "div#spa #spa-content p", + type: "setHtml", + content: "ALL YOUR BASE ARE BELONG TO US", + prehidingSelector: "div#spa #spa-content p" + }, + id: "a44af51a-e073-4e8c-92e1-84ac28210043" + } + ], + scope: "web://mywebsite.com" + } + ]); + }); + + it("ignores payloads that aren't json-ruleset type", () => { + decisionProvider.addPayload({ + id: "AT:eyJhY3Rpdml0eUlkIjoiMTQxMDY0IiwiZXhwZXJpZW5jZUlkIjoiMCJ9", + scope: "__view__", + scopeDetails: { + decisionProvider: "TGT", + activity: { + id: "141064" + }, + experience: { + id: "0" + }, + strategies: [ + { + algorithmID: "0", + trafficType: "0" + } + ], + characteristics: { + eventToken: "abc" + }, + correlationID: "141064:0:0:0" + }, + items: [ + { + id: "284525", + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + type: "setHtml", + format: "application/vnd.adobe.target.dom-action", + content: "
oh hai
", + selector: "head", + prehidingSelector: "head" + } + } + ] + }); + + expect(decisionProvider.evaluate()).toEqual([]); + }); +}); diff --git a/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js b/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js new file mode 100644 index 000000000..fb5faef0d --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js @@ -0,0 +1,277 @@ +import RulesEngine from "@adobe/aep-rules-engine"; +import createEvaluableRulesetPayload from "../../../../../src/components/DecisioningEngine/createEvaluableRulesetPayload"; + +describe("DecisioningEngine:createEvaluableRulesetPayload", () => { + it("does", () => { + const ruleset = RulesEngine({ + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [ + { + definition: { + conditions: [ + { + definition: { + key: "color", + matcher: "eq", + values: ["orange", "blue"] + }, + type: "matcher" + }, + { + definition: { + key: "action", + matcher: "eq", + values: ["lipstick"] + }, + type: "matcher" + } + ], + logic: "and" + }, + type: "group" + } + ], + logic: "and" + }, + type: "group" + }, + consequences: [ + { + type: "item", + detail: { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" + }, + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }, + { + type: "item", + detail: { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + } + ] + } + ] + }); + + expect(ruleset.execute({ color: "orange", action: "lipstick" })).toEqual([ + [ + { + type: "item", + detail: { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" + }, + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }, + { + type: "item", + detail: { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + } + ] + ]); + }); + it("works", () => { + const evaluableRulesetPayload = createEvaluableRulesetPayload({ + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: "abc" + }, + strategies: [ + { + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" + } + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" + }, + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" + }, + id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", + items: [ + { + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", + schema: "https://ns.adobe.com/personalization/json-ruleset-item", + data: { + content: JSON.stringify({ + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [ + { + definition: { + conditions: [ + { + definition: { + key: "color", + matcher: "eq", + values: ["orange", "blue"] + }, + type: "matcher" + }, + { + definition: { + key: "action", + matcher: "eq", + values: ["lipstick"] + }, + type: "matcher" + } + ], + logic: "and" + }, + type: "group" + } + ], + logic: "and" + }, + type: "group" + }, + consequences: [ + { + type: "item", + detail: { + schema: + "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" + }, + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }, + { + type: "item", + detail: { + schema: + "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + } + ] + } + ] + }) + } + } + ], + scope: "web://mywebsite.com" + }); + + expect( + evaluableRulesetPayload.evaluate({ color: "orange", action: "lipstick" }) + ).toEqual({ + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: "abc" + }, + strategies: [ + { + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" + } + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" + }, + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" + }, + id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" + }, + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }, + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + } + ], + scope: "web://mywebsite.com" + }); + }); +}); diff --git a/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js b/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js new file mode 100644 index 000000000..3b4407401 --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js @@ -0,0 +1,122 @@ +import createEventRegistry from "../../../../../src/components/DecisioningEngine/createEventRegistry"; + +describe("DecisioningEngine:createEventRegistry", () => { + it("registers events", () => { + const eventRegistry = createEventRegistry(); + + const getContent = () => ({ + xdm: { + eventType: "display", + _experience: { + decisioning: { + propositions: [{ id: "abc" }, { id: "def" }, { id: "ghi" }] + } + } + } + }); + + const event = { + getContent + }; + + eventRegistry.rememberEvent(event); + + expect(eventRegistry.toJSON()).toEqual({ + abc: { + event: { id: "abc", type: "display" }, + timestamp: jasmine.any(Number), + count: 1 + }, + def: { + event: { id: "def", type: "display" }, + timestamp: jasmine.any(Number), + count: 1 + }, + ghi: { + event: { id: "ghi", type: "display" }, + timestamp: jasmine.any(Number), + count: 1 + } + }); + }); + + it("does not register invalid events", () => { + const eventRegistry = createEventRegistry(); + + eventRegistry.rememberEvent({ + getContent: () => ({ + xdm: { + eventType: "display" + } + }) + }); + eventRegistry.rememberEvent({ + getContent: () => ({ + xdm: { + eventType: "display", + _experience: {} + } + }) + }); + eventRegistry.rememberEvent({ + getContent: () => ({ + xdm: { + eventType: "display", + _experience: { + decisioning: {} + } + } + }) + }); + eventRegistry.rememberEvent({ + getContent: () => ({}) + }); + + expect(eventRegistry.toJSON()).toEqual({}); + }); + + it("increments count and sets timestamp", done => { + const eventRegistry = createEventRegistry(); + + const getContent = () => ({ + xdm: { + eventType: "display", + _experience: { + decisioning: { + propositions: [{ id: "abc" }] + } + } + } + }); + + const event = { + getContent + }; + let lastEventTime = 0; + eventRegistry.rememberEvent(event); + + expect(eventRegistry.getEvent("abc")).toEqual({ + event: { id: "abc", type: "display" }, + timestamp: jasmine.any(Number), + count: 1 + }); + expect(eventRegistry.getEvent("abc").timestamp).toBeGreaterThan( + lastEventTime + ); + lastEventTime = eventRegistry.getEvent("abc").timestamp; + + setTimeout(() => { + eventRegistry.rememberEvent(event); // again + + expect(eventRegistry.getEvent("abc")).toEqual({ + event: { id: "abc", type: "display" }, + timestamp: jasmine.any(Number), + count: 2 + }); + expect(eventRegistry.getEvent("abc").timestamp).toBeGreaterThan( + lastEventTime + ); + done(); + }, 10); + }); +}); diff --git a/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js b/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js new file mode 100644 index 000000000..230593519 --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js @@ -0,0 +1,342 @@ +import createOnResponseHandler from "../../../../../src/components/DecisioningEngine/createOnResponseHandler"; +import createDecisionProvider from "../../../../../src/components/DecisioningEngine/createDecisionProvider"; +import createApplyResponse from "../../../../../src/components/DecisioningEngine/createApplyResponse"; + +describe("DecisioningEngine:createOnResponseHandler", () => { + it("calls lifecycle.onDecision with propositions based on decisionContext", () => { + const lifecycle = jasmine.createSpyObj("lifecycle", { + onDecision: Promise.resolve() + }); + + const decisionProvider = createDecisionProvider(); + const applyResponse = createApplyResponse(lifecycle); + + const event = { + getViewName: () => undefined, + getContent: () => ({ + xdm: { + web: { + webPageDetails: { + viewName: "contact", + URL: "https://mywebsite.com" + }, + webReferrer: { + URL: "https://google.com" + } + }, + timestamp: new Date().toISOString(), + implementationDetails: { + name: "https://ns.adobe.com/experience/alloy", + version: "2.15.0", + environment: "browser" + } + }, + data: { + moo: "woof" + } + }) + }; + + const decisionContext = { + color: "orange", + action: "lipstick" + }; + + const responseHandler = createOnResponseHandler({ + decisionProvider, + applyResponse, + event, + decisionContext + }); + + const response = { + getPayloadsByType: () => [ + { + id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", + items: [ + { + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", + schema: "https://ns.adobe.com/personalization/json-ruleset-item", + data: { + content: JSON.stringify({ + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [ + { + definition: { + conditions: [ + { + definition: { + key: "color", + matcher: "eq", + values: ["orange", "blue"] + }, + type: "matcher" + }, + { + definition: { + key: "action", + matcher: "eq", + values: ["lipstick"] + }, + type: "matcher" + } + ], + logic: "and" + }, + type: "group" + } + ], + logic: "and" + }, + type: "group" + }, + consequences: [ + { + type: "item", + detail: { + schema: + "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" + }, + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }, + { + type: "item", + detail: { + schema: + "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + } + ] + } + ] + }) + } + } + ], + scope: "web://target.jasonwaters.dev/aep.html" + } + ] + }; + + responseHandler({ + response + }); + + expect(lifecycle.onDecision).toHaveBeenCalledWith({ + viewName: undefined, + propositions: [ + { + id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" + }, + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }, + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + } + ], + scope: "web://target.jasonwaters.dev/aep.html" + } + ] + }); + }); + + it("calls lifecycle.onDecision with propositions based on xdm and event data", () => { + const lifecycle = jasmine.createSpyObj("lifecycle", { + onDecision: Promise.resolve() + }); + + const decisionProvider = createDecisionProvider(); + const applyResponse = createApplyResponse(lifecycle); + + const event = { + getViewName: () => "home", + getContent: () => ({ + xdm: { + web: { + webPageDetails: { + viewName: "contact", + URL: "https://mywebsite.com" + }, + webReferrer: { + URL: "https://google.com" + } + }, + timestamp: new Date().toISOString(), + implementationDetails: { + name: "https://ns.adobe.com/experience/alloy", + version: "12345", + environment: "browser" + } + }, + data: { + moo: "woof" + } + }) + }; + + const decisionContext = {}; + + const responseHandler = createOnResponseHandler({ + decisionProvider, + applyResponse, + event, + decisionContext + }); + + const response = { + getPayloadsByType: () => [ + { + id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", + items: [ + { + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", + schema: "https://ns.adobe.com/personalization/json-ruleset-item", + data: { + content: JSON.stringify({ + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [ + { + definition: { + conditions: [ + { + definition: { + key: "xdm.web.webPageDetails.viewName", + matcher: "eq", + values: ["contact"] + }, + type: "matcher" + }, + { + definition: { + key: "xdm.implementationDetails.version", + matcher: "eq", + values: ["12345"] + }, + type: "matcher" + }, + { + definition: { + key: "data.moo", + matcher: "eq", + values: ["woof"] + }, + type: "matcher" + } + ], + logic: "and" + }, + type: "group" + } + ], + logic: "and" + }, + type: "group" + }, + consequences: [ + { + type: "item", + detail: { + schema: + "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + } + ] + } + ] + }) + } + } + ], + scope: "web://target.jasonwaters.dev/aep.html" + } + ] + }; + + responseHandler({ + response + }); + + expect(lifecycle.onDecision).toHaveBeenCalledWith({ + viewName: "home", + propositions: [ + { + id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + } + ], + scope: "web://target.jasonwaters.dev/aep.html" + } + ] + }); + }); +}); diff --git a/test/unit/specs/core/createEventManager.spec.js b/test/unit/specs/core/createEventManager.spec.js index bb2ec31bd..f916c544e 100644 --- a/test/unit/specs/core/createEventManager.spec.js +++ b/test/unit/specs/core/createEventManager.spec.js @@ -115,6 +115,7 @@ describe("createEventManager", () => { expect(lifecycle.onBeforeEvent).toHaveBeenCalledWith({ event, renderDecisions: true, + decisionContext: undefined, decisionScopes: undefined, personalization: undefined, onResponse: jasmine.any(Function), diff --git a/test/unit/specs/utils/flattenArray.spec.js b/test/unit/specs/utils/flattenArray.spec.js new file mode 100644 index 000000000..1142baeba --- /dev/null +++ b/test/unit/specs/utils/flattenArray.spec.js @@ -0,0 +1,62 @@ +import flattenArray from "../../../../src/utils/flattenArray"; + +describe("flattenArray", () => { + it("recursively flattens an array", () => { + expect( + flattenArray([ + "a", + ["b", "c"], + "d", + ["e"], + "f", + ["g"], + [ + "h", + [ + "i", + ["j"], + "k", + ["l", ["m"], ["n", ["o"], ["p", ["q"], "r"], "s"], "t"] + ], + "u" + ], + "v", + "w", + "x", + "y", + "z" + ]) + ).toEqual([ + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z" + ]); + }); + + it("handles non arrays", () => { + expect(flattenArray({ wat: true })).toEqual({ wat: true }); + }); +}); diff --git a/test/unit/specs/utils/flattenObject.spec.js b/test/unit/specs/utils/flattenObject.spec.js new file mode 100644 index 000000000..02d0596b1 --- /dev/null +++ b/test/unit/specs/utils/flattenObject.spec.js @@ -0,0 +1,105 @@ +import flattenObject from "../../../../src/utils/flattenObject"; + +describe("flattenObject", () => { + it("flattens event object", () => { + expect( + flattenObject({ + xdm: { + web: { + webPageDetails: { + viewName: "contact", + URL: "https://localhost/aep.html#contact" + }, + webReferrer: { + URL: "https://google.com" + } + }, + timestamp: "2023-04-12T17:37:56.519Z", + implementationDetails: { + name: "https://ns.adobe.com/experience/alloy", + version: "2.15.0", + environment: "browser" + } + }, + data: { + moo: "woof" + } + }) + ).toEqual({ + "xdm.web.webPageDetails.viewName": "contact", + "xdm.web.webPageDetails.URL": "https://localhost/aep.html#contact", + "xdm.web.webReferrer.URL": "https://google.com", + "xdm.timestamp": "2023-04-12T17:37:56.519Z", + "xdm.implementationDetails.name": "https://ns.adobe.com/experience/alloy", + "xdm.implementationDetails.version": "2.15.0", + "xdm.implementationDetails.environment": "browser", + "data.moo": "woof" + }); + }); + + it("flattens nested arrays", () => { + expect( + flattenObject({ + pre: true, + a: { + one: 1, + two: 2, + three: { + aa: 2, + bb: 43, + cc: [ + "alf", + "fred", + { + cool: "beans", + lets: "go" + } + ] + } + }, + b: { + one: 1, + two: 2, + three: { + poo: true + } + }, + c: { + uno: true, + dos: false, + tres: { + value: "yeah ok" + } + } + }) + ).toEqual({ + pre: true, + "a.one": 1, + "a.two": 2, + "a.three.aa": 2, + "a.three.bb": 43, + "a.three.cc.0": "alf", + "a.three.cc.1": "fred", + "a.three.cc.2.cool": "beans", + "a.three.cc.2.lets": "go", + "b.one": 1, + "b.two": 2, + "b.three.poo": true, + "c.uno": true, + "c.dos": false, + "c.tres.value": "yeah ok" + }); + }); + + it("handles non-objects", () => { + expect(flattenObject(true)).toEqual(true); + expect(flattenObject([1, 2, 3])).toEqual([1, 2, 3]); + expect(flattenObject("hello")).toEqual("hello"); + + let obj = new Set(); + expect(obj).toEqual(obj); + + obj = () => undefined; + expect(flattenObject(obj)).toEqual(obj); + }); +}); From ffcc6dd3084bb179ad9610f6ab52806ff0257bb9 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Mon, 17 Apr 2023 15:05:47 -0600 Subject: [PATCH 05/66] fix test --- .../in-app-message-actions/actions/displayModal.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js index e30224918..a11343a37 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js @@ -47,9 +47,9 @@ describe("Personalization::IAM:modal", () => { expect(modal.previousElementSibling).toEqual(something); expect(modal.nextElementSibling).toBeNull(); - expect(modal.querySelector(".alloy-modal-content").innerText).toEqual( - "Special offer, don't delay!" - ); + expect( + modal.querySelector(".alloy-modal-content").innerText.trim() + ).toEqual("Special offer, don't delay!"); const buttons = modal.querySelector(".alloy-modal-buttons"); From 9d4662e0d702bc5f266898cecaf680d7475f8be0 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Tue, 18 Apr 2023 15:27:48 -0600 Subject: [PATCH 06/66] removed modulesProvider and refactored it into actionsProvider with an executeAction method --- .../Personalization/createActionsProvider.js | 75 +++++++++++++++ .../Personalization/createExecuteDecisions.js | 5 +- .../Personalization/createModulesProvider.js | 21 ----- .../Personalization/executeActions.js | 66 +------------- src/components/Personalization/index.js | 9 +- .../createActionsProvider.spec.js | 84 +++++++++++++++++ .../createExecuteDecisions.spec.js | 27 +++--- .../createModulesProvider.spec.js | 91 ------------------- .../Personalization/executeActions.spec.js | 30 +++--- 9 files changed, 199 insertions(+), 209 deletions(-) create mode 100644 src/components/Personalization/createActionsProvider.js delete mode 100644 src/components/Personalization/createModulesProvider.js create mode 100644 test/unit/specs/components/Personalization/createActionsProvider.spec.js delete mode 100644 test/unit/specs/components/Personalization/createModulesProvider.spec.js diff --git a/src/components/Personalization/createActionsProvider.js b/src/components/Personalization/createActionsProvider.js new file mode 100644 index 000000000..496674276 --- /dev/null +++ b/src/components/Personalization/createActionsProvider.js @@ -0,0 +1,75 @@ +import createPreprocessors from "./createPreprocessors"; +import { assign } from "../../utils"; + +export default ({ modules, preprocessors = createPreprocessors(), logger }) => { + const logActionError = (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 = action => { + if (logger.enabled) { + const details = JSON.stringify(action); + + logger.info(`Action ${details} executed.`); + } + }; + + const getExecuteAction = (schema, type) => { + if (!modules[schema] || !modules[schema][type]) { + return () => + Promise.reject( + new Error(`Action "${type}" not found for schema "${schema}"`) + ); + } + + return modules[schema][type]; + }; + + const applyPreprocessors = action => { + const { schema } = action; + + const preprocessorsList = preprocessors[schema]; + + if ( + !schema || + !(preprocessorsList instanceof Array) || + preprocessorsList.length === 0 + ) { + return action; + } + + return preprocessorsList.reduce( + (processed, fn) => assign(processed, fn(processed)), + action + ); + }; + + const executeAction = action => { + const processedAction = applyPreprocessors(action); + const { type, schema } = processedAction; + + const execute = getExecuteAction(schema, type); + + return execute(processedAction) + .then(result => { + logActionCompleted(processedAction); + return result; + }) + .catch(error => { + logActionError(processedAction, error); + throw error; + }); + }; + + return { + executeAction + }; +}; diff --git a/src/components/Personalization/createExecuteDecisions.js b/src/components/Personalization/createExecuteDecisions.js index 02caa436a..20c0b113c 100644 --- a/src/components/Personalization/createExecuteDecisions.js +++ b/src/components/Personalization/createExecuteDecisions.js @@ -67,12 +67,11 @@ const processMetas = (logger, actionResults) => { return finalMetas; }; -export default ({ modulesProvider, logger, executeActions }) => { +export default ({ actionsProvider, logger, executeActions }) => { return decisions => { const actionResultsPromises = decisions.map(decision => { const actions = buildActions(decision); - - return executeActions(actions, modulesProvider, logger); + return executeActions(actions, actionsProvider); }); return Promise.all(actionResultsPromises) .then(results => processMetas(logger, results)) diff --git a/src/components/Personalization/createModulesProvider.js b/src/components/Personalization/createModulesProvider.js deleted file mode 100644 index d9fe825ff..000000000 --- a/src/components/Personalization/createModulesProvider.js +++ /dev/null @@ -1,21 +0,0 @@ -import createPreprocessors from "./createPreprocessors"; - -export default ({ modules, preprocessors = createPreprocessors() }) => { - const getModules = schema => { - return { - getAction: type => { - if (!modules[schema]) { - return undefined; - } - - return modules[schema][type]; - }, - getPreprocessors: () => preprocessors[schema], - getSchema: () => schema - }; - }; - - return { - getModules - }; -}; diff --git a/src/components/Personalization/executeActions.js b/src/components/Personalization/executeActions.js index 3d512c6fd..7aae7af01 100644 --- a/src/components/Personalization/executeActions.js +++ b/src/components/Personalization/executeActions.js @@ -10,69 +10,9 @@ 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"; - -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.getAction(type); - - if (!execute) { - const error = new Error( - `Action "${type}" not found for schema ${modules.getSchema()}` - ); - logActionError(logger, args[0], error); - throw error; - } - return execute(...args); -}; - -const preprocess = (preprocessors, action) => { - if (!(preprocessors instanceof Array) || preprocessors.length === 0) { - return action; - } - - return preprocessors.reduce( - (processed, fn) => assign(processed, fn(processed)), - action +export default (actions, actionsProvider) => { + const actionPromises = actions.map(action => + actionsProvider.executeAction(action) ); -}; -export default (actions, modulesProvider, logger) => { - const actionPromises = actions.map(action => { - const { schema } = action; - - const modules = modulesProvider.getModules(schema); - - const processedAction = preprocess(modules.getPreprocessors(), 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/index.js b/src/components/Personalization/index.js index afda29bbe..5fc4a4577 100644 --- a/src/components/Personalization/index.js +++ b/src/components/Personalization/index.js @@ -31,7 +31,7 @@ import createNonRenderingHandler from "./createNonRenderingHandler"; import createApplyPropositions from "./createApplyPropositions"; import createGetPageLocation from "./createGetPageLocation"; import createSetTargetMigration from "./createSetTargetMigration"; -import createModulesProvider from "./createModulesProvider"; +import createActionsProvider from "./createActionsProvider"; import executeActions from "./executeActions"; import createModules from "./createModules"; import createPreprocessors from "./createPreprocessors"; @@ -48,13 +48,14 @@ const createPersonalization = ({ config, logger, eventManager }) => { const getPageLocation = createGetPageLocation({ window }); const viewCache = createViewCacheManager(); - const modulesProvider = createModulesProvider({ + const actionsProvider = createActionsProvider({ modules: createModules(storeClickMetrics), - preprocessors: createPreprocessors() + preprocessors: createPreprocessors(), + logger }); const executeDecisions = createExecuteDecisions({ - modulesProvider, + actionsProvider, logger, executeActions }); diff --git a/test/unit/specs/components/Personalization/createActionsProvider.spec.js b/test/unit/specs/components/Personalization/createActionsProvider.spec.js new file mode 100644 index 000000000..5322980dd --- /dev/null +++ b/test/unit/specs/components/Personalization/createActionsProvider.spec.js @@ -0,0 +1,84 @@ +import createActionsProvider from "../../../../../src/components/Personalization/createActionsProvider"; + +describe("createActionsProvider", () => { + let actionsProvider; + let logger; + + beforeEach(() => { + logger = jasmine.createSpyObj("logger", ["warn", "error", "info"]); + logger.enabled = true; + + actionsProvider = createActionsProvider({ + modules: { + something: { + eat: () => Promise.resolve("yum"), + sleep: () => Promise.resolve(), + exercise: () => Promise.resolve() + } + }, + preprocessors: { + something: [action => action], + superfluous: [action => action, action => action, action => action] + }, + logger + }); + }); + + it("executes appropriate action", done => { + const actionDetails = { + schema: "something", + type: "eat", + itWorked: true + }; + + actionsProvider.executeAction(actionDetails).then(result => { + expect(result).toEqual("yum"); + expect(logger.info).toHaveBeenCalledOnceWith( + jasmine.stringContaining( + `Action ${JSON.stringify(actionDetails)} executed.` + ) + ); + done(); + }); + }); + + it("throws error if missing schema", done => { + const actionDetails = { + schema: "hidden-valley", + type: "truckee", + itWorked: true + }; + + actionsProvider.executeAction(actionDetails).catch(error => { + expect(error.message).toEqual( + `Action "truckee" not found for schema "hidden-valley"` + ); + expect(logger.error).toHaveBeenCalledOnceWith( + jasmine.stringContaining( + `Failed to execute action ${JSON.stringify(actionDetails)}.` + ) + ); + done(); + }); + }); + + it("throws error if missing action", done => { + const actionDetails = { + schema: "something", + type: "truckee", + itWorked: true + }; + + actionsProvider.executeAction(actionDetails).catch(error => { + expect(error.message).toEqual( + `Action "truckee" not found for schema "something"` + ); + expect(logger.error).toHaveBeenCalledOnceWith( + jasmine.stringContaining( + `Failed to execute action ${JSON.stringify(actionDetails)}.` + ) + ); + done(); + }); + }); +}); diff --git a/test/unit/specs/components/Personalization/createExecuteDecisions.spec.js b/test/unit/specs/components/Personalization/createExecuteDecisions.spec.js index 53c95c8a3..1f9c98153 100644 --- a/test/unit/specs/components/Personalization/createExecuteDecisions.spec.js +++ b/test/unit/specs/components/Personalization/createExecuteDecisions.spec.js @@ -12,11 +12,12 @@ governing permissions and limitations under the License. import createExecuteDecisions from "../../../../../src/components/Personalization/createExecuteDecisions"; import { DOM_ACTION } from "../../../../../src/components/Personalization/constants/schema"; -import createModulesProvider from "../../../../../src/components/Personalization/createModulesProvider"; +import createActionsProvider from "../../../../../src/components/Personalization/createActionsProvider"; describe("Personalization::createExecuteDecisions", () => { let logger; let executeActions; + let actionsProvider; const decisions = [ { @@ -82,17 +83,18 @@ describe("Personalization::createExecuteDecisions", () => { } ]; - const modulesProvider = createModulesProvider({ - modules: { - [DOM_ACTION]: { - foo() {} - } - } - }); - beforeEach(() => { logger = jasmine.createSpyObj("logger", ["info", "warn", "error"]); executeActions = jasmine.createSpy(); + + actionsProvider = createActionsProvider({ + modules: { + [DOM_ACTION]: { + foo() {} + } + }, + logger + }); }); it("should trigger executeActions when provided with an array of actions", () => { @@ -101,15 +103,14 @@ describe("Personalization::createExecuteDecisions", () => { [{ meta: metas[1], error: "could not render this item" }] ); const executeDecisions = createExecuteDecisions({ - modulesProvider, + actionsProvider, logger, executeActions }); return executeDecisions(decisions).then(() => { expect(executeActions).toHaveBeenCalledWith( expectedAction, - modulesProvider, - logger + actionsProvider ); expect(logger.warn).toHaveBeenCalledWith({ meta: metas[1], @@ -121,7 +122,7 @@ describe("Personalization::createExecuteDecisions", () => { it("shouldn't trigger executeActions when provided with empty array of actions", () => { executeActions.and.callThrough(); const executeDecisions = createExecuteDecisions({ - modulesProvider, + actionsProvider, logger, executeActions }); diff --git a/test/unit/specs/components/Personalization/createModulesProvider.spec.js b/test/unit/specs/components/Personalization/createModulesProvider.spec.js deleted file mode 100644 index 33f641e88..000000000 --- a/test/unit/specs/components/Personalization/createModulesProvider.spec.js +++ /dev/null @@ -1,91 +0,0 @@ -import { DOM_ACTION } from "@adobe/alloy/libEs5/components/Personalization/constants/schema"; -import createModulesProvider from "../../../../../src/components/Personalization/createModulesProvider"; -import createPreprocessors from "../../../../../src/components/Personalization/createPreprocessors"; - -describe("createModulesProvider", () => { - let modulesProvider; - beforeEach(() => { - modulesProvider = createModulesProvider({ - modules: { - something: { - eat: () => undefined, - sleep: () => undefined, - exercise: () => undefined - }, - superfluous: { - bend: () => undefined, - crease: () => undefined, - fold: () => undefined - } - }, - preprocessors: { - something: [() => undefined], - superfluous: [() => undefined, () => undefined, () => undefined] - } - }); - }); - - it("has schema property", () => { - expect(modulesProvider.getModules("something").getSchema()).toEqual( - "something" - ); - }); - - it("has 'something' module actions", () => { - expect(modulesProvider.getModules("something").getAction("eat")).toEqual( - jasmine.any(Function) - ); - expect(modulesProvider.getModules("something").getAction("sleep")).toEqual( - jasmine.any(Function) - ); - expect( - modulesProvider.getModules("something").getAction("exercise") - ).toEqual(jasmine.any(Function)); - }); - - it("has 'superfluous' module actions", () => { - expect(modulesProvider.getModules("superfluous").getAction("bend")).toEqual( - jasmine.any(Function) - ); - expect( - modulesProvider.getModules("superfluous").getAction("crease") - ).toEqual(jasmine.any(Function)); - expect(modulesProvider.getModules("superfluous").getAction("fold")).toEqual( - jasmine.any(Function) - ); - }); - it("does not have 'moo' module actions", () => { - expect(modulesProvider.getModules("moo").getAction("a")).not.toBeDefined(); - }); - - it("has 'something' preprocessors", () => { - expect(modulesProvider.getModules("something").getPreprocessors()).toEqual([ - jasmine.any(Function) - ]); - }); - it("has 'superfluous' preprocessors", () => { - expect( - modulesProvider.getModules("superfluous").getPreprocessors() - ).toEqual([ - jasmine.any(Function), - jasmine.any(Function), - jasmine.any(Function) - ]); - }); - - it("has default preprocessors if none specified", () => { - const provider = createModulesProvider({ - modules: { - something: { - eat: () => undefined, - sleep: () => undefined, - exercise: () => undefined - } - } - }); - - expect(provider.getModules(DOM_ACTION).getPreprocessors()).toEqual( - createPreprocessors()[DOM_ACTION] - ); - }); -}); diff --git a/test/unit/specs/components/Personalization/executeActions.spec.js b/test/unit/specs/components/Personalization/executeActions.spec.js index 16bcb075f..6cca94634 100644 --- a/test/unit/specs/components/Personalization/executeActions.spec.js +++ b/test/unit/specs/components/Personalization/executeActions.spec.js @@ -12,7 +12,7 @@ governing permissions and limitations under the License. import { DOM_ACTION } from "@adobe/alloy/libEs5/components/Personalization/constants/schema"; import executeActions from "../../../../../src/components/Personalization/executeActions"; -import createModulesProvider from "../../../../../src/components/Personalization/createModulesProvider"; +import createActionsProvider from "../../../../../src/components/Personalization/createActionsProvider"; describe("Personalization::executeActions", () => { it("should execute actions", () => { @@ -23,14 +23,14 @@ describe("Personalization::executeActions", () => { return executeActions( actions, - createModulesProvider({ + createActionsProvider({ modules: { [DOM_ACTION]: { foo: actionSpy } - } - }), - logger + }, + logger + }) ).then(result => { expect(result).toEqual([1]); expect(actionSpy).toHaveBeenCalled(); @@ -68,17 +68,18 @@ describe("Personalization::executeActions", () => { } ]; - const modulesProvider = createModulesProvider({ + const actionsProvider = createActionsProvider({ modules: { [DOM_ACTION]: { setHtml: setHtmlActionSpy, appendHtml: appendHtmlActionSpy, customCode: customCodeActionSpy } - } + }, + logger }); - return executeActions(actions, modulesProvider, logger).then(result => { + return executeActions(actions, actionsProvider).then(result => { expect(result).toEqual([2, 9]); expect(setHtmlActionSpy).not.toHaveBeenCalled(); expect(appendHtmlActionSpy).toHaveBeenCalledOnceWith( @@ -107,15 +108,16 @@ describe("Personalization::executeActions", () => { logger.enabled = false; const actions = [{ type: "foo", schema: DOM_ACTION }]; - const modulesProvider = createModulesProvider({ + const actionsProvider = createActionsProvider({ modules: { [DOM_ACTION]: { foo: actionSpy } - } + }, + logger }); - return executeActions(actions, modulesProvider, logger).then(result => { + return executeActions(actions, actionsProvider).then(result => { expect(result).toEqual([1]); expect(actionSpy).toHaveBeenCalled(); expect(logger.info.calls.count()).toEqual(0); @@ -131,7 +133,7 @@ describe("Personalization::executeActions", () => { foo: jasmine.createSpy().and.throwError("foo's error") }; - expect(() => executeActions(actions, modules, logger)).toThrowError(); + expect(() => executeActions(actions, modules)).toThrowError(); }); it("should log nothing when there are no actions", () => { @@ -139,7 +141,7 @@ describe("Personalization::executeActions", () => { const actions = []; const modules = {}; - return executeActions(actions, modules, logger).then(result => { + return executeActions(actions, modules).then(result => { expect(result).toEqual([]); expect(logger.info).not.toHaveBeenCalled(); expect(logger.error).not.toHaveBeenCalled(); @@ -153,6 +155,6 @@ describe("Personalization::executeActions", () => { const modules = { foo: () => {} }; - expect(() => executeActions(actions, modules, logger)).toThrowError(); + expect(() => executeActions(actions, modules)).toThrowError(); }); }); From c7f77465f0e048f135c3661fc3f3b3667f57d166 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Wed, 19 Apr 2023 09:44:58 -0600 Subject: [PATCH 07/66] license headers --- .../Personalization/createActionsProvider.js | 11 +++++ .../Personalization/createModules.js | 11 +++++ .../Personalization/createPreprocessors.js | 11 +++++ .../actions/displayBanner.js | 11 +++++ .../actions/displayModal.js | 11 +++++ .../in-app-message-actions/utils.js | 11 +++++ .../createActionsProvider.spec.js | 11 +++++ .../Personalization/createModules.spec.js | 11 +++++ .../createPreprocessors.spec.js | 46 +++++++++++++++++++ .../actions/displayBanner.spec.js | 11 +++++ .../actions/displayModal.spec.js | 11 +++++ .../in-app-message-actions/utils.spec.js | 11 +++++ 12 files changed, 167 insertions(+) diff --git a/src/components/Personalization/createActionsProvider.js b/src/components/Personalization/createActionsProvider.js index 496674276..253ea512f 100644 --- a/src/components/Personalization/createActionsProvider.js +++ b/src/components/Personalization/createActionsProvider.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 createPreprocessors from "./createPreprocessors"; import { assign } from "../../utils"; diff --git a/src/components/Personalization/createModules.js b/src/components/Personalization/createModules.js index a4548d7b9..a0d2817e3 100644 --- a/src/components/Personalization/createModules.js +++ b/src/components/Personalization/createModules.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 { DOM_ACTION, IN_APP_MESSAGE } from "./constants/schema"; import { initDomActionsModules } from "./dom-actions"; import initMessagingActionsModules from "./in-app-message-actions/initMessagingActionsModules"; diff --git a/src/components/Personalization/createPreprocessors.js b/src/components/Personalization/createPreprocessors.js index 18ce13e00..46dc3706c 100644 --- a/src/components/Personalization/createPreprocessors.js +++ b/src/components/Personalization/createPreprocessors.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 { DOM_ACTION } from "./constants/schema"; import remapHeadOffers from "./dom-actions/remapHeadOffers"; import remapCustomCodeOffers from "./dom-actions/remapCustomCodeOffers"; diff --git a/src/components/Personalization/in-app-message-actions/actions/displayBanner.js b/src/components/Personalization/in-app-message-actions/actions/displayBanner.js index 95f010c08..c092616eb 100644 --- a/src/components/Personalization/in-app-message-actions/actions/displayBanner.js +++ b/src/components/Personalization/in-app-message-actions/actions/displayBanner.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 { addStyle, removeElements } from "../utils"; const STYLE_TAG_ID = "alloy-messaging-banner-styles"; diff --git a/src/components/Personalization/in-app-message-actions/actions/displayModal.js b/src/components/Personalization/in-app-message-actions/actions/displayModal.js index d125975b1..63ba952be 100644 --- a/src/components/Personalization/in-app-message-actions/actions/displayModal.js +++ b/src/components/Personalization/in-app-message-actions/actions/displayModal.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 { addStyle, removeElements } from "../utils"; const STYLE_TAG_ID = "alloy-messaging-modal-styles"; diff --git a/src/components/Personalization/in-app-message-actions/utils.js b/src/components/Personalization/in-app-message-actions/utils.js index 24b92fb69..336746076 100644 --- a/src/components/Personalization/in-app-message-actions/utils.js +++ b/src/components/Personalization/in-app-message-actions/utils.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. +*/ export const addStyle = (styleTagId, cssText) => { const existingStyle = document.getElementById(styleTagId); if (existingStyle) { diff --git a/test/unit/specs/components/Personalization/createActionsProvider.spec.js b/test/unit/specs/components/Personalization/createActionsProvider.spec.js index 5322980dd..c3170f431 100644 --- a/test/unit/specs/components/Personalization/createActionsProvider.spec.js +++ b/test/unit/specs/components/Personalization/createActionsProvider.spec.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 createActionsProvider from "../../../../../src/components/Personalization/createActionsProvider"; describe("createActionsProvider", () => { diff --git a/test/unit/specs/components/Personalization/createModules.spec.js b/test/unit/specs/components/Personalization/createModules.spec.js index 9bde44fbb..a005bdbf4 100644 --- a/test/unit/specs/components/Personalization/createModules.spec.js +++ b/test/unit/specs/components/Personalization/createModules.spec.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 createModules from "../../../../../src/components/Personalization/createModules"; import { DOM_ACTION, diff --git a/test/unit/specs/components/Personalization/createPreprocessors.spec.js b/test/unit/specs/components/Personalization/createPreprocessors.spec.js index e69de29bb..3ef888b51 100644 --- a/test/unit/specs/components/Personalization/createPreprocessors.spec.js +++ b/test/unit/specs/components/Personalization/createPreprocessors.spec.js @@ -0,0 +1,46 @@ +/* +Copyright 2023 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +import { DOM_ACTION } from "@adobe/alloy/libEs5/components/Personalization/constants/schema"; +import createPreprocessors from "../../../../../src/components/Personalization/createPreprocessors"; + +describe("Personalization::createPreprocessors", () => { + it("has dom-action preprocessors", () => { + const preprocessors = createPreprocessors(); + + expect(preprocessors).toEqual({ + [DOM_ACTION]: jasmine.any(Array) + }); + + expect(preprocessors[DOM_ACTION].length).toEqual(2); + + preprocessors[DOM_ACTION].forEach(preprocessor => { + expect(preprocessor).toEqual(jasmine.any(Function)); + }); + }); + + it("is structured correctly", () => { + const preprocessors = createPreprocessors(); + + Object.keys(preprocessors).forEach(key => { + expect( + key.startsWith("https://ns.adobe.com/personalization/") + ).toBeTrue(); + }); + + Object.values(preprocessors).forEach(list => { + expect(list instanceof Array).toBeTrue(); + list.forEach(preprocessor => { + expect(preprocessor).toEqual(jasmine.any(Function)); + }); + }); + }); +}); diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js index 99e71613f..25749f090 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.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 displayBanner from "../../../../../../../src/components/Personalization/in-app-message-actions/actions/displayBanner"; import { createNode } from "../../../../../../../src/utils/dom"; import { DIV } from "../../../../../../../src/constants/tagName"; diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js index a11343a37..5ec5782b8 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.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 { createNode } from "../../../../../../../src/utils/dom"; import { DIV } from "../../../../../../../src/constants/tagName"; import displayModal from "../../../../../../../src/components/Personalization/in-app-message-actions/actions/displayModal"; diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/utils.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/utils.spec.js index 70b786d12..79a78af56 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/utils.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/utils.spec.js @@ -1 +1,12 @@ +/* +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. +*/ // TODO From fc01df24d7ac194687807aedf5c28438d3302d62 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Wed, 19 Apr 2023 09:56:34 -0600 Subject: [PATCH 08/66] license headers --- package-lock.json | 44 +++++++++++++++++-- package.json | 2 +- .../DecisioningEngine/createApplyResponse.js | 11 +++++ .../createDecisionProvider.js | 11 +++++ .../createEvaluableRulesetPayload.js | 11 +++++ .../DecisioningEngine/createEventRegistry.js | 11 +++++ .../createOnResponseHandler.js | 11 +++++ src/components/DecisioningEngine/index.js | 11 +++++ .../Personalization/constants/handle.js | 11 +++++ src/utils/flattenArray.js | 11 +++++ src/utils/flattenObject.js | 11 +++++ .../createApplyResponse.spec.js | 11 +++++ .../createDecisionProvider.spec.js | 11 +++++ .../createEvaluableRulesetPayload.spec.js | 11 +++++ .../createEventRegistry.spec.js | 11 +++++ .../createOnResponseHandler.spec.js | 11 +++++ test/unit/specs/utils/flattenArray.spec.js | 11 +++++ test/unit/specs/utils/flattenObject.spec.js | 11 +++++ 18 files changed, 218 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index f2508b316..5ba61e5b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "2.15.0", "license": "Apache-2.0", "dependencies": { - "@adobe/aep-rules-engine": "file:../aepsdk-rulesengine-javascript", + "@adobe/aep-rules-engine": "file:../aepsdk-rulesengine-typescript", "@adobe/reactor-cookie": "^1.0.0", "@adobe/reactor-load-script": "^1.1.1", "@adobe/reactor-object-assign": "^1.0.0", @@ -84,6 +84,44 @@ }, "../aepsdk-rulesengine-javascript": { "name": "@adobe/aep-rules-engine", + "version": "1.0.0", + "extraneous": true, + "license": "Apache-2.0", + "devDependencies": { + "@babel/core": "^7.21.3", + "@babel/preset-env": "^7.20.2", + "@babel/preset-typescript": "^7.21.0", + "@lwc/eslint-plugin-lwc": "^1.6.2", + "@rollup/plugin-babel": "^6.0.3", + "@rollup/plugin-node-resolve": "^15.0.1", + "@rollup/plugin-replace": "^5.0.2", + "@rollup/plugin-terser": "^0.4.0", + "@types/jest": "^29.5.0", + "@typescript-eslint/eslint-plugin": "^5.57.0", + "@typescript-eslint/parser": "^5.57.0", + "babel-jest": "^29.5.0", + "eslint": "^8.36.0", + "eslint-config-airbnb-typescript": "^17.0.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jest": "^27.2.1", + "eslint-plugin-n": "^15.6.1", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-promise": "^6.1.1", + "handlebars": "^4.7.7", + "husky": "^8.0.0", + "jest": "^29.5.0", + "prettier": "^2.8.4", + "pretty-quick": "^3.1.3", + "rimraf": "^4.4.0", + "rollup": "^3.19.1", + "rollup-plugin-cleanup": "^3.2.1", + "rollup-plugin-typescript2": "^0.34.1", + "staged-git-files": "^1.3.0", + "typescript": "^5.0.2" + } + }, + "../aepsdk-rulesengine-typescript": { "version": "1.0.0", "license": "Apache-2.0", "devDependencies": { @@ -121,7 +159,7 @@ } }, "node_modules/@adobe/aep-rules-engine": { - "resolved": "../aepsdk-rulesengine-javascript", + "resolved": "../aepsdk-rulesengine-typescript", "link": true }, "node_modules/@adobe/alloy": { @@ -15326,7 +15364,7 @@ }, "dependencies": { "@adobe/aep-rules-engine": { - "version": "file:../aepsdk-rulesengine-javascript", + "version": "file:../aepsdk-rulesengine-typescript", "requires": { "@babel/core": "^7.21.3", "@babel/preset-env": "^7.20.2", diff --git a/package.json b/package.json index cf41b1eab..077689801 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ } ], "dependencies": { - "@adobe/aep-rules-engine": "file:../aepsdk-rulesengine-javascript", + "@adobe/aep-rules-engine": "file:../aepsdk-rulesengine-typescript", "@adobe/reactor-cookie": "^1.0.0", "@adobe/reactor-load-script": "^1.1.1", "@adobe/reactor-object-assign": "^1.0.0", diff --git a/src/components/DecisioningEngine/createApplyResponse.js b/src/components/DecisioningEngine/createApplyResponse.js index b7f18dc5e..1bf66de01 100644 --- a/src/components/DecisioningEngine/createApplyResponse.js +++ b/src/components/DecisioningEngine/createApplyResponse.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. +*/ export default lifecycle => { return ({ viewName, propositions = [] }) => { if (propositions.length > 0 && lifecycle) { diff --git a/src/components/DecisioningEngine/createDecisionProvider.js b/src/components/DecisioningEngine/createDecisionProvider.js index 7b40e7fd7..204cdf590 100644 --- a/src/components/DecisioningEngine/createDecisionProvider.js +++ b/src/components/DecisioningEngine/createDecisionProvider.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 createEvaluableRulesetPayload from "./createEvaluableRulesetPayload"; export default () => { diff --git a/src/components/DecisioningEngine/createEvaluableRulesetPayload.js b/src/components/DecisioningEngine/createEvaluableRulesetPayload.js index 8ec248117..29366f09b 100644 --- a/src/components/DecisioningEngine/createEvaluableRulesetPayload.js +++ b/src/components/DecisioningEngine/createEvaluableRulesetPayload.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 RulesEngine from "@adobe/aep-rules-engine"; import { JSON_RULESET_ITEM } from "../Personalization/constants/schema"; import flattenArray from "../../utils/flattenArray"; diff --git a/src/components/DecisioningEngine/createEventRegistry.js b/src/components/DecisioningEngine/createEventRegistry.js index 839f60cdf..bd5090413 100644 --- a/src/components/DecisioningEngine/createEventRegistry.js +++ b/src/components/DecisioningEngine/createEventRegistry.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. +*/ export default () => { const events = {}; const rememberEvent = event => { diff --git a/src/components/DecisioningEngine/createOnResponseHandler.js b/src/components/DecisioningEngine/createOnResponseHandler.js index 436340210..ead5b97d3 100644 --- a/src/components/DecisioningEngine/createOnResponseHandler.js +++ b/src/components/DecisioningEngine/createOnResponseHandler.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 { PERSONALIZATION_DECISIONS_HANDLE } from "../Personalization/constants/handle"; import flattenObject from "../../utils/flattenObject"; diff --git a/src/components/DecisioningEngine/index.js b/src/components/DecisioningEngine/index.js index 9af7c4bad..be73a36c0 100644 --- a/src/components/DecisioningEngine/index.js +++ b/src/components/DecisioningEngine/index.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 { noop } from "../../utils"; import createOnResponseHandler from "./createOnResponseHandler"; import createDecisionProvider from "./createDecisionProvider"; diff --git a/src/components/Personalization/constants/handle.js b/src/components/Personalization/constants/handle.js index a37beb080..2d2084e7b 100644 --- a/src/components/Personalization/constants/handle.js +++ b/src/components/Personalization/constants/handle.js @@ -1 +1,12 @@ +/* +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 PERSONALIZATION_DECISIONS_HANDLE = "personalization:decisions"; diff --git a/src/utils/flattenArray.js b/src/utils/flattenArray.js index 5e87b9dec..c0bab7b5d 100644 --- a/src/utils/flattenArray.js +++ b/src/utils/flattenArray.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 flattenArray = (items = []) => { const flat = []; diff --git a/src/utils/flattenObject.js b/src/utils/flattenObject.js index 20c2c64c7..eea2bf997 100644 --- a/src/utils/flattenObject.js +++ b/src/utils/flattenObject.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 isPlainObject = obj => obj !== null && typeof obj === "object" && diff --git a/test/unit/specs/components/DecisioningEngine/createApplyResponse.spec.js b/test/unit/specs/components/DecisioningEngine/createApplyResponse.spec.js index 23d9655a8..af77eb4a8 100644 --- a/test/unit/specs/components/DecisioningEngine/createApplyResponse.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createApplyResponse.spec.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 createApplyResponse from "../../../../../src/components/DecisioningEngine/createApplyResponse"; describe("DecisioningEngine:createApplyResponse", () => { diff --git a/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js b/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js index a2b80d15b..00c7906f7 100644 --- a/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.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 createDecisionProvider from "../../../../../src/components/DecisioningEngine/createDecisionProvider"; describe("DecisioningEngine:createDecisionProvider", () => { diff --git a/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js b/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js index fb5faef0d..c1c80925d 100644 --- a/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.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 RulesEngine from "@adobe/aep-rules-engine"; import createEvaluableRulesetPayload from "../../../../../src/components/DecisioningEngine/createEvaluableRulesetPayload"; diff --git a/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js b/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js index 3b4407401..ded4a7a65 100644 --- a/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.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 createEventRegistry from "../../../../../src/components/DecisioningEngine/createEventRegistry"; describe("DecisioningEngine:createEventRegistry", () => { diff --git a/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js b/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js index 230593519..f4f4abef3 100644 --- a/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.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 createOnResponseHandler from "../../../../../src/components/DecisioningEngine/createOnResponseHandler"; import createDecisionProvider from "../../../../../src/components/DecisioningEngine/createDecisionProvider"; import createApplyResponse from "../../../../../src/components/DecisioningEngine/createApplyResponse"; diff --git a/test/unit/specs/utils/flattenArray.spec.js b/test/unit/specs/utils/flattenArray.spec.js index 1142baeba..b30e2575a 100644 --- a/test/unit/specs/utils/flattenArray.spec.js +++ b/test/unit/specs/utils/flattenArray.spec.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 flattenArray from "../../../../src/utils/flattenArray"; describe("flattenArray", () => { diff --git a/test/unit/specs/utils/flattenObject.spec.js b/test/unit/specs/utils/flattenObject.spec.js index 02d0596b1..794858b99 100644 --- a/test/unit/specs/utils/flattenObject.spec.js +++ b/test/unit/specs/utils/flattenObject.spec.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 flattenObject from "../../../../src/utils/flattenObject"; describe("flattenObject", () => { From e52ba88383d7580ef8443444b7543450210bb9b0 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Wed, 26 Apr 2023 10:39:42 -0600 Subject: [PATCH 09/66] introduced context provider for managing context --- .../createContextProvider.js | 14 ++++++ src/components/DecisioningEngine/index.js | 8 +++- .../createContextProvider.spec.js | 48 +++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 src/components/DecisioningEngine/createContextProvider.js create mode 100644 test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js diff --git a/src/components/DecisioningEngine/createContextProvider.js b/src/components/DecisioningEngine/createContextProvider.js new file mode 100644 index 000000000..21d6a4b53 --- /dev/null +++ b/src/components/DecisioningEngine/createContextProvider.js @@ -0,0 +1,14 @@ +export default ({ eventRegistry }) => { + const globalContext = {}; // holder of global context like current time/date, browser name/version, day of week, scroll position, time on page, etc + const getContext = addedContext => { + return { + ...globalContext, + ...addedContext, + events: eventRegistry.toJSON() + }; + }; + + return { + getContext + }; +}; diff --git a/src/components/DecisioningEngine/index.js b/src/components/DecisioningEngine/index.js index be73a36c0..6baeb9482 100644 --- a/src/components/DecisioningEngine/index.js +++ b/src/components/DecisioningEngine/index.js @@ -14,11 +14,13 @@ import createOnResponseHandler from "./createOnResponseHandler"; import createDecisionProvider from "./createDecisionProvider"; import createApplyResponse from "./createApplyResponse"; import createEventRegistry from "./createEventRegistry"; +import createContextProvider from "./createContextProvider"; const createDecisioningEngine = () => { const eventRegistry = createEventRegistry(); let applyResponse = createApplyResponse(); const decisionProvider = createDecisionProvider(); + const contextProvider = createContextProvider({ eventRegistry }); return { lifecycle: { @@ -37,7 +39,7 @@ const createDecisioningEngine = () => { decisionProvider, applyResponse, event, - decisionContext + decisionContext: contextProvider.getContext(decisionContext) }) ); return; @@ -50,7 +52,9 @@ const createDecisioningEngine = () => { renderDecisions: { run: decisionContext => applyResponse({ - propositions: decisionProvider.evaluate(decisionContext) + propositions: decisionProvider.evaluate( + contextProvider.getContext(decisionContext) + ) }) } } diff --git a/test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js b/test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js new file mode 100644 index 000000000..d5f9c2147 --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js @@ -0,0 +1,48 @@ +/* +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 createContextProvider from "../../../../../src/components/DecisioningEngine/createContextProvider"; +import createEventRegistry from "../../../../../src/components/DecisioningEngine/createEventRegistry"; + +describe("DecisioningEngine:createContextProvider", () => { + let contextProvider; + let eventRegistry; + + it("includes provided context passed in", () => { + eventRegistry = createEventRegistry(); + contextProvider = createContextProvider({ eventRegistry }); + + expect(contextProvider.getContext({ cool: "beans" })).toEqual({ + cool: "beans", + events: {} + }); + }); + + it("includes provided context passed in", () => { + const events = { + abc: { + event: { id: "abc", type: "display" }, + timestamp: new Date().getTime(), + count: 1 + } + }; + + eventRegistry = { + toJSON: () => events + }; + contextProvider = createContextProvider({ eventRegistry }); + + expect(contextProvider.getContext({ cool: "beans" })).toEqual({ + cool: "beans", + events + }); + }); +}); From 571a5b698b06e9382f01f1a91c17d2abafcbde02 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Wed, 26 Apr 2023 10:41:04 -0600 Subject: [PATCH 10/66] license header --- .../DecisioningEngine/createContextProvider.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/DecisioningEngine/createContextProvider.js b/src/components/DecisioningEngine/createContextProvider.js index 21d6a4b53..33b839100 100644 --- a/src/components/DecisioningEngine/createContextProvider.js +++ b/src/components/DecisioningEngine/createContextProvider.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. +*/ export default ({ eventRegistry }) => { const globalContext = {}; // holder of global context like current time/date, browser name/version, day of week, scroll position, time on page, etc const getContext = addedContext => { From 62a8daf91851a9dbf93733bb778af72c6e2e1923 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Wed, 26 Apr 2023 10:39:42 -0600 Subject: [PATCH 11/66] introduced context provider for managing context --- .../createContextProvider.js | 14 ++++++ src/components/DecisioningEngine/index.js | 8 +++- .../createContextProvider.spec.js | 48 +++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 src/components/DecisioningEngine/createContextProvider.js create mode 100644 test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js diff --git a/src/components/DecisioningEngine/createContextProvider.js b/src/components/DecisioningEngine/createContextProvider.js new file mode 100644 index 000000000..21d6a4b53 --- /dev/null +++ b/src/components/DecisioningEngine/createContextProvider.js @@ -0,0 +1,14 @@ +export default ({ eventRegistry }) => { + const globalContext = {}; // holder of global context like current time/date, browser name/version, day of week, scroll position, time on page, etc + const getContext = addedContext => { + return { + ...globalContext, + ...addedContext, + events: eventRegistry.toJSON() + }; + }; + + return { + getContext + }; +}; diff --git a/src/components/DecisioningEngine/index.js b/src/components/DecisioningEngine/index.js index be73a36c0..6baeb9482 100644 --- a/src/components/DecisioningEngine/index.js +++ b/src/components/DecisioningEngine/index.js @@ -14,11 +14,13 @@ import createOnResponseHandler from "./createOnResponseHandler"; import createDecisionProvider from "./createDecisionProvider"; import createApplyResponse from "./createApplyResponse"; import createEventRegistry from "./createEventRegistry"; +import createContextProvider from "./createContextProvider"; const createDecisioningEngine = () => { const eventRegistry = createEventRegistry(); let applyResponse = createApplyResponse(); const decisionProvider = createDecisionProvider(); + const contextProvider = createContextProvider({ eventRegistry }); return { lifecycle: { @@ -37,7 +39,7 @@ const createDecisioningEngine = () => { decisionProvider, applyResponse, event, - decisionContext + decisionContext: contextProvider.getContext(decisionContext) }) ); return; @@ -50,7 +52,9 @@ const createDecisioningEngine = () => { renderDecisions: { run: decisionContext => applyResponse({ - propositions: decisionProvider.evaluate(decisionContext) + propositions: decisionProvider.evaluate( + contextProvider.getContext(decisionContext) + ) }) } } diff --git a/test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js b/test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js new file mode 100644 index 000000000..d5f9c2147 --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js @@ -0,0 +1,48 @@ +/* +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 createContextProvider from "../../../../../src/components/DecisioningEngine/createContextProvider"; +import createEventRegistry from "../../../../../src/components/DecisioningEngine/createEventRegistry"; + +describe("DecisioningEngine:createContextProvider", () => { + let contextProvider; + let eventRegistry; + + it("includes provided context passed in", () => { + eventRegistry = createEventRegistry(); + contextProvider = createContextProvider({ eventRegistry }); + + expect(contextProvider.getContext({ cool: "beans" })).toEqual({ + cool: "beans", + events: {} + }); + }); + + it("includes provided context passed in", () => { + const events = { + abc: { + event: { id: "abc", type: "display" }, + timestamp: new Date().getTime(), + count: 1 + } + }; + + eventRegistry = { + toJSON: () => events + }; + contextProvider = createContextProvider({ eventRegistry }); + + expect(contextProvider.getContext({ cool: "beans" })).toEqual({ + cool: "beans", + events + }); + }); +}); From 395c2a63aba4dd6108e4c9ae26cf7ccff14f819a Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Wed, 26 Apr 2023 10:41:04 -0600 Subject: [PATCH 12/66] license header --- .../DecisioningEngine/createContextProvider.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/DecisioningEngine/createContextProvider.js b/src/components/DecisioningEngine/createContextProvider.js index 21d6a4b53..33b839100 100644 --- a/src/components/DecisioningEngine/createContextProvider.js +++ b/src/components/DecisioningEngine/createContextProvider.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. +*/ export default ({ eventRegistry }) => { const globalContext = {}; // holder of global context like current time/date, browser name/version, day of week, scroll position, time on page, etc const getContext = addedContext => { From c3f2ed5dd3bbae210c90e0e61bcc58efd44ded3b Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Fri, 28 Apr 2023 17:01:11 -0600 Subject: [PATCH 13/66] message feed POC --- package-lock.json | 3 +- .../createDecisionHistory.js | 19 ++ .../createDecisionProvider.js | 11 +- .../createEvaluableRulesetPayload.js | 20 +- .../DecisioningEngine/createEventRegistry.js | 31 +- src/components/DecisioningEngine/index.js | 16 +- src/components/DecisioningEngine/utils.js | 22 ++ .../Personalization/createCollect.js | 17 +- .../Personalization/createComponent.js | 7 +- .../createSubscribeMessageFeed.js | 113 ++++++ .../initMessagingActionsModules.js | 4 +- src/components/Personalization/index.js | 10 +- src/utils/debounce.js | 12 + test/functional/helpers/createAlloyProxy.js | 3 +- .../createContextProvider.spec.js | 8 +- .../createDecisionHistory.spec.js | 1 + .../createDecisionProvider.spec.js | 40 ++- .../createEvaluableRulesetPayload.spec.js | 323 +++++++----------- .../createEventRegistry.spec.js | 37 +- .../createOnResponseHandler.spec.js | 25 +- .../DecisioningEngine/utils.spec.js | 1 + .../Personalization/createComponent.spec.js | 5 +- .../Personalization/createModules.spec.js | 6 +- .../createSubscribeMessageFeed.spec.js | 1 + test/unit/specs/utils/debounce.spec.js | 1 + 25 files changed, 470 insertions(+), 266 deletions(-) create mode 100644 src/components/DecisioningEngine/createDecisionHistory.js create mode 100644 src/components/DecisioningEngine/utils.js create mode 100644 src/components/Personalization/createSubscribeMessageFeed.js create mode 100644 src/utils/debounce.js create mode 100644 test/unit/specs/components/DecisioningEngine/createDecisionHistory.spec.js create mode 100644 test/unit/specs/components/DecisioningEngine/utils.spec.js create mode 100644 test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js create mode 100644 test/unit/specs/utils/debounce.spec.js diff --git a/package-lock.json b/package-lock.json index bfe390d87..916f5d112 100644 --- a/package-lock.json +++ b/package-lock.json @@ -122,7 +122,8 @@ } }, "../aepsdk-rulesengine-typescript": { - "version": "1.0.0", + "name": "@adobe/aep-rules-engine", + "version": "2.0.1", "license": "Apache-2.0", "devDependencies": { "@babel/core": "^7.21.3", diff --git a/src/components/DecisioningEngine/createDecisionHistory.js b/src/components/DecisioningEngine/createDecisionHistory.js new file mode 100644 index 000000000..d2cd0c19f --- /dev/null +++ b/src/components/DecisioningEngine/createDecisionHistory.js @@ -0,0 +1,19 @@ +import { createRestoreStorage, createSaveStorage } from "./utils"; + +const STORAGE_KEY = "history"; +export default ({ storage }) => { + const restore = createRestoreStorage(storage, STORAGE_KEY); + const save = createSaveStorage(storage, STORAGE_KEY); + + const history = restore({}); + + const recordDecision = id => { + if (typeof history[id] !== "number") { + history[id] = new Date().getTime(); + save(history); + } + return history[id]; + }; + + return { recordDecision }; +}; diff --git a/src/components/DecisioningEngine/createDecisionProvider.js b/src/components/DecisioningEngine/createDecisionProvider.js index 204cdf590..ab3c1ceb6 100644 --- a/src/components/DecisioningEngine/createDecisionProvider.js +++ b/src/components/DecisioningEngine/createDecisionProvider.js @@ -10,16 +10,23 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ import createEvaluableRulesetPayload from "./createEvaluableRulesetPayload"; +import createDecisionHistory from "./createDecisionHistory"; -export default () => { +export default ({ eventRegistry, storage }) => { const payloads = {}; + const decisionHistory = createDecisionHistory({ storage }); + const addPayload = payload => { if (!payload.id) { return; } - const evaluableRulesetPayload = createEvaluableRulesetPayload(payload); + const evaluableRulesetPayload = createEvaluableRulesetPayload( + payload, + eventRegistry, + decisionHistory + ); if (evaluableRulesetPayload.isEvaluable) { payloads[payload.id] = evaluableRulesetPayload; diff --git a/src/components/DecisioningEngine/createEvaluableRulesetPayload.js b/src/components/DecisioningEngine/createEvaluableRulesetPayload.js index 29366f09b..18778d449 100644 --- a/src/components/DecisioningEngine/createEvaluableRulesetPayload.js +++ b/src/components/DecisioningEngine/createEvaluableRulesetPayload.js @@ -12,8 +12,9 @@ governing permissions and limitations under the License. import RulesEngine from "@adobe/aep-rules-engine"; import { JSON_RULESET_ITEM } from "../Personalization/constants/schema"; import flattenArray from "../../utils/flattenArray"; +import { DISPLAY } from "../Personalization/constants/eventType"; -export default payload => { +export default (payload, eventRegistry, decisionHistory) => { const items = []; const addItem = item => { @@ -28,11 +29,22 @@ export default payload => { }; const evaluate = context => { + const displayEvent = eventRegistry.getEvent(DISPLAY, payload.id); + + const displayedDate = displayEvent + ? displayEvent.firstTimestamp + : undefined; + + const qualifyingItems = flattenArray( + items.map(item => item.execute(context)) + ).map(item => { + const qualifiedDate = decisionHistory.recordDecision(item.id); + return { ...item.detail, qualifiedDate, displayedDate }; + }); + return { ...payload, - items: flattenArray(items.map(item => item.execute(context))).map( - item => item.detail - ) + items: qualifyingItems }; }; diff --git a/src/components/DecisioningEngine/createEventRegistry.js b/src/components/DecisioningEngine/createEventRegistry.js index bd5090413..c876f18c1 100644 --- a/src/components/DecisioningEngine/createEventRegistry.js +++ b/src/components/DecisioningEngine/createEventRegistry.js @@ -9,8 +9,17 @@ 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 () => { - const events = {}; +import { createRestoreStorage, createSaveStorage } from "./utils"; + +const STORAGE_KEY = "events"; +const eventKey = (eventType, eventId) => `${eventType}|${eventId}`; + +export default ({ storage }) => { + const restore = createRestoreStorage(storage, STORAGE_KEY); + const save = createSaveStorage(storage, STORAGE_KEY, 150); + + const events = restore({}); + const rememberEvent = event => { const { xdm = {} } = event.getContent(); const { eventType = "", _experience } = xdm; @@ -28,21 +37,29 @@ export default () => { const { propositions = [] } = decisioning; propositions.forEach(proposition => { + const key = eventKey(eventType, proposition.id); let count = 0; - const existingEvent = events[proposition.id]; + const timestamp = new Date().getTime(); + let firstTimestamp = timestamp; + + const existingEvent = events[key]; if (existingEvent) { count = existingEvent.count; + firstTimestamp = + existingEvent.firstTimestamp || existingEvent.timestamp; } - events[proposition.id] = { + events[key] = { event: { id: proposition.id, type: eventType }, - timestamp: new Date().getTime(), + firstTimestamp, + timestamp, count: count + 1 }; }); - }; - const getEvent = eventId => events[eventId]; + save(events); + }; + const getEvent = (eventType, eventId) => events[eventKey(eventType, eventId)]; return { rememberEvent, getEvent, toJSON: () => events }; }; diff --git a/src/components/DecisioningEngine/index.js b/src/components/DecisioningEngine/index.js index 6baeb9482..eb356227d 100644 --- a/src/components/DecisioningEngine/index.js +++ b/src/components/DecisioningEngine/index.js @@ -9,17 +9,25 @@ 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 { noop } from "../../utils"; +import { noop, sanitizeOrgIdForCookieName } from "../../utils"; import createOnResponseHandler from "./createOnResponseHandler"; import createDecisionProvider from "./createDecisionProvider"; import createApplyResponse from "./createApplyResponse"; import createEventRegistry from "./createEventRegistry"; import createContextProvider from "./createContextProvider"; -const createDecisioningEngine = () => { - const eventRegistry = createEventRegistry(); +const createDecisioningEngine = ({ config, createNamespacedStorage }) => { + const { orgId } = config; + const storage = createNamespacedStorage( + `${sanitizeOrgIdForCookieName(orgId)}.decisioning.` + ); + + const eventRegistry = createEventRegistry({ storage: storage.persistent }); let applyResponse = createApplyResponse(); - const decisionProvider = createDecisionProvider(); + const decisionProvider = createDecisionProvider({ + eventRegistry, + storage: storage.persistent + }); const contextProvider = createContextProvider({ eventRegistry }); return { diff --git a/src/components/DecisioningEngine/utils.js b/src/components/DecisioningEngine/utils.js new file mode 100644 index 000000000..8830ebfc7 --- /dev/null +++ b/src/components/DecisioningEngine/utils.js @@ -0,0 +1,22 @@ +import debounce from "../../utils/debounce"; + +export const createRestoreStorage = (storage, storageKey) => { + return defaultValue => { + const stored = storage.getItem(storageKey); + if (!stored) { + return defaultValue; + } + + try { + return JSON.parse(stored); + } catch (e) { + return defaultValue; + } + }; +}; + +export const createSaveStorage = (storage, storageKey, debounceDelay = 150) => { + return debounce(value => { + storage.setItem(storageKey, JSON.stringify(value)); + }, debounceDelay); +}; diff --git a/src/components/Personalization/createCollect.js b/src/components/Personalization/createCollect.js index 809029318..b9520be35 100644 --- a/src/components/Personalization/createCollect.js +++ b/src/components/Personalization/createCollect.js @@ -15,9 +15,14 @@ import { isNonEmptyArray } from "../../utils"; export default ({ eventManager, mergeDecisionsMeta }) => { // Called when a decision is auto-rendered for the __view__ scope or a SPA view(display and empty display notification) - return ({ decisionsMeta = [], documentMayUnload = false, viewName }) => { + return ({ + decisionsMeta = [], + documentMayUnload = false, + eventType = DISPLAY, + viewName + }) => { const event = eventManager.createEvent(); - const data = { eventType: DISPLAY }; + const data = { eventType }; if (viewName) { data.web = { @@ -25,7 +30,13 @@ export default ({ eventManager, mergeDecisionsMeta }) => { }; } if (isNonEmptyArray(decisionsMeta)) { - mergeDecisionsMeta(event, decisionsMeta, PropositionEventType.DISPLAY); + mergeDecisionsMeta( + event, + decisionsMeta, + eventType === DISPLAY + ? PropositionEventType.DISPLAY + : PropositionEventType.INTERACT + ); } event.mergeXdm(data); diff --git a/src/components/Personalization/createComponent.js b/src/components/Personalization/createComponent.js index d5e1f7a90..bed029ab4 100644 --- a/src/components/Personalization/createComponent.js +++ b/src/components/Personalization/createComponent.js @@ -27,11 +27,13 @@ export default ({ viewCache, showContainers, applyPropositions, - setTargetMigration + setTargetMigration, + subscribeMessageFeed }) => { return { lifecycle: { onDecision({ viewName, propositions }) { + subscribeMessageFeed.refresh(propositions); autoRenderingHandler({ viewName, pageWideScopeDecisions: propositions, @@ -105,7 +107,8 @@ export default ({ optionsValidator: options => validateApplyPropositionsOptions({ logger, options }), run: applyPropositions - } + }, + subscribeMessageFeed: subscribeMessageFeed.command } }; }; diff --git a/src/components/Personalization/createSubscribeMessageFeed.js b/src/components/Personalization/createSubscribeMessageFeed.js new file mode 100644 index 000000000..82622b269 --- /dev/null +++ b/src/components/Personalization/createSubscribeMessageFeed.js @@ -0,0 +1,113 @@ +/* eslint-disable */ +import { callback, objectOf, string } from "../../utils/validation"; +import { IN_APP_MESSAGE } from "./constants/schema"; +import { DISPLAY, INTERACT } from "./constants/eventType"; + +const validateSubscribeMessageFeedOptions = ({ options }) => { + const validator = objectOf({ + surface: string().required(), + callback: callback().required() + }).noUnknownFields(); + + return validator(options); +}; + +export default ({ collect }) => { + let subscriptionHandler; + let surfaceIdentifier; + const run = ({ surface, callback }) => { + subscriptionHandler = callback; + surfaceIdentifier = surface; + }; + + const optionsValidator = options => + validateSubscribeMessageFeedOptions({ options }); + + const createFeedItem = (payload, item) => { + const { id, scope, scopeDetails } = payload; + + const { data = {}, qualifiedDate, displayedDate } = item; + const { content = {} } = data; + + return { + ...content, + qualifiedDate, + displayedDate, + getSurface: () => item.meta.surface, + getAnalyticsDetail: () => { + return { id, scope, scopeDetails }; + } + }; + }; + + const renderedSet = new Set(); + + const clicked = (items = []) => { + const decisionsMeta = []; + const clickedSet = new Set(); + + items.forEach(item => { + const analyticsMeta = item.getAnalyticsDetail(); + if (!clickedSet.has(analyticsMeta.id)) { + decisionsMeta.push(analyticsMeta); + clickedSet.add(analyticsMeta.id); + } + }); + + if (decisionsMeta.length > 0) { + collect({ decisionsMeta, eventType: INTERACT, documentMayUnload: true }); + } + }; + + const rendered = (items = []) => { + const decisionsMeta = []; + + items.forEach(item => { + const analyticsMeta = item.getAnalyticsDetail(); + if (!renderedSet.has(analyticsMeta.id)) { + decisionsMeta.push(analyticsMeta); + renderedSet.add(analyticsMeta.id); + } + }); + + if (decisionsMeta.length > 0) { + collect({ decisionsMeta, eventType: DISPLAY }); + } + }; + + const refresh = propositions => { + if (!subscriptionHandler || !surfaceIdentifier) { + return; + } + + const result = propositions + .filter(payload => payload.scope === surfaceIdentifier) + .reduce((allItems, payload) => { + const { items = [] } = payload; + + return [ + ...allItems, + ...items + .filter( + item => + item.schema === IN_APP_MESSAGE && item.data.type === "feed" + ) + .map(item => createFeedItem(payload, item)) + ]; + }, []) + .sort( + (a, b) => + b.qualifiedDate - a.qualifiedDate || b.publishedDate - a.publishedDate + ); + + subscriptionHandler.call(null, { items: result, clicked, rendered }); + }; + + return { + refresh, + command: { + optionsValidator, + run + } + }; +}; diff --git a/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js b/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js index 4beea254f..b968d36df 100644 --- a/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js +++ b/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js @@ -2,10 +2,12 @@ import displayModal from "./actions/displayModal"; import displayBanner from "./actions/displayBanner"; +import noop from "../../../utils/noop"; export default store => { return { modal: settings => displayModal(settings), - banner: settings => displayBanner(settings) + banner: settings => displayBanner(settings), + feed: () => Promise.resolve() // TODO: consider not using in-app-message type here, or leveraging this for some purpose }; }; diff --git a/src/components/Personalization/index.js b/src/components/Personalization/index.js index 93507579e..4719b20bf 100644 --- a/src/components/Personalization/index.js +++ b/src/components/Personalization/index.js @@ -35,6 +35,7 @@ import createActionsProvider from "./createActionsProvider"; import executeActions from "./executeActions"; import createModules from "./createModules"; import createPreprocessors from "./createPreprocessors"; +import createSubscribeMessageFeed from "./createSubscribeMessageFeed"; const createPersonalization = ({ config, logger, eventManager }) => { const { targetMigrationEnabled, prehidingStyle } = config; @@ -89,6 +90,7 @@ const createPersonalization = ({ config, logger, eventManager }) => { mergeQuery }); const onClickHandler = createOnClickHandler({ + eventManager, mergeDecisionsMeta, collectClicks, getClickSelectors, @@ -103,6 +105,11 @@ const createPersonalization = ({ config, logger, eventManager }) => { const setTargetMigration = createSetTargetMigration({ targetMigrationEnabled }); + + const subscribeMessageFeed = createSubscribeMessageFeed({ + collect + }); + return createComponent({ getPageLocation, logger, @@ -115,7 +122,8 @@ const createPersonalization = ({ config, logger, eventManager }) => { viewCache, showContainers, applyPropositions, - setTargetMigration + setTargetMigration, + subscribeMessageFeed }); }; diff --git a/src/utils/debounce.js b/src/utils/debounce.js new file mode 100644 index 000000000..ab930d0d4 --- /dev/null +++ b/src/utils/debounce.js @@ -0,0 +1,12 @@ +export default (fn, delay = 150) => { + let timer; + return (...args) => { + if (timer) { + clearTimeout(timer); + } + + timer = setTimeout(() => { + fn(...args); + }, delay); + }; +}; diff --git a/test/functional/helpers/createAlloyProxy.js b/test/functional/helpers/createAlloyProxy.js index 6efc74d7c..d8617a891 100644 --- a/test/functional/helpers/createAlloyProxy.js +++ b/test/functional/helpers/createAlloyProxy.js @@ -90,7 +90,8 @@ const commands = [ "setDebug", "getLibraryInfo", "appendIdentityToUrl", - "applyPropositions" + "applyPropositions", + "subscribeMessageFeed" ]; export default (instanceName = "alloy") => { diff --git a/test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js b/test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js index d5f9c2147..075a51c2d 100644 --- a/test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js @@ -16,8 +16,14 @@ describe("DecisioningEngine:createContextProvider", () => { let contextProvider; let eventRegistry; + let storage; + + beforeEach(() => { + storage = jasmine.createSpyObj("storage", ["getItem", "setItem", "clear"]); + }); + it("includes provided context passed in", () => { - eventRegistry = createEventRegistry(); + eventRegistry = createEventRegistry({ storage }); contextProvider = createContextProvider({ eventRegistry }); expect(contextProvider.getContext({ cool: "beans" })).toEqual({ diff --git a/test/unit/specs/components/DecisioningEngine/createDecisionHistory.spec.js b/test/unit/specs/components/DecisioningEngine/createDecisionHistory.spec.js new file mode 100644 index 000000000..70b786d12 --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/createDecisionHistory.spec.js @@ -0,0 +1 @@ +// TODO diff --git a/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js b/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js index 00c7906f7..20f1b7db0 100644 --- a/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js @@ -10,12 +10,18 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ import createDecisionProvider from "../../../../../src/components/DecisioningEngine/createDecisionProvider"; +import createEventRegistry from "../../../../../src/components/DecisioningEngine/createEventRegistry"; describe("DecisioningEngine:createDecisionProvider", () => { let decisionProvider; + let storage; + let eventRegistry; beforeEach(() => { - decisionProvider = createDecisionProvider(); + storage = jasmine.createSpyObj("storage", ["getItem", "setItem", "clear"]); + eventRegistry = createEventRegistry({ storage }); + + decisionProvider = createDecisionProvider({ eventRegistry, storage }); decisionProvider.addPayloads([ { scopeDetails: { @@ -252,7 +258,9 @@ describe("DecisioningEngine:createDecisionProvider", () => { prehidingSelector: "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" }, - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined }, { schema: "https://ns.adobe.com/personalization/dom-action", @@ -263,7 +271,9 @@ describe("DecisioningEngine:createDecisionProvider", () => { prehidingSelector: "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined } ], scope: "web://mywebsite.com" @@ -302,7 +312,9 @@ describe("DecisioningEngine:createDecisionProvider", () => { content: "i can haz?", prehidingSelector: "div#spa #spa-content h3" }, - id: "8a0d7a45-70fb-4845-a093-2133b5744c8d" + id: "8a0d7a45-70fb-4845-a093-2133b5744c8d", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined }, { schema: "https://ns.adobe.com/personalization/dom-action", @@ -312,7 +324,9 @@ describe("DecisioningEngine:createDecisionProvider", () => { content: "ALL YOUR BASE ARE BELONG TO US", prehidingSelector: "div#spa #spa-content p" }, - id: "a44af51a-e073-4e8c-92e1-84ac28210043" + id: "a44af51a-e073-4e8c-92e1-84ac28210043", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined } ], scope: "web://mywebsite.com" @@ -358,7 +372,9 @@ describe("DecisioningEngine:createDecisionProvider", () => { prehidingSelector: "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" }, - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined }, { schema: "https://ns.adobe.com/personalization/dom-action", @@ -369,7 +385,9 @@ describe("DecisioningEngine:createDecisionProvider", () => { prehidingSelector: "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined } ], scope: "web://mywebsite.com" @@ -402,7 +420,9 @@ describe("DecisioningEngine:createDecisionProvider", () => { content: "i can haz?", prehidingSelector: "div#spa #spa-content h3" }, - id: "8a0d7a45-70fb-4845-a093-2133b5744c8d" + id: "8a0d7a45-70fb-4845-a093-2133b5744c8d", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined }, { schema: "https://ns.adobe.com/personalization/dom-action", @@ -412,7 +432,9 @@ describe("DecisioningEngine:createDecisionProvider", () => { content: "ALL YOUR BASE ARE BELONG TO US", prehidingSelector: "div#spa #spa-content p" }, - id: "a44af51a-e073-4e8c-92e1-84ac28210043" + id: "a44af51a-e073-4e8c-92e1-84ac28210043", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined } ], scope: "web://mywebsite.com" diff --git a/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js b/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js index c1c80925d..de6d66c1d 100644 --- a/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js @@ -9,231 +9,132 @@ 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 RulesEngine from "@adobe/aep-rules-engine"; import createEvaluableRulesetPayload from "../../../../../src/components/DecisioningEngine/createEvaluableRulesetPayload"; +import createEventRegistry from "../../../../../src/components/DecisioningEngine/createEventRegistry"; +import createDecisionHistory from "../../../../../src/components/DecisioningEngine/createDecisionHistory"; describe("DecisioningEngine:createEvaluableRulesetPayload", () => { - it("does", () => { - const ruleset = RulesEngine({ - version: 1, - rules: [ - { - condition: { - definition: { - conditions: [ - { - definition: { - conditions: [ - { - definition: { - key: "color", - matcher: "eq", - values: ["orange", "blue"] - }, - type: "matcher" - }, - { - definition: { - key: "action", - matcher: "eq", - values: ["lipstick"] - }, - type: "matcher" - } - ], - logic: "and" - }, - type: "group" - } - ], - logic: "and" - }, - type: "group" + let storage; + let eventRegistry; + let decisionHistory; + + beforeEach(() => { + storage = jasmine.createSpyObj("storage", ["getItem", "setItem", "clear"]); + eventRegistry = createEventRegistry({ storage }); + decisionHistory = createDecisionHistory({ storage }); + }); + + it("evaluates rules and return a payload", () => { + const evaluableRulesetPayload = createEvaluableRulesetPayload( + { + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: "abc" }, - consequences: [ - { - type: "item", - detail: { - schema: "https://ns.adobe.com/personalization/dom-action", - data: { - selector: - "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", - type: "setAttribute", - content: { - src: "img/demo-marketing-offer1-exp-A.png" - }, - prehidingSelector: - "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" - }, - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" - }, - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" - }, + strategies: [ { - type: "item", - detail: { - schema: "https://ns.adobe.com/personalization/dom-action", - data: { - selector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", - type: "setHtml", - content: "Hello Treatment A!", - prehidingSelector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" - }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" - }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" } - ] - } - ] - }); - - expect(ruleset.execute({ color: "orange", action: "lipstick" })).toEqual([ - [ - { - type: "item", - detail: { - schema: "https://ns.adobe.com/personalization/dom-action", - data: { - selector: "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", - type: "setAttribute", - content: { - src: "img/demo-marketing-offer1-exp-A.png" - }, - prehidingSelector: - "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" - }, - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" - }, - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" - }, - { - type: "item", - detail: { - schema: "https://ns.adobe.com/personalization/dom-action", - data: { - selector: "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", - type: "setHtml", - content: "Hello Treatment A!", - prehidingSelector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" - }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" - } - ] - ]); - }); - it("works", () => { - const evaluableRulesetPayload = createEvaluableRulesetPayload({ - scopeDetails: { - decisionProvider: "AJO", - characteristics: { - eventToken: "abc" + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" }, - strategies: [ + id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", + items: [ { - strategyID: "3VQe3oIqiYq2RAsYzmDTSf", - treatmentID: "yu7rkogezumca7i0i44v" - } - ], - activity: { - id: - "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" - }, - correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" - }, - id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", - items: [ - { - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", - schema: "https://ns.adobe.com/personalization/json-ruleset-item", - data: { - content: JSON.stringify({ - version: 1, - rules: [ - { - condition: { - definition: { - conditions: [ - { - definition: { - conditions: [ - { - definition: { - key: "color", - matcher: "eq", - values: ["orange", "blue"] - }, - type: "matcher" - }, - { - definition: { - key: "action", - matcher: "eq", - values: ["lipstick"] + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", + schema: "https://ns.adobe.com/personalization/json-ruleset-item", + data: { + content: JSON.stringify({ + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [ + { + definition: { + conditions: [ + { + definition: { + key: "color", + matcher: "eq", + values: ["orange", "blue"] + }, + type: "matcher" }, - type: "matcher" - } - ], - logic: "and" - }, - type: "group" - } - ], - logic: "and" + { + definition: { + key: "action", + matcher: "eq", + values: ["lipstick"] + }, + type: "matcher" + } + ], + logic: "and" + }, + type: "group" + } + ], + logic: "and" + }, + type: "group" }, - type: "group" - }, - consequences: [ - { - type: "item", - detail: { - schema: - "https://ns.adobe.com/personalization/dom-action", - data: { - selector: - "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", - type: "setAttribute", - content: { - src: "img/demo-marketing-offer1-exp-A.png" + consequences: [ + { + type: "item", + detail: { + schema: + "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" + }, + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" }, - prehidingSelector: - "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" }, id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" }, - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" - }, - { - type: "item", - detail: { - schema: - "https://ns.adobe.com/personalization/dom-action", - data: { - selector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", - type: "setHtml", - content: "Hello Treatment A!", - prehidingSelector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" + { + type: "item", + detail: { + schema: + "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" }, id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" - }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" - } - ] - } - ] - }) + } + ] + } + ] + }) + } } - } - ], - scope: "web://mywebsite.com" - }); + ], + scope: "web://mywebsite.com" + }, + eventRegistry, + decisionHistory + ); expect( evaluableRulesetPayload.evaluate({ color: "orange", action: "lipstick" }) @@ -268,7 +169,9 @@ describe("DecisioningEngine:createEvaluableRulesetPayload", () => { prehidingSelector: "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" }, - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined }, { schema: "https://ns.adobe.com/personalization/dom-action", @@ -279,7 +182,9 @@ describe("DecisioningEngine:createEvaluableRulesetPayload", () => { prehidingSelector: "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined } ], scope: "web://mywebsite.com" diff --git a/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js b/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js index ded4a7a65..761fe7446 100644 --- a/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js @@ -12,8 +12,14 @@ governing permissions and limitations under the License. import createEventRegistry from "../../../../../src/components/DecisioningEngine/createEventRegistry"; describe("DecisioningEngine:createEventRegistry", () => { + let storage; + + beforeEach(() => { + storage = jasmine.createSpyObj("storage", ["getItem", "setItem", "clear"]); + }); + it("registers events", () => { - const eventRegistry = createEventRegistry(); + const eventRegistry = createEventRegistry({ storage }); const getContent = () => ({ xdm: { @@ -33,18 +39,21 @@ describe("DecisioningEngine:createEventRegistry", () => { eventRegistry.rememberEvent(event); expect(eventRegistry.toJSON()).toEqual({ - abc: { + "display|abc": { event: { id: "abc", type: "display" }, + firstTimestamp: jasmine.any(Number), timestamp: jasmine.any(Number), count: 1 }, - def: { + "display|def": { event: { id: "def", type: "display" }, + firstTimestamp: jasmine.any(Number), timestamp: jasmine.any(Number), count: 1 }, - ghi: { + "display|ghi": { event: { id: "ghi", type: "display" }, + firstTimestamp: jasmine.any(Number), timestamp: jasmine.any(Number), count: 1 } @@ -52,7 +61,7 @@ describe("DecisioningEngine:createEventRegistry", () => { }); it("does not register invalid events", () => { - const eventRegistry = createEventRegistry(); + const eventRegistry = createEventRegistry({ storage }); eventRegistry.rememberEvent({ getContent: () => ({ @@ -87,7 +96,7 @@ describe("DecisioningEngine:createEventRegistry", () => { }); it("increments count and sets timestamp", done => { - const eventRegistry = createEventRegistry(); + const eventRegistry = createEventRegistry({ storage }); const getContent = () => ({ xdm: { @@ -106,27 +115,29 @@ describe("DecisioningEngine:createEventRegistry", () => { let lastEventTime = 0; eventRegistry.rememberEvent(event); - expect(eventRegistry.getEvent("abc")).toEqual({ + expect(eventRegistry.getEvent("display", "abc")).toEqual({ event: { id: "abc", type: "display" }, + firstTimestamp: jasmine.any(Number), timestamp: jasmine.any(Number), count: 1 }); - expect(eventRegistry.getEvent("abc").timestamp).toBeGreaterThan( + expect(eventRegistry.getEvent("display", "abc").timestamp).toBeGreaterThan( lastEventTime ); - lastEventTime = eventRegistry.getEvent("abc").timestamp; + lastEventTime = eventRegistry.getEvent("display", "abc").timestamp; setTimeout(() => { eventRegistry.rememberEvent(event); // again - expect(eventRegistry.getEvent("abc")).toEqual({ + expect(eventRegistry.getEvent("display", "abc")).toEqual({ event: { id: "abc", type: "display" }, + firstTimestamp: jasmine.any(Number), timestamp: jasmine.any(Number), count: 2 }); - expect(eventRegistry.getEvent("abc").timestamp).toBeGreaterThan( - lastEventTime - ); + expect( + eventRegistry.getEvent("display", "abc").timestamp + ).toBeGreaterThan(lastEventTime); done(); }, 10); }); diff --git a/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js b/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js index f4f4abef3..454d86a2e 100644 --- a/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js @@ -12,14 +12,23 @@ governing permissions and limitations under the License. import createOnResponseHandler from "../../../../../src/components/DecisioningEngine/createOnResponseHandler"; import createDecisionProvider from "../../../../../src/components/DecisioningEngine/createDecisionProvider"; import createApplyResponse from "../../../../../src/components/DecisioningEngine/createApplyResponse"; +import createEventRegistry from "../../../../../src/components/DecisioningEngine/createEventRegistry"; describe("DecisioningEngine:createOnResponseHandler", () => { + let eventRegistry; + let storage; + + beforeEach(() => { + storage = jasmine.createSpyObj("storage", ["getItem", "setItem", "clear"]); + eventRegistry = createEventRegistry({ storage }); + }); + it("calls lifecycle.onDecision with propositions based on decisionContext", () => { const lifecycle = jasmine.createSpyObj("lifecycle", { onDecision: Promise.resolve() }); - const decisionProvider = createDecisionProvider(); + const decisionProvider = createDecisionProvider({ eventRegistry, storage }); const applyResponse = createApplyResponse(lifecycle); const event = { @@ -175,7 +184,9 @@ describe("DecisioningEngine:createOnResponseHandler", () => { prehidingSelector: "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" }, - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined }, { schema: "https://ns.adobe.com/personalization/dom-action", @@ -187,7 +198,9 @@ describe("DecisioningEngine:createOnResponseHandler", () => { prehidingSelector: "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined } ], scope: "web://target.jasonwaters.dev/aep.html" @@ -201,7 +214,7 @@ describe("DecisioningEngine:createOnResponseHandler", () => { onDecision: Promise.resolve() }); - const decisionProvider = createDecisionProvider(); + const decisionProvider = createDecisionProvider({ eventRegistry, storage }); const applyResponse = createApplyResponse(lifecycle); const event = { @@ -342,7 +355,9 @@ describe("DecisioningEngine:createOnResponseHandler", () => { prehidingSelector: "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined } ], scope: "web://target.jasonwaters.dev/aep.html" diff --git a/test/unit/specs/components/DecisioningEngine/utils.spec.js b/test/unit/specs/components/DecisioningEngine/utils.spec.js new file mode 100644 index 000000000..70b786d12 --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/utils.spec.js @@ -0,0 +1 @@ +// TODO diff --git a/test/unit/specs/components/Personalization/createComponent.spec.js b/test/unit/specs/components/Personalization/createComponent.spec.js index fcb8d92d3..9b38ef0be 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 subscribeMessageFeed; const build = () => { personalizationComponent = createComponent({ @@ -35,7 +36,8 @@ describe("Personalization", () => { mergeQuery, viewCache, showContainers, - setTargetMigration + setTargetMigration, + subscribeMessageFeed }); }; @@ -59,6 +61,7 @@ describe("Personalization", () => { "storeViews" ]); setTargetMigration = jasmine.createSpy("setTargetMigration"); + subscribeMessageFeed = jasmine.createSpy("subscribeMessageFeed"); build(); }); diff --git a/test/unit/specs/components/Personalization/createModules.spec.js b/test/unit/specs/components/Personalization/createModules.spec.js index a005bdbf4..55a09e202 100644 --- a/test/unit/specs/components/Personalization/createModules.spec.js +++ b/test/unit/specs/components/Personalization/createModules.spec.js @@ -41,14 +41,16 @@ describe("createModules", () => { expect(Object.keys(modules[DOM_ACTION]).length).toEqual(17); }); + it("has in-app-message modules", () => { const modules = createModules(() => undefined); expect(modules[IN_APP_MESSAGE]).toEqual({ modal: jasmine.any(Function), - banner: jasmine.any(Function) + banner: jasmine.any(Function), + feed: jasmine.any(Function) }); - expect(Object.keys(modules[IN_APP_MESSAGE]).length).toEqual(2); + expect(Object.keys(modules[IN_APP_MESSAGE]).length).toEqual(3); }); }); diff --git a/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js b/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js new file mode 100644 index 000000000..70b786d12 --- /dev/null +++ b/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js @@ -0,0 +1 @@ +// TODO diff --git a/test/unit/specs/utils/debounce.spec.js b/test/unit/specs/utils/debounce.spec.js new file mode 100644 index 000000000..70b786d12 --- /dev/null +++ b/test/unit/specs/utils/debounce.spec.js @@ -0,0 +1 @@ +// TODO From 13a766882adc299ba2cef590768137b95db8d5db Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Tue, 2 May 2023 11:40:45 -0600 Subject: [PATCH 14/66] added some tests. --- package-lock.json | 2 +- package.json | 2 +- .../createDecisionHistory.js | 11 + .../DecisioningEngine/createEventRegistry.js | 18 +- src/components/DecisioningEngine/utils.js | 11 + .../createSubscribeMessageFeed.js | 2 +- src/utils/debounce.js | 11 + .../createDecisionHistory.spec.js | 43 +- .../createEventRegistry.spec.js | 36 +- .../createSubscribeMessageFeed.spec.js | 478 +++++++++++++++++- test/unit/specs/utils/debounce.spec.js | 42 +- 11 files changed, 628 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index 916f5d112..a79f4e4d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "2.16.0-alpha.0", "license": "Apache-2.0", "dependencies": { - "@adobe/aep-rules-engine": "file:../aepsdk-rulesengine-typescript", + "@adobe/aep-rules-engine": "^2.0.1", "@adobe/reactor-cookie": "^1.0.0", "@adobe/reactor-load-script": "^1.1.1", "@adobe/reactor-object-assign": "^1.0.0", diff --git a/package.json b/package.json index 46b01d8d1..60a99d563 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ } ], "dependencies": { - "@adobe/aep-rules-engine": "file:../aepsdk-rulesengine-typescript", + "@adobe/aep-rules-engine": "^2.0.1", "@adobe/reactor-cookie": "^1.0.0", "@adobe/reactor-load-script": "^1.1.1", "@adobe/reactor-object-assign": "^1.0.0", diff --git a/src/components/DecisioningEngine/createDecisionHistory.js b/src/components/DecisioningEngine/createDecisionHistory.js index d2cd0c19f..3c2f71cd1 100644 --- a/src/components/DecisioningEngine/createDecisionHistory.js +++ b/src/components/DecisioningEngine/createDecisionHistory.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 { createRestoreStorage, createSaveStorage } from "./utils"; const STORAGE_KEY = "history"; diff --git a/src/components/DecisioningEngine/createEventRegistry.js b/src/components/DecisioningEngine/createEventRegistry.js index c876f18c1..6ebd7bb36 100644 --- a/src/components/DecisioningEngine/createEventRegistry.js +++ b/src/components/DecisioningEngine/createEventRegistry.js @@ -12,7 +12,6 @@ governing permissions and limitations under the License. import { createRestoreStorage, createSaveStorage } from "./utils"; const STORAGE_KEY = "events"; -const eventKey = (eventType, eventId) => `${eventType}|${eventId}`; export default ({ storage }) => { const restore = createRestoreStorage(storage, STORAGE_KEY); @@ -37,19 +36,22 @@ export default ({ storage }) => { const { propositions = [] } = decisioning; propositions.forEach(proposition => { - const key = eventKey(eventType, proposition.id); let count = 0; const timestamp = new Date().getTime(); let firstTimestamp = timestamp; - const existingEvent = events[key]; + if (!events[eventType]) { + events[eventType] = {}; + } + + const existingEvent = events[eventType][proposition.id]; if (existingEvent) { count = existingEvent.count; firstTimestamp = existingEvent.firstTimestamp || existingEvent.timestamp; } - events[key] = { + events[eventType][proposition.id] = { event: { id: proposition.id, type: eventType }, firstTimestamp, timestamp, @@ -59,7 +61,13 @@ export default ({ storage }) => { save(events); }; - const getEvent = (eventType, eventId) => events[eventKey(eventType, eventId)]; + const getEvent = (eventType, eventId) => { + if (!events[eventType]) { + return undefined; + } + + return events[eventType][eventId]; + }; return { rememberEvent, getEvent, toJSON: () => events }; }; diff --git a/src/components/DecisioningEngine/utils.js b/src/components/DecisioningEngine/utils.js index 8830ebfc7..0cf24dae5 100644 --- a/src/components/DecisioningEngine/utils.js +++ b/src/components/DecisioningEngine/utils.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 debounce from "../../utils/debounce"; export const createRestoreStorage = (storage, storageKey) => { diff --git a/src/components/Personalization/createSubscribeMessageFeed.js b/src/components/Personalization/createSubscribeMessageFeed.js index 82622b269..2142b13c3 100644 --- a/src/components/Personalization/createSubscribeMessageFeed.js +++ b/src/components/Personalization/createSubscribeMessageFeed.js @@ -33,7 +33,7 @@ export default ({ collect }) => { ...content, qualifiedDate, displayedDate, - getSurface: () => item.meta.surface, + getSurface: () => data.meta.surface, getAnalyticsDetail: () => { return { id, scope, scopeDetails }; } diff --git a/src/utils/debounce.js b/src/utils/debounce.js index ab930d0d4..c2cdc924b 100644 --- a/src/utils/debounce.js +++ b/src/utils/debounce.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. +*/ export default (fn, delay = 150) => { let timer; return (...args) => { diff --git a/test/unit/specs/components/DecisioningEngine/createDecisionHistory.spec.js b/test/unit/specs/components/DecisioningEngine/createDecisionHistory.spec.js index 70b786d12..cb5b62e92 100644 --- a/test/unit/specs/components/DecisioningEngine/createDecisionHistory.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createDecisionHistory.spec.js @@ -1 +1,42 @@ -// TODO +import createDecisionHistory from "../../../../../src/components/DecisioningEngine/createDecisionHistory"; + +describe("DecisioningEngine:decisionHistory", () => { + let storage; + let history; + + beforeEach(() => { + storage = jasmine.createSpyObj("storage", ["getItem", "setItem", "clear"]); + history = createDecisionHistory({ storage }); + }); + + it("records decision time", () => { + const decisionTime = history.recordDecision("abc"); + + expect(decisionTime).toEqual(jasmine.any(Number)); + }); + + it("uses prior decision time, if decision already recorded", done => { + const firstDecisionTime = history.recordDecision("abc"); + + setTimeout(() => { + expect(history.recordDecision("abc")).toEqual(firstDecisionTime); + done(); + }, 200); + }); + + it("restores history from storage", () => { + expect(storage.getItem).toHaveBeenCalledWith("history"); + }); + + it("saves history to storage", done => { + history.recordDecision("abc"); + + setTimeout(() => { + expect(storage.setItem).toHaveBeenCalledWith( + "history", + jasmine.any(String) + ); + done(); + }, 200); + }); +}); diff --git a/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js b/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js index 761fe7446..971d83cbf 100644 --- a/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js @@ -39,23 +39,25 @@ describe("DecisioningEngine:createEventRegistry", () => { eventRegistry.rememberEvent(event); expect(eventRegistry.toJSON()).toEqual({ - "display|abc": { - event: { id: "abc", type: "display" }, - firstTimestamp: jasmine.any(Number), - timestamp: jasmine.any(Number), - count: 1 - }, - "display|def": { - event: { id: "def", type: "display" }, - firstTimestamp: jasmine.any(Number), - timestamp: jasmine.any(Number), - count: 1 - }, - "display|ghi": { - event: { id: "ghi", type: "display" }, - firstTimestamp: jasmine.any(Number), - timestamp: jasmine.any(Number), - count: 1 + display: { + abc: { + event: { id: "abc", type: "display" }, + firstTimestamp: jasmine.any(Number), + timestamp: jasmine.any(Number), + count: 1 + }, + def: { + event: { id: "def", type: "display" }, + firstTimestamp: jasmine.any(Number), + timestamp: jasmine.any(Number), + count: 1 + }, + ghi: { + event: { id: "ghi", type: "display" }, + firstTimestamp: jasmine.any(Number), + timestamp: jasmine.any(Number), + count: 1 + } } }); }); diff --git a/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js b/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js index 70b786d12..5125306db 100644 --- a/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js +++ b/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js @@ -1 +1,477 @@ -// TODO +import createSubscribeMessageFeed from "../../../../../src/components/Personalization/createSubscribeMessageFeed"; + +describe("Personalization:subscribeMessageFeed", () => { + let collect; + let subscribeMessageFeed; + + const PROPOSITIONS = [ + { + id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" + }, + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", + qualifiedDate: 1683042673387, + displayedDate: 1683042673395 + }, + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f", + qualifiedDate: 1683042673387, + displayedDate: 1683042673395 + } + ], + scope: "web://mywebsite.com/feed" + }, + { + id: "1a3d874f-39ee-4310-bfa9-6559a10041a4", + items: [ + { + schema: "https://ns.adobe.com/personalization/in-app-message", + data: { + expiryDate: 1712190456, + type: "feed", + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + parameters: {}, + content: { + imageUrl: "img/lumon.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + publishedDate: 1677752640000, + body: "a handshake is available upon request.", + title: "Welcome to Lumon!" + }, + contentType: "application/json" + }, + id: "a48ca420-faea-467e-989a-5d179d9f562d", + qualifiedDate: 1683042628064, + displayedDate: 1683042628070 + }, + { + schema: "https://ns.adobe.com/personalization/in-app-message", + data: { + expiryDate: 1712190456, + type: "feed", + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + parameters: {}, + content: { + imageUrl: "img/achievement.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + publishedDate: 1677839040000, + body: "Great job, you completed your profile.", + title: "Achievement Unlocked!" + }, + contentType: "application/json" + }, + id: "b7173290-588f-40c6-a05c-43ed5ec08b28", + qualifiedDate: 1683042628064, + displayedDate: 1683042628070 + } + ], + scope: "web://mywebsite.com/feed" + }, + { + id: "1ae11bc5-96dc-41c7-8f71-157c57a5290e", + items: [ + { + schema: "https://ns.adobe.com/personalization/in-app-message", + data: { + expiryDate: 1712190456, + type: "feed", + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + parameters: {}, + content: { + imageUrl: "img/twitter.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + publishedDate: 1678098240000, + body: "Posting on social media helps us spread the word.", + title: "Thanks for sharing!" + }, + contentType: "application/json" + }, + id: "cfcb1af7-7bc2-45b2-a86a-0aa93fe69ce7", + qualifiedDate: 1683042658312, + displayedDate: 1683042658316 + } + ], + scope: "web://mywebsite.com/feed", + scopeDetails: { + id: "1ae11bc5-96dc-41c7-8f71-157c57a5290e", + scope: "web://mywebsite.com/feed", + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: + "eyJtZXNzYWdlRXhlY3V0aW9uIjp7Im1lc3NhZ2VFeGVjdXRpb25JRCI6Ik5BIiwibWVzc2FnZUlEIjoiMDJjNzdlYTgtN2MwZS00ZDMzLTgwOTAtNGE1YmZkM2Q3NTAzIiwibWVzc2FnZVR5cGUiOiJtYXJrZXRpbmciLCJjYW1wYWlnbklEIjoiMzlhZThkNGItYjU1ZS00M2RjLWExNDMtNzdmNTAxOTViNDg3IiwiY2FtcGFpZ25WZXJzaW9uSUQiOiJiZDg1ZDllOC0yMDM3LTQyMmYtYjZkMi0zOTU3YzkwNTU5ZDMiLCJjYW1wYWlnbkFjdGlvbklEIjoiYjQ3ZmRlOGItNTdjMS00YmJlLWFlMjItNjRkNWI3ODJkMTgzIiwibWVzc2FnZVB1YmxpY2F0aW9uSUQiOiJhZTUyY2VkOC0yMDBjLTQ5N2UtODc4Ny1lZjljZmMxNzgyMTUifSwibWVzc2FnZVByb2ZpbGUiOnsiY2hhbm5lbCI6eyJfaWQiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbHMvd2ViIiwiX3R5cGUiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbC10eXBlcy93ZWIifSwibWVzc2FnZVByb2ZpbGVJRCI6ImY1Y2Q5OTk1LTZiNDQtNDIyMS05YWI3LTViNTMzOGQ1ZjE5MyJ9fQ==" + }, + strategies: [ + { + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" + } + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" + }, + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" + } + } + }, + { + id: "d1f7d411-a549-47bc-a4d8-c8e638b0a46b", + items: [ + { + schema: "https://ns.adobe.com/personalization/in-app-message", + data: { + expiryDate: 1712190456, + type: "feed", + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + parameters: {}, + content: { + imageUrl: "img/gold-coin.jpg", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + publishedDate: 1678184640000, + body: "Now you're ready to earn!", + title: "Funds deposited!" + }, + contentType: "application/json" + }, + id: "0263e171-fa32-4c7a-9611-36b28137a81d", + qualifiedDate: 1683042653905, + displayedDate: 1683042653909 + } + ], + scope: "web://mywebsite.com/feed" + } + ]; + + beforeEach(() => { + collect = jasmine.createSpy().and.returnValue(Promise.resolve()); + subscribeMessageFeed = createSubscribeMessageFeed({ collect }); + }); + + it("has a command defined", () => { + const { command } = subscribeMessageFeed; + + expect(command).toEqual({ + optionsValidator: jasmine.any(Function), + run: jasmine.any(Function) + }); + }); + + it("calls the callback with list of items", () => { + const { command, refresh } = subscribeMessageFeed; + + const callback = jasmine.createSpy(); + + // register a subscription. equivalent to alloy("subscribeMessageFeed", {surface, callback}) + command.run({ surface: "web://mywebsite.com/feed", callback }); + + refresh(PROPOSITIONS); + expect(callback).toHaveBeenCalledOnceWith({ + items: [ + jasmine.objectContaining({ + imageUrl: "img/twitter.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + publishedDate: 1678098240000, + body: "Posting on social media helps us spread the word.", + title: "Thanks for sharing!", + qualifiedDate: 1683042658312, + displayedDate: 1683042658316 + }), + jasmine.objectContaining({ + imageUrl: "img/gold-coin.jpg", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + publishedDate: 1678184640000, + body: "Now you're ready to earn!", + title: "Funds deposited!", + qualifiedDate: 1683042653905, + displayedDate: 1683042653909 + }), + jasmine.objectContaining({ + imageUrl: "img/achievement.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + publishedDate: 1677839040000, + body: "Great job, you completed your profile.", + title: "Achievement Unlocked!", + qualifiedDate: 1683042628064, + displayedDate: 1683042628070 + }), + jasmine.objectContaining({ + imageUrl: "img/lumon.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + publishedDate: 1677752640000, + body: "a handshake is available upon request.", + title: "Welcome to Lumon!", + qualifiedDate: 1683042628064, + displayedDate: 1683042628070 + }) + ], + clicked: jasmine.any(Function), + rendered: jasmine.any(Function) + }); + }); + it("has helper methods on items", () => { + const { command, refresh } = subscribeMessageFeed; + + const callback = jasmine.createSpy(); + + command.run({ surface: "web://mywebsite.com/feed", callback }); + + refresh(PROPOSITIONS); + + const { items } = callback.calls.first().args[0]; + + expect(items[0].getSurface).toEqual(jasmine.any(Function)); + expect(items[0].getAnalyticsDetail).toEqual(jasmine.any(Function)); + + expect(items[0].getSurface()).toEqual("web://mywebsite.com/feed"); + expect(items[0].getAnalyticsDetail()).toEqual({ + id: "1ae11bc5-96dc-41c7-8f71-157c57a5290e", + scope: "web://mywebsite.com/feed", + scopeDetails: { + id: "1ae11bc5-96dc-41c7-8f71-157c57a5290e", + scope: "web://mywebsite.com/feed", + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: + "eyJtZXNzYWdlRXhlY3V0aW9uIjp7Im1lc3NhZ2VFeGVjdXRpb25JRCI6Ik5BIiwibWVzc2FnZUlEIjoiMDJjNzdlYTgtN2MwZS00ZDMzLTgwOTAtNGE1YmZkM2Q3NTAzIiwibWVzc2FnZVR5cGUiOiJtYXJrZXRpbmciLCJjYW1wYWlnbklEIjoiMzlhZThkNGItYjU1ZS00M2RjLWExNDMtNzdmNTAxOTViNDg3IiwiY2FtcGFpZ25WZXJzaW9uSUQiOiJiZDg1ZDllOC0yMDM3LTQyMmYtYjZkMi0zOTU3YzkwNTU5ZDMiLCJjYW1wYWlnbkFjdGlvbklEIjoiYjQ3ZmRlOGItNTdjMS00YmJlLWFlMjItNjRkNWI3ODJkMTgzIiwibWVzc2FnZVB1YmxpY2F0aW9uSUQiOiJhZTUyY2VkOC0yMDBjLTQ5N2UtODc4Ny1lZjljZmMxNzgyMTUifSwibWVzc2FnZVByb2ZpbGUiOnsiY2hhbm5lbCI6eyJfaWQiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbHMvd2ViIiwiX3R5cGUiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbC10eXBlcy93ZWIifSwibWVzc2FnZVByb2ZpbGVJRCI6ImY1Y2Q5OTk1LTZiNDQtNDIyMS05YWI3LTViNTMzOGQ1ZjE5MyJ9fQ==" + }, + strategies: [ + { + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" + } + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" + }, + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" + } + } + }); + }); + + it("collects interact events", () => { + const { command, refresh } = subscribeMessageFeed; + + const callback = jasmine.createSpy(); + + command.run({ surface: "web://mywebsite.com/feed", callback }); + + refresh(PROPOSITIONS); + + const { items, clicked } = callback.calls.first().args[0]; + + clicked([items[0]]); + + expect(collect).toHaveBeenCalledWith({ + decisionsMeta: [items[0].getAnalyticsDetail()], + eventType: "decisioning.propositionInteract", + documentMayUnload: true + }); + }); + + it("collects only one interact event per proposition", () => { + const { command, refresh } = subscribeMessageFeed; + + const callback = jasmine.createSpy(); + + command.run({ surface: "web://mywebsite.com/feed", callback }); + + refresh(PROPOSITIONS); + + const { items, clicked } = callback.calls.first().args[0]; + + clicked([items[0], items[0], items[0]]); + + expect(collect).toHaveBeenCalledWith({ + decisionsMeta: [items[0].getAnalyticsDetail()], + eventType: "decisioning.propositionInteract", + documentMayUnload: true + }); + }); + + it("collects separately interact events for each distinct proposition", () => { + const { command, refresh } = subscribeMessageFeed; + + const callback = jasmine.createSpy(); + + command.run({ surface: "web://mywebsite.com/feed", callback }); + + refresh(PROPOSITIONS); + + const { items, clicked } = callback.calls.first().args[0]; + + clicked([items[0]]); + + expect(collect).toHaveBeenCalledWith({ + decisionsMeta: [items[0].getAnalyticsDetail()], + eventType: "decisioning.propositionInteract", + documentMayUnload: true + }); + + clicked([items[0]]); + + expect(collect).toHaveBeenCalledWith({ + decisionsMeta: [items[0].getAnalyticsDetail()], + eventType: "decisioning.propositionInteract", + documentMayUnload: true + }); + + expect(collect).toHaveBeenCalledTimes(2); + }); + + it("collects multiple interact events for distinct propositions", () => { + const { command, refresh } = subscribeMessageFeed; + + const callback = jasmine.createSpy(); + + command.run({ surface: "web://mywebsite.com/feed", callback }); + + refresh(PROPOSITIONS); + + const { items, clicked } = callback.calls.first().args[0]; + + clicked([items[0], items[1]]); + + expect(collect).toHaveBeenCalledOnceWith({ + decisionsMeta: [ + items[0].getAnalyticsDetail(), + items[1].getAnalyticsDetail() + ], + eventType: "decisioning.propositionInteract", + documentMayUnload: true + }); + }); + + it("collects display events", () => { + const { command, refresh } = subscribeMessageFeed; + + const callback = jasmine.createSpy(); + + command.run({ surface: "web://mywebsite.com/feed", callback }); + + refresh(PROPOSITIONS); + + const { items, rendered } = callback.calls.first().args[0]; + + rendered([items[0]]); + + expect(collect).toHaveBeenCalledWith({ + decisionsMeta: [items[0].getAnalyticsDetail()], + eventType: "decisioning.propositionDisplay" + }); + }); + + it("collects only one display event per proposition", () => { + const { command, refresh } = subscribeMessageFeed; + + const callback = jasmine.createSpy(); + + command.run({ surface: "web://mywebsite.com/feed", callback }); + + refresh(PROPOSITIONS); + + const { items, rendered } = callback.calls.first().args[0]; + + rendered([items[0]]); + rendered([items[0], items[0]]); + + expect(collect).toHaveBeenCalledOnceWith({ + decisionsMeta: [items[0].getAnalyticsDetail()], + eventType: "decisioning.propositionDisplay" + }); + }); + + it("collects multiple display events for distinct propositions", () => { + const { command, refresh } = subscribeMessageFeed; + + const callback = jasmine.createSpy(); + + command.run({ surface: "web://mywebsite.com/feed", callback }); + + refresh(PROPOSITIONS); + + const { items, rendered } = callback.calls.first().args[0]; + + rendered([items[0], items[1]]); + + expect(collect).toHaveBeenCalledOnceWith({ + decisionsMeta: [ + items[0].getAnalyticsDetail(), + items[1].getAnalyticsDetail() + ], + eventType: "decisioning.propositionDisplay" + }); + }); + + it("collects display events only once per session", () => { + const { command, refresh } = subscribeMessageFeed; + + const callback = jasmine.createSpy(); + + command.run({ surface: "web://mywebsite.com/feed", callback }); + + refresh(PROPOSITIONS); + + const { items, rendered } = callback.calls.first().args[0]; + + rendered([items[0], items[1]]); + rendered([items[0], items[1]]); + rendered([items[2]]); + + expect(collect).toHaveBeenCalledTimes(2); + + expect(collect).toHaveBeenCalledWith({ + decisionsMeta: [ + items[0].getAnalyticsDetail(), + items[1].getAnalyticsDetail() + ], + eventType: "decisioning.propositionDisplay" + }); + + expect(collect).toHaveBeenCalledWith({ + decisionsMeta: [items[2].getAnalyticsDetail()], + eventType: "decisioning.propositionDisplay" + }); + }); +}); diff --git a/test/unit/specs/utils/debounce.spec.js b/test/unit/specs/utils/debounce.spec.js index 70b786d12..6d3b8e411 100644 --- a/test/unit/specs/utils/debounce.spec.js +++ b/test/unit/specs/utils/debounce.spec.js @@ -1 +1,41 @@ -// TODO +import debounce from "../../../../src/utils/debounce"; + +describe("debounce", () => { + let callback; + + beforeEach(() => { + callback = jasmine.createSpy(); + }); + + it("calls a function only once", done => { + const fn = debounce(callback, 150); + + for (let i = 0; i < 10; i += 1) { + fn("oh", "hai"); + } + + setTimeout(() => { + expect(callback).toHaveBeenCalledOnceWith("oh", "hai"); + expect(callback).toHaveBeenCalledTimes(1); + done(); + }, 160); + }); + + it("calls a function only once per delay period", done => { + const fn = debounce(callback, 20); + fn("oh", "hai"); + fn("oh", "hai"); + + setTimeout(() => { + fn("cool", "beans"); + expect(callback).toHaveBeenCalledWith("oh", "hai"); + expect(callback).toHaveBeenCalledTimes(1); + }, 25); + + setTimeout(() => { + expect(callback).toHaveBeenCalledWith("cool", "beans"); + expect(callback).toHaveBeenCalledTimes(2); + done(); + }, 50); + }); +}); From f3ffb3b4b2b2090d20639c5cc7501ab91d2af02e Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Tue, 2 May 2023 12:41:41 -0600 Subject: [PATCH 15/66] track display/interact events and save to storage --- package.json | 3 +- .../DecisioningEngine/createEventRegistry.js | 37 ++++++++-- src/components/DecisioningEngine/index.js | 11 ++- src/components/DecisioningEngine/utils.js | 32 +++++++++ .../Personalization/createCollect.js | 17 ++++- src/utils/debounce.js | 22 ++++++ .../createContextProvider.spec.js | 10 ++- .../createEventRegistry.spec.js | 61 ++++++++++------- .../DecisioningEngine/utils.spec.js | 68 +++++++++++++++++++ test/unit/specs/utils/debounce.spec.js | 41 +++++++++++ 10 files changed, 262 insertions(+), 40 deletions(-) create mode 100644 src/components/DecisioningEngine/utils.js create mode 100644 src/utils/debounce.js create mode 100644 test/unit/specs/components/DecisioningEngine/utils.spec.js create mode 100644 test/unit/specs/utils/debounce.spec.js diff --git a/package.json b/package.json index cf3f2aa6c..b495e24c9 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "format": "prettier --write \"*.{html,js}\" \"{sandbox,src,test,scripts}/**/*.{html,js}\"", "test": "npm run test:unit && npm run test:scripts && npm run test:functional", "test:unit": "karma start --single-run", - "testdebug": "karma start --browsers=Chrome --single-run=false --debug", "test:unit:watch": "karma start", "test:unit:saucelabs:local": "karma start karma.saucelabs.conf.js --single-run", "test:unit:coverage": "karma start --single-run --reporters spec,coverage", @@ -58,7 +57,7 @@ } ], "dependencies": { - "@adobe/aep-rules-engine": "file:../aepsdk-rulesengine-typescript", + "@adobe/aep-rules-engine": "^2.0.1", "@adobe/reactor-cookie": "^1.0.0", "@adobe/reactor-load-script": "^1.1.1", "@adobe/reactor-object-assign": "^1.0.0", diff --git a/src/components/DecisioningEngine/createEventRegistry.js b/src/components/DecisioningEngine/createEventRegistry.js index bd5090413..6ebd7bb36 100644 --- a/src/components/DecisioningEngine/createEventRegistry.js +++ b/src/components/DecisioningEngine/createEventRegistry.js @@ -9,8 +9,16 @@ 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 () => { - const events = {}; +import { createRestoreStorage, createSaveStorage } from "./utils"; + +const STORAGE_KEY = "events"; + +export default ({ storage }) => { + const restore = createRestoreStorage(storage, STORAGE_KEY); + const save = createSaveStorage(storage, STORAGE_KEY, 150); + + const events = restore({}); + const rememberEvent = event => { const { xdm = {} } = event.getContent(); const { eventType = "", _experience } = xdm; @@ -29,20 +37,37 @@ export default () => { propositions.forEach(proposition => { let count = 0; - const existingEvent = events[proposition.id]; + const timestamp = new Date().getTime(); + let firstTimestamp = timestamp; + + if (!events[eventType]) { + events[eventType] = {}; + } + + const existingEvent = events[eventType][proposition.id]; if (existingEvent) { count = existingEvent.count; + firstTimestamp = + existingEvent.firstTimestamp || existingEvent.timestamp; } - events[proposition.id] = { + events[eventType][proposition.id] = { event: { id: proposition.id, type: eventType }, - timestamp: new Date().getTime(), + firstTimestamp, + timestamp, count: count + 1 }; }); + + save(events); }; + const getEvent = (eventType, eventId) => { + if (!events[eventType]) { + return undefined; + } - const getEvent = eventId => events[eventId]; + return events[eventType][eventId]; + }; return { rememberEvent, getEvent, toJSON: () => events }; }; diff --git a/src/components/DecisioningEngine/index.js b/src/components/DecisioningEngine/index.js index 6baeb9482..6702e0157 100644 --- a/src/components/DecisioningEngine/index.js +++ b/src/components/DecisioningEngine/index.js @@ -9,15 +9,20 @@ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTA OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import { noop } from "../../utils"; +import { noop, sanitizeOrgIdForCookieName } from "../../utils"; import createOnResponseHandler from "./createOnResponseHandler"; import createDecisionProvider from "./createDecisionProvider"; import createApplyResponse from "./createApplyResponse"; import createEventRegistry from "./createEventRegistry"; import createContextProvider from "./createContextProvider"; -const createDecisioningEngine = () => { - const eventRegistry = createEventRegistry(); +const createDecisioningEngine = ({ config, createNamespacedStorage }) => { + const { orgId } = config; + const storage = createNamespacedStorage( + `${sanitizeOrgIdForCookieName(orgId)}.decisioning.` + ); + + const eventRegistry = createEventRegistry({ storage: storage.persistent }); let applyResponse = createApplyResponse(); const decisionProvider = createDecisionProvider(); const contextProvider = createContextProvider({ eventRegistry }); diff --git a/src/components/DecisioningEngine/utils.js b/src/components/DecisioningEngine/utils.js new file mode 100644 index 000000000..3ff792d84 --- /dev/null +++ b/src/components/DecisioningEngine/utils.js @@ -0,0 +1,32 @@ +/* +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 debounce from "../../utils/debounce"; + +export const createRestoreStorage = (storage, storageKey) => { + return defaultValue => { + const stored = storage.getItem(storageKey); + if (!stored) { + return defaultValue; + } + + try { + return JSON.parse(stored); + } catch (e) { + return defaultValue; + } + }; +}; + +export const createSaveStorage = (storage, storageKey, debounceDelay = 150) => { + return debounce(value => { + storage.setItem(storageKey, JSON.stringify(value)); + }, debounceDelay); +}; diff --git a/src/components/Personalization/createCollect.js b/src/components/Personalization/createCollect.js index 809029318..b9520be35 100644 --- a/src/components/Personalization/createCollect.js +++ b/src/components/Personalization/createCollect.js @@ -15,9 +15,14 @@ import { isNonEmptyArray } from "../../utils"; export default ({ eventManager, mergeDecisionsMeta }) => { // Called when a decision is auto-rendered for the __view__ scope or a SPA view(display and empty display notification) - return ({ decisionsMeta = [], documentMayUnload = false, viewName }) => { + return ({ + decisionsMeta = [], + documentMayUnload = false, + eventType = DISPLAY, + viewName + }) => { const event = eventManager.createEvent(); - const data = { eventType: DISPLAY }; + const data = { eventType }; if (viewName) { data.web = { @@ -25,7 +30,13 @@ export default ({ eventManager, mergeDecisionsMeta }) => { }; } if (isNonEmptyArray(decisionsMeta)) { - mergeDecisionsMeta(event, decisionsMeta, PropositionEventType.DISPLAY); + mergeDecisionsMeta( + event, + decisionsMeta, + eventType === DISPLAY + ? PropositionEventType.DISPLAY + : PropositionEventType.INTERACT + ); } event.mergeXdm(data); diff --git a/src/utils/debounce.js b/src/utils/debounce.js new file mode 100644 index 000000000..da6df79ce --- /dev/null +++ b/src/utils/debounce.js @@ -0,0 +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. +*/ +export default (fn, delay = 150) => { + let timer; + return (...args) => { + if (timer) { + clearTimeout(timer); + } + + timer = setTimeout(() => { + fn(...args); + }, delay); + }; +}; diff --git a/test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js b/test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js index d5f9c2147..74debedb8 100644 --- a/test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js @@ -16,8 +16,14 @@ describe("DecisioningEngine:createContextProvider", () => { let contextProvider; let eventRegistry; + let storage; + + beforeEach(() => { + storage = jasmine.createSpyObj("storage", ["getItem", "setItem", "clear"]); + }); + it("includes provided context passed in", () => { - eventRegistry = createEventRegistry(); + eventRegistry = createEventRegistry({ storage }); contextProvider = createContextProvider({ eventRegistry }); expect(contextProvider.getContext({ cool: "beans" })).toEqual({ @@ -26,7 +32,7 @@ describe("DecisioningEngine:createContextProvider", () => { }); }); - it("includes provided context passed in", () => { + it("includes events context", () => { const events = { abc: { event: { id: "abc", type: "display" }, diff --git a/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js b/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js index ded4a7a65..971d83cbf 100644 --- a/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js @@ -12,8 +12,14 @@ governing permissions and limitations under the License. import createEventRegistry from "../../../../../src/components/DecisioningEngine/createEventRegistry"; describe("DecisioningEngine:createEventRegistry", () => { + let storage; + + beforeEach(() => { + storage = jasmine.createSpyObj("storage", ["getItem", "setItem", "clear"]); + }); + it("registers events", () => { - const eventRegistry = createEventRegistry(); + const eventRegistry = createEventRegistry({ storage }); const getContent = () => ({ xdm: { @@ -33,26 +39,31 @@ describe("DecisioningEngine:createEventRegistry", () => { eventRegistry.rememberEvent(event); expect(eventRegistry.toJSON()).toEqual({ - abc: { - event: { id: "abc", type: "display" }, - timestamp: jasmine.any(Number), - count: 1 - }, - def: { - event: { id: "def", type: "display" }, - timestamp: jasmine.any(Number), - count: 1 - }, - ghi: { - event: { id: "ghi", type: "display" }, - timestamp: jasmine.any(Number), - count: 1 + display: { + abc: { + event: { id: "abc", type: "display" }, + firstTimestamp: jasmine.any(Number), + timestamp: jasmine.any(Number), + count: 1 + }, + def: { + event: { id: "def", type: "display" }, + firstTimestamp: jasmine.any(Number), + timestamp: jasmine.any(Number), + count: 1 + }, + ghi: { + event: { id: "ghi", type: "display" }, + firstTimestamp: jasmine.any(Number), + timestamp: jasmine.any(Number), + count: 1 + } } }); }); it("does not register invalid events", () => { - const eventRegistry = createEventRegistry(); + const eventRegistry = createEventRegistry({ storage }); eventRegistry.rememberEvent({ getContent: () => ({ @@ -87,7 +98,7 @@ describe("DecisioningEngine:createEventRegistry", () => { }); it("increments count and sets timestamp", done => { - const eventRegistry = createEventRegistry(); + const eventRegistry = createEventRegistry({ storage }); const getContent = () => ({ xdm: { @@ -106,27 +117,29 @@ describe("DecisioningEngine:createEventRegistry", () => { let lastEventTime = 0; eventRegistry.rememberEvent(event); - expect(eventRegistry.getEvent("abc")).toEqual({ + expect(eventRegistry.getEvent("display", "abc")).toEqual({ event: { id: "abc", type: "display" }, + firstTimestamp: jasmine.any(Number), timestamp: jasmine.any(Number), count: 1 }); - expect(eventRegistry.getEvent("abc").timestamp).toBeGreaterThan( + expect(eventRegistry.getEvent("display", "abc").timestamp).toBeGreaterThan( lastEventTime ); - lastEventTime = eventRegistry.getEvent("abc").timestamp; + lastEventTime = eventRegistry.getEvent("display", "abc").timestamp; setTimeout(() => { eventRegistry.rememberEvent(event); // again - expect(eventRegistry.getEvent("abc")).toEqual({ + expect(eventRegistry.getEvent("display", "abc")).toEqual({ event: { id: "abc", type: "display" }, + firstTimestamp: jasmine.any(Number), timestamp: jasmine.any(Number), count: 2 }); - expect(eventRegistry.getEvent("abc").timestamp).toBeGreaterThan( - lastEventTime - ); + expect( + eventRegistry.getEvent("display", "abc").timestamp + ).toBeGreaterThan(lastEventTime); done(); }, 10); }); diff --git a/test/unit/specs/components/DecisioningEngine/utils.spec.js b/test/unit/specs/components/DecisioningEngine/utils.spec.js new file mode 100644 index 000000000..0be44c9cc --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/utils.spec.js @@ -0,0 +1,68 @@ +/* +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 { + createRestoreStorage, + createSaveStorage +} from "../../../../../src/components/DecisioningEngine/utils"; + +describe("DecisioningEngine:utils", () => { + let storage; + + beforeEach(() => { + storage = jasmine.createSpyObj("storage", ["getItem", "setItem", "clear"]); + }); + + it("restores from storage", () => { + storage.getItem.and.returnValue( + '{ "something": true, "color": "orange", "person": { "height": 5.83 } }' + ); + const restore = createRestoreStorage(storage, "zoink"); + + expect(restore({ good: true })).toEqual({ + something: true, + color: "orange", + person: { height: 5.83 } + }); + + expect(storage.getItem).toHaveBeenCalledWith("zoink"); + }); + + it("uses default value if storage unavailable", () => { + storage.getItem.and.returnValue(undefined); + const restore = createRestoreStorage(storage, "zoink"); + + expect(restore({ good: true })).toEqual({ good: true }); + + expect(storage.getItem).toHaveBeenCalledWith("zoink"); + }); + + it("saves to storage", done => { + storage.getItem.and.returnValue( + '{ "something": true, "color": "orange", "person": { "height": 5.83 } }' + ); + const save = createSaveStorage(storage, "zoink", 10); + + save({ + something: true, + color: "orange", + person: { height: 5.83 } + }); + + setTimeout(() => { + expect(storage.setItem).toHaveBeenCalledWith( + "zoink", + '{"something":true,"color":"orange","person":{"height":5.83}}' + ); + + done(); + }, 20); + }); +}); diff --git a/test/unit/specs/utils/debounce.spec.js b/test/unit/specs/utils/debounce.spec.js new file mode 100644 index 000000000..6d3b8e411 --- /dev/null +++ b/test/unit/specs/utils/debounce.spec.js @@ -0,0 +1,41 @@ +import debounce from "../../../../src/utils/debounce"; + +describe("debounce", () => { + let callback; + + beforeEach(() => { + callback = jasmine.createSpy(); + }); + + it("calls a function only once", done => { + const fn = debounce(callback, 150); + + for (let i = 0; i < 10; i += 1) { + fn("oh", "hai"); + } + + setTimeout(() => { + expect(callback).toHaveBeenCalledOnceWith("oh", "hai"); + expect(callback).toHaveBeenCalledTimes(1); + done(); + }, 160); + }); + + it("calls a function only once per delay period", done => { + const fn = debounce(callback, 20); + fn("oh", "hai"); + fn("oh", "hai"); + + setTimeout(() => { + fn("cool", "beans"); + expect(callback).toHaveBeenCalledWith("oh", "hai"); + expect(callback).toHaveBeenCalledTimes(1); + }, 25); + + setTimeout(() => { + expect(callback).toHaveBeenCalledWith("cool", "beans"); + expect(callback).toHaveBeenCalledTimes(2); + done(); + }, 50); + }); +}); From cdb7fb76bee94c9b1da7d4b899b4a77d497e4ace Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Tue, 2 May 2023 14:55:16 -0600 Subject: [PATCH 16/66] limit event storage --- .../DecisioningEngine/createEventRegistry.js | 24 ++++- src/components/DecisioningEngine/utils.js | 9 +- .../createEventRegistry.spec.js | 90 ++++++++++++++++++- 3 files changed, 119 insertions(+), 4 deletions(-) diff --git a/src/components/DecisioningEngine/createEventRegistry.js b/src/components/DecisioningEngine/createEventRegistry.js index 6ebd7bb36..417221cc7 100644 --- a/src/components/DecisioningEngine/createEventRegistry.js +++ b/src/components/DecisioningEngine/createEventRegistry.js @@ -12,10 +12,32 @@ governing permissions and limitations under the License. import { createRestoreStorage, createSaveStorage } from "./utils"; const STORAGE_KEY = "events"; +const MAX_EVENT_RECORDS = 1000; + +export const createEventPruner = (limit = MAX_EVENT_RECORDS) => { + return events => { + const pruned = {}; + Object.keys(events).forEach(eventType => { + pruned[eventType] = {}; + Object.values(events[eventType]) + .sort((a, b) => a.firstTimestamp - b.firstTimestamp) + .slice(-1 * limit) + .forEach(entry => { + pruned[eventType][entry.event.id] = entry; + }); + }); + return pruned; + }; +}; export default ({ storage }) => { const restore = createRestoreStorage(storage, STORAGE_KEY); - const save = createSaveStorage(storage, STORAGE_KEY, 150); + const save = createSaveStorage( + storage, + STORAGE_KEY, + 150, + createEventPruner(MAX_EVENT_RECORDS) + ); const events = restore({}); diff --git a/src/components/DecisioningEngine/utils.js b/src/components/DecisioningEngine/utils.js index 3ff792d84..e248cec6e 100644 --- a/src/components/DecisioningEngine/utils.js +++ b/src/components/DecisioningEngine/utils.js @@ -25,8 +25,13 @@ export const createRestoreStorage = (storage, storageKey) => { }; }; -export const createSaveStorage = (storage, storageKey, debounceDelay = 150) => { +export const createSaveStorage = ( + storage, + storageKey, + debounceDelay = 150, + prepareFn = value => value +) => { return debounce(value => { - storage.setItem(storageKey, JSON.stringify(value)); + storage.setItem(storageKey, JSON.stringify(prepareFn(value))); }, debounceDelay); }; diff --git a/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js b/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js index 971d83cbf..ff4cc60c7 100644 --- a/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js @@ -9,7 +9,9 @@ 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 createEventRegistry from "../../../../../src/components/DecisioningEngine/createEventRegistry"; +import createEventRegistry, { + createEventPruner +} from "../../../../../src/components/DecisioningEngine/createEventRegistry"; describe("DecisioningEngine:createEventRegistry", () => { let storage; @@ -143,4 +145,90 @@ describe("DecisioningEngine:createEventRegistry", () => { done(); }, 10); }); + + it("limits events to 1000 events", () => { + const prune = createEventPruner(); + + const events = {}; + events["decisioning.propositionDisplay"] = {}; + events["decisioning.propositionInteract"] = {}; + + for (let i = 0; i < 2000; i += 1) { + events["decisioning.propositionDisplay"][i] = { + event: { + id: i, + type: "decisioning.propositionDisplay" + }, + firstTimestamp: 1, + timestamp: 1, + count: 1 + }; + + events["decisioning.propositionInteract"][i] = { + event: { + id: i, + type: "decisioning.propositionInteract" + }, + firstTimestamp: 1, + timestamp: 1, + count: 1 + }; + + const pruned = prune(events); + + const interactEvents = Object.values( + pruned["decisioning.propositionInteract"] + ); + + const displayEvents = Object.values( + pruned["decisioning.propositionDisplay"] + ); + + expect(interactEvents.length).not.toBeGreaterThan(1000); + expect(displayEvents.length).not.toBeGreaterThan(1000); + + if (i > 1000) { + expect(interactEvents[0].event.id).toEqual(i - 999); + expect(displayEvents[0].event.id).toEqual(i - 999); + } + + if (i > 0) { + expect( + interactEvents[0].timestamp < + interactEvents[interactEvents.length - 1].timestamp + ); + expect( + displayEvents[0].timestamp < + displayEvents[interactEvents.length - 1].timestamp + ); + } + } + }); + + it("has configurable limits", () => { + const prune = createEventPruner(10); + + const events = {}; + events["decisioning.propositionDisplay"] = {}; + + for (let i = 0; i < 20; i += 1) { + events["decisioning.propositionDisplay"][i] = { + event: { + id: i, + type: "decisioning.propositionDisplay" + }, + firstTimestamp: 1, + timestamp: 1, + count: 1 + }; + + const pruned = prune(events); + + const displayEvents = Object.values( + pruned["decisioning.propositionDisplay"] + ); + + expect(displayEvents.length).not.toBeGreaterThan(10); + } + }); }); From 8ec3aed68f68f6e43d0bf8ebdb05bbf39d606de0 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Thu, 4 May 2023 10:55:40 -0600 Subject: [PATCH 17/66] store decision history as object --- .../DecisioningEngine/createDecisionHistory.js | 8 ++++++-- .../createEvaluableRulesetPayload.js | 4 +++- .../DecisioningEngine/createDecisionHistory.spec.js | 11 +++++++---- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/components/DecisioningEngine/createDecisionHistory.js b/src/components/DecisioningEngine/createDecisionHistory.js index 3c2f71cd1..97be19f25 100644 --- a/src/components/DecisioningEngine/createDecisionHistory.js +++ b/src/components/DecisioningEngine/createDecisionHistory.js @@ -19,8 +19,12 @@ export default ({ storage }) => { const history = restore({}); const recordDecision = id => { - if (typeof history[id] !== "number") { - history[id] = new Date().getTime(); + if (!history[id]) { + history[id] = {}; + } + + if (typeof history[id].timestamp !== "number") { + history[id].timestamp = new Date().getTime(); save(history); } return history[id]; diff --git a/src/components/DecisioningEngine/createEvaluableRulesetPayload.js b/src/components/DecisioningEngine/createEvaluableRulesetPayload.js index 18778d449..de559371d 100644 --- a/src/components/DecisioningEngine/createEvaluableRulesetPayload.js +++ b/src/components/DecisioningEngine/createEvaluableRulesetPayload.js @@ -38,7 +38,9 @@ export default (payload, eventRegistry, decisionHistory) => { const qualifyingItems = flattenArray( items.map(item => item.execute(context)) ).map(item => { - const qualifiedDate = decisionHistory.recordDecision(item.id); + const { timestamp: qualifiedDate } = decisionHistory.recordDecision( + item.id + ); return { ...item.detail, qualifiedDate, displayedDate }; }); diff --git a/test/unit/specs/components/DecisioningEngine/createDecisionHistory.spec.js b/test/unit/specs/components/DecisioningEngine/createDecisionHistory.spec.js index cb5b62e92..f827feb33 100644 --- a/test/unit/specs/components/DecisioningEngine/createDecisionHistory.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createDecisionHistory.spec.js @@ -10,16 +10,19 @@ describe("DecisioningEngine:decisionHistory", () => { }); it("records decision time", () => { - const decisionTime = history.recordDecision("abc"); + const decision = history.recordDecision("abc"); - expect(decisionTime).toEqual(jasmine.any(Number)); + expect(Object.getPrototypeOf(decision)).toEqual(Object.prototype); + expect(decision.timestamp).toEqual(jasmine.any(Number)); }); it("uses prior decision time, if decision already recorded", done => { - const firstDecisionTime = history.recordDecision("abc"); + const firstDecision = history.recordDecision("abc"); setTimeout(() => { - expect(history.recordDecision("abc")).toEqual(firstDecisionTime); + expect(history.recordDecision("abc").timestamp).toEqual( + firstDecision.timestamp + ); done(); }, 200); }); From 772dff91cf3f5c0e4cf21f70b1c38d1968f46ccd Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Thu, 4 May 2023 15:14:11 -0600 Subject: [PATCH 18/66] decisioning.qualifiedItem event --- .../createDecisionHistory.js | 25 +++----- .../createDecisionProvider.js | 4 +- .../createEvaluableRulesetPayload.js | 4 +- .../DecisioningEngine/createEventRegistry.js | 61 ++++++++++--------- src/components/DecisioningEngine/index.js | 5 +- src/components/DecisioningEngine/utils.js | 2 +- .../createDecisionHistory.spec.js | 31 ++++++---- .../createDecisionProvider.spec.js | 2 +- .../createEvaluableRulesetPayload.spec.js | 2 +- .../createEventRegistry.spec.js | 28 ++++----- .../createOnResponseHandler.spec.js | 4 +- 11 files changed, 84 insertions(+), 84 deletions(-) diff --git a/src/components/DecisioningEngine/createDecisionHistory.js b/src/components/DecisioningEngine/createDecisionHistory.js index 97be19f25..4610c7b27 100644 --- a/src/components/DecisioningEngine/createDecisionHistory.js +++ b/src/components/DecisioningEngine/createDecisionHistory.js @@ -9,26 +9,17 @@ 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 { createRestoreStorage, createSaveStorage } from "./utils"; -const STORAGE_KEY = "history"; -export default ({ storage }) => { - const restore = createRestoreStorage(storage, STORAGE_KEY); - const save = createSaveStorage(storage, STORAGE_KEY); +const QUALIFIED_EVENT_TYPE = "decisioning.qualifiedItem"; - const history = restore({}); - - const recordDecision = id => { - if (!history[id]) { - history[id] = {}; - } - - if (typeof history[id].timestamp !== "number") { - history[id].timestamp = new Date().getTime(); - save(history); +export default ({ eventRegistry }) => { + const recordQualified = item => { + const { id } = item; + if (!id) { + return undefined; } - return history[id]; + return eventRegistry.addEvent(item, QUALIFIED_EVENT_TYPE, id); }; - return { recordDecision }; + return { recordQualified }; }; diff --git a/src/components/DecisioningEngine/createDecisionProvider.js b/src/components/DecisioningEngine/createDecisionProvider.js index ab3c1ceb6..da01cc4ad 100644 --- a/src/components/DecisioningEngine/createDecisionProvider.js +++ b/src/components/DecisioningEngine/createDecisionProvider.js @@ -12,10 +12,10 @@ governing permissions and limitations under the License. import createEvaluableRulesetPayload from "./createEvaluableRulesetPayload"; import createDecisionHistory from "./createDecisionHistory"; -export default ({ eventRegistry, storage }) => { +export default ({ eventRegistry }) => { const payloads = {}; - const decisionHistory = createDecisionHistory({ storage }); + const decisionHistory = createDecisionHistory({ eventRegistry }); const addPayload = payload => { if (!payload.id) { diff --git a/src/components/DecisioningEngine/createEvaluableRulesetPayload.js b/src/components/DecisioningEngine/createEvaluableRulesetPayload.js index de559371d..80b403aa2 100644 --- a/src/components/DecisioningEngine/createEvaluableRulesetPayload.js +++ b/src/components/DecisioningEngine/createEvaluableRulesetPayload.js @@ -38,8 +38,8 @@ export default (payload, eventRegistry, decisionHistory) => { const qualifyingItems = flattenArray( items.map(item => item.execute(context)) ).map(item => { - const { timestamp: qualifiedDate } = decisionHistory.recordDecision( - item.id + const { firstTimestamp: qualifiedDate } = decisionHistory.recordQualified( + item ); return { ...item.detail, qualifiedDate, displayedDate }; }); diff --git a/src/components/DecisioningEngine/createEventRegistry.js b/src/components/DecisioningEngine/createEventRegistry.js index 417221cc7..256e44ec6 100644 --- a/src/components/DecisioningEngine/createEventRegistry.js +++ b/src/components/DecisioningEngine/createEventRegistry.js @@ -13,6 +13,7 @@ import { createRestoreStorage, createSaveStorage } from "./utils"; const STORAGE_KEY = "events"; const MAX_EVENT_RECORDS = 1000; +const DEFAULT_SAVE_DELAY = 500; export const createEventPruner = (limit = MAX_EVENT_RECORDS) => { return events => { @@ -30,18 +31,42 @@ export const createEventPruner = (limit = MAX_EVENT_RECORDS) => { }; }; -export default ({ storage }) => { +export default ({ storage, saveDelay = DEFAULT_SAVE_DELAY }) => { const restore = createRestoreStorage(storage, STORAGE_KEY); const save = createSaveStorage( storage, STORAGE_KEY, - 150, + saveDelay, createEventPruner(MAX_EVENT_RECORDS) ); const events = restore({}); + const addEvent = (event, eventType, eventId) => { + if (!events[eventType]) { + events[eventType] = {}; + } + + const existingEvent = events[eventType][eventId]; + + const count = existingEvent ? existingEvent.count : 0; + const timestamp = new Date().getTime(); + const firstTimestamp = existingEvent + ? existingEvent.firstTimestamp || existingEvent.timestamp + : timestamp; + + events[eventType][eventId] = { + event: { ...event, id: eventId, type: eventType }, + firstTimestamp, + timestamp, + count: count + 1 + }; + + save(events); - const rememberEvent = event => { + return events[eventType][eventId]; + }; + + const addExperienceEdgeEvent = event => { const { xdm = {} } = event.getContent(); const { eventType = "", _experience } = xdm; @@ -57,31 +82,9 @@ export default ({ storage }) => { const { decisioning = {} } = _experience; const { propositions = [] } = decisioning; - propositions.forEach(proposition => { - let count = 0; - const timestamp = new Date().getTime(); - let firstTimestamp = timestamp; - - if (!events[eventType]) { - events[eventType] = {}; - } - - const existingEvent = events[eventType][proposition.id]; - if (existingEvent) { - count = existingEvent.count; - firstTimestamp = - existingEvent.firstTimestamp || existingEvent.timestamp; - } - - events[eventType][proposition.id] = { - event: { id: proposition.id, type: eventType }, - firstTimestamp, - timestamp, - count: count + 1 - }; - }); - - save(events); + propositions.forEach(proposition => + addEvent({ proposition }, eventType, proposition.id) + ); }; const getEvent = (eventType, eventId) => { if (!events[eventType]) { @@ -91,5 +94,5 @@ export default ({ storage }) => { return events[eventType][eventId]; }; - return { rememberEvent, getEvent, toJSON: () => events }; + return { addExperienceEdgeEvent, addEvent, getEvent, toJSON: () => events }; }; diff --git a/src/components/DecisioningEngine/index.js b/src/components/DecisioningEngine/index.js index eb356227d..9cc6be566 100644 --- a/src/components/DecisioningEngine/index.js +++ b/src/components/DecisioningEngine/index.js @@ -25,8 +25,7 @@ const createDecisioningEngine = ({ config, createNamespacedStorage }) => { const eventRegistry = createEventRegistry({ storage: storage.persistent }); let applyResponse = createApplyResponse(); const decisionProvider = createDecisionProvider({ - eventRegistry, - storage: storage.persistent + eventRegistry }); const contextProvider = createContextProvider({ eventRegistry }); @@ -53,7 +52,7 @@ const createDecisioningEngine = ({ config, createNamespacedStorage }) => { return; } - eventRegistry.rememberEvent(event); + eventRegistry.addExperienceEdgeEvent(event); } }, commands: { diff --git a/src/components/DecisioningEngine/utils.js b/src/components/DecisioningEngine/utils.js index 0894b3ca5..a43763615 100644 --- a/src/components/DecisioningEngine/utils.js +++ b/src/components/DecisioningEngine/utils.js @@ -29,7 +29,7 @@ export const createRestoreStorage = (storage, storageKey) => { export const createSaveStorage = ( storage, storageKey, - debounceDelay = 150, + debounceDelay = 500, prepareFn = value => value ) => { return debounce(value => { diff --git a/test/unit/specs/components/DecisioningEngine/createDecisionHistory.spec.js b/test/unit/specs/components/DecisioningEngine/createDecisionHistory.spec.js index f827feb33..452f783a2 100644 --- a/test/unit/specs/components/DecisioningEngine/createDecisionHistory.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createDecisionHistory.spec.js @@ -1,4 +1,5 @@ import createDecisionHistory from "../../../../../src/components/DecisioningEngine/createDecisionHistory"; +import createEventRegistry from "../../../../../src/components/DecisioningEngine/createEventRegistry"; describe("DecisioningEngine:decisionHistory", () => { let storage; @@ -6,40 +7,46 @@ describe("DecisioningEngine:decisionHistory", () => { beforeEach(() => { storage = jasmine.createSpyObj("storage", ["getItem", "setItem", "clear"]); - history = createDecisionHistory({ storage }); + + history = createDecisionHistory({ + eventRegistry: createEventRegistry({ storage, saveDelay: 10 }) + }); }); it("records decision time", () => { - const decision = history.recordDecision("abc"); + const decision = history.recordQualified({ id: "abc" }); expect(Object.getPrototypeOf(decision)).toEqual(Object.prototype); expect(decision.timestamp).toEqual(jasmine.any(Number)); }); - it("uses prior decision time, if decision already recorded", done => { - const firstDecision = history.recordDecision("abc"); + it("preserves first decision time, if decision already recorded", done => { + const firstDecision = history.recordQualified({ id: "abc" }); setTimeout(() => { - expect(history.recordDecision("abc").timestamp).toEqual( + expect(history.recordQualified({ id: "abc" }).firstTimestamp).toEqual( + firstDecision.firstTimestamp + ); + expect(history.recordQualified({ id: "abc" }).firstTimestamp).toEqual( firstDecision.timestamp ); done(); - }, 200); + }, 20); }); - it("restores history from storage", () => { - expect(storage.getItem).toHaveBeenCalledWith("history"); + it("restores history from event storage", () => { + expect(storage.getItem).toHaveBeenCalledWith("events"); }); - it("saves history to storage", done => { - history.recordDecision("abc"); + it("saves history to event storage", done => { + history.recordQualified({ id: "abc" }); setTimeout(() => { expect(storage.setItem).toHaveBeenCalledWith( - "history", + "events", jasmine.any(String) ); done(); - }, 200); + }, 20); }); }); diff --git a/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js b/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js index 20f1b7db0..57f84e25c 100644 --- a/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js @@ -21,7 +21,7 @@ describe("DecisioningEngine:createDecisionProvider", () => { storage = jasmine.createSpyObj("storage", ["getItem", "setItem", "clear"]); eventRegistry = createEventRegistry({ storage }); - decisionProvider = createDecisionProvider({ eventRegistry, storage }); + decisionProvider = createDecisionProvider({ eventRegistry }); decisionProvider.addPayloads([ { scopeDetails: { diff --git a/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js b/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js index de6d66c1d..08330eca7 100644 --- a/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js @@ -21,7 +21,7 @@ describe("DecisioningEngine:createEvaluableRulesetPayload", () => { beforeEach(() => { storage = jasmine.createSpyObj("storage", ["getItem", "setItem", "clear"]); eventRegistry = createEventRegistry({ storage }); - decisionHistory = createDecisionHistory({ storage }); + decisionHistory = createDecisionHistory({ eventRegistry }); }); it("evaluates rules and return a payload", () => { diff --git a/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js b/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js index ff4cc60c7..59d35113a 100644 --- a/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js @@ -38,24 +38,24 @@ describe("DecisioningEngine:createEventRegistry", () => { getContent }; - eventRegistry.rememberEvent(event); + eventRegistry.addExperienceEdgeEvent(event); expect(eventRegistry.toJSON()).toEqual({ display: { abc: { - event: { id: "abc", type: "display" }, + event: jasmine.objectContaining({ id: "abc", type: "display" }), firstTimestamp: jasmine.any(Number), timestamp: jasmine.any(Number), count: 1 }, def: { - event: { id: "def", type: "display" }, + event: jasmine.objectContaining({ id: "def", type: "display" }), firstTimestamp: jasmine.any(Number), timestamp: jasmine.any(Number), count: 1 }, ghi: { - event: { id: "ghi", type: "display" }, + event: jasmine.objectContaining({ id: "ghi", type: "display" }), firstTimestamp: jasmine.any(Number), timestamp: jasmine.any(Number), count: 1 @@ -67,14 +67,14 @@ describe("DecisioningEngine:createEventRegistry", () => { it("does not register invalid events", () => { const eventRegistry = createEventRegistry({ storage }); - eventRegistry.rememberEvent({ + eventRegistry.addExperienceEdgeEvent({ getContent: () => ({ xdm: { eventType: "display" } }) }); - eventRegistry.rememberEvent({ + eventRegistry.addExperienceEdgeEvent({ getContent: () => ({ xdm: { eventType: "display", @@ -82,7 +82,7 @@ describe("DecisioningEngine:createEventRegistry", () => { } }) }); - eventRegistry.rememberEvent({ + eventRegistry.addExperienceEdgeEvent({ getContent: () => ({ xdm: { eventType: "display", @@ -92,7 +92,7 @@ describe("DecisioningEngine:createEventRegistry", () => { } }) }); - eventRegistry.rememberEvent({ + eventRegistry.addExperienceEdgeEvent({ getContent: () => ({}) }); @@ -100,7 +100,7 @@ describe("DecisioningEngine:createEventRegistry", () => { }); it("increments count and sets timestamp", done => { - const eventRegistry = createEventRegistry({ storage }); + const eventRegistry = createEventRegistry({ storage, saveDelay: 10 }); const getContent = () => ({ xdm: { @@ -117,10 +117,10 @@ describe("DecisioningEngine:createEventRegistry", () => { getContent }; let lastEventTime = 0; - eventRegistry.rememberEvent(event); + eventRegistry.addExperienceEdgeEvent(event); expect(eventRegistry.getEvent("display", "abc")).toEqual({ - event: { id: "abc", type: "display" }, + event: jasmine.objectContaining({ id: "abc", type: "display" }), firstTimestamp: jasmine.any(Number), timestamp: jasmine.any(Number), count: 1 @@ -131,10 +131,10 @@ describe("DecisioningEngine:createEventRegistry", () => { lastEventTime = eventRegistry.getEvent("display", "abc").timestamp; setTimeout(() => { - eventRegistry.rememberEvent(event); // again + eventRegistry.addExperienceEdgeEvent(event); // again expect(eventRegistry.getEvent("display", "abc")).toEqual({ - event: { id: "abc", type: "display" }, + event: jasmine.objectContaining({ id: "abc", type: "display" }), firstTimestamp: jasmine.any(Number), timestamp: jasmine.any(Number), count: 2 @@ -143,7 +143,7 @@ describe("DecisioningEngine:createEventRegistry", () => { eventRegistry.getEvent("display", "abc").timestamp ).toBeGreaterThan(lastEventTime); done(); - }, 10); + }, 50); }); it("limits events to 1000 events", () => { diff --git a/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js b/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js index 454d86a2e..f89d0a271 100644 --- a/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js @@ -28,7 +28,7 @@ describe("DecisioningEngine:createOnResponseHandler", () => { onDecision: Promise.resolve() }); - const decisionProvider = createDecisionProvider({ eventRegistry, storage }); + const decisionProvider = createDecisionProvider({ eventRegistry }); const applyResponse = createApplyResponse(lifecycle); const event = { @@ -214,7 +214,7 @@ describe("DecisioningEngine:createOnResponseHandler", () => { onDecision: Promise.resolve() }); - const decisionProvider = createDecisionProvider({ eventRegistry, storage }); + const decisionProvider = createDecisionProvider({ eventRegistry }); const applyResponse = createApplyResponse(lifecycle); const event = { From 8447c99978c76abc589700d909acd2af78f43d91 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Thu, 4 May 2023 15:25:02 -0600 Subject: [PATCH 19/66] refactor event registry a little --- .../DecisioningEngine/createEventRegistry.js | 61 ++++++++++--------- src/components/DecisioningEngine/index.js | 2 +- src/components/DecisioningEngine/utils.js | 2 +- .../createEventRegistry.spec.js | 28 ++++----- 4 files changed, 48 insertions(+), 45 deletions(-) diff --git a/src/components/DecisioningEngine/createEventRegistry.js b/src/components/DecisioningEngine/createEventRegistry.js index 417221cc7..256e44ec6 100644 --- a/src/components/DecisioningEngine/createEventRegistry.js +++ b/src/components/DecisioningEngine/createEventRegistry.js @@ -13,6 +13,7 @@ import { createRestoreStorage, createSaveStorage } from "./utils"; const STORAGE_KEY = "events"; const MAX_EVENT_RECORDS = 1000; +const DEFAULT_SAVE_DELAY = 500; export const createEventPruner = (limit = MAX_EVENT_RECORDS) => { return events => { @@ -30,18 +31,42 @@ export const createEventPruner = (limit = MAX_EVENT_RECORDS) => { }; }; -export default ({ storage }) => { +export default ({ storage, saveDelay = DEFAULT_SAVE_DELAY }) => { const restore = createRestoreStorage(storage, STORAGE_KEY); const save = createSaveStorage( storage, STORAGE_KEY, - 150, + saveDelay, createEventPruner(MAX_EVENT_RECORDS) ); const events = restore({}); + const addEvent = (event, eventType, eventId) => { + if (!events[eventType]) { + events[eventType] = {}; + } + + const existingEvent = events[eventType][eventId]; + + const count = existingEvent ? existingEvent.count : 0; + const timestamp = new Date().getTime(); + const firstTimestamp = existingEvent + ? existingEvent.firstTimestamp || existingEvent.timestamp + : timestamp; + + events[eventType][eventId] = { + event: { ...event, id: eventId, type: eventType }, + firstTimestamp, + timestamp, + count: count + 1 + }; + + save(events); - const rememberEvent = event => { + return events[eventType][eventId]; + }; + + const addExperienceEdgeEvent = event => { const { xdm = {} } = event.getContent(); const { eventType = "", _experience } = xdm; @@ -57,31 +82,9 @@ export default ({ storage }) => { const { decisioning = {} } = _experience; const { propositions = [] } = decisioning; - propositions.forEach(proposition => { - let count = 0; - const timestamp = new Date().getTime(); - let firstTimestamp = timestamp; - - if (!events[eventType]) { - events[eventType] = {}; - } - - const existingEvent = events[eventType][proposition.id]; - if (existingEvent) { - count = existingEvent.count; - firstTimestamp = - existingEvent.firstTimestamp || existingEvent.timestamp; - } - - events[eventType][proposition.id] = { - event: { id: proposition.id, type: eventType }, - firstTimestamp, - timestamp, - count: count + 1 - }; - }); - - save(events); + propositions.forEach(proposition => + addEvent({ proposition }, eventType, proposition.id) + ); }; const getEvent = (eventType, eventId) => { if (!events[eventType]) { @@ -91,5 +94,5 @@ export default ({ storage }) => { return events[eventType][eventId]; }; - return { rememberEvent, getEvent, toJSON: () => events }; + return { addExperienceEdgeEvent, addEvent, getEvent, toJSON: () => events }; }; diff --git a/src/components/DecisioningEngine/index.js b/src/components/DecisioningEngine/index.js index 6702e0157..c8f21b479 100644 --- a/src/components/DecisioningEngine/index.js +++ b/src/components/DecisioningEngine/index.js @@ -50,7 +50,7 @@ const createDecisioningEngine = ({ config, createNamespacedStorage }) => { return; } - eventRegistry.rememberEvent(event); + eventRegistry.addExperienceEdgeEvent(event); } }, commands: { diff --git a/src/components/DecisioningEngine/utils.js b/src/components/DecisioningEngine/utils.js index e248cec6e..4a44b253f 100644 --- a/src/components/DecisioningEngine/utils.js +++ b/src/components/DecisioningEngine/utils.js @@ -28,7 +28,7 @@ export const createRestoreStorage = (storage, storageKey) => { export const createSaveStorage = ( storage, storageKey, - debounceDelay = 150, + debounceDelay = 500, prepareFn = value => value ) => { return debounce(value => { diff --git a/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js b/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js index ff4cc60c7..59d35113a 100644 --- a/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js @@ -38,24 +38,24 @@ describe("DecisioningEngine:createEventRegistry", () => { getContent }; - eventRegistry.rememberEvent(event); + eventRegistry.addExperienceEdgeEvent(event); expect(eventRegistry.toJSON()).toEqual({ display: { abc: { - event: { id: "abc", type: "display" }, + event: jasmine.objectContaining({ id: "abc", type: "display" }), firstTimestamp: jasmine.any(Number), timestamp: jasmine.any(Number), count: 1 }, def: { - event: { id: "def", type: "display" }, + event: jasmine.objectContaining({ id: "def", type: "display" }), firstTimestamp: jasmine.any(Number), timestamp: jasmine.any(Number), count: 1 }, ghi: { - event: { id: "ghi", type: "display" }, + event: jasmine.objectContaining({ id: "ghi", type: "display" }), firstTimestamp: jasmine.any(Number), timestamp: jasmine.any(Number), count: 1 @@ -67,14 +67,14 @@ describe("DecisioningEngine:createEventRegistry", () => { it("does not register invalid events", () => { const eventRegistry = createEventRegistry({ storage }); - eventRegistry.rememberEvent({ + eventRegistry.addExperienceEdgeEvent({ getContent: () => ({ xdm: { eventType: "display" } }) }); - eventRegistry.rememberEvent({ + eventRegistry.addExperienceEdgeEvent({ getContent: () => ({ xdm: { eventType: "display", @@ -82,7 +82,7 @@ describe("DecisioningEngine:createEventRegistry", () => { } }) }); - eventRegistry.rememberEvent({ + eventRegistry.addExperienceEdgeEvent({ getContent: () => ({ xdm: { eventType: "display", @@ -92,7 +92,7 @@ describe("DecisioningEngine:createEventRegistry", () => { } }) }); - eventRegistry.rememberEvent({ + eventRegistry.addExperienceEdgeEvent({ getContent: () => ({}) }); @@ -100,7 +100,7 @@ describe("DecisioningEngine:createEventRegistry", () => { }); it("increments count and sets timestamp", done => { - const eventRegistry = createEventRegistry({ storage }); + const eventRegistry = createEventRegistry({ storage, saveDelay: 10 }); const getContent = () => ({ xdm: { @@ -117,10 +117,10 @@ describe("DecisioningEngine:createEventRegistry", () => { getContent }; let lastEventTime = 0; - eventRegistry.rememberEvent(event); + eventRegistry.addExperienceEdgeEvent(event); expect(eventRegistry.getEvent("display", "abc")).toEqual({ - event: { id: "abc", type: "display" }, + event: jasmine.objectContaining({ id: "abc", type: "display" }), firstTimestamp: jasmine.any(Number), timestamp: jasmine.any(Number), count: 1 @@ -131,10 +131,10 @@ describe("DecisioningEngine:createEventRegistry", () => { lastEventTime = eventRegistry.getEvent("display", "abc").timestamp; setTimeout(() => { - eventRegistry.rememberEvent(event); // again + eventRegistry.addExperienceEdgeEvent(event); // again expect(eventRegistry.getEvent("display", "abc")).toEqual({ - event: { id: "abc", type: "display" }, + event: jasmine.objectContaining({ id: "abc", type: "display" }), firstTimestamp: jasmine.any(Number), timestamp: jasmine.any(Number), count: 2 @@ -143,7 +143,7 @@ describe("DecisioningEngine:createEventRegistry", () => { eventRegistry.getEvent("display", "abc").timestamp ).toBeGreaterThan(lastEventTime); done(); - }, 10); + }, 50); }); it("limits events to 1000 events", () => { From 788722c0e26d4f27304d56411de98a50b0d49eeb Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Fri, 5 May 2023 14:46:04 -0600 Subject: [PATCH 20/66] provide collect function to messaging actions --- src/components/Personalization/createModules.js | 4 ++-- .../in-app-message-actions/initMessagingActionsModules.js | 7 ++++--- src/components/Personalization/index.js | 2 +- .../specs/components/Personalization/createModules.spec.js | 7 +++++-- .../initMessagingActionsModules.spec.js | 3 ++- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/components/Personalization/createModules.js b/src/components/Personalization/createModules.js index a0d2817e3..017b9d034 100644 --- a/src/components/Personalization/createModules.js +++ b/src/components/Personalization/createModules.js @@ -13,9 +13,9 @@ import { DOM_ACTION, IN_APP_MESSAGE } from "./constants/schema"; import { initDomActionsModules } from "./dom-actions"; import initMessagingActionsModules from "./in-app-message-actions/initMessagingActionsModules"; -export default storeClickMetrics => { +export default ({ storeClickMetrics, collect }) => { return { [DOM_ACTION]: initDomActionsModules(storeClickMetrics), - [IN_APP_MESSAGE]: initMessagingActionsModules(storeClickMetrics) + [IN_APP_MESSAGE]: initMessagingActionsModules(collect) }; }; diff --git a/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js b/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js index 4beea254f..7f225aa31 100644 --- a/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js +++ b/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js @@ -3,9 +3,10 @@ import displayModal from "./actions/displayModal"; import displayBanner from "./actions/displayBanner"; -export default store => { +export default collect => { + // TODO: use collect to capture click and display metrics return { - modal: settings => displayModal(settings), - banner: settings => displayBanner(settings) + modal: settings => displayModal(settings, collect), + banner: settings => displayBanner(settings, collect) }; }; diff --git a/src/components/Personalization/index.js b/src/components/Personalization/index.js index 5fc4a4577..4cf7b0675 100644 --- a/src/components/Personalization/index.js +++ b/src/components/Personalization/index.js @@ -49,7 +49,7 @@ const createPersonalization = ({ config, logger, eventManager }) => { const viewCache = createViewCacheManager(); const actionsProvider = createActionsProvider({ - modules: createModules(storeClickMetrics), + modules: createModules({ storeClickMetrics, collect }), preprocessors: createPreprocessors(), logger }); diff --git a/test/unit/specs/components/Personalization/createModules.spec.js b/test/unit/specs/components/Personalization/createModules.spec.js index a005bdbf4..0b4abedfe 100644 --- a/test/unit/specs/components/Personalization/createModules.spec.js +++ b/test/unit/specs/components/Personalization/createModules.spec.js @@ -16,8 +16,10 @@ import { } from "../../../../../src/components/Personalization/constants/schema"; describe("createModules", () => { + const noop = () => undefined; + it("has dom-action modules", () => { - const modules = createModules(() => undefined); + const modules = createModules({ storeClickMetrics: noop, collect: noop }); expect(modules[DOM_ACTION]).toEqual({ setHtml: jasmine.any(Function), @@ -41,8 +43,9 @@ describe("createModules", () => { expect(Object.keys(modules[DOM_ACTION]).length).toEqual(17); }); + it("has in-app-message modules", () => { - const modules = createModules(() => undefined); + const modules = createModules({ storeClickMetrics: noop, collect: noop }); expect(modules[IN_APP_MESSAGE]).toEqual({ modal: jasmine.any(Function), diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/initMessagingActionsModules.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/initMessagingActionsModules.spec.js index ab92659d6..600413ccd 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/initMessagingActionsModules.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/initMessagingActionsModules.spec.js @@ -15,7 +15,8 @@ import createModules from "../../../../../../src/components/Personalization/crea import { IN_APP_MESSAGE } from "../../../../../../src/components/Personalization/constants/schema"; describe("Personalization::turbine::initMessagingActionsModules", () => { - const modules = createModules(() => undefined); + const noop = () => undefined; + const modules = createModules({ storeClickMetrics: noop, collect: noop }); const expectedModules = modules[IN_APP_MESSAGE]; it("should have all the required modules", () => { From 4c5d3c3f6ed0e9b0a15701b23ac0b9be829cb75a Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Fri, 5 May 2023 15:07:55 -0600 Subject: [PATCH 21/66] fix package-lock.json --- package-lock.json | 120 +++------------------------------------------- 1 file changed, 7 insertions(+), 113 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5ef3acfdf..fb0e6b950 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "2.16.0", "license": "Apache-2.0", "dependencies": { - "@adobe/aep-rules-engine": "file:../aepsdk-rulesengine-typescript", + "@adobe/aep-rules-engine": "^2.0.1", "@adobe/reactor-cookie": "^1.0.0", "@adobe/reactor-load-script": "^1.1.1", "@adobe/reactor-object-assign": "^1.0.0", @@ -82,85 +82,10 @@ "yargs": "^16.2.0" } }, - "../aepsdk-rulesengine-javascript": { - "name": "@adobe/aep-rules-engine", - "version": "1.0.0", - "extraneous": true, - "license": "Apache-2.0", - "devDependencies": { - "@babel/core": "^7.21.3", - "@babel/preset-env": "^7.20.2", - "@babel/preset-typescript": "^7.21.0", - "@lwc/eslint-plugin-lwc": "^1.6.2", - "@rollup/plugin-babel": "^6.0.3", - "@rollup/plugin-node-resolve": "^15.0.1", - "@rollup/plugin-replace": "^5.0.2", - "@rollup/plugin-terser": "^0.4.0", - "@types/jest": "^29.5.0", - "@typescript-eslint/eslint-plugin": "^5.57.0", - "@typescript-eslint/parser": "^5.57.0", - "babel-jest": "^29.5.0", - "eslint": "^8.36.0", - "eslint-config-airbnb-typescript": "^17.0.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jest": "^27.2.1", - "eslint-plugin-n": "^15.6.1", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-promise": "^6.1.1", - "handlebars": "^4.7.7", - "husky": "^8.0.0", - "jest": "^29.5.0", - "prettier": "^2.8.4", - "pretty-quick": "^3.1.3", - "rimraf": "^4.4.0", - "rollup": "^3.19.1", - "rollup-plugin-cleanup": "^3.2.1", - "rollup-plugin-typescript2": "^0.34.1", - "staged-git-files": "^1.3.0", - "typescript": "^5.0.2" - } - }, - "../aepsdk-rulesengine-typescript": { - "version": "1.0.0", - "license": "Apache-2.0", - "devDependencies": { - "@babel/core": "^7.21.3", - "@babel/preset-env": "^7.20.2", - "@babel/preset-typescript": "^7.21.0", - "@lwc/eslint-plugin-lwc": "^1.6.2", - "@rollup/plugin-babel": "^6.0.3", - "@rollup/plugin-node-resolve": "^15.0.1", - "@rollup/plugin-replace": "^5.0.2", - "@rollup/plugin-terser": "^0.4.0", - "@types/jest": "^29.5.0", - "@typescript-eslint/eslint-plugin": "^5.57.0", - "@typescript-eslint/parser": "^5.57.0", - "babel-jest": "^29.5.0", - "eslint": "^8.36.0", - "eslint-config-airbnb-typescript": "^17.0.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jest": "^27.2.1", - "eslint-plugin-n": "^15.6.1", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-promise": "^6.1.1", - "handlebars": "^4.7.7", - "husky": "^8.0.0", - "jest": "^29.5.0", - "prettier": "^2.8.4", - "pretty-quick": "^3.1.3", - "rimraf": "^4.4.0", - "rollup": "^3.19.1", - "rollup-plugin-cleanup": "^3.2.1", - "rollup-plugin-typescript2": "^0.34.1", - "staged-git-files": "^1.3.0", - "typescript": "^5.0.2" - } - }, "node_modules/@adobe/aep-rules-engine": { - "resolved": "../aepsdk-rulesengine-typescript", - "link": true + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@adobe/aep-rules-engine/-/aep-rules-engine-2.0.1.tgz", + "integrity": "sha512-lcnxd8SN4D7/4A9pOivH9BGzhWjCkcDimI97vhlqWkA0nP2/NzScbG1OR4W2adXUcMYkB18suad9vXmpwqa4YA==" }, "node_modules/@adobe/alloy": { "version": "2.16.0", @@ -15340,40 +15265,9 @@ }, "dependencies": { "@adobe/aep-rules-engine": { - "version": "file:../aepsdk-rulesengine-typescript", - "requires": { - "@babel/core": "^7.21.3", - "@babel/preset-env": "^7.20.2", - "@babel/preset-typescript": "^7.21.0", - "@lwc/eslint-plugin-lwc": "^1.6.2", - "@rollup/plugin-babel": "^6.0.3", - "@rollup/plugin-node-resolve": "^15.0.1", - "@rollup/plugin-replace": "^5.0.2", - "@rollup/plugin-terser": "^0.4.0", - "@types/jest": "^29.5.0", - "@typescript-eslint/eslint-plugin": "^5.57.0", - "@typescript-eslint/parser": "^5.57.0", - "babel-jest": "^29.5.0", - "eslint": "^8.36.0", - "eslint-config-airbnb-typescript": "^17.0.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jest": "^27.2.1", - "eslint-plugin-n": "^15.6.1", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-promise": "^6.1.1", - "handlebars": "^4.7.7", - "husky": "^8.0.0", - "jest": "^29.5.0", - "prettier": "^2.8.4", - "pretty-quick": "^3.1.3", - "rimraf": "^4.4.0", - "rollup": "^3.19.1", - "rollup-plugin-cleanup": "^3.2.1", - "rollup-plugin-typescript2": "^0.34.1", - "staged-git-files": "^1.3.0", - "typescript": "^5.0.2" - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@adobe/aep-rules-engine/-/aep-rules-engine-2.0.1.tgz", + "integrity": "sha512-lcnxd8SN4D7/4A9pOivH9BGzhWjCkcDimI97vhlqWkA0nP2/NzScbG1OR4W2adXUcMYkB18suad9vXmpwqa4YA==" }, "@adobe/alloy": { "version": "2.16.0", From 14d7130e00a489dd382639bce6b0edc2f41660d4 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Fri, 5 May 2023 15:18:22 -0600 Subject: [PATCH 22/66] fix debounce test --- test/unit/specs/utils/debounce.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/specs/utils/debounce.spec.js b/test/unit/specs/utils/debounce.spec.js index 6d3b8e411..8a026a56c 100644 --- a/test/unit/specs/utils/debounce.spec.js +++ b/test/unit/specs/utils/debounce.spec.js @@ -22,7 +22,7 @@ describe("debounce", () => { }); it("calls a function only once per delay period", done => { - const fn = debounce(callback, 20); + const fn = debounce(callback, 10); fn("oh", "hai"); fn("oh", "hai"); From dd8e2ddbb14c2c6d9b1c344a09d2ddff8e5d281a Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Tue, 9 May 2023 17:08:17 -0600 Subject: [PATCH 23/66] applyResponse initialized once --- src/components/DecisioningEngine/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DecisioningEngine/index.js b/src/components/DecisioningEngine/index.js index c8f21b479..7841de2bf 100644 --- a/src/components/DecisioningEngine/index.js +++ b/src/components/DecisioningEngine/index.js @@ -23,7 +23,7 @@ const createDecisioningEngine = ({ config, createNamespacedStorage }) => { ); const eventRegistry = createEventRegistry({ storage: storage.persistent }); - let applyResponse = createApplyResponse(); + let applyResponse; const decisionProvider = createDecisionProvider(); const contextProvider = createContextProvider({ eventRegistry }); From a4e280a1f839144fd7221d78944d7a770a7352f7 Mon Sep 17 00:00:00 2001 From: Happy Shandilya Date: Mon, 22 May 2023 10:59:14 -0700 Subject: [PATCH 24/66] CJM-45417-globalDecisionContext (#986) * Setting global decisionContext with data like current date/time, date/time page loaded, browser name/version, scroll position, etc * update time and window context on each getContext call, parse url using lib Todo: fix unit tests, add more tests and flattenObject * dependency * code refactoring based on code review * add-license * include the package-lock.json file in the pull request * pass in userAgent in custom window object * mock to ensure that the date remains the same across different timezones * flattening the context object so that rules can apply * test to check if flattening is applied and rules conditions are met for onDecision with propositions to be called * applied fix * a little structured tests * following convention * evaluates global contexts * tests breakdown * tests * test global context separately * context flattened * mocking time, Jasmine clock does not inherently take into account different time zones * reverted redundant tests from other files * flattenObject only needs to be called once. * narrowed the scope of the test for specificity * Revert "narrowed the scope of the test for specificity" This reverts commit 3939b611bfd681a92767f90670564e941fc1a78f. * narrowed the scope of the test for specificity * extracted reused methods * unit test for renderDecisions command * add license * run for all the browser with renderDecisions * validate a global context with command * validate a global context with command * always index rulesets --------- Co-authored-by: Jason Waters --- package-lock.json | 14 + package.json | 1 + .../createContextProvider.js | 73 +++- .../createOnResponseHandler.js | 14 +- src/components/DecisioningEngine/index.js | 26 +- src/utils/index.js | 1 + src/utils/parseUrl.js | 64 ++++ .../DecisioningEngine/contextTestUtils.js | 136 +++++++ .../createContextProvider.spec.js | 153 +++++++- .../createOnResponseHandler.spec.js | 2 + .../decisioningContext.browser.spec.js | 56 +++ .../decisioningContext.page.spec.js | 345 ++++++++++++++++++ .../decisioningContext.referringPage.spec.js | 288 +++++++++++++++ .../decisioningContext.timestamp.spec.js | 334 +++++++++++++++++ .../decisioningContext.window.spec.js | 185 ++++++++++ .../DecisioningEngine/index.spec.js | 135 +++++++ test/unit/specs/utils/debounce.spec.js | 11 + test/unit/specs/utils/parseUrl.spec.js | 63 ++++ 18 files changed, 1871 insertions(+), 30 deletions(-) create mode 100644 src/utils/parseUrl.js create mode 100644 test/unit/specs/components/DecisioningEngine/contextTestUtils.js create mode 100644 test/unit/specs/components/DecisioningEngine/decisioningContext.browser.spec.js create mode 100644 test/unit/specs/components/DecisioningEngine/decisioningContext.page.spec.js create mode 100644 test/unit/specs/components/DecisioningEngine/decisioningContext.referringPage.spec.js create mode 100644 test/unit/specs/components/DecisioningEngine/decisioningContext.timestamp.spec.js create mode 100644 test/unit/specs/components/DecisioningEngine/decisioningContext.window.spec.js create mode 100644 test/unit/specs/components/DecisioningEngine/index.spec.js create mode 100644 test/unit/specs/utils/parseUrl.spec.js diff --git a/package-lock.json b/package-lock.json index c4fa1ef4b..23348b89b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@adobe/reactor-object-assign": "^1.0.0", "@adobe/reactor-query-string": "^1.0.0", "css.escape": "^1.5.1", + "parse-uri": "^1.0.7", "uuid": "^3.3.2" }, "devDependencies": { @@ -10657,6 +10658,14 @@ "node": ">=4" } }, + "node_modules/parse-uri": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/parse-uri/-/parse-uri-1.0.7.tgz", + "integrity": "sha512-eWuZCMKNlVkXrEoANdXxbmqhu2SQO9jUMCSpdbJDObin0JxISn6e400EWsSRbr/czdKvWKkhZnMKEGUwf/Plmg==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/parse5": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz", @@ -23471,6 +23480,11 @@ "json-parse-better-errors": "^1.0.1" } }, + "parse-uri": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/parse-uri/-/parse-uri-1.0.7.tgz", + "integrity": "sha512-eWuZCMKNlVkXrEoANdXxbmqhu2SQO9jUMCSpdbJDObin0JxISn6e400EWsSRbr/czdKvWKkhZnMKEGUwf/Plmg==" + }, "parse5": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz", diff --git a/package.json b/package.json index b495e24c9..6e5b4db8b 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "@adobe/reactor-object-assign": "^1.0.0", "@adobe/reactor-query-string": "^1.0.0", "css.escape": "^1.5.1", + "parse-uri": "^1.0.7", "uuid": "^3.3.2" }, "devDependencies": { diff --git a/src/components/DecisioningEngine/createContextProvider.js b/src/components/DecisioningEngine/createContextProvider.js index 33b839100..91c81b449 100644 --- a/src/components/DecisioningEngine/createContextProvider.js +++ b/src/components/DecisioningEngine/createContextProvider.js @@ -9,16 +9,81 @@ 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 ({ eventRegistry }) => { - const globalContext = {}; // holder of global context like current time/date, browser name/version, day of week, scroll position, time on page, etc +import getBrowser from "../../utils/getBrowser"; +import parseUrl from "../../utils/parseUrl"; + +export default ({ eventRegistry, window }) => { + const pageLoadTimestamp = new Date().getTime(); + const getBrowserContext = () => { + return { + name: getBrowser(window) + }; + }; + const getPageContext = () => { + return { + title: window.title, + url: window.url, + ...parseUrl(window.url) + }; + }; + + const getReferrerContext = () => { + return { + url: window.referrer, + ...parseUrl(window.referrer) + }; + }; + const getTimeContext = () => { + const now = new Date(); + const currentTimestamp = now.getTime(); + + return { + pageLoadTimestamp, + currentTimestamp, + currentDate: now.getDate(), + currentDay: now.getDay(), + currentHour: now.getHours(), + currentMinute: now.getMinutes(), + currentMonth: now.getMonth(), + currentYear: now.getFullYear(), + pageVisitDuration: currentTimestamp - pageLoadTimestamp + }; + }; + + const getWindowContext = () => { + const height = window.height; + const width = window.width; + const scrollY = window.scrollY; + const scrollX = window.scrollX; + return { + height, + width, + scrollY, + scrollX + }; + }; + + const coreGlobalContext = { + browser: getBrowserContext(), + page: getPageContext(), + referringPage: getReferrerContext() + }; + + const getGlobalContext = () => { + return { + ...coreGlobalContext, + ...getTimeContext(), + window: getWindowContext() + }; + }; + const getContext = addedContext => { return { - ...globalContext, + ...getGlobalContext(), ...addedContext, events: eventRegistry.toJSON() }; }; - return { getContext }; diff --git a/src/components/DecisioningEngine/createOnResponseHandler.js b/src/components/DecisioningEngine/createOnResponseHandler.js index ead5b97d3..cb9d7c40b 100644 --- a/src/components/DecisioningEngine/createOnResponseHandler.js +++ b/src/components/DecisioningEngine/createOnResponseHandler.js @@ -13,23 +13,25 @@ import { PERSONALIZATION_DECISIONS_HANDLE } from "../Personalization/constants/h import flattenObject from "../../utils/flattenObject"; export default ({ + renderDecisions, decisionProvider, applyResponse, event, decisionContext }) => { - const context = { - ...flattenObject(event.getContent()), + const context = flattenObject({ + ...event.getContent(), ...decisionContext - }; - + }); const viewName = event.getViewName(); return ({ response }) => { decisionProvider.addPayloads( response.getPayloadsByType(PERSONALIZATION_DECISIONS_HANDLE) ); - const propositions = decisionProvider.evaluate(context); - applyResponse({ viewName, propositions }); + if (renderDecisions) { + const propositions = decisionProvider.evaluate(context); + applyResponse({ viewName, propositions }); + } }; }; diff --git a/src/components/DecisioningEngine/index.js b/src/components/DecisioningEngine/index.js index 7841de2bf..3c2b028dc 100644 --- a/src/components/DecisioningEngine/index.js +++ b/src/components/DecisioningEngine/index.js @@ -15,17 +15,17 @@ import createDecisionProvider from "./createDecisionProvider"; import createApplyResponse from "./createApplyResponse"; import createEventRegistry from "./createEventRegistry"; import createContextProvider from "./createContextProvider"; +import flattenObject from "../../utils/flattenObject"; const createDecisioningEngine = ({ config, createNamespacedStorage }) => { const { orgId } = config; const storage = createNamespacedStorage( `${sanitizeOrgIdForCookieName(orgId)}.decisioning.` ); - const eventRegistry = createEventRegistry({ storage: storage.persistent }); let applyResponse; const decisionProvider = createDecisionProvider(); - const contextProvider = createContextProvider({ eventRegistry }); + const contextProvider = createContextProvider({ eventRegistry, window }); return { lifecycle: { @@ -38,17 +38,15 @@ const createDecisioningEngine = ({ config, createNamespacedStorage }) => { decisionContext = {}, onResponse = noop }) { - if (renderDecisions) { - onResponse( - createOnResponseHandler({ - decisionProvider, - applyResponse, - event, - decisionContext: contextProvider.getContext(decisionContext) - }) - ); - return; - } + onResponse( + createOnResponseHandler({ + renderDecisions, + decisionProvider, + applyResponse, + event, + decisionContext: contextProvider.getContext(decisionContext) + }) + ); eventRegistry.addExperienceEdgeEvent(event); } @@ -58,7 +56,7 @@ const createDecisioningEngine = ({ config, createNamespacedStorage }) => { run: decisionContext => applyResponse({ propositions: decisionProvider.evaluate( - contextProvider.getContext(decisionContext) + flattenObject(contextProvider.getContext(decisionContext)) ) }) } diff --git a/src/utils/index.js b/src/utils/index.js index 927e3a28b..098570284 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -51,6 +51,7 @@ export { default as isString } from "./isString"; export { default as memoize } from "./memoize"; export { default as noop } from "./noop"; export { default as padStart } from "./padStart"; +export { default as parseUrl } from "./parseUrl"; export { default as prepareConfigOverridesForEdge } from "./prepareConfigOverridesForEdge"; export { default as queryString } from "./querystring"; export { default as sanitizeOrgIdForCookieName } from "./sanitizeOrgIdForCookieName"; diff --git a/src/utils/parseUrl.js b/src/utils/parseUrl.js new file mode 100644 index 000000000..4ff08e204 --- /dev/null +++ b/src/utils/parseUrl.js @@ -0,0 +1,64 @@ +/* +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 parseUri from "parse-uri"; +import isString from "./isString"; + +const parseDomainBasic = host => { + const result = {}; + const domainParts = host.split("."); + + switch (domainParts.length) { + case 1: + result.subdomain = ""; + result.domain = host; + result.topLevelDomain = ""; + break; + case 2: + result.subdomain = ""; + result.domain = host; + result.topLevelDomain = domainParts[1]; + break; + case 3: + result.subdomain = domainParts[0] === "www" ? "" : domainParts[0]; + result.domain = host; + result.topLevelDomain = domainParts[2]; + break; + case 4: + result.subdomain = domainParts[0] === "www" ? "" : domainParts[0]; + result.domain = host; + result.topLevelDomain = `${domainParts[2]}.${domainParts[3]}`; + break; + default: + break; + } + + return result; +}; + +const parseUrl = (url, parseDomain = parseDomainBasic) => { + if (!isString(url)) { + // eslint-disable-next-line no-param-reassign + url = ""; + } + const parsed = parseUri(url) || {}; + + const { host = "", path = "", query = "", anchor = "" } = parsed; + + return { + path, + query, + fragment: anchor, + ...parseDomain(host) + }; +}; + +export default parseUrl; diff --git a/test/unit/specs/components/DecisioningEngine/contextTestUtils.js b/test/unit/specs/components/DecisioningEngine/contextTestUtils.js new file mode 100644 index 000000000..9efac36b0 --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/contextTestUtils.js @@ -0,0 +1,136 @@ +/* +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 createContextProvider from "../../../../../src/components/DecisioningEngine/createContextProvider"; +import createOnResponseHandler from "../../../../../src/components/DecisioningEngine/createOnResponseHandler"; +import createEventRegistry from "../../../../../src/components/DecisioningEngine/createEventRegistry"; +import createDecisionProvider from "../../../../../src/components/DecisioningEngine/createDecisionProvider"; + +export const proposition = { + id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", + items: [ + { + schema: "https://ns.adobe.com/personalization/mock-action", + data: { + hello: "kitty" + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + } + ], + scope: "web://mywebsite.com" +}; + +export const mockWindow = ({ + title = "My awesome website", + referrer = "https://www.google.com/search?q=adobe+journey+optimizer&oq=adobe+journey+optimizer", + url = "https://pro.mywebsite.org:8080/about?m=1&t=5&name=jimmy#home", + width = 100, + height = 100, + scrollX = 0, + scrollY = 10, + userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36" +}) => ({ + title, + referrer, + url, + width, + height, + scrollX, + scrollY, + navigator: { + userAgent + } +}); + +export const payloadWithCondition = condition => { + return { + id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", + items: [ + { + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", + schema: "https://ns.adobe.com/personalization/json-ruleset-item", + data: { + content: JSON.stringify({ + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [condition], + logic: "and" + }, + type: "group" + }, + consequences: [ + { + type: "item", + detail: { + schema: + "https://ns.adobe.com/personalization/mock-action", + data: { + hello: "kitty" + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + } + ] + } + ] + }) + } + } + ], + scope: "web://mywebsite.com" + }; +}; +export const mockRulesetResponseWithCondition = condition => { + return { + getPayloadsByType: () => [ + payloadWithCondition({ + definition: { + conditions: [condition], + logic: "and" + }, + type: "group" + }) + ] + }; +}; + +const mockEvent = { getContent: () => ({}), getViewName: () => undefined }; + +export const setupResponseHandler = (applyResponse, window, condition) => { + const storage = jasmine.createSpyObj("storage", [ + "getItem", + "setItem", + "clear" + ]); + const eventRegistry = createEventRegistry({ storage }); + const decisionProvider = createDecisionProvider(); + + const contextProvider = createContextProvider({ + eventRegistry, + window + }); + + const onResponseHandler = createOnResponseHandler({ + renderDecisions: true, + decisionProvider, + applyResponse, + event: mockEvent, + decisionContext: contextProvider.getContext() + }); + + onResponseHandler({ + response: mockRulesetResponseWithCondition(condition) + }); +}; diff --git a/test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js b/test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js index 74debedb8..5a8b5aad9 100644 --- a/test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js @@ -15,20 +15,125 @@ import createEventRegistry from "../../../../../src/components/DecisioningEngine describe("DecisioningEngine:createContextProvider", () => { let contextProvider; let eventRegistry; - let storage; + let window; + let mockedTimestamp; beforeEach(() => { storage = jasmine.createSpyObj("storage", ["getItem", "setItem", "clear"]); + window = { + title: "My awesome website", + referrer: "https://stage.applookout.net/", + url: "https://my.web-site.net:8080/about?m=1&t=5&name=jimmy#home", + width: 100, + height: 100, + scrollX: 10, + scrollY: 10, + navigator: { + userAgent: + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36" + } + }; + mockedTimestamp = new Date(Date.UTC(2023, 4, 11, 12, 34, 56)); + jasmine.clock().install(); + jasmine.clock().mockDate(mockedTimestamp); + }); + + afterEach(() => { + jasmine.clock().uninstall(); + }); + it("returns page context", () => { + eventRegistry = createEventRegistry({ storage }); + contextProvider = createContextProvider({ eventRegistry, window }); + + expect(contextProvider.getContext().page).toEqual({ + title: "My awesome website", + url: "https://my.web-site.net:8080/about?m=1&t=5&name=jimmy#home", + path: "/about", + query: "m=1&t=5&name=jimmy", + fragment: "home", + domain: "my.web-site.net", + subdomain: "my", + topLevelDomain: "net" + }); }); + it("returns referring page context", () => { + eventRegistry = createEventRegistry({ storage }); + contextProvider = createContextProvider({ eventRegistry, window }); + expect(contextProvider.getContext().referringPage).toEqual({ + url: "https://stage.applookout.net/", + path: "/", + query: "", + fragment: "", + domain: "stage.applookout.net", + subdomain: "stage", + topLevelDomain: "net" + }); + }); + it("returns browser context", () => { + eventRegistry = createEventRegistry({ storage }); + contextProvider = createContextProvider({ eventRegistry, window }); + + expect(contextProvider.getContext().browser).toEqual({ + name: "Chrome" + }); + }); + it("returns windows context", () => { + eventRegistry = createEventRegistry({ storage }); + contextProvider = createContextProvider({ eventRegistry, window }); + + expect(contextProvider.getContext().window).toEqual({ + height: 100, + width: 100, + scrollY: 10, + scrollX: 10 + }); + }); it("includes provided context passed in", () => { eventRegistry = createEventRegistry({ storage }); - contextProvider = createContextProvider({ eventRegistry }); + contextProvider = createContextProvider({ eventRegistry, window }); expect(contextProvider.getContext({ cool: "beans" })).toEqual({ cool: "beans", - events: {} + events: {}, + currentTimestamp: mockedTimestamp.getTime(), + currentHour: mockedTimestamp.getHours(), + currentMinute: mockedTimestamp.getMinutes(), + currentYear: mockedTimestamp.getFullYear(), + currentMonth: mockedTimestamp.getMonth(), + currentDate: mockedTimestamp.getDate(), + currentDay: mockedTimestamp.getDay(), + pageLoadTimestamp: mockedTimestamp.getTime(), + pageVisitDuration: 0, + browser: { + name: "Chrome" + }, + window: { + height: 100, + width: 100, + scrollY: 10, + scrollX: 10 + }, + page: { + title: "My awesome website", + url: "https://my.web-site.net:8080/about?m=1&t=5&name=jimmy#home", + path: "/about", + query: "m=1&t=5&name=jimmy", + fragment: "home", + domain: "my.web-site.net", + subdomain: "my", + topLevelDomain: "net" + }, + referringPage: { + url: "https://stage.applookout.net/", + path: "/", + query: "", + fragment: "", + domain: "stage.applookout.net", + subdomain: "stage", + topLevelDomain: "net" + } }); }); @@ -40,15 +145,51 @@ describe("DecisioningEngine:createContextProvider", () => { count: 1 } }; - eventRegistry = { toJSON: () => events }; - contextProvider = createContextProvider({ eventRegistry }); + contextProvider = createContextProvider({ eventRegistry, window }); expect(contextProvider.getContext({ cool: "beans" })).toEqual({ cool: "beans", - events + events, + currentTimestamp: mockedTimestamp.getTime(), + currentHour: mockedTimestamp.getHours(), + currentMinute: mockedTimestamp.getMinutes(), + currentYear: mockedTimestamp.getFullYear(), + currentMonth: mockedTimestamp.getMonth(), + currentDate: mockedTimestamp.getDate(), + currentDay: mockedTimestamp.getDay(), + pageLoadTimestamp: mockedTimestamp.getTime(), + pageVisitDuration: 0, + browser: { + name: "Chrome" + }, + window: { + height: 100, + width: 100, + scrollY: 10, + scrollX: 10 + }, + page: { + title: "My awesome website", + url: "https://my.web-site.net:8080/about?m=1&t=5&name=jimmy#home", + path: "/about", + query: "m=1&t=5&name=jimmy", + fragment: "home", + domain: "my.web-site.net", + subdomain: "my", + topLevelDomain: "net" + }, + referringPage: { + url: "https://stage.applookout.net/", + path: "/", + query: "", + fragment: "", + domain: "stage.applookout.net", + subdomain: "stage", + topLevelDomain: "net" + } }); }); }); diff --git a/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js b/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js index f4f4abef3..c2266a6ad 100644 --- a/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js @@ -54,6 +54,7 @@ describe("DecisioningEngine:createOnResponseHandler", () => { }; const responseHandler = createOnResponseHandler({ + renderDecisions: true, decisionProvider, applyResponse, event, @@ -233,6 +234,7 @@ describe("DecisioningEngine:createOnResponseHandler", () => { const decisionContext = {}; const responseHandler = createOnResponseHandler({ + renderDecisions: true, decisionProvider, applyResponse, event, diff --git a/test/unit/specs/components/DecisioningEngine/decisioningContext.browser.spec.js b/test/unit/specs/components/DecisioningEngine/decisioningContext.browser.spec.js new file mode 100644 index 000000000..e95574d8c --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/decisioningContext.browser.spec.js @@ -0,0 +1,56 @@ +/* +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 { + mockWindow, + setupResponseHandler, + proposition +} from "./contextTestUtils"; + +describe("DecisioningEngine:globalContext:browser", () => { + let applyResponse; + beforeEach(() => { + applyResponse = jasmine.createSpy(); + }); + it("satisfies rule based on matched browser", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "browser.name", + matcher: "eq", + values: ["chrome"] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched browser", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "browser.name", + matcher: "co", + values: ["Edge"] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); +}); diff --git a/test/unit/specs/components/DecisioningEngine/decisioningContext.page.spec.js b/test/unit/specs/components/DecisioningEngine/decisioningContext.page.spec.js new file mode 100644 index 000000000..c6a51c6e2 --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/decisioningContext.page.spec.js @@ -0,0 +1,345 @@ +/* +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 { + mockWindow, + setupResponseHandler, + proposition +} from "./contextTestUtils"; + +describe("DecisioningEngine:globalContext:page", () => { + let applyResponse; + beforeEach(() => { + applyResponse = jasmine.createSpy(); + }); + + it("satisfies rule based on matched page url", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + url: "https://pro.mywebsite.org:8080/about?m=1&t=5&name=bob#home" + }), + { + definition: { + key: "page.url", + matcher: "eq", + values: ["https://pro.mywebsite.org:8080/about?m=1&t=5&name=bob#home"] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched page url", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + url: "https://pro.mywebsite.org:8080/about?m=1&t=5&name=richard#home" + }), + { + definition: { + key: "page.url", + matcher: "eq", + values: ["https://pro.mywebsite.org:8080/about?m=1&t=5&name=bob#home"] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); + + it("satisfy rule based on matched domain", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + url: "https://pro.mywebsite.org:8080/about?m=1&t=5&name=bob#home" + }), + { + definition: { + key: "page.domain", + matcher: "eq", + values: ["pro.mywebsite.org"] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched domain", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + url: "https://pro.mywebsite.org:8080/about?m=1&t=5&name=bob#home" + }), + { + definition: { + key: "page.domain", + matcher: "eq", + values: ["pro.mywebsite.com"] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); + + it("satisfied rule based on matched page subdomain", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + url: "https://pro.mywebsite.org:8080/about?m=1&t=5&name=bob#home" + }), + { + definition: { + key: "page.subdomain", + matcher: "eq", + values: ["pro"] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + // Note that we have custom parse url [refer to implementation] which will give empty string in case of www + it("does not satisfy rule due to unmatched page subdomain", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + url: "https://www.mywebsite.org:8080/about?m=1&t=5&name=bob#home" + }), + { + definition: { + key: "page.subdomain", + matcher: "eq", + values: ["www"] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); + + it("satisfies rule based on matched page topLevelDomain", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + url: "https://pro.mywebsite.org:8080/about?m=1&t=5&name=bob#home" + }), + { + definition: { + key: "page.topLevelDomain", + matcher: "eq", + values: ["org"] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched page topLevelDomain", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + url: "https://pro.mywebsite.org:8080/about?m=1&t=5&name=bob#home" + }), + { + definition: { + key: "page.topLevelDomain", + matcher: "eq", + values: ["com"] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); + + it("satisfies rule based on matched page path", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + url: "https://pro.mywebsite.org:8080/about?m=1&t=5&name=bob#home" + }), + { + definition: { + key: "page.path", + matcher: "eq", + values: ["/about"] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched page path", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + url: "https://pro.mywebsite.org:8080/about?m=1&t=5&name=bob#home" + }), + { + definition: { + key: "page.path", + matcher: "eq", + values: ["/home"] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); + + it("satisfies rule based on matched page query", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + url: "https://pro.mywebsite.org:8080/about?m=1&t=5&name=bob#home" + }), + { + definition: { + key: "page.query", + matcher: "co", + values: ["name=bob"] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched page query", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + url: "https://pro.mywebsite.org:8080/about?m=1&t=5&name=richard#home" + }), + { + definition: { + key: "page.query", + matcher: "co", + values: ["name=bob"] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); + + it("satisfies rule based on matched page fragment", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + url: "https://pro.mywebsite.org:8080/about?m=1&t=5&name=bob#home" + }), + { + definition: { + key: "page.fragment", + matcher: "eq", + values: ["home"] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched page fragment", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + url: "https://pro.mywebsite.org:8080/about?m=1&t=5&name=bob#about" + }), + { + definition: { + key: "page.fragment", + matcher: "eq", + values: ["home"] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); +}); diff --git a/test/unit/specs/components/DecisioningEngine/decisioningContext.referringPage.spec.js b/test/unit/specs/components/DecisioningEngine/decisioningContext.referringPage.spec.js new file mode 100644 index 000000000..e0917a202 --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/decisioningContext.referringPage.spec.js @@ -0,0 +1,288 @@ +/* +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 { + mockWindow, + setupResponseHandler, + proposition +} from "./contextTestUtils"; + +describe("DecisioningEngine:globalContext:referringPage", () => { + let applyResponse; + beforeEach(() => { + applyResponse = jasmine.createSpy(); + }); + + it("satisfies rule based on matched domain", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + referrer: + "https://business.adobe.com/search?q=adobe+journey+optimizer&oq=adobe+journey+optimizer#home" + }), + { + definition: { + key: "referringPage.domain", + matcher: "eq", + values: ["business.adobe.com"] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched domain", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + referrer: + "https://pro.mywebsite.org:8080/about?m=1&t=5&name=richard#home" + }), + { + definition: { + key: "referringPage.domain", + matcher: "co", + values: ["business.adobe.com"] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); + + it("satisfies rule based on matched referringPage subdomain", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + referrer: + "https://business.adobe.com/search?q=adobe+journey+optimizer&oq=adobe+journey+optimizer#home" + }), + { + definition: { + key: "referringPage.subdomain", + matcher: "co", + values: ["business"] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched subdomain", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + referrer: + "https://pro.mywebsite.org:8080/about?m=1&t=5&name=richard#home" + }), + { + definition: { + key: "referringPage.subdomain", + matcher: "eq", + values: ["business"] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); + + it("satisfies rule based on matched referringPage topLevelDomain", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "referringPage.topLevelDomain", + matcher: "eq", + values: ["com"] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched topLevelDomain", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + referrer: + "https://pro.mywebsite.org:8080/about?m=1&t=5&name=richard#home" + }), + { + definition: { + key: "referringPage.topLevelDomain", + matcher: "eq", + values: ["com"] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); + + it("satisfies rule based on matched referringPage path", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "referringPage.path", + matcher: "co", + values: ["/search"] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched referringPage path", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + referrer: + "https://pro.mywebsite.org:8080/about?m=1&t=5&name=richard#home" + }), + { + definition: { + key: "referringPage.path", + matcher: "co", + values: ["/search"] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); + + it("satisfies rule based on matched referringPage query", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "referringPage.query", + matcher: "co", + values: ["q=adobe+journey+optimizer&oq=adobe+journey+optimizer"] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched referringPage query", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + referrer: + "https://pro.mywebsite.org:8080/about?m=1&t=5&name=richard#home" + }), + { + definition: { + key: "referringPage.query", + matcher: "co", + values: ["q=adobe+journey+optimizer&oq=adobe+journey+optimizer"] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); + + it("satisfies rule based on matched referringPage fragment", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + referrer: + "https://business.adobe.com/search?q=adobe+journey+optimizer&oq=adobe+journey+optimizer#home" + }), + { + definition: { + key: "referringPage.fragment", + matcher: "co", + values: ["home"] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule based on unmatched referringPage fragment", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + referrer: "https://pro.mywebsite.org:8080/about?m=1&t=5&name=bob#about" + }), + { + definition: { + key: "referringPage.fragment", + matcher: "co", + values: ["home"] + }, + type: "matcher" + } + ); + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); +}); diff --git a/test/unit/specs/components/DecisioningEngine/decisioningContext.timestamp.spec.js b/test/unit/specs/components/DecisioningEngine/decisioningContext.timestamp.spec.js new file mode 100644 index 000000000..d161c374a --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/decisioningContext.timestamp.spec.js @@ -0,0 +1,334 @@ +/* +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 { + mockWindow, + setupResponseHandler, + proposition +} from "./contextTestUtils"; + +let mockedTimestamp; +describe("DecisioningEngine:globalContext:timeContext", () => { + let applyResponse; + beforeEach(() => { + applyResponse = jasmine.createSpy(); + mockedTimestamp = new Date(Date.UTC(2023, 4, 11, 13, 34, 56)); + jasmine.clock().install(); + jasmine.clock().mockDate(mockedTimestamp); + }); + afterEach(() => { + jasmine.clock().uninstall(); + }); + + it("satisfies rule based on matched pageLoadTimestamp", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "pageLoadTimestamp", + matcher: "eq", + values: [mockedTimestamp.getTime()] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched pageLoadTimestamp", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "pageLoadTimestamp", + matcher: "eq", + values: [mockedTimestamp.getTime() + 1] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); + + it("satisfies rule based on matched currentTimestamp", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "currentTimestamp", + matcher: "eq", + values: [mockedTimestamp.getTime()] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched currentTimestamp", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "currentTimestamp", + matcher: "eq", + values: [mockedTimestamp.getTime() + 1] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); + + it("satisfies rule based on matched currentDate", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "currentDate", + matcher: "eq", + values: [mockedTimestamp.getDate()] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched currentDate", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "currentDate", + matcher: "eq", + values: [mockedTimestamp.getDate() + 1] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); + + it("satisfies rule based on matched currentDay", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "currentDay", + matcher: "eq", + values: [mockedTimestamp.getDay()] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched currentDay", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "currentDay", + matcher: "eq", + values: [mockedTimestamp.getDay() + 1] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); + + it("satisfies rule based on matched currentHour", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "currentHour", + matcher: "eq", + values: [mockedTimestamp.getHours()] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched currentHour", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "currentHour", + matcher: "eq", + values: [mockedTimestamp.getHours() + 1] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); + + it("satisfies rule based on matched currentMinute", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "currentMinute", + matcher: "eq", + values: [mockedTimestamp.getMinutes()] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched currentMinute", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "currentMinute", + matcher: "eq", + values: [mockedTimestamp.getMinutes() + 1] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); + + it("satisfies rule based on matched currentMonth", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "currentMonth", + matcher: "eq", + values: [mockedTimestamp.getMonth()] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched currentMonth", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "currentMonth", + matcher: "eq", + values: [mockedTimestamp.getMonth() + 1] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); + + it("satisfies rule based on matched currentYear", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "currentYear", + matcher: "eq", + values: [mockedTimestamp.getFullYear()] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched currentYear", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "currentYear", + matcher: "eq", + values: [mockedTimestamp.getFullYear() + 1] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); + + it("satisfies rule based on matched pageVisitDuration", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "pageVisitDuration", + matcher: "eq", + values: [0] + }, + type: "matcher" + }); + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched pageVisitDuration", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "pageVisitDuration", + matcher: "eq", + values: [1] + }, + type: "matcher" + }); + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); +}); diff --git a/test/unit/specs/components/DecisioningEngine/decisioningContext.window.spec.js b/test/unit/specs/components/DecisioningEngine/decisioningContext.window.spec.js new file mode 100644 index 000000000..d02df3304 --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/decisioningContext.window.spec.js @@ -0,0 +1,185 @@ +/* +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 { + mockWindow, + setupResponseHandler, + proposition +} from "./contextTestUtils"; + +describe("DecisioningEngine:globalContext:window", () => { + let applyResponse; + beforeEach(() => { + applyResponse = jasmine.createSpy(); + }); + + it("satisfies rule based on matched window height", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "window.height", + matcher: "gt", + values: [90] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched window height", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + height: 50 + }), + { + definition: { + key: "window.height", + matcher: "gt", + values: [90] + }, + type: "matcher" + } + ); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); + + it("satisfies rule based on matched window width", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + width: 200 + }), + { + definition: { + key: "window.width", + matcher: "gt", + values: [90] + }, + type: "matcher" + } + ); + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched window width", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + width: 50 + }), + { + definition: { + key: "window.width", + matcher: "gt", + values: [90] + }, + type: "matcher" + } + ); + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); + + it("satisfies rule based on matched window scrollX", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + scrollX: 200 + }), + { + definition: { + key: "window.scrollX", + matcher: "gt", + values: [90] + }, + type: "matcher" + } + ); + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched window scrollX", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + scrollX: 50 + }), + { + definition: { + key: "window.scrollX", + matcher: "gt", + values: [90] + }, + type: "matcher" + } + ); + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); + + it("satisfies rule based on matched window scrollY", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + scrollY: 200 + }), + { + definition: { + key: "window.scrollY", + matcher: "gt", + values: [90] + }, + type: "matcher" + } + ); + }); + + it("does not satisfy rule due to unmatched window scrollY", () => { + setupResponseHandler( + applyResponse, + mockWindow({ + scrollY: 50 + }), + { + definition: { + key: "window.scrollY", + matcher: "gt", + values: [90] + }, + type: "matcher" + } + ); + }); +}); diff --git a/test/unit/specs/components/DecisioningEngine/index.spec.js b/test/unit/specs/components/DecisioningEngine/index.spec.js new file mode 100644 index 000000000..57267734a --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/index.spec.js @@ -0,0 +1,135 @@ +/* +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 createDecisioningEngine from "../../../../../src/components/DecisioningEngine/index"; +import { injectStorage } from "../../../../../src/utils"; +import { + mockRulesetResponseWithCondition, + proposition +} from "./contextTestUtils"; + +describe("createDecisioningEngine:commands:renderDecisions", () => { + let mockEvent; + let onResponseHandler; + let decisioningEngine; + beforeEach(() => { + const config = { orgId: "exampleOrgId" }; + window.referrer = + "https://www.google.com/search?q=adobe+journey+optimizer&oq=adobe+journey+optimizer"; + const createNamespacedStorage = injectStorage(window); + decisioningEngine = createDecisioningEngine({ + config, + createNamespacedStorage + }); + mockEvent = { getContent: () => ({}), getViewName: () => undefined }; + decisioningEngine.lifecycle.onComponentsRegistered(() => {}); + }); + + it("should run the renderDecisions command and satisfy the rule based on global context", () => { + onResponseHandler = onResponse => { + onResponse({ + response: mockRulesetResponseWithCondition({ + definition: { + key: "referringPage.path", + matcher: "eq", + values: ["/search"] + }, + type: "matcher" + }) + }); + }; + decisioningEngine.lifecycle.onBeforeEvent({ + event: mockEvent, + renderDecisions: true, + decisionContext: {}, + onResponse: onResponseHandler + }); + const result = decisioningEngine.commands.renderDecisions.run({}); + expect(result).toEqual({ + propositions: [proposition] + }); + }); + + it("should run the renderDecisions command and does not satisfy rule due to unmatched global context", () => { + onResponseHandler = onResponse => { + onResponse({ + response: mockRulesetResponseWithCondition({ + definition: { + key: "referringPage.path", + matcher: "eq", + values: ["/about"] + }, + type: "matcher" + }) + }); + }; + decisioningEngine.lifecycle.onBeforeEvent({ + event: mockEvent, + renderDecisions: true, + decisionContext: {}, + onResponse: onResponseHandler + }); + const result = decisioningEngine.commands.renderDecisions.run({}); + expect(result).toEqual({ + propositions: [] + }); + }); + + it("should run the renderDecisions command and return propositions with renderDecisions true", () => { + onResponseHandler = onResponse => { + onResponse({ + response: mockRulesetResponseWithCondition({ + definition: { + key: "referringPage.path", + matcher: "eq", + values: ["/search"] + }, + type: "matcher" + }) + }); + }; + decisioningEngine.lifecycle.onBeforeEvent({ + event: mockEvent, + renderDecisions: true, + decisionContext: {}, + onResponse: onResponseHandler + }); + const result = decisioningEngine.commands.renderDecisions.run({}); + expect(result).toEqual({ + propositions: [proposition] + }); + }); + + it("should run the renderDecisions command returns propositions with renderDecisions false", () => { + onResponseHandler = onResponse => { + onResponse({ + response: mockRulesetResponseWithCondition({ + definition: { + key: "referringPage.path", + matcher: "eq", + values: ["/search"] + }, + type: "matcher" + }) + }); + }; + decisioningEngine.lifecycle.onBeforeEvent({ + event: mockEvent, + renderDecisions: false, + decisionContext: {}, + onResponse: onResponseHandler + }); + const result = decisioningEngine.commands.renderDecisions.run({}); + expect(result).toEqual({ + propositions: [proposition] + }); + }); +}); diff --git a/test/unit/specs/utils/debounce.spec.js b/test/unit/specs/utils/debounce.spec.js index 8a026a56c..d30912377 100644 --- a/test/unit/specs/utils/debounce.spec.js +++ b/test/unit/specs/utils/debounce.spec.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 debounce from "../../../../src/utils/debounce"; describe("debounce", () => { diff --git a/test/unit/specs/utils/parseUrl.spec.js b/test/unit/specs/utils/parseUrl.spec.js new file mode 100644 index 000000000..e398c5f69 --- /dev/null +++ b/test/unit/specs/utils/parseUrl.spec.js @@ -0,0 +1,63 @@ +/* +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 parseUrl from ".../../../../src/utils/parseUrl"; + +describe("parseUrl", () => { + it("should parse a valid URL with all components", () => { + const url = "https://example.com/path/to/page?param=value#section"; + + const result = parseUrl(url); + expect(result.path).toBe("/path/to/page"); + expect(result.query).toBe("param=value"); + expect(result.fragment).toBe("section"); + expect(result.domain).toBe("example.com"); + expect(result.subdomain).toBe(""); + expect(result.topLevelDomain).toBe("com"); + }); + + it("should handle URL without subdomain", () => { + const url = "https://example.com"; + + const result = parseUrl(url); + + expect(result.path).toBe(""); + expect(result.query).toBe(""); + expect(result.fragment).toBe(""); + expect(result.domain).toBe("example.com"); + expect(result.subdomain).toBe(""); + expect(result.topLevelDomain).toBe("com"); + }); + + it("should handle empty URL and return default values", () => { + const url = ""; + + const result = parseUrl(url); + expect(result.path).toBe(""); + expect(result.query).toBe(""); + expect(result.fragment).toBe(""); + expect(result.domain).toBe(""); + expect(result.subdomain).toBe(""); + expect(result.topLevelDomain).toBe(""); + }); + + it("should handle URL with subdomain", () => { + const url = "https://www.example.com"; + + const result = parseUrl(url); + expect(result.path).toBe(""); + expect(result.query).toBe(""); + expect(result.fragment).toBe(""); + expect(result.domain).toBe("www.example.com"); + expect(result.subdomain).toBe(""); + expect(result.topLevelDomain).toBe("com"); + }); +}); From 918bc026c71459b54c7bd71d48b44764983f60ab Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Mon, 22 May 2023 16:42:44 -0600 Subject: [PATCH 25/66] don't flatten events on context --- .../createContextProvider.js | 10 +- .../createOnResponseHandler.js | 6 +- src/components/DecisioningEngine/index.js | 3 +- .../DecisioningEngine/contextTestUtils.js | 5 +- .../createContextProvider.spec.js | 155 +++++------------- .../createOnResponseHandler.spec.js | 21 ++- 6 files changed, 66 insertions(+), 134 deletions(-) diff --git a/src/components/DecisioningEngine/createContextProvider.js b/src/components/DecisioningEngine/createContextProvider.js index 91c81b449..cead84294 100644 --- a/src/components/DecisioningEngine/createContextProvider.js +++ b/src/components/DecisioningEngine/createContextProvider.js @@ -11,6 +11,7 @@ governing permissions and limitations under the License. */ import getBrowser from "../../utils/getBrowser"; import parseUrl from "../../utils/parseUrl"; +import flattenObject from "../../utils/flattenObject"; export default ({ eventRegistry, window }) => { const pageLoadTimestamp = new Date().getTime(); @@ -77,12 +78,13 @@ export default ({ eventRegistry, window }) => { }; }; - const getContext = addedContext => { - return { + const getContext = (addedContext = {}) => { + const context = { ...getGlobalContext(), - ...addedContext, - events: eventRegistry.toJSON() + ...addedContext }; + + return { ...flattenObject(context), events: eventRegistry.toJSON() }; }; return { getContext diff --git a/src/components/DecisioningEngine/createOnResponseHandler.js b/src/components/DecisioningEngine/createOnResponseHandler.js index cb9d7c40b..9e9702b69 100644 --- a/src/components/DecisioningEngine/createOnResponseHandler.js +++ b/src/components/DecisioningEngine/createOnResponseHandler.js @@ -19,10 +19,10 @@ export default ({ event, decisionContext }) => { - const context = flattenObject({ - ...event.getContent(), + const context = { + ...flattenObject(event.getContent()), ...decisionContext - }); + }; const viewName = event.getViewName(); return ({ response }) => { diff --git a/src/components/DecisioningEngine/index.js b/src/components/DecisioningEngine/index.js index 3c2b028dc..fb28fccc5 100644 --- a/src/components/DecisioningEngine/index.js +++ b/src/components/DecisioningEngine/index.js @@ -15,7 +15,6 @@ import createDecisionProvider from "./createDecisionProvider"; import createApplyResponse from "./createApplyResponse"; import createEventRegistry from "./createEventRegistry"; import createContextProvider from "./createContextProvider"; -import flattenObject from "../../utils/flattenObject"; const createDecisioningEngine = ({ config, createNamespacedStorage }) => { const { orgId } = config; @@ -56,7 +55,7 @@ const createDecisioningEngine = ({ config, createNamespacedStorage }) => { run: decisionContext => applyResponse({ propositions: decisionProvider.evaluate( - flattenObject(contextProvider.getContext(decisionContext)) + contextProvider.getContext(decisionContext) ) }) } diff --git a/test/unit/specs/components/DecisioningEngine/contextTestUtils.js b/test/unit/specs/components/DecisioningEngine/contextTestUtils.js index 9efac36b0..f6a52a9fa 100644 --- a/test/unit/specs/components/DecisioningEngine/contextTestUtils.js +++ b/test/unit/specs/components/DecisioningEngine/contextTestUtils.js @@ -117,10 +117,7 @@ export const setupResponseHandler = (applyResponse, window, condition) => { const eventRegistry = createEventRegistry({ storage }); const decisionProvider = createDecisionProvider(); - const contextProvider = createContextProvider({ - eventRegistry, - window - }); + const contextProvider = createContextProvider({ eventRegistry, window }); const onResponseHandler = createOnResponseHandler({ renderDecisions: true, diff --git a/test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js b/test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js index 5a8b5aad9..3dbd6aeca 100644 --- a/test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js @@ -46,95 +46,68 @@ describe("DecisioningEngine:createContextProvider", () => { eventRegistry = createEventRegistry({ storage }); contextProvider = createContextProvider({ eventRegistry, window }); - expect(contextProvider.getContext().page).toEqual({ - title: "My awesome website", - url: "https://my.web-site.net:8080/about?m=1&t=5&name=jimmy#home", - path: "/about", - query: "m=1&t=5&name=jimmy", - fragment: "home", - domain: "my.web-site.net", - subdomain: "my", - topLevelDomain: "net" - }); + expect(contextProvider.getContext()).toEqual( + jasmine.objectContaining({ + "page.title": "My awesome website", + "page.url": + "https://my.web-site.net:8080/about?m=1&t=5&name=jimmy#home", + "page.path": "/about", + "page.query": "m=1&t=5&name=jimmy", + "page.fragment": "home", + "page.domain": "my.web-site.net", + "page.subdomain": "my", + "page.topLevelDomain": "net" + }) + ); }); it("returns referring page context", () => { eventRegistry = createEventRegistry({ storage }); contextProvider = createContextProvider({ eventRegistry, window }); - expect(contextProvider.getContext().referringPage).toEqual({ - url: "https://stage.applookout.net/", - path: "/", - query: "", - fragment: "", - domain: "stage.applookout.net", - subdomain: "stage", - topLevelDomain: "net" - }); + expect(contextProvider.getContext()).toEqual( + jasmine.objectContaining({ + "referringPage.url": "https://stage.applookout.net/", + "referringPage.path": "/", + "referringPage.query": "", + "referringPage.fragment": "", + "referringPage.domain": "stage.applookout.net", + "referringPage.subdomain": "stage", + "referringPage.topLevelDomain": "net" + }) + ); }); it("returns browser context", () => { eventRegistry = createEventRegistry({ storage }); contextProvider = createContextProvider({ eventRegistry, window }); - expect(contextProvider.getContext().browser).toEqual({ - name: "Chrome" - }); + expect(contextProvider.getContext()).toEqual( + jasmine.objectContaining({ + "browser.name": "Chrome" + }) + ); }); it("returns windows context", () => { eventRegistry = createEventRegistry({ storage }); contextProvider = createContextProvider({ eventRegistry, window }); - expect(contextProvider.getContext().window).toEqual({ - height: 100, - width: 100, - scrollY: 10, - scrollX: 10 - }); + expect(contextProvider.getContext()).toEqual( + jasmine.objectContaining({ + "window.height": 100, + "window.width": 100, + "window.scrollY": 10, + "window.scrollX": 10 + }) + ); }); it("includes provided context passed in", () => { eventRegistry = createEventRegistry({ storage }); contextProvider = createContextProvider({ eventRegistry, window }); - expect(contextProvider.getContext({ cool: "beans" })).toEqual({ - cool: "beans", - events: {}, - currentTimestamp: mockedTimestamp.getTime(), - currentHour: mockedTimestamp.getHours(), - currentMinute: mockedTimestamp.getMinutes(), - currentYear: mockedTimestamp.getFullYear(), - currentMonth: mockedTimestamp.getMonth(), - currentDate: mockedTimestamp.getDate(), - currentDay: mockedTimestamp.getDay(), - pageLoadTimestamp: mockedTimestamp.getTime(), - pageVisitDuration: 0, - browser: { - name: "Chrome" - }, - window: { - height: 100, - width: 100, - scrollY: 10, - scrollX: 10 - }, - page: { - title: "My awesome website", - url: "https://my.web-site.net:8080/about?m=1&t=5&name=jimmy#home", - path: "/about", - query: "m=1&t=5&name=jimmy", - fragment: "home", - domain: "my.web-site.net", - subdomain: "my", - topLevelDomain: "net" - }, - referringPage: { - url: "https://stage.applookout.net/", - path: "/", - query: "", - fragment: "", - domain: "stage.applookout.net", - subdomain: "stage", - topLevelDomain: "net" - } - }); + expect(contextProvider.getContext({ cool: "beans" })).toEqual( + jasmine.objectContaining({ + cool: "beans" + }) + ); }); it("includes events context", () => { @@ -150,46 +123,8 @@ describe("DecisioningEngine:createContextProvider", () => { }; contextProvider = createContextProvider({ eventRegistry, window }); - expect(contextProvider.getContext({ cool: "beans" })).toEqual({ - cool: "beans", - events, - currentTimestamp: mockedTimestamp.getTime(), - currentHour: mockedTimestamp.getHours(), - currentMinute: mockedTimestamp.getMinutes(), - currentYear: mockedTimestamp.getFullYear(), - currentMonth: mockedTimestamp.getMonth(), - currentDate: mockedTimestamp.getDate(), - currentDay: mockedTimestamp.getDay(), - pageLoadTimestamp: mockedTimestamp.getTime(), - pageVisitDuration: 0, - browser: { - name: "Chrome" - }, - window: { - height: 100, - width: 100, - scrollY: 10, - scrollX: 10 - }, - page: { - title: "My awesome website", - url: "https://my.web-site.net:8080/about?m=1&t=5&name=jimmy#home", - path: "/about", - query: "m=1&t=5&name=jimmy", - fragment: "home", - domain: "my.web-site.net", - subdomain: "my", - topLevelDomain: "net" - }, - referringPage: { - url: "https://stage.applookout.net/", - path: "/", - query: "", - fragment: "", - domain: "stage.applookout.net", - subdomain: "stage", - topLevelDomain: "net" - } - }); + expect(contextProvider.getContext({ cool: "beans" }).events).toEqual( + events + ); }); }); diff --git a/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js b/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js index c2266a6ad..394bef954 100644 --- a/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js @@ -14,14 +14,20 @@ import createDecisionProvider from "../../../../../src/components/DecisioningEng import createApplyResponse from "../../../../../src/components/DecisioningEngine/createApplyResponse"; describe("DecisioningEngine:createOnResponseHandler", () => { - it("calls lifecycle.onDecision with propositions based on decisionContext", () => { - const lifecycle = jasmine.createSpyObj("lifecycle", { + let lifecycle; + let decisionProvider; + let applyResponse; + + beforeEach(() => { + lifecycle = jasmine.createSpyObj("lifecycle", { onDecision: Promise.resolve() }); - const decisionProvider = createDecisionProvider(); - const applyResponse = createApplyResponse(lifecycle); + decisionProvider = createDecisionProvider(); + applyResponse = createApplyResponse(lifecycle); + }); + it("calls lifecycle.onDecision with propositions based on decisionContext", () => { const event = { getViewName: () => undefined, getContent: () => ({ @@ -198,13 +204,6 @@ describe("DecisioningEngine:createOnResponseHandler", () => { }); it("calls lifecycle.onDecision with propositions based on xdm and event data", () => { - const lifecycle = jasmine.createSpyObj("lifecycle", { - onDecision: Promise.resolve() - }); - - const decisionProvider = createDecisionProvider(); - const applyResponse = createApplyResponse(lifecycle); - const event = { getViewName: () => "home", getContent: () => ({ From f71029212a4cd83cec37c964275b60d67f87b304 Mon Sep 17 00:00:00 2001 From: Happy Shandilya Date: Thu, 1 Jun 2023 15:07:55 -0700 Subject: [PATCH 26/66] [CJM-48525]Renamed renderDecision command to evaluateRulesets (#994) * renamed renderDecision to evaluateRulesets * renamed renderDecision to evaluateRulesets fix typo * evaluateRulesets --- src/components/DecisioningEngine/index.js | 2 +- .../components/DecisioningEngine/index.spec.js | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/DecisioningEngine/index.js b/src/components/DecisioningEngine/index.js index fb28fccc5..afe2763a6 100644 --- a/src/components/DecisioningEngine/index.js +++ b/src/components/DecisioningEngine/index.js @@ -51,7 +51,7 @@ const createDecisioningEngine = ({ config, createNamespacedStorage }) => { } }, commands: { - renderDecisions: { + evaluateRulesets: { run: decisionContext => applyResponse({ propositions: decisionProvider.evaluate( diff --git a/test/unit/specs/components/DecisioningEngine/index.spec.js b/test/unit/specs/components/DecisioningEngine/index.spec.js index 57267734a..3859c4971 100644 --- a/test/unit/specs/components/DecisioningEngine/index.spec.js +++ b/test/unit/specs/components/DecisioningEngine/index.spec.js @@ -16,7 +16,7 @@ import { proposition } from "./contextTestUtils"; -describe("createDecisioningEngine:commands:renderDecisions", () => { +describe("createDecisioningEngine:commands:evaluateRulesets", () => { let mockEvent; let onResponseHandler; let decisioningEngine; @@ -33,7 +33,7 @@ describe("createDecisioningEngine:commands:renderDecisions", () => { decisioningEngine.lifecycle.onComponentsRegistered(() => {}); }); - it("should run the renderDecisions command and satisfy the rule based on global context", () => { + it("should run the evaluateRulesets command and satisfy the rule based on global context", () => { onResponseHandler = onResponse => { onResponse({ response: mockRulesetResponseWithCondition({ @@ -52,13 +52,13 @@ describe("createDecisioningEngine:commands:renderDecisions", () => { decisionContext: {}, onResponse: onResponseHandler }); - const result = decisioningEngine.commands.renderDecisions.run({}); + const result = decisioningEngine.commands.evaluateRulesets.run({}); expect(result).toEqual({ propositions: [proposition] }); }); - it("should run the renderDecisions command and does not satisfy rule due to unmatched global context", () => { + it("should run the evaluateRulesets command and does not satisfy rule due to unmatched global context", () => { onResponseHandler = onResponse => { onResponse({ response: mockRulesetResponseWithCondition({ @@ -77,13 +77,13 @@ describe("createDecisioningEngine:commands:renderDecisions", () => { decisionContext: {}, onResponse: onResponseHandler }); - const result = decisioningEngine.commands.renderDecisions.run({}); + const result = decisioningEngine.commands.evaluateRulesets.run({}); expect(result).toEqual({ propositions: [] }); }); - it("should run the renderDecisions command and return propositions with renderDecisions true", () => { + it("should run the evaluateRulesets command and return propositions with renderDecisions true", () => { onResponseHandler = onResponse => { onResponse({ response: mockRulesetResponseWithCondition({ @@ -102,13 +102,13 @@ describe("createDecisioningEngine:commands:renderDecisions", () => { decisionContext: {}, onResponse: onResponseHandler }); - const result = decisioningEngine.commands.renderDecisions.run({}); + const result = decisioningEngine.commands.evaluateRulesets.run({}); expect(result).toEqual({ propositions: [proposition] }); }); - it("should run the renderDecisions command returns propositions with renderDecisions false", () => { + it("should run the evaluateRulesets command returns propositions with renderDecisions false", () => { onResponseHandler = onResponse => { onResponse({ response: mockRulesetResponseWithCondition({ @@ -127,7 +127,7 @@ describe("createDecisioningEngine:commands:renderDecisions", () => { decisionContext: {}, onResponse: onResponseHandler }); - const result = decisioningEngine.commands.renderDecisions.run({}); + const result = decisioningEngine.commands.evaluateRulesets.run({}); expect(result).toEqual({ propositions: [proposition] }); From e2ac6f567323d53e990b4def4f46bbcfe4431a83 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Thu, 27 Jul 2023 14:52:59 -0600 Subject: [PATCH 27/66] added consequence adapter and refactored iframe display code --- .../inAppMessageConsequenceAdapter.js | 41 +++++ .../createConsequenceAdapter.js | 17 ++ .../createEvaluableRulesetPayload.js | 8 +- .../actions/displayBanner.js | 54 +----- .../actions/displayCustom.js | 17 ++ .../actions/displayFullScreen.js | 5 + .../actions/displayIframeContent.js | 169 ++++++++++++++++++ .../actions/displayModal.js | 116 +----------- .../initMessagingActionsModules.js | 14 +- .../inAppMessageConsequenceAdapter.spec.js | 52 ++++++ .../createConsequenceAdapter.spec.js | 56 ++++++ .../Personalization/createModules.spec.js | 6 +- .../actions/displayBanner.spec.js | 55 ++++-- .../actions/displayCustom.spec.js | 0 .../actions/displayFullScreen.spec.js | 0 .../actions/displayIframeContent.spec.js | 0 .../actions/displayModal.spec.js | 68 +++---- 17 files changed, 458 insertions(+), 220 deletions(-) create mode 100644 src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js create mode 100644 src/components/DecisioningEngine/createConsequenceAdapter.js create mode 100644 src/components/Personalization/in-app-message-actions/actions/displayCustom.js create mode 100644 src/components/Personalization/in-app-message-actions/actions/displayFullScreen.js create mode 100644 src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js create mode 100644 test/unit/specs/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.spec.js create mode 100644 test/unit/specs/components/DecisioningEngine/createConsequenceAdapter.spec.js create mode 100644 test/unit/specs/components/Personalization/in-app-message-actions/actions/displayCustom.spec.js create mode 100644 test/unit/specs/components/Personalization/in-app-message-actions/actions/displayFullScreen.spec.js create mode 100644 test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js diff --git a/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js b/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js new file mode 100644 index 000000000..ff23fb580 --- /dev/null +++ b/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js @@ -0,0 +1,41 @@ +import { IN_APP_MESSAGE } from "../../Personalization/constants/schema"; +import { + IAM_ACTION_TYPE_BANNER, + IAM_ACTION_TYPE_CUSTOM, + IAM_ACTION_TYPE_FULLSCREEN, + IAM_ACTION_TYPE_MODAL +} from "../../Personalization/in-app-message-actions/initMessagingActionsModules"; + +const deduceType = html => { + if (html.includes("banner")) { + return IAM_ACTION_TYPE_BANNER; + } + + if (html.includes("modal")) { + return IAM_ACTION_TYPE_MODAL; + } + + if (html.includes("fullscreen")) { + return IAM_ACTION_TYPE_FULLSCREEN; + } + + return IAM_ACTION_TYPE_CUSTOM; +}; + +export default (id, type, detail) => { + const { html, mobileParameters } = detail; + + const webParameters = { info: "this is a placeholder" }; + + return { + schema: IN_APP_MESSAGE, + data: { + type: deduceType(html, mobileParameters), + mobileParameters, + webParameters, + content: html, + contentType: "text/html" + }, + id + }; +}; diff --git a/src/components/DecisioningEngine/createConsequenceAdapter.js b/src/components/DecisioningEngine/createConsequenceAdapter.js new file mode 100644 index 000000000..5c8f51ea7 --- /dev/null +++ b/src/components/DecisioningEngine/createConsequenceAdapter.js @@ -0,0 +1,17 @@ +import inAppMessageConsequenceAdapter from "./consequenceAdapters/inAppMessageConsequenceAdapter"; + +const CJM_IN_APP_MESSAGE_TYPE = "cjmiam"; + +const adapters = { + [CJM_IN_APP_MESSAGE_TYPE]: inAppMessageConsequenceAdapter +}; + +export default () => { + return consequence => { + const { id, type, detail } = consequence; + + return typeof adapters[type] === "function" + ? adapters[type](id, type, detail) + : detail; + }; +}; diff --git a/src/components/DecisioningEngine/createEvaluableRulesetPayload.js b/src/components/DecisioningEngine/createEvaluableRulesetPayload.js index 29366f09b..7d512f0b5 100644 --- a/src/components/DecisioningEngine/createEvaluableRulesetPayload.js +++ b/src/components/DecisioningEngine/createEvaluableRulesetPayload.js @@ -12,8 +12,10 @@ governing permissions and limitations under the License. import RulesEngine from "@adobe/aep-rules-engine"; import { JSON_RULESET_ITEM } from "../Personalization/constants/schema"; import flattenArray from "../../utils/flattenArray"; +import createConsequenceAdapter from "./createConsequenceAdapter"; export default payload => { + const consequenceAdapter = createConsequenceAdapter(); const items = []; const addItem = item => { @@ -24,14 +26,16 @@ export default payload => { return; } - items.push(RulesEngine(JSON.parse(content))); + items.push( + RulesEngine(typeof content === "string" ? JSON.parse(content) : content) + ); }; const evaluate = context => { return { ...payload, items: flattenArray(items.map(item => item.execute(context))).map( - item => item.detail + consequenceAdapter ) }; }; diff --git a/src/components/Personalization/in-app-message-actions/actions/displayBanner.js b/src/components/Personalization/in-app-message-actions/actions/displayBanner.js index c092616eb..84de75a0a 100644 --- a/src/components/Personalization/in-app-message-actions/actions/displayBanner.js +++ b/src/components/Personalization/in-app-message-actions/actions/displayBanner.js @@ -1,53 +1,5 @@ -/* -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 +import displayIframeContent from "./displayIframeContent"; -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 { addStyle, removeElements } from "../utils"; - -const STYLE_TAG_ID = "alloy-messaging-banner-styles"; -const ELEMENT_TAG_ID = "alloy-messaging-banner"; -const BANNER_CSS_CLASSNAME = "alloy-banner"; - -const showBanner = ({ background, content }) => { - removeElements(BANNER_CSS_CLASSNAME); - - addStyle( - STYLE_TAG_ID, - `.alloy-banner { - display: flex; - justify-content: center; - padding: 10px; - background: ${background}; - } - .alloy-banner-content { - }` - ); - - const banner = document.createElement("div"); - banner.id = ELEMENT_TAG_ID; - banner.className = BANNER_CSS_CLASSNAME; - - const bannerContent = document.createElement("div"); - bannerContent.className = "alloy-banner-content"; - bannerContent.innerHTML = content; - banner.appendChild(bannerContent); - - document.body.prepend(banner); -}; - -export default settings => { - return new Promise(resolve => { - const { meta } = settings; - - showBanner(settings); - - resolve({ meta }); - }); +export default (settings, collect) => { + return displayIframeContent(settings, collect); }; diff --git a/src/components/Personalization/in-app-message-actions/actions/displayCustom.js b/src/components/Personalization/in-app-message-actions/actions/displayCustom.js new file mode 100644 index 000000000..2b79fdd8a --- /dev/null +++ b/src/components/Personalization/in-app-message-actions/actions/displayCustom.js @@ -0,0 +1,17 @@ +/* +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 displayIframeContent from "./displayIframeContent"; + +export default (settings, collect) => { + return displayIframeContent(settings, collect); +}; diff --git a/src/components/Personalization/in-app-message-actions/actions/displayFullScreen.js b/src/components/Personalization/in-app-message-actions/actions/displayFullScreen.js new file mode 100644 index 000000000..84de75a0a --- /dev/null +++ b/src/components/Personalization/in-app-message-actions/actions/displayFullScreen.js @@ -0,0 +1,5 @@ +import displayIframeContent from "./displayIframeContent"; + +export default (settings, collect) => { + return displayIframeContent(settings, collect); +}; diff --git a/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js b/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js new file mode 100644 index 000000000..a4112bb75 --- /dev/null +++ b/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js @@ -0,0 +1,169 @@ +/* eslint-disable no-unused-vars */ +/* +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 { removeElements } from "../utils"; + +const ELEMENT_TAG_CLASSNAME = "alloy-messaging-container"; +const ELEMENT_TAG_ID = "alloy-messaging-container"; +const ANCHOR_HREF_REGEX = /adbinapp:\/\/(\w+)\?interaction=(\w+)/i; + +export const buildStyleFromParameters = (mobileParameters, webParameters) => { + const { + verticalAlign, + width, + horizontalAlign, + backdropColor, + height, + cornerRadius, + horizontalInset, + verticalInset, + uiTakeOver + } = mobileParameters; + + return { + verticalAlign: verticalAlign === "center" ? "middle" : verticalAlign, + top: verticalAlign === "top" ? "0px" : "auto", + width: width ? `${width}%` : "100%", + horizontalAlign: horizontalAlign === "center" ? "middle" : horizontalAlign, + backgroundColor: backdropColor || "rgba(0, 0, 0, 0.5)", + height: height ? `${height}vh` : "100%", + borderRadius: cornerRadius ? `${cornerRadius}px` : "0px", + border: "none", + marginLeft: horizontalInset ? `${horizontalInset}px` : "0px", + marginRight: horizontalInset ? `${horizontalInset}px` : "0px", + marginTop: verticalInset ? `${verticalInset}px` : "0px", + marginBottom: verticalInset ? `${verticalInset}px` : "0px", + zIndex: uiTakeOver ? "9999" : "0", + position: uiTakeOver ? "fixed" : "relative", + overflow: "hidden" + }; +}; + +const createIframeClickHandler = (container, collect) => { + return event => { + event.preventDefault(); + event.stopImmediatePropagation(); + + const { target } = event; + + const anchor = + target.tagName.toLowerCase() === "a" ? target : target.closest("a"); + + if (anchor) { + if (ANCHOR_HREF_REGEX.test(anchor.href)) { + const matches = ANCHOR_HREF_REGEX.exec(anchor.href); + + const action = matches.length >= 2 ? matches[1] : ""; + const interaction = matches.length >= 3 ? matches[2] : ""; + + if (interaction === "clicked") { + const uuid = anchor.getAttribute("data-uuid"); + // eslint-disable-next-line no-console + console.log(`clicked ${uuid}`); + // TODO: collect analytics + // collect({ + // eventType: INTERACT + // }); + } + + if (action === "dismiss") { + container.remove(); + } + } else { + window.location.href = anchor.href; + } + } + }; +}; + +const createIframe = (htmlContent, clickHandler) => { + const parser = new DOMParser(); + const htmlDocument = parser.parseFromString(htmlContent, "text/html"); + + const element = document.createElement("iframe"); + element.src = URL.createObjectURL( + new Blob([htmlDocument.documentElement.outerHTML], { type: "text/html" }) + ); + // element.sandbox = "allow-same-origin allow-scripts"; + + Object.assign(element.style, { + border: "none", + width: "100%", + height: "100%" + }); + + element.addEventListener("load", () => { + const { addEventListener } = + element.contentDocument || element.contentWindow.document; + addEventListener("click", clickHandler); + }); + + return element; +}; + +const createContainerElement = settings => { + const { mobileParameters = {}, webParameters = {} } = settings; + + const element = document.createElement("div"); + element.id = ELEMENT_TAG_ID; + element.className = `${ELEMENT_TAG_CLASSNAME}`; + + Object.assign(element.style, { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + "background-color": "white", + padding: "0px", + border: "1px solid black", + "box-shadow": "10px 10px 5px #888888" + }); + + Object.assign( + element.style, + buildStyleFromParameters(mobileParameters, webParameters) + ); + + return element; +}; + +const displayHTMLContentInIframe = (settings, collect) => { + removeElements(ELEMENT_TAG_CLASSNAME); + + const { content, contentType } = settings; + + if (contentType !== "text/html") { + // TODO: whoops, no can do. + } + + const container = createContainerElement(settings); + + const iframe = createIframe( + content, + createIframeClickHandler(container, collect) + ); + + container.appendChild(iframe); + + document.body.append(container); +}; + +export default (settings, collect) => { + return new Promise(resolve => { + const { meta } = settings; + + displayHTMLContentInIframe(settings, collect); + + resolve({ meta }); + }); +}; diff --git a/src/components/Personalization/in-app-message-actions/actions/displayModal.js b/src/components/Personalization/in-app-message-actions/actions/displayModal.js index 63ba952be..84de75a0a 100644 --- a/src/components/Personalization/in-app-message-actions/actions/displayModal.js +++ b/src/components/Personalization/in-app-message-actions/actions/displayModal.js @@ -1,115 +1,5 @@ -/* -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 +import displayIframeContent from "./displayIframeContent"; -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 { addStyle, removeElements } from "../utils"; - -const STYLE_TAG_ID = "alloy-messaging-modal-styles"; -const ELEMENT_TAG_ID = "alloy-messaging-modal"; -const MODAL_CSS_CLASSNAME = "alloy-modal"; - -const closeModal = () => { - removeElements(MODAL_CSS_CLASSNAME); -}; -const showModal = ({ buttons = [], content }) => { - removeElements(MODAL_CSS_CLASSNAME); - - addStyle( - STYLE_TAG_ID, - `.alloy-modal { - display: none; - width: 100%; - height: 100%; - position: fixed; - left: 0; - top: 0; - background: rgba(0, 0, 0, 0.3); - z-index: 999; - } - .alloy-modal--show { display: flex; } - .alloy-align-center { - justify-content: center; - } - .alloy-align-vertical { - align-items: center; - } - .alloy-modal-container { - position: relative; - width: 100%; - max-width: 600px; - max-height: 800px; - padding: 20px; - margin: 12px; - background: #fff; - } - .alloy-modal-content { - margin: 15px 0; - vertical-align: top; - text-align: left; - } - .alloy-modal-close--x { - font-size: 30px; - position: absolute; - top: 3px; - right: 10px; - } - .alloy-modal-close--x:hover { - cursor: pointer; - } - .alloy-modal-buttons button { - margin-right: 5px; - }` - ); - - const modal = document.createElement("div"); - modal.id = ELEMENT_TAG_ID; - modal.className = `${MODAL_CSS_CLASSNAME} alloy-align-center alloy-align-vertical alloy-modal--show`; - - const modalContainer = document.createElement("div"); - modalContainer.className = "alloy-modal-container"; - - const closeButton = document.createElement("a"); - closeButton.className = "alloy-modal-close alloy-modal-close--x"; - closeButton.innerText = "✕"; - closeButton.addEventListener("click", closeModal); - closeButton.setAttribute("aria-hidden", "true"); - - const modalContent = document.createElement("div"); - modalContent.className = "alloy-modal-content"; - modalContent.innerHTML = content; - - const modalButtons = document.createElement("div"); - modalButtons.className = "alloy-modal-buttons"; - - buttons.forEach(buttonDetails => { - const button = document.createElement("button"); - button.className = "alloy_modal_button alloy-modal-close"; - button.innerText = buttonDetails.title; - button.addEventListener("click", closeModal); - modalButtons.appendChild(button); - }); - - modalContainer.appendChild(closeButton); - modalContainer.appendChild(modalContent); - modalContainer.appendChild(modalButtons); - - modal.appendChild(modalContainer); - document.body.append(modal); -}; - -export default settings => { - return new Promise(resolve => { - const { meta } = settings; - - showModal(settings); - - resolve({ meta }); - }); +export default (settings, collect) => { + return displayIframeContent(settings, collect); }; diff --git a/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js b/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js index 7f225aa31..1bae4a54b 100644 --- a/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js +++ b/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js @@ -2,11 +2,21 @@ import displayModal from "./actions/displayModal"; import displayBanner from "./actions/displayBanner"; +import displayCustom from "./actions/displayCustom"; +import displayFullScreen from "./actions/displayFullScreen"; + +export const IAM_ACTION_TYPE_MODAL = "modal"; +export const IAM_ACTION_TYPE_BANNER = "banner"; +export const IAM_ACTION_TYPE_FULLSCREEN = "fullscreen"; +export const IAM_ACTION_TYPE_CUSTOM = "custom"; export default collect => { // TODO: use collect to capture click and display metrics return { - modal: settings => displayModal(settings, collect), - banner: settings => displayBanner(settings, collect) + [IAM_ACTION_TYPE_MODAL]: settings => displayModal(settings, collect), + [IAM_ACTION_TYPE_BANNER]: settings => displayBanner(settings, collect), + [IAM_ACTION_TYPE_FULLSCREEN]: settings => + displayFullScreen(settings, collect), + [IAM_ACTION_TYPE_CUSTOM]: settings => displayCustom(settings, collect) }; }; diff --git a/test/unit/specs/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.spec.js b/test/unit/specs/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.spec.js new file mode 100644 index 000000000..d80b6d0e4 --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.spec.js @@ -0,0 +1,52 @@ +import inAppMessageConsequenceAdapter from "../../../../../../src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter"; + +describe("DecisioningEngine:inAppMessageConsequenceAdapter", () => { + it("works", () => { + expect( + inAppMessageConsequenceAdapter( + "72042c7c-4e34-44f6-af95-1072ae117424", + "cjmiam", + { + mobileParameters: { + verticalAlign: "center", + dismissAnimation: "top", + verticalInset: 0, + backdropOpacity: 0.2, + cornerRadius: 15, + horizontalInset: 0, + uiTakeover: true, + horizontalAlign: "center", + width: 80, + displayAnimation: "top", + backdropColor: "#000000", + height: 60 + }, + html: "
modal
" + } + ) + ).toEqual({ + schema: "https://ns.adobe.com/personalization/in-app-message", + data: { + type: "modal", + mobileParameters: { + verticalAlign: "center", + dismissAnimation: "top", + verticalInset: 0, + backdropOpacity: 0.2, + cornerRadius: 15, + horizontalInset: 0, + uiTakeover: true, + horizontalAlign: "center", + width: 80, + displayAnimation: "top", + backdropColor: "#000000", + height: 60 + }, + webParameters: jasmine.any(Object), + content: "
modal
", + contentType: "text/html" + }, + id: "72042c7c-4e34-44f6-af95-1072ae117424" + }); + }); +}); diff --git a/test/unit/specs/components/DecisioningEngine/createConsequenceAdapter.spec.js b/test/unit/specs/components/DecisioningEngine/createConsequenceAdapter.spec.js new file mode 100644 index 000000000..0e7cbbbca --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/createConsequenceAdapter.spec.js @@ -0,0 +1,56 @@ +import createConsequenceAdapter from "../../../../../src/components/DecisioningEngine/createConsequenceAdapter"; + +describe("DecisioningEngine:createConsequenceAdapter", () => { + it("works", () => { + const consequenceAdapter = createConsequenceAdapter(); + + const originalConsequence = { + id: "72042c7c-4e34-44f6-af95-1072ae117424", + type: "cjmiam", + detail: { + mobileParameters: { + verticalAlign: "center", + dismissAnimation: "top", + verticalInset: 0, + backdropOpacity: 0.2, + cornerRadius: 15, + horizontalInset: 0, + uiTakeover: true, + horizontalAlign: "center", + width: 80, + displayAnimation: "top", + backdropColor: "#000000", + height: 60 + }, + html: "
modal
" + } + }; + + const adaptedConsequence = consequenceAdapter(originalConsequence); + + expect(adaptedConsequence).toEqual({ + schema: "https://ns.adobe.com/personalization/in-app-message", + data: { + type: "modal", + mobileParameters: { + verticalAlign: "center", + dismissAnimation: "top", + verticalInset: 0, + backdropOpacity: 0.2, + cornerRadius: 15, + horizontalInset: 0, + uiTakeover: true, + horizontalAlign: "center", + width: 80, + displayAnimation: "top", + backdropColor: "#000000", + height: 60 + }, + webParameters: jasmine.any(Object), + content: "
modal
", + contentType: "text/html" + }, + id: "72042c7c-4e34-44f6-af95-1072ae117424" + }); + }); +}); diff --git a/test/unit/specs/components/Personalization/createModules.spec.js b/test/unit/specs/components/Personalization/createModules.spec.js index 0b4abedfe..5e1ecbbe7 100644 --- a/test/unit/specs/components/Personalization/createModules.spec.js +++ b/test/unit/specs/components/Personalization/createModules.spec.js @@ -49,9 +49,11 @@ describe("createModules", () => { expect(modules[IN_APP_MESSAGE]).toEqual({ modal: jasmine.any(Function), - banner: jasmine.any(Function) + banner: jasmine.any(Function), + fullscreen: jasmine.any(Function), + custom: jasmine.any(Function) }); - expect(Object.keys(modules[IN_APP_MESSAGE]).length).toEqual(2); + expect(Object.keys(modules[IN_APP_MESSAGE]).length).toEqual(4); }); }); diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js index 25749f090..72575e9dc 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js @@ -9,9 +9,9 @@ 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 displayBanner from "../../../../../../../src/components/Personalization/in-app-message-actions/actions/displayBanner"; import { createNode } from "../../../../../../../src/utils/dom"; import { DIV } from "../../../../../../../src/constants/tagName"; +import displayBanner from "../../../../../../../src/components/Personalization/in-app-message-actions/actions/displayBanner"; describe("Personalization::IAM:banner", () => { it("inserts banner into dom", async () => { @@ -24,29 +24,50 @@ describe("Personalization::IAM:banner", () => { } ); - document.body.prepend(something); + document.body.append(something); await displayBanner({ - type: "banner", - position: "top", - closeButton: false, - background: "#00a0fe", - content: - "FLASH SALE!! 50% off everything, 24 hours only!" + mobileParameters: { + verticalAlign: "center", + dismissAnimation: "top", + verticalInset: 0, + backdropOpacity: 0.2, + cornerRadius: 15, + horizontalInset: 0, + uiTakeover: true, + horizontalAlign: "center", + width: 80, + displayAnimation: "top", + backdropColor: "#000000", + height: 60 + }, + content: `
banner
Alf Says`, + contentType: "text/html" }); - const banner = document.querySelector("div#alloy-messaging-banner"); - const bannerStyle = document.querySelector( - "style#alloy-messaging-banner-styles" + const container = document.querySelector("div#alloy-messaging-container"); + + expect(container).not.toBeNull(); + + expect(container.parentNode).toEqual(document.body); + + expect(container.previousElementSibling).toEqual(something); + expect(container.nextElementSibling).toBeNull(); + + const iframe = document.querySelector( + ".alloy-messaging-container > iframe" ); - expect(banner).not.toBeNull(); - expect(bannerStyle).not.toBeNull(); + expect(iframe).not.toBeNull(); - expect(banner.parentNode).toEqual(document.body); - expect(bannerStyle.parentNode).toEqual(document.head); + await new Promise(resolve => { + iframe.addEventListener("load", () => { + resolve(); + }); + }); - expect(banner.previousElementSibling).toBeNull(); - expect(banner.nextElementSibling).toEqual(something); + expect( + (iframe.contentDocument || iframe.contentWindow.document).body.outerHTML + ).toContain("Alf Says"); }); }); diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayCustom.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayCustom.spec.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayFullScreen.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayFullScreen.spec.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js index 5ec5782b8..be60a2709 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js @@ -14,7 +14,7 @@ import { DIV } from "../../../../../../../src/constants/tagName"; import displayModal from "../../../../../../../src/components/Personalization/in-app-message-actions/actions/displayModal"; describe("Personalization::IAM:modal", () => { - it("inserts banner into dom", async () => { + it("inserts modal into dom", async () => { const something = createNode( DIV, { className: "something" }, @@ -27,45 +27,47 @@ describe("Personalization::IAM:modal", () => { document.body.append(something); await displayModal({ - type: "modal", - horizontalAlign: "center", - verticalAlign: "center", - closeButton: true, - dimBackground: true, - content: - "

Special offer, don't delay!

", - buttons: [ - { - title: "Yes please!" - }, - { - title: "No, thanks" - } - ] + mobileParameters: { + verticalAlign: "center", + dismissAnimation: "top", + verticalInset: 0, + backdropOpacity: 0.2, + cornerRadius: 15, + horizontalInset: 0, + uiTakeover: true, + horizontalAlign: "center", + width: 80, + displayAnimation: "top", + backdropColor: "#000000", + height: 60 + }, + content: `
modal
Alf Says`, + contentType: "text/html" }); - const modal = document.querySelector("div#alloy-messaging-modal"); - const modalStyle = document.querySelector( - "style#alloy-messaging-modal-styles" - ); + const container = document.querySelector("div#alloy-messaging-container"); - expect(modal).not.toBeNull(); - expect(modalStyle).not.toBeNull(); + expect(container).not.toBeNull(); - expect(modal.parentNode).toEqual(document.body); - expect(modalStyle.parentNode).toEqual(document.head); + expect(container.parentNode).toEqual(document.body); - expect(modal.previousElementSibling).toEqual(something); - expect(modal.nextElementSibling).toBeNull(); + expect(container.previousElementSibling).toEqual(something); + expect(container.nextElementSibling).toBeNull(); - expect( - modal.querySelector(".alloy-modal-content").innerText.trim() - ).toEqual("Special offer, don't delay!"); + const iframe = document.querySelector( + ".alloy-messaging-container > iframe" + ); - const buttons = modal.querySelector(".alloy-modal-buttons"); + expect(iframe).not.toBeNull(); - expect(buttons.childElementCount).toEqual(2); - expect(buttons.children[0].innerText).toEqual("Yes please!"); - expect(buttons.children[1].innerText).toEqual("No, thanks"); + await new Promise(resolve => { + iframe.addEventListener("load", () => { + resolve(); + }); + }); + + expect( + (iframe.contentDocument || iframe.contentWindow.document).body.outerHTML + ).toContain("Alf Says"); }); }); From 32a432da7bd40e962a62b6e73e710ce21925bb61 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Thu, 27 Jul 2023 15:07:04 -0600 Subject: [PATCH 28/66] license --- .../inAppMessageConsequenceAdapter.js | 11 +++++++++++ .../DecisioningEngine/createConsequenceAdapter.js | 11 +++++++++++ .../in-app-message-actions/actions/displayBanner.js | 11 +++++++++++ .../actions/displayFullScreen.js | 11 +++++++++++ .../in-app-message-actions/actions/displayModal.js | 11 +++++++++++ 5 files changed, 55 insertions(+) diff --git a/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js b/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js index ff23fb580..7a4bf249e 100644 --- a/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js +++ b/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.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 { IN_APP_MESSAGE } from "../../Personalization/constants/schema"; import { IAM_ACTION_TYPE_BANNER, diff --git a/src/components/DecisioningEngine/createConsequenceAdapter.js b/src/components/DecisioningEngine/createConsequenceAdapter.js index 5c8f51ea7..0e9f1f820 100644 --- a/src/components/DecisioningEngine/createConsequenceAdapter.js +++ b/src/components/DecisioningEngine/createConsequenceAdapter.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 inAppMessageConsequenceAdapter from "./consequenceAdapters/inAppMessageConsequenceAdapter"; const CJM_IN_APP_MESSAGE_TYPE = "cjmiam"; diff --git a/src/components/Personalization/in-app-message-actions/actions/displayBanner.js b/src/components/Personalization/in-app-message-actions/actions/displayBanner.js index 84de75a0a..46e52f9c4 100644 --- a/src/components/Personalization/in-app-message-actions/actions/displayBanner.js +++ b/src/components/Personalization/in-app-message-actions/actions/displayBanner.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 displayIframeContent from "./displayIframeContent"; export default (settings, collect) => { diff --git a/src/components/Personalization/in-app-message-actions/actions/displayFullScreen.js b/src/components/Personalization/in-app-message-actions/actions/displayFullScreen.js index 84de75a0a..46e52f9c4 100644 --- a/src/components/Personalization/in-app-message-actions/actions/displayFullScreen.js +++ b/src/components/Personalization/in-app-message-actions/actions/displayFullScreen.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 displayIframeContent from "./displayIframeContent"; export default (settings, collect) => { diff --git a/src/components/Personalization/in-app-message-actions/actions/displayModal.js b/src/components/Personalization/in-app-message-actions/actions/displayModal.js index 84de75a0a..46e52f9c4 100644 --- a/src/components/Personalization/in-app-message-actions/actions/displayModal.js +++ b/src/components/Personalization/in-app-message-actions/actions/displayModal.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 displayIframeContent from "./displayIframeContent"; export default (settings, collect) => { From 86c742bc9fbeacb6540aab39b9d75f163ec7cd24 Mon Sep 17 00:00:00 2001 From: Happy Shandilya Date: Wed, 2 Aug 2023 12:54:39 -0700 Subject: [PATCH 29/66] [CJM-48522]Purge historical events post retention period (#997) * purge historical events post retention period * fixed fragile test --- .../DecisioningEngine/createEventRegistry.js | 18 ++- src/components/DecisioningEngine/utils.js | 6 + .../createEventRegistry.spec.js | 103 ++++++++++++++++-- .../DecisioningEngine/utils.spec.js | 14 ++- 4 files changed, 129 insertions(+), 12 deletions(-) diff --git a/src/components/DecisioningEngine/createEventRegistry.js b/src/components/DecisioningEngine/createEventRegistry.js index 256e44ec6..84c158a43 100644 --- a/src/components/DecisioningEngine/createEventRegistry.js +++ b/src/components/DecisioningEngine/createEventRegistry.js @@ -9,18 +9,30 @@ 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 { createRestoreStorage, createSaveStorage } from "./utils"; +import { + createRestoreStorage, + createSaveStorage, + getExpirationDate +} from "./utils"; const STORAGE_KEY = "events"; const MAX_EVENT_RECORDS = 1000; const DEFAULT_SAVE_DELAY = 500; +const RETENTION_PERIOD = 30; -export const createEventPruner = (limit = MAX_EVENT_RECORDS) => { +export const createEventPruner = ( + limit = MAX_EVENT_RECORDS, + retentionPeriod = RETENTION_PERIOD +) => { return events => { const pruned = {}; Object.keys(events).forEach(eventType => { pruned[eventType] = {}; Object.values(events[eventType]) + .filter( + entry => + new Date(entry.firstTimestamp) >= getExpirationDate(retentionPeriod) + ) .sort((a, b) => a.firstTimestamp - b.firstTimestamp) .slice(-1 * limit) .forEach(entry => { @@ -37,7 +49,7 @@ export default ({ storage, saveDelay = DEFAULT_SAVE_DELAY }) => { storage, STORAGE_KEY, saveDelay, - createEventPruner(MAX_EVENT_RECORDS) + createEventPruner(MAX_EVENT_RECORDS, RETENTION_PERIOD) ); const events = restore({}); diff --git a/src/components/DecisioningEngine/utils.js b/src/components/DecisioningEngine/utils.js index 4a44b253f..3bfde1b3c 100644 --- a/src/components/DecisioningEngine/utils.js +++ b/src/components/DecisioningEngine/utils.js @@ -35,3 +35,9 @@ export const createSaveStorage = ( storage.setItem(storageKey, JSON.stringify(prepareFn(value))); }, debounceDelay); }; + +export const getExpirationDate = retentionPeriod => { + const expirationDate = new Date(); + expirationDate.setDate(expirationDate.getDate() - retentionPeriod); + return expirationDate; +}; diff --git a/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js b/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js index 59d35113a..1f268031c 100644 --- a/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createEventRegistry.spec.js @@ -15,9 +15,16 @@ import createEventRegistry, { describe("DecisioningEngine:createEventRegistry", () => { let storage; - + let mockedTimestamp; beforeEach(() => { storage = jasmine.createSpyObj("storage", ["getItem", "setItem", "clear"]); + mockedTimestamp = new Date("2023-05-24T08:00:00Z"); + jasmine.clock().install(); + jasmine.clock().mockDate(mockedTimestamp); + }); + + afterEach(() => { + jasmine.clock().uninstall(); }); it("registers events", () => { @@ -144,11 +151,12 @@ describe("DecisioningEngine:createEventRegistry", () => { ).toBeGreaterThan(lastEventTime); done(); }, 50); + + jasmine.clock().tick(60); }); it("limits events to 1000 events", () => { const prune = createEventPruner(); - const events = {}; events["decisioning.propositionDisplay"] = {}; events["decisioning.propositionInteract"] = {}; @@ -159,8 +167,8 @@ describe("DecisioningEngine:createEventRegistry", () => { id: i, type: "decisioning.propositionDisplay" }, - firstTimestamp: 1, - timestamp: 1, + firstTimestamp: "2023-05-23T08:00:00Z", + timestamp: mockedTimestamp, count: 1 }; @@ -169,13 +177,12 @@ describe("DecisioningEngine:createEventRegistry", () => { id: i, type: "decisioning.propositionInteract" }, - firstTimestamp: 1, - timestamp: 1, + firstTimestamp: "2023-05-23T08:00:00Z", + timestamp: mockedTimestamp, count: 1 }; const pruned = prune(events); - const interactEvents = Object.values( pruned["decisioning.propositionInteract"] ); @@ -183,7 +190,6 @@ describe("DecisioningEngine:createEventRegistry", () => { const displayEvents = Object.values( pruned["decisioning.propositionDisplay"] ); - expect(interactEvents.length).not.toBeGreaterThan(1000); expect(displayEvents.length).not.toBeGreaterThan(1000); @@ -231,4 +237,85 @@ describe("DecisioningEngine:createEventRegistry", () => { expect(displayEvents.length).not.toBeGreaterThan(10); } }); + + it("should filter events based on expiration date", () => { + const pruner = createEventPruner(4, 2); + + const events = {}; + events["decisioning.propositionDisplay"] = { + 1: { + event: { + id: 1, + type: "decisioning.propositionInteract" + }, + firstTimestamp: "2023-05-20T10:00:00Z", + timestamp: mockedTimestamp, + count: 1 + }, + 2: { + event: { + id: 2, + type: "decisioning.propositionInteract" + }, + firstTimestamp: "2023-05-24T15:00:00Z", + timestamp: mockedTimestamp, + count: 1 + } + }; + events["decisioning.propositionInteract"] = { + 3: { + event: { + id: 3, + type: "decisioning.propositionInteract" + }, + firstTimestamp: "2023-05-23T08:00:00Z", + timestamp: mockedTimestamp, + count: 1 + }, + 4: { + event: { + id: 4, + type: "decisioning.propositionInteract" + }, + firstTimestamp: "2023-05-23T08:00:00Z", + timestamp: mockedTimestamp, + count: 1 + } + }; + + const prunedEvents = pruner(events); + expect(prunedEvents).toEqual({ + "decisioning.propositionDisplay": { + 2: { + event: { + id: 2, + type: "decisioning.propositionInteract" + }, + firstTimestamp: "2023-05-24T15:00:00Z", + timestamp: mockedTimestamp, + count: 1 + } + }, + "decisioning.propositionInteract": { + 3: { + event: { + id: 3, + type: "decisioning.propositionInteract" + }, + firstTimestamp: "2023-05-23T08:00:00Z", + timestamp: mockedTimestamp, + count: 1 + }, + 4: { + event: { + id: 4, + type: "decisioning.propositionInteract" + }, + firstTimestamp: "2023-05-23T08:00:00Z", + timestamp: mockedTimestamp, + count: 1 + } + } + }); + }); }); diff --git a/test/unit/specs/components/DecisioningEngine/utils.spec.js b/test/unit/specs/components/DecisioningEngine/utils.spec.js index 0be44c9cc..bd1f008b6 100644 --- a/test/unit/specs/components/DecisioningEngine/utils.spec.js +++ b/test/unit/specs/components/DecisioningEngine/utils.spec.js @@ -10,7 +10,8 @@ governing permissions and limitations under the License. */ import { createRestoreStorage, - createSaveStorage + createSaveStorage, + getExpirationDate } from "../../../../../src/components/DecisioningEngine/utils"; describe("DecisioningEngine:utils", () => { @@ -65,4 +66,15 @@ describe("DecisioningEngine:utils", () => { done(); }, 20); }); + it("should return the date of expiration", () => { + const mockedTimestamp = new Date(Date.UTC(2023, 8, 2, 13, 34, 56)); + jasmine.clock().install(); + jasmine.clock().mockDate(mockedTimestamp); + const retentionPeriod = 10; + const expectedDate = new Date(mockedTimestamp); + expectedDate.setDate(expectedDate.getDate() - retentionPeriod); + const result = getExpirationDate(retentionPeriod); + expect(result).toEqual(expectedDate); + jasmine.clock().uninstall(); + }); }); From 0f013226eb3f0352ff69de40c16c4f2a497b9358 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Mon, 7 Aug 2023 16:24:48 -0600 Subject: [PATCH 30/66] defensively check for ruleset items --- .../createEvaluableRulesetPayload.js | 30 ++- .../DecisioningEngine/contextTestUtils.js | 2 +- .../createDecisionProvider.spec.js | 8 +- .../createEvaluableRulesetPayload.spec.js | 248 +++++++++++------- .../createOnResponseHandler.spec.js | 6 +- 5 files changed, 180 insertions(+), 114 deletions(-) diff --git a/src/components/DecisioningEngine/createEvaluableRulesetPayload.js b/src/components/DecisioningEngine/createEvaluableRulesetPayload.js index 7d512f0b5..e1afd4e1f 100644 --- a/src/components/DecisioningEngine/createEvaluableRulesetPayload.js +++ b/src/components/DecisioningEngine/createEvaluableRulesetPayload.js @@ -10,10 +10,34 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ import RulesEngine from "@adobe/aep-rules-engine"; -import { JSON_RULESET_ITEM } from "../Personalization/constants/schema"; +import { + JSON_CONTENT_ITEM, + JSON_RULESET_ITEM +} from "../Personalization/constants/schema"; import flattenArray from "../../utils/flattenArray"; import createConsequenceAdapter from "./createConsequenceAdapter"; +const isJsonRulesetItem = item => { + const { schema, data } = item; + + if (schema === JSON_RULESET_ITEM) { + return true; + } + + if (schema !== JSON_CONTENT_ITEM) { + return false; + } + + const content = + typeof data.content === "string" ? JSON.parse(data.content) : data.content; + + return ( + content && + Object.prototype.hasOwnProperty.call(content, "version") && + Object.prototype.hasOwnProperty.call(content, "rules") + ); +}; + export default payload => { const consequenceAdapter = createConsequenceAdapter(); const items = []; @@ -41,9 +65,7 @@ export default payload => { }; if (Array.isArray(payload.items)) { - payload.items - .filter(item => item.schema === JSON_RULESET_ITEM) - .forEach(addItem); + payload.items.filter(isJsonRulesetItem).forEach(addItem); } return { diff --git a/test/unit/specs/components/DecisioningEngine/contextTestUtils.js b/test/unit/specs/components/DecisioningEngine/contextTestUtils.js index f6a52a9fa..e5b723243 100644 --- a/test/unit/specs/components/DecisioningEngine/contextTestUtils.js +++ b/test/unit/specs/components/DecisioningEngine/contextTestUtils.js @@ -71,7 +71,7 @@ export const payloadWithCondition = condition => { }, consequences: [ { - type: "item", + type: "schema", detail: { schema: "https://ns.adobe.com/personalization/mock-action", diff --git a/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js b/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js index 00c7906f7..d90fc28f9 100644 --- a/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js @@ -79,7 +79,7 @@ describe("DecisioningEngine:createDecisionProvider", () => { }, consequences: [ { - type: "item", + type: "schema", detail: { schema: "https://ns.adobe.com/personalization/dom-action", @@ -98,7 +98,7 @@ describe("DecisioningEngine:createDecisionProvider", () => { id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" }, { - type: "item", + type: "schema", detail: { schema: "https://ns.adobe.com/personalization/dom-action", @@ -177,7 +177,7 @@ describe("DecisioningEngine:createDecisionProvider", () => { }, consequences: [ { - type: "item", + type: "schema", detail: { schema: "https://ns.adobe.com/personalization/dom-action", @@ -192,7 +192,7 @@ describe("DecisioningEngine:createDecisionProvider", () => { id: "8a0d7a45-70fb-4845-a093-2133b5744c8d" }, { - type: "item", + type: "schema", detail: { schema: "https://ns.adobe.com/personalization/dom-action", diff --git a/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js b/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js index c1c80925d..e34c2561e 100644 --- a/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js @@ -9,125 +9,169 @@ 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 RulesEngine from "@adobe/aep-rules-engine"; import createEvaluableRulesetPayload from "../../../../../src/components/DecisioningEngine/createEvaluableRulesetPayload"; describe("DecisioningEngine:createEvaluableRulesetPayload", () => { - it("does", () => { - const ruleset = RulesEngine({ - version: 1, - rules: [ + it("consumes json-ruleset-items", () => { + const evaluableRulesetPayload = createEvaluableRulesetPayload({ + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: "abc" + }, + strategies: [ + { + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" + } + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" + }, + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" + }, + id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", + items: [ { - condition: { - definition: { - conditions: [ + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", + schema: "https://ns.adobe.com/personalization/json-ruleset-item", + data: { + content: JSON.stringify({ + version: 1, + rules: [ { - definition: { - conditions: [ - { - definition: { - key: "color", - matcher: "eq", - values: ["orange", "blue"] + condition: { + definition: { + conditions: [ + { + definition: { + conditions: [ + { + definition: { + key: "color", + matcher: "eq", + values: ["orange", "blue"] + }, + type: "matcher" + }, + { + definition: { + key: "action", + matcher: "eq", + values: ["lipstick"] + }, + type: "matcher" + } + ], + logic: "and" + }, + type: "group" + } + ], + logic: "and" + }, + type: "group" + }, + consequences: [ + { + type: "schema", + detail: { + schema: + "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" + }, + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" }, - type: "matcher" + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" }, - { - definition: { - key: "action", - matcher: "eq", - values: ["lipstick"] + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }, + { + type: "schema", + detail: { + schema: + "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" }, - type: "matcher" - } - ], - logic: "and" - }, - type: "group" + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + } + ] } - ], - logic: "and" - }, - type: "group" - }, - consequences: [ - { - type: "item", - detail: { - schema: "https://ns.adobe.com/personalization/dom-action", - data: { - selector: - "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", - type: "setAttribute", - content: { - src: "img/demo-marketing-offer1-exp-A.png" - }, - prehidingSelector: - "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" - }, - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" - }, - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" - }, - { - type: "item", - detail: { - schema: "https://ns.adobe.com/personalization/dom-action", - data: { - selector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", - type: "setHtml", - content: "Hello Treatment A!", - prehidingSelector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" - }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" - }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" - } - ] + ] + }) + } } - ] + ], + scope: "web://mywebsite.com" }); - expect(ruleset.execute({ color: "orange", action: "lipstick" })).toEqual([ - [ + expect( + evaluableRulesetPayload.evaluate({ color: "orange", action: "lipstick" }) + ).toEqual({ + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: "abc" + }, + strategies: [ + { + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" + } + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" + }, + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" + }, + id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", + items: [ { - type: "item", - detail: { - schema: "https://ns.adobe.com/personalization/dom-action", - data: { - selector: "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", - type: "setAttribute", - content: { - src: "img/demo-marketing-offer1-exp-A.png" - }, - prehidingSelector: - "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" }, - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" }, id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" }, { - type: "item", - detail: { - schema: "https://ns.adobe.com/personalization/dom-action", - data: { - selector: "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", - type: "setHtml", - content: "Hello Treatment A!", - prehidingSelector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" - }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" }, id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" } - ] - ]); + ], + scope: "web://mywebsite.com" + }); }); - it("works", () => { + + it("consumes json-content-items", () => { const evaluableRulesetPayload = createEvaluableRulesetPayload({ scopeDetails: { decisionProvider: "AJO", @@ -150,9 +194,9 @@ describe("DecisioningEngine:createEvaluableRulesetPayload", () => { items: [ { id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", - schema: "https://ns.adobe.com/personalization/json-ruleset-item", + schema: "https://ns.adobe.com/personalization/json-content-item", data: { - content: JSON.stringify({ + content: { version: 1, rules: [ { @@ -190,7 +234,7 @@ describe("DecisioningEngine:createEvaluableRulesetPayload", () => { }, consequences: [ { - type: "item", + type: "schema", detail: { schema: "https://ns.adobe.com/personalization/dom-action", @@ -209,7 +253,7 @@ describe("DecisioningEngine:createEvaluableRulesetPayload", () => { id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" }, { - type: "item", + type: "schema", detail: { schema: "https://ns.adobe.com/personalization/dom-action", @@ -228,7 +272,7 @@ describe("DecisioningEngine:createEvaluableRulesetPayload", () => { ] } ] - }) + } } } ], diff --git a/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js b/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js index 394bef954..50e147522 100644 --- a/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js @@ -114,7 +114,7 @@ describe("DecisioningEngine:createOnResponseHandler", () => { }, consequences: [ { - type: "item", + type: "schema", detail: { schema: "https://ns.adobe.com/personalization/dom-action", @@ -133,7 +133,7 @@ describe("DecisioningEngine:createOnResponseHandler", () => { id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" }, { - type: "item", + type: "schema", detail: { schema: "https://ns.adobe.com/personalization/dom-action", @@ -295,7 +295,7 @@ describe("DecisioningEngine:createOnResponseHandler", () => { }, consequences: [ { - type: "item", + type: "schema", detail: { schema: "https://ns.adobe.com/personalization/dom-action", From f0b9c69aa72014815ee5e874b35664628bfeb32f Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Wed, 23 Aug 2023 11:57:27 -0600 Subject: [PATCH 31/66] update to schema based actions --- .../Personalization/constants/schema.js | 10 +++++++-- .../createApplyPropositions.js | 4 ++-- .../Personalization/createModules.js | 11 ++++++++-- .../createPersonalizationDetails.js | 4 ++-- .../createSubscribeMessageFeed.js | 10 ++++----- .../Personalization/groupDecisions.js | 4 ++-- .../initMessagingActionsModules.js | 2 +- .../Personalization/createModules.spec.js | 14 +++++++----- .../createPersonalizationDetails.spec.js | 16 +++++++------- .../createSubscribeMessageFeed.spec.js | 22 +++++++++---------- .../initMessagingActionsModules.spec.js | 4 ++-- 11 files changed, 57 insertions(+), 44 deletions(-) diff --git a/src/components/Personalization/constants/schema.js b/src/components/Personalization/constants/schema.js index 4b33fbf97..99ea09d8e 100644 --- a/src/components/Personalization/constants/schema.js +++ b/src/components/Personalization/constants/schema.js @@ -22,7 +22,13 @@ export const JSON_RULESET_ITEM = export const REDIRECT_ITEM = "https://ns.adobe.com/personalization/redirect-item"; -export const IN_APP_MESSAGE = - "https://ns.adobe.com/personalization/in-app-message"; +export const MESSAGE_IN_APP = + "https://ns.adobe.com/personalization/message/in-app"; + +export const MESSAGE_FEED_ITEM = + "https://ns.adobe.com/personalization/message/feed-item"; +export const MESSAGE_NATIVE_ALERT = + "https://ns.adobe.com/personalization/message/native-alert"; + export const MEASUREMENT_SCHEMA = "https://ns.adobe.com/personalization/measurement"; diff --git a/src/components/Personalization/createApplyPropositions.js b/src/components/Personalization/createApplyPropositions.js index 8657b55d8..9fabed22e 100644 --- a/src/components/Personalization/createApplyPropositions.js +++ b/src/components/Personalization/createApplyPropositions.js @@ -15,7 +15,7 @@ import { isNonEmptyArray, isObject } from "../../utils"; import { DOM_ACTION, HTML_CONTENT_ITEM, - IN_APP_MESSAGE + MESSAGE_IN_APP } from "./constants/schema"; import PAGE_WIDE_SCOPE from "../../constants/pageWideScope"; import { EMPTY_PROPOSITIONS } from "./validateApplyPropositionsOptions"; @@ -23,7 +23,7 @@ import { EMPTY_PROPOSITIONS } from "./validateApplyPropositionsOptions"; export const SUPPORTED_SCHEMAS = [ DOM_ACTION, HTML_CONTENT_ITEM, - IN_APP_MESSAGE + MESSAGE_IN_APP ]; export default ({ executeDecisions }) => { diff --git a/src/components/Personalization/createModules.js b/src/components/Personalization/createModules.js index 017b9d034..715484de2 100644 --- a/src/components/Personalization/createModules.js +++ b/src/components/Personalization/createModules.js @@ -9,13 +9,20 @@ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTA OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import { DOM_ACTION, IN_APP_MESSAGE } from "./constants/schema"; +import { + DOM_ACTION, + MESSAGE_FEED_ITEM, + MESSAGE_IN_APP +} from "./constants/schema"; import { initDomActionsModules } from "./dom-actions"; import initMessagingActionsModules from "./in-app-message-actions/initMessagingActionsModules"; export default ({ storeClickMetrics, collect }) => { + const messagingActionsModules = initMessagingActionsModules(collect); + return { [DOM_ACTION]: initDomActionsModules(storeClickMetrics), - [IN_APP_MESSAGE]: initMessagingActionsModules(collect) + [MESSAGE_IN_APP]: messagingActionsModules, + [MESSAGE_FEED_ITEM]: messagingActionsModules }; }; diff --git a/src/components/Personalization/createPersonalizationDetails.js b/src/components/Personalization/createPersonalizationDetails.js index 952d22f5a..0e3c1bcb2 100644 --- a/src/components/Personalization/createPersonalizationDetails.js +++ b/src/components/Personalization/createPersonalizationDetails.js @@ -17,7 +17,7 @@ import { DEFAULT_CONTENT_ITEM, DOM_ACTION, HTML_CONTENT_ITEM, - IN_APP_MESSAGE, + MESSAGE_IN_APP, JSON_CONTENT_ITEM, REDIRECT_ITEM } from "./constants/schema"; @@ -88,7 +88,7 @@ export default ({ HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, - IN_APP_MESSAGE + MESSAGE_IN_APP ]; if (includes(scopes, PAGE_WIDE_SCOPE)) { diff --git a/src/components/Personalization/createSubscribeMessageFeed.js b/src/components/Personalization/createSubscribeMessageFeed.js index 2142b13c3..d89915dc9 100644 --- a/src/components/Personalization/createSubscribeMessageFeed.js +++ b/src/components/Personalization/createSubscribeMessageFeed.js @@ -1,6 +1,6 @@ /* eslint-disable */ import { callback, objectOf, string } from "../../utils/validation"; -import { IN_APP_MESSAGE } from "./constants/schema"; +import { MESSAGE_FEED_ITEM, MESSAGE_IN_APP } from "./constants/schema"; import { DISPLAY, INTERACT } from "./constants/eventType"; const validateSubscribeMessageFeedOptions = ({ options }) => { @@ -27,12 +27,13 @@ export default ({ collect }) => { const { id, scope, scopeDetails } = payload; const { data = {}, qualifiedDate, displayedDate } = item; - const { content = {} } = data; + const { content = {}, publishedDate } = data; return { ...content, qualifiedDate, displayedDate, + publishedDate, getSurface: () => data.meta.surface, getAnalyticsDetail: () => { return { id, scope, scopeDetails }; @@ -88,10 +89,7 @@ export default ({ collect }) => { return [ ...allItems, ...items - .filter( - item => - item.schema === IN_APP_MESSAGE && item.data.type === "feed" - ) + .filter(item => item.schema === MESSAGE_FEED_ITEM) .map(item => createFeedItem(payload, item)) ]; }, []) diff --git a/src/components/Personalization/groupDecisions.js b/src/components/Personalization/groupDecisions.js index 17485f649..00b979f01 100644 --- a/src/components/Personalization/groupDecisions.js +++ b/src/components/Personalization/groupDecisions.js @@ -17,7 +17,7 @@ import { REDIRECT_ITEM, DEFAULT_CONTENT_ITEM, MEASUREMENT_SCHEMA, - IN_APP_MESSAGE + MESSAGE_IN_APP } from "./constants/schema"; import { VIEW_SCOPE_TYPE } from "./constants/scopeType"; @@ -125,7 +125,7 @@ const groupDecisions = unprocessedDecisions => { const decisionsGroupedByRenderableSchemas = splitDecisions( mergedMetricDecisions.unmatchedDecisions, DOM_ACTION, - IN_APP_MESSAGE, + MESSAGE_IN_APP, DEFAULT_CONTENT_ITEM ); // group renderable decisions by scope diff --git a/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js b/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js index 2623a993b..d83432db9 100644 --- a/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js +++ b/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js @@ -8,6 +8,6 @@ export default collect => { return { modal: settings => displayModal(settings, collect), banner: settings => displayBanner(settings, collect), - feed: () => Promise.resolve() // TODO: consider not using in-app-message type here, or leveraging this for some purpose + defaultContent: () => Promise.resolve() }; }; diff --git a/test/unit/specs/components/Personalization/createModules.spec.js b/test/unit/specs/components/Personalization/createModules.spec.js index 1028dca2c..aae103253 100644 --- a/test/unit/specs/components/Personalization/createModules.spec.js +++ b/test/unit/specs/components/Personalization/createModules.spec.js @@ -12,7 +12,8 @@ governing permissions and limitations under the License. import createModules from "../../../../../src/components/Personalization/createModules"; import { DOM_ACTION, - IN_APP_MESSAGE + MESSAGE_FEED_ITEM, + MESSAGE_IN_APP } from "../../../../../src/components/Personalization/constants/schema"; describe("createModules", () => { @@ -47,12 +48,15 @@ describe("createModules", () => { it("has in-app-message modules", () => { const modules = createModules({ storeClickMetrics: noop, collect: noop }); - expect(modules[IN_APP_MESSAGE]).toEqual({ + const messageModules = { modal: jasmine.any(Function), banner: jasmine.any(Function), - feed: jasmine.any(Function) - }); + defaultContent: jasmine.any(Function) + }; + + expect(modules[MESSAGE_IN_APP]).toEqual(messageModules); + expect(modules[MESSAGE_FEED_ITEM]).toEqual(messageModules); - expect(Object.keys(modules[IN_APP_MESSAGE]).length).toEqual(3); + expect(Object.keys(modules[MESSAGE_IN_APP]).length).toEqual(3); }); }); diff --git a/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js b/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js index be2b41557..b2343c814 100644 --- a/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js +++ b/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js @@ -16,7 +16,7 @@ import { DEFAULT_CONTENT_ITEM, DOM_ACTION, HTML_CONTENT_ITEM, - IN_APP_MESSAGE, + MESSAGE_IN_APP, JSON_CONTENT_ITEM, REDIRECT_ITEM } from "../../../../../src/components/Personalization/constants/schema"; @@ -63,7 +63,7 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, - IN_APP_MESSAGE, + MESSAGE_IN_APP, DOM_ACTION ], decisionScopes: expectedDecisionScopes, @@ -102,7 +102,7 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, - IN_APP_MESSAGE, + MESSAGE_IN_APP, DOM_ACTION ], decisionScopes: expectedDecisionScopes, @@ -141,7 +141,7 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, - IN_APP_MESSAGE, + MESSAGE_IN_APP, DOM_ACTION ], decisionScopes: expectedDecisionScopes, @@ -180,7 +180,7 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, - IN_APP_MESSAGE + MESSAGE_IN_APP ], decisionScopes: expectedDecisionScopes, surfaces: [] @@ -220,7 +220,7 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, - IN_APP_MESSAGE + MESSAGE_IN_APP ], decisionScopes: expectedDecisionScopes, surfaces: ["web://test1.com/"] @@ -262,7 +262,7 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, - IN_APP_MESSAGE + MESSAGE_IN_APP ], decisionScopes: expectedDecisionScopes, surfaces: ["web://test1.com/"] @@ -398,7 +398,7 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, - IN_APP_MESSAGE, + MESSAGE_IN_APP, DOM_ACTION ], decisionScopes: expectedDecisionScopes, diff --git a/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js b/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js index 5125306db..df3e1f40c 100644 --- a/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js +++ b/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js @@ -1,4 +1,5 @@ import createSubscribeMessageFeed from "../../../../../src/components/Personalization/createSubscribeMessageFeed"; +import { MESSAGE_FEED_ITEM } from "../../../../../src/components/Personalization/constants/schema"; describe("Personalization:subscribeMessageFeed", () => { let collect; @@ -43,10 +44,10 @@ describe("Personalization:subscribeMessageFeed", () => { id: "1a3d874f-39ee-4310-bfa9-6559a10041a4", items: [ { - schema: "https://ns.adobe.com/personalization/in-app-message", + schema: MESSAGE_FEED_ITEM, data: { expiryDate: 1712190456, - type: "feed", + publishedDate: 1677752640000, meta: { feedName: "Winter Promo", surface: "web://mywebsite.com/feed" @@ -56,7 +57,6 @@ describe("Personalization:subscribeMessageFeed", () => { imageUrl: "img/lumon.png", actionTitle: "Shop the sale!", actionUrl: "https://luma.com/sale", - publishedDate: 1677752640000, body: "a handshake is available upon request.", title: "Welcome to Lumon!" }, @@ -67,10 +67,11 @@ describe("Personalization:subscribeMessageFeed", () => { displayedDate: 1683042628070 }, { - schema: "https://ns.adobe.com/personalization/in-app-message", + schema: MESSAGE_FEED_ITEM, data: { expiryDate: 1712190456, - type: "feed", + publishedDate: 1677839040000, + meta: { feedName: "Winter Promo", surface: "web://mywebsite.com/feed" @@ -80,7 +81,6 @@ describe("Personalization:subscribeMessageFeed", () => { imageUrl: "img/achievement.png", actionTitle: "Shop the sale!", actionUrl: "https://luma.com/sale", - publishedDate: 1677839040000, body: "Great job, you completed your profile.", title: "Achievement Unlocked!" }, @@ -97,10 +97,10 @@ describe("Personalization:subscribeMessageFeed", () => { id: "1ae11bc5-96dc-41c7-8f71-157c57a5290e", items: [ { - schema: "https://ns.adobe.com/personalization/in-app-message", + schema: MESSAGE_FEED_ITEM, data: { expiryDate: 1712190456, - type: "feed", + publishedDate: 1678098240000, meta: { feedName: "Winter Promo", surface: "web://mywebsite.com/feed" @@ -110,7 +110,6 @@ describe("Personalization:subscribeMessageFeed", () => { imageUrl: "img/twitter.png", actionTitle: "Shop the sale!", actionUrl: "https://luma.com/sale", - publishedDate: 1678098240000, body: "Posting on social media helps us spread the word.", title: "Thanks for sharing!" }, @@ -149,10 +148,10 @@ describe("Personalization:subscribeMessageFeed", () => { id: "d1f7d411-a549-47bc-a4d8-c8e638b0a46b", items: [ { - schema: "https://ns.adobe.com/personalization/in-app-message", + schema: MESSAGE_FEED_ITEM, data: { expiryDate: 1712190456, - type: "feed", + publishedDate: 1678184640000, meta: { feedName: "Winter Promo", surface: "web://mywebsite.com/feed" @@ -162,7 +161,6 @@ describe("Personalization:subscribeMessageFeed", () => { imageUrl: "img/gold-coin.jpg", actionTitle: "Shop the sale!", actionUrl: "https://luma.com/sale", - publishedDate: 1678184640000, body: "Now you're ready to earn!", title: "Funds deposited!" }, diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/initMessagingActionsModules.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/initMessagingActionsModules.spec.js index 600413ccd..a2dbd792c 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/initMessagingActionsModules.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/initMessagingActionsModules.spec.js @@ -12,12 +12,12 @@ governing permissions and limitations under the License. import initMessagingActionsModules from "../../../../../../src/components/Personalization/in-app-message-actions/initMessagingActionsModules"; import createModules from "../../../../../../src/components/Personalization/createModules"; -import { IN_APP_MESSAGE } from "../../../../../../src/components/Personalization/constants/schema"; +import { MESSAGE_IN_APP } from "../../../../../../src/components/Personalization/constants/schema"; describe("Personalization::turbine::initMessagingActionsModules", () => { const noop = () => undefined; const modules = createModules({ storeClickMetrics: noop, collect: noop }); - const expectedModules = modules[IN_APP_MESSAGE]; + const expectedModules = modules[MESSAGE_IN_APP]; it("should have all the required modules", () => { const messagingActionsModules = initMessagingActionsModules(() => {}); From 52f44b1436bbcde3501166e4e002b67e627eb191 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Fri, 25 Aug 2023 13:47:47 -0600 Subject: [PATCH 32/66] update schemas --- .../Personalization/constants/schema.js | 9 +++++++-- .../Personalization/createApplyPropositions.js | 4 ++-- src/components/Personalization/createModules.js | 4 ++-- .../createPersonalizationDetails.js | 4 ++-- src/components/Personalization/groupDecisions.js | 4 ++-- .../initMessagingActionsModules.js | 3 ++- .../Personalization/createModules.spec.js | 9 +++++---- .../createPersonalizationDetails.spec.js | 16 ++++++++-------- .../initMessagingActionsModules.spec.js | 4 ++-- 9 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/components/Personalization/constants/schema.js b/src/components/Personalization/constants/schema.js index 4b33fbf97..239029ef1 100644 --- a/src/components/Personalization/constants/schema.js +++ b/src/components/Personalization/constants/schema.js @@ -19,10 +19,15 @@ export const JSON_CONTENT_ITEM = "https://ns.adobe.com/personalization/json-content-item"; export const JSON_RULESET_ITEM = "https://ns.adobe.com/personalization/json-ruleset-item"; - export const REDIRECT_ITEM = "https://ns.adobe.com/personalization/redirect-item"; -export const IN_APP_MESSAGE = + +export const MESSAGE_IN_APP = "https://ns.adobe.com/personalization/in-app-message"; +export const MESSAGE_FEED_ITEM = + "https://ns.adobe.com/personalization/message/feed-item"; +export const MESSAGE_NATIVE_ALERT = + "https://ns.adobe.com/personalization/message/native-alert"; + export const MEASUREMENT_SCHEMA = "https://ns.adobe.com/personalization/measurement"; diff --git a/src/components/Personalization/createApplyPropositions.js b/src/components/Personalization/createApplyPropositions.js index 8657b55d8..9fabed22e 100644 --- a/src/components/Personalization/createApplyPropositions.js +++ b/src/components/Personalization/createApplyPropositions.js @@ -15,7 +15,7 @@ import { isNonEmptyArray, isObject } from "../../utils"; import { DOM_ACTION, HTML_CONTENT_ITEM, - IN_APP_MESSAGE + MESSAGE_IN_APP } from "./constants/schema"; import PAGE_WIDE_SCOPE from "../../constants/pageWideScope"; import { EMPTY_PROPOSITIONS } from "./validateApplyPropositionsOptions"; @@ -23,7 +23,7 @@ import { EMPTY_PROPOSITIONS } from "./validateApplyPropositionsOptions"; export const SUPPORTED_SCHEMAS = [ DOM_ACTION, HTML_CONTENT_ITEM, - IN_APP_MESSAGE + MESSAGE_IN_APP ]; export default ({ executeDecisions }) => { diff --git a/src/components/Personalization/createModules.js b/src/components/Personalization/createModules.js index 017b9d034..a9884775e 100644 --- a/src/components/Personalization/createModules.js +++ b/src/components/Personalization/createModules.js @@ -9,13 +9,13 @@ 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 { DOM_ACTION, IN_APP_MESSAGE } from "./constants/schema"; +import { DOM_ACTION, MESSAGE_IN_APP } from "./constants/schema"; import { initDomActionsModules } from "./dom-actions"; import initMessagingActionsModules from "./in-app-message-actions/initMessagingActionsModules"; export default ({ storeClickMetrics, collect }) => { return { [DOM_ACTION]: initDomActionsModules(storeClickMetrics), - [IN_APP_MESSAGE]: initMessagingActionsModules(collect) + [MESSAGE_IN_APP]: initMessagingActionsModules(collect) }; }; diff --git a/src/components/Personalization/createPersonalizationDetails.js b/src/components/Personalization/createPersonalizationDetails.js index 952d22f5a..0e3c1bcb2 100644 --- a/src/components/Personalization/createPersonalizationDetails.js +++ b/src/components/Personalization/createPersonalizationDetails.js @@ -17,7 +17,7 @@ import { DEFAULT_CONTENT_ITEM, DOM_ACTION, HTML_CONTENT_ITEM, - IN_APP_MESSAGE, + MESSAGE_IN_APP, JSON_CONTENT_ITEM, REDIRECT_ITEM } from "./constants/schema"; @@ -88,7 +88,7 @@ export default ({ HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, - IN_APP_MESSAGE + MESSAGE_IN_APP ]; if (includes(scopes, PAGE_WIDE_SCOPE)) { diff --git a/src/components/Personalization/groupDecisions.js b/src/components/Personalization/groupDecisions.js index 17485f649..00b979f01 100644 --- a/src/components/Personalization/groupDecisions.js +++ b/src/components/Personalization/groupDecisions.js @@ -17,7 +17,7 @@ import { REDIRECT_ITEM, DEFAULT_CONTENT_ITEM, MEASUREMENT_SCHEMA, - IN_APP_MESSAGE + MESSAGE_IN_APP } from "./constants/schema"; import { VIEW_SCOPE_TYPE } from "./constants/scopeType"; @@ -125,7 +125,7 @@ const groupDecisions = unprocessedDecisions => { const decisionsGroupedByRenderableSchemas = splitDecisions( mergedMetricDecisions.unmatchedDecisions, DOM_ACTION, - IN_APP_MESSAGE, + MESSAGE_IN_APP, DEFAULT_CONTENT_ITEM ); // group renderable decisions by scope diff --git a/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js b/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js index 7f225aa31..d83432db9 100644 --- a/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js +++ b/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js @@ -7,6 +7,7 @@ export default collect => { // TODO: use collect to capture click and display metrics return { modal: settings => displayModal(settings, collect), - banner: settings => displayBanner(settings, collect) + banner: settings => displayBanner(settings, collect), + defaultContent: () => Promise.resolve() }; }; diff --git a/test/unit/specs/components/Personalization/createModules.spec.js b/test/unit/specs/components/Personalization/createModules.spec.js index 0b4abedfe..3d933c0a4 100644 --- a/test/unit/specs/components/Personalization/createModules.spec.js +++ b/test/unit/specs/components/Personalization/createModules.spec.js @@ -12,7 +12,7 @@ governing permissions and limitations under the License. import createModules from "../../../../../src/components/Personalization/createModules"; import { DOM_ACTION, - IN_APP_MESSAGE + MESSAGE_IN_APP } from "../../../../../src/components/Personalization/constants/schema"; describe("createModules", () => { @@ -47,11 +47,12 @@ describe("createModules", () => { it("has in-app-message modules", () => { const modules = createModules({ storeClickMetrics: noop, collect: noop }); - expect(modules[IN_APP_MESSAGE]).toEqual({ + expect(modules[MESSAGE_IN_APP]).toEqual({ modal: jasmine.any(Function), - banner: jasmine.any(Function) + banner: jasmine.any(Function), + defaultContent: jasmine.any(Function) }); - expect(Object.keys(modules[IN_APP_MESSAGE]).length).toEqual(2); + expect(Object.keys(modules[MESSAGE_IN_APP]).length).toEqual(3); }); }); diff --git a/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js b/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js index be2b41557..b2343c814 100644 --- a/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js +++ b/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js @@ -16,7 +16,7 @@ import { DEFAULT_CONTENT_ITEM, DOM_ACTION, HTML_CONTENT_ITEM, - IN_APP_MESSAGE, + MESSAGE_IN_APP, JSON_CONTENT_ITEM, REDIRECT_ITEM } from "../../../../../src/components/Personalization/constants/schema"; @@ -63,7 +63,7 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, - IN_APP_MESSAGE, + MESSAGE_IN_APP, DOM_ACTION ], decisionScopes: expectedDecisionScopes, @@ -102,7 +102,7 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, - IN_APP_MESSAGE, + MESSAGE_IN_APP, DOM_ACTION ], decisionScopes: expectedDecisionScopes, @@ -141,7 +141,7 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, - IN_APP_MESSAGE, + MESSAGE_IN_APP, DOM_ACTION ], decisionScopes: expectedDecisionScopes, @@ -180,7 +180,7 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, - IN_APP_MESSAGE + MESSAGE_IN_APP ], decisionScopes: expectedDecisionScopes, surfaces: [] @@ -220,7 +220,7 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, - IN_APP_MESSAGE + MESSAGE_IN_APP ], decisionScopes: expectedDecisionScopes, surfaces: ["web://test1.com/"] @@ -262,7 +262,7 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, - IN_APP_MESSAGE + MESSAGE_IN_APP ], decisionScopes: expectedDecisionScopes, surfaces: ["web://test1.com/"] @@ -398,7 +398,7 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, - IN_APP_MESSAGE, + MESSAGE_IN_APP, DOM_ACTION ], decisionScopes: expectedDecisionScopes, diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/initMessagingActionsModules.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/initMessagingActionsModules.spec.js index 600413ccd..a2dbd792c 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/initMessagingActionsModules.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/initMessagingActionsModules.spec.js @@ -12,12 +12,12 @@ governing permissions and limitations under the License. import initMessagingActionsModules from "../../../../../../src/components/Personalization/in-app-message-actions/initMessagingActionsModules"; import createModules from "../../../../../../src/components/Personalization/createModules"; -import { IN_APP_MESSAGE } from "../../../../../../src/components/Personalization/constants/schema"; +import { MESSAGE_IN_APP } from "../../../../../../src/components/Personalization/constants/schema"; describe("Personalization::turbine::initMessagingActionsModules", () => { const noop = () => undefined; const modules = createModules({ storeClickMetrics: noop, collect: noop }); - const expectedModules = modules[IN_APP_MESSAGE]; + const expectedModules = modules[MESSAGE_IN_APP]; it("should have all the required modules", () => { const messagingActionsModules = initMessagingActionsModules(() => {}); From 303d992d0e6ab167bc7345bd51536a680249c6ff Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Fri, 25 Aug 2023 14:17:28 -0600 Subject: [PATCH 33/66] rename json-ruleset-item to ruleset-item --- .../createEvaluableRulesetPayload.js | 13 +- .../Personalization/constants/schema.js | 3 +- .../DecisioningEngine/contextTestUtils.js | 49 ++-- .../createDecisionProvider.spec.js | 252 +++++++++-------- .../createEvaluableRulesetPayload.spec.js | 138 +++++----- .../createOnResponseHandler.spec.js | 260 +++++++++--------- 6 files changed, 350 insertions(+), 365 deletions(-) diff --git a/src/components/DecisioningEngine/createEvaluableRulesetPayload.js b/src/components/DecisioningEngine/createEvaluableRulesetPayload.js index e1afd4e1f..9b85aebfe 100644 --- a/src/components/DecisioningEngine/createEvaluableRulesetPayload.js +++ b/src/components/DecisioningEngine/createEvaluableRulesetPayload.js @@ -12,15 +12,15 @@ governing permissions and limitations under the License. import RulesEngine from "@adobe/aep-rules-engine"; import { JSON_CONTENT_ITEM, - JSON_RULESET_ITEM + RULESET_ITEM } from "../Personalization/constants/schema"; import flattenArray from "../../utils/flattenArray"; import createConsequenceAdapter from "./createConsequenceAdapter"; -const isJsonRulesetItem = item => { +const isRulesetItem = item => { const { schema, data } = item; - if (schema === JSON_RULESET_ITEM) { + if (schema === RULESET_ITEM) { return true; } @@ -43,8 +43,9 @@ export default payload => { const items = []; const addItem = item => { - const { data = {} } = item; - const { content } = data; + const { data = {}, schema } = item; + + const content = schema === RULESET_ITEM ? data : data.content; if (!content) { return; @@ -65,7 +66,7 @@ export default payload => { }; if (Array.isArray(payload.items)) { - payload.items.filter(isJsonRulesetItem).forEach(addItem); + payload.items.filter(isRulesetItem).forEach(addItem); } return { diff --git a/src/components/Personalization/constants/schema.js b/src/components/Personalization/constants/schema.js index 239029ef1..6421ed0e1 100644 --- a/src/components/Personalization/constants/schema.js +++ b/src/components/Personalization/constants/schema.js @@ -17,8 +17,7 @@ export const HTML_CONTENT_ITEM = "https://ns.adobe.com/personalization/html-content-item"; export const JSON_CONTENT_ITEM = "https://ns.adobe.com/personalization/json-content-item"; -export const JSON_RULESET_ITEM = - "https://ns.adobe.com/personalization/json-ruleset-item"; +export const RULESET_ITEM = "https://ns.adobe.com/personalization/ruleset-item"; export const REDIRECT_ITEM = "https://ns.adobe.com/personalization/redirect-item"; diff --git a/test/unit/specs/components/DecisioningEngine/contextTestUtils.js b/test/unit/specs/components/DecisioningEngine/contextTestUtils.js index e5b723243..43e6a8603 100644 --- a/test/unit/specs/components/DecisioningEngine/contextTestUtils.js +++ b/test/unit/specs/components/DecisioningEngine/contextTestUtils.js @@ -56,36 +56,33 @@ export const payloadWithCondition = condition => { items: [ { id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", - schema: "https://ns.adobe.com/personalization/json-ruleset-item", + schema: "https://ns.adobe.com/personalization/ruleset-item", data: { - content: JSON.stringify({ - version: 1, - rules: [ - { - condition: { - definition: { - conditions: [condition], - logic: "and" - }, - type: "group" + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [condition], + logic: "and" }, - consequences: [ - { - type: "schema", - detail: { - schema: - "https://ns.adobe.com/personalization/mock-action", - data: { - hello: "kitty" - }, - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + type: "group" + }, + consequences: [ + { + type: "schema", + detail: { + schema: "https://ns.adobe.com/personalization/mock-action", + data: { + hello: "kitty" }, id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" - } - ] - } - ] - }) + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + } + ] + } + ] } } ], diff --git a/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js b/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js index d90fc28f9..e392794e7 100644 --- a/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js @@ -39,85 +39,83 @@ describe("DecisioningEngine:createDecisionProvider", () => { items: [ { id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", - schema: "https://ns.adobe.com/personalization/json-ruleset-item", + schema: "https://ns.adobe.com/personalization/ruleset-item", data: { - content: JSON.stringify({ - version: 1, - rules: [ - { - condition: { - definition: { - conditions: [ - { - definition: { - conditions: [ - { - definition: { - key: "color", - matcher: "eq", - values: ["orange", "blue"] - }, - type: "matcher" + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [ + { + definition: { + conditions: [ + { + definition: { + key: "color", + matcher: "eq", + values: ["orange", "blue"] }, - { - definition: { - key: "action", - matcher: "eq", - values: ["lipstick"] - }, - type: "matcher" - } - ], - logic: "and" - }, - type: "group" - } - ], - logic: "and" - }, - type: "group" + type: "matcher" + }, + { + definition: { + key: "action", + matcher: "eq", + values: ["lipstick"] + }, + type: "matcher" + } + ], + logic: "and" + }, + type: "group" + } + ], + logic: "and" }, - consequences: [ - { - type: "schema", - detail: { - schema: - "https://ns.adobe.com/personalization/dom-action", - data: { - selector: - "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", - type: "setAttribute", - content: { - src: "img/demo-marketing-offer1-exp-A.png" - }, - prehidingSelector: - "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" + type: "group" + }, + consequences: [ + { + type: "schema", + detail: { + schema: + "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" }, - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" }, id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" }, - { - type: "schema", - detail: { - schema: - "https://ns.adobe.com/personalization/dom-action", - data: { - selector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", - type: "setHtml", - content: "Hello Treatment A!", - prehidingSelector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" - }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }, + { + type: "schema", + detail: { + schema: + "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" }, id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" - } - ] - } - ] - }) + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + } + ] + } + ] } } ], @@ -145,71 +143,69 @@ describe("DecisioningEngine:createDecisionProvider", () => { items: [ { id: "5229f502-38d6-40c3-9a3a-b5b1a6adc441", - schema: "https://ns.adobe.com/personalization/json-ruleset-item", + schema: "https://ns.adobe.com/personalization/ruleset-item", data: { - content: JSON.stringify({ - version: 1, - rules: [ - { - condition: { - definition: { - conditions: [ - { - definition: { - conditions: [ - { - definition: { - key: "xdm.web.webPageDetails.viewName", - matcher: "eq", - values: ["home"] - }, - type: "matcher" - } - ], - logic: "and" - }, - type: "group" - } - ], - logic: "and" - }, - type: "group" - }, - consequences: [ - { - type: "schema", - detail: { - schema: - "https://ns.adobe.com/personalization/dom-action", - data: { - selector: "div#spa #spa-content h3", - type: "setHtml", - content: "i can haz?", - prehidingSelector: "div#spa #spa-content h3" + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [ + { + definition: { + conditions: [ + { + definition: { + key: "xdm.web.webPageDetails.viewName", + matcher: "eq", + values: ["home"] + }, + type: "matcher" + } + ], + logic: "and" }, - id: "8a0d7a45-70fb-4845-a093-2133b5744c8d" + type: "group" + } + ], + logic: "and" + }, + type: "group" + }, + consequences: [ + { + type: "schema", + detail: { + schema: + "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "div#spa #spa-content h3", + type: "setHtml", + content: "i can haz?", + prehidingSelector: "div#spa #spa-content h3" }, id: "8a0d7a45-70fb-4845-a093-2133b5744c8d" }, - { - type: "schema", - detail: { - schema: - "https://ns.adobe.com/personalization/dom-action", - data: { - selector: "div#spa #spa-content p", - type: "setHtml", - content: "ALL YOUR BASE ARE BELONG TO US", - prehidingSelector: "div#spa #spa-content p" - }, - id: "a44af51a-e073-4e8c-92e1-84ac28210043" + id: "8a0d7a45-70fb-4845-a093-2133b5744c8d" + }, + { + type: "schema", + detail: { + schema: + "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "div#spa #spa-content p", + type: "setHtml", + content: "ALL YOUR BASE ARE BELONG TO US", + prehidingSelector: "div#spa #spa-content p" }, id: "a44af51a-e073-4e8c-92e1-84ac28210043" - } - ] - } - ] - }) + }, + id: "a44af51a-e073-4e8c-92e1-84ac28210043" + } + ] + } + ] } } ], diff --git a/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js b/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js index e34c2561e..2e30498a8 100644 --- a/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js @@ -12,7 +12,7 @@ governing permissions and limitations under the License. import createEvaluableRulesetPayload from "../../../../../src/components/DecisioningEngine/createEvaluableRulesetPayload"; describe("DecisioningEngine:createEvaluableRulesetPayload", () => { - it("consumes json-ruleset-items", () => { + it("consumes ruleset-items", () => { const evaluableRulesetPayload = createEvaluableRulesetPayload({ scopeDetails: { decisionProvider: "AJO", @@ -35,85 +35,81 @@ describe("DecisioningEngine:createEvaluableRulesetPayload", () => { items: [ { id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", - schema: "https://ns.adobe.com/personalization/json-ruleset-item", + schema: "https://ns.adobe.com/personalization/ruleset-item", data: { - content: JSON.stringify({ - version: 1, - rules: [ - { - condition: { - definition: { - conditions: [ - { - definition: { - conditions: [ - { - definition: { - key: "color", - matcher: "eq", - values: ["orange", "blue"] - }, - type: "matcher" + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [ + { + definition: { + conditions: [ + { + definition: { + key: "color", + matcher: "eq", + values: ["orange", "blue"] }, - { - definition: { - key: "action", - matcher: "eq", - values: ["lipstick"] - }, - type: "matcher" - } - ], - logic: "and" - }, - type: "group" - } - ], - logic: "and" - }, - type: "group" + type: "matcher" + }, + { + definition: { + key: "action", + matcher: "eq", + values: ["lipstick"] + }, + type: "matcher" + } + ], + logic: "and" + }, + type: "group" + } + ], + logic: "and" }, - consequences: [ - { - type: "schema", - detail: { - schema: - "https://ns.adobe.com/personalization/dom-action", - data: { - selector: - "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", - type: "setAttribute", - content: { - src: "img/demo-marketing-offer1-exp-A.png" - }, - prehidingSelector: - "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" + type: "group" + }, + consequences: [ + { + type: "schema", + detail: { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" }, - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" }, id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" }, - { - type: "schema", - detail: { - schema: - "https://ns.adobe.com/personalization/dom-action", - data: { - selector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", - type: "setHtml", - content: "Hello Treatment A!", - prehidingSelector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" - }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }, + { + type: "schema", + detail: { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" }, id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" - } - ] - } - ] - }) + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + } + ] + } + ] } } ], diff --git a/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js b/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js index 50e147522..8c4417a12 100644 --- a/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js @@ -74,85 +74,83 @@ describe("DecisioningEngine:createOnResponseHandler", () => { items: [ { id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", - schema: "https://ns.adobe.com/personalization/json-ruleset-item", + schema: "https://ns.adobe.com/personalization/ruleset-item", data: { - content: JSON.stringify({ - version: 1, - rules: [ - { - condition: { - definition: { - conditions: [ - { - definition: { - conditions: [ - { - definition: { - key: "color", - matcher: "eq", - values: ["orange", "blue"] - }, - type: "matcher" + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [ + { + definition: { + conditions: [ + { + definition: { + key: "color", + matcher: "eq", + values: ["orange", "blue"] }, - { - definition: { - key: "action", - matcher: "eq", - values: ["lipstick"] - }, - type: "matcher" - } - ], - logic: "and" - }, - type: "group" - } - ], - logic: "and" - }, - type: "group" + type: "matcher" + }, + { + definition: { + key: "action", + matcher: "eq", + values: ["lipstick"] + }, + type: "matcher" + } + ], + logic: "and" + }, + type: "group" + } + ], + logic: "and" }, - consequences: [ - { - type: "schema", - detail: { - schema: - "https://ns.adobe.com/personalization/dom-action", - data: { - selector: - "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", - type: "setAttribute", - content: { - src: "img/demo-marketing-offer1-exp-A.png" - }, - prehidingSelector: - "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" + type: "group" + }, + consequences: [ + { + type: "schema", + detail: { + schema: + "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" }, - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" }, id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" }, - { - type: "schema", - detail: { - schema: - "https://ns.adobe.com/personalization/dom-action", - data: { - selector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", - type: "setHtml", - content: "Hello Treatment A!", - prehidingSelector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" - }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }, + { + type: "schema", + detail: { + schema: + "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" }, id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" - } - ] - } - ] - }) + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + } + ] + } + ] } } ], @@ -247,74 +245,72 @@ describe("DecisioningEngine:createOnResponseHandler", () => { items: [ { id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", - schema: "https://ns.adobe.com/personalization/json-ruleset-item", + schema: "https://ns.adobe.com/personalization/ruleset-item", data: { - content: JSON.stringify({ - version: 1, - rules: [ - { - condition: { - definition: { - conditions: [ - { - definition: { - conditions: [ - { - definition: { - key: "xdm.web.webPageDetails.viewName", - matcher: "eq", - values: ["contact"] - }, - type: "matcher" + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [ + { + definition: { + conditions: [ + { + definition: { + key: "xdm.web.webPageDetails.viewName", + matcher: "eq", + values: ["contact"] }, - { - definition: { - key: "xdm.implementationDetails.version", - matcher: "eq", - values: ["12345"] - }, - type: "matcher" + type: "matcher" + }, + { + definition: { + key: "xdm.implementationDetails.version", + matcher: "eq", + values: ["12345"] }, - { - definition: { - key: "data.moo", - matcher: "eq", - values: ["woof"] - }, - type: "matcher" - } - ], - logic: "and" - }, - type: "group" - } - ], - logic: "and" - }, - type: "group" - }, - consequences: [ - { - type: "schema", - detail: { - schema: - "https://ns.adobe.com/personalization/dom-action", - data: { - selector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", - type: "setHtml", - content: "Hello Treatment A!", - prehidingSelector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" + type: "matcher" + }, + { + definition: { + key: "data.moo", + matcher: "eq", + values: ["woof"] + }, + type: "matcher" + } + ], + logic: "and" }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + type: "group" + } + ], + logic: "and" + }, + type: "group" + }, + consequences: [ + { + type: "schema", + detail: { + schema: + "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" }, id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" - } - ] - } - ] - }) + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + } + ] + } + ] } } ], From 599a676fb8269e73da179558c9d1803829811214 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Mon, 28 Aug 2023 11:04:03 -0600 Subject: [PATCH 34/66] rename in-app message schema --- src/components/Personalization/constants/schema.js | 2 +- .../consequenceAdapters/inAppMessageConsequenceAdapter.spec.js | 2 +- .../DecisioningEngine/createConsequenceAdapter.spec.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Personalization/constants/schema.js b/src/components/Personalization/constants/schema.js index 6421ed0e1..b2c048c87 100644 --- a/src/components/Personalization/constants/schema.js +++ b/src/components/Personalization/constants/schema.js @@ -22,7 +22,7 @@ export const REDIRECT_ITEM = "https://ns.adobe.com/personalization/redirect-item"; export const MESSAGE_IN_APP = - "https://ns.adobe.com/personalization/in-app-message"; + "https://ns.adobe.com/personalization/message/in-app"; export const MESSAGE_FEED_ITEM = "https://ns.adobe.com/personalization/message/feed-item"; export const MESSAGE_NATIVE_ALERT = diff --git a/test/unit/specs/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.spec.js b/test/unit/specs/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.spec.js index d80b6d0e4..c0a221997 100644 --- a/test/unit/specs/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.spec.js +++ b/test/unit/specs/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.spec.js @@ -25,7 +25,7 @@ describe("DecisioningEngine:inAppMessageConsequenceAdapter", () => { } ) ).toEqual({ - schema: "https://ns.adobe.com/personalization/in-app-message", + schema: "https://ns.adobe.com/personalization/message/in-app", data: { type: "modal", mobileParameters: { diff --git a/test/unit/specs/components/DecisioningEngine/createConsequenceAdapter.spec.js b/test/unit/specs/components/DecisioningEngine/createConsequenceAdapter.spec.js index 0e7cbbbca..af4252c53 100644 --- a/test/unit/specs/components/DecisioningEngine/createConsequenceAdapter.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createConsequenceAdapter.spec.js @@ -29,7 +29,7 @@ describe("DecisioningEngine:createConsequenceAdapter", () => { const adaptedConsequence = consequenceAdapter(originalConsequence); expect(adaptedConsequence).toEqual({ - schema: "https://ns.adobe.com/personalization/in-app-message", + schema: "https://ns.adobe.com/personalization/message/in-app", data: { type: "modal", mobileParameters: { From d3f749c231add780dc9331f9a9c9b88c33070358 Mon Sep 17 00:00:00 2001 From: Happy Shandilya Date: Mon, 28 Aug 2023 21:58:44 -0700 Subject: [PATCH 35/66] Dismiss Messages & url click (#1024) * Ui parameter bug fix Added Nonce to script * better styling * demo ready * sandbox demo * added unit test for buildStyleFromParameters * unit test * remove existing modal and overlay before displaying * remove existing modal and overlay before displaying * url click * test * clean up dom * more test * renamed to InAppMessages for sandbox demo * code refactoring based on review comments * test for script tag nonce * transformPayload removed from alloy sandbox, it is taken care on the backend side * test fix --------- Co-authored-by: Jason Waters --- sandbox/src/App.js | 5 + .../src/components/ContentSecurityPolicy.js | 2 +- .../InAppMessagesDemo/InAppMessages.js | 74 ++++ .../InAppMessagesDemo/InAppMessagesStyle.css | 7 + .../actions/displayIframeContent.js | 170 ++++++--- .../in-app-message-actions/utils.js | 7 + .../inAppMessageConsequenceAdapter.spec.js | 11 + .../createConsequenceAdapter.spec.js | 11 + .../actions/displayBanner.spec.js | 18 +- .../actions/displayCustom.spec.js | 11 + .../actions/displayFullScreen.spec.js | 11 + .../actions/displayIframeContent.spec.js | 342 ++++++++++++++++++ .../actions/displayModal.spec.js | 14 +- .../in-app-message-actions/utils.spec.js | 30 +- 14 files changed, 642 insertions(+), 71 deletions(-) create mode 100644 sandbox/src/components/InAppMessagesDemo/InAppMessages.js create mode 100644 sandbox/src/components/InAppMessagesDemo/InAppMessagesStyle.css diff --git a/sandbox/src/App.js b/sandbox/src/App.js index fa22d833f..1282f112b 100755 --- a/sandbox/src/App.js +++ b/sandbox/src/App.js @@ -31,6 +31,7 @@ import PersonalizationFormBased from "./PersonalizationFormBased"; import Identity from "./Identity"; import AlloyVersion from "./components/AlloyVersion"; import ConfigOverrides from "./ConfigOverrides.jsx"; +import InAppMessages from "./components/InAppMessagesDemo/InAppMessages"; const BasicExample = () => { return ( @@ -96,6 +97,9 @@ const BasicExample = () => {
  • Config Overrides
  • +
  • + In-app Messages +

  • @@ -125,6 +129,7 @@ const BasicExample = () => { + diff --git a/sandbox/src/components/ContentSecurityPolicy.js b/sandbox/src/components/ContentSecurityPolicy.js index 0d650dd3f..f629006a4 100644 --- a/sandbox/src/components/ContentSecurityPolicy.js +++ b/sandbox/src/components/ContentSecurityPolicy.js @@ -20,7 +20,7 @@ export default function ContentSecurityPolicy() { http-equiv="Content-Security-Policy" // cdn.tt.omtrdc.net is necessary for Target VEC to function properly. // *.sc.omtrdc.net is necessary for Analytics Data Insertion API to function properly - content={`default-src 'self'; + content={`default-src 'self' blob:; script-src 'self' 'nonce-${process.env.REACT_APP_NONCE}' cdn.jsdelivr.net assets.adobedtm.com cdn.tt.omtrdc.net; style-src 'self' 'unsafe-inline'; img-src * data:; diff --git a/sandbox/src/components/InAppMessagesDemo/InAppMessages.js b/sandbox/src/components/InAppMessagesDemo/InAppMessages.js new file mode 100644 index 000000000..74db8c0d6 --- /dev/null +++ b/sandbox/src/components/InAppMessagesDemo/InAppMessages.js @@ -0,0 +1,74 @@ +import React, { useState, useEffect } from "react"; +import ContentSecurityPolicy from "../ContentSecurityPolicy"; +import "./InAppMessagesStyle.css"; + +export default function DecisionEngine() { + const [realResponse, setRealResponse] = useState(null); + async function getInAppPayload(payload) { + const res = await fetch( + `https://edge.adobedc.net/ee/or2/v1/interact?configId=7a19c434-6648-48d3-948f-ba0258505d98&requestId=520353b2-dc0d-428c-9e0d-138fc6cbec4e`, + { + method: "POST", + body: JSON.stringify(payload) + } + ); + return await res.json(); + } + + useEffect(() => { + async function fetchInAppPayload() { + console.log("fetching in app payload"); + const response = await getInAppPayload({ + events: [ + { + query: { + personalization: { + surfaces: ["mobileapp://com.adobe.iamTutorialiOS"] + } + }, + xdm: { + timestamp: new Date().toISOString(), + implementationDetails: { + name: "https://ns.adobe.com/experience/mobilesdk/ios", + version: "3.7.4+1.5.0", + environment: "app" + } + } + } + ] + }); + setRealResponse(response); + } + fetchInAppPayload(); + }, []); + + const renderDecisions = e => { + e.stopPropagation(); + e.preventDefault(); + window.alloy("evaluateRulesets", { + "~type": "com.adobe.eventType.generic.track", + "~source": "com.adobe.eventSource.requestContent", + state: "", + "~state.com.adobe.module.lifecycle/lifecyclecontextdata.dayofweek": 1 + }); + + window.alloy("applyResponse", { + renderDecisions: true, + decisionContext: { + "~type": "com.adobe.eventType.generic.track", + "~source": "com.adobe.eventSource.requestContent", + state: "", + "~state.com.adobe.module.lifecycle/lifecyclecontextdata.dayofweek": 1 + }, + responseBody: realResponse + }); + }; + + return ( +
    + +

    In App Messages For Web

    + +
    + ); +} diff --git a/sandbox/src/components/InAppMessagesDemo/InAppMessagesStyle.css b/sandbox/src/components/InAppMessagesDemo/InAppMessagesStyle.css new file mode 100644 index 000000000..87e55e3cc --- /dev/null +++ b/sandbox/src/components/InAppMessagesDemo/InAppMessagesStyle.css @@ -0,0 +1,7 @@ +.custom-heading { + color: red; + font-size: 25px; +} +.element-spacing { + margin-right: 10px; +} \ No newline at end of file diff --git a/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js b/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js index a4112bb75..d0ce5b4cc 100644 --- a/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js +++ b/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js @@ -11,12 +11,21 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import { removeElements } from "../utils"; +import { getNonce } from "../../dom-actions/dom"; +import { INTERACT } from "../../constants/eventType"; +import { removeElementById } from "../utils"; const ELEMENT_TAG_CLASSNAME = "alloy-messaging-container"; const ELEMENT_TAG_ID = "alloy-messaging-container"; const ANCHOR_HREF_REGEX = /adbinapp:\/\/(\w+)\?interaction=(\w+)/i; +const OVERLAY_TAG_CLASSNAME = "alloy-overlay-container"; +const OVERLAY_TAG_ID = "alloy-overlay-container"; +const ALLOY_IFRAME_ID = "alloy-iframe-id"; + +const dismissMessage = () => + [ELEMENT_TAG_ID, OVERLAY_TAG_ID].forEach(removeElementById); + export const buildStyleFromParameters = (mobileParameters, webParameters) => { const { verticalAlign, @@ -27,29 +36,57 @@ export const buildStyleFromParameters = (mobileParameters, webParameters) => { cornerRadius, horizontalInset, verticalInset, - uiTakeOver + uiTakeover } = mobileParameters; - return { - verticalAlign: verticalAlign === "center" ? "middle" : verticalAlign, - top: verticalAlign === "top" ? "0px" : "auto", + const style = { width: width ? `${width}%` : "100%", - horizontalAlign: horizontalAlign === "center" ? "middle" : horizontalAlign, backgroundColor: backdropColor || "rgba(0, 0, 0, 0.5)", - height: height ? `${height}vh` : "100%", borderRadius: cornerRadius ? `${cornerRadius}px` : "0px", border: "none", - marginLeft: horizontalInset ? `${horizontalInset}px` : "0px", - marginRight: horizontalInset ? `${horizontalInset}px` : "0px", - marginTop: verticalInset ? `${verticalInset}px` : "0px", - marginBottom: verticalInset ? `${verticalInset}px` : "0px", - zIndex: uiTakeOver ? "9999" : "0", - position: uiTakeOver ? "fixed" : "relative", + position: uiTakeover ? "fixed" : "relative", overflow: "hidden" }; + if (horizontalAlign === "left") { + style.left = horizontalInset ? `${horizontalInset}%` : "0"; + } else if (horizontalAlign === "right") { + style.right = horizontalInset ? `${horizontalInset}%` : "0"; + } else if (horizontalAlign === "center") { + style.left = "50%"; + style.transform = "translateX(-50%)"; + } + + if (verticalAlign === "top") { + style.top = verticalInset ? `${verticalInset}%` : "0"; + } else if (verticalAlign === "bottom") { + style.position = "fixed"; + style.bottom = verticalInset ? `${verticalInset}%` : "0"; + } else if (verticalAlign === "center") { + style.top = "50%"; + style.transform = `${ + horizontalAlign === "center" ? `${style.transform} ` : "" + }translateY(-50%)`; + style.display = "flex"; + style.alignItems = "center"; + style.justifyContent = "center"; + } + + if (height) { + style.height = `${height}vh`; + } else { + style.height = "100%"; + } + return style; +}; +export const setWindowLocationHref = link => { + window.location.assign(link); }; -const createIframeClickHandler = (container, collect) => { +export const createIframeClickHandler = ( + container, + collect, + mobileParameters +) => { return event => { event.preventDefault(); event.stopImmediatePropagation(); @@ -60,41 +97,52 @@ const createIframeClickHandler = (container, collect) => { target.tagName.toLowerCase() === "a" ? target : target.closest("a"); if (anchor) { - if (ANCHOR_HREF_REGEX.test(anchor.href)) { - const matches = ANCHOR_HREF_REGEX.exec(anchor.href); - - const action = matches.length >= 2 ? matches[1] : ""; - const interaction = matches.length >= 3 ? matches[2] : ""; - - if (interaction === "clicked") { - const uuid = anchor.getAttribute("data-uuid"); - // eslint-disable-next-line no-console - console.log(`clicked ${uuid}`); - // TODO: collect analytics - // collect({ - // eventType: INTERACT - // }); - } - - if (action === "dismiss") { - container.remove(); + const parts = anchor.href.split("?"); + const actionPart = parts[0].split("://")[1]; + let action = ""; + let interaction = ""; + let link = ""; + if (parts.length > 1) { + const queryParams = new URLSearchParams(parts[1]); + action = actionPart; + interaction = queryParams.get("interaction") || ""; + link = queryParams.get("link") || ""; + const uuid = anchor.getAttribute("data-uuid") || ""; + // eslint-disable-next-line no-console + console.log(`clicked ${uuid}`); + // TODO: collect analytics + // collect({ + // eventType: INTERACT, + // eventSource: "inAppMessage", + // eventData: { + // action, + // interaction + // } + // }); + if (link && interaction === "clicked") { + link = decodeURIComponent(link); + setWindowLocationHref(link); + } else if (action === "dismiss") { + dismissMessage(); } - } else { - window.location.href = anchor.href; } } }; }; -const createIframe = (htmlContent, clickHandler) => { +export const createIframe = (htmlContent, clickHandler) => { const parser = new DOMParser(); const htmlDocument = parser.parseFromString(htmlContent, "text/html"); + const scriptTag = htmlDocument.querySelector("script"); + if (scriptTag) { + scriptTag.setAttribute("nonce", getNonce()); + } const element = document.createElement("iframe"); element.src = URL.createObjectURL( new Blob([htmlDocument.documentElement.outerHTML], { type: "text/html" }) ); - // element.sandbox = "allow-same-origin allow-scripts"; + element.id = ALLOY_IFRAME_ID; Object.assign(element.style, { border: "none", @@ -111,24 +159,11 @@ const createIframe = (htmlContent, clickHandler) => { return element; }; -const createContainerElement = settings => { +export const createContainerElement = settings => { const { mobileParameters = {}, webParameters = {} } = settings; - const element = document.createElement("div"); element.id = ELEMENT_TAG_ID; element.className = `${ELEMENT_TAG_CLASSNAME}`; - - Object.assign(element.style, { - position: "absolute", - top: "50%", - left: "50%", - transform: "translate(-50%, -50%)", - "background-color": "white", - padding: "0px", - border: "1px solid black", - "box-shadow": "10px 10px 5px #888888" - }); - Object.assign( element.style, buildStyleFromParameters(mobileParameters, webParameters) @@ -137,10 +172,30 @@ const createContainerElement = settings => { return element; }; -const displayHTMLContentInIframe = (settings, collect) => { - removeElements(ELEMENT_TAG_CLASSNAME); +export const createOverlayElement = parameter => { + const element = document.createElement("div"); + const backdropOpacity = parameter.backdropOpacity || 0.5; + const backdropColor = parameter.backdropColor || "#FFFFFF"; + element.id = OVERLAY_TAG_ID; + element.className = `${OVERLAY_TAG_CLASSNAME}`; + + Object.assign(element.style, { + position: "fixed", + top: "0", + left: "0", + width: "100%", + height: "100%", + background: "transparent", + opacity: backdropOpacity, + backgroundColor: backdropColor + }); + + return element; +}; - const { content, contentType } = settings; +export const displayHTMLContentInIframe = (settings, collect) => { + dismissMessage(); + const { content, contentType, mobileParameters } = settings; if (contentType !== "text/html") { // TODO: whoops, no can do. @@ -150,12 +205,17 @@ const displayHTMLContentInIframe = (settings, collect) => { const iframe = createIframe( content, - createIframeClickHandler(container, collect) + createIframeClickHandler(container, collect, mobileParameters) ); container.appendChild(iframe); - document.body.append(container); + if (mobileParameters.uiTakeover) { + const overlay = createOverlayElement(mobileParameters); + document.body.appendChild(overlay); + document.body.style.overflow = "hidden"; + } + document.body.appendChild(container); }; export default (settings, collect) => { diff --git a/src/components/Personalization/in-app-message-actions/utils.js b/src/components/Personalization/in-app-message-actions/utils.js index 336746076..b2e7d51ed 100644 --- a/src/components/Personalization/in-app-message-actions/utils.js +++ b/src/components/Personalization/in-app-message-actions/utils.js @@ -30,3 +30,10 @@ export const removeElements = cssClassName => { element.remove(); }); }; + +export const removeElementById = id => { + const element = document.getElementById(id); + if (element) { + element.remove(); + } +}; diff --git a/test/unit/specs/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.spec.js b/test/unit/specs/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.spec.js index c0a221997..848e14ee5 100644 --- a/test/unit/specs/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.spec.js +++ b/test/unit/specs/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.spec.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 inAppMessageConsequenceAdapter from "../../../../../../src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter"; describe("DecisioningEngine:inAppMessageConsequenceAdapter", () => { diff --git a/test/unit/specs/components/DecisioningEngine/createConsequenceAdapter.spec.js b/test/unit/specs/components/DecisioningEngine/createConsequenceAdapter.spec.js index af4252c53..d0294d9da 100644 --- a/test/unit/specs/components/DecisioningEngine/createConsequenceAdapter.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createConsequenceAdapter.spec.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 createConsequenceAdapter from "../../../../../src/components/DecisioningEngine/createConsequenceAdapter"; describe("DecisioningEngine:createConsequenceAdapter", () => { diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js index 72575e9dc..1b6e31151 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js @@ -34,7 +34,7 @@ describe("Personalization::IAM:banner", () => { backdropOpacity: 0.2, cornerRadius: 15, horizontalInset: 0, - uiTakeover: true, + uiTakeover: false, horizontalAlign: "center", width: 80, displayAnimation: "top", @@ -45,14 +45,18 @@ describe("Personalization::IAM:banner", () => { contentType: "text/html" }); - const container = document.querySelector("div#alloy-messaging-container"); - - expect(container).not.toBeNull(); + const overlayContainer = document.querySelector( + "div#alloy-overlay-container" + ); + const messagingContainer = document.querySelector( + "div#alloy-messaging-container" + ); - expect(container.parentNode).toEqual(document.body); + expect(overlayContainer).toBeNull(); + expect(messagingContainer).not.toBeNull(); - expect(container.previousElementSibling).toEqual(something); - expect(container.nextElementSibling).toBeNull(); + expect(messagingContainer.parentNode).toEqual(document.body); + expect(messagingContainer.nextElementSibling).toBeNull(); const iframe = document.querySelector( ".alloy-messaging-container > iframe" diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayCustom.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayCustom.spec.js index e69de29bb..26ef92385 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayCustom.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayCustom.spec.js @@ -0,0 +1,11 @@ +/* +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. +*/ diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayFullScreen.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayFullScreen.spec.js index e69de29bb..26ef92385 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayFullScreen.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayFullScreen.spec.js @@ -0,0 +1,11 @@ +/* +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. +*/ diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js index e69de29bb..8ab37e7ed 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js @@ -0,0 +1,342 @@ +/* +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 { + buildStyleFromParameters, + createOverlayElement, + createIframe, + createIframeClickHandler, + displayHTMLContentInIframe +} from "../../../../../../../src/components/Personalization/in-app-message-actions/actions/displayIframeContent"; +import cleanUpDomChanges from "../../../../../helpers/cleanUpDomChanges"; +import { getNonce } from "../../../../../../../src/components/Personalization/dom-actions/dom"; +import { testResetCachedNonce } from "../../../../../../../src/components/Personalization/dom-actions/dom/getNonce"; + +describe("DOM Actions on Iframe", () => { + beforeEach(() => { + cleanUpDomChanges("alloy-messaging-container"); + cleanUpDomChanges("alloy-overlay-container"); + cleanUpDomChanges("alloy-iframe-id"); + }); + + afterEach(() => { + cleanUpDomChanges("alloy-messaging-container"); + cleanUpDomChanges("alloy-overlay-container"); + cleanUpDomChanges("alloy-iframe-id"); + }); + describe("buildStyleFromParameters", () => { + it("should build the style object correctly", () => { + const mobileParameters = { + verticalAlign: "center", + width: 80, + horizontalAlign: "left", + backdropColor: "rgba(0, 0, 0, 0.7)", + height: 60, + cornerRadius: 10, + horizontalInset: 5, + verticalInset: 10, + uiTakeover: true + }; + + const webParameters = {}; + + const style = buildStyleFromParameters(mobileParameters, webParameters); + + expect(style.width).toBe("80%"); + expect(style.backgroundColor).toBe("rgba(0, 0, 0, 0.7)"); + expect(style.borderRadius).toBe("10px"); + expect(style.border).toBe("none"); + expect(style.position).toBe("fixed"); + expect(style.overflow).toBe("hidden"); + expect(style.left).toBe("5%"); + expect(style.height).toBe("60vh"); + }); + }); + + describe("createOverlayElement", () => { + it("should create overlay element with correct styles", () => { + const parameter = { + backdropOpacity: 0.8, + backdropColor: "#000000" + }; + + const overlayElement = createOverlayElement(parameter); + + expect(overlayElement.id).toBe("alloy-overlay-container"); + expect(overlayElement.style.position).toBe("fixed"); + expect(overlayElement.style.top).toBe("0px"); + expect(overlayElement.style.left).toBe("0px"); + expect(overlayElement.style.width).toBe("100%"); + expect(overlayElement.style.height).toBe("100%"); + expect(overlayElement.style.background).toBe("rgb(0, 0, 0)"); + expect(overlayElement.style.opacity).toBe("0.8"); + expect(overlayElement.style.backgroundColor).toBe("rgb(0, 0, 0)"); + }); + }); + describe("createIframe function", () => { + it("should create an iframe element with specified properties", () => { + const mockHtmlContent = + '\u003c!doctype html\u003e\\n\u003chtml\u003e\\n\u003chead\u003e\\n \u003ctitle\u003eBumper Sale!\u003c/title\u003e\\n \u003cstyle\u003e\\n body {\\n margin: 0;\\n padding: 0;\\n font-family: Arial, sans-serif;\\n }\\n\\n #announcement {\\n position: fixed;\\n top: 0;\\n left: 0;\\n width: 100%;\\n height: 100%;\\n background-color: rgba(0, 0, 0, 0.8);\\n display: flex;\\n flex-direction: column;\\n align-items: center;\\n justify-content: center;\\n color: #fff;\\n }\\n\\n #announcement img {\\n max-width: 80%;\\n height: auto;\\n margin-bottom: 20px;\\n }\\n\\n #cross {\\n position: absolute;\\n top: 10px;\\n right: 10px;\\n cursor: pointer;\\n font-size: 24px;\\n color: #fff;\\n }\\n\\n #buttons {\\n display: flex;\\n justify-content: center;\\n margin-top: 20px;\\n }\\n\\n #buttons a {\\n margin: 0 10px;\\n padding: 10px 20px;\\n background-color: #ff5500;\\n color: #fff;\\n text-decoration: none;\\n border-radius: 4px;\\n font-weight: bold;\\n transition: background-color 0.3s ease;\\n }\\n\\n #buttons a:hover {\\n background-color: #ff3300;\\n }\\n \u003c/style\u003e\\n\u003c/head\u003e\\n\u003cbody\u003e\\n\u003cdiv id\u003d"announcement" class\u003d"fullscreen"\u003e\\n \u003cspan id\u003d"cross" class\u003d"dismiss"\u003e✕\u003c/span\u003e\\n \u003ch2\u003eBlack Friday Sale!\u003c/h2\u003e\\n \u003cimg src\u003d"https://source.unsplash.com/800x600/?technology,gadget" alt\u003d"Technology Image"\u003e\\n \u003cp\u003eDon\u0027t miss out on our incredible discounts and deals at our gadgets!\u003c/p\u003e\\n \u003cdiv id\u003d"buttons"\u003e\\n \u003ca class\u003d"forward" href\u003d"http://localhost:3000/"\u003eShop\u003c/a\u003e\\n \u003ca class\u003d"dismiss"\u003eDismiss\u003c/a\u003e\\n \u003c/div\u003e\\n\u003c/div\u003e\\n\\n\u003c/body\u003e\u003c/html\u003e\\n'; + const mockClickHandler = jasmine.createSpy("clickHandler"); + + const iframe = createIframe(mockHtmlContent, mockClickHandler); + + expect(iframe).toBeDefined(); + expect(iframe instanceof HTMLIFrameElement).toBe(true); + expect(iframe.src).toContain("blob:"); + expect(iframe.style.border).toBe("none"); + expect(iframe.style.width).toBe("100%"); + expect(iframe.style.height).toBe("100%"); + }); + + it("should set 'nonce' attribute on script tag if it exists", async () => { + const mockHtmlContentWithScript = + "\n" + + "\n" + + "\n" + + " Bumper Sale!\n" + + " \n" + + "\n" + + "\n" + + '
    \n' + + ' \n' + + "

    Black Friday Sale!

    \n" + + ' Technology Image\n' + + "

    Don't miss out on our incredible discounts and deals at our gadgets!

    \n" + + '
    \n' + + ' Shop\n' + + ' Dismiss\n' + + "
    \n" + + "
    \n" + + "\n" + + "\n" + + "\n"; + + testResetCachedNonce(); + + const childElement = document.createElement("div"); + childElement.setAttribute("nonce", "12345"); + const parentElement = document.createElement("div"); + parentElement.appendChild(childElement); + const originalGetNonce = getNonce(parentElement); + + const mockClickHandler = jasmine.createSpy("clickHandler"); + const iframe = createIframe(mockHtmlContentWithScript, mockClickHandler); + + const blob = await fetch(iframe.src).then(r => r.blob()); + const text = await blob.text(); + const parser = new DOMParser(); + const iframeDocument = parser.parseFromString(text, "text/html"); + + const scriptTag = iframeDocument.querySelector("script"); + expect(scriptTag).toBeDefined(); + expect(scriptTag.getAttribute("nonce")).toEqual(originalGetNonce); + }); + }); + + describe("createIframeClickHandler", () => { + let container; + let mockedCollect; + let mobileParameters; + + beforeEach(() => { + container = document.createElement("div"); + container.setAttribute("id", "alloy-messaging-container"); + document.body.appendChild(container); + + mockedCollect = jasmine.createSpy("collect"); + + mobileParameters = { + verticalAlign: "center", + width: 80, + horizontalAlign: "left", + backdropColor: "rgba(0, 0, 0, 0.7)", + height: 60, + cornerRadius: 10, + horizontalInset: 5, + verticalInset: 10 + }; + }); + + it("should remove display message when dismiss is clicked and UI takeover is false", () => { + Object.assign(mobileParameters, { + uiTakeover: false + }); + + const anchor = document.createElement("a"); + Object.assign(anchor, { + "data-uuid": "12345", + href: "adbinapp://dismiss?interaction=cancel" + }); + + const mockEvent = { + target: anchor, + preventDefault: () => {}, + stopImmediatePropagation: () => {} + }; + const iframeClickHandler = createIframeClickHandler( + container, + mockedCollect, + mobileParameters + ); + iframeClickHandler(mockEvent); + const alloyMessagingContainer = document.getElementById( + "alloy-messaging-container" + ); + expect(alloyMessagingContainer).toBeNull(); + }); + + it("should remove display message when dismiss is clicked and Ui takeover is true", () => { + Object.assign(mobileParameters, { + uiTakeover: true + }); + + const overlayContainer = document.createElement("div"); + overlayContainer.setAttribute("id", "alloy-overlay-container"); + + document.body.appendChild(overlayContainer); + + const anchor = document.createElement("a"); + Object.assign(anchor, { + "data-uuid": "12345", + href: "adbinapp://dismiss?interaction=cancel" + }); + const mockEvent = { + target: anchor, + preventDefault: () => {}, + stopImmediatePropagation: () => {} + }; + const iframeClickHandler = createIframeClickHandler( + container, + mockedCollect, + mobileParameters + ); + iframeClickHandler(mockEvent); + const overlayContainerAfterDismissal = document.getElementById( + "alloy-overlay-container" + ); + expect(overlayContainerAfterDismissal).toBeNull(); + }); + }); + describe("displayHTMLContentInIframe", () => { + let originalAppendChild; + let originalBodyStyle; + let mockCollect; + let originalCreateContainerElement; + let originalCreateIframe; + let originalCreateOverlayElement; + + beforeEach(() => { + mockCollect = jasmine.createSpy("collect"); + originalAppendChild = document.body.appendChild; + document.body.appendChild = jasmine.createSpy("appendChild"); + originalBodyStyle = document.body.style; + document.body.style = {}; + + originalCreateContainerElement = window.createContainerElement; + window.createContainerElement = jasmine + .createSpy("createContainerElement") + .and.callFake(() => { + const element = document.createElement("div"); + element.id = "alloy-messaging-container"; + return element; + }); + + originalCreateIframe = window.createIframe; + window.createIframe = jasmine + .createSpy("createIframe") + .and.callFake(() => { + const element = document.createElement("iframe"); + element.id = "alloy-iframe-id"; + return element; + }); + + originalCreateOverlayElement = window.createOverlayElement; + window.createOverlayElement = jasmine + .createSpy("createOverlayElement") + .and.callFake(() => { + const element = document.createElement("div"); + element.id = "alloy-overlay-container"; + return element; + }); + }); + + afterEach(() => { + document.body.appendChild = originalAppendChild; + document.body.style = originalBodyStyle; + document.body.innerHTML = ""; + window.createContainerElement = originalCreateContainerElement; + window.createOverlayElement = originalCreateOverlayElement; + window.createIframe = originalCreateIframe; + }); + + it("should display HTML content in iframe with overlay", () => { + const settings = { + type: "custom", + mobileParameters: { + verticalAlign: "center", + dismissAnimation: "bottom", + verticalInset: 20, + backdropOpacity: 0.78, + cornerRadius: 20, + gestures: {}, + horizontalInset: -14, + uiTakeover: true, + horizontalAlign: "center", + width: 72, + displayAnimation: "bottom", + backdropColor: "#4CA206", + height: 63 + }, + webParameters: { + info: "this is a placeholder" + }, + content: + '\n\n\n Bumper Sale!\n \n\n\n
    \n \n

    Black Friday Sale!

    \n Technology Image\n

    Don\'t miss out on our incredible discounts and deals at our gadgets!

    \n
    \n Shop\n Dismiss\n
    \n
    \n\n\n\n', + contentType: "text/html", + schema: "https://ns.adobe.com/personalization/in-app-message", + meta: { + id: "9441e3c4-d673-4c1b-8fb9-d1c0f7826dcc", + scope: "mobileapp://com.adobe.iamTutorialiOS", + scopeDetails: { + decisionProvider: "AJO", + correlationID: "8794bfb9-3254-478a-860e-04f9da59ad82", + characteristics: { + eventToken: + "eyJtZXNzYWdlRXhlY3V0aW9uIjp7Im1lc3NhZ2VFeGVjdXRpb25JRCI6Ik5BIiwibWVzc2FnZUlEIjoiODc5NGJmYjktMzI1NC00NzhhLTg2MGUtMDRmOWRhNTlhZDgyIiwibWVzc2FnZVB1YmxpY2F0aW9uSUQiOiI1ZmYzZmM5Zi0zZTY2LTRiNzktODRmMS1kNzUzMGYwOWQ1ZTIiLCJtZXNzYWdlVHlwZSI6Im1hcmtldGluZyIsImNhbXBhaWduSUQiOiIyOGJlYTAxMS1lNTk2LTQ0MjktYjhmNy1iNWJkNjMwYzY3NDMiLCJjYW1wYWlnblZlcnNpb25JRCI6ImQ5OTQzODJhLTJjZDAtNDkwYS04NGM4LWM0NTk2NmMwYjYwZiIsImNhbXBhaWduQWN0aW9uSUQiOiJiNDU0OThjYi05NmQxLTQxN2EtODFlYi0yZjA5MTU3YWQ4YzYifSwibWVzc2FnZVByb2ZpbGUiOnsibWVzc2FnZVByb2ZpbGVJRCI6IjQ2MTg5Yjg1LWEwYTYtNDc4NS1hNmJlLTg4OWRiZjU3NjhiOSIsImNoYW5uZWwiOnsiX2lkIjoiaHR0cHM6Ly9ucy5hZG9iZS5jb20veGRtL2NoYW5uZWxzL2luQXBwIiwiX3R5cGUiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbC10eXBlcy9pbkFwcCJ9fX0=" + }, + activity: { + id: + "28bea011-e596-4429-b8f7-b5bd630c6743#b45498cb-96d1-417a-81eb-2f09157ad8c6" + } + } + } + }; + + displayHTMLContentInIframe(settings, mockCollect); + + expect(document.body.appendChild).toHaveBeenCalledTimes(2); + expect(document.body.style.overflow).toBe("hidden"); + }); + }); +}); diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js index be60a2709..5bc1179b2 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js @@ -44,15 +44,15 @@ describe("Personalization::IAM:modal", () => { content: `
    modal
    Alf Says`, contentType: "text/html" }); + document.querySelector("div#alloy-overlay-container"); + const messagingContainer = document.querySelector( + "div#alloy-messaging-container" + ); - const container = document.querySelector("div#alloy-messaging-container"); - - expect(container).not.toBeNull(); - - expect(container.parentNode).toEqual(document.body); + expect(messagingContainer).not.toBeNull(); - expect(container.previousElementSibling).toEqual(something); - expect(container.nextElementSibling).toBeNull(); + expect(messagingContainer.parentNode).toEqual(document.body); + expect(messagingContainer.nextElementSibling).toBeNull(); const iframe = document.querySelector( ".alloy-messaging-container > iframe" diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/utils.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/utils.spec.js index 79a78af56..28e375686 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/utils.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/utils.spec.js @@ -9,4 +9,32 @@ 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. */ -// TODO + +import { removeElementById } from "../../../../../../src/components/Personalization/in-app-message-actions/utils"; + +describe("removeElementById", () => { + beforeEach(() => { + document.body.innerHTML = ` +
    +`; + }); + + it("should remove an element when it exists", () => { + const elementId = "test-element"; + const element = document.getElementById(elementId); + + expect(element).toBeTruthy(); + + removeElementById(elementId); + + expect(document.getElementById(elementId)).toBeNull(); + }); + + it("should do nothing when the element does not exist", () => { + const nonExistentId = "non-existent-element"; + + removeElementById(nonExistentId); + + expect(document.getElementById(nonExistentId)).toBeNull(); + }); +}); From 3fe4b6548f146eb7f49e88b3d0aca4514a9ffaaa Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Tue, 29 Aug 2023 11:00:03 -0600 Subject: [PATCH 36/66] rename qualified event to decisioning.propositionQualified --- .../createDecisionHistory.js | 2 +- .../createEvaluableRulesetPayload.js | 6 +- .../DecisioningEngine/contextTestUtils.js | 6 +- .../createDecisionProvider.spec.js | 64 +++++++++---------- .../createEvaluableRulesetPayload.spec.js | 24 +++---- .../createOnResponseHandler.spec.js | 24 +++---- 6 files changed, 65 insertions(+), 61 deletions(-) diff --git a/src/components/DecisioningEngine/createDecisionHistory.js b/src/components/DecisioningEngine/createDecisionHistory.js index 4610c7b27..85bb5e4d2 100644 --- a/src/components/DecisioningEngine/createDecisionHistory.js +++ b/src/components/DecisioningEngine/createDecisionHistory.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. */ -const QUALIFIED_EVENT_TYPE = "decisioning.qualifiedItem"; +const QUALIFIED_EVENT_TYPE = "decisioning.propositionQualified"; export default ({ eventRegistry }) => { const recordQualified = item => { diff --git a/src/components/DecisioningEngine/createEvaluableRulesetPayload.js b/src/components/DecisioningEngine/createEvaluableRulesetPayload.js index 8f9f41e2d..6b7aff4cc 100644 --- a/src/components/DecisioningEngine/createEvaluableRulesetPayload.js +++ b/src/components/DecisioningEngine/createEvaluableRulesetPayload.js @@ -73,7 +73,11 @@ export default (payload, eventRegistry, decisionHistory) => { const { firstTimestamp: qualifiedDate } = decisionHistory.recordQualified(item); - return { ...item, qualifiedDate, displayedDate }; + + return { + ...item, + data: { ...item.data, qualifiedDate, displayedDate } + }; }); return { diff --git a/test/unit/specs/components/DecisioningEngine/contextTestUtils.js b/test/unit/specs/components/DecisioningEngine/contextTestUtils.js index 961d5ce1d..a99492b8f 100644 --- a/test/unit/specs/components/DecisioningEngine/contextTestUtils.js +++ b/test/unit/specs/components/DecisioningEngine/contextTestUtils.js @@ -20,10 +20,10 @@ export const proposition = { { schema: "https://ns.adobe.com/personalization/mock-action", data: { - hello: "kitty" + hello: "kitty", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined }, - qualifiedDate: jasmine.any(Number), - displayedDate: undefined, id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" } ], diff --git a/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js b/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js index 18882f150..82c78f28e 100644 --- a/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createDecisionProvider.spec.js @@ -252,11 +252,11 @@ describe("DecisioningEngine:createDecisionProvider", () => { src: "img/demo-marketing-offer1-exp-A.png" }, prehidingSelector: - "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined }, - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", - qualifiedDate: jasmine.any(Number), - displayedDate: undefined + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" }, { schema: "https://ns.adobe.com/personalization/dom-action", @@ -265,11 +265,11 @@ describe("DecisioningEngine:createDecisionProvider", () => { type: "setHtml", content: "Hello Treatment A!", prehidingSelector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f", - qualifiedDate: jasmine.any(Number), - displayedDate: undefined + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" } ], scope: "web://mywebsite.com" @@ -306,11 +306,11 @@ describe("DecisioningEngine:createDecisionProvider", () => { selector: "div#spa #spa-content h3", type: "setHtml", content: "i can haz?", - prehidingSelector: "div#spa #spa-content h3" + prehidingSelector: "div#spa #spa-content h3", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined }, - id: "8a0d7a45-70fb-4845-a093-2133b5744c8d", - qualifiedDate: jasmine.any(Number), - displayedDate: undefined + id: "8a0d7a45-70fb-4845-a093-2133b5744c8d" }, { schema: "https://ns.adobe.com/personalization/dom-action", @@ -318,11 +318,11 @@ describe("DecisioningEngine:createDecisionProvider", () => { selector: "div#spa #spa-content p", type: "setHtml", content: "ALL YOUR BASE ARE BELONG TO US", - prehidingSelector: "div#spa #spa-content p" + prehidingSelector: "div#spa #spa-content p", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined }, - id: "a44af51a-e073-4e8c-92e1-84ac28210043", - qualifiedDate: jasmine.any(Number), - displayedDate: undefined + id: "a44af51a-e073-4e8c-92e1-84ac28210043" } ], scope: "web://mywebsite.com" @@ -366,11 +366,11 @@ describe("DecisioningEngine:createDecisionProvider", () => { src: "img/demo-marketing-offer1-exp-A.png" }, prehidingSelector: - "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined }, - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", - qualifiedDate: jasmine.any(Number), - displayedDate: undefined + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" }, { schema: "https://ns.adobe.com/personalization/dom-action", @@ -379,11 +379,11 @@ describe("DecisioningEngine:createDecisionProvider", () => { type: "setHtml", content: "Hello Treatment A!", prehidingSelector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f", - qualifiedDate: jasmine.any(Number), - displayedDate: undefined + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" } ], scope: "web://mywebsite.com" @@ -414,11 +414,11 @@ describe("DecisioningEngine:createDecisionProvider", () => { selector: "div#spa #spa-content h3", type: "setHtml", content: "i can haz?", - prehidingSelector: "div#spa #spa-content h3" + prehidingSelector: "div#spa #spa-content h3", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined }, - id: "8a0d7a45-70fb-4845-a093-2133b5744c8d", - qualifiedDate: jasmine.any(Number), - displayedDate: undefined + id: "8a0d7a45-70fb-4845-a093-2133b5744c8d" }, { schema: "https://ns.adobe.com/personalization/dom-action", @@ -426,11 +426,11 @@ describe("DecisioningEngine:createDecisionProvider", () => { selector: "div#spa #spa-content p", type: "setHtml", content: "ALL YOUR BASE ARE BELONG TO US", - prehidingSelector: "div#spa #spa-content p" + prehidingSelector: "div#spa #spa-content p", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined }, - id: "a44af51a-e073-4e8c-92e1-84ac28210043", - qualifiedDate: jasmine.any(Number), - displayedDate: undefined + id: "a44af51a-e073-4e8c-92e1-84ac28210043" } ], scope: "web://mywebsite.com" diff --git a/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js b/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js index f53d3927b..c24130324 100644 --- a/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createEvaluableRulesetPayload.spec.js @@ -165,10 +165,10 @@ describe("DecisioningEngine:createEvaluableRulesetPayload", () => { src: "img/demo-marketing-offer1-exp-A.png" }, prehidingSelector: - "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined }, - qualifiedDate: jasmine.any(Number), - displayedDate: undefined, id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" }, { @@ -178,10 +178,10 @@ describe("DecisioningEngine:createEvaluableRulesetPayload", () => { type: "setHtml", content: "Hello Treatment A!", prehidingSelector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined }, - qualifiedDate: jasmine.any(Number), - displayedDate: undefined, id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" } ], @@ -332,10 +332,10 @@ describe("DecisioningEngine:createEvaluableRulesetPayload", () => { src: "img/demo-marketing-offer1-exp-A.png" }, prehidingSelector: - "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined }, - qualifiedDate: jasmine.any(Number), - displayedDate: undefined, id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" }, { @@ -345,10 +345,10 @@ describe("DecisioningEngine:createEvaluableRulesetPayload", () => { type: "setHtml", content: "Hello Treatment A!", prehidingSelector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined }, - qualifiedDate: jasmine.any(Number), - displayedDate: undefined, id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" } ], diff --git a/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js b/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js index 746336420..5cb886fa1 100644 --- a/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js @@ -184,11 +184,11 @@ describe("DecisioningEngine:createOnResponseHandler", () => { src: "img/demo-marketing-offer1-exp-A.png" }, prehidingSelector: - "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined }, - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", - qualifiedDate: jasmine.any(Number), - displayedDate: undefined + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" }, { schema: "https://ns.adobe.com/personalization/dom-action", @@ -198,11 +198,11 @@ describe("DecisioningEngine:createOnResponseHandler", () => { type: "setHtml", content: "Hello Treatment A!", prehidingSelector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f", - qualifiedDate: jasmine.any(Number), - displayedDate: undefined + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" } ], scope: "web://target.jasonwaters.dev/aep.html" @@ -347,11 +347,11 @@ describe("DecisioningEngine:createOnResponseHandler", () => { type: "setHtml", content: "Hello Treatment A!", prehidingSelector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f", - qualifiedDate: jasmine.any(Number), - displayedDate: undefined + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" } ], scope: "web://target.jasonwaters.dev/aep.html" From e852f18443ad916c2983a74944657422a9a80b97 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Tue, 29 Aug 2023 12:07:08 -0600 Subject: [PATCH 37/66] fix test --- .../createSubscribeMessageFeed.js | 4 +- .../createSubscribeMessageFeed.spec.js | 48 +++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/components/Personalization/createSubscribeMessageFeed.js b/src/components/Personalization/createSubscribeMessageFeed.js index d89915dc9..26b5e302b 100644 --- a/src/components/Personalization/createSubscribeMessageFeed.js +++ b/src/components/Personalization/createSubscribeMessageFeed.js @@ -26,8 +26,8 @@ export default ({ collect }) => { const createFeedItem = (payload, item) => { const { id, scope, scopeDetails } = payload; - const { data = {}, qualifiedDate, displayedDate } = item; - const { content = {}, publishedDate } = data; + const { data = {} } = item; + const { content = {}, publishedDate, qualifiedDate, displayedDate } = data; return { ...content, diff --git a/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js b/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js index df3e1f40c..07fb9a7e1 100644 --- a/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js +++ b/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js @@ -18,11 +18,11 @@ describe("Personalization:subscribeMessageFeed", () => { src: "img/demo-marketing-offer1-exp-A.png" }, prehidingSelector: - "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)", + qualifiedDate: 1683042673387, + displayedDate: 1683042673395 }, - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", - qualifiedDate: 1683042673387, - displayedDate: 1683042673395 + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" }, { schema: "https://ns.adobe.com/personalization/dom-action", @@ -31,11 +31,11 @@ describe("Personalization:subscribeMessageFeed", () => { type: "setHtml", content: "Hello Treatment A!", prehidingSelector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)" + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + qualifiedDate: 1683042673387, + displayedDate: 1683042673395 }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f", - qualifiedDate: 1683042673387, - displayedDate: 1683042673395 + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" } ], scope: "web://mywebsite.com/feed" @@ -60,11 +60,11 @@ describe("Personalization:subscribeMessageFeed", () => { body: "a handshake is available upon request.", title: "Welcome to Lumon!" }, - contentType: "application/json" + contentType: "application/json", + qualifiedDate: 1683042628064, + displayedDate: 1683042628070 }, - id: "a48ca420-faea-467e-989a-5d179d9f562d", - qualifiedDate: 1683042628064, - displayedDate: 1683042628070 + id: "a48ca420-faea-467e-989a-5d179d9f562d" }, { schema: MESSAGE_FEED_ITEM, @@ -84,11 +84,11 @@ describe("Personalization:subscribeMessageFeed", () => { body: "Great job, you completed your profile.", title: "Achievement Unlocked!" }, - contentType: "application/json" + contentType: "application/json", + qualifiedDate: 1683042628064, + displayedDate: 1683042628070 }, - id: "b7173290-588f-40c6-a05c-43ed5ec08b28", - qualifiedDate: 1683042628064, - displayedDate: 1683042628070 + id: "b7173290-588f-40c6-a05c-43ed5ec08b28" } ], scope: "web://mywebsite.com/feed" @@ -113,11 +113,11 @@ describe("Personalization:subscribeMessageFeed", () => { body: "Posting on social media helps us spread the word.", title: "Thanks for sharing!" }, - contentType: "application/json" + contentType: "application/json", + qualifiedDate: 1683042658312, + displayedDate: 1683042658316 }, - id: "cfcb1af7-7bc2-45b2-a86a-0aa93fe69ce7", - qualifiedDate: 1683042658312, - displayedDate: 1683042658316 + id: "cfcb1af7-7bc2-45b2-a86a-0aa93fe69ce7" } ], scope: "web://mywebsite.com/feed", @@ -164,11 +164,11 @@ describe("Personalization:subscribeMessageFeed", () => { body: "Now you're ready to earn!", title: "Funds deposited!" }, - contentType: "application/json" + contentType: "application/json", + qualifiedDate: 1683042653905, + displayedDate: 1683042653909 }, - id: "0263e171-fa32-4c7a-9611-36b28137a81d", - qualifiedDate: 1683042653905, - displayedDate: 1683042653909 + id: "0263e171-fa32-4c7a-9611-36b28137a81d" } ], scope: "web://mywebsite.com/feed" From fcef6acf765f8a4eaf7098d16203b9c1d1f3bd30 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Tue, 29 Aug 2023 16:12:47 -0600 Subject: [PATCH 38/66] subscribeRulesetItems command --- .../createSubscribeRulesetItems.js | 64 +++ src/components/DecisioningEngine/index.js | 9 +- .../createSubscribeMessageFeed.js | 16 +- src/components/Personalization/index.js | 2 +- test/functional/helpers/createAlloyProxy.js | 1 + .../createSubscribeRulesetItems.spec.js | 474 ++++++++++++++++++ .../createSubscribeMessageFeed.spec.js | 4 - 7 files changed, 557 insertions(+), 13 deletions(-) create mode 100644 src/components/DecisioningEngine/createSubscribeRulesetItems.js create mode 100644 test/unit/specs/components/DecisioningEngine/createSubscribeRulesetItems.spec.js diff --git a/src/components/DecisioningEngine/createSubscribeRulesetItems.js b/src/components/DecisioningEngine/createSubscribeRulesetItems.js new file mode 100644 index 000000000..1c9246194 --- /dev/null +++ b/src/components/DecisioningEngine/createSubscribeRulesetItems.js @@ -0,0 +1,64 @@ +import { + arrayOf, + callback as callbackType, + objectOf, + string +} from "../../utils/validation"; + +const validateOptions = ({ options }) => { + const validator = objectOf({ + surface: string().required(), + schemas: arrayOf(string()).uniqueItems(), + callback: callbackType().required() + }).noUnknownFields(); + + return validator(options); +}; + +export default () => { + let subscriptionHandler; + let surfaceIdentifier; + let schemasFilter; + + const run = ({ surface, schemas, callback }) => { + subscriptionHandler = callback; + surfaceIdentifier = surface; + schemasFilter = schemas instanceof Array ? schemas : undefined; + }; + + const optionsValidator = options => validateOptions({ options }); + + const refresh = propositions => { + if (!subscriptionHandler || !surfaceIdentifier) { + return; + } + + const result = propositions + .filter(payload => payload.scope === surfaceIdentifier) + .reduce((allItems, payload) => { + const { items = [] } = payload; + + return [ + ...allItems, + ...items.filter(item => + schemasFilter ? schemasFilter.includes(item.schema) : true + ) + ]; + }, []) + .sort((a, b) => b.data.qualifiedDate - a.data.qualifiedDate); + + if (result.length === 0) { + return; + } + + subscriptionHandler.call(null, { items: result }); + }; + + return { + refresh, + command: { + optionsValidator, + run + } + }; +}; diff --git a/src/components/DecisioningEngine/index.js b/src/components/DecisioningEngine/index.js index b2fdd6399..f408a0fcb 100644 --- a/src/components/DecisioningEngine/index.js +++ b/src/components/DecisioningEngine/index.js @@ -15,6 +15,7 @@ import createDecisionProvider from "./createDecisionProvider"; import createApplyResponse from "./createApplyResponse"; import createEventRegistry from "./createEventRegistry"; import createContextProvider from "./createContextProvider"; +import createSubscribeRulesetItems from "./createSubscribeRulesetItems"; const createDecisioningEngine = ({ config, createNamespacedStorage }) => { const { orgId } = config; @@ -26,8 +27,13 @@ const createDecisioningEngine = ({ config, createNamespacedStorage }) => { const decisionProvider = createDecisionProvider({ eventRegistry }); const contextProvider = createContextProvider({ eventRegistry, window }); + const subscribeRulesetItems = createSubscribeRulesetItems(); + return { lifecycle: { + onDecision({ propositions }) { + subscribeRulesetItems.refresh(propositions); + }, onComponentsRegistered(tools) { applyResponse = createApplyResponse(tools.lifecycle); }, @@ -58,7 +64,8 @@ const createDecisioningEngine = ({ config, createNamespacedStorage }) => { contextProvider.getContext(decisionContext) ) }) - } + }, + subscribeRulesetItems: subscribeRulesetItems.command } }; }; diff --git a/src/components/Personalization/createSubscribeMessageFeed.js b/src/components/Personalization/createSubscribeMessageFeed.js index 26b5e302b..4fce76199 100644 --- a/src/components/Personalization/createSubscribeMessageFeed.js +++ b/src/components/Personalization/createSubscribeMessageFeed.js @@ -1,12 +1,15 @@ -/* eslint-disable */ -import { callback, objectOf, string } from "../../utils/validation"; -import { MESSAGE_FEED_ITEM, MESSAGE_IN_APP } from "./constants/schema"; +import { + callback as callbackType, + objectOf, + string +} from "../../utils/validation"; +import { MESSAGE_FEED_ITEM } from "./constants/schema"; import { DISPLAY, INTERACT } from "./constants/eventType"; -const validateSubscribeMessageFeedOptions = ({ options }) => { +const validateOptions = ({ options }) => { const validator = objectOf({ surface: string().required(), - callback: callback().required() + callback: callbackType().required() }).noUnknownFields(); return validator(options); @@ -20,8 +23,7 @@ export default ({ collect }) => { surfaceIdentifier = surface; }; - const optionsValidator = options => - validateSubscribeMessageFeedOptions({ options }); + const optionsValidator = options => validateOptions({ options }); const createFeedItem = (payload, item) => { const { id, scope, scopeDetails } = payload; diff --git a/src/components/Personalization/index.js b/src/components/Personalization/index.js index 368f727d7..83d0e384d 100644 --- a/src/components/Personalization/index.js +++ b/src/components/Personalization/index.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 { string, boolean, objectOf } from "../../utils/validation"; +import { boolean, objectOf, string } from "../../utils/validation"; import createComponent from "./createComponent"; import createCollect from "./createCollect"; import createExecuteDecisions from "./createExecuteDecisions"; diff --git a/test/functional/helpers/createAlloyProxy.js b/test/functional/helpers/createAlloyProxy.js index d8617a891..dd3a68e94 100644 --- a/test/functional/helpers/createAlloyProxy.js +++ b/test/functional/helpers/createAlloyProxy.js @@ -91,6 +91,7 @@ const commands = [ "getLibraryInfo", "appendIdentityToUrl", "applyPropositions", + "subscribeRulesetItems", "subscribeMessageFeed" ]; diff --git a/test/unit/specs/components/DecisioningEngine/createSubscribeRulesetItems.spec.js b/test/unit/specs/components/DecisioningEngine/createSubscribeRulesetItems.spec.js new file mode 100644 index 000000000..85a5edf15 --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/createSubscribeRulesetItems.spec.js @@ -0,0 +1,474 @@ +import { DOM_ACTION } from "@adobe/alloy/libEs5/components/Personalization/constants/schema"; +import createSubscribeRulesetItems from "../../../../../src/components/DecisioningEngine/createSubscribeRulesetItems"; +import { MESSAGE_FEED_ITEM } from "../../../../../src/components/Personalization/constants/schema"; + +describe("DecisioningEngine:subscribeRulesetItems", () => { + let subscribeRulesetItems; + + const PROPOSITIONS = [ + { + id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", + items: [ + { + schema: DOM_ACTION, + data: { + selector: "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" + }, + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)", + qualifiedDate: 1683042673380, + displayedDate: 1683042673395 + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }, + { + schema: DOM_ACTION, + data: { + selector: "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + qualifiedDate: 1683042673387, + displayedDate: 1683042673395 + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + } + ], + scope: "web://mywebsite.com/feed" + }, + { + id: "1a3d874f-39ee-4310-bfa9-6559a10041a4", + items: [ + { + schema: MESSAGE_FEED_ITEM, + data: { + expiryDate: 1712190456, + publishedDate: 1677752640000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/lumon.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "a handshake is available upon request.", + title: "Welcome to Lumon!" + }, + contentType: "application/json", + qualifiedDate: 1683042628060, + displayedDate: 1683042628070 + }, + id: "a48ca420-faea-467e-989a-5d179d9f562d" + }, + { + schema: MESSAGE_FEED_ITEM, + data: { + expiryDate: 1712190456, + publishedDate: 1677839040000, + + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/achievement.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "Great job, you completed your profile.", + title: "Achievement Unlocked!" + }, + contentType: "application/json", + qualifiedDate: 1683042628064, + displayedDate: 1683042628070 + }, + id: "b7173290-588f-40c6-a05c-43ed5ec08b28" + } + ], + scope: "web://mywebsite.com/feed" + }, + { + id: "1ae11bc5-96dc-41c7-8f71-157c57a5290e", + items: [ + { + schema: MESSAGE_FEED_ITEM, + data: { + expiryDate: 1712190456, + publishedDate: 1678098240000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/twitter.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "Posting on social media helps us spread the word.", + title: "Thanks for sharing!" + }, + contentType: "application/json", + qualifiedDate: 1683042658312, + displayedDate: 1683042658316 + }, + id: "cfcb1af7-7bc2-45b2-a86a-0aa93fe69ce7" + } + ], + scope: "web://mywebsite.com/feed", + scopeDetails: { + id: "1ae11bc5-96dc-41c7-8f71-157c57a5290e", + scope: "web://mywebsite.com/feed", + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: + "eyJtZXNzYWdlRXhlY3V0aW9uIjp7Im1lc3NhZ2VFeGVjdXRpb25JRCI6Ik5BIiwibWVzc2FnZUlEIjoiMDJjNzdlYTgtN2MwZS00ZDMzLTgwOTAtNGE1YmZkM2Q3NTAzIiwibWVzc2FnZVR5cGUiOiJtYXJrZXRpbmciLCJjYW1wYWlnbklEIjoiMzlhZThkNGItYjU1ZS00M2RjLWExNDMtNzdmNTAxOTViNDg3IiwiY2FtcGFpZ25WZXJzaW9uSUQiOiJiZDg1ZDllOC0yMDM3LTQyMmYtYjZkMi0zOTU3YzkwNTU5ZDMiLCJjYW1wYWlnbkFjdGlvbklEIjoiYjQ3ZmRlOGItNTdjMS00YmJlLWFlMjItNjRkNWI3ODJkMTgzIiwibWVzc2FnZVB1YmxpY2F0aW9uSUQiOiJhZTUyY2VkOC0yMDBjLTQ5N2UtODc4Ny1lZjljZmMxNzgyMTUifSwibWVzc2FnZVByb2ZpbGUiOnsiY2hhbm5lbCI6eyJfaWQiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbHMvd2ViIiwiX3R5cGUiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbC10eXBlcy93ZWIifSwibWVzc2FnZVByb2ZpbGVJRCI6ImY1Y2Q5OTk1LTZiNDQtNDIyMS05YWI3LTViNTMzOGQ1ZjE5MyJ9fQ==" + }, + strategies: [ + { + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" + } + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" + }, + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" + } + } + }, + { + id: "d1f7d411-a549-47bc-a4d8-c8e638b0a46b", + items: [ + { + schema: MESSAGE_FEED_ITEM, + data: { + expiryDate: 1712190456, + publishedDate: 1678184640000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/gold-coin.jpg", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "Now you're ready to earn!", + title: "Funds deposited!" + }, + contentType: "application/json", + qualifiedDate: 1683042653905, + displayedDate: 1683042653909 + }, + id: "0263e171-fa32-4c7a-9611-36b28137a81d" + } + ], + scope: "web://mywebsite.com/feed" + } + ]; + + beforeEach(() => { + subscribeRulesetItems = createSubscribeRulesetItems(); + }); + + it("has a command defined", () => { + const { command } = subscribeRulesetItems; + + expect(command).toEqual({ + optionsValidator: jasmine.any(Function), + run: jasmine.any(Function) + }); + }); + + it("calls the callback with list of feed items", () => { + const { command, refresh } = subscribeRulesetItems; + + const callback = jasmine.createSpy(); + + // register a subscription. equivalent to alloy("subscribeRulesetItems", {surface, callback}) + command.run({ + surface: "web://mywebsite.com/feed", + schemas: [MESSAGE_FEED_ITEM], + callback + }); + + refresh(PROPOSITIONS); + expect(callback).toHaveBeenCalledOnceWith({ + items: [ + jasmine.objectContaining({ + schema: MESSAGE_FEED_ITEM, + data: { + expiryDate: 1712190456, + publishedDate: 1678098240000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/twitter.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "Posting on social media helps us spread the word.", + title: "Thanks for sharing!" + }, + contentType: "application/json", + qualifiedDate: 1683042658312, + displayedDate: 1683042658316 + }, + id: "cfcb1af7-7bc2-45b2-a86a-0aa93fe69ce7" + }), + jasmine.objectContaining({ + schema: MESSAGE_FEED_ITEM, + data: { + expiryDate: 1712190456, + publishedDate: 1678184640000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/gold-coin.jpg", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "Now you're ready to earn!", + title: "Funds deposited!" + }, + contentType: "application/json", + qualifiedDate: 1683042653905, + displayedDate: 1683042653909 + }, + id: "0263e171-fa32-4c7a-9611-36b28137a81d" + }), + jasmine.objectContaining({ + schema: MESSAGE_FEED_ITEM, + data: { + expiryDate: 1712190456, + publishedDate: 1677839040000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/achievement.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "Great job, you completed your profile.", + title: "Achievement Unlocked!" + }, + contentType: "application/json", + qualifiedDate: 1683042628064, + displayedDate: 1683042628070 + }, + id: "b7173290-588f-40c6-a05c-43ed5ec08b28" + }), + jasmine.objectContaining({ + schema: MESSAGE_FEED_ITEM, + data: { + expiryDate: 1712190456, + publishedDate: 1677752640000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/lumon.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "a handshake is available upon request.", + title: "Welcome to Lumon!" + }, + contentType: "application/json", + qualifiedDate: 1683042628060, + displayedDate: 1683042628070 + }, + id: "a48ca420-faea-467e-989a-5d179d9f562d" + }) + ] + }); + }); + + it("calls the callback with list of dom action items", () => { + const { command, refresh } = subscribeRulesetItems; + + const callback = jasmine.createSpy(); + + // register a subscription. equivalent to alloy("subscribeRulesetItems", {surface, callback}) + command.run({ + surface: "web://mywebsite.com/feed", + schemas: [DOM_ACTION], + callback + }); + + refresh(PROPOSITIONS); + expect(callback).toHaveBeenCalledOnceWith({ + items: [ + jasmine.objectContaining({ + schema: DOM_ACTION, + data: { + selector: "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + qualifiedDate: 1683042673387, + displayedDate: 1683042673395 + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + }), + jasmine.objectContaining({ + schema: DOM_ACTION, + data: { + selector: "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" + }, + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)", + qualifiedDate: 1683042673380, + displayedDate: 1683042673395 + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }) + ] + }); + }); + + it("calls the callback with list of all schema-based items", () => { + const { command, refresh } = subscribeRulesetItems; + + const callback = jasmine.createSpy(); + + // register a subscription. equivalent to alloy("subscribeRulesetItems", {surface, callback}) + command.run({ + surface: "web://mywebsite.com/feed", + callback + }); + + refresh(PROPOSITIONS); + expect(callback).toHaveBeenCalledOnceWith({ + items: [ + jasmine.objectContaining({ + schema: DOM_ACTION, + data: { + selector: "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + qualifiedDate: 1683042673387, + displayedDate: 1683042673395 + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + }), + jasmine.objectContaining({ + schema: DOM_ACTION, + data: { + selector: "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" + }, + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)", + qualifiedDate: 1683042673380, + displayedDate: 1683042673395 + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }), + jasmine.objectContaining({ + schema: MESSAGE_FEED_ITEM, + data: { + expiryDate: 1712190456, + publishedDate: 1678098240000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/twitter.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "Posting on social media helps us spread the word.", + title: "Thanks for sharing!" + }, + contentType: "application/json", + qualifiedDate: 1683042658312, + displayedDate: 1683042658316 + }, + id: "cfcb1af7-7bc2-45b2-a86a-0aa93fe69ce7" + }), + jasmine.objectContaining({ + schema: MESSAGE_FEED_ITEM, + data: { + expiryDate: 1712190456, + publishedDate: 1678184640000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/gold-coin.jpg", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "Now you're ready to earn!", + title: "Funds deposited!" + }, + contentType: "application/json", + qualifiedDate: 1683042653905, + displayedDate: 1683042653909 + }, + id: "0263e171-fa32-4c7a-9611-36b28137a81d" + }), + jasmine.objectContaining({ + schema: MESSAGE_FEED_ITEM, + data: { + expiryDate: 1712190456, + publishedDate: 1677839040000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/achievement.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "Great job, you completed your profile.", + title: "Achievement Unlocked!" + }, + contentType: "application/json", + qualifiedDate: 1683042628064, + displayedDate: 1683042628070 + }, + id: "b7173290-588f-40c6-a05c-43ed5ec08b28" + }), + jasmine.objectContaining({ + schema: MESSAGE_FEED_ITEM, + data: { + expiryDate: 1712190456, + publishedDate: 1677752640000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/lumon.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "a handshake is available upon request.", + title: "Welcome to Lumon!" + }, + contentType: "application/json", + qualifiedDate: 1683042628060, + displayedDate: 1683042628070 + }, + id: "a48ca420-faea-467e-989a-5d179d9f562d" + }) + ] + }); + }); +}); diff --git a/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js b/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js index 07fb9a7e1..84df58f75 100644 --- a/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js +++ b/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js @@ -52,7 +52,6 @@ describe("Personalization:subscribeMessageFeed", () => { feedName: "Winter Promo", surface: "web://mywebsite.com/feed" }, - parameters: {}, content: { imageUrl: "img/lumon.png", actionTitle: "Shop the sale!", @@ -76,7 +75,6 @@ describe("Personalization:subscribeMessageFeed", () => { feedName: "Winter Promo", surface: "web://mywebsite.com/feed" }, - parameters: {}, content: { imageUrl: "img/achievement.png", actionTitle: "Shop the sale!", @@ -105,7 +103,6 @@ describe("Personalization:subscribeMessageFeed", () => { feedName: "Winter Promo", surface: "web://mywebsite.com/feed" }, - parameters: {}, content: { imageUrl: "img/twitter.png", actionTitle: "Shop the sale!", @@ -156,7 +153,6 @@ describe("Personalization:subscribeMessageFeed", () => { feedName: "Winter Promo", surface: "web://mywebsite.com/feed" }, - parameters: {}, content: { imageUrl: "img/gold-coin.jpg", actionTitle: "Shop the sale!", From 8c037cf2324eb0d79004e387d221df21b48b8399 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Tue, 29 Aug 2023 16:14:01 -0600 Subject: [PATCH 39/66] license --- .../DecisioningEngine/createSubscribeRulesetItems.js | 11 +++++++++++ .../Personalization/createSubscribeMessageFeed.js | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/components/DecisioningEngine/createSubscribeRulesetItems.js b/src/components/DecisioningEngine/createSubscribeRulesetItems.js index 1c9246194..f47cf7df8 100644 --- a/src/components/DecisioningEngine/createSubscribeRulesetItems.js +++ b/src/components/DecisioningEngine/createSubscribeRulesetItems.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 { arrayOf, callback as callbackType, diff --git a/src/components/Personalization/createSubscribeMessageFeed.js b/src/components/Personalization/createSubscribeMessageFeed.js index 4fce76199..4ebc8f4db 100644 --- a/src/components/Personalization/createSubscribeMessageFeed.js +++ b/src/components/Personalization/createSubscribeMessageFeed.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 { callback as callbackType, objectOf, From 050f7b6314700c1ae70c72d70568e9cdcc18285a Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Wed, 30 Aug 2023 14:33:02 -0600 Subject: [PATCH 40/66] remove IAM types, add message feed actions modules --- .../inAppMessageConsequenceAdapter.js | 23 ------------- .../Personalization/createModules.js | 9 +++-- .../actions/displayBanner.js | 16 --------- .../actions/displayCustom.js | 17 ---------- .../actions/displayFullScreen.js | 16 --------- .../actions/displayIframeContent.js | 18 +++------- .../actions/displayModal.js | 16 --------- .../initInAppMessageActionsModules.js | 9 +++++ .../initMessagingActionsModules.js | 23 ------------- .../initMessageFeedActionsModules.js | 5 +++ .../inAppMessageConsequenceAdapter.spec.js | 5 ++- .../createConsequenceAdapter.spec.js | 1 - .../Personalization/createModules.spec.js | 21 +++++++----- .../actions/displayBanner.spec.js | 4 +-- .../actions/displayCustom.spec.js | 11 ------- .../actions/displayFullScreen.spec.js | 11 ------- .../actions/displayModal.spec.js | 4 +-- ...=> initInAppMessageActionsModules.spec.js} | 6 ++-- .../initMessageFeedActionsModules.spec.js | 33 +++++++++++++++++++ 19 files changed, 78 insertions(+), 170 deletions(-) delete mode 100644 src/components/Personalization/in-app-message-actions/actions/displayBanner.js delete mode 100644 src/components/Personalization/in-app-message-actions/actions/displayCustom.js delete mode 100644 src/components/Personalization/in-app-message-actions/actions/displayFullScreen.js delete mode 100644 src/components/Personalization/in-app-message-actions/actions/displayModal.js create mode 100644 src/components/Personalization/in-app-message-actions/initInAppMessageActionsModules.js delete mode 100644 src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js create mode 100644 src/components/Personalization/message-feed-actions/initMessageFeedActionsModules.js delete mode 100644 test/unit/specs/components/Personalization/in-app-message-actions/actions/displayCustom.spec.js delete mode 100644 test/unit/specs/components/Personalization/in-app-message-actions/actions/displayFullScreen.spec.js rename test/unit/specs/components/Personalization/in-app-message-actions/{initMessagingActionsModules.spec.js => initInAppMessageActionsModules.spec.js} (80%) create mode 100644 test/unit/specs/components/Personalization/message-feed-actions/initMessageFeedActionsModules.spec.js diff --git a/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js b/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js index 88374b916..7c8f2dccd 100644 --- a/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js +++ b/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js @@ -10,28 +10,6 @@ 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 "../../Personalization/constants/schema"; -import { - IAM_ACTION_TYPE_BANNER, - IAM_ACTION_TYPE_CUSTOM, - IAM_ACTION_TYPE_FULLSCREEN, - IAM_ACTION_TYPE_MODAL -} from "../../Personalization/in-app-message-actions/initMessagingActionsModules"; - -const deduceType = html => { - if (html.includes("banner")) { - return IAM_ACTION_TYPE_BANNER; - } - - if (html.includes("modal")) { - return IAM_ACTION_TYPE_MODAL; - } - - if (html.includes("fullscreen")) { - return IAM_ACTION_TYPE_FULLSCREEN; - } - - return IAM_ACTION_TYPE_CUSTOM; -}; export default (id, type, detail) => { const { html, mobileParameters } = detail; @@ -41,7 +19,6 @@ export default (id, type, detail) => { return { schema: MESSAGE_IN_APP, data: { - type: deduceType(html, mobileParameters), mobileParameters, webParameters, content: html, diff --git a/src/components/Personalization/createModules.js b/src/components/Personalization/createModules.js index 715484de2..b633b16e9 100644 --- a/src/components/Personalization/createModules.js +++ b/src/components/Personalization/createModules.js @@ -15,14 +15,13 @@ import { MESSAGE_IN_APP } from "./constants/schema"; import { initDomActionsModules } from "./dom-actions"; -import initMessagingActionsModules from "./in-app-message-actions/initMessagingActionsModules"; +import initInAppMessageActionsModules from "./in-app-message-actions/initInAppMessageActionsModules"; +import initMessageFeedActionsModules from "./message-feed-actions/initMessageFeedActionsModules"; export default ({ storeClickMetrics, collect }) => { - const messagingActionsModules = initMessagingActionsModules(collect); - return { [DOM_ACTION]: initDomActionsModules(storeClickMetrics), - [MESSAGE_IN_APP]: messagingActionsModules, - [MESSAGE_FEED_ITEM]: messagingActionsModules + [MESSAGE_IN_APP]: initInAppMessageActionsModules(collect), + [MESSAGE_FEED_ITEM]: initMessageFeedActionsModules() }; }; diff --git a/src/components/Personalization/in-app-message-actions/actions/displayBanner.js b/src/components/Personalization/in-app-message-actions/actions/displayBanner.js deleted file mode 100644 index 46e52f9c4..000000000 --- a/src/components/Personalization/in-app-message-actions/actions/displayBanner.js +++ /dev/null @@ -1,16 +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 displayIframeContent from "./displayIframeContent"; - -export default (settings, collect) => { - return displayIframeContent(settings, collect); -}; diff --git a/src/components/Personalization/in-app-message-actions/actions/displayCustom.js b/src/components/Personalization/in-app-message-actions/actions/displayCustom.js deleted file mode 100644 index 2b79fdd8a..000000000 --- a/src/components/Personalization/in-app-message-actions/actions/displayCustom.js +++ /dev/null @@ -1,17 +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 displayIframeContent from "./displayIframeContent"; - -export default (settings, collect) => { - return displayIframeContent(settings, collect); -}; diff --git a/src/components/Personalization/in-app-message-actions/actions/displayFullScreen.js b/src/components/Personalization/in-app-message-actions/actions/displayFullScreen.js deleted file mode 100644 index 46e52f9c4..000000000 --- a/src/components/Personalization/in-app-message-actions/actions/displayFullScreen.js +++ /dev/null @@ -1,16 +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 displayIframeContent from "./displayIframeContent"; - -export default (settings, collect) => { - return displayIframeContent(settings, collect); -}; diff --git a/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js b/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js index d0ce5b4cc..e10ef9082 100644 --- a/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js +++ b/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js @@ -1,4 +1,3 @@ -/* eslint-disable no-unused-vars */ /* Copyright 2023 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); @@ -12,12 +11,10 @@ governing permissions and limitations under the License. */ import { getNonce } from "../../dom-actions/dom"; -import { INTERACT } from "../../constants/eventType"; import { removeElementById } from "../utils"; const ELEMENT_TAG_CLASSNAME = "alloy-messaging-container"; const ELEMENT_TAG_ID = "alloy-messaging-container"; -const ANCHOR_HREF_REGEX = /adbinapp:\/\/(\w+)\?interaction=(\w+)/i; const OVERLAY_TAG_CLASSNAME = "alloy-overlay-container"; const OVERLAY_TAG_ID = "alloy-overlay-container"; @@ -26,6 +23,7 @@ const ALLOY_IFRAME_ID = "alloy-iframe-id"; const dismissMessage = () => [ELEMENT_TAG_ID, OVERLAY_TAG_ID].forEach(removeElementById); +// eslint-disable-next-line no-unused-vars export const buildStyleFromParameters = (mobileParameters, webParameters) => { const { verticalAlign, @@ -36,7 +34,7 @@ export const buildStyleFromParameters = (mobileParameters, webParameters) => { cornerRadius, horizontalInset, verticalInset, - uiTakeover + uiTakeover = false } = mobileParameters; const style = { @@ -82,11 +80,8 @@ export const setWindowLocationHref = link => { window.location.assign(link); }; -export const createIframeClickHandler = ( - container, - collect, - mobileParameters -) => { +// eslint-disable-next-line no-unused-vars +export const createIframeClickHandler = collect => { return event => { event.preventDefault(); event.stopImmediatePropagation(); @@ -203,10 +198,7 @@ export const displayHTMLContentInIframe = (settings, collect) => { const container = createContainerElement(settings); - const iframe = createIframe( - content, - createIframeClickHandler(container, collect, mobileParameters) - ); + const iframe = createIframe(content, createIframeClickHandler(collect)); container.appendChild(iframe); diff --git a/src/components/Personalization/in-app-message-actions/actions/displayModal.js b/src/components/Personalization/in-app-message-actions/actions/displayModal.js deleted file mode 100644 index 46e52f9c4..000000000 --- a/src/components/Personalization/in-app-message-actions/actions/displayModal.js +++ /dev/null @@ -1,16 +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 displayIframeContent from "./displayIframeContent"; - -export default (settings, collect) => { - return displayIframeContent(settings, collect); -}; diff --git a/src/components/Personalization/in-app-message-actions/initInAppMessageActionsModules.js b/src/components/Personalization/in-app-message-actions/initInAppMessageActionsModules.js new file mode 100644 index 000000000..dd157e761 --- /dev/null +++ b/src/components/Personalization/in-app-message-actions/initInAppMessageActionsModules.js @@ -0,0 +1,9 @@ +/* eslint-disable no-unused-vars */ + +import displayIframeContent from "./actions/displayIframeContent"; + +export default collect => { + return { + defaultContent: settings => displayIframeContent(settings, collect) + }; +}; diff --git a/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js b/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js deleted file mode 100644 index ad82fcfab..000000000 --- a/src/components/Personalization/in-app-message-actions/initMessagingActionsModules.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable no-unused-vars */ - -import displayModal from "./actions/displayModal"; -import displayBanner from "./actions/displayBanner"; -import displayCustom from "./actions/displayCustom"; -import displayFullScreen from "./actions/displayFullScreen"; - -export const IAM_ACTION_TYPE_MODAL = "modal"; -export const IAM_ACTION_TYPE_BANNER = "banner"; -export const IAM_ACTION_TYPE_FULLSCREEN = "fullscreen"; -export const IAM_ACTION_TYPE_CUSTOM = "custom"; - -export default collect => { - // TODO: use collect to capture click and display metrics - return { - [IAM_ACTION_TYPE_MODAL]: settings => displayModal(settings, collect), - [IAM_ACTION_TYPE_BANNER]: settings => displayBanner(settings, collect), - [IAM_ACTION_TYPE_FULLSCREEN]: settings => - displayFullScreen(settings, collect), - [IAM_ACTION_TYPE_CUSTOM]: settings => displayCustom(settings, collect), - defaultContent: () => Promise.resolve() - }; -}; diff --git a/src/components/Personalization/message-feed-actions/initMessageFeedActionsModules.js b/src/components/Personalization/message-feed-actions/initMessageFeedActionsModules.js new file mode 100644 index 000000000..81c60e615 --- /dev/null +++ b/src/components/Personalization/message-feed-actions/initMessageFeedActionsModules.js @@ -0,0 +1,5 @@ +export default () => { + return { + defaultContent: () => Promise.resolve() + }; +}; diff --git a/test/unit/specs/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.spec.js b/test/unit/specs/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.spec.js index 848e14ee5..53394a39c 100644 --- a/test/unit/specs/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.spec.js +++ b/test/unit/specs/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.spec.js @@ -32,13 +32,12 @@ describe("DecisioningEngine:inAppMessageConsequenceAdapter", () => { backdropColor: "#000000", height: 60 }, - html: "
    modal
    " + html: "" } ) ).toEqual({ schema: "https://ns.adobe.com/personalization/message/in-app", data: { - type: "modal", mobileParameters: { verticalAlign: "center", dismissAnimation: "top", @@ -54,7 +53,7 @@ describe("DecisioningEngine:inAppMessageConsequenceAdapter", () => { height: 60 }, webParameters: jasmine.any(Object), - content: "
    modal
    ", + content: "", contentType: "text/html" }, id: "72042c7c-4e34-44f6-af95-1072ae117424" diff --git a/test/unit/specs/components/DecisioningEngine/createConsequenceAdapter.spec.js b/test/unit/specs/components/DecisioningEngine/createConsequenceAdapter.spec.js index d0294d9da..26ecf5319 100644 --- a/test/unit/specs/components/DecisioningEngine/createConsequenceAdapter.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createConsequenceAdapter.spec.js @@ -42,7 +42,6 @@ describe("DecisioningEngine:createConsequenceAdapter", () => { expect(adaptedConsequence).toEqual({ schema: "https://ns.adobe.com/personalization/message/in-app", data: { - type: "modal", mobileParameters: { verticalAlign: "center", dismissAnimation: "top", diff --git a/test/unit/specs/components/Personalization/createModules.spec.js b/test/unit/specs/components/Personalization/createModules.spec.js index f0eccc285..39e5d21b3 100644 --- a/test/unit/specs/components/Personalization/createModules.spec.js +++ b/test/unit/specs/components/Personalization/createModules.spec.js @@ -48,17 +48,22 @@ describe("createModules", () => { it("has in-app-message modules", () => { const modules = createModules({ storeClickMetrics: noop, collect: noop }); - const messageModules = { - modal: jasmine.any(Function), - banner: jasmine.any(Function), - fullscreen: jasmine.any(Function), - custom: jasmine.any(Function), + const inAppMessageModules = { defaultContent: jasmine.any(Function) }; - expect(modules[MESSAGE_IN_APP]).toEqual(messageModules); - expect(modules[MESSAGE_FEED_ITEM]).toEqual(messageModules); + expect(modules[MESSAGE_IN_APP]).toEqual(inAppMessageModules); + expect(Object.keys(modules[MESSAGE_IN_APP]).length).toEqual(1); + }); + + it("has message feed modules", () => { + const modules = createModules({ storeClickMetrics: noop, collect: noop }); + + const messageFeedModules = { + defaultContent: jasmine.any(Function) + }; - expect(Object.keys(modules[MESSAGE_IN_APP]).length).toEqual(5); + expect(modules[MESSAGE_FEED_ITEM]).toEqual(messageFeedModules); + expect(Object.keys(modules[MESSAGE_FEED_ITEM]).length).toEqual(1); }); }); diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js index 1b6e31151..d814e40af 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js @@ -11,7 +11,7 @@ governing permissions and limitations under the License. */ import { createNode } from "../../../../../../../src/utils/dom"; import { DIV } from "../../../../../../../src/constants/tagName"; -import displayBanner from "../../../../../../../src/components/Personalization/in-app-message-actions/actions/displayBanner"; +import displayIframeContent from "../../../../../../../src/components/Personalization/in-app-message-actions/actions/displayIframeContent"; describe("Personalization::IAM:banner", () => { it("inserts banner into dom", async () => { @@ -26,7 +26,7 @@ describe("Personalization::IAM:banner", () => { document.body.append(something); - await displayBanner({ + await displayIframeContent({ mobileParameters: { verticalAlign: "center", dismissAnimation: "top", diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayCustom.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayCustom.spec.js deleted file mode 100644 index 26ef92385..000000000 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayCustom.spec.js +++ /dev/null @@ -1,11 +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. -*/ diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayFullScreen.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayFullScreen.spec.js deleted file mode 100644 index 26ef92385..000000000 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayFullScreen.spec.js +++ /dev/null @@ -1,11 +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. -*/ diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js index 5bc1179b2..2c4b3faae 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js @@ -11,7 +11,7 @@ governing permissions and limitations under the License. */ import { createNode } from "../../../../../../../src/utils/dom"; import { DIV } from "../../../../../../../src/constants/tagName"; -import displayModal from "../../../../../../../src/components/Personalization/in-app-message-actions/actions/displayModal"; +import displayIframeContent from "../../../../../../../src/components/Personalization/in-app-message-actions/actions/displayIframeContent"; describe("Personalization::IAM:modal", () => { it("inserts modal into dom", async () => { @@ -26,7 +26,7 @@ describe("Personalization::IAM:modal", () => { document.body.append(something); - await displayModal({ + await displayIframeContent({ mobileParameters: { verticalAlign: "center", dismissAnimation: "top", diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/initMessagingActionsModules.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/initInAppMessageActionsModules.spec.js similarity index 80% rename from test/unit/specs/components/Personalization/in-app-message-actions/initMessagingActionsModules.spec.js rename to test/unit/specs/components/Personalization/in-app-message-actions/initInAppMessageActionsModules.spec.js index a2dbd792c..47ec60312 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/initMessagingActionsModules.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/initInAppMessageActionsModules.spec.js @@ -10,17 +10,17 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import initMessagingActionsModules from "../../../../../../src/components/Personalization/in-app-message-actions/initMessagingActionsModules"; import createModules from "../../../../../../src/components/Personalization/createModules"; import { MESSAGE_IN_APP } from "../../../../../../src/components/Personalization/constants/schema"; +import initInAppMessageActionsModules from "../../../../../../src/components/Personalization/in-app-message-actions/initInAppMessageActionsModules"; -describe("Personalization::turbine::initMessagingActionsModules", () => { +describe("Personalization::turbine::initInAppMessageActionsModules", () => { const noop = () => undefined; const modules = createModules({ storeClickMetrics: noop, collect: noop }); const expectedModules = modules[MESSAGE_IN_APP]; it("should have all the required modules", () => { - const messagingActionsModules = initMessagingActionsModules(() => {}); + const messagingActionsModules = initInAppMessageActionsModules(() => {}); expect(Object.keys(messagingActionsModules).length).toEqual( Object.keys(expectedModules).length diff --git a/test/unit/specs/components/Personalization/message-feed-actions/initMessageFeedActionsModules.spec.js b/test/unit/specs/components/Personalization/message-feed-actions/initMessageFeedActionsModules.spec.js new file mode 100644 index 000000000..dbf1d71d2 --- /dev/null +++ b/test/unit/specs/components/Personalization/message-feed-actions/initMessageFeedActionsModules.spec.js @@ -0,0 +1,33 @@ +/* +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 createModules from "../../../../../../src/components/Personalization/createModules"; +import { MESSAGE_FEED_ITEM } from "../../../../../../src/components/Personalization/constants/schema"; +import initMessageFeedActionsModules from "../../../../../../src/components/Personalization/message-feed-actions/initMessageFeedActionsModules"; + +describe("Personalization::turbine::initMessageFeedActionsModules", () => { + const noop = () => undefined; + const modules = createModules({ storeClickMetrics: noop, collect: noop }); + const expectedModules = modules[MESSAGE_FEED_ITEM]; + + it("should have all the required modules", () => { + const messageFeedActionsModules = initMessageFeedActionsModules(); + + expect(Object.keys(messageFeedActionsModules).length).toEqual( + Object.keys(expectedModules).length + ); + + Object.keys(expectedModules).forEach(key => { + expect(messageFeedActionsModules[key]).toEqual(jasmine.any(Function)); + }); + }); +}); From da2de83d4bf4a20c4e2b84f4a5d91f4abbdedd12 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Wed, 30 Aug 2023 14:33:24 -0600 Subject: [PATCH 41/66] license --- .../initMessageFeedActionsModules.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/Personalization/message-feed-actions/initMessageFeedActionsModules.js b/src/components/Personalization/message-feed-actions/initMessageFeedActionsModules.js index 81c60e615..53be3e9c2 100644 --- a/src/components/Personalization/message-feed-actions/initMessageFeedActionsModules.js +++ b/src/components/Personalization/message-feed-actions/initMessageFeedActionsModules.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. +*/ export default () => { return { defaultContent: () => Promise.resolve() From 8d485b9769176716015ebb05c28e669cec5444b8 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Thu, 31 Aug 2023 14:40:57 -0600 Subject: [PATCH 42/66] ensure schema based ruleset consequences --- src/components/DecisioningEngine/index.js | 3 ++ src/components/DecisioningEngine/utils.js | 10 +++++ src/core/createEvent.js | 8 +++- .../createDecisionHistory.spec.js | 11 ++++++ .../createSubscribeRulesetItems.spec.js | 11 ++++++ .../DecisioningEngine/index.spec.js | 38 ++++++++++++++++++- .../createSubscribeMessageFeed.spec.js | 11 ++++++ 7 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/components/DecisioningEngine/index.js b/src/components/DecisioningEngine/index.js index f408a0fcb..ee7739dcb 100644 --- a/src/components/DecisioningEngine/index.js +++ b/src/components/DecisioningEngine/index.js @@ -16,6 +16,7 @@ import createApplyResponse from "./createApplyResponse"; import createEventRegistry from "./createEventRegistry"; import createContextProvider from "./createContextProvider"; import createSubscribeRulesetItems from "./createSubscribeRulesetItems"; +import { ensureSchemaBasedRulesetConsequences } from "./utils"; const createDecisioningEngine = ({ config, createNamespacedStorage }) => { const { orgId } = config; @@ -43,6 +44,8 @@ const createDecisioningEngine = ({ config, createNamespacedStorage }) => { decisionContext = {}, onResponse = noop }) { + ensureSchemaBasedRulesetConsequences(event); + onResponse( createOnResponseHandler({ renderDecisions, diff --git a/src/components/DecisioningEngine/utils.js b/src/components/DecisioningEngine/utils.js index 7de6b4d7a..5b7f5338c 100644 --- a/src/components/DecisioningEngine/utils.js +++ b/src/components/DecisioningEngine/utils.js @@ -42,3 +42,13 @@ export const getExpirationDate = retentionPeriod => { expirationDate.setDate(expirationDate.getDate() - retentionPeriod); return expirationDate; }; + +export const ensureSchemaBasedRulesetConsequences = event => { + event.mergeData({ + __adobe: { + ajo: { + "in-app-response-format": 2 + } + } + }); +}; diff --git a/src/core/createEvent.js b/src/core/createEvent.js index 10a0f3b69..b62d61864 100644 --- a/src/core/createEvent.js +++ b/src/core/createEvent.js @@ -56,6 +56,12 @@ export default () => { deepAssign(content, { xdm }); } }, + mergeData(data) { + throwIfEventFinalized("mergeData"); + if (data) { + deepAssign(content, { data }); + } + }, mergeMeta(meta) { throwIfEventFinalized("mergeMeta"); if (meta) { @@ -81,7 +87,7 @@ export default () => { } if (userData) { - content.data = userData; + event.mergeData(userData); } // the event should already be considered finalized in case onBeforeEventSend throws an error diff --git a/test/unit/specs/components/DecisioningEngine/createDecisionHistory.spec.js b/test/unit/specs/components/DecisioningEngine/createDecisionHistory.spec.js index 452f783a2..ff775adb8 100644 --- a/test/unit/specs/components/DecisioningEngine/createDecisionHistory.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createDecisionHistory.spec.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 createDecisionHistory from "../../../../../src/components/DecisioningEngine/createDecisionHistory"; import createEventRegistry from "../../../../../src/components/DecisioningEngine/createEventRegistry"; diff --git a/test/unit/specs/components/DecisioningEngine/createSubscribeRulesetItems.spec.js b/test/unit/specs/components/DecisioningEngine/createSubscribeRulesetItems.spec.js index 85a5edf15..41b197f24 100644 --- a/test/unit/specs/components/DecisioningEngine/createSubscribeRulesetItems.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createSubscribeRulesetItems.spec.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 { DOM_ACTION } from "@adobe/alloy/libEs5/components/Personalization/constants/schema"; import createSubscribeRulesetItems from "../../../../../src/components/DecisioningEngine/createSubscribeRulesetItems"; import { MESSAGE_FEED_ITEM } from "../../../../../src/components/Personalization/constants/schema"; diff --git a/test/unit/specs/components/DecisioningEngine/index.spec.js b/test/unit/specs/components/DecisioningEngine/index.spec.js index 3859c4971..17980fffb 100644 --- a/test/unit/specs/components/DecisioningEngine/index.spec.js +++ b/test/unit/specs/components/DecisioningEngine/index.spec.js @@ -17,10 +17,12 @@ import { } from "./contextTestUtils"; describe("createDecisioningEngine:commands:evaluateRulesets", () => { + let mergeData; let mockEvent; let onResponseHandler; let decisioningEngine; beforeEach(() => { + mergeData = jasmine.createSpy(); const config = { orgId: "exampleOrgId" }; window.referrer = "https://www.google.com/search?q=adobe+journey+optimizer&oq=adobe+journey+optimizer"; @@ -29,7 +31,11 @@ describe("createDecisioningEngine:commands:evaluateRulesets", () => { config, createNamespacedStorage }); - mockEvent = { getContent: () => ({}), getViewName: () => undefined }; + mockEvent = { + getContent: () => ({}), + getViewName: () => undefined, + mergeData + }; decisioningEngine.lifecycle.onComponentsRegistered(() => {}); }); @@ -132,4 +138,34 @@ describe("createDecisioningEngine:commands:evaluateRulesets", () => { propositions: [proposition] }); }); + + it("ensures schema-based ruleset consequences", () => { + onResponseHandler = onResponse => { + onResponse({ + response: mockRulesetResponseWithCondition({ + definition: { + key: "referringPage.path", + matcher: "eq", + values: ["/search"] + }, + type: "matcher" + }) + }); + }; + + decisioningEngine.lifecycle.onBeforeEvent({ + event: mockEvent, + renderDecisions: false, + decisionContext: {}, + onResponse: onResponseHandler + }); + + expect(mergeData).toHaveBeenCalledOnceWith({ + __adobe: { + ajo: { + "in-app-response-format": 2 + } + } + }); + }); }); diff --git a/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js b/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js index 84df58f75..32c0716d9 100644 --- a/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.js +++ b/test/unit/specs/components/Personalization/createSubscribeMessageFeed.spec.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 createSubscribeMessageFeed from "../../../../../src/components/Personalization/createSubscribeMessageFeed"; import { MESSAGE_FEED_ITEM } from "../../../../../src/components/Personalization/constants/schema"; From 8447ce91b8f6ab493831b3794eaf6045f15824eb Mon Sep 17 00:00:00 2001 From: Happy Shandilya Date: Wed, 6 Sep 2023 12:59:14 -0700 Subject: [PATCH 43/66] message feed sandbox (#1028) * message feed sandbox * fixed eslint for InApp demo * clear local storage and reload the page --- sandbox/src/App.js | 5 + .../InAppMessagesDemo/InAppMessages.js | 11 +- .../MessageFeedDemo/MessageFeed.css | 15 + .../components/MessageFeedDemo/MessageFeed.js | 504 ++++++++++++++++++ 4 files changed, 529 insertions(+), 6 deletions(-) create mode 100644 sandbox/src/components/MessageFeedDemo/MessageFeed.css create mode 100644 sandbox/src/components/MessageFeedDemo/MessageFeed.js diff --git a/sandbox/src/App.js b/sandbox/src/App.js index 1282f112b..8bdf2516a 100755 --- a/sandbox/src/App.js +++ b/sandbox/src/App.js @@ -32,6 +32,7 @@ import Identity from "./Identity"; import AlloyVersion from "./components/AlloyVersion"; import ConfigOverrides from "./ConfigOverrides.jsx"; import InAppMessages from "./components/InAppMessagesDemo/InAppMessages"; +import MessageFeed from "./components/MessageFeedDemo/MessageFeed"; const BasicExample = () => { return ( @@ -100,6 +101,9 @@ const BasicExample = () => {
  • In-app Messages
  • +
  • + Message Feed +

  • @@ -130,6 +134,7 @@ const BasicExample = () => { + diff --git a/sandbox/src/components/InAppMessagesDemo/InAppMessages.js b/sandbox/src/components/InAppMessagesDemo/InAppMessages.js index 74db8c0d6..6cadd13d5 100644 --- a/sandbox/src/components/InAppMessagesDemo/InAppMessages.js +++ b/sandbox/src/components/InAppMessagesDemo/InAppMessages.js @@ -4,7 +4,7 @@ import "./InAppMessagesStyle.css"; export default function DecisionEngine() { const [realResponse, setRealResponse] = useState(null); - async function getInAppPayload(payload) { + const getInAppPayload = async payload => { const res = await fetch( `https://edge.adobedc.net/ee/or2/v1/interact?configId=7a19c434-6648-48d3-948f-ba0258505d98&requestId=520353b2-dc0d-428c-9e0d-138fc6cbec4e`, { @@ -12,12 +12,11 @@ export default function DecisionEngine() { body: JSON.stringify(payload) } ); - return await res.json(); - } + return res.json(); + }; useEffect(() => { - async function fetchInAppPayload() { - console.log("fetching in app payload"); + const fetchInAppPayload = async () => { const response = await getInAppPayload({ events: [ { @@ -38,7 +37,7 @@ export default function DecisionEngine() { ] }); setRealResponse(response); - } + }; fetchInAppPayload(); }, []); diff --git a/sandbox/src/components/MessageFeedDemo/MessageFeed.css b/sandbox/src/components/MessageFeedDemo/MessageFeed.css new file mode 100644 index 000000000..3fbd057da --- /dev/null +++ b/sandbox/src/components/MessageFeedDemo/MessageFeed.css @@ -0,0 +1,15 @@ +/* Card.css */ +.pretty-card { + color: black; + border: 1px solid #ccc; + border-radius: 8px; + padding: 16px; + margin: 16px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + background-color: lightblue; + transition: transform 0.2s; +} + +.pretty-card:hover { + transform: scale(1.05); +} diff --git a/sandbox/src/components/MessageFeedDemo/MessageFeed.js b/sandbox/src/components/MessageFeedDemo/MessageFeed.js new file mode 100644 index 000000000..e9f2e1926 --- /dev/null +++ b/sandbox/src/components/MessageFeedDemo/MessageFeed.js @@ -0,0 +1,504 @@ +import React, { useEffect, useState } from "react"; +import ContentSecurityPolicy from "../ContentSecurityPolicy"; +import "./MessageFeed.css"; + +export default function MessageFeed() { + const [messageFeedItems, setMessageFeedItems] = useState([]); + + const prettyDate = value => { + let output = ""; + + if (typeof value !== "undefined") { + const now = new Date().getTime(); + const seconds = Math.floor(now / 1000); + const oldTimestamp = Math.floor(value / 1000); + const difference = seconds - oldTimestamp; + + if (difference < 60) { + output = `${difference} second(s) ago`; + } else if (difference < 3600) { + output = `${Math.floor(difference / 60)} min ago`; + } else if (difference < 86400) { + output = `${Math.floor(difference / 3600)} hour(s) ago`; + } else if (difference < 2620800) { + output = `${Math.floor(difference / 86400)} day(s) ago`; + } else if (difference < 31449600) { + output = `${Math.floor(difference / 2620800)} month(s) ago`; + } else { + output = `${Math.floor(difference / 31449600)} year(s) ago`; + } + } + + return output; + }; + + useEffect(() => { + /* eslint-disable */ + const mockResponse = { + requestId: "5a38a9ef-67d7-4f66-8977-c4dc0e0967b6", + handle: [ + { + payload: [ + { + id: "11893040138696185741718511332124641876", + namespace: { + code: "ECID" + } + } + ], + type: "identity:result" + }, + { + payload: [ + { + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: + "eyJtZXNzYWdlRXhlY3V0aW9uIjp7Im1lc3NhZ2VFeGVjdXRpb25JRCI6Ik5BIiwibWVzc2FnZUlEIjoiMDJjNzdlYTgtN2MwZS00ZDMzLTgwOTAtNGE1YmZkM2Q3NTAzIiwibWVzc2FnZVR5cGUiOiJtYXJrZXRpbmciLCJjYW1wYWlnbklEIjoiMzlhZThkNGItYjU1ZS00M2RjLWExNDMtNzdmNTAxOTViNDg3IiwiY2FtcGFpZ25WZXJzaW9uSUQiOiJiZDg1ZDllOC0yMDM3LTQyMmYtYjZkMi0zOTU3YzkwNTU5ZDMiLCJjYW1wYWlnbkFjdGlvbklEIjoiYjQ3ZmRlOGItNTdjMS00YmJlLWFlMjItNjRkNWI3ODJkMTgzIiwibWVzc2FnZVB1YmxpY2F0aW9uSUQiOiJhZTUyY2VkOC0yMDBjLTQ5N2UtODc4Ny1lZjljZmMxNzgyMTUifSwibWVzc2FnZVByb2ZpbGUiOnsiY2hhbm5lbCI6eyJfaWQiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbHMvd2ViIiwiX3R5cGUiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbC10eXBlcy93ZWIifSwibWVzc2FnZVByb2ZpbGVJRCI6ImY1Y2Q5OTk1LTZiNDQtNDIyMS05YWI3LTViNTMzOGQ1ZjE5MyJ9fQ==" + }, + strategies: [ + { + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" + } + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" + }, + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" + }, + id: "1a3d874f-39ee-4310-bfa9-6559a10041a4", + items: [ + { + id: "9d9c6e62-a8e5-419b-abe3-429950c27425", + schema: "https://ns.adobe.com/personalization/ruleset-item", + data: { + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [ + { + definition: { + conditions: [ + { + definition: { + key: "events", + matcher: "ex" + }, + type: "matcher" + } + ], + logic: "and" + }, + type: "group" + } + ], + logic: "and" + }, + type: "group" + }, + consequences: [ + { + type: "schema", + detail: { + schema: + "https://ns.adobe.com/personalization/message/feed-item", + data: { + expiryDate: 1712190456, + publishedDate: 1677752640000, + meta: { + feedName: "Winter Promo", + surface: + "mobileapp://com.adobe.sampleApp/feed/promos" + }, + content: { + imageUrl: + "https://target.jasonwaters.dev/img/lumon.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: + "a handshake is available upon request.", + title: "Welcome to Lumon!" + }, + contentType: "application/json" + }, + id: "a48ca420-faea-467e-989a-5d179d9f562d" + }, + id: "a48ca420-faea-467e-989a-5d179d9f562d" + }, + { + type: "schema", + detail: { + schema: + "https://ns.adobe.com/personalization/message/feed-item", + data: { + expiryDate: 1712190456, + publishedDate: 1677839040000, + meta: { + feedName: "Winter Promo", + surface: + "mobileapp://com.adobe.sampleApp/feed/promos" + }, + content: { + imageUrl: + "https://media.giphy.com/media/l0Ex3vQtX5VX2YtAQ/giphy.gif", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: + "Great job, you completed your profile.", + title: "Achievement Unlocked!" + }, + contentType: "application/json" + }, + id: "b7173290-588f-40c6-a05c-43ed5ec08b28" + }, + id: "b7173290-588f-40c6-a05c-43ed5ec08b28" + } + ] + } + ] + } + } + ], + scope: "web://target.jasonwaters.dev/aep.html" + }, + { + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: + "eyJtZXNzYWdlRXhlY3V0aW9uIjp7Im1lc3NhZ2VFeGVjdXRpb25JRCI6Ik5BIiwibWVzc2FnZUlEIjoiMDJjNzdlYTgtN2MwZS00ZDMzLTgwOTAtNGE1YmZkM2Q3NTAzIiwibWVzc2FnZVR5cGUiOiJtYXJrZXRpbmciLCJjYW1wYWlnbklEIjoiMzlhZThkNGItYjU1ZS00M2RjLWExNDMtNzdmNTAxOTViNDg3IiwiY2FtcGFpZ25WZXJzaW9uSUQiOiJiZDg1ZDllOC0yMDM3LTQyMmYtYjZkMi0zOTU3YzkwNTU5ZDMiLCJjYW1wYWlnbkFjdGlvbklEIjoiYjQ3ZmRlOGItNTdjMS00YmJlLWFlMjItNjRkNWI3ODJkMTgzIiwibWVzc2FnZVB1YmxpY2F0aW9uSUQiOiJhZTUyY2VkOC0yMDBjLTQ5N2UtODc4Ny1lZjljZmMxNzgyMTUifSwibWVzc2FnZVByb2ZpbGUiOnsiY2hhbm5lbCI6eyJfaWQiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbHMvd2ViIiwiX3R5cGUiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbC10eXBlcy93ZWIifSwibWVzc2FnZVByb2ZpbGVJRCI6ImY1Y2Q5OTk1LTZiNDQtNDIyMS05YWI3LTViNTMzOGQ1ZjE5MyJ9fQ==" + }, + strategies: [ + { + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" + } + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" + }, + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" + }, + id: "1ae11bc5-96dc-41c7-8f71-157c57a5290e", + items: [ + { + id: "e0575812-74e5-46b9-a4f2-9541dfaec2d0", + schema: "https://ns.adobe.com/personalization/ruleset-item", + data: { + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [ + { + definition: { + conditions: [ + { + definition: { + key: "action", + matcher: "eq", + values: ["share-social-media"] + }, + type: "matcher" + }, + { + definition: { + events: [ + { + type: + "decisioning.propositionDisplay", + id: + "1ae11bc5-96dc-41c7-8f71-157c57a5290e" + } + ], + matcher: "ge", + value: 1 + }, + type: "historical" + } + ], + logic: "or" + }, + type: "group" + } + ], + logic: "and" + }, + type: "group" + }, + consequences: [ + { + type: "schema", + detail: { + schema: + "https://ns.adobe.com/personalization/message/feed-item", + data: { + expiryDate: 1712190456, + publishedDate: 1678098240000, + meta: { + feedName: "Winter Promo", + surface: + "mobileapp://com.adobe.sampleApp/feed/promos" + }, + content: { + imageUrl: + "https://target.jasonwaters.dev/img/twitter.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: + "Posting on social media helps us spread the word.", + title: "Thanks for sharing!" + }, + contentType: "application/json" + }, + id: "cfcb1af7-7bc2-45b2-a86a-0aa93fe69ce7" + }, + id: "cfcb1af7-7bc2-45b2-a86a-0aa93fe69ce7" + } + ] + } + ] + } + } + ], + scope: "web://target.jasonwaters.dev/aep.html" + }, + { + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: + "eyJtZXNzYWdlRXhlY3V0aW9uIjp7Im1lc3NhZ2VFeGVjdXRpb25JRCI6Ik5BIiwibWVzc2FnZUlEIjoiMDJjNzdlYTgtN2MwZS00ZDMzLTgwOTAtNGE1YmZkM2Q3NTAzIiwibWVzc2FnZVR5cGUiOiJtYXJrZXRpbmciLCJjYW1wYWlnbklEIjoiMzlhZThkNGItYjU1ZS00M2RjLWExNDMtNzdmNTAxOTViNDg3IiwiY2FtcGFpZ25WZXJzaW9uSUQiOiJiZDg1ZDllOC0yMDM3LTQyMmYtYjZkMi0zOTU3YzkwNTU5ZDMiLCJjYW1wYWlnbkFjdGlvbklEIjoiYjQ3ZmRlOGItNTdjMS00YmJlLWFlMjItNjRkNWI3ODJkMTgzIiwibWVzc2FnZVB1YmxpY2F0aW9uSUQiOiJhZTUyY2VkOC0yMDBjLTQ5N2UtODc4Ny1lZjljZmMxNzgyMTUifSwibWVzc2FnZVByb2ZpbGUiOnsiY2hhbm5lbCI6eyJfaWQiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbHMvd2ViIiwiX3R5cGUiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbC10eXBlcy93ZWIifSwibWVzc2FnZVByb2ZpbGVJRCI6ImY1Y2Q5OTk1LTZiNDQtNDIyMS05YWI3LTViNTMzOGQ1ZjE5MyJ9fQ==" + }, + strategies: [ + { + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" + } + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" + }, + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" + }, + id: "d1f7d411-a549-47bc-a4d8-c8e638b0a46b", + items: [ + { + id: "f47638a0-b785-4f56-afa6-c24e714b8ff4", + schema: "https://ns.adobe.com/personalization/ruleset-item", + data: { + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [ + { + definition: { + conditions: [ + { + definition: { + key: "action", + matcher: "eq", + values: ["deposit-funds"] + }, + type: "matcher" + }, + { + definition: { + events: [ + { + type: + "decisioning.propositionDisplay", + id: + "d1f7d411-a549-47bc-a4d8-c8e638b0a46b" + } + ], + matcher: "ge", + value: 1 + }, + type: "historical" + } + ], + logic: "or" + }, + type: "group" + } + ], + logic: "and" + }, + type: "group" + }, + consequences: [ + { + type: "schema", + detail: { + schema: + "https://ns.adobe.com/personalization/message/feed-item", + data: { + expiryDate: 1712190456, + publishedDate: 1678184640000, + meta: { + feedName: "Winter Promo", + surface: + "mobileapp://com.adobe.sampleApp/feed/promos" + }, + content: { + imageUrl: + "https://media.giphy.com/media/ADgfsbHcS62Jy/giphy.gif", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "Now you're ready to earn!", + title: "Funds deposited!" + }, + contentType: "application/json" + }, + id: "0263e171-fa32-4c7a-9611-36b28137a81d" + }, + id: "0263e171-fa32-4c7a-9611-36b28137a81d" + } + ] + } + ] + } + } + ], + scope: "web://target.jasonwaters.dev/aep.html" + } + ], + type: "personalization:decisions", + eventIndex: 0 + }, + { + payload: [ + { + scope: "Target", + hint: "35", + ttlSeconds: 1800 + }, + { + scope: "AAM", + hint: "9", + ttlSeconds: 1800 + }, + { + scope: "EdgeNetwork", + hint: "or2", + ttlSeconds: 1800 + } + ], + type: "locationHint:result" + }, + { + payload: [ + { + key: "kndctr_4DA0571C5FDC4BF70A495FC2_AdobeOrg_cluster", + value: "or2", + maxAge: 1800, + attrs: { + SameSite: "None" + } + } + ], + type: "state:store" + } + ] + }; + async function applyRuleEngineResponse() { + window.alloy("applyResponse", { + renderDecisions: true, + decisionContext: { + color: "pink", + action: "lipstick" + }, + responseBody: mockResponse + }); + } + document + .getElementById("social-media-share") + .addEventListener("click", evt => { + evt.stopImmediatePropagation(); + evt.preventDefault(); + + window.alloy("evaluateRulesets", { + action: "share-social-media" + }); + }); + + document.getElementById("deposit-funds").addEventListener("click", evt => { + evt.stopImmediatePropagation(); + evt.preventDefault(); + window.alloy("evaluateRulesets", { + action: "deposit-funds" + }); + }); + + document.getElementById("reset").addEventListener("click", evt => { + evt.stopImmediatePropagation(); + evt.preventDefault(); + localStorage.clear(); + location.reload(); + }); + + let isMounted = true; + const fetchData = async () => { + try { + const result = await window.alloy("subscribeMessageFeed", { + surface: "web://target.jasonwaters.dev/aep.html", + callback: ({ items = [], clicked, rendered }) => { + if (isMounted) { + setMessageFeedItems(items); + } + clicked(items); + rendered(items); + } + }); + if (isMounted) { + await applyRuleEngineResponse(result); + } + } catch (error) { + console.error("Error:", error); + } + }; + fetchData(); + return () => { + isMounted = false; + }; + }, []); + + return ( +
    + +
    + + + +
    +
    +

    Message Feed

    +
    + {messageFeedItems.map((item, index) => ( +
    +

    {item.title}

    +

    + {item.imageUrl && Item Image} +

    +

    {item.body}

    +

    Published: {prettyDate(item.publishedDate)}

    +

    Qualified: {prettyDate(item.qualifiedDate)}

    +

    Displayed: {prettyDate(item.displayedDate)}

    +
    + ))} +
    +
    +
    + ); +} From d6fdfcab2bb846b07eeb58bd7303bbd48c99ef61 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Thu, 7 Sep 2023 13:32:18 -0600 Subject: [PATCH 44/66] support "Send data to platform" trigger (#1030) --- .../InAppMessagesDemo/InAppMessages.js | 56 ++++++++++++++----- src/components/DecisioningEngine/constants.js | 26 +++++++++ src/components/DecisioningEngine/index.js | 11 +++- .../createPersonalizationDetails.js | 8 ++- .../DecisioningEngine/constants.spec.js | 11 ++++ .../createPersonalizationDetails.spec.js | 24 ++++++-- 6 files changed, 115 insertions(+), 21 deletions(-) create mode 100644 src/components/DecisioningEngine/constants.js create mode 100644 test/unit/specs/components/DecisioningEngine/constants.spec.js diff --git a/sandbox/src/components/InAppMessagesDemo/InAppMessages.js b/sandbox/src/components/InAppMessagesDemo/InAppMessages.js index 6cadd13d5..cb2db2301 100644 --- a/sandbox/src/components/InAppMessagesDemo/InAppMessages.js +++ b/sandbox/src/components/InAppMessagesDemo/InAppMessages.js @@ -1,12 +1,51 @@ +/* eslint-disable no-bitwise */ import React, { useState, useEffect } from "react"; import ContentSecurityPolicy from "../ContentSecurityPolicy"; import "./InAppMessagesStyle.css"; +const configKey = "cjmProdNld2"; + +const config = { + cjmProdNld2: { + datastreamId: "7a19c434-6648-48d3-948f-ba0258505d98", + surface: "mobileapp://com.adobe.iamTutorialiOS", + decisionContext: { + "~type": "com.adobe.eventType.generic.track", + "~source": "com.adobe.eventSource.requestContent", + state: "", + "~state.com.adobe.module.lifecycle/lifecyclecontextdata.dayofweek": 1 + }, + activeCampaigns: [ + "https://experience.adobe.com/#/@cjmprodnld2/sname:prod/journey-optimizer/campaigns/summary/59bfdc09-03b9-4cd5-9ab8-5c2a045b0b2e" + ] + }, + aemonacpprodcampaign: { + datastreamId: "8cefc5ca-1c2a-479f-88f2-3d42cc302514", + surface: "mobileapp://com.adobe.aguaAppIos", + decisionContext: {}, + activeCampaigns: [ + "https://experience.adobe.com/#/@aemonacpprodcampaign/sname:prod/journey-optimizer/campaigns/summary/8bb52c05-d381-4d8b-a67a-95f345776322" + ] + } +}; + +const { datastreamId, surface, decisionContext } = config[configKey]; + +const uuidv4 = () => { + return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => + ( + c ^ + (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4))) + ).toString(16) + ); +}; + export default function DecisionEngine() { const [realResponse, setRealResponse] = useState(null); + const getInAppPayload = async payload => { const res = await fetch( - `https://edge.adobedc.net/ee/or2/v1/interact?configId=7a19c434-6648-48d3-948f-ba0258505d98&requestId=520353b2-dc0d-428c-9e0d-138fc6cbec4e`, + `https://edge.adobedc.net/ee/or2/v1/interact?configId=${datastreamId}&requestId=${uuidv4()}`, { method: "POST", body: JSON.stringify(payload) @@ -22,7 +61,7 @@ export default function DecisionEngine() { { query: { personalization: { - surfaces: ["mobileapp://com.adobe.iamTutorialiOS"] + surfaces: [surface] } }, xdm: { @@ -44,21 +83,10 @@ export default function DecisionEngine() { const renderDecisions = e => { e.stopPropagation(); e.preventDefault(); - window.alloy("evaluateRulesets", { - "~type": "com.adobe.eventType.generic.track", - "~source": "com.adobe.eventSource.requestContent", - state: "", - "~state.com.adobe.module.lifecycle/lifecyclecontextdata.dayofweek": 1 - }); window.alloy("applyResponse", { renderDecisions: true, - decisionContext: { - "~type": "com.adobe.eventType.generic.track", - "~source": "com.adobe.eventSource.requestContent", - state: "", - "~state.com.adobe.module.lifecycle/lifecyclecontextdata.dayofweek": 1 - }, + decisionContext, responseBody: realResponse }); }; diff --git a/src/components/DecisioningEngine/constants.js b/src/components/DecisioningEngine/constants.js new file mode 100644 index 000000000..924ed954b --- /dev/null +++ b/src/components/DecisioningEngine/constants.js @@ -0,0 +1,26 @@ +/* +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 CONTEXT_KEY = { + TYPE: "~type", + SOURCE: "~source" +}; + +export const MOBILE_EVENT_TYPE = { + LIFECYCLE: "com.adobe.eventType.lifecycle", + TRACK: "com.adobe.eventType.generic.track", + EDGE: "com.adobe.eventType.edge" +}; + +export const MOBILE_EVENT_SOURCE = { + LAUNCH: "com.adobe.eventSource.applicationLaunch", + REQUEST: "com.adobe.eventSource.requestContent" +}; diff --git a/src/components/DecisioningEngine/index.js b/src/components/DecisioningEngine/index.js index ee7739dcb..86b890bf9 100644 --- a/src/components/DecisioningEngine/index.js +++ b/src/components/DecisioningEngine/index.js @@ -17,6 +17,11 @@ import createEventRegistry from "./createEventRegistry"; import createContextProvider from "./createContextProvider"; import createSubscribeRulesetItems from "./createSubscribeRulesetItems"; import { ensureSchemaBasedRulesetConsequences } from "./utils"; +import { + CONTEXT_KEY, + MOBILE_EVENT_SOURCE, + MOBILE_EVENT_TYPE +} from "./constants"; const createDecisioningEngine = ({ config, createNamespacedStorage }) => { const { orgId } = config; @@ -52,7 +57,11 @@ const createDecisioningEngine = ({ config, createNamespacedStorage }) => { decisionProvider, applyResponse, event, - decisionContext: contextProvider.getContext(decisionContext) + decisionContext: contextProvider.getContext({ + [CONTEXT_KEY.TYPE]: MOBILE_EVENT_TYPE.EDGE, + [CONTEXT_KEY.SOURCE]: MOBILE_EVENT_SOURCE.REQUEST, + ...decisionContext + }) }) ); diff --git a/src/components/Personalization/createPersonalizationDetails.js b/src/components/Personalization/createPersonalizationDetails.js index 0e3c1bcb2..6f8901ed9 100644 --- a/src/components/Personalization/createPersonalizationDetails.js +++ b/src/components/Personalization/createPersonalizationDetails.js @@ -19,7 +19,9 @@ import { HTML_CONTENT_ITEM, MESSAGE_IN_APP, JSON_CONTENT_ITEM, - REDIRECT_ITEM + REDIRECT_ITEM, + RULESET_ITEM, + MESSAGE_FEED_ITEM } from "./constants/schema"; const addPageWideScope = scopes => { @@ -88,7 +90,9 @@ export default ({ HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, - MESSAGE_IN_APP + RULESET_ITEM, + MESSAGE_IN_APP, + MESSAGE_FEED_ITEM ]; if (includes(scopes, PAGE_WIDE_SCOPE)) { diff --git a/test/unit/specs/components/DecisioningEngine/constants.spec.js b/test/unit/specs/components/DecisioningEngine/constants.spec.js new file mode 100644 index 000000000..26ef92385 --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/constants.spec.js @@ -0,0 +1,11 @@ +/* +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. +*/ diff --git a/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js b/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js index b2343c814..35c8b0e4b 100644 --- a/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js +++ b/test/unit/specs/components/Personalization/createPersonalizationDetails.spec.js @@ -18,7 +18,9 @@ import { HTML_CONTENT_ITEM, MESSAGE_IN_APP, JSON_CONTENT_ITEM, - REDIRECT_ITEM + REDIRECT_ITEM, + RULESET_ITEM, + MESSAGE_FEED_ITEM } from "../../../../../src/components/Personalization/constants/schema"; describe("Personalization::createPersonalizationDetails", () => { @@ -63,7 +65,9 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, + RULESET_ITEM, MESSAGE_IN_APP, + MESSAGE_FEED_ITEM, DOM_ACTION ], decisionScopes: expectedDecisionScopes, @@ -102,7 +106,9 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, + RULESET_ITEM, MESSAGE_IN_APP, + MESSAGE_FEED_ITEM, DOM_ACTION ], decisionScopes: expectedDecisionScopes, @@ -141,7 +147,9 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, + RULESET_ITEM, MESSAGE_IN_APP, + MESSAGE_FEED_ITEM, DOM_ACTION ], decisionScopes: expectedDecisionScopes, @@ -180,7 +188,9 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, - MESSAGE_IN_APP + RULESET_ITEM, + MESSAGE_IN_APP, + MESSAGE_FEED_ITEM ], decisionScopes: expectedDecisionScopes, surfaces: [] @@ -220,7 +230,9 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, - MESSAGE_IN_APP + RULESET_ITEM, + MESSAGE_IN_APP, + MESSAGE_FEED_ITEM ], decisionScopes: expectedDecisionScopes, surfaces: ["web://test1.com/"] @@ -262,7 +274,9 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, - MESSAGE_IN_APP + RULESET_ITEM, + MESSAGE_IN_APP, + MESSAGE_FEED_ITEM ], decisionScopes: expectedDecisionScopes, surfaces: ["web://test1.com/"] @@ -398,7 +412,9 @@ describe("Personalization::createPersonalizationDetails", () => { HTML_CONTENT_ITEM, JSON_CONTENT_ITEM, REDIRECT_ITEM, + RULESET_ITEM, MESSAGE_IN_APP, + MESSAGE_FEED_ITEM, DOM_ACTION ], decisionScopes: expectedDecisionScopes, From 211a477152653142ce3cafd50fe88bc236dabfa0 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Mon, 11 Sep 2023 10:38:03 -0600 Subject: [PATCH 45/66] [CJM-46521] Send interact events when in-app messages are clicked (#1031) * Send interact events when in-app messages are clicked * update sandbox --- .../InAppMessagesDemo/InAppMessages.js | 94 +- .../components/MessageFeedDemo/MessageFeed.js | 845 +++++++++--------- .../inAppMessageConsequenceAdapter.js | 3 +- .../Personalization/constants/contentType.js | 12 + .../Personalization/createCollect.js | 4 +- .../Personalization/createOnClickHandler.js | 2 +- .../createSubscribeMessageFeed.js | 4 + src/components/Personalization/event.js | 9 +- .../actions/displayIframeContent.js | 82 +- .../in-app-message-actions/utils.js | 37 + .../inAppMessageConsequenceAdapter.spec.js | 3 +- .../createConsequenceAdapter.spec.js | 3 +- .../Personalization/createCollect.spec.js | 3 +- .../actions/displayBanner.spec.js | 3 +- .../actions/displayIframeContent.spec.js | 81 +- .../actions/displayModal.spec.js | 3 +- .../responsesMock/eventResponses.js | 1 + 17 files changed, 642 insertions(+), 547 deletions(-) create mode 100644 src/components/Personalization/constants/contentType.js diff --git a/sandbox/src/components/InAppMessagesDemo/InAppMessages.js b/sandbox/src/components/InAppMessagesDemo/InAppMessages.js index cb2db2301..1a7149629 100644 --- a/sandbox/src/components/InAppMessagesDemo/InAppMessages.js +++ b/sandbox/src/components/InAppMessagesDemo/InAppMessages.js @@ -1,5 +1,5 @@ /* eslint-disable no-bitwise */ -import React, { useState, useEffect } from "react"; +import React from "react"; import ContentSecurityPolicy from "../ContentSecurityPolicy"; import "./InAppMessagesStyle.css"; @@ -40,62 +40,64 @@ const uuidv4 = () => { ); }; -export default function DecisionEngine() { - const [realResponse, setRealResponse] = useState(null); +const getInAppPayload = async payload => { + const res = await fetch( + `https://edge.adobedc.net/ee/or2/v1/interact?configId=${datastreamId}&requestId=${uuidv4()}`, + { + method: "POST", + body: JSON.stringify(payload) + } + ); + return res.json(); +}; - const getInAppPayload = async payload => { - const res = await fetch( - `https://edge.adobedc.net/ee/or2/v1/interact?configId=${datastreamId}&requestId=${uuidv4()}`, +const fetchMobilePayload = () => + getInAppPayload({ + events: [ { - method: "POST", - body: JSON.stringify(payload) + query: { + personalization: { + surfaces: [surface] + } + }, + xdm: { + timestamp: new Date().toISOString(), + implementationDetails: { + name: "https://ns.adobe.com/experience/mobilesdk/ios", + version: "3.7.4+1.5.0", + environment: "app" + } + } } - ); - return res.json(); - }; + ] + }); - useEffect(() => { - const fetchInAppPayload = async () => { - const response = await getInAppPayload({ - events: [ - { - query: { - personalization: { - surfaces: [surface] - } - }, - xdm: { - timestamp: new Date().toISOString(), - implementationDetails: { - name: "https://ns.adobe.com/experience/mobilesdk/ios", - version: "3.7.4+1.5.0", - environment: "app" - } - } - } - ] +export default function InAppMessages() { + const renderDecisions = () => { + window + .alloy("subscribeRulesetItems", { + surface, + callback: result => { + console.log("subscribeRulesetItems", result); + } + }) + .then(fetchMobilePayload) + .then(response => { + window.alloy("applyResponse", { + renderDecisions: true, + decisionContext, + responseBody: response + }); }); - setRealResponse(response); - }; - fetchInAppPayload(); - }, []); - - const renderDecisions = e => { - e.stopPropagation(); - e.preventDefault(); - - window.alloy("applyResponse", { - renderDecisions: true, - decisionContext, - responseBody: realResponse - }); }; return (

    In App Messages For Web

    - +
    ); } diff --git a/sandbox/src/components/MessageFeedDemo/MessageFeed.js b/sandbox/src/components/MessageFeedDemo/MessageFeed.js index e9f2e1926..a4c80c3a5 100644 --- a/sandbox/src/components/MessageFeedDemo/MessageFeed.js +++ b/sandbox/src/components/MessageFeedDemo/MessageFeed.js @@ -2,491 +2,480 @@ import React, { useEffect, useState } from "react"; import ContentSecurityPolicy from "../ContentSecurityPolicy"; import "./MessageFeed.css"; -export default function MessageFeed() { - const [messageFeedItems, setMessageFeedItems] = useState([]); - - const prettyDate = value => { - let output = ""; - - if (typeof value !== "undefined") { - const now = new Date().getTime(); - const seconds = Math.floor(now / 1000); - const oldTimestamp = Math.floor(value / 1000); - const difference = seconds - oldTimestamp; - - if (difference < 60) { - output = `${difference} second(s) ago`; - } else if (difference < 3600) { - output = `${Math.floor(difference / 60)} min ago`; - } else if (difference < 86400) { - output = `${Math.floor(difference / 3600)} hour(s) ago`; - } else if (difference < 2620800) { - output = `${Math.floor(difference / 86400)} day(s) ago`; - } else if (difference < 31449600) { - output = `${Math.floor(difference / 2620800)} month(s) ago`; - } else { - output = `${Math.floor(difference / 31449600)} year(s) ago`; - } - } - - return output; - }; - - useEffect(() => { - /* eslint-disable */ - const mockResponse = { - requestId: "5a38a9ef-67d7-4f66-8977-c4dc0e0967b6", - handle: [ +const mockResponse = { + requestId: "5a38a9ef-67d7-4f66-8977-c4dc0e0967b6", + handle: [ + { + payload: [ { - payload: [ - { - id: "11893040138696185741718511332124641876", - namespace: { - code: "ECID" - } - } - ], - type: "identity:result" - }, + id: "11893040138696185741718511332124641876", + namespace: { + code: "ECID" + } + } + ], + type: "identity:result" + }, + { + payload: [ { - payload: [ + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: + "eyJtZXNzYWdlRXhlY3V0aW9uIjp7Im1lc3NhZ2VFeGVjdXRpb25JRCI6Ik5BIiwibWVzc2FnZUlEIjoiMDJjNzdlYTgtN2MwZS00ZDMzLTgwOTAtNGE1YmZkM2Q3NTAzIiwibWVzc2FnZVR5cGUiOiJtYXJrZXRpbmciLCJjYW1wYWlnbklEIjoiMzlhZThkNGItYjU1ZS00M2RjLWExNDMtNzdmNTAxOTViNDg3IiwiY2FtcGFpZ25WZXJzaW9uSUQiOiJiZDg1ZDllOC0yMDM3LTQyMmYtYjZkMi0zOTU3YzkwNTU5ZDMiLCJjYW1wYWlnbkFjdGlvbklEIjoiYjQ3ZmRlOGItNTdjMS00YmJlLWFlMjItNjRkNWI3ODJkMTgzIiwibWVzc2FnZVB1YmxpY2F0aW9uSUQiOiJhZTUyY2VkOC0yMDBjLTQ5N2UtODc4Ny1lZjljZmMxNzgyMTUifSwibWVzc2FnZVByb2ZpbGUiOnsiY2hhbm5lbCI6eyJfaWQiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbHMvd2ViIiwiX3R5cGUiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbC10eXBlcy93ZWIifSwibWVzc2FnZVByb2ZpbGVJRCI6ImY1Y2Q5OTk1LTZiNDQtNDIyMS05YWI3LTViNTMzOGQ1ZjE5MyJ9fQ==" + }, + strategies: [ + { + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" + } + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" + }, + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" + }, + id: "1a3d874f-39ee-4310-bfa9-6559a10041a4", + items: [ { - scopeDetails: { - decisionProvider: "AJO", - characteristics: { - eventToken: - "eyJtZXNzYWdlRXhlY3V0aW9uIjp7Im1lc3NhZ2VFeGVjdXRpb25JRCI6Ik5BIiwibWVzc2FnZUlEIjoiMDJjNzdlYTgtN2MwZS00ZDMzLTgwOTAtNGE1YmZkM2Q3NTAzIiwibWVzc2FnZVR5cGUiOiJtYXJrZXRpbmciLCJjYW1wYWlnbklEIjoiMzlhZThkNGItYjU1ZS00M2RjLWExNDMtNzdmNTAxOTViNDg3IiwiY2FtcGFpZ25WZXJzaW9uSUQiOiJiZDg1ZDllOC0yMDM3LTQyMmYtYjZkMi0zOTU3YzkwNTU5ZDMiLCJjYW1wYWlnbkFjdGlvbklEIjoiYjQ3ZmRlOGItNTdjMS00YmJlLWFlMjItNjRkNWI3ODJkMTgzIiwibWVzc2FnZVB1YmxpY2F0aW9uSUQiOiJhZTUyY2VkOC0yMDBjLTQ5N2UtODc4Ny1lZjljZmMxNzgyMTUifSwibWVzc2FnZVByb2ZpbGUiOnsiY2hhbm5lbCI6eyJfaWQiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbHMvd2ViIiwiX3R5cGUiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbC10eXBlcy93ZWIifSwibWVzc2FnZVByb2ZpbGVJRCI6ImY1Y2Q5OTk1LTZiNDQtNDIyMS05YWI3LTViNTMzOGQ1ZjE5MyJ9fQ==" - }, - strategies: [ + id: "9d9c6e62-a8e5-419b-abe3-429950c27425", + schema: "https://ns.adobe.com/personalization/ruleset-item", + data: { + version: 1, + rules: [ { - strategyID: "3VQe3oIqiYq2RAsYzmDTSf", - treatmentID: "yu7rkogezumca7i0i44v" - } - ], - activity: { - id: - "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" - }, - correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" - }, - id: "1a3d874f-39ee-4310-bfa9-6559a10041a4", - items: [ - { - id: "9d9c6e62-a8e5-419b-abe3-429950c27425", - schema: "https://ns.adobe.com/personalization/ruleset-item", - data: { - version: 1, - rules: [ + condition: { + definition: { + conditions: [ + { + definition: { + conditions: [ + { + definition: { + key: "events", + matcher: "ex" + }, + type: "matcher" + } + ], + logic: "and" + }, + type: "group" + } + ], + logic: "and" + }, + type: "group" + }, + consequences: [ { - condition: { - definition: { - conditions: [ - { - definition: { - conditions: [ - { - definition: { - key: "events", - matcher: "ex" - }, - type: "matcher" - } - ], - logic: "and" - }, - type: "group" - } - ], - logic: "and" + type: "schema", + detail: { + schema: + "https://ns.adobe.com/personalization/message/feed-item", + data: { + expiryDate: 1712190456, + publishedDate: 1677752640000, + meta: { + feedName: "Winter Promo", + surface: + "mobileapp://com.adobe.sampleApp/feed/promos" + }, + content: { + imageUrl: + "https://target.jasonwaters.dev/img/lumon.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "a handshake is available upon request.", + title: "Welcome to Lumon!" + }, + contentType: "application/json" }, - type: "group" + id: "a48ca420-faea-467e-989a-5d179d9f562d" }, - consequences: [ - { - type: "schema", - detail: { - schema: - "https://ns.adobe.com/personalization/message/feed-item", - data: { - expiryDate: 1712190456, - publishedDate: 1677752640000, - meta: { - feedName: "Winter Promo", - surface: - "mobileapp://com.adobe.sampleApp/feed/promos" - }, - content: { - imageUrl: - "https://target.jasonwaters.dev/img/lumon.png", - actionTitle: "Shop the sale!", - actionUrl: "https://luma.com/sale", - body: - "a handshake is available upon request.", - title: "Welcome to Lumon!" - }, - contentType: "application/json" - }, - id: "a48ca420-faea-467e-989a-5d179d9f562d" + id: "a48ca420-faea-467e-989a-5d179d9f562d" + }, + { + type: "schema", + detail: { + schema: + "https://ns.adobe.com/personalization/message/feed-item", + data: { + expiryDate: 1712190456, + publishedDate: 1677839040000, + meta: { + feedName: "Winter Promo", + surface: + "mobileapp://com.adobe.sampleApp/feed/promos" }, - id: "a48ca420-faea-467e-989a-5d179d9f562d" - }, - { - type: "schema", - detail: { - schema: - "https://ns.adobe.com/personalization/message/feed-item", - data: { - expiryDate: 1712190456, - publishedDate: 1677839040000, - meta: { - feedName: "Winter Promo", - surface: - "mobileapp://com.adobe.sampleApp/feed/promos" - }, - content: { - imageUrl: - "https://media.giphy.com/media/l0Ex3vQtX5VX2YtAQ/giphy.gif", - actionTitle: "Shop the sale!", - actionUrl: "https://luma.com/sale", - body: - "Great job, you completed your profile.", - title: "Achievement Unlocked!" - }, - contentType: "application/json" - }, - id: "b7173290-588f-40c6-a05c-43ed5ec08b28" + content: { + imageUrl: + "https://media.giphy.com/media/l0Ex3vQtX5VX2YtAQ/giphy.gif", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "Great job, you completed your profile.", + title: "Achievement Unlocked!" }, - id: "b7173290-588f-40c6-a05c-43ed5ec08b28" - } - ] + contentType: "application/json" + }, + id: "b7173290-588f-40c6-a05c-43ed5ec08b28" + }, + id: "b7173290-588f-40c6-a05c-43ed5ec08b28" } ] } - } - ], - scope: "web://target.jasonwaters.dev/aep.html" + ] + } + } + ], + scope: "web://target.jasonwaters.dev/aep.html" + }, + { + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: + "eyJtZXNzYWdlRXhlY3V0aW9uIjp7Im1lc3NhZ2VFeGVjdXRpb25JRCI6Ik5BIiwibWVzc2FnZUlEIjoiMDJjNzdlYTgtN2MwZS00ZDMzLTgwOTAtNGE1YmZkM2Q3NTAzIiwibWVzc2FnZVR5cGUiOiJtYXJrZXRpbmciLCJjYW1wYWlnbklEIjoiMzlhZThkNGItYjU1ZS00M2RjLWExNDMtNzdmNTAxOTViNDg3IiwiY2FtcGFpZ25WZXJzaW9uSUQiOiJiZDg1ZDllOC0yMDM3LTQyMmYtYjZkMi0zOTU3YzkwNTU5ZDMiLCJjYW1wYWlnbkFjdGlvbklEIjoiYjQ3ZmRlOGItNTdjMS00YmJlLWFlMjItNjRkNWI3ODJkMTgzIiwibWVzc2FnZVB1YmxpY2F0aW9uSUQiOiJhZTUyY2VkOC0yMDBjLTQ5N2UtODc4Ny1lZjljZmMxNzgyMTUifSwibWVzc2FnZVByb2ZpbGUiOnsiY2hhbm5lbCI6eyJfaWQiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbHMvd2ViIiwiX3R5cGUiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbC10eXBlcy93ZWIifSwibWVzc2FnZVByb2ZpbGVJRCI6ImY1Y2Q5OTk1LTZiNDQtNDIyMS05YWI3LTViNTMzOGQ1ZjE5MyJ9fQ==" + }, + strategies: [ + { + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" + } + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" }, + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" + }, + id: "1ae11bc5-96dc-41c7-8f71-157c57a5290e", + items: [ { - scopeDetails: { - decisionProvider: "AJO", - characteristics: { - eventToken: - "eyJtZXNzYWdlRXhlY3V0aW9uIjp7Im1lc3NhZ2VFeGVjdXRpb25JRCI6Ik5BIiwibWVzc2FnZUlEIjoiMDJjNzdlYTgtN2MwZS00ZDMzLTgwOTAtNGE1YmZkM2Q3NTAzIiwibWVzc2FnZVR5cGUiOiJtYXJrZXRpbmciLCJjYW1wYWlnbklEIjoiMzlhZThkNGItYjU1ZS00M2RjLWExNDMtNzdmNTAxOTViNDg3IiwiY2FtcGFpZ25WZXJzaW9uSUQiOiJiZDg1ZDllOC0yMDM3LTQyMmYtYjZkMi0zOTU3YzkwNTU5ZDMiLCJjYW1wYWlnbkFjdGlvbklEIjoiYjQ3ZmRlOGItNTdjMS00YmJlLWFlMjItNjRkNWI3ODJkMTgzIiwibWVzc2FnZVB1YmxpY2F0aW9uSUQiOiJhZTUyY2VkOC0yMDBjLTQ5N2UtODc4Ny1lZjljZmMxNzgyMTUifSwibWVzc2FnZVByb2ZpbGUiOnsiY2hhbm5lbCI6eyJfaWQiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbHMvd2ViIiwiX3R5cGUiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbC10eXBlcy93ZWIifSwibWVzc2FnZVByb2ZpbGVJRCI6ImY1Y2Q5OTk1LTZiNDQtNDIyMS05YWI3LTViNTMzOGQ1ZjE5MyJ9fQ==" - }, - strategies: [ + id: "e0575812-74e5-46b9-a4f2-9541dfaec2d0", + schema: "https://ns.adobe.com/personalization/ruleset-item", + data: { + version: 1, + rules: [ { - strategyID: "3VQe3oIqiYq2RAsYzmDTSf", - treatmentID: "yu7rkogezumca7i0i44v" - } - ], - activity: { - id: - "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" - }, - correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" - }, - id: "1ae11bc5-96dc-41c7-8f71-157c57a5290e", - items: [ - { - id: "e0575812-74e5-46b9-a4f2-9541dfaec2d0", - schema: "https://ns.adobe.com/personalization/ruleset-item", - data: { - version: 1, - rules: [ - { - condition: { - definition: { - conditions: [ - { - definition: { - conditions: [ - { - definition: { - key: "action", - matcher: "eq", - values: ["share-social-media"] - }, - type: "matcher" - }, - { - definition: { - events: [ - { - type: - "decisioning.propositionDisplay", - id: - "1ae11bc5-96dc-41c7-8f71-157c57a5290e" - } - ], - matcher: "ge", - value: 1 - }, - type: "historical" - } - ], - logic: "or" - }, - type: "group" - } - ], - logic: "and" - }, - type: "group" - }, - consequences: [ + condition: { + definition: { + conditions: [ { - type: "schema", - detail: { - schema: - "https://ns.adobe.com/personalization/message/feed-item", - data: { - expiryDate: 1712190456, - publishedDate: 1678098240000, - meta: { - feedName: "Winter Promo", - surface: - "mobileapp://com.adobe.sampleApp/feed/promos" - }, - content: { - imageUrl: - "https://target.jasonwaters.dev/img/twitter.png", - actionTitle: "Shop the sale!", - actionUrl: "https://luma.com/sale", - body: - "Posting on social media helps us spread the word.", - title: "Thanks for sharing!" + definition: { + conditions: [ + { + definition: { + key: "action", + matcher: "eq", + values: ["share-social-media"] + }, + type: "matcher" }, - contentType: "application/json" - }, - id: "cfcb1af7-7bc2-45b2-a86a-0aa93fe69ce7" + { + definition: { + events: [ + { + type: "decisioning.propositionDisplay", + id: + "1ae11bc5-96dc-41c7-8f71-157c57a5290e" + } + ], + matcher: "ge", + value: 1 + }, + type: "historical" + } + ], + logic: "or" }, - id: "cfcb1af7-7bc2-45b2-a86a-0aa93fe69ce7" + type: "group" } - ] + ], + logic: "and" + }, + type: "group" + }, + consequences: [ + { + type: "schema", + detail: { + schema: + "https://ns.adobe.com/personalization/message/feed-item", + data: { + expiryDate: 1712190456, + publishedDate: 1678098240000, + meta: { + feedName: "Winter Promo", + surface: + "mobileapp://com.adobe.sampleApp/feed/promos" + }, + content: { + imageUrl: + "https://target.jasonwaters.dev/img/twitter.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: + "Posting on social media helps us spread the word.", + title: "Thanks for sharing!" + }, + contentType: "application/json" + }, + id: "cfcb1af7-7bc2-45b2-a86a-0aa93fe69ce7" + }, + id: "cfcb1af7-7bc2-45b2-a86a-0aa93fe69ce7" } ] } - } - ], - scope: "web://target.jasonwaters.dev/aep.html" + ] + } + } + ], + scope: "web://target.jasonwaters.dev/aep.html" + }, + { + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: + "eyJtZXNzYWdlRXhlY3V0aW9uIjp7Im1lc3NhZ2VFeGVjdXRpb25JRCI6Ik5BIiwibWVzc2FnZUlEIjoiMDJjNzdlYTgtN2MwZS00ZDMzLTgwOTAtNGE1YmZkM2Q3NTAzIiwibWVzc2FnZVR5cGUiOiJtYXJrZXRpbmciLCJjYW1wYWlnbklEIjoiMzlhZThkNGItYjU1ZS00M2RjLWExNDMtNzdmNTAxOTViNDg3IiwiY2FtcGFpZ25WZXJzaW9uSUQiOiJiZDg1ZDllOC0yMDM3LTQyMmYtYjZkMi0zOTU3YzkwNTU5ZDMiLCJjYW1wYWlnbkFjdGlvbklEIjoiYjQ3ZmRlOGItNTdjMS00YmJlLWFlMjItNjRkNWI3ODJkMTgzIiwibWVzc2FnZVB1YmxpY2F0aW9uSUQiOiJhZTUyY2VkOC0yMDBjLTQ5N2UtODc4Ny1lZjljZmMxNzgyMTUifSwibWVzc2FnZVByb2ZpbGUiOnsiY2hhbm5lbCI6eyJfaWQiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbHMvd2ViIiwiX3R5cGUiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbC10eXBlcy93ZWIifSwibWVzc2FnZVByb2ZpbGVJRCI6ImY1Y2Q5OTk1LTZiNDQtNDIyMS05YWI3LTViNTMzOGQ1ZjE5MyJ9fQ==" + }, + strategies: [ + { + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" + } + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" }, + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" + }, + id: "d1f7d411-a549-47bc-a4d8-c8e638b0a46b", + items: [ { - scopeDetails: { - decisionProvider: "AJO", - characteristics: { - eventToken: - "eyJtZXNzYWdlRXhlY3V0aW9uIjp7Im1lc3NhZ2VFeGVjdXRpb25JRCI6Ik5BIiwibWVzc2FnZUlEIjoiMDJjNzdlYTgtN2MwZS00ZDMzLTgwOTAtNGE1YmZkM2Q3NTAzIiwibWVzc2FnZVR5cGUiOiJtYXJrZXRpbmciLCJjYW1wYWlnbklEIjoiMzlhZThkNGItYjU1ZS00M2RjLWExNDMtNzdmNTAxOTViNDg3IiwiY2FtcGFpZ25WZXJzaW9uSUQiOiJiZDg1ZDllOC0yMDM3LTQyMmYtYjZkMi0zOTU3YzkwNTU5ZDMiLCJjYW1wYWlnbkFjdGlvbklEIjoiYjQ3ZmRlOGItNTdjMS00YmJlLWFlMjItNjRkNWI3ODJkMTgzIiwibWVzc2FnZVB1YmxpY2F0aW9uSUQiOiJhZTUyY2VkOC0yMDBjLTQ5N2UtODc4Ny1lZjljZmMxNzgyMTUifSwibWVzc2FnZVByb2ZpbGUiOnsiY2hhbm5lbCI6eyJfaWQiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbHMvd2ViIiwiX3R5cGUiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbC10eXBlcy93ZWIifSwibWVzc2FnZVByb2ZpbGVJRCI6ImY1Y2Q5OTk1LTZiNDQtNDIyMS05YWI3LTViNTMzOGQ1ZjE5MyJ9fQ==" - }, - strategies: [ + id: "f47638a0-b785-4f56-afa6-c24e714b8ff4", + schema: "https://ns.adobe.com/personalization/ruleset-item", + data: { + version: 1, + rules: [ { - strategyID: "3VQe3oIqiYq2RAsYzmDTSf", - treatmentID: "yu7rkogezumca7i0i44v" - } - ], - activity: { - id: - "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" - }, - correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" - }, - id: "d1f7d411-a549-47bc-a4d8-c8e638b0a46b", - items: [ - { - id: "f47638a0-b785-4f56-afa6-c24e714b8ff4", - schema: "https://ns.adobe.com/personalization/ruleset-item", - data: { - version: 1, - rules: [ - { - condition: { - definition: { - conditions: [ - { - definition: { - conditions: [ - { - definition: { - key: "action", - matcher: "eq", - values: ["deposit-funds"] - }, - type: "matcher" - }, - { - definition: { - events: [ - { - type: - "decisioning.propositionDisplay", - id: - "d1f7d411-a549-47bc-a4d8-c8e638b0a46b" - } - ], - matcher: "ge", - value: 1 - }, - type: "historical" - } - ], - logic: "or" - }, - type: "group" - } - ], - logic: "and" - }, - type: "group" - }, - consequences: [ + condition: { + definition: { + conditions: [ { - type: "schema", - detail: { - schema: - "https://ns.adobe.com/personalization/message/feed-item", - data: { - expiryDate: 1712190456, - publishedDate: 1678184640000, - meta: { - feedName: "Winter Promo", - surface: - "mobileapp://com.adobe.sampleApp/feed/promos" - }, - content: { - imageUrl: - "https://media.giphy.com/media/ADgfsbHcS62Jy/giphy.gif", - actionTitle: "Shop the sale!", - actionUrl: "https://luma.com/sale", - body: "Now you're ready to earn!", - title: "Funds deposited!" + definition: { + conditions: [ + { + definition: { + key: "action", + matcher: "eq", + values: ["deposit-funds"] + }, + type: "matcher" }, - contentType: "application/json" - }, - id: "0263e171-fa32-4c7a-9611-36b28137a81d" + { + definition: { + events: [ + { + type: "decisioning.propositionDisplay", + id: + "d1f7d411-a549-47bc-a4d8-c8e638b0a46b" + } + ], + matcher: "ge", + value: 1 + }, + type: "historical" + } + ], + logic: "or" }, - id: "0263e171-fa32-4c7a-9611-36b28137a81d" + type: "group" } - ] + ], + logic: "and" + }, + type: "group" + }, + consequences: [ + { + type: "schema", + detail: { + schema: + "https://ns.adobe.com/personalization/message/feed-item", + data: { + expiryDate: 1712190456, + publishedDate: 1678184640000, + meta: { + feedName: "Winter Promo", + surface: + "mobileapp://com.adobe.sampleApp/feed/promos" + }, + content: { + imageUrl: + "https://media.giphy.com/media/ADgfsbHcS62Jy/giphy.gif", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "Now you're ready to earn!", + title: "Funds deposited!" + }, + contentType: "application/json" + }, + id: "0263e171-fa32-4c7a-9611-36b28137a81d" + }, + id: "0263e171-fa32-4c7a-9611-36b28137a81d" } ] } - } - ], - scope: "web://target.jasonwaters.dev/aep.html" + ] + } } ], - type: "personalization:decisions", - eventIndex: 0 + scope: "web://target.jasonwaters.dev/aep.html" + } + ], + type: "personalization:decisions", + eventIndex: 0 + }, + { + payload: [ + { + scope: "Target", + hint: "35", + ttlSeconds: 1800 }, { - payload: [ - { - scope: "Target", - hint: "35", - ttlSeconds: 1800 - }, - { - scope: "AAM", - hint: "9", - ttlSeconds: 1800 - }, - { - scope: "EdgeNetwork", - hint: "or2", - ttlSeconds: 1800 - } - ], - type: "locationHint:result" + scope: "AAM", + hint: "9", + ttlSeconds: 1800 }, { - payload: [ - { - key: "kndctr_4DA0571C5FDC4BF70A495FC2_AdobeOrg_cluster", - value: "or2", - maxAge: 1800, - attrs: { - SameSite: "None" - } - } - ], - type: "state:store" + scope: "EdgeNetwork", + hint: "or2", + ttlSeconds: 1800 } - ] - }; - async function applyRuleEngineResponse() { + ], + type: "locationHint:result" + }, + { + payload: [ + { + key: "kndctr_4DA0571C5FDC4BF70A495FC2_AdobeOrg_cluster", + value: "or2", + maxAge: 1800, + attrs: { + SameSite: "None" + } + } + ], + type: "state:store" + } + ] +}; + +const prettyDate = value => { + let output = ""; + + if (typeof value !== "undefined") { + const now = new Date().getTime(); + const seconds = Math.floor(now / 1000); + const oldTimestamp = Math.floor(value / 1000); + const difference = seconds - oldTimestamp; + + if (difference < 60) { + output = `${difference} second(s) ago`; + } else if (difference < 3600) { + output = `${Math.floor(difference / 60)} min ago`; + } else if (difference < 86400) { + output = `${Math.floor(difference / 3600)} hour(s) ago`; + } else if (difference < 2620800) { + output = `${Math.floor(difference / 86400)} day(s) ago`; + } else if (difference < 31449600) { + output = `${Math.floor(difference / 2620800)} month(s) ago`; + } else { + output = `${Math.floor(difference / 31449600)} year(s) ago`; + } + } + + return output; +}; + +export default function MessageFeed() { + const [clickHandler, setClickHandler] = useState(() => items => + console.log("items clicked!", items) + ); + + const [messageFeedItems, setMessageFeedItems] = useState([]); + + useEffect(() => { + Promise.all([ + window.alloy("subscribeRulesetItems", { + surface: "web://target.jasonwaters.dev/aep.html", + callback: result => { + console.log("subscribeRulesetItems", result); + } + }), + window.alloy("subscribeMessageFeed", { + surface: "web://target.jasonwaters.dev/aep.html", + callback: ({ items = [], rendered, clicked }) => { + console.log("setMessageFeedItems", items, clicked); + setClickHandler(() => clicked); + + setMessageFeedItems(items); + rendered(items); + } + }), window.alloy("applyResponse", { renderDecisions: true, - decisionContext: { - color: "pink", - action: "lipstick" - }, responseBody: mockResponse - }); - } - document - .getElementById("social-media-share") - .addEventListener("click", evt => { - evt.stopImmediatePropagation(); - evt.preventDefault(); - - window.alloy("evaluateRulesets", { - action: "share-social-media" - }); - }); + }) + ]); + }, []); - document.getElementById("deposit-funds").addEventListener("click", evt => { - evt.stopImmediatePropagation(); - evt.preventDefault(); - window.alloy("evaluateRulesets", { - action: "deposit-funds" - }); + const shareSocialMedia = () => { + window.alloy("evaluateRulesets", { + action: "share-social-media" }); + }; - document.getElementById("reset").addEventListener("click", evt => { - evt.stopImmediatePropagation(); - evt.preventDefault(); - localStorage.clear(); - location.reload(); + const depositFunds = () => { + window.alloy("evaluateRulesets", { + action: "deposit-funds" }); + }; - let isMounted = true; - const fetchData = async () => { - try { - const result = await window.alloy("subscribeMessageFeed", { - surface: "web://target.jasonwaters.dev/aep.html", - callback: ({ items = [], clicked, rendered }) => { - if (isMounted) { - setMessageFeedItems(items); - } - clicked(items); - rendered(items); - } - }); - if (isMounted) { - await applyRuleEngineResponse(result); - } - } catch (error) { - console.error("Error:", error); - } - }; - fetchData(); - return () => { - isMounted = false; - }; - }, []); + const resetPersistentData = () => { + localStorage.clear(); + window.location.reload(); + }; return (
    - - - + + +

    Message Feed

    {messageFeedItems.map((item, index) => ( -
    +
    clickHandler([item])} + >

    {item.title}

    {item.imageUrl && Item Image} diff --git a/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js b/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js index 7c8f2dccd..1249d8bc8 100644 --- a/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js +++ b/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.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 { MESSAGE_IN_APP } from "../../Personalization/constants/schema"; +import { TEXT_HTML } from "../../Personalization/constants/contentType"; export default (id, type, detail) => { const { html, mobileParameters } = detail; @@ -22,7 +23,7 @@ export default (id, type, detail) => { mobileParameters, webParameters, content: html, - contentType: "text/html" + contentType: TEXT_HTML }, id }; diff --git a/src/components/Personalization/constants/contentType.js b/src/components/Personalization/constants/contentType.js new file mode 100644 index 000000000..ee1d7312c --- /dev/null +++ b/src/components/Personalization/constants/contentType.js @@ -0,0 +1,12 @@ +/* +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 TEXT_HTML = "text/html"; diff --git a/src/components/Personalization/createCollect.js b/src/components/Personalization/createCollect.js index b9520be35..7112cd238 100644 --- a/src/components/Personalization/createCollect.js +++ b/src/components/Personalization/createCollect.js @@ -17,6 +17,7 @@ export default ({ eventManager, mergeDecisionsMeta }) => { // Called when a decision is auto-rendered for the __view__ scope or a SPA view(display and empty display notification) return ({ decisionsMeta = [], + propositionAction, documentMayUnload = false, eventType = DISPLAY, viewName @@ -35,7 +36,8 @@ export default ({ eventManager, mergeDecisionsMeta }) => { decisionsMeta, eventType === DISPLAY ? PropositionEventType.DISPLAY - : PropositionEventType.INTERACT + : PropositionEventType.INTERACT, + propositionAction ); } diff --git a/src/components/Personalization/createOnClickHandler.js b/src/components/Personalization/createOnClickHandler.js index 343b33263..6dd612e88 100644 --- a/src/components/Personalization/createOnClickHandler.js +++ b/src/components/Personalization/createOnClickHandler.js @@ -48,7 +48,7 @@ export default ({ event, decisionsMeta, PropositionEventType.INTERACT, - eventLabel + eventLabel ? { label: eventLabel } : undefined ); } } diff --git a/src/components/Personalization/createSubscribeMessageFeed.js b/src/components/Personalization/createSubscribeMessageFeed.js index 4ebc8f4db..be2dfba30 100644 --- a/src/components/Personalization/createSubscribeMessageFeed.js +++ b/src/components/Personalization/createSubscribeMessageFeed.js @@ -57,6 +57,10 @@ export default ({ collect }) => { const renderedSet = new Set(); const clicked = (items = []) => { + if (!(items instanceof Array)) { + return; + } + const decisionsMeta = []; const clickedSet = new Set(); diff --git a/src/components/Personalization/event.js b/src/components/Personalization/event.js index 196283ed4..c0f7d8e17 100644 --- a/src/components/Personalization/event.js +++ b/src/components/Personalization/event.js @@ -17,7 +17,7 @@ export const mergeDecisionsMeta = ( event, decisionsMeta, eventType, - eventLabel = "" + propositionAction ) => { const xdm = { _experience: { @@ -29,10 +29,9 @@ export const mergeDecisionsMeta = ( } } }; - if (eventLabel) { - xdm._experience.decisioning.propositionAction = { - label: eventLabel - }; + + if (propositionAction) { + xdm._experience.decisioning.propositionAction = propositionAction; } event.mergeXdm(xdm); }; diff --git a/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js b/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js index e10ef9082..cbc649709 100644 --- a/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js +++ b/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js @@ -11,7 +11,9 @@ governing permissions and limitations under the License. */ import { getNonce } from "../../dom-actions/dom"; -import { removeElementById } from "../utils"; +import { parseAnchor, removeElementById } from "../utils"; +import { TEXT_HTML } from "../../constants/contentType"; +import { INTERACT } from "../../constants/eventType"; const ELEMENT_TAG_CLASSNAME = "alloy-messaging-container"; const ELEMENT_TAG_ID = "alloy-messaging-container"; @@ -76,12 +78,15 @@ export const buildStyleFromParameters = (mobileParameters, webParameters) => { } return style; }; -export const setWindowLocationHref = link => { + +const setWindowLocationHref = link => { window.location.assign(link); }; -// eslint-disable-next-line no-unused-vars -export const createIframeClickHandler = collect => { +export const createIframeClickHandler = ( + interact, + navigateToUrl = setWindowLocationHref +) => { return event => { event.preventDefault(); event.stopImmediatePropagation(); @@ -91,43 +96,32 @@ export const createIframeClickHandler = collect => { const anchor = target.tagName.toLowerCase() === "a" ? target : target.closest("a"); - if (anchor) { - const parts = anchor.href.split("?"); - const actionPart = parts[0].split("://")[1]; - let action = ""; - let interaction = ""; - let link = ""; - if (parts.length > 1) { - const queryParams = new URLSearchParams(parts[1]); - action = actionPart; - interaction = queryParams.get("interaction") || ""; - link = queryParams.get("link") || ""; - const uuid = anchor.getAttribute("data-uuid") || ""; - // eslint-disable-next-line no-console - console.log(`clicked ${uuid}`); - // TODO: collect analytics - // collect({ - // eventType: INTERACT, - // eventSource: "inAppMessage", - // eventData: { - // action, - // interaction - // } - // }); - if (link && interaction === "clicked") { - link = decodeURIComponent(link); - setWindowLocationHref(link); - } else if (action === "dismiss") { - dismissMessage(); - } - } + if (!anchor) { + return; + } + + const { action, interaction, link, label, uuid } = parseAnchor(anchor); + + interact({ + label, + id: interaction, + uuid, + link + }); + + if (action === "dismiss") { + dismissMessage(); + } + + if (typeof link === "string" && link.length > 0) { + navigateToUrl(link); } }; }; export const createIframe = (htmlContent, clickHandler) => { const parser = new DOMParser(); - const htmlDocument = parser.parseFromString(htmlContent, "text/html"); + const htmlDocument = parser.parseFromString(htmlContent, TEXT_HTML); const scriptTag = htmlDocument.querySelector("script"); if (scriptTag) { @@ -135,7 +129,7 @@ export const createIframe = (htmlContent, clickHandler) => { } const element = document.createElement("iframe"); element.src = URL.createObjectURL( - new Blob([htmlDocument.documentElement.outerHTML], { type: "text/html" }) + new Blob([htmlDocument.documentElement.outerHTML], { type: TEXT_HTML }) ); element.id = ALLOY_IFRAME_ID; @@ -188,17 +182,17 @@ export const createOverlayElement = parameter => { return element; }; -export const displayHTMLContentInIframe = (settings, collect) => { +export const displayHTMLContentInIframe = (settings, interact) => { dismissMessage(); const { content, contentType, mobileParameters } = settings; - if (contentType !== "text/html") { - // TODO: whoops, no can do. + if (contentType !== TEXT_HTML) { + return; } const container = createContainerElement(settings); - const iframe = createIframe(content, createIframeClickHandler(collect)); + const iframe = createIframe(content, createIframeClickHandler(interact)); container.appendChild(iframe); @@ -214,7 +208,13 @@ export default (settings, collect) => { return new Promise(resolve => { const { meta } = settings; - displayHTMLContentInIframe(settings, collect); + displayHTMLContentInIframe(settings, propositionAction => { + collect({ + decisionsMeta: [meta], + propositionAction, + eventType: INTERACT + }); + }); resolve({ meta }); }); diff --git a/src/components/Personalization/in-app-message-actions/utils.js b/src/components/Personalization/in-app-message-actions/utils.js index b2e7d51ed..b8f82bc49 100644 --- a/src/components/Personalization/in-app-message-actions/utils.js +++ b/src/components/Personalization/in-app-message-actions/utils.js @@ -9,6 +9,8 @@ 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 { startsWith } from "../../../utils"; + export const addStyle = (styleTagId, cssText) => { const existingStyle = document.getElementById(styleTagId); if (existingStyle) { @@ -37,3 +39,38 @@ export const removeElementById = id => { element.remove(); } }; + +export const parseAnchor = anchor => { + const nothing = {}; + + if (!anchor || anchor.tagName.toLowerCase() !== "a") { + return nothing; + } + + const { href } = anchor; + if (!href || !startsWith(href, "adbinapp://")) { + return nothing; + } + + const hrefParts = href.split("?"); + + const action = hrefParts[0].split("://")[1]; + const label = anchor.innerText; + const uuid = anchor.getAttribute("data-uuid") || ""; + + let interaction; + let link; + + if (hrefParts.length > 1) { + const queryParams = new URLSearchParams(hrefParts[1]); + interaction = queryParams.get("interaction") || ""; + link = decodeURIComponent(queryParams.get("link") || ""); + } + return { + action, + interaction, + link, + label, + uuid + }; +}; diff --git a/test/unit/specs/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.spec.js b/test/unit/specs/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.spec.js index 53394a39c..047eb2d45 100644 --- a/test/unit/specs/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.spec.js +++ b/test/unit/specs/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.spec.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 inAppMessageConsequenceAdapter from "../../../../../../src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter"; +import { TEXT_HTML } from "../../../../../../src/components/Personalization/constants/contentType"; describe("DecisioningEngine:inAppMessageConsequenceAdapter", () => { it("works", () => { @@ -54,7 +55,7 @@ describe("DecisioningEngine:inAppMessageConsequenceAdapter", () => { }, webParameters: jasmine.any(Object), content: "", - contentType: "text/html" + contentType: TEXT_HTML }, id: "72042c7c-4e34-44f6-af95-1072ae117424" }); diff --git a/test/unit/specs/components/DecisioningEngine/createConsequenceAdapter.spec.js b/test/unit/specs/components/DecisioningEngine/createConsequenceAdapter.spec.js index 26ecf5319..33def2175 100644 --- a/test/unit/specs/components/DecisioningEngine/createConsequenceAdapter.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createConsequenceAdapter.spec.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 createConsequenceAdapter from "../../../../../src/components/DecisioningEngine/createConsequenceAdapter"; +import { TEXT_HTML } from "../../../../../src/components/Personalization/constants/contentType"; describe("DecisioningEngine:createConsequenceAdapter", () => { it("works", () => { @@ -58,7 +59,7 @@ describe("DecisioningEngine:createConsequenceAdapter", () => { }, webParameters: jasmine.any(Object), content: "

    modal
    ", - contentType: "text/html" + contentType: TEXT_HTML }, id: "72042c7c-4e34-44f6-af95-1072ae117424" }); diff --git a/test/unit/specs/components/Personalization/createCollect.spec.js b/test/unit/specs/components/Personalization/createCollect.spec.js index 095772185..6c65207af 100644 --- a/test/unit/specs/components/Personalization/createCollect.spec.js +++ b/test/unit/specs/components/Personalization/createCollect.spec.js @@ -43,7 +43,8 @@ describe("Personalization::createCollect", () => { expect(mergeDecisionsMeta).toHaveBeenCalledWith( event, decisionsMeta, - PropositionEventType.DISPLAY + PropositionEventType.DISPLAY, + undefined ); expect(eventManager.sendEvent).toHaveBeenCalled(); }); diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js index d814e40af..9b85d9f29 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js @@ -12,6 +12,7 @@ governing permissions and limitations under the License. import { createNode } from "../../../../../../../src/utils/dom"; import { DIV } from "../../../../../../../src/constants/tagName"; import displayIframeContent from "../../../../../../../src/components/Personalization/in-app-message-actions/actions/displayIframeContent"; +import { TEXT_HTML } from "../../../../../../../src/components/Personalization/constants/contentType"; describe("Personalization::IAM:banner", () => { it("inserts banner into dom", async () => { @@ -42,7 +43,7 @@ describe("Personalization::IAM:banner", () => { height: 60 }, content: `
    banner
    Alf Says`, - contentType: "text/html" + contentType: TEXT_HTML }); const overlayContainer = document.querySelector( diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js index e3c7aaeae..0013c3b77 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js @@ -19,6 +19,7 @@ import { import cleanUpDomChanges from "../../../../../helpers/cleanUpDomChanges"; import { getNonce } from "../../../../../../../src/components/Personalization/dom-actions/dom"; import { testResetCachedNonce } from "../../../../../../../src/components/Personalization/dom-actions/dom/getNonce"; +import { TEXT_HTML } from "../../../../../../../src/components/Personalization/constants/contentType"; describe("DOM Actions on Iframe", () => { beforeEach(() => { @@ -146,7 +147,7 @@ describe("DOM Actions on Iframe", () => { const blob = await fetch(iframe.src).then(r => r.blob()); const text = await blob.text(); const parser = new DOMParser(); - const iframeDocument = parser.parseFromString(text, "text/html"); + const iframeDocument = parser.parseFromString(text, TEXT_HTML); const scriptTag = iframeDocument.querySelector("script"); expect(scriptTag).toBeDefined(); @@ -156,7 +157,7 @@ describe("DOM Actions on Iframe", () => { describe("createIframeClickHandler", () => { let container; - let mockedCollect; + let mockedInteract; let mobileParameters; beforeEach(() => { @@ -164,7 +165,7 @@ describe("DOM Actions on Iframe", () => { container.setAttribute("id", "alloy-messaging-container"); document.body.appendChild(container); - mockedCollect = jasmine.createSpy("collect"); + mockedInteract = jasmine.createSpy("interact"); mobileParameters = { verticalAlign: "center", @@ -184,25 +185,26 @@ describe("DOM Actions on Iframe", () => { }); const anchor = document.createElement("a"); - Object.assign(anchor, { - "data-uuid": "12345", - href: "adbinapp://dismiss?interaction=cancel" - }); + anchor.setAttribute("data-uuid", "12345"); + anchor.href = "adbinapp://dismiss?interaction=cancel"; + anchor.innerText = "Cancel"; const mockEvent = { target: anchor, preventDefault: () => {}, stopImmediatePropagation: () => {} }; - const iframeClickHandler = createIframeClickHandler( - container, - mockedCollect, - mobileParameters - ); + const iframeClickHandler = createIframeClickHandler(mockedInteract); iframeClickHandler(mockEvent); const alloyMessagingContainer = document.getElementById( "alloy-messaging-container" ); + expect(mockedInteract).toHaveBeenCalledOnceWith({ + label: "Cancel", + id: "cancel", + uuid: "12345", + link: "" + }); expect(alloyMessagingContainer).toBeNull(); }); @@ -217,27 +219,68 @@ describe("DOM Actions on Iframe", () => { document.body.appendChild(overlayContainer); const anchor = document.createElement("a"); - Object.assign(anchor, { - "data-uuid": "12345", - href: "adbinapp://dismiss?interaction=cancel" + anchor.setAttribute("data-uuid", "54321"); + anchor.href = "adbinapp://dismiss?interaction=cancel"; + anchor.innerText = "Aloha"; + + const mockEvent = { + target: anchor, + preventDefault: () => {}, + stopImmediatePropagation: () => {} + }; + const iframeClickHandler = createIframeClickHandler(mockedInteract); + iframeClickHandler(mockEvent); + const overlayContainerAfterDismissal = document.getElementById( + "alloy-overlay-container" + ); + expect(mockedInteract).toHaveBeenCalledOnceWith({ + label: "Aloha", + id: "cancel", + uuid: "54321", + link: "" }); + + expect(overlayContainerAfterDismissal).toBeNull(); + }); + + it("extracts propositionAction details from anchor tag and sends to interact()", () => { + const mockNavigateToUrl = jasmine.createSpy("mockNavigateToUrl"); + Object.assign(mobileParameters, { + uiTakeover: true + }); + + const anchor = document.createElement("a"); + anchor.setAttribute("data-uuid", "blippi"); + anchor.href = + "adbinapp://dismiss?interaction=accept&link=https%3A%2F%2Fwww.google.com"; + anchor.innerText = "Woof"; + const mockEvent = { target: anchor, preventDefault: () => {}, stopImmediatePropagation: () => {} }; const iframeClickHandler = createIframeClickHandler( - container, - mockedCollect, - mobileParameters + mockedInteract, + mockNavigateToUrl ); iframeClickHandler(mockEvent); const overlayContainerAfterDismissal = document.getElementById( "alloy-overlay-container" ); + expect(mockedInteract).toHaveBeenCalledOnceWith({ + label: "Woof", + id: "accept", + uuid: "blippi", + link: "https://www.google.com" + }); + expect(mockNavigateToUrl).toHaveBeenCalledOnceWith( + "https://www.google.com" + ); expect(overlayContainerAfterDismissal).toBeNull(); }); }); + describe("displayHTMLContentInIframe", () => { let originalAppendChild; let originalBodyStyle; @@ -313,7 +356,7 @@ describe("DOM Actions on Iframe", () => { }, content: '\n\n\n Bumper Sale!\n \n\n\n
    \n \n

    Black Friday Sale!

    \n Technology Image\n

    Don\'t miss out on our incredible discounts and deals at our gadgets!

    \n
    \n Shop\n Dismiss\n
    \n
    \n\n\n\n', - contentType: "text/html", + contentType: TEXT_HTML, schema: "https://ns.adobe.com/personalization/message/in-app", meta: { id: "9441e3c4-d673-4c1b-8fb9-d1c0f7826dcc", diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js index 2c4b3faae..1edb52313 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js @@ -12,6 +12,7 @@ governing permissions and limitations under the License. import { createNode } from "../../../../../../../src/utils/dom"; import { DIV } from "../../../../../../../src/constants/tagName"; import displayIframeContent from "../../../../../../../src/components/Personalization/in-app-message-actions/actions/displayIframeContent"; +import { TEXT_HTML } from "../../../../../../../src/components/Personalization/constants/contentType"; describe("Personalization::IAM:modal", () => { it("inserts modal into dom", async () => { @@ -42,7 +43,7 @@ describe("Personalization::IAM:modal", () => { height: 60 }, content: `
    modal
    Alf Says`, - contentType: "text/html" + contentType: TEXT_HTML }); document.querySelector("div#alloy-overlay-container"); const messagingContainer = document.querySelector( diff --git a/test/unit/specs/components/Personalization/responsesMock/eventResponses.js b/test/unit/specs/components/Personalization/responsesMock/eventResponses.js index 329b340c5..10c6e00d3 100644 --- a/test/unit/specs/components/Personalization/responsesMock/eventResponses.js +++ b/test/unit/specs/components/Personalization/responsesMock/eventResponses.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. */ + export const SCOPES_FOO1_FOO2_DECISIONS = [ { id: "TNT:ABC:A", From ee9948ab04b7e1f1943ea0b1eddd2b2b2ef421b2 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Mon, 11 Sep 2023 13:47:04 -0600 Subject: [PATCH 46/66] evaluateRulesetsCommand (#1033) --- .../InAppMessagesDemo/InAppMessages.js | 2 +- .../components/MessageFeedDemo/MessageFeed.js | 13 +- .../DecisioningEngine/createApplyResponse.js | 4 +- .../createEvaluateRulesetsCommand.js | 39 + .../createOnResponseHandler.js | 8 +- .../createSubscribeRulesetItems.js | 43 +- src/components/DecisioningEngine/index.js | 20 +- .../Personalization/createComponent.js | 7 +- .../actions/displayIframeContent.js | 2 +- .../createApplyResponse.spec.js | 8 +- .../createEvaluateRulesetsCommand.spec.js | 192 ++++ .../createOnResponseHandler.spec.js | 2 + .../createSubscribeRulesetItems.spec.js | 849 +++++++++++++----- .../actions/displayIframeContent.spec.js | 6 +- 14 files changed, 916 insertions(+), 279 deletions(-) create mode 100644 src/components/DecisioningEngine/createEvaluateRulesetsCommand.js create mode 100644 test/unit/specs/components/DecisioningEngine/createEvaluateRulesetsCommand.spec.js diff --git a/sandbox/src/components/InAppMessagesDemo/InAppMessages.js b/sandbox/src/components/InAppMessagesDemo/InAppMessages.js index 1a7149629..bd17dd19e 100644 --- a/sandbox/src/components/InAppMessagesDemo/InAppMessages.js +++ b/sandbox/src/components/InAppMessagesDemo/InAppMessages.js @@ -76,7 +76,7 @@ export default function InAppMessages() { const renderDecisions = () => { window .alloy("subscribeRulesetItems", { - surface, + surfaces: [surface], callback: result => { console.log("subscribeRulesetItems", result); } diff --git a/sandbox/src/components/MessageFeedDemo/MessageFeed.js b/sandbox/src/components/MessageFeedDemo/MessageFeed.js index a4c80c3a5..e40f5920a 100644 --- a/sandbox/src/components/MessageFeedDemo/MessageFeed.js +++ b/sandbox/src/components/MessageFeedDemo/MessageFeed.js @@ -414,7 +414,7 @@ export default function MessageFeed() { useEffect(() => { Promise.all([ window.alloy("subscribeRulesetItems", { - surface: "web://target.jasonwaters.dev/aep.html", + surfaces: ["web://target.jasonwaters.dev/aep.html"], callback: result => { console.log("subscribeRulesetItems", result); } @@ -422,7 +422,6 @@ export default function MessageFeed() { window.alloy("subscribeMessageFeed", { surface: "web://target.jasonwaters.dev/aep.html", callback: ({ items = [], rendered, clicked }) => { - console.log("setMessageFeedItems", items, clicked); setClickHandler(() => clicked); setMessageFeedItems(items); @@ -438,13 +437,19 @@ export default function MessageFeed() { const shareSocialMedia = () => { window.alloy("evaluateRulesets", { - action: "share-social-media" + renderDecisions: true, + decisionContext: { + action: "share-social-media" + } }); }; const depositFunds = () => { window.alloy("evaluateRulesets", { - action: "deposit-funds" + renderDecisions: true, + decisionContext: { + action: "deposit-funds" + } }); }; diff --git a/src/components/DecisioningEngine/createApplyResponse.js b/src/components/DecisioningEngine/createApplyResponse.js index 1bf66de01..dda753c49 100644 --- a/src/components/DecisioningEngine/createApplyResponse.js +++ b/src/components/DecisioningEngine/createApplyResponse.js @@ -10,9 +10,9 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ export default lifecycle => { - return ({ viewName, propositions = [] }) => { + return ({ viewName, renderDecisions = false, propositions = [] }) => { if (propositions.length > 0 && lifecycle) { - lifecycle.onDecision({ viewName, propositions }); + lifecycle.onDecision({ renderDecisions, viewName, propositions }); } return { propositions }; diff --git a/src/components/DecisioningEngine/createEvaluateRulesetsCommand.js b/src/components/DecisioningEngine/createEvaluateRulesetsCommand.js new file mode 100644 index 000000000..ae5bfb73f --- /dev/null +++ b/src/components/DecisioningEngine/createEvaluateRulesetsCommand.js @@ -0,0 +1,39 @@ +/* +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 { boolean, objectOf } from "../../utils/validation"; + +const validateOptions = ({ options }) => { + const validator = objectOf({ + renderDecisions: boolean(), + decisionContext: objectOf({}) + }).noUnknownFields(); + + return validator(options); +}; + +export default ({ contextProvider, decisionProvider }) => { + const run = ({ renderDecisions, decisionContext, applyResponse }) => { + return applyResponse({ + renderDecisions, + propositions: decisionProvider.evaluate( + contextProvider.getContext(decisionContext) + ) + }); + }; + + const optionsValidator = options => validateOptions({ options }); + + return { + optionsValidator, + run + }; +}; diff --git a/src/components/DecisioningEngine/createOnResponseHandler.js b/src/components/DecisioningEngine/createOnResponseHandler.js index 9e9702b69..89e6d43cd 100644 --- a/src/components/DecisioningEngine/createOnResponseHandler.js +++ b/src/components/DecisioningEngine/createOnResponseHandler.js @@ -29,9 +29,9 @@ export default ({ decisionProvider.addPayloads( response.getPayloadsByType(PERSONALIZATION_DECISIONS_HANDLE) ); - if (renderDecisions) { - const propositions = decisionProvider.evaluate(context); - applyResponse({ viewName, propositions }); - } + + const propositions = decisionProvider.evaluate(context); + + applyResponse({ viewName, renderDecisions, propositions }); }; }; diff --git a/src/components/DecisioningEngine/createSubscribeRulesetItems.js b/src/components/DecisioningEngine/createSubscribeRulesetItems.js index f47cf7df8..4081f1574 100644 --- a/src/components/DecisioningEngine/createSubscribeRulesetItems.js +++ b/src/components/DecisioningEngine/createSubscribeRulesetItems.js @@ -18,7 +18,7 @@ import { const validateOptions = ({ options }) => { const validator = objectOf({ - surface: string().required(), + surfaces: arrayOf(string()).uniqueItems(), schemas: arrayOf(string()).uniqueItems(), callback: callbackType().required() }).noUnknownFields(); @@ -28,41 +28,44 @@ const validateOptions = ({ options }) => { export default () => { let subscriptionHandler; - let surfaceIdentifier; + let surfacesFilter; let schemasFilter; - const run = ({ surface, schemas, callback }) => { + const run = ({ surfaces, schemas, callback }) => { subscriptionHandler = callback; - surfaceIdentifier = surface; + surfacesFilter = surfaces instanceof Array ? surfaces : undefined; schemasFilter = schemas instanceof Array ? schemas : undefined; }; const optionsValidator = options => validateOptions({ options }); const refresh = propositions => { - if (!subscriptionHandler || !surfaceIdentifier) { + if (!subscriptionHandler) { return; } - const result = propositions - .filter(payload => payload.scope === surfaceIdentifier) - .reduce((allItems, payload) => { - const { items = [] } = payload; + const result = { + propositions: propositions + .filter(payload => + surfacesFilter ? surfacesFilter.includes(payload.scope) : true + ) + .map(payload => { + const { items = [] } = payload; + return { + ...payload, + items: items.filter(item => + schemasFilter ? schemasFilter.includes(item.schema) : true + ) + }; + }) + .filter(payload => payload.items.length > 0) + }; - return [ - ...allItems, - ...items.filter(item => - schemasFilter ? schemasFilter.includes(item.schema) : true - ) - ]; - }, []) - .sort((a, b) => b.data.qualifiedDate - a.data.qualifiedDate); - - if (result.length === 0) { + if (result.propositions.length === 0) { return; } - subscriptionHandler.call(null, { items: result }); + subscriptionHandler.call(null, result); }; return { diff --git a/src/components/DecisioningEngine/index.js b/src/components/DecisioningEngine/index.js index 86b890bf9..c7b2bc4fe 100644 --- a/src/components/DecisioningEngine/index.js +++ b/src/components/DecisioningEngine/index.js @@ -22,6 +22,7 @@ import { MOBILE_EVENT_SOURCE, MOBILE_EVENT_TYPE } from "./constants"; +import createEvaluateRulesetsCommand from "./createEvaluateRulesetsCommand"; const createDecisioningEngine = ({ config, createNamespacedStorage }) => { const { orgId } = config; @@ -30,9 +31,15 @@ const createDecisioningEngine = ({ config, createNamespacedStorage }) => { ); const eventRegistry = createEventRegistry({ storage: storage.persistent }); let applyResponse; + const decisionProvider = createDecisionProvider({ eventRegistry }); const contextProvider = createContextProvider({ eventRegistry, window }); + const evaluateRulesetsCommand = createEvaluateRulesetsCommand({ + contextProvider, + decisionProvider + }); + const subscribeRulesetItems = createSubscribeRulesetItems(); return { @@ -70,12 +77,13 @@ const createDecisioningEngine = ({ config, createNamespacedStorage }) => { }, commands: { evaluateRulesets: { - run: decisionContext => - applyResponse({ - propositions: decisionProvider.evaluate( - contextProvider.getContext(decisionContext) - ) - }) + run: ({ renderDecisions, decisionContext }) => + evaluateRulesetsCommand.run({ + renderDecisions, + decisionContext, + applyResponse + }), + optionsValidator: evaluateRulesetsCommand.optionsValidator }, subscribeRulesetItems: subscribeRulesetItems.command } diff --git a/src/components/Personalization/createComponent.js b/src/components/Personalization/createComponent.js index bed029ab4..4322a2ae9 100644 --- a/src/components/Personalization/createComponent.js +++ b/src/components/Personalization/createComponent.js @@ -32,8 +32,13 @@ export default ({ }) => { return { lifecycle: { - onDecision({ viewName, propositions }) { + onDecision({ viewName, renderDecisions, propositions }) { subscribeMessageFeed.refresh(propositions); + + if (!renderDecisions) { + return; + } + autoRenderingHandler({ viewName, pageWideScopeDecisions: propositions, diff --git a/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js b/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js index cbc649709..225f9cb00 100644 --- a/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js +++ b/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js @@ -20,7 +20,7 @@ const ELEMENT_TAG_ID = "alloy-messaging-container"; const OVERLAY_TAG_CLASSNAME = "alloy-overlay-container"; const OVERLAY_TAG_ID = "alloy-overlay-container"; -const ALLOY_IFRAME_ID = "alloy-iframe-id"; +const ALLOY_IFRAME_ID = "alloy-content-iframe"; const dismissMessage = () => [ELEMENT_TAG_ID, OVERLAY_TAG_ID].forEach(removeElementById); diff --git a/test/unit/specs/components/DecisioningEngine/createApplyResponse.spec.js b/test/unit/specs/components/DecisioningEngine/createApplyResponse.spec.js index af77eb4a8..e092afb85 100644 --- a/test/unit/specs/components/DecisioningEngine/createApplyResponse.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createApplyResponse.spec.js @@ -29,6 +29,7 @@ describe("DecisioningEngine:createApplyResponse", () => { expect(lifecycle.onDecision).toHaveBeenCalledWith({ viewName: undefined, + renderDecisions: false, propositions: [proposition] }); }); @@ -40,10 +41,15 @@ describe("DecisioningEngine:createApplyResponse", () => { const applyResponse = createApplyResponse(lifecycle); - applyResponse({ viewName: "oh hai", propositions: [proposition] }); + applyResponse({ + viewName: "oh hai", + renderDecisions: true, + propositions: [proposition] + }); expect(lifecycle.onDecision).toHaveBeenCalledWith({ viewName: "oh hai", + renderDecisions: true, propositions: [proposition] }); }); diff --git a/test/unit/specs/components/DecisioningEngine/createEvaluateRulesetsCommand.spec.js b/test/unit/specs/components/DecisioningEngine/createEvaluateRulesetsCommand.spec.js new file mode 100644 index 000000000..f2fa070e1 --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/createEvaluateRulesetsCommand.spec.js @@ -0,0 +1,192 @@ +/* +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 createEvaluateRulesetsCommand from "../../../../../src/components/DecisioningEngine/createEvaluateRulesetsCommand"; +import createContextProvider from "../../../../../src/components/DecisioningEngine/createContextProvider"; +import createEventRegistry from "../../../../../src/components/DecisioningEngine/createEventRegistry"; +import createDecisionProvider from "../../../../../src/components/DecisioningEngine/createDecisionProvider"; +import createApplyResponse from "../../../../../src/components/DecisioningEngine/createApplyResponse"; + +describe("DecisioningEngine:evaluateRulesetsCommand", () => { + let onDecision; + let applyResponse; + let storage; + let eventRegistry; + let contextProvider; + let decisionProvider; + let evaluateRulesetsCommand; + + beforeEach(() => { + onDecision = jasmine.createSpy(); + applyResponse = createApplyResponse({ onDecision }); + + storage = jasmine.createSpyObj("storage", ["getItem", "setItem", "clear"]); + eventRegistry = createEventRegistry({ storage }); + contextProvider = createContextProvider({ eventRegistry, window }); + decisionProvider = createDecisionProvider({ eventRegistry }); + + decisionProvider.addPayload({ + id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", + items: [ + { + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe", + schema: "https://ns.adobe.com/personalization/ruleset-item", + data: { + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [ + { + definition: { + conditions: [ + { + definition: { + key: "color", + matcher: "eq", + values: ["orange", "blue"] + }, + type: "matcher" + }, + { + definition: { + key: "action", + matcher: "eq", + values: ["greet"] + }, + type: "matcher" + } + ], + logic: "and" + }, + type: "group" + } + ], + logic: "and" + }, + type: "group" + }, + consequences: [ + { + type: "schema", + detail: { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" + }, + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)" + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + } + ] + } + ] + } + } + ], + scope: "web://mywebsite.com" + }); + + evaluateRulesetsCommand = createEvaluateRulesetsCommand({ + contextProvider, + decisionProvider + }); + }); + + it("onDecisions receives renderDecisions=true", () => { + const result = evaluateRulesetsCommand.run({ + renderDecisions: true, + decisionContext: { color: "orange", action: "greet" }, + applyResponse + }); + + const expectedResult = { + propositions: [ + { + id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" + }, + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + } + ], + scope: "web://mywebsite.com" + } + ] + }; + + expect(result).toEqual(expectedResult); + expect(onDecision).toHaveBeenCalledOnceWith({ + viewName: undefined, + renderDecisions: true, + ...expectedResult + }); + }); + + it("onDecisions receives renderDecisions=false", () => { + const result = evaluateRulesetsCommand.run({ + decisionContext: { color: "orange", action: "greet" }, + applyResponse + }); + + const expectedResult = { + propositions: [ + { + id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" + }, + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)", + qualifiedDate: jasmine.any(Number), + displayedDate: undefined + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" + } + ], + scope: "web://mywebsite.com" + } + ] + }; + + expect(result).toEqual(expectedResult); + expect(onDecision).toHaveBeenCalledOnceWith({ + viewName: undefined, + renderDecisions: false, + ...expectedResult + }); + }); +}); diff --git a/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js b/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js index 5cb886fa1..5a177c1ef 100644 --- a/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createOnResponseHandler.spec.js @@ -171,6 +171,7 @@ describe("DecisioningEngine:createOnResponseHandler", () => { expect(lifecycle.onDecision).toHaveBeenCalledWith({ viewName: undefined, + renderDecisions: true, propositions: [ { id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", @@ -335,6 +336,7 @@ describe("DecisioningEngine:createOnResponseHandler", () => { expect(lifecycle.onDecision).toHaveBeenCalledWith({ viewName: "home", + renderDecisions: true, propositions: [ { id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", diff --git a/test/unit/specs/components/DecisioningEngine/createSubscribeRulesetItems.spec.js b/test/unit/specs/components/DecisioningEngine/createSubscribeRulesetItems.spec.js index 41b197f24..24c3c0b71 100644 --- a/test/unit/specs/components/DecisioningEngine/createSubscribeRulesetItems.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createSubscribeRulesetItems.spec.js @@ -17,6 +17,29 @@ describe("DecisioningEngine:subscribeRulesetItems", () => { let subscribeRulesetItems; const PROPOSITIONS = [ + { + id: "abc", + items: [ + { + schema: DOM_ACTION, + data: { + selector: "a", + type: "setAttribute", + content: { + src: "img/test.png" + }, + prehidingSelector: "a", + qualifiedDate: 1694198274647, + displayedDate: 1694198274647 + }, + id: "aabbcc" + } + ], + scope: "web://something.com", + scopeDetails: { + decisionProvider: "AJO" + } + }, { id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", items: [ @@ -200,104 +223,144 @@ describe("DecisioningEngine:subscribeRulesetItems", () => { const callback = jasmine.createSpy(); - // register a subscription. equivalent to alloy("subscribeRulesetItems", {surface, callback}) + // register a subscription. equivalent to alloy("subscribeRulesetItems", ... command.run({ - surface: "web://mywebsite.com/feed", + surfaces: ["web://mywebsite.com/feed"], schemas: [MESSAGE_FEED_ITEM], callback }); refresh(PROPOSITIONS); expect(callback).toHaveBeenCalledOnceWith({ - items: [ - jasmine.objectContaining({ - schema: MESSAGE_FEED_ITEM, - data: { - expiryDate: 1712190456, - publishedDate: 1678098240000, - meta: { - feedName: "Winter Promo", - surface: "web://mywebsite.com/feed" - }, - content: { - imageUrl: "img/twitter.png", - actionTitle: "Shop the sale!", - actionUrl: "https://luma.com/sale", - body: "Posting on social media helps us spread the word.", - title: "Thanks for sharing!" - }, - contentType: "application/json", - qualifiedDate: 1683042658312, - displayedDate: 1683042658316 - }, - id: "cfcb1af7-7bc2-45b2-a86a-0aa93fe69ce7" - }), - jasmine.objectContaining({ - schema: MESSAGE_FEED_ITEM, - data: { - expiryDate: 1712190456, - publishedDate: 1678184640000, - meta: { - feedName: "Winter Promo", - surface: "web://mywebsite.com/feed" - }, - content: { - imageUrl: "img/gold-coin.jpg", - actionTitle: "Shop the sale!", - actionUrl: "https://luma.com/sale", - body: "Now you're ready to earn!", - title: "Funds deposited!" - }, - contentType: "application/json", - qualifiedDate: 1683042653905, - displayedDate: 1683042653909 - }, - id: "0263e171-fa32-4c7a-9611-36b28137a81d" - }), - jasmine.objectContaining({ - schema: MESSAGE_FEED_ITEM, - data: { - expiryDate: 1712190456, - publishedDate: 1677839040000, - meta: { - feedName: "Winter Promo", - surface: "web://mywebsite.com/feed" - }, - content: { - imageUrl: "img/achievement.png", - actionTitle: "Shop the sale!", - actionUrl: "https://luma.com/sale", - body: "Great job, you completed your profile.", - title: "Achievement Unlocked!" - }, - contentType: "application/json", - qualifiedDate: 1683042628064, - displayedDate: 1683042628070 - }, - id: "b7173290-588f-40c6-a05c-43ed5ec08b28" - }), - jasmine.objectContaining({ - schema: MESSAGE_FEED_ITEM, - data: { - expiryDate: 1712190456, - publishedDate: 1677752640000, - meta: { - feedName: "Winter Promo", - surface: "web://mywebsite.com/feed" - }, - content: { - imageUrl: "img/lumon.png", - actionTitle: "Shop the sale!", - actionUrl: "https://luma.com/sale", - body: "a handshake is available upon request.", - title: "Welcome to Lumon!" + propositions: [ + { + id: "1a3d874f-39ee-4310-bfa9-6559a10041a4", + items: [ + { + schema: "https://ns.adobe.com/personalization/message/feed-item", + data: { + expiryDate: 1712190456, + publishedDate: 1677752640000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/lumon.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "a handshake is available upon request.", + title: "Welcome to Lumon!" + }, + contentType: "application/json", + qualifiedDate: 1683042628060, + displayedDate: 1683042628070 + }, + id: "a48ca420-faea-467e-989a-5d179d9f562d" }, - contentType: "application/json", - qualifiedDate: 1683042628060, - displayedDate: 1683042628070 - }, - id: "a48ca420-faea-467e-989a-5d179d9f562d" - }) + { + schema: "https://ns.adobe.com/personalization/message/feed-item", + data: { + expiryDate: 1712190456, + publishedDate: 1677839040000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/achievement.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "Great job, you completed your profile.", + title: "Achievement Unlocked!" + }, + contentType: "application/json", + qualifiedDate: 1683042628064, + displayedDate: 1683042628070 + }, + id: "b7173290-588f-40c6-a05c-43ed5ec08b28" + } + ], + scope: "web://mywebsite.com/feed" + }, + { + id: "1ae11bc5-96dc-41c7-8f71-157c57a5290e", + items: [ + { + schema: "https://ns.adobe.com/personalization/message/feed-item", + data: { + expiryDate: 1712190456, + publishedDate: 1678098240000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/twitter.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "Posting on social media helps us spread the word.", + title: "Thanks for sharing!" + }, + contentType: "application/json", + qualifiedDate: 1683042658312, + displayedDate: 1683042658316 + }, + id: "cfcb1af7-7bc2-45b2-a86a-0aa93fe69ce7" + } + ], + scope: "web://mywebsite.com/feed", + scopeDetails: { + id: "1ae11bc5-96dc-41c7-8f71-157c57a5290e", + scope: "web://mywebsite.com/feed", + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: + "eyJtZXNzYWdlRXhlY3V0aW9uIjp7Im1lc3NhZ2VFeGVjdXRpb25JRCI6Ik5BIiwibWVzc2FnZUlEIjoiMDJjNzdlYTgtN2MwZS00ZDMzLTgwOTAtNGE1YmZkM2Q3NTAzIiwibWVzc2FnZVR5cGUiOiJtYXJrZXRpbmciLCJjYW1wYWlnbklEIjoiMzlhZThkNGItYjU1ZS00M2RjLWExNDMtNzdmNTAxOTViNDg3IiwiY2FtcGFpZ25WZXJzaW9uSUQiOiJiZDg1ZDllOC0yMDM3LTQyMmYtYjZkMi0zOTU3YzkwNTU5ZDMiLCJjYW1wYWlnbkFjdGlvbklEIjoiYjQ3ZmRlOGItNTdjMS00YmJlLWFlMjItNjRkNWI3ODJkMTgzIiwibWVzc2FnZVB1YmxpY2F0aW9uSUQiOiJhZTUyY2VkOC0yMDBjLTQ5N2UtODc4Ny1lZjljZmMxNzgyMTUifSwibWVzc2FnZVByb2ZpbGUiOnsiY2hhbm5lbCI6eyJfaWQiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbHMvd2ViIiwiX3R5cGUiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbC10eXBlcy93ZWIifSwibWVzc2FnZVByb2ZpbGVJRCI6ImY1Y2Q5OTk1LTZiNDQtNDIyMS05YWI3LTViNTMzOGQ1ZjE5MyJ9fQ==" + }, + strategies: [ + { + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" + } + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" + }, + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" + } + } + }, + { + id: "d1f7d411-a549-47bc-a4d8-c8e638b0a46b", + items: [ + { + schema: "https://ns.adobe.com/personalization/message/feed-item", + data: { + expiryDate: 1712190456, + publishedDate: 1678184640000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/gold-coin.jpg", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "Now you're ready to earn!", + title: "Funds deposited!" + }, + contentType: "application/json", + qualifiedDate: 1683042653905, + displayedDate: 1683042653909 + }, + id: "0263e171-fa32-4c7a-9611-36b28137a81d" + } + ], + scope: "web://mywebsite.com/feed" + } ] }); }); @@ -307,178 +370,492 @@ describe("DecisioningEngine:subscribeRulesetItems", () => { const callback = jasmine.createSpy(); - // register a subscription. equivalent to alloy("subscribeRulesetItems", {surface, callback}) + // register a subscription. equivalent to alloy("subscribeRulesetItems", ... command.run({ - surface: "web://mywebsite.com/feed", + surfaces: ["web://mywebsite.com/feed"], schemas: [DOM_ACTION], callback }); refresh(PROPOSITIONS); expect(callback).toHaveBeenCalledOnceWith({ - items: [ - jasmine.objectContaining({ - schema: DOM_ACTION, - data: { - selector: "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", - type: "setHtml", - content: "Hello Treatment A!", - prehidingSelector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", - qualifiedDate: 1683042673387, - displayedDate: 1683042673395 - }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" - }), - jasmine.objectContaining({ - schema: DOM_ACTION, - data: { - selector: "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", - type: "setAttribute", - content: { - src: "img/demo-marketing-offer1-exp-A.png" + propositions: [ + { + id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" + }, + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)", + qualifiedDate: 1683042673380, + displayedDate: 1683042673395 + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" }, - prehidingSelector: - "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)", - qualifiedDate: 1683042673380, - displayedDate: 1683042673395 - }, - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" - }) + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + qualifiedDate: 1683042673387, + displayedDate: 1683042673395 + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + } + ], + scope: "web://mywebsite.com/feed" + } ] }); }); - it("calls the callback with list of all schema-based items", () => { + it("calls the callback with list of all schema-based items for single schema", () => { const { command, refresh } = subscribeRulesetItems; const callback = jasmine.createSpy(); - // register a subscription. equivalent to alloy("subscribeRulesetItems", {surface, callback}) + // register a subscription. equivalent to alloy("subscribeRulesetItems", ... command.run({ - surface: "web://mywebsite.com/feed", + surfaces: ["web://mywebsite.com/feed"], callback }); refresh(PROPOSITIONS); expect(callback).toHaveBeenCalledOnceWith({ - items: [ - jasmine.objectContaining({ - schema: DOM_ACTION, - data: { - selector: "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", - type: "setHtml", - content: "Hello Treatment A!", - prehidingSelector: - "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", - qualifiedDate: 1683042673387, - displayedDate: 1683042673395 - }, - id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" - }), - jasmine.objectContaining({ - schema: DOM_ACTION, - data: { - selector: "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", - type: "setAttribute", - content: { - src: "img/demo-marketing-offer1-exp-A.png" - }, - prehidingSelector: - "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)", - qualifiedDate: 1683042673380, - displayedDate: 1683042673395 - }, - id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" - }), - jasmine.objectContaining({ - schema: MESSAGE_FEED_ITEM, - data: { - expiryDate: 1712190456, - publishedDate: 1678098240000, - meta: { - feedName: "Winter Promo", - surface: "web://mywebsite.com/feed" - }, - content: { - imageUrl: "img/twitter.png", - actionTitle: "Shop the sale!", - actionUrl: "https://luma.com/sale", - body: "Posting on social media helps us spread the word.", - title: "Thanks for sharing!" - }, - contentType: "application/json", - qualifiedDate: 1683042658312, - displayedDate: 1683042658316 - }, - id: "cfcb1af7-7bc2-45b2-a86a-0aa93fe69ce7" - }), - jasmine.objectContaining({ - schema: MESSAGE_FEED_ITEM, - data: { - expiryDate: 1712190456, - publishedDate: 1678184640000, - meta: { - feedName: "Winter Promo", - surface: "web://mywebsite.com/feed" - }, - content: { - imageUrl: "img/gold-coin.jpg", - actionTitle: "Shop the sale!", - actionUrl: "https://luma.com/sale", - body: "Now you're ready to earn!", - title: "Funds deposited!" - }, - contentType: "application/json", - qualifiedDate: 1683042653905, - displayedDate: 1683042653909 - }, - id: "0263e171-fa32-4c7a-9611-36b28137a81d" - }), - jasmine.objectContaining({ - schema: MESSAGE_FEED_ITEM, - data: { - expiryDate: 1712190456, - publishedDate: 1677839040000, - meta: { - feedName: "Winter Promo", - surface: "web://mywebsite.com/feed" + propositions: [ + { + id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" + }, + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)", + qualifiedDate: 1683042673380, + displayedDate: 1683042673395 + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" }, - content: { - imageUrl: "img/achievement.png", - actionTitle: "Shop the sale!", - actionUrl: "https://luma.com/sale", - body: "Great job, you completed your profile.", - title: "Achievement Unlocked!" + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + qualifiedDate: 1683042673387, + displayedDate: 1683042673395 + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + } + ], + scope: "web://mywebsite.com/feed" + }, + { + id: "1a3d874f-39ee-4310-bfa9-6559a10041a4", + items: [ + { + schema: "https://ns.adobe.com/personalization/message/feed-item", + data: { + expiryDate: 1712190456, + publishedDate: 1677752640000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/lumon.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "a handshake is available upon request.", + title: "Welcome to Lumon!" + }, + contentType: "application/json", + qualifiedDate: 1683042628060, + displayedDate: 1683042628070 + }, + id: "a48ca420-faea-467e-989a-5d179d9f562d" }, - contentType: "application/json", - qualifiedDate: 1683042628064, - displayedDate: 1683042628070 - }, - id: "b7173290-588f-40c6-a05c-43ed5ec08b28" - }), - jasmine.objectContaining({ - schema: MESSAGE_FEED_ITEM, - data: { - expiryDate: 1712190456, - publishedDate: 1677752640000, - meta: { - feedName: "Winter Promo", - surface: "web://mywebsite.com/feed" + { + schema: "https://ns.adobe.com/personalization/message/feed-item", + data: { + expiryDate: 1712190456, + publishedDate: 1677839040000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/achievement.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "Great job, you completed your profile.", + title: "Achievement Unlocked!" + }, + contentType: "application/json", + qualifiedDate: 1683042628064, + displayedDate: 1683042628070 + }, + id: "b7173290-588f-40c6-a05c-43ed5ec08b28" + } + ], + scope: "web://mywebsite.com/feed" + }, + { + id: "1ae11bc5-96dc-41c7-8f71-157c57a5290e", + items: [ + { + schema: "https://ns.adobe.com/personalization/message/feed-item", + data: { + expiryDate: 1712190456, + publishedDate: 1678098240000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/twitter.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "Posting on social media helps us spread the word.", + title: "Thanks for sharing!" + }, + contentType: "application/json", + qualifiedDate: 1683042658312, + displayedDate: 1683042658316 + }, + id: "cfcb1af7-7bc2-45b2-a86a-0aa93fe69ce7" + } + ], + scope: "web://mywebsite.com/feed", + scopeDetails: { + id: "1ae11bc5-96dc-41c7-8f71-157c57a5290e", + scope: "web://mywebsite.com/feed", + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: + "eyJtZXNzYWdlRXhlY3V0aW9uIjp7Im1lc3NhZ2VFeGVjdXRpb25JRCI6Ik5BIiwibWVzc2FnZUlEIjoiMDJjNzdlYTgtN2MwZS00ZDMzLTgwOTAtNGE1YmZkM2Q3NTAzIiwibWVzc2FnZVR5cGUiOiJtYXJrZXRpbmciLCJjYW1wYWlnbklEIjoiMzlhZThkNGItYjU1ZS00M2RjLWExNDMtNzdmNTAxOTViNDg3IiwiY2FtcGFpZ25WZXJzaW9uSUQiOiJiZDg1ZDllOC0yMDM3LTQyMmYtYjZkMi0zOTU3YzkwNTU5ZDMiLCJjYW1wYWlnbkFjdGlvbklEIjoiYjQ3ZmRlOGItNTdjMS00YmJlLWFlMjItNjRkNWI3ODJkMTgzIiwibWVzc2FnZVB1YmxpY2F0aW9uSUQiOiJhZTUyY2VkOC0yMDBjLTQ5N2UtODc4Ny1lZjljZmMxNzgyMTUifSwibWVzc2FnZVByb2ZpbGUiOnsiY2hhbm5lbCI6eyJfaWQiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbHMvd2ViIiwiX3R5cGUiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbC10eXBlcy93ZWIifSwibWVzc2FnZVByb2ZpbGVJRCI6ImY1Y2Q5OTk1LTZiNDQtNDIyMS05YWI3LTViNTMzOGQ1ZjE5MyJ9fQ==" + }, + strategies: [ + { + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" + } + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" + }, + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" + } + } + }, + { + id: "d1f7d411-a549-47bc-a4d8-c8e638b0a46b", + items: [ + { + schema: "https://ns.adobe.com/personalization/message/feed-item", + data: { + expiryDate: 1712190456, + publishedDate: 1678184640000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/gold-coin.jpg", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "Now you're ready to earn!", + title: "Funds deposited!" + }, + contentType: "application/json", + qualifiedDate: 1683042653905, + displayedDate: 1683042653909 + }, + id: "0263e171-fa32-4c7a-9611-36b28137a81d" + } + ], + scope: "web://mywebsite.com/feed" + } + ] + }); + }); + + it("filters out all surfaces", () => { + const { command, refresh } = subscribeRulesetItems; + + const callback = jasmine.createSpy(); + + // register a subscription. equivalent to alloy("subscribeRulesetItems", ... + command.run({ + surfaces: [], + schemas: [MESSAGE_FEED_ITEM], + callback + }); + + refresh(PROPOSITIONS); + expect(callback).not.toHaveBeenCalled(); + }); + + it("filters on surface", () => { + const { command, refresh } = subscribeRulesetItems; + + const callback = jasmine.createSpy(); + + // register a subscription. equivalent to alloy("subscribeRulesetItems", ... + command.run({ + surfaces: ["web://something.com"], + callback + }); + + refresh(PROPOSITIONS); + expect(callback).toHaveBeenCalledOnceWith({ + propositions: [ + { + id: "abc", + items: [ + jasmine.objectContaining({ + schema: DOM_ACTION, + data: { + selector: "a", + type: "setAttribute", + content: { + src: "img/test.png" + }, + prehidingSelector: "a", + qualifiedDate: 1694198274647, + displayedDate: 1694198274647 + }, + id: "aabbcc" + }) + ], + scope: "web://something.com", + scopeDetails: { + decisionProvider: "AJO" + } + } + ] + }); + }); + + it("returns all surfaces and schemas", () => { + const { command, refresh } = subscribeRulesetItems; + + const callback = jasmine.createSpy(); + + // register a subscription. equivalent to alloy("subscribeRulesetItems", ... + command.run({ + callback + }); + + refresh(PROPOSITIONS); + expect(callback).toHaveBeenCalledOnceWith({ + propositions: [ + { + id: "abc", + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "a", + type: "setAttribute", + content: { + src: "img/test.png" + }, + prehidingSelector: "a", + qualifiedDate: 1694198274647, + displayedDate: 1694198274647 + }, + id: "aabbcc" + } + ], + scope: "web://something.com", + scopeDetails: { + decisionProvider: "AJO" + } + }, + { + id: "2e4c7b28-b3e7-4d5b-ae6a-9ab0b44af87e", + items: [ + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: "HTML > BODY > DIV.offer:eq(0) > IMG:nth-of-type(1)", + type: "setAttribute", + content: { + src: "img/demo-marketing-offer1-exp-A.png" + }, + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(2) > IMG:nth-of-type(1)", + qualifiedDate: 1683042673380, + displayedDate: 1683042673395 + }, + id: "79129ecf-6430-4fbd-955a-b4f1dfdaa6fe" }, - content: { - imageUrl: "img/lumon.png", - actionTitle: "Shop the sale!", - actionUrl: "https://luma.com/sale", - body: "a handshake is available upon request.", - title: "Welcome to Lumon!" + { + schema: "https://ns.adobe.com/personalization/dom-action", + data: { + selector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + type: "setHtml", + content: "Hello Treatment A!", + prehidingSelector: + "HTML > BODY > DIV:nth-of-type(1) > H1:nth-of-type(1)", + qualifiedDate: 1683042673387, + displayedDate: 1683042673395 + }, + id: "10da709c-aa1a-40e5-84dd-966e2e8a1d5f" + } + ], + scope: "web://mywebsite.com/feed" + }, + { + id: "1a3d874f-39ee-4310-bfa9-6559a10041a4", + items: [ + { + schema: "https://ns.adobe.com/personalization/message/feed-item", + data: { + expiryDate: 1712190456, + publishedDate: 1677752640000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/lumon.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "a handshake is available upon request.", + title: "Welcome to Lumon!" + }, + contentType: "application/json", + qualifiedDate: 1683042628060, + displayedDate: 1683042628070 + }, + id: "a48ca420-faea-467e-989a-5d179d9f562d" }, - contentType: "application/json", - qualifiedDate: 1683042628060, - displayedDate: 1683042628070 - }, - id: "a48ca420-faea-467e-989a-5d179d9f562d" - }) + { + schema: "https://ns.adobe.com/personalization/message/feed-item", + data: { + expiryDate: 1712190456, + publishedDate: 1677839040000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/achievement.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "Great job, you completed your profile.", + title: "Achievement Unlocked!" + }, + contentType: "application/json", + qualifiedDate: 1683042628064, + displayedDate: 1683042628070 + }, + id: "b7173290-588f-40c6-a05c-43ed5ec08b28" + } + ], + scope: "web://mywebsite.com/feed" + }, + { + id: "1ae11bc5-96dc-41c7-8f71-157c57a5290e", + items: [ + { + schema: "https://ns.adobe.com/personalization/message/feed-item", + data: { + expiryDate: 1712190456, + publishedDate: 1678098240000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/twitter.png", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "Posting on social media helps us spread the word.", + title: "Thanks for sharing!" + }, + contentType: "application/json", + qualifiedDate: 1683042658312, + displayedDate: 1683042658316 + }, + id: "cfcb1af7-7bc2-45b2-a86a-0aa93fe69ce7" + } + ], + scope: "web://mywebsite.com/feed", + scopeDetails: { + id: "1ae11bc5-96dc-41c7-8f71-157c57a5290e", + scope: "web://mywebsite.com/feed", + scopeDetails: { + decisionProvider: "AJO", + characteristics: { + eventToken: + "eyJtZXNzYWdlRXhlY3V0aW9uIjp7Im1lc3NhZ2VFeGVjdXRpb25JRCI6Ik5BIiwibWVzc2FnZUlEIjoiMDJjNzdlYTgtN2MwZS00ZDMzLTgwOTAtNGE1YmZkM2Q3NTAzIiwibWVzc2FnZVR5cGUiOiJtYXJrZXRpbmciLCJjYW1wYWlnbklEIjoiMzlhZThkNGItYjU1ZS00M2RjLWExNDMtNzdmNTAxOTViNDg3IiwiY2FtcGFpZ25WZXJzaW9uSUQiOiJiZDg1ZDllOC0yMDM3LTQyMmYtYjZkMi0zOTU3YzkwNTU5ZDMiLCJjYW1wYWlnbkFjdGlvbklEIjoiYjQ3ZmRlOGItNTdjMS00YmJlLWFlMjItNjRkNWI3ODJkMTgzIiwibWVzc2FnZVB1YmxpY2F0aW9uSUQiOiJhZTUyY2VkOC0yMDBjLTQ5N2UtODc4Ny1lZjljZmMxNzgyMTUifSwibWVzc2FnZVByb2ZpbGUiOnsiY2hhbm5lbCI6eyJfaWQiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbHMvd2ViIiwiX3R5cGUiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbC10eXBlcy93ZWIifSwibWVzc2FnZVByb2ZpbGVJRCI6ImY1Y2Q5OTk1LTZiNDQtNDIyMS05YWI3LTViNTMzOGQ1ZjE5MyJ9fQ==" + }, + strategies: [ + { + strategyID: "3VQe3oIqiYq2RAsYzmDTSf", + treatmentID: "yu7rkogezumca7i0i44v" + } + ], + activity: { + id: + "39ae8d4b-b55e-43dc-a143-77f50195b487#b47fde8b-57c1-4bbe-ae22-64d5b782d183" + }, + correlationID: "02c77ea8-7c0e-4d33-8090-4a5bfd3d7503" + } + } + }, + { + id: "d1f7d411-a549-47bc-a4d8-c8e638b0a46b", + items: [ + { + schema: "https://ns.adobe.com/personalization/message/feed-item", + data: { + expiryDate: 1712190456, + publishedDate: 1678184640000, + meta: { + feedName: "Winter Promo", + surface: "web://mywebsite.com/feed" + }, + content: { + imageUrl: "img/gold-coin.jpg", + actionTitle: "Shop the sale!", + actionUrl: "https://luma.com/sale", + body: "Now you're ready to earn!", + title: "Funds deposited!" + }, + contentType: "application/json", + qualifiedDate: 1683042653905, + displayedDate: 1683042653909 + }, + id: "0263e171-fa32-4c7a-9611-36b28137a81d" + } + ], + scope: "web://mywebsite.com/feed" + } ] }); }); diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js index 0013c3b77..af01778e8 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js @@ -25,13 +25,13 @@ describe("DOM Actions on Iframe", () => { beforeEach(() => { cleanUpDomChanges("alloy-messaging-container"); cleanUpDomChanges("alloy-overlay-container"); - cleanUpDomChanges("alloy-iframe-id"); + cleanUpDomChanges("alloy-content-iframe"); }); afterEach(() => { cleanUpDomChanges("alloy-messaging-container"); cleanUpDomChanges("alloy-overlay-container"); - cleanUpDomChanges("alloy-iframe-id"); + cleanUpDomChanges("alloy-content-iframe"); }); describe("buildStyleFromParameters", () => { it("should build the style object correctly", () => { @@ -310,7 +310,7 @@ describe("DOM Actions on Iframe", () => { .createSpy("createIframe") .and.callFake(() => { const element = document.createElement("iframe"); - element.id = "alloy-iframe-id"; + element.id = "alloy-content-iframe"; return element; }); From 544cfb3cde111e73fc12ad5cd74ace309677de76 Mon Sep 17 00:00:00 2001 From: Happy Shandilya Date: Fri, 15 Sep 2023 09:07:00 -0700 Subject: [PATCH 47/66] functional tests (#1034) * tests * to debug issue in sauce lab * rephrased * clean up --- test/functional/helpers/createAlloyProxy.js | 3 +- .../specs/DecisioningEngine/C13348429.js | 217 +++++++++++++++++ .../specs/DecisioningEngine/C13405889.js | 220 +++++++++++++++++ .../specs/DecisioningEngine/C13419240.js | 222 ++++++++++++++++++ 4 files changed, 661 insertions(+), 1 deletion(-) create mode 100644 test/functional/specs/DecisioningEngine/C13348429.js create mode 100644 test/functional/specs/DecisioningEngine/C13405889.js create mode 100644 test/functional/specs/DecisioningEngine/C13419240.js diff --git a/test/functional/helpers/createAlloyProxy.js b/test/functional/helpers/createAlloyProxy.js index dd3a68e94..6de61e425 100644 --- a/test/functional/helpers/createAlloyProxy.js +++ b/test/functional/helpers/createAlloyProxy.js @@ -92,7 +92,8 @@ const commands = [ "appendIdentityToUrl", "applyPropositions", "subscribeRulesetItems", - "subscribeMessageFeed" + "subscribeMessageFeed", + "evaluateRulesets" ]; export default (instanceName = "alloy") => { diff --git a/test/functional/specs/DecisioningEngine/C13348429.js b/test/functional/specs/DecisioningEngine/C13348429.js new file mode 100644 index 000000000..d54c5b3d3 --- /dev/null +++ b/test/functional/specs/DecisioningEngine/C13348429.js @@ -0,0 +1,217 @@ +/* +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 { ClientFunction, t } from "testcafe"; +import createNetworkLogger from "../../helpers/networkLogger"; +import createFixture from "../../helpers/createFixture"; +import { TEST_PAGE as TEST_PAGE_URL } from "../../helpers/constants/url"; +import createAlloyProxy from "../../helpers/createAlloyProxy"; +import getBaseConfig from "../../helpers/getBaseConfig"; +import { compose, debugEnabled } from "../../helpers/constants/configParts"; + +const networkLogger = createNetworkLogger(); +const organizationId = "4DA0571C5FDC4BF70A495FC2@AdobeOrg"; +const dataStreamId = "7a19c434-6648-48d3-948f-ba0258505d98"; + +const orgMainConfigMain = getBaseConfig(organizationId, dataStreamId); +const config = compose(orgMainConfigMain, debugEnabled); + +const mockResponse = { + requestId: "5a38a9ef-67d7-4f66-8977-c4dc0e0967b6", + handle: [ + { + payload: [ + { + id: "a4486740-0a6d-433a-8b65-bfb3ac20485d", + scope: "mobileapp://com.adobe.aguaAppIos", + scopeDetails: { + decisionProvider: "AJO", + correlationID: "a6b7639b-6606-42af-9679-48eb138632d2", + characteristics: { + eventToken: + "eyJtZXNzYWdlRXhlY3V0aW9uIjp7Im1lc3NhZ2VFeGVjdXRpb25JRCI6Ik5BIiwibWVzc2FnZUlEIjoiYTZiNzYzOWItNjYwNi00MmFmLTk2NzktNDhlYjEzODYzMmQyIiwibWVzc2FnZVB1YmxpY2F0aW9uSUQiOiIzZjQxZGNjNy1mNDE0LTRlMmYtYTdjOS1hMTk4ODdlYzNlNWEiLCJtZXNzYWdlVHlwZSI6Im1hcmtldGluZyIsImNhbXBhaWduSUQiOiIwY2RmMDFkZi02ZmE5LTQ0MjktOGE3My05M2ZiY2U1NTIyYWEiLCJjYW1wYWlnblZlcnNpb25JRCI6ImFiYWVhMThhLTJmNzEtNDZlMy1iZWRmLTUxNzg0YTE4MWJiZiIsImNhbXBhaWduQWN0aW9uSUQiOiIzZmIxMTY1OC1iOTMyLTRlMDktYWIyNy03ZWEyOTc2NzY2YTUifSwibWVzc2FnZVByb2ZpbGUiOnsibWVzc2FnZVByb2ZpbGVJRCI6ImVlY2U5NDNlLWVlNWYtNGMwNC1iZGI1LTQ5YjFhMjViMTNmZSIsImNoYW5uZWwiOnsiX2lkIjoiaHR0cHM6Ly9ucy5hZG9iZS5jb20veGRtL2NoYW5uZWxzL2luQXBwIiwiX3R5cGUiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbC10eXBlcy9pbkFwcCJ9fX0=" + }, + activity: { + id: + "0cdf01df-6fa9-4429-8a73-93fbce5522aa#3fb11658-b932-4e09-ab27-7ea2976766a5" + } + }, + items: [ + { + id: "f5134bfa-381e-4b94-8546-d7023e1f3601", + schema: "https://ns.adobe.com/personalization/ruleset-item", + data: { + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [ + { + definition: { + conditions: [ + { + definition: { + key: "~type", + matcher: "eq", + values: [ + "com.adobe.eventType.generic.track" + ] + }, + type: "matcher" + }, + { + definition: { + key: "~source", + matcher: "eq", + values: [ + "com.adobe.eventSource.requestContent" + ] + }, + type: "matcher" + }, + { + definition: { + key: + "~state.com.adobe.module.lifecycle/lifecyclecontextdata.dayofweek", + matcher: "eq", + values: [2] + }, + type: "matcher" + } + ], + logic: "and" + }, + type: "group" + } + ], + logic: "and" + }, + type: "group" + }, + consequences: [ + { + id: "c46c7d03-eb06-4596-9087-272486cb6c41", + type: "cjmiam", + detail: { + mobileParameters: { + verticalAlign: "center", + dismissAnimation: "top", + verticalInset: 0, + backdropOpacity: 0.75, + gestures: { + swipeUp: "adbinapp://dismiss?interaction=swipeUp", + swipeDown: + "adbinapp://dismiss?interaction=swipeDown", + swipeLeft: + "adbinapp://dismiss?interaction=swipeLeft", + swipeRight: + "adbinapp://dismiss?interaction=swipeRight", + tapBackground: + "adbinapp://dismiss?interaction=tapBackground" + }, + cornerRadius: 15, + horizontalInset: 0, + uiTakeover: true, + horizontalAlign: "center", + displayAnimation: "top", + width: 80, + backdropColor: "#ffa500", + height: 60 + }, + html: + '\n\n \n \n\n \n \n \n \n \n\n \n \n \n \n \n\n \n

    Fifty percent off!

    One hour only!

    \n \n\n', + remoteAssets: [ + "https://media3.giphy.com/media/R7ifMrDG24Uc89TpZH/giphy.gif?cid=ecf05e47ohtez4exx2e0u3x1zko365r8pw6lqw0qtjq32z2h&ep=v1_gifs_search&rid=giphy.gif&ct=g" + ] + } + } + ] + } + ] + } + } + ] + } + ], + type: "personalization:decisions", + eventIndex: 0 + }, + { + payload: [ + { + scope: "Target", + hint: "35", + ttlSeconds: 1800 + }, + { + scope: "AAM", + hint: "9", + ttlSeconds: 1800 + }, + { + scope: "EdgeNetwork", + hint: "or2", + ttlSeconds: 1800 + } + ], + type: "locationHint:result" + }, + { + payload: [ + { + key: "kndctr_4DA0571C5FDC4BF70A495FC2_AdobeOrg_cluster", + value: "or2", + maxAge: 1800, + attrs: { + SameSite: "None" + } + } + ], + type: "state:store" + } + ] +}; + +createFixture({ + title: "Test C13348429: Verify DOM action using the applyResponse command.", + requestHooks: [networkLogger.edgeEndpointLogs], + url: `${TEST_PAGE_URL}?test=C13348429` +}); + +test.meta({ + ID: "C13348429", + SEVERITY: "P0", + TEST_RUN: "Regression" +}); + +const getIframeContainer = ClientFunction(() => { + const element = document.querySelector("#alloy-messaging-container"); + return element ? element.innerHTML : ""; +}); + +test("Test C13348429: Verify DOM action using the applyResponse command.", async () => { + const alloy = createAlloyProxy(); + await alloy.configure(config); + await alloy.sendEvent({}); + + await alloy.applyResponse({ + renderDecisions: true, + decisionContext: { + "~type": "com.adobe.eventType.generic.track", + "~source": "com.adobe.eventSource.requestContent", + "~state.com.adobe.module.lifecycle/lifecyclecontextdata.dayofweek": 2 + }, + responseBody: mockResponse + }); + const containerElement = await getIframeContainer(); + await t.expect(containerElement).contains("alloy-content-iframe"); +}); diff --git a/test/functional/specs/DecisioningEngine/C13405889.js b/test/functional/specs/DecisioningEngine/C13405889.js new file mode 100644 index 000000000..1d10b13ff --- /dev/null +++ b/test/functional/specs/DecisioningEngine/C13405889.js @@ -0,0 +1,220 @@ +/* +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 { ClientFunction, t } from "testcafe"; +import createNetworkLogger from "../../helpers/networkLogger"; +import createFixture from "../../helpers/createFixture"; +import { TEST_PAGE as TEST_PAGE_URL } from "../../helpers/constants/url"; +import createAlloyProxy from "../../helpers/createAlloyProxy"; +import getBaseConfig from "../../helpers/getBaseConfig"; +import { compose, debugEnabled } from "../../helpers/constants/configParts"; + +const networkLogger = createNetworkLogger(); +const organizationId = "4DA0571C5FDC4BF70A495FC2@AdobeOrg"; +const dataStreamId = "7a19c434-6648-48d3-948f-ba0258505d98"; + +const orgMainConfigMain = getBaseConfig(organizationId, dataStreamId); +const config = compose(orgMainConfigMain, debugEnabled); + +const mockResponse = { + requestId: "5a38a9ef-67d7-4f66-8977-c4dc0e0967b6", + handle: [ + { + payload: [ + { + id: "a4486740-0a6d-433a-8b65-bfb3ac20485d", + scope: "mobileapp://com.adobe.aguaAppIos", + scopeDetails: { + decisionProvider: "AJO", + correlationID: "a6b7639b-6606-42af-9679-48eb138632d2", + characteristics: { + eventToken: + "eyJtZXNzYWdlRXhlY3V0aW9uIjp7Im1lc3NhZ2VFeGVjdXRpb25JRCI6Ik5BIiwibWVzc2FnZUlEIjoiYTZiNzYzOWItNjYwNi00MmFmLTk2NzktNDhlYjEzODYzMmQyIiwibWVzc2FnZVB1YmxpY2F0aW9uSUQiOiIzZjQxZGNjNy1mNDE0LTRlMmYtYTdjOS1hMTk4ODdlYzNlNWEiLCJtZXNzYWdlVHlwZSI6Im1hcmtldGluZyIsImNhbXBhaWduSUQiOiIwY2RmMDFkZi02ZmE5LTQ0MjktOGE3My05M2ZiY2U1NTIyYWEiLCJjYW1wYWlnblZlcnNpb25JRCI6ImFiYWVhMThhLTJmNzEtNDZlMy1iZWRmLTUxNzg0YTE4MWJiZiIsImNhbXBhaWduQWN0aW9uSUQiOiIzZmIxMTY1OC1iOTMyLTRlMDktYWIyNy03ZWEyOTc2NzY2YTUifSwibWVzc2FnZVByb2ZpbGUiOnsibWVzc2FnZVByb2ZpbGVJRCI6ImVlY2U5NDNlLWVlNWYtNGMwNC1iZGI1LTQ5YjFhMjViMTNmZSIsImNoYW5uZWwiOnsiX2lkIjoiaHR0cHM6Ly9ucy5hZG9iZS5jb20veGRtL2NoYW5uZWxzL2luQXBwIiwiX3R5cGUiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbC10eXBlcy9pbkFwcCJ9fX0=" + }, + activity: { + id: + "0cdf01df-6fa9-4429-8a73-93fbce5522aa#3fb11658-b932-4e09-ab27-7ea2976766a5" + } + }, + items: [ + { + id: "f5134bfa-381e-4b94-8546-d7023e1f3601", + schema: "https://ns.adobe.com/personalization/ruleset-item", + data: { + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [ + { + definition: { + conditions: [ + { + definition: { + key: "~type", + matcher: "eq", + values: [ + "com.adobe.eventType.generic.track" + ] + }, + type: "matcher" + }, + { + definition: { + key: "~source", + matcher: "eq", + values: [ + "com.adobe.eventSource.requestContent" + ] + }, + type: "matcher" + }, + { + definition: { + key: + "~state.com.adobe.module.lifecycle/lifecyclecontextdata.dayofweek", + matcher: "eq", + values: [2] + }, + type: "matcher" + } + ], + logic: "and" + }, + type: "group" + } + ], + logic: "and" + }, + type: "group" + }, + consequences: [ + { + id: "c46c7d03-eb06-4596-9087-272486cb6c41", + type: "cjmiam", + detail: { + mobileParameters: { + verticalAlign: "center", + dismissAnimation: "top", + verticalInset: 0, + backdropOpacity: 0.75, + gestures: { + swipeUp: "adbinapp://dismiss?interaction=swipeUp", + swipeDown: + "adbinapp://dismiss?interaction=swipeDown", + swipeLeft: + "adbinapp://dismiss?interaction=swipeLeft", + swipeRight: + "adbinapp://dismiss?interaction=swipeRight", + tapBackground: + "adbinapp://dismiss?interaction=tapBackground" + }, + cornerRadius: 15, + horizontalInset: 0, + uiTakeover: true, + horizontalAlign: "center", + displayAnimation: "top", + width: 80, + backdropColor: "#ffa500", + height: 60 + }, + html: + '\n\n \n \n\n \n \n \n \n \n\n \n \n \n \n \n\n \n

    Fifty percent off!

    One hour only!

    \n \n\n', + remoteAssets: [ + "https://media3.giphy.com/media/R7ifMrDG24Uc89TpZH/giphy.gif?cid=ecf05e47ohtez4exx2e0u3x1zko365r8pw6lqw0qtjq32z2h&ep=v1_gifs_search&rid=giphy.gif&ct=g" + ] + } + } + ] + } + ] + } + } + ] + } + ], + type: "personalization:decisions", + eventIndex: 0 + }, + { + payload: [ + { + scope: "Target", + hint: "35", + ttlSeconds: 1800 + }, + { + scope: "AAM", + hint: "9", + ttlSeconds: 1800 + }, + { + scope: "EdgeNetwork", + hint: "or2", + ttlSeconds: 1800 + } + ], + type: "locationHint:result" + }, + { + payload: [ + { + key: "kndctr_4DA0571C5FDC4BF70A495FC2_AdobeOrg_cluster", + value: "or2", + maxAge: 1800, + attrs: { + SameSite: "None" + } + } + ], + type: "state:store" + } + ] +}; + +createFixture({ + title: "Test C13405889: Verify DOM action using the evaluateRulesets command", + requestHooks: [networkLogger.edgeEndpointLogs], + url: `${TEST_PAGE_URL}?test=C13348429` +}); + +test.meta({ + ID: "C13405889", + SEVERITY: "P0", + TEST_RUN: "Regression" +}); + +const getIframeContainer = ClientFunction(() => { + const element = document.querySelector("#alloy-messaging-container"); + return element ? element.innerHTML : ""; +}); + +test("Test C13405889: Verify DOM action using the evaluateRulesets command", async () => { + const alloy = createAlloyProxy(); + await alloy.configure(config); + await alloy.sendEvent({}); + + await alloy.applyResponse({ + renderDecisions: false, + responseBody: mockResponse + }); + await alloy.evaluateRulesets({ + renderDecisions: true, + decisionContext: { + "~type": "com.adobe.eventType.generic.track", + "~source": "com.adobe.eventSource.requestContent", + "~state.com.adobe.module.lifecycle/lifecyclecontextdata.dayofweek": 2 + } + }); + const containerElement = await getIframeContainer(); + await t.expect(containerElement).contains("alloy-content-iframe"); +}); diff --git a/test/functional/specs/DecisioningEngine/C13419240.js b/test/functional/specs/DecisioningEngine/C13419240.js new file mode 100644 index 000000000..09619111b --- /dev/null +++ b/test/functional/specs/DecisioningEngine/C13419240.js @@ -0,0 +1,222 @@ +/* +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 { ClientFunction, t } from "testcafe"; +import createNetworkLogger from "../../helpers/networkLogger"; +import createFixture from "../../helpers/createFixture"; +import { TEST_PAGE as TEST_PAGE_URL } from "../../helpers/constants/url"; +import createAlloyProxy from "../../helpers/createAlloyProxy"; +import getBaseConfig from "../../helpers/getBaseConfig"; +import { compose, debugEnabled } from "../../helpers/constants/configParts"; + +const networkLogger = createNetworkLogger(); +const organizationId = "4DA0571C5FDC4BF70A495FC2@AdobeOrg"; +const dataStreamId = "7a19c434-6648-48d3-948f-ba0258505d98"; + +const orgMainConfigMain = getBaseConfig(organizationId, dataStreamId); +const config = compose(orgMainConfigMain, debugEnabled); + +const mockResponse = { + requestId: "5a38a9ef-67d7-4f66-8977-c4dc0e0967b6", + handle: [ + { + payload: [ + { + id: "a4486740-0a6d-433a-8b65-bfb3ac20485d", + scope: "mobileapp://com.adobe.aguaAppIos", + scopeDetails: { + decisionProvider: "AJO", + correlationID: "a6b7639b-6606-42af-9679-48eb138632d2", + characteristics: { + eventToken: + "eyJtZXNzYWdlRXhlY3V0aW9uIjp7Im1lc3NhZ2VFeGVjdXRpb25JRCI6Ik5BIiwibWVzc2FnZUlEIjoiYTZiNzYzOWItNjYwNi00MmFmLTk2NzktNDhlYjEzODYzMmQyIiwibWVzc2FnZVB1YmxpY2F0aW9uSUQiOiIzZjQxZGNjNy1mNDE0LTRlMmYtYTdjOS1hMTk4ODdlYzNlNWEiLCJtZXNzYWdlVHlwZSI6Im1hcmtldGluZyIsImNhbXBhaWduSUQiOiIwY2RmMDFkZi02ZmE5LTQ0MjktOGE3My05M2ZiY2U1NTIyYWEiLCJjYW1wYWlnblZlcnNpb25JRCI6ImFiYWVhMThhLTJmNzEtNDZlMy1iZWRmLTUxNzg0YTE4MWJiZiIsImNhbXBhaWduQWN0aW9uSUQiOiIzZmIxMTY1OC1iOTMyLTRlMDktYWIyNy03ZWEyOTc2NzY2YTUifSwibWVzc2FnZVByb2ZpbGUiOnsibWVzc2FnZVByb2ZpbGVJRCI6ImVlY2U5NDNlLWVlNWYtNGMwNC1iZGI1LTQ5YjFhMjViMTNmZSIsImNoYW5uZWwiOnsiX2lkIjoiaHR0cHM6Ly9ucy5hZG9iZS5jb20veGRtL2NoYW5uZWxzL2luQXBwIiwiX3R5cGUiOiJodHRwczovL25zLmFkb2JlLmNvbS94ZG0vY2hhbm5lbC10eXBlcy9pbkFwcCJ9fX0=" + }, + activity: { + id: + "0cdf01df-6fa9-4429-8a73-93fbce5522aa#3fb11658-b932-4e09-ab27-7ea2976766a5" + } + }, + items: [ + { + id: "f5134bfa-381e-4b94-8546-d7023e1f3601", + schema: "https://ns.adobe.com/personalization/ruleset-item", + data: { + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [ + { + definition: { + conditions: [ + { + definition: { + key: "~type", + matcher: "eq", + values: [ + "com.adobe.eventType.generic.track" + ] + }, + type: "matcher" + }, + { + definition: { + key: "~source", + matcher: "eq", + values: [ + "com.adobe.eventSource.requestContent" + ] + }, + type: "matcher" + }, + { + definition: { + key: + "~state.com.adobe.module.lifecycle/lifecyclecontextdata.dayofweek", + matcher: "eq", + values: [2] + }, + type: "matcher" + } + ], + logic: "and" + }, + type: "group" + } + ], + logic: "and" + }, + type: "group" + }, + consequences: [ + { + id: "c46c7d03-eb06-4596-9087-272486cb6c41", + type: "cjmiam", + detail: { + mobileParameters: { + verticalAlign: "center", + dismissAnimation: "top", + verticalInset: 0, + backdropOpacity: 0.75, + gestures: { + swipeUp: "adbinapp://dismiss?interaction=swipeUp", + swipeDown: + "adbinapp://dismiss?interaction=swipeDown", + swipeLeft: + "adbinapp://dismiss?interaction=swipeLeft", + swipeRight: + "adbinapp://dismiss?interaction=swipeRight", + tapBackground: + "adbinapp://dismiss?interaction=tapBackground" + }, + cornerRadius: 15, + horizontalInset: 0, + uiTakeover: true, + horizontalAlign: "center", + displayAnimation: "top", + width: 80, + backdropColor: "#ffa500", + height: 60 + }, + html: + '\n\n \n \n\n \n \n \n \n \n\n \n \n \n \n \n\n \n

    Fifty percent off!

    One hour only!

    \n \n\n', + remoteAssets: [ + "https://media3.giphy.com/media/R7ifMrDG24Uc89TpZH/giphy.gif?cid=ecf05e47ohtez4exx2e0u3x1zko365r8pw6lqw0qtjq32z2h&ep=v1_gifs_search&rid=giphy.gif&ct=g" + ] + } + } + ] + } + ] + } + } + ] + } + ], + type: "personalization:decisions", + eventIndex: 0 + }, + { + payload: [ + { + scope: "Target", + hint: "35", + ttlSeconds: 1800 + }, + { + scope: "AAM", + hint: "9", + ttlSeconds: 1800 + }, + { + scope: "EdgeNetwork", + hint: "or2", + ttlSeconds: 1800 + } + ], + type: "locationHint:result" + }, + { + payload: [ + { + key: "kndctr_4DA0571C5FDC4BF70A495FC2_AdobeOrg_cluster", + value: "or2", + maxAge: 1800, + attrs: { + SameSite: "None" + } + } + ], + type: "state:store" + } + ] +}; + +createFixture({ + title: "Test C13419240: Verify DOM action using the sendEvent command", + requestHooks: [networkLogger.edgeEndpointLogs], + url: `${TEST_PAGE_URL}?test=C13348429` +}); + +test.meta({ + ID: "C13419240", + SEVERITY: "P0", + TEST_RUN: "Regression" +}); + +const getIframeContainer = ClientFunction(() => { + const element = document.querySelector("#alloy-messaging-container"); + return element ? element.innerHTML : ""; +}); + +test("Test C13419240: Verify DOM action using the sendEvent command", async () => { + const alloy = createAlloyProxy(); + await alloy.configure(config); + await alloy.sendEvent({}); // establish an identity + + await alloy.applyResponse({ + renderDecisions: false, + responseBody: mockResponse + }); + + await alloy.sendEvent({ + renderDecisions: true, + decisionContext: { + "~type": "com.adobe.eventType.generic.track", + "~source": "com.adobe.eventSource.requestContent", + "~state.com.adobe.module.lifecycle/lifecyclecontextdata.dayofweek": 2 + } + }); + + const containerElement = await getIframeContainer(); + await t.expect(containerElement).contains("alloy-content-iframe"); +}); From a3f09aeeb72098dd0bfae780568b1616257c8b8d Mon Sep 17 00:00:00 2001 From: Happy Shandilya Date: Fri, 22 Sep 2023 11:09:39 -0700 Subject: [PATCH 48/66] web parameters support (#1035) * web parameters support removed redundant files & some clean up * code refactoring * adding test for web parameteres * code refactoring based on review * webProperties (#1039) * webProperties * webProperties * improve isValidWebParameters --------- Co-authored-by: Jason Waters Co-authored-by: Jason Waters --- .../inAppMessageConsequenceAdapter.js | 2 + .../actions/displayIframeContent.js | 278 ++++++++++++------ .../in-app-message-actions/utils.js | 5 + .../actions/displayBanner.spec.js | 78 ----- .../actions/displayIframeContent.spec.js | 191 ++++++++---- .../actions/displayModal.spec.js | 74 ----- 6 files changed, 319 insertions(+), 309 deletions(-) delete mode 100644 test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js delete mode 100644 test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js diff --git a/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js b/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js index 1249d8bc8..0bf3090b3 100644 --- a/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js +++ b/src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js @@ -13,8 +13,10 @@ import { MESSAGE_IN_APP } from "../../Personalization/constants/schema"; import { TEXT_HTML } from "../../Personalization/constants/contentType"; export default (id, type, detail) => { + // TODO: add webParameters when available from the authoring UI in detail const { html, mobileParameters } = detail; + // TODO: Remove it once we have webParameters const webParameters = { info: "this is a placeholder" }; return { diff --git a/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js b/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js index 39fcacf9b..4582c1df6 100644 --- a/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js +++ b/src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js @@ -11,73 +11,19 @@ governing permissions and limitations under the License. */ import { getNonce } from "../../dom-actions/dom"; -import { parseAnchor, removeElementById } from "../utils"; +import { createElement, parseAnchor, removeElementById } from "../utils"; import { TEXT_HTML } from "../../constants/contentType"; +import { assign } from "../../../../utils"; import { getEventType } from "../../constants/propositionEventType"; -const ELEMENT_TAG_CLASSNAME = "alloy-messaging-container"; -const ELEMENT_TAG_ID = "alloy-messaging-container"; - -const OVERLAY_TAG_CLASSNAME = "alloy-overlay-container"; -const OVERLAY_TAG_ID = "alloy-overlay-container"; +const ALLOY_MESSAGING_CONTAINER_ID = "alloy-messaging-container"; +const ALLOY_OVERLAY_CONTAINER_ID = "alloy-overlay-container"; const ALLOY_IFRAME_ID = "alloy-content-iframe"; const dismissMessage = () => - [ELEMENT_TAG_ID, OVERLAY_TAG_ID].forEach(removeElementById); - -// eslint-disable-next-line no-unused-vars -export const buildStyleFromParameters = (mobileParameters, webParameters) => { - const { - verticalAlign, - width, - horizontalAlign, - backdropColor, - height, - cornerRadius, - horizontalInset, - verticalInset, - uiTakeover = false - } = mobileParameters; - - const style = { - width: width ? `${width}%` : "100%", - backgroundColor: backdropColor || "rgba(0, 0, 0, 0.5)", - borderRadius: cornerRadius ? `${cornerRadius}px` : "0px", - border: "none", - position: uiTakeover ? "fixed" : "relative", - overflow: "hidden" - }; - if (horizontalAlign === "left") { - style.left = horizontalInset ? `${horizontalInset}%` : "0"; - } else if (horizontalAlign === "right") { - style.right = horizontalInset ? `${horizontalInset}%` : "0"; - } else if (horizontalAlign === "center") { - style.left = "50%"; - style.transform = "translateX(-50%)"; - } - - if (verticalAlign === "top") { - style.top = verticalInset ? `${verticalInset}%` : "0"; - } else if (verticalAlign === "bottom") { - style.position = "fixed"; - style.bottom = verticalInset ? `${verticalInset}%` : "0"; - } else if (verticalAlign === "center") { - style.top = "50%"; - style.transform = `${ - horizontalAlign === "center" ? `${style.transform} ` : "" - }translateY(-50%)`; - style.display = "flex"; - style.alignItems = "center"; - style.justifyContent = "center"; - } - - if (height) { - style.height = `${height}vh`; - } else { - style.height = "100%"; - } - return style; -}; + [ALLOY_MESSAGING_CONTAINER_ID, ALLOY_OVERLAY_CONTAINER_ID].forEach( + removeElementById + ); const setWindowLocationHref = link => { window.location.assign(link); @@ -132,13 +78,6 @@ export const createIframe = (htmlContent, clickHandler) => { new Blob([htmlDocument.documentElement.outerHTML], { type: TEXT_HTML }) ); element.id = ALLOY_IFRAME_ID; - - Object.assign(element.style, { - border: "none", - width: "100%", - height: "100%" - }); - element.addEventListener("load", () => { const { addEventListener } = element.contentDocument || element.contentWindow.document; @@ -148,60 +87,203 @@ export const createIframe = (htmlContent, clickHandler) => { return element; }; -export const createContainerElement = settings => { - const { mobileParameters = {}, webParameters = {} } = settings; - const element = document.createElement("div"); - element.id = ELEMENT_TAG_ID; - element.className = `${ELEMENT_TAG_CLASSNAME}`; - Object.assign( - element.style, - buildStyleFromParameters(mobileParameters, webParameters) - ); - - return element; +const renderMessage = (iframe, webParameters, container, overlay) => { + [ + { id: ALLOY_OVERLAY_CONTAINER_ID, element: overlay }, + { id: ALLOY_MESSAGING_CONTAINER_ID, element: container }, + { id: ALLOY_IFRAME_ID, element: iframe } + ].forEach(({ id, element }) => { + const { style = {}, params = {} } = webParameters[id]; + + assign(element.style, style); + + const { + parentElement = "body", + insertionMethod = "appendChild", + enabled = true + } = params; + + const parent = document.querySelector(parentElement); + if (enabled && parent && typeof parent[insertionMethod] === "function") { + parent[insertionMethod](element); + } + }); }; -export const createOverlayElement = parameter => { - const element = document.createElement("div"); - const backdropOpacity = parameter.backdropOpacity || 0.5; - const backdropColor = parameter.backdropColor || "#FFFFFF"; - element.id = OVERLAY_TAG_ID; - element.className = `${OVERLAY_TAG_CLASSNAME}`; +export const buildStyleFromMobileParameters = mobileParameters => { + const { + verticalAlign, + width, + horizontalAlign, + backdropColor, + height, + cornerRadius, + horizontalInset, + verticalInset, + uiTakeover = false + } = mobileParameters; - Object.assign(element.style, { + const style = { + width: width ? `${width}%` : "100%", + backgroundColor: backdropColor || "rgba(0, 0, 0, 0.5)", + borderRadius: cornerRadius ? `${cornerRadius}px` : "0px", + border: "none", + position: uiTakeover ? "fixed" : "relative", + overflow: "hidden" + }; + if (horizontalAlign === "left") { + style.left = horizontalInset ? `${horizontalInset}%` : "0"; + } else if (horizontalAlign === "right") { + style.right = horizontalInset ? `${horizontalInset}%` : "0"; + } else if (horizontalAlign === "center") { + style.left = "50%"; + style.transform = "translateX(-50%)"; + } + + if (verticalAlign === "top") { + style.top = verticalInset ? `${verticalInset}%` : "0"; + } else if (verticalAlign === "bottom") { + style.position = "fixed"; + style.bottom = verticalInset ? `${verticalInset}%` : "0"; + } else if (verticalAlign === "center") { + style.top = "50%"; + style.transform = `${ + horizontalAlign === "center" ? `${style.transform} ` : "" + }translateY(-50%)`; + style.display = "flex"; + style.alignItems = "center"; + style.justifyContent = "center"; + } + + if (height) { + style.height = `${height}vh`; + } else { + style.height = "100%"; + } + return style; +}; + +export const mobileOverlay = mobileParameters => { + const { backdropOpacity, backdropColor } = mobileParameters; + const opacity = backdropOpacity || 0.5; + const color = backdropColor || "#FFFFFF"; + const style = { position: "fixed", top: "0", left: "0", width: "100%", height: "100%", background: "transparent", - opacity: backdropOpacity, - backgroundColor: backdropColor - }); + opacity, + backgroundColor: color + }; + return style; +}; - return element; +const REQUIRED_PARAMS = ["enabled", "parentElement", "insertionMethod"]; + +const isValidWebParameters = webParameters => { + if (!webParameters) { + return false; + } + + const ids = Object.keys(webParameters); + + if (!ids.includes(ALLOY_MESSAGING_CONTAINER_ID)) { + return false; + } + + if (!ids.includes(ALLOY_OVERLAY_CONTAINER_ID)) { + return false; + } + + const values = Object.values(webParameters); + + for (let i = 0; i < values.length; i += 1) { + if (!Object.prototype.hasOwnProperty.call(values[i], "style")) { + return false; + } + + if (!Object.prototype.hasOwnProperty.call(values[i], "params")) { + return false; + } + + for (let j = 0; j < REQUIRED_PARAMS.length; j += 1) { + if ( + !Object.prototype.hasOwnProperty.call( + values[i].params, + REQUIRED_PARAMS[j] + ) + ) { + return false; + } + } + } + + return true; +}; + +const generateWebParameters = mobileParameters => { + if (!mobileParameters) { + return undefined; + } + + const { uiTakeover = false } = mobileParameters; + + return { + [ALLOY_IFRAME_ID]: { + style: { + border: "none", + width: "100%", + height: "100%" + }, + params: { + enabled: true, + parentElement: "#alloy-messaging-container", + insertionMethod: "appendChild" + } + }, + [ALLOY_MESSAGING_CONTAINER_ID]: { + style: buildStyleFromMobileParameters(mobileParameters), + params: { + enabled: true, + parentElement: "body", + insertionMethod: "appendChild" + } + }, + [ALLOY_OVERLAY_CONTAINER_ID]: { + style: mobileOverlay(mobileParameters), + params: { + enabled: uiTakeover === true, + parentElement: "body", + insertionMethod: "appendChild" + } + } + }; }; export const displayHTMLContentInIframe = (settings, interact) => { dismissMessage(); const { content, contentType, mobileParameters } = settings; + let { webParameters } = settings; if (contentType !== TEXT_HTML) { return; } - const container = createContainerElement(settings); - + const container = createElement(ALLOY_MESSAGING_CONTAINER_ID); const iframe = createIframe(content, createIframeClickHandler(interact)); + const overlay = createElement(ALLOY_OVERLAY_CONTAINER_ID); - container.appendChild(iframe); + if (!isValidWebParameters(webParameters)) { + webParameters = generateWebParameters(mobileParameters); + } - if (mobileParameters.uiTakeover) { - const overlay = createOverlayElement(mobileParameters); - document.body.appendChild(overlay); - document.body.style.overflow = "hidden"; + if (!webParameters) { + return; } - document.body.appendChild(container); + + renderMessage(iframe, webParameters, container, overlay); }; export default (settings, collect) => { diff --git a/src/components/Personalization/in-app-message-actions/utils.js b/src/components/Personalization/in-app-message-actions/utils.js index b8f82bc49..aa4f42567 100644 --- a/src/components/Personalization/in-app-message-actions/utils.js +++ b/src/components/Personalization/in-app-message-actions/utils.js @@ -74,3 +74,8 @@ export const parseAnchor = anchor => { uuid }; }; +export const createElement = elementTagId => { + const element = document.createElement("div"); + element.id = elementTagId; + return element; +}; diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js deleted file mode 100644 index 9b85d9f29..000000000 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayBanner.spec.js +++ /dev/null @@ -1,78 +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 { createNode } from "../../../../../../../src/utils/dom"; -import { DIV } from "../../../../../../../src/constants/tagName"; -import displayIframeContent from "../../../../../../../src/components/Personalization/in-app-message-actions/actions/displayIframeContent"; -import { TEXT_HTML } from "../../../../../../../src/components/Personalization/constants/contentType"; - -describe("Personalization::IAM:banner", () => { - it("inserts banner into dom", async () => { - const something = createNode( - DIV, - { className: "something" }, - { - innerHTML: - "

    Amet cillum consectetur elit cupidatat voluptate nisi duis et occaecat enim pariatur.

    " - } - ); - - document.body.append(something); - - await displayIframeContent({ - mobileParameters: { - verticalAlign: "center", - dismissAnimation: "top", - verticalInset: 0, - backdropOpacity: 0.2, - cornerRadius: 15, - horizontalInset: 0, - uiTakeover: false, - horizontalAlign: "center", - width: 80, - displayAnimation: "top", - backdropColor: "#000000", - height: 60 - }, - content: `
    banner
    Alf Says`, - contentType: TEXT_HTML - }); - - const overlayContainer = document.querySelector( - "div#alloy-overlay-container" - ); - const messagingContainer = document.querySelector( - "div#alloy-messaging-container" - ); - - expect(overlayContainer).toBeNull(); - expect(messagingContainer).not.toBeNull(); - - expect(messagingContainer.parentNode).toEqual(document.body); - expect(messagingContainer.nextElementSibling).toBeNull(); - - const iframe = document.querySelector( - ".alloy-messaging-container > iframe" - ); - - expect(iframe).not.toBeNull(); - - await new Promise(resolve => { - iframe.addEventListener("load", () => { - resolve(); - }); - }); - - expect( - (iframe.contentDocument || iframe.contentWindow.document).body.outerHTML - ).toContain("Alf Says"); - }); -}); diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js index 3e836449c..e03314eaa 100644 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js +++ b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.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. */ import { - buildStyleFromParameters, - createOverlayElement, + buildStyleFromMobileParameters, createIframe, createIframeClickHandler, displayHTMLContentInIframe @@ -46,11 +45,7 @@ describe("DOM Actions on Iframe", () => { verticalInset: 10, uiTakeover: true }; - - const webParameters = {}; - - const style = buildStyleFromParameters(mobileParameters, webParameters); - + const style = buildStyleFromMobileParameters(mobileParameters); expect(style.width).toBe("80%"); expect(style.backgroundColor).toBe("rgba(0, 0, 0, 0.7)"); expect(style.borderRadius).toBe("10px"); @@ -62,26 +57,6 @@ describe("DOM Actions on Iframe", () => { }); }); - describe("createOverlayElement", () => { - it("should create overlay element with correct styles", () => { - const parameter = { - backdropOpacity: 0.8, - backdropColor: "#000000" - }; - - const overlayElement = createOverlayElement(parameter); - - expect(overlayElement.id).toBe("alloy-overlay-container"); - expect(overlayElement.style.position).toBe("fixed"); - expect(overlayElement.style.top).toBe("0px"); - expect(overlayElement.style.left).toBe("0px"); - expect(overlayElement.style.width).toBe("100%"); - expect(overlayElement.style.height).toBe("100%"); - expect(overlayElement.style.background).toBe("rgb(0, 0, 0)"); - expect(overlayElement.style.opacity).toBe("0.8"); - expect(overlayElement.style.backgroundColor).toBe("rgb(0, 0, 0)"); - }); - }); describe("createIframe function", () => { it("should create an iframe element with specified properties", () => { const mockHtmlContent = @@ -89,13 +64,9 @@ describe("DOM Actions on Iframe", () => { const mockClickHandler = jasmine.createSpy("clickHandler"); const iframe = createIframe(mockHtmlContent, mockClickHandler); - expect(iframe).toBeDefined(); expect(iframe instanceof HTMLIFrameElement).toBe(true); expect(iframe.src).toContain("blob:"); - expect(iframe.style.border).toBe("none"); - expect(iframe.style.width).toBe("100%"); - expect(iframe.style.height).toBe("100%"); }); it("should set 'nonce' attribute on script tag if it exists", async () => { @@ -285,9 +256,7 @@ describe("DOM Actions on Iframe", () => { let originalAppendChild; let originalBodyStyle; let mockCollect; - let originalCreateContainerElement; let originalCreateIframe; - let originalCreateOverlayElement; beforeEach(() => { mockCollect = jasmine.createSpy("collect"); @@ -295,17 +264,8 @@ describe("DOM Actions on Iframe", () => { document.body.appendChild = jasmine.createSpy("appendChild"); originalBodyStyle = document.body.style; document.body.style = {}; - - originalCreateContainerElement = window.createContainerElement; - window.createContainerElement = jasmine - .createSpy("createContainerElement") - .and.callFake(() => { - const element = document.createElement("div"); - element.id = "alloy-messaging-container"; - return element; - }); - originalCreateIframe = window.createIframe; + window.createIframe = jasmine .createSpy("createIframe") .and.callFake(() => { @@ -313,29 +273,19 @@ describe("DOM Actions on Iframe", () => { element.id = "alloy-content-iframe"; return element; }); - - originalCreateOverlayElement = window.createOverlayElement; - window.createOverlayElement = jasmine - .createSpy("createOverlayElement") - .and.callFake(() => { - const element = document.createElement("div"); - element.id = "alloy-overlay-container"; - return element; - }); }); afterEach(() => { document.body.appendChild = originalAppendChild; document.body.style = originalBodyStyle; document.body.innerHTML = ""; - window.createContainerElement = originalCreateContainerElement; - window.createOverlayElement = originalCreateOverlayElement; window.createIframe = originalCreateIframe; }); - it("should display HTML content in iframe with overlay", () => { + it("should display HTML content in iframe with overlay using mobile parameters", () => { const settings = { type: "custom", + webParameters: { info: "this is a placeholder" }, mobileParameters: { verticalAlign: "center", dismissAnimation: "bottom", @@ -351,9 +301,6 @@ describe("DOM Actions on Iframe", () => { backdropColor: "#4CA206", height: 63 }, - webParameters: { - info: "this is a placeholder" - }, content: '\n\n\n Bumper Sale!\n \n\n\n
    \n \n

    Black Friday Sale!

    \n Technology Image\n

    Don\'t miss out on our incredible discounts and deals at our gadgets!

    \n
    \n Shop\n Dismiss\n
    \n
    \n\n\n\n', contentType: TEXT_HTML, @@ -379,7 +326,133 @@ describe("DOM Actions on Iframe", () => { displayHTMLContentInIframe(settings, mockCollect); expect(document.body.appendChild).toHaveBeenCalledTimes(2); - expect(document.body.style.overflow).toBe("hidden"); + }); + + it("should display HTML content in iframe with overlay using web parameters", () => { + const settings = { + webParameters: { + "alloy-overlay-container": { + style: { + position: "fixed", + top: "0", + left: "0", + width: "100%", + height: "100%", + background: "transparent", + opacity: 0.5, + backgroundColor: "#FFFFFF" + }, + params: { + enabled: true, + parentElement: "body", + insertionMethod: "appendChild" + } + }, + "alloy-messaging-container": { + style: { + width: "72%", + backgroundColor: "orange", + borderRadius: "20px", + border: "none", + position: "fixed", + overflow: "hidden", + left: "50%", + transform: "translateX(-50%) translateY(-50%)", + top: "50%", + display: "flex", + alignItems: "center", + justifyContent: "center", + height: "63vh" + }, + params: { + enabled: true, + parentElement: "body", + insertionMethod: "appendChild" + } + }, + "alloy-content-iframe": { + style: { + width: "100%", + height: "100%" + }, + params: { + enabled: true, + parentElement: "#alloy-messaging-container", + insertionMethod: "appendChild" + } + } + }, + content: + '\n\n\n Bumper Sale!\n \n\n\n
    \n \n

    Black Friday Sale!

    \n Technology Image\n

    Don\'t miss out on our incredible discounts and deals at our gadgets!

    \n
    \n Shop\n Dismiss\n
    \n
    \n\n\n\n', + contentType: TEXT_HTML, + schema: "https://ns.adobe.com/personalization/message/in-app" + }; + + displayHTMLContentInIframe(settings, mockCollect); + expect(document.body.appendChild).toHaveBeenCalledTimes(2); + }); + it("should display HTML content in iframe with no overlay using web parameters", () => { + const settings = { + webParameters: { + "alloy-overlay-container": { + style: { + position: "fixed", + top: "0", + left: "0", + width: "100%", + height: "100%", + background: "transparent", + opacity: 0.5, + backgroundColor: "#FFFFFF" + }, + params: { + enabled: false, + parentElement: "body", + insertionMethod: "appendChild" + } + }, + "alloy-messaging-container": { + style: { + width: "72%", + backgroundColor: "orange", + borderRadius: "20px", + border: "none", + position: "fixed", + overflow: "hidden", + left: "50%", + transform: "translateX(-50%) translateY(-50%)", + top: "50%", + display: "flex", + alignItems: "center", + justifyContent: "center", + height: "63vh" + }, + params: { + enabled: true, + parentElement: "body", + insertionMethod: "appendChild" + } + }, + "alloy-content-iframe": { + style: { + width: "100%", + height: "100%" + }, + params: { + enabled: true, + parentElement: "#alloy-messaging-container", + insertionMethod: "appendChild" + } + } + }, + content: + '\n\n\n Bumper Sale!\n \n\n\n
    \n \n

    Black Friday Sale!

    \n Technology Image\n

    Don\'t miss out on our incredible discounts and deals at our gadgets!

    \n
    \n Shop\n Dismiss\n
    \n
    \n\n\n\n', + contentType: TEXT_HTML, + schema: "https://ns.adobe.com/personalization/message/in-app" + }; + + displayHTMLContentInIframe(settings, mockCollect); + expect(document.body.appendChild).toHaveBeenCalledTimes(1); }); }); }); diff --git a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js deleted file mode 100644 index 1edb52313..000000000 --- a/test/unit/specs/components/Personalization/in-app-message-actions/actions/displayModal.spec.js +++ /dev/null @@ -1,74 +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 { createNode } from "../../../../../../../src/utils/dom"; -import { DIV } from "../../../../../../../src/constants/tagName"; -import displayIframeContent from "../../../../../../../src/components/Personalization/in-app-message-actions/actions/displayIframeContent"; -import { TEXT_HTML } from "../../../../../../../src/components/Personalization/constants/contentType"; - -describe("Personalization::IAM:modal", () => { - it("inserts modal into dom", async () => { - const something = createNode( - DIV, - { className: "something" }, - { - innerHTML: - "

    Amet cillum consectetur elit cupidatat voluptate nisi duis et occaecat enim pariatur.

    " - } - ); - - document.body.append(something); - - await displayIframeContent({ - mobileParameters: { - verticalAlign: "center", - dismissAnimation: "top", - verticalInset: 0, - backdropOpacity: 0.2, - cornerRadius: 15, - horizontalInset: 0, - uiTakeover: true, - horizontalAlign: "center", - width: 80, - displayAnimation: "top", - backdropColor: "#000000", - height: 60 - }, - content: `
    modal
    Alf Says`, - contentType: TEXT_HTML - }); - document.querySelector("div#alloy-overlay-container"); - const messagingContainer = document.querySelector( - "div#alloy-messaging-container" - ); - - expect(messagingContainer).not.toBeNull(); - - expect(messagingContainer.parentNode).toEqual(document.body); - expect(messagingContainer.nextElementSibling).toBeNull(); - - const iframe = document.querySelector( - ".alloy-messaging-container > iframe" - ); - - expect(iframe).not.toBeNull(); - - await new Promise(resolve => { - iframe.addEventListener("load", () => { - resolve(); - }); - }); - - expect( - (iframe.contentDocument || iframe.contentWindow.document).body.outerHTML - ).toContain("Alf Says"); - }); -}); From 0add1a2f7f6e9678e68e5d5d7fa8999f70cd3e2f Mon Sep 17 00:00:00 2001 From: Happy Shandilya Date: Tue, 26 Sep 2023 13:14:23 -0700 Subject: [PATCH 49/66] support "~timestampu" and "~timestampz" (#1044) * support "~imestampu" and "~timestampz" * support ~sdkver - the current Adobe Experience Platform SDKs version string. * use existing libraryVersion * keeping package-lock.json untouched. --- .../createContextProvider.js | 8 ++- .../decisioningContext.sdkVersion.spec.js | 59 +++++++++++++++++++ .../decisioningContext.timestamp.spec.js | 33 +++++++++++ 3 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 test/unit/specs/components/DecisioningEngine/decisioningContext.sdkVersion.spec.js diff --git a/src/components/DecisioningEngine/createContextProvider.js b/src/components/DecisioningEngine/createContextProvider.js index cead84294..8c183aa3f 100644 --- a/src/components/DecisioningEngine/createContextProvider.js +++ b/src/components/DecisioningEngine/createContextProvider.js @@ -12,6 +12,7 @@ governing permissions and limitations under the License. import getBrowser from "../../utils/getBrowser"; import parseUrl from "../../utils/parseUrl"; import flattenObject from "../../utils/flattenObject"; +import libraryVersion from "../../constants/libraryVersion"; export default ({ eventRegistry, window }) => { const pageLoadTimestamp = new Date().getTime(); @@ -47,7 +48,9 @@ export default ({ eventRegistry, window }) => { currentMinute: now.getMinutes(), currentMonth: now.getMonth(), currentYear: now.getFullYear(), - pageVisitDuration: currentTimestamp - pageLoadTimestamp + pageVisitDuration: currentTimestamp - pageLoadTimestamp, + "~timestampu": currentTimestamp / 1000, + "~timestampz": now.toISOString() }; }; @@ -74,7 +77,8 @@ export default ({ eventRegistry, window }) => { return { ...coreGlobalContext, ...getTimeContext(), - window: getWindowContext() + window: getWindowContext(), + "~sdkver": libraryVersion }; }; diff --git a/test/unit/specs/components/DecisioningEngine/decisioningContext.sdkVersion.spec.js b/test/unit/specs/components/DecisioningEngine/decisioningContext.sdkVersion.spec.js new file mode 100644 index 000000000..3596e09f8 --- /dev/null +++ b/test/unit/specs/components/DecisioningEngine/decisioningContext.sdkVersion.spec.js @@ -0,0 +1,59 @@ +/* +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 libraryVersion from "../../../../../src/constants/libraryVersion"; +import { + mockWindow, + setupResponseHandler, + proposition +} from "./contextTestUtils"; + +describe("DecisioningEngine:globalContext:sdkVersion", () => { + let applyResponse; + const currentVersion = libraryVersion; + beforeEach(() => { + applyResponse = jasmine.createSpy(); + }); + + it("satisfies rule based on matched alloy sdk version", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "~sdkver", + matcher: "eq", + values: [currentVersion] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("does not satisfy rule due to unmatched dk version", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "~sdkver", + matcher: "eq", + values: ["2.18.0-beta.0"] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [] + }) + ); + }); +}); diff --git a/test/unit/specs/components/DecisioningEngine/decisioningContext.timestamp.spec.js b/test/unit/specs/components/DecisioningEngine/decisioningContext.timestamp.spec.js index d161c374a..3e75c6b2e 100644 --- a/test/unit/specs/components/DecisioningEngine/decisioningContext.timestamp.spec.js +++ b/test/unit/specs/components/DecisioningEngine/decisioningContext.timestamp.spec.js @@ -331,4 +331,37 @@ describe("DecisioningEngine:globalContext:timeContext", () => { }) ); }); + it("satisfies rule based on matched ~timestampu", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "~timestampu", + matcher: "eq", + values: [mockedTimestamp.getTime() / 1000] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); + + it("satisfies rule based on matched ~timestampz", () => { + setupResponseHandler(applyResponse, mockWindow({}), { + definition: { + key: "~timestampz", + matcher: "eq", + values: [mockedTimestamp.toISOString()] + }, + type: "matcher" + }); + + expect(applyResponse).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + propositions: [proposition] + }) + ); + }); }); From 0cec5f1691091bb3b6433d4c991e22667672ad59 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Tue, 10 Oct 2023 15:46:35 -0600 Subject: [PATCH 50/66] In browser messages e2e (#1045) * updated demo to use web IAM campaign on stage * fixed header * update adapters for latest schema based items * add custom trait to demo page * allow choose environment --- sandbox/public/index.html | 2 +- .../InAppMessagesDemo/InAppMessages.js | 118 +++++++++++++++--- .../inAppMessageConsequenceAdapter.js | 4 +- .../schemaTypeConsequenceAdapter.js | 17 +++ .../createConsequenceAdapter.js | 5 +- .../Personalization/constants/contentType.js | 1 + .../handlers/createProcessInAppMessage.js | 83 +++++++----- .../actions/displayIframeContent.js | 2 +- .../inAppMessageConsequenceAdapter.spec.js | 2 +- .../schemaTypeConsequenceAdapter.spec.js | 70 +++++++++++ .../createConsequenceAdapter.spec.js | 68 ++++++---- 11 files changed, 287 insertions(+), 85 deletions(-) create mode 100644 src/components/DecisioningEngine/consequenceAdapters/schemaTypeConsequenceAdapter.js create mode 100644 test/unit/specs/components/DecisioningEngine/consequenceAdapters/schemaTypeConsequenceAdapter.spec.js diff --git a/sandbox/public/index.html b/sandbox/public/index.html index 2779d2be5..8590202d8 100755 --- a/sandbox/public/index.html +++ b/sandbox/public/index.html @@ -54,7 +54,7 @@ !function(n,o){o.forEach(function(o){n[o]||((n.__alloyNS=n.__alloyNS|| []).push(o),n[o]=function(){var u=arguments;return new Promise( function(i,l){n[o].q.push([i,l,u])})},n[o].q=[])})} - (window,["alloy", "organizationTwo", "cjmProd"]); + (window,["alloy", "organizationTwo", "cjmProd", 'iamAlloy']);