From 23eb053792846bd291b5c316f43ff1b4fa3292b0 Mon Sep 17 00:00:00 2001 From: Jason Waters Date: Fri, 27 Oct 2023 10:28:07 -0600 Subject: [PATCH] In browser messages (#1029) * messaging action modules * banner test * modal test * decisioning engine component * fix test * removed modulesProvider and refactored it into actionsProvider with an executeAction method * license headers * license headers * introduced context provider for managing context * license header * introduced context provider for managing context * license header * message feed POC * added some tests. * track display/interact events and save to storage * limit event storage * store decision history as object * decisioning.qualifiedItem event * refactor event registry a little * provide collect function to messaging actions * fix package-lock.json * fix debounce test * applyResponse initialized once * 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 * don't flatten events on context * [CJM-48525]Renamed renderDecision command to evaluateRulesets (#994) * renamed renderDecision to evaluateRulesets * renamed renderDecision to evaluateRulesets fix typo * evaluateRulesets * added consequence adapter and refactored iframe display code * license * [CJM-48522]Purge historical events post retention period (#997) * purge historical events post retention period * fixed fragile test * defensively check for ruleset items * update to schema based actions * update schemas * rename json-ruleset-item to ruleset-item * rename in-app message schema * 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 * rename qualified event to decisioning.propositionQualified * fix test * subscribeRulesetItems command * license * remove IAM types, add message feed actions modules * license * ensure schema based ruleset consequences * message feed sandbox (#1028) * message feed sandbox * fixed eslint for InApp demo * clear local storage and reload the page * support "Send data to platform" trigger (#1030) * [CJM-46521] Send interact events when in-app messages are clicked (#1031) * Send interact events when in-app messages are clicked * update sandbox * evaluateRulesetsCommand (#1033) * functional tests (#1034) * tests * to debug issue in sauce lab * rephrased * clean up * 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 * 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. * 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 * resolve config warning on iam demo page * support manual triggers * remove surface designation on IAM sandbox demo * clear cookies when switching demo environment * Historical events fix (#1051) * save payloads based on activityId * CJM-53824 * right keys for the day and hour * corrected payload * npm link to local rule engine, will change to the deployed version before merging * saved by activityId in the local storage * constants shared between the two components src/constants * using v2.0.2 of ruleEngine to support historical search * fix for click through (#1054) * Code Review Fixes (#1052) * use existing Alloy DOM utils * removed unused utils method * renamed to "test:unit:debug" * added a try/catch to handle potential JSON parsing errors * removed ensureSchemaBasedRulesetConsequences * using shorthand object property notation * new line * constants shared between components moved to the shared src/constants module * use alloy utils method includes * use alloy utils method values, objectOf * lint * removeElementById for readability --------- Co-authored-by: Jason Waters * IAM: support multiple subscriptions (#1059) * support multi-subscription * support multi-subscription * remove mobile app fudge * support distinct emissions for each subscription with conditions * event.hasQuery() * values() * rename mobile events * Consent & local storage config flag (#1064) * config flag to enable storage in localStorage * based on consent set event registry * tests * for sandbox testing * fix after test * in-memory storage * tests * utils tests * use setStorage only --------- Co-authored-by: Jason Waters * remove message-feed (#1065) * Move decisionContext within personalization (#1068) * few test (#1067) * few test * few test * few test * fix test * code review fix (#1069) * using selectNodes, isNonEmptyArray, isNonEmptyString utils * using toArray * using createRedirect * using warning instead of error * some fixes (#1071) * remove ALLOY from constants --------- Co-authored-by: Happy Shandilya --- package-lock.json | 42 + package.json | 4 + sandbox/public/index.html | 2 +- sandbox/src/App.js | 5 + .../src/components/ContentSecurityPolicy.js | 2 +- .../InAppMessagesDemo/InAppMessages.js | 163 ++++ .../InAppMessagesDemo/InAppMessagesStyle.css | 7 + src/components/DataCollector/index.js | 2 + .../DataCollector/validateApplyResponse.js | 3 +- .../DataCollector/validateUserEventOptions.js | 3 +- .../inAppMessageConsequenceAdapter.js | 30 + .../schemaTypeConsequenceAdapter.js | 17 + src/components/DecisioningEngine/constants.js | 27 + .../DecisioningEngine/createApplyResponse.js | 20 + .../createConsequenceAdapter.js | 31 + .../createContextProvider.js | 98 ++ .../createDecisionHistory.js | 23 + .../createDecisionProvider.js | 52 ++ .../createEvaluableRulesetPayload.js | 102 +++ .../createEvaluateRulesetsCommand.js | 41 + .../DecisioningEngine/createEventRegistry.js | 152 +++ .../createOnResponseHandler.js | 42 + .../createSubscribeRulesetItems.js | 89 ++ src/components/DecisioningEngine/index.js | 120 +++ src/components/DecisioningEngine/utils.js | 77 ++ .../Personalization/createActionsProvider.js | 86 ++ .../createApplyPropositions.js | 8 +- .../Personalization/createCollect.js | 22 +- .../Personalization/createComponent.js | 12 +- .../Personalization/createOnClickHandler.js | 8 +- .../createOnDecisionHandler.js | 40 + .../createPersonalizationDetails.js | 12 +- .../Personalization/createPreprocessors.js | 20 + .../Personalization/createViewCacheManager.js | 2 +- src/components/Personalization/event.js | 28 +- .../handlers/createProcessInAppMessage.js | 86 ++ .../actions/displayIframeContent.js | 304 ++++++ .../initInAppMessageActionsModules.js | 18 + .../in-app-message-actions/utils.js | 54 ++ src/components/Personalization/index.js | 30 +- src/constants/contentType.js | 13 + .../constants/eventType.js | 3 + .../handle.js} | 8 +- src/constants/propositionEventType.js | 39 + .../Personalization => }/constants/schema.js | 9 + src/core/componentCreators.js | 4 +- src/core/config/createCoreConfigs.js | 1 + src/core/createEvent.js | 24 +- src/core/createEventManager.js | 2 + src/core/createLifecycle.js | 5 +- src/utils/createSubscription.js | 76 ++ src/utils/debounce.js | 23 + src/utils/flattenArray.js | 30 + src/utils/flattenObject.js | 35 + src/utils/index.js | 1 + src/utils/parseUrl.js | 64 ++ .../configParts/configOverridesAlt.js | 11 + .../configParts/configOverridesMain.js | 11 + test/functional/helpers/createAlloyProxy.js | 4 +- .../specs/DecisioningEngine/C13348429.js | 86 ++ .../specs/DecisioningEngine/C13405889.js | 90 ++ .../specs/DecisioningEngine/C13419240.js | 59 ++ test/functional/specs/Migration/helper.js | 1 + test/functional/specs/Visitor/C35448.js | 1 - .../inAppMessageConsequenceAdapter.spec.js | 63 ++ .../schemaTypeConsequenceAdapter.spec.js | 70 ++ .../DecisioningEngine/constants.spec.js | 11 + .../DecisioningEngine/contextTestUtils.js | 145 +++ .../createApplyResponse.spec.js | 68 ++ .../createConsequenceAdapter.spec.js | 81 ++ .../createContextProvider.spec.js | 130 +++ .../createDecisionHistory.spec.js | 63 ++ .../createDecisionProvider.spec.js | 480 ++++++++++ .../createEvaluableRulesetPayload.spec.js | 358 ++++++++ .../createEvaluateRulesetsCommand.spec.js | 207 +++++ .../createEventRegistry.spec.js | 367 ++++++++ .../createOnResponseHandler.spec.js | 387 ++++++++ .../createSubscribeRulesetItems.spec.js | 862 ++++++++++++++++++ .../decisioningContext.browser.spec.js | 56 ++ .../decisioningContext.page.spec.js | 345 +++++++ .../decisioningContext.referringPage.spec.js | 288 ++++++ .../decisioningContext.sdkVersion.spec.js | 59 ++ .../decisioningContext.timestamp.spec.js | 367 ++++++++ .../decisioningContext.window.spec.js | 185 ++++ .../DecisioningEngine/index.spec.js | 200 ++++ .../DecisioningEngine/utils.spec.js | 220 +++++ .../createActionsProvider.spec.js | 95 ++ .../Personalization/createCollect.spec.js | 5 +- .../Personalization/createModules.spec.js | 62 ++ .../createOnDecisionHandler.spec.js | 236 +++++ .../createPersonalizationDetails.spec.js | 34 +- .../createPreprocessors.spec.js | 46 + .../createViewCacheManager.spec.js | 2 +- .../dom-actions/createPreprocess.spec.js | 11 + .../components/Personalization/event.spec.js | 4 +- .../handlers/createProcessDomAction.spec.js | 11 + .../handlers/createProcessHtmlContent.spec.js | 11 + .../createProcessInAppMessage.spec.js | 136 +++ .../createProcessPropositions.spec.js | 11 + .../handlers/createProcessRedirect.spec.js | 11 + .../handlers/injectCreateProposition.spec.js | 11 + .../handlers/processDefaultContent.spec.js | 11 + .../actions/displayIframeContent.spec.js | 458 ++++++++++ .../initInAppMessageActionsModules.spec.js | 27 + .../in-app-message-actions/utils.spec.js | 40 + .../responsesMock/eventResponses.js | 1 + .../Personalization/topLevel/buildAlloy.js | 15 +- .../topLevel/cartViewDecisions.spec.js | 17 + .../topLevel/mergedMetricDecisions.spec.js | 14 + .../topLevel/mixedPropositions.spec.js | 14 + ...eDecisionsWithDomActionSchemaItems.spec.js | 14 + .../topLevel/pageWideScopeDecisions.spec.js | 14 + ...cisionsWithoutDomActionSchemaItems.spec.js | 14 + .../topLevel/productsViewDecisions.spec.js | 14 + .../redirectPageWideScopeDecision.spec.js | 14 + .../topLevel/scopesFoo1Foo2Decisions.spec.js | 14 + .../utils/createAsyncArray.spec.js | 11 + .../specs/utils/createSubscription.spec.js | 156 ++++ test/unit/specs/utils/debounce.spec.js | 52 ++ .../unit/specs/utils/deduplicateArray.spec.js | 11 + test/unit/specs/utils/flattenArray.spec.js | 73 ++ test/unit/specs/utils/flattenObject.spec.js | 116 +++ test/unit/specs/utils/parseUrl.spec.js | 63 ++ 123 files changed, 9280 insertions(+), 65 deletions(-) create mode 100644 sandbox/src/components/InAppMessagesDemo/InAppMessages.js create mode 100644 sandbox/src/components/InAppMessagesDemo/InAppMessagesStyle.css create mode 100644 src/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.js create mode 100644 src/components/DecisioningEngine/consequenceAdapters/schemaTypeConsequenceAdapter.js create mode 100644 src/components/DecisioningEngine/constants.js create mode 100644 src/components/DecisioningEngine/createApplyResponse.js create mode 100644 src/components/DecisioningEngine/createConsequenceAdapter.js create mode 100644 src/components/DecisioningEngine/createContextProvider.js create mode 100644 src/components/DecisioningEngine/createDecisionHistory.js create mode 100644 src/components/DecisioningEngine/createDecisionProvider.js create mode 100644 src/components/DecisioningEngine/createEvaluableRulesetPayload.js create mode 100644 src/components/DecisioningEngine/createEvaluateRulesetsCommand.js create mode 100644 src/components/DecisioningEngine/createEventRegistry.js create mode 100644 src/components/DecisioningEngine/createOnResponseHandler.js create mode 100644 src/components/DecisioningEngine/createSubscribeRulesetItems.js create mode 100644 src/components/DecisioningEngine/index.js create mode 100644 src/components/DecisioningEngine/utils.js create mode 100644 src/components/Personalization/createActionsProvider.js create mode 100644 src/components/Personalization/createOnDecisionHandler.js create mode 100644 src/components/Personalization/createPreprocessors.js create mode 100644 src/components/Personalization/handlers/createProcessInAppMessage.js create mode 100644 src/components/Personalization/in-app-message-actions/actions/displayIframeContent.js create mode 100644 src/components/Personalization/in-app-message-actions/initInAppMessageActionsModules.js create mode 100644 src/components/Personalization/in-app-message-actions/utils.js create mode 100644 src/constants/contentType.js rename src/{components/Personalization => }/constants/eventType.js (82%) rename src/{components/Personalization/constants/propositionEventType.js => constants/handle.js} (80%) create mode 100644 src/constants/propositionEventType.js rename src/{components/Personalization => }/constants/schema.js (75%) create mode 100644 src/utils/createSubscription.js create mode 100644 src/utils/debounce.js create mode 100644 src/utils/flattenArray.js create mode 100644 src/utils/flattenObject.js create mode 100644 src/utils/parseUrl.js 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 create mode 100644 test/unit/specs/components/DecisioningEngine/consequenceAdapters/inAppMessageConsequenceAdapter.spec.js create mode 100644 test/unit/specs/components/DecisioningEngine/consequenceAdapters/schemaTypeConsequenceAdapter.spec.js create mode 100644 test/unit/specs/components/DecisioningEngine/constants.spec.js create mode 100644 test/unit/specs/components/DecisioningEngine/contextTestUtils.js create mode 100644 test/unit/specs/components/DecisioningEngine/createApplyResponse.spec.js create mode 100644 test/unit/specs/components/DecisioningEngine/createConsequenceAdapter.spec.js create mode 100644 test/unit/specs/components/DecisioningEngine/createContextProvider.spec.js create mode 100644 test/unit/specs/components/DecisioningEngine/createDecisionHistory.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/createEvaluateRulesetsCommand.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/components/DecisioningEngine/createSubscribeRulesetItems.spec.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.sdkVersion.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/components/DecisioningEngine/utils.spec.js create mode 100644 test/unit/specs/components/Personalization/createActionsProvider.spec.js create mode 100644 test/unit/specs/components/Personalization/createModules.spec.js create mode 100644 test/unit/specs/components/Personalization/createOnDecisionHandler.spec.js create mode 100644 test/unit/specs/components/Personalization/createPreprocessors.spec.js create mode 100644 test/unit/specs/components/Personalization/handlers/createProcessInAppMessage.spec.js create mode 100644 test/unit/specs/components/Personalization/in-app-message-actions/actions/displayIframeContent.spec.js create mode 100644 test/unit/specs/components/Personalization/in-app-message-actions/initInAppMessageActionsModules.spec.js create mode 100644 test/unit/specs/components/Personalization/in-app-message-actions/utils.spec.js create mode 100644 test/unit/specs/utils/createSubscription.spec.js create mode 100644 test/unit/specs/utils/debounce.spec.js create mode 100644 test/unit/specs/utils/flattenArray.spec.js create mode 100644 test/unit/specs/utils/flattenObject.spec.js create mode 100644 test/unit/specs/utils/parseUrl.spec.js diff --git a/package-lock.json b/package-lock.json index 00eea539d..0c295c266 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,14 @@ "version": "2.19.0-beta.11", "license": "Apache-2.0", "dependencies": { + "@adobe/aep-rules-engine": "^2.0.2", + "@adobe/reactor-cookie": "^1.0.0", "@adobe/reactor-load-script": "^1.1.1", "@adobe/reactor-object-assign": "^1.0.0", "@adobe/reactor-query-string": "^1.0.0", "css.escape": "^1.5.1", "js-cookie": "2.2.1", + "parse-uri": "^1.0.7", "uuid": "^3.3.2" }, "devDependencies": { @@ -83,6 +86,11 @@ "yargs": "^16.2.0" } }, + "node_modules/@adobe/aep-rules-engine": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@adobe/aep-rules-engine/-/aep-rules-engine-2.0.2.tgz", + "integrity": "sha512-y5B1LcLo1xbUtRZLe4FRGiburzLu6kgY2VgLutgjoz0bpsKFxb21mqJ1axemsTfpJawYEvuP23+No1ud1ZsP2A==" + }, "node_modules/@adobe/alloy": { "version": "2.19.0-beta.11", "resolved": "https://registry.npmjs.org/@adobe/alloy/-/alloy-2.19.0-beta.11.tgz", @@ -97,6 +105,14 @@ "uuid": "^3.3.2" } }, + "node_modules/@adobe/reactor-cookie": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@adobe/reactor-cookie/-/reactor-cookie-1.1.0.tgz", + "integrity": "sha512-JlC0In45XJOrGuTnfLxj5Hmz9VFJO0hKJHnfc6Qtsrqw2vpKKHmfB8qMExtUw2HJ8iVQd3t+/q3h9wjfvUnVMA==", + "dependencies": { + "js-cookie": "2.2.1" + } + }, "node_modules/@adobe/reactor-load-script": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@adobe/reactor-load-script/-/reactor-load-script-1.1.1.tgz", @@ -14656,6 +14672,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", @@ -19740,6 +19764,11 @@ } }, "dependencies": { + "@adobe/aep-rules-engine": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@adobe/aep-rules-engine/-/aep-rules-engine-2.0.2.tgz", + "integrity": "sha512-y5B1LcLo1xbUtRZLe4FRGiburzLu6kgY2VgLutgjoz0bpsKFxb21mqJ1axemsTfpJawYEvuP23+No1ud1ZsP2A==" + }, "@adobe/alloy": { "version": "2.19.0-beta.11", "resolved": "https://registry.npmjs.org/@adobe/alloy/-/alloy-2.19.0-beta.11.tgz", @@ -19754,6 +19783,14 @@ "uuid": "^3.3.2" } }, + "@adobe/reactor-cookie": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@adobe/reactor-cookie/-/reactor-cookie-1.1.0.tgz", + "integrity": "sha512-JlC0In45XJOrGuTnfLxj5Hmz9VFJO0hKJHnfc6Qtsrqw2vpKKHmfB8qMExtUw2HJ8iVQd3t+/q3h9wjfvUnVMA==", + "requires": { + "js-cookie": "2.2.1" + } + }, "@adobe/reactor-load-script": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@adobe/reactor-load-script/-/reactor-load-script-1.1.1.tgz", @@ -30959,6 +30996,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 b85a1c087..412830f42 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", + "test:unit:debug": "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,11 +59,14 @@ } ], "dependencies": { + "@adobe/aep-rules-engine": "^2.0.2", + "@adobe/reactor-cookie": "^1.0.0", "@adobe/reactor-load-script": "^1.1.1", "@adobe/reactor-object-assign": "^1.0.0", "@adobe/reactor-query-string": "^1.0.0", "css.escape": "^1.5.1", "js-cookie": "2.2.1", + "parse-uri": "^1.0.7", "uuid": "^3.3.2" }, "devDependencies": { 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']); \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 mockedInteract; + let mobileParameters; + + beforeEach(() => { + container = document.createElement("div"); + container.setAttribute("id", "alloy-messaging-container"); + document.body.appendChild(container); + + mockedInteract = jasmine.createSpy("interact"); + + 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"); + anchor.setAttribute("data-uuid", "12345"); + anchor.href = "adbinapp://dismiss?interaction=cancel"; + anchor.innerText = "Cancel"; + + const mockEvent = { + target: anchor, + preventDefault: () => {}, + stopImmediatePropagation: () => {} + }; + const iframeClickHandler = createIframeClickHandler(mockedInteract); + iframeClickHandler(mockEvent); + const alloyMessagingContainer = document.getElementById( + "alloy-messaging-container" + ); + expect(mockedInteract).toHaveBeenCalledOnceWith("dismiss", { + label: "Cancel", + id: "cancel", + uuid: "12345", + link: "" + }); + 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"); + 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("dismiss", { + 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( + mockedInteract, + mockNavigateToUrl + ); + iframeClickHandler(mockEvent); + const overlayContainerAfterDismissal = document.getElementById( + "alloy-overlay-container" + ); + expect(mockedInteract).toHaveBeenCalledOnceWith("dismiss", { + 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; + let mockCollect; + let originalCreateIframe; + + beforeEach(() => { + mockCollect = jasmine.createSpy("collect"); + originalAppendChild = document.body.appendChild; + document.body.appendChild = jasmine.createSpy("appendChild"); + originalBodyStyle = document.body.style; + document.body.style = {}; + originalCreateIframe = window.createIframe; + + window.createIframe = jasmine + .createSpy("createIframe") + .and.callFake(() => { + const element = document.createElement("iframe"); + element.id = "alloy-content-iframe"; + return element; + }); + }); + + afterEach(() => { + document.body.appendChild = originalAppendChild; + document.body.style = originalBodyStyle; + document.body.innerHTML = ""; + window.createIframe = originalCreateIframe; + }); + + 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", + verticalInset: 20, + backdropOpacity: 0.78, + cornerRadius: 20, + gestures: {}, + horizontalInset: -14, + uiTakeover: true, + horizontalAlign: "center", + width: 72, + displayAnimation: "bottom", + backdropColor: "#4CA206", + height: 63 + }, + 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", + 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); + }); + + 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/initInAppMessageActionsModules.spec.js b/test/unit/specs/components/Personalization/in-app-message-actions/initInAppMessageActionsModules.spec.js new file mode 100644 index 000000000..f7ed723b6 --- /dev/null +++ b/test/unit/specs/components/Personalization/in-app-message-actions/initInAppMessageActionsModules.spec.js @@ -0,0 +1,27 @@ +/* +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 initInAppMessageActionsModules from "../../../../../../src/components/Personalization/in-app-message-actions/initInAppMessageActionsModules"; + +describe("Personalization::turbine::initInAppMessageActionsModules", () => { + const noop = () => undefined; + + it("should have all the required modules", () => { + const messagingActionsModules = initInAppMessageActionsModules(noop); + + expect(Object.keys(messagingActionsModules).length).toEqual(1); + + expect(messagingActionsModules.defaultContent).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..28e375686 --- /dev/null +++ b/test/unit/specs/components/Personalization/in-app-message-actions/utils.spec.js @@ -0,0 +1,40 @@ +/* +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 { 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(); + }); +}); diff --git a/test/unit/specs/components/Personalization/responsesMock/eventResponses.js b/test/unit/specs/components/Personalization/responsesMock/eventResponses.js index 9482fb28c..c4047e256 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", diff --git a/test/unit/specs/components/Personalization/topLevel/buildAlloy.js b/test/unit/specs/components/Personalization/topLevel/buildAlloy.js index a4b257679..e1590ecd3 100644 --- a/test/unit/specs/components/Personalization/topLevel/buildAlloy.js +++ b/test/unit/specs/components/Personalization/topLevel/buildAlloy.js @@ -26,16 +26,17 @@ import createViewChangeHandler from "../../../../../../src/components/Personaliz import createClickStorage from "../../../../../../src/components/Personalization/createClickStorage"; import createApplyPropositions from "../../../../../../src/components/Personalization/createApplyPropositions"; import createSetTargetMigration from "../../../../../../src/components/Personalization/createSetTargetMigration"; -import { createCallbackAggregator, assign } from "../../../../../../src/utils"; +import { assign, createCallbackAggregator } from "../../../../../../src/utils"; import injectCreateProposition from "../../../../../../src/components/Personalization/handlers/injectCreateProposition"; import createProcessPropositions from "../../../../../../src/components/Personalization/handlers/createProcessPropositions"; import createAsyncArray from "../../../../../../src/components/Personalization/utils/createAsyncArray"; -import * as schema from "../../../../../../src/components/Personalization/constants/schema"; +import * as schema from "../../../../../../src/constants/schema"; import createProcessDomAction from "../../../../../../src/components/Personalization/handlers/createProcessDomAction"; import createProcessHtmlContent from "../../../../../../src/components/Personalization/handlers/createProcessHtmlContent"; import createProcessRedirect from "../../../../../../src/components/Personalization/handlers/createProcessRedirect"; import processDefaultContent from "../../../../../../src/components/Personalization/handlers/processDefaultContent"; import { isPageWideSurface } from "../../../../../../src/components/Personalization/utils/surfaceUtils"; +import createOnDecisionHandler from "../../../../../../src/components/Personalization/createOnDecisionHandler"; const createAction = renderFunc => ({ selector, content }) => { renderFunc(selector, content); @@ -143,6 +144,13 @@ const buildComponent = ({ const setTargetMigration = createSetTargetMigration({ targetMigrationEnabled }); + + const onDecisionHandler = createOnDecisionHandler({ + processPropositions, + createProposition, + collect + }); + return createComponent({ getPageLocation, logger, @@ -156,7 +164,8 @@ const buildComponent = ({ applyPropositions, setTargetMigration, mergeDecisionsMeta, - renderedPropositions + renderedPropositions, + onDecisionHandler }); }; diff --git a/test/unit/specs/components/Personalization/topLevel/cartViewDecisions.spec.js b/test/unit/specs/components/Personalization/topLevel/cartViewDecisions.spec.js index 6aa3faca5..3587fe7c6 100644 --- a/test/unit/specs/components/Personalization/topLevel/cartViewDecisions.spec.js +++ b/test/unit/specs/components/Personalization/topLevel/cartViewDecisions.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 { CART_VIEW_DECISIONS } from "../responsesMock/eventResponses"; import buildMocks from "./buildMocks"; @@ -23,6 +34,9 @@ describe("PersonalizationComponent", () => { "https://ns.adobe.com/personalization/html-content-item", "https://ns.adobe.com/personalization/json-content-item", "https://ns.adobe.com/personalization/redirect-item", + "https://ns.adobe.com/personalization/ruleset-item", + "https://ns.adobe.com/personalization/message/in-app", + "https://ns.adobe.com/personalization/message/feed-item", "https://ns.adobe.com/personalization/dom-action" ], decisionScopes: ["__view__"], @@ -161,6 +175,9 @@ describe("PersonalizationComponent", () => { "https://ns.adobe.com/personalization/html-content-item", "https://ns.adobe.com/personalization/json-content-item", "https://ns.adobe.com/personalization/redirect-item", + "https://ns.adobe.com/personalization/ruleset-item", + "https://ns.adobe.com/personalization/message/in-app", + "https://ns.adobe.com/personalization/message/feed-item", "https://ns.adobe.com/personalization/dom-action" ], decisionScopes: ["__view__"], diff --git a/test/unit/specs/components/Personalization/topLevel/mergedMetricDecisions.spec.js b/test/unit/specs/components/Personalization/topLevel/mergedMetricDecisions.spec.js index 3f77d60be..ea505d235 100644 --- a/test/unit/specs/components/Personalization/topLevel/mergedMetricDecisions.spec.js +++ b/test/unit/specs/components/Personalization/topLevel/mergedMetricDecisions.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 { MERGED_METRIC_DECISIONS } from "../responsesMock/eventResponses"; import buildMocks from "./buildMocks"; @@ -21,6 +32,9 @@ describe("PersonalizationComponent", () => { "https://ns.adobe.com/personalization/html-content-item", "https://ns.adobe.com/personalization/json-content-item", "https://ns.adobe.com/personalization/redirect-item", + "https://ns.adobe.com/personalization/ruleset-item", + "https://ns.adobe.com/personalization/message/in-app", + "https://ns.adobe.com/personalization/message/feed-item", "https://ns.adobe.com/personalization/dom-action" ], decisionScopes: ["__view__"], diff --git a/test/unit/specs/components/Personalization/topLevel/mixedPropositions.spec.js b/test/unit/specs/components/Personalization/topLevel/mixedPropositions.spec.js index d378ca581..37e4342f9 100644 --- a/test/unit/specs/components/Personalization/topLevel/mixedPropositions.spec.js +++ b/test/unit/specs/components/Personalization/topLevel/mixedPropositions.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 { MIXED_PROPOSITIONS } from "../responsesMock/eventResponses"; import buildMocks from "./buildMocks"; @@ -23,6 +34,9 @@ describe("PersonalizationComponent", () => { "https://ns.adobe.com/personalization/html-content-item", "https://ns.adobe.com/personalization/json-content-item", "https://ns.adobe.com/personalization/redirect-item", + "https://ns.adobe.com/personalization/ruleset-item", + "https://ns.adobe.com/personalization/message/in-app", + "https://ns.adobe.com/personalization/message/feed-item", "https://ns.adobe.com/personalization/dom-action" ], decisionScopes: ["__view__"], diff --git a/test/unit/specs/components/Personalization/topLevel/pageWideDecisionsWithDomActionSchemaItems.spec.js b/test/unit/specs/components/Personalization/topLevel/pageWideDecisionsWithDomActionSchemaItems.spec.js index 3d936ca45..ba5f7b0ab 100644 --- a/test/unit/specs/components/Personalization/topLevel/pageWideDecisionsWithDomActionSchemaItems.spec.js +++ b/test/unit/specs/components/Personalization/topLevel/pageWideDecisionsWithDomActionSchemaItems.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 { PAGE_WIDE_DECISIONS_WITH_DOM_ACTION_SCHEMA_ITEMS } from "../responsesMock/eventResponses"; import buildMocks from "./buildMocks"; @@ -21,6 +32,9 @@ describe("PersonalizationComponent", () => { "https://ns.adobe.com/personalization/html-content-item", "https://ns.adobe.com/personalization/json-content-item", "https://ns.adobe.com/personalization/redirect-item", + "https://ns.adobe.com/personalization/ruleset-item", + "https://ns.adobe.com/personalization/message/in-app", + "https://ns.adobe.com/personalization/message/feed-item", "https://ns.adobe.com/personalization/dom-action" ], decisionScopes: ["__view__"], diff --git a/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisions.spec.js b/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisions.spec.js index d4d9fec2c..184bd30b4 100644 --- a/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisions.spec.js +++ b/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisions.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 { PAGE_WIDE_SCOPE_DECISIONS } from "../responsesMock/eventResponses"; import buildMocks from "./buildMocks"; @@ -21,6 +32,9 @@ describe("PersonalizationComponent", () => { "https://ns.adobe.com/personalization/html-content-item", "https://ns.adobe.com/personalization/json-content-item", "https://ns.adobe.com/personalization/redirect-item", + "https://ns.adobe.com/personalization/ruleset-item", + "https://ns.adobe.com/personalization/message/in-app", + "https://ns.adobe.com/personalization/message/feed-item", "https://ns.adobe.com/personalization/dom-action" ], decisionScopes: ["__view__"], diff --git a/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisionsWithoutDomActionSchemaItems.spec.js b/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisionsWithoutDomActionSchemaItems.spec.js index fc96d24bd..c46284f04 100644 --- a/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisionsWithoutDomActionSchemaItems.spec.js +++ b/test/unit/specs/components/Personalization/topLevel/pageWideScopeDecisionsWithoutDomActionSchemaItems.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 { PAGE_WIDE_SCOPE_DECISIONS_WITHOUT_DOM_ACTION_SCHEMA_ITEMS } from "../responsesMock/eventResponses"; import buildMocks from "./buildMocks"; @@ -23,6 +34,9 @@ describe("PersonalizationComponent", () => { "https://ns.adobe.com/personalization/html-content-item", "https://ns.adobe.com/personalization/json-content-item", "https://ns.adobe.com/personalization/redirect-item", + "https://ns.adobe.com/personalization/ruleset-item", + "https://ns.adobe.com/personalization/message/in-app", + "https://ns.adobe.com/personalization/message/feed-item", "https://ns.adobe.com/personalization/dom-action" ], decisionScopes: ["__view__"], diff --git a/test/unit/specs/components/Personalization/topLevel/productsViewDecisions.spec.js b/test/unit/specs/components/Personalization/topLevel/productsViewDecisions.spec.js index 0f7c72002..f1a988cc3 100644 --- a/test/unit/specs/components/Personalization/topLevel/productsViewDecisions.spec.js +++ b/test/unit/specs/components/Personalization/topLevel/productsViewDecisions.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 { PRODUCTS_VIEW_DECISIONS } from "../responsesMock/eventResponses"; import buildMocks from "./buildMocks"; @@ -21,6 +32,9 @@ describe("PersonalizationComponent", () => { "https://ns.adobe.com/personalization/html-content-item", "https://ns.adobe.com/personalization/json-content-item", "https://ns.adobe.com/personalization/redirect-item", + "https://ns.adobe.com/personalization/ruleset-item", + "https://ns.adobe.com/personalization/message/in-app", + "https://ns.adobe.com/personalization/message/feed-item", "https://ns.adobe.com/personalization/dom-action" ], decisionScopes: ["__view__"], diff --git a/test/unit/specs/components/Personalization/topLevel/redirectPageWideScopeDecision.spec.js b/test/unit/specs/components/Personalization/topLevel/redirectPageWideScopeDecision.spec.js index 92c6d984c..180be30d1 100644 --- a/test/unit/specs/components/Personalization/topLevel/redirectPageWideScopeDecision.spec.js +++ b/test/unit/specs/components/Personalization/topLevel/redirectPageWideScopeDecision.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 { REDIRECT_PAGE_WIDE_SCOPE_DECISION } from "../responsesMock/eventResponses"; import buildMocks from "./buildMocks"; @@ -21,6 +32,9 @@ describe("PersonalizationComponent", () => { "https://ns.adobe.com/personalization/html-content-item", "https://ns.adobe.com/personalization/json-content-item", "https://ns.adobe.com/personalization/redirect-item", + "https://ns.adobe.com/personalization/ruleset-item", + "https://ns.adobe.com/personalization/message/in-app", + "https://ns.adobe.com/personalization/message/feed-item", "https://ns.adobe.com/personalization/dom-action" ], decisionScopes: ["__view__"], diff --git a/test/unit/specs/components/Personalization/topLevel/scopesFoo1Foo2Decisions.spec.js b/test/unit/specs/components/Personalization/topLevel/scopesFoo1Foo2Decisions.spec.js index fad11f532..82bb287a2 100644 --- a/test/unit/specs/components/Personalization/topLevel/scopesFoo1Foo2Decisions.spec.js +++ b/test/unit/specs/components/Personalization/topLevel/scopesFoo1Foo2Decisions.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 { SCOPES_FOO1_FOO2_DECISIONS } from "../responsesMock/eventResponses"; import buildMocks from "./buildMocks"; @@ -21,6 +32,9 @@ describe("PersonalizationComponent", () => { "https://ns.adobe.com/personalization/html-content-item", "https://ns.adobe.com/personalization/json-content-item", "https://ns.adobe.com/personalization/redirect-item", + "https://ns.adobe.com/personalization/ruleset-item", + "https://ns.adobe.com/personalization/message/in-app", + "https://ns.adobe.com/personalization/message/feed-item", "https://ns.adobe.com/personalization/dom-action" ], decisionScopes: ["__view__"], diff --git a/test/unit/specs/components/Personalization/utils/createAsyncArray.spec.js b/test/unit/specs/components/Personalization/utils/createAsyncArray.spec.js index c7557a5ae..3e4731307 100644 --- a/test/unit/specs/components/Personalization/utils/createAsyncArray.spec.js +++ b/test/unit/specs/components/Personalization/utils/createAsyncArray.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 createAsyncArray from "../../../../../../src/components/Personalization/utils/createAsyncArray"; import { defer } from "../../../../../../src/utils"; import flushPromiseChains from "../../../../helpers/flushPromiseChains"; diff --git a/test/unit/specs/utils/createSubscription.spec.js b/test/unit/specs/utils/createSubscription.spec.js new file mode 100644 index 000000000..23719373f --- /dev/null +++ b/test/unit/specs/utils/createSubscription.spec.js @@ -0,0 +1,156 @@ +import createSubscription from "../../../../src/utils/createSubscription"; + +describe("createSubscription", () => { + const value = { something: 42 }; + + let callback1; + let callback2; + let callback3; + + beforeEach(() => { + callback1 = jasmine.createSpy("callback1"); + callback2 = jasmine.createSpy("callback2"); + callback3 = jasmine.createSpy("callback3"); + }); + + it("supports a single subscription", () => { + const subscription = createSubscription(); + expect(subscription.hasSubscriptions()).toBeFalse(); + + const unsubscribe = subscription.add(callback1); + + expect(subscription.hasSubscriptions()).toBeTrue(); + + subscription.emit(value); + + expect(callback1).toHaveBeenCalledOnceWith(value); + + unsubscribe(); + + expect(subscription.hasSubscriptions()).toBeFalse(); + subscription.emit(value); + + expect(callback1).toHaveBeenCalledOnceWith(value); + }); + + it("supports multiple subscriptions", () => { + const subscription = createSubscription(); + expect(subscription.hasSubscriptions()).toBeFalse(); + + const unsubsubscribe1 = subscription.add(callback1); + const unsubsubscribe2 = subscription.add(callback2); + const unsubsubscribe3 = subscription.add(callback3); + + expect(subscription.hasSubscriptions()).toBeTrue(); + + subscription.emit(value); + + expect(callback1).toHaveBeenCalledOnceWith(value); + expect(callback2).toHaveBeenCalledOnceWith(value); + expect(callback3).toHaveBeenCalledOnceWith(value); + + // unsubscribe the first callback + unsubsubscribe1(); + + expect(subscription.hasSubscriptions()).toBeTrue(); + + subscription.emit(value); + + expect(callback1).toHaveBeenCalledTimes(1); + expect(callback2).toHaveBeenCalledTimes(2); + expect(callback3).toHaveBeenCalledTimes(2); + + // unsubscribe the second callback + unsubsubscribe2(); + + expect(subscription.hasSubscriptions()).toBeTrue(); + + subscription.emit(value); + + expect(callback1).toHaveBeenCalledTimes(1); + expect(callback2).toHaveBeenCalledTimes(2); + expect(callback3).toHaveBeenCalledTimes(3); + + // unsubscribe the third callback + unsubsubscribe3(); + + expect(subscription.hasSubscriptions()).toBeFalse(); + + subscription.emit(value); + + expect(callback1).toHaveBeenCalledTimes(1); + expect(callback2).toHaveBeenCalledTimes(2); + expect(callback3).toHaveBeenCalledTimes(3); + }); + + it("emits distinct values for multiple subscriptions", () => { + const subscription = createSubscription(); + subscription.setEmissionPreprocessor((params, basePrice) => { + const { name, profitMargin } = params; + const price = basePrice * profitMargin; + return [`hello ${name}! The price is $${price}`]; + }); + + const unsubsubscribe1 = subscription.add(callback1, { + name: "jim", + profitMargin: 3 + }); + const unsubsubscribe2 = subscription.add(callback2, { + name: "bob", + profitMargin: 1.8 + }); + const unsubsubscribe3 = subscription.add(callback3, { + name: "tina", + profitMargin: 1.1 + }); + + subscription.emit(10); + + expect(callback1).toHaveBeenCalledOnceWith("hello jim! The price is $30"); + expect(callback2).toHaveBeenCalledOnceWith("hello bob! The price is $18"); + expect(callback3).toHaveBeenCalledOnceWith("hello tina! The price is $11"); + + unsubsubscribe1(); + unsubsubscribe2(); + unsubsubscribe3(); + }); + + it("emits distinct values conditionally", () => { + const subscription = createSubscription(); + subscription.setEmissionPreprocessor((params, basePrice) => { + const { name, profitMargin } = params; + const price = basePrice * profitMargin; + return [`hello ${name}! The price is $${price}`]; + }); + subscription.setEmissionCondition((params, result) => { + const price = parseInt( + result.substring(result.length - 2, result.length), + 10 + ); + return price < 20; + }); + + const unsubsubscribe1 = subscription.add(callback1, { + name: "jim", + profitMargin: 3 + }); + const unsubsubscribe2 = subscription.add(callback2, { + name: "bob", + profitMargin: 1.8 + }); + const unsubsubscribe3 = subscription.add(callback3, { + name: "tina", + profitMargin: 1.1 + }); + + subscription.emit(10); + + expect(callback1).not.toHaveBeenCalled(); // price is > 20, so no emission + expect(callback2).toHaveBeenCalledOnceWith("hello bob! The price is $18"); + expect(callback3).toHaveBeenCalledOnceWith("hello tina! The price is $11"); + + unsubsubscribe1(); + unsubsubscribe2(); + unsubsubscribe3(); + }); +}); diff --git a/test/unit/specs/utils/debounce.spec.js b/test/unit/specs/utils/debounce.spec.js new file mode 100644 index 000000000..d30912377 --- /dev/null +++ b/test/unit/specs/utils/debounce.spec.js @@ -0,0 +1,52 @@ +/* +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", () => { + 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, 10); + 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); + }); +}); diff --git a/test/unit/specs/utils/deduplicateArray.spec.js b/test/unit/specs/utils/deduplicateArray.spec.js index f94b954a7..3d508686c 100644 --- a/test/unit/specs/utils/deduplicateArray.spec.js +++ b/test/unit/specs/utils/deduplicateArray.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 { deduplicateArray } from "../../../../src/utils"; describe("deduplicateArray", () => { diff --git a/test/unit/specs/utils/flattenArray.spec.js b/test/unit/specs/utils/flattenArray.spec.js new file mode 100644 index 000000000..b30e2575a --- /dev/null +++ b/test/unit/specs/utils/flattenArray.spec.js @@ -0,0 +1,73 @@ +/* +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", () => { + 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..794858b99 --- /dev/null +++ b/test/unit/specs/utils/flattenObject.spec.js @@ -0,0 +1,116 @@ +/* +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", () => { + 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); + }); +}); 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"); + }); +});