diff --git a/bundlesize.json b/bundlesize.json index 4256f8f83..623d49f10 100644 --- a/bundlesize.json +++ b/bundlesize.json @@ -5,13 +5,13 @@ "brotiliSize": 133 }, "dist/alloy.js": { - "uncompressedSize": 589421, - "gzippedSize": 89564, - "brotiliSize": 69620 + "uncompressedSize": 593980, + "gzippedSize": 90304, + "brotiliSize": 70171 }, "dist/alloy.min.js": { - "uncompressedSize": 123486, - "gzippedSize": 40839, - "brotiliSize": 35412 + "uncompressedSize": 124393, + "gzippedSize": 41151, + "brotiliSize": 35684 } } \ No newline at end of file diff --git a/src/components/Personalization/createFetchDataHandler.js b/src/components/Personalization/createFetchDataHandler.js index 587d50447..4a9f47a2b 100644 --- a/src/components/Personalization/createFetchDataHandler.js +++ b/src/components/Personalization/createFetchDataHandler.js @@ -11,6 +11,7 @@ governing permissions and limitations under the License. */ import { groupBy, isNonEmptyArray } from "../../utils/index.js"; import PAGE_WIDE_SCOPE from "../../constants/pageWideScope.js"; +import uuid from "../../utils/uuid.js"; const DECISIONS_HANDLE = "personalization:decisions"; @@ -56,7 +57,17 @@ export default ({ }, }); } - const propositions = handles.map((handle) => createProposition(handle)); + const propositions = handles.map((handle) => { + const proposition = { + ...handle, + items: handle.items.map((item) => { + item.id = !item.id || item.id === "0" ? uuid() : item.id; + return item; + }), + }; + return createProposition(proposition); + }); + const { page: pagePropositions = [], view: viewPropositions = [], diff --git a/src/components/Personalization/dom-actions/action.js b/src/components/Personalization/dom-actions/action.js index 5de0ad718..890141b87 100644 --- a/src/components/Personalization/dom-actions/action.js +++ b/src/components/Personalization/dom-actions/action.js @@ -13,7 +13,6 @@ governing permissions and limitations under the License. import { awaitSelector } from "../../../utils/dom/index.js"; import { hideElements, showElements } from "../flicker/index.js"; import { selectNodesWithEq } from "./dom/index.js"; - export { default as setText } from "./setText.js"; export { default as setHtml } from "./setHtml.js"; export { default as appendHtml } from "./appendHtml.js"; @@ -26,24 +25,38 @@ export { default as setAttributes } from "./setAttributes.js"; export { default as swapImage } from "./swapImage.js"; export { default as rearrangeChildren } from "./rearrangeChildren.js"; -const renderContent = (elements, content, decorateProposition, renderFunc) => { - const executions = elements.map((element) => - renderFunc(element, content, decorateProposition), - ); +const renderContent = ( + elements, + content, + decorateProposition, + renderFunc, + renderedHandler, +) => { + const executions = elements.map((element) => { + if(renderedHandler.shouldRender(element)) { + return renderFunc(element, content, decorateProposition, renderedHandler.markAsRendered); + } + return Promise.resolve(); + }); return Promise.all(executions); }; export const createAction = (renderFunc) => { - return (itemData, decorateProposition) => { + return (itemData, decorateProposition, renderedHandler) => { const { selector, prehidingSelector, content } = itemData; - hideElements(prehidingSelector); return awaitSelector(selector, selectNodesWithEq) - .then((elements) => - renderContent(elements, content, decorateProposition, renderFunc), - ) + .then((elements) => { + return renderContent( + elements, + content, + decorateProposition, + renderFunc, + renderedHandler, + ); + }) .then( () => { // if everything is OK, show elements diff --git a/src/components/Personalization/dom-actions/appendHtml.js b/src/components/Personalization/dom-actions/appendHtml.js index 0efe70c83..45581228d 100644 --- a/src/components/Personalization/dom-actions/appendHtml.js +++ b/src/components/Personalization/dom-actions/appendHtml.js @@ -21,7 +21,7 @@ import { getRemoteScriptsUrls, } from "./scripts.js"; -export default (container, html, decorateProposition) => { +export default (container, html, decorateProposition, markAsRendered) => { const fragment = createFragment(html); addNonceToInlineStyleElements(fragment); const elements = getChildNodes(fragment); @@ -35,7 +35,7 @@ export default (container, html, decorateProposition) => { }); decorateProposition(container); - + markAsRendered(container); executeInlineScripts(container, scripts); return executeRemoteScripts(scriptsUrls); diff --git a/src/components/Personalization/dom-actions/dom/addCssClass.js b/src/components/Personalization/dom-actions/dom/addCssClass.js new file mode 100644 index 000000000..401cbe83c --- /dev/null +++ b/src/components/Personalization/dom-actions/dom/addCssClass.js @@ -0,0 +1,7 @@ +import { isNonEmptyString } from "../../../../utils/index.js"; + +export default (element, name) => { + if (name && isNonEmptyString(name)) { + element.classList.add(name); + } +}; diff --git a/src/components/Personalization/dom-actions/dom/hasCssClass.js b/src/components/Personalization/dom-actions/dom/hasCssClass.js new file mode 100644 index 000000000..6616b13ec --- /dev/null +++ b/src/components/Personalization/dom-actions/dom/hasCssClass.js @@ -0,0 +1,8 @@ +import { isNonEmptyString } from "../../../../utils/index.js"; + +export default (element, name) => { + if (name && isNonEmptyString(name)) { + return element.classList.contains(name); + } + return false; +}; diff --git a/src/components/Personalization/dom-actions/insertHtmlAfter.js b/src/components/Personalization/dom-actions/insertHtmlAfter.js index 0a72d0ea5..3c0670f3e 100644 --- a/src/components/Personalization/dom-actions/insertHtmlAfter.js +++ b/src/components/Personalization/dom-actions/insertHtmlAfter.js @@ -20,7 +20,7 @@ import { executeRemoteScripts, } from "./scripts.js"; -export default (container, html, decorateProposition) => { +export default (container, html, decorateProposition, markRendered) => { const fragment = createFragment(html); addNonceToInlineStyleElements(fragment); const elements = getChildNodes(fragment); @@ -36,7 +36,7 @@ export default (container, html, decorateProposition) => { insertAfter(insertionPoint, element); insertionPoint = element; }); - + markRendered(container); executeInlineScripts(container, scripts); return executeRemoteScripts(scriptsUrls); diff --git a/src/components/Personalization/dom-actions/insertHtmlBefore.js b/src/components/Personalization/dom-actions/insertHtmlBefore.js index 2ab4d28eb..71ae1e4f1 100644 --- a/src/components/Personalization/dom-actions/insertHtmlBefore.js +++ b/src/components/Personalization/dom-actions/insertHtmlBefore.js @@ -20,7 +20,7 @@ import { getRemoteScriptsUrls, } from "./scripts.js"; -export default (container, html, decorateProposition) => { +export default (container, html, decorateProposition, markRendered) => { const fragment = createFragment(html); addNonceToInlineStyleElements(fragment); const elements = getChildNodes(fragment); @@ -33,7 +33,7 @@ export default (container, html, decorateProposition) => { decorateProposition(element); insertBefore(container, element); }); - + markRendered(container); executeInlineScripts(container, scripts); return executeRemoteScripts(scriptsUrls); diff --git a/src/components/Personalization/dom-actions/prependHtml.js b/src/components/Personalization/dom-actions/prependHtml.js index c91748e56..e3ee9ea29 100644 --- a/src/components/Personalization/dom-actions/prependHtml.js +++ b/src/components/Personalization/dom-actions/prependHtml.js @@ -26,7 +26,7 @@ import { getRemoteScriptsUrls, } from "./scripts.js"; -export default (container, html, decorateProposition) => { +export default (container, html, decorateProposition, markRendered) => { const fragment = createFragment(html); addNonceToInlineStyleElements(fragment); const elements = getChildNodes(fragment); @@ -53,7 +53,7 @@ export default (container, html, decorateProposition) => { i -= 1; } - + markRendered(container); executeInlineScripts(container, scripts); return executeRemoteScripts(scriptsUrls); diff --git a/src/components/Personalization/dom-actions/rearrangeChildren.js b/src/components/Personalization/dom-actions/rearrangeChildren.js index 3071930ec..7186c5507 100644 --- a/src/components/Personalization/dom-actions/rearrangeChildren.js +++ b/src/components/Personalization/dom-actions/rearrangeChildren.js @@ -12,7 +12,7 @@ governing permissions and limitations under the License. import { getChildren, insertAfter, insertBefore } from "./dom/index.js"; -export default (container, { from, to }, decorateProposition) => { +export default (container, { from, to }, decorateProposition, markRendered) => { const children = getChildren(container); const elementFrom = children[from]; const elementTo = children[to]; @@ -31,4 +31,5 @@ export default (container, { from, to }, decorateProposition) => { decorateProposition(elementTo); decorateProposition(elementFrom); + markRendered(container); }; diff --git a/src/components/Personalization/dom-actions/replaceHtml.js b/src/components/Personalization/dom-actions/replaceHtml.js index da5eea767..484f159bc 100644 --- a/src/components/Personalization/dom-actions/replaceHtml.js +++ b/src/components/Personalization/dom-actions/replaceHtml.js @@ -13,8 +13,13 @@ governing permissions and limitations under the License. import { removeNode } from "../../../utils/dom/index.js"; import insertHtmlBefore from "./insertHtmlBefore.js"; -export default (container, html, decorateProposition) => { - return insertHtmlBefore(container, html, decorateProposition).then(() => { +export default (container, html, decorateProposition, markRendered) => { + return insertHtmlBefore( + container, + html, + decorateProposition, + markRendered, + ).then(() => { removeNode(container); }); }; diff --git a/src/components/Personalization/dom-actions/setAttributes.js b/src/components/Personalization/dom-actions/setAttributes.js index 0b588d835..cc5e70c24 100644 --- a/src/components/Personalization/dom-actions/setAttributes.js +++ b/src/components/Personalization/dom-actions/setAttributes.js @@ -12,10 +12,11 @@ governing permissions and limitations under the License. import { setAttribute } from "./dom/index.js"; -export default (container, attributes, decorateProposition) => { +export default (container, attributes, decorateProposition, markRendered) => { Object.keys(attributes).forEach((key) => { setAttribute(container, key, attributes[key]); }); decorateProposition(container); + markRendered(container); }; diff --git a/src/components/Personalization/dom-actions/setHtml.js b/src/components/Personalization/dom-actions/setHtml.js index 3fb0817b8..3d57580e1 100644 --- a/src/components/Personalization/dom-actions/setHtml.js +++ b/src/components/Personalization/dom-actions/setHtml.js @@ -21,7 +21,7 @@ const clear = (container) => { childNodes.forEach(removeNode); }; -export default (container, html, decorateProposition) => { +export default (container, html, decorateProposition, markRendered) => { clear(container); - return appendHtml(container, html, decorateProposition); + return appendHtml(container, html, decorateProposition, markRendered); }; diff --git a/src/components/Personalization/dom-actions/setStyles.js b/src/components/Personalization/dom-actions/setStyles.js index 821495987..4faf19454 100644 --- a/src/components/Personalization/dom-actions/setStyles.js +++ b/src/components/Personalization/dom-actions/setStyles.js @@ -12,7 +12,7 @@ governing permissions and limitations under the License. import { setStyle } from "./dom/index.js"; -export default (container, styles, decorateProposition) => { +export default (container, styles, decorateProposition, markRendered) => { const { priority, ...style } = styles; Object.keys(style).forEach((key) => { @@ -20,4 +20,5 @@ export default (container, styles, decorateProposition) => { }); decorateProposition(container); + markRendered(container); }; diff --git a/src/components/Personalization/dom-actions/setText.js b/src/components/Personalization/dom-actions/setText.js index 63108582d..8fa2b7659 100644 --- a/src/components/Personalization/dom-actions/setText.js +++ b/src/components/Personalization/dom-actions/setText.js @@ -10,7 +10,8 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -export default (container, text, decorateProposition) => { +export default (container, text, decorateProposition, markRendered) => { decorateProposition(container); container.textContent = text; + markRendered(container); }; diff --git a/src/components/Personalization/dom-actions/swapImage.js b/src/components/Personalization/dom-actions/swapImage.js index abaf1bfc0..769c2a6f4 100644 --- a/src/components/Personalization/dom-actions/swapImage.js +++ b/src/components/Personalization/dom-actions/swapImage.js @@ -14,7 +14,7 @@ import { SRC } from "../../../constants/elementAttribute.js"; import { removeAttribute, setAttribute } from "./dom/index.js"; import { isImage, loadImage } from "./images.js"; -export default (container, url, decorateProposition) => { +export default (container, url, decorateProposition, markRendered) => { if (!isImage(container)) { return; } @@ -29,4 +29,5 @@ export default (container, url, decorateProposition) => { // Replace the image "src" setAttribute(container, SRC, url); + markRendered(container); }; diff --git a/src/components/Personalization/handlers/createProcessDomAction.js b/src/components/Personalization/handlers/createProcessDomAction.js index a2da4dd41..547c6aa41 100644 --- a/src/components/Personalization/handlers/createProcessDomAction.js +++ b/src/components/Personalization/handlers/createProcessDomAction.js @@ -12,6 +12,7 @@ governing permissions and limitations under the License. import createDecorateProposition from "./createDecorateProposition.js"; import { DOM_ACTION_CLICK } from "../dom-actions/initDomActionsModules.js"; +import createRenderedHandler from "./createRenderedHandler.js"; export default ({ modules, @@ -64,9 +65,14 @@ export default ({ item.getProposition().getNotification(), storeInteractionMeta, ); + const renderedHandler = createRenderedHandler( + item.getProposition().getScopeType(), + item.getIdentifier(), + ); return { - render: () => modules[type](item.getData(), decorateProposition), + render: () => + modules[type](item.getData(), decorateProposition, renderedHandler), setRenderAttempted: true, includeInNotification: true, }; diff --git a/src/components/Personalization/handlers/createRenderedHandler.js b/src/components/Personalization/handlers/createRenderedHandler.js new file mode 100644 index 000000000..433a35f7c --- /dev/null +++ b/src/components/Personalization/handlers/createRenderedHandler.js @@ -0,0 +1,29 @@ +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +import hasCssClass from "../dom-actions/dom/hasCssClass.js"; +import addCssClass from "../dom-actions/dom/addCssClass.js"; + +export default (scopeType, itemIdentifier) => { + return { + shouldRender: (element) => { + if (scopeType === "view") { + return !hasCssClass(element, itemIdentifier); + } + return true; + }, + markAsRendered: (element) => { + if (scopeType === "view") { + addCssClass(element, itemIdentifier); + } + }, + }; +}; diff --git a/src/components/Personalization/handlers/injectCreateProposition.js b/src/components/Personalization/handlers/injectCreateProposition.js index a91c0df06..5dafda56e 100644 --- a/src/components/Personalization/handlers/injectCreateProposition.js +++ b/src/components/Personalization/handlers/injectCreateProposition.js @@ -18,7 +18,7 @@ import { } from "../constants/scopeType.js"; export default ({ preprocess, isPageWideSurface }) => { - const createItem = (item, proposition) => { + const createItem = (item, proposition, itemIdentifier) => { const { id, schema, data, characteristics: { trackingLabel } = {} } = item; const schemaType = data ? data.type : undefined; @@ -29,6 +29,9 @@ export default ({ preprocess, isPageWideSurface }) => { getId() { return id; }, + getIdentifier() { + return itemIdentifier; + }, getSchema() { return schema; }, @@ -63,6 +66,10 @@ export default ({ preprocess, isPageWideSurface }) => { ) => { const { id, scope, scopeDetails, items = [] } = payload; const { characteristics: { scopeType } = {} } = scopeDetails || {}; + const keyedItems = items.map((item) => ({ + key: `alloy-${item.id}`, + item, + })); return { getScope() { @@ -78,7 +85,9 @@ export default ({ preprocess, isPageWideSurface }) => { return PROPOSITION_SCOPE_TYPE; }, getItems() { - return items.map((item) => createItem(item, this)); + return keyedItems.map((keyedItem) => + createItem(keyedItem.item, this, keyedItem.key), + ); }, getNotification() { return { id, scope, scopeDetails }; diff --git a/src/components/StreamingMedia/createTrackMediaSession.js b/src/components/StreamingMedia/createTrackMediaSession.js index 22f9f713c..f82aca490 100644 --- a/src/components/StreamingMedia/createTrackMediaSession.js +++ b/src/components/StreamingMedia/createTrackMediaSession.js @@ -38,7 +38,7 @@ export default ({ getPlayerDetails, legacy, }, - edgeConfigOverrides + edgeConfigOverrides, }); mediaSessionCacheManager.storeSession({ diff --git a/src/components/StreamingMedia/validateMediaSessionOptions.js b/src/components/StreamingMedia/validateMediaSessionOptions.js index 08f61b3fd..89c608d87 100644 --- a/src/components/StreamingMedia/validateMediaSessionOptions.js +++ b/src/components/StreamingMedia/validateMediaSessionOptions.js @@ -30,7 +30,7 @@ export default ({ options }) => { sessionDetails: objectOf(anything()).required(), }), }), - edgeConfigOverrides: objectOf({}) + edgeConfigOverrides: objectOf({}), }).required(), objectOf({ @@ -40,7 +40,7 @@ export default ({ options }) => { sessionDetails: objectOf(anything()).required(), }), }), - edgeConfigOverrides: objectOf({}) + edgeConfigOverrides: objectOf({}), }).required(), ], diff --git a/src/utils/request/createRequestParams.js b/src/utils/request/createRequestParams.js index 6c091a9e3..aed49132d 100644 --- a/src/utils/request/createRequestParams.js +++ b/src/utils/request/createRequestParams.js @@ -9,7 +9,7 @@ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTA OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import {isEmptyObject} from "../index.js"; +import { isEmptyObject } from "../index.js"; /** * @typedef {{ datastreamId: string, [k: string]: Object }} Override @@ -33,7 +33,10 @@ export default ({ localConfigOverrides, globalConfigOverrides, payload }) => { payload.mergeConfigOverride(globalConfigOverrides); } - if (localConfigOverridesWithoutDatastreamId && !isEmptyObject(localConfigOverridesWithoutDatastreamId)) { + if ( + localConfigOverridesWithoutDatastreamId && + !isEmptyObject(localConfigOverridesWithoutDatastreamId) + ) { payload.mergeConfigOverride(localConfigOverridesWithoutDatastreamId); } return requestParams; diff --git a/test/functional/specs/Personalization/C5805676.js b/test/functional/specs/Personalization/C5805676.js index 28ce72419..3e1826608 100644 --- a/test/functional/specs/Personalization/C5805676.js +++ b/test/functional/specs/Personalization/C5805676.js @@ -103,7 +103,12 @@ test("Test C5805676: Merged metric propositions should be delivered", async () = await t.expect(responseBodyProposition.items.length).eql(2); - await t.expect(responseBodyProposition.items[0]).eql(DEFAULT_CONTENT_ITEM); + await t + .expect(responseBodyProposition.items[0].schema) + .eql(DEFAULT_CONTENT_ITEM.schema); + await t + .expect(responseBodyProposition.items[0].meta) + .eql(DEFAULT_CONTENT_ITEM.meta); await t.expect(responseBodyProposition.items[1]).eql(MEASUREMENT_ITEM); const formBasedScopePropositions = eventResult.propositions.filter( @@ -114,7 +119,16 @@ test("Test C5805676: Merged metric propositions should be delivered", async () = await t.expect(formBasedScopePropositions[0].items.length).eql(2); await t.expect(formBasedScopePropositions[0].renderAttempted).eql(false); await t - .expect(formBasedScopePropositions[0].items[0]) - .eql(DEFAULT_CONTENT_ITEM); - await t.expect(formBasedScopePropositions[0].items[1]).eql(MEASUREMENT_ITEM); + .expect(formBasedScopePropositions[0].items[0].schema) + .eql(DEFAULT_CONTENT_ITEM.schema); + await t + .expect(formBasedScopePropositions[0].items[0].meta) + .eql(DEFAULT_CONTENT_ITEM.meta); + + await t + .expect(formBasedScopePropositions[0].items[1].data) + .eql(MEASUREMENT_ITEM.data); + await t + .expect(formBasedScopePropositions[0].items[1].schema) + .eql(MEASUREMENT_ITEM.schema); }); diff --git a/test/unit/helpers/createDecoratePropositionForTest2.js b/test/unit/helpers/createDecoratePropositionForTest2.js new file mode 100644 index 000000000..43528d0d9 --- /dev/null +++ b/test/unit/helpers/createDecoratePropositionForTest2.js @@ -0,0 +1,53 @@ +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ +import { + ADOBE_JOURNEY_OPTIMIZER, + ADOBE_TARGET, +} from "../../../src/constants/decisionProvider.js"; +import createInteractionStorage from "../../../src/components/Personalization/createInteractionStorage.js"; +import createDecorateProposition from "../../../src/components/Personalization/handlers/createDecorateProposition.js"; +import { + ALWAYS, + NEVER, +} from "../../../src/constants/propositionInteractionType.js"; + +export default ({ + autoCollectPropositionInteractions = { + [ADOBE_JOURNEY_OPTIMIZER]: ALWAYS, + [ADOBE_TARGET]: NEVER, + }, + type, + propositionId = "propositionID", + itemId = "itemId", + trackingLabel = "trackingLabel", + scopeType = "page", + notification = { + id: "notifyId", + scope: "web://mywebsite.com", + scopeDetails: { + something: true, + decisionProvider: ADOBE_JOURNEY_OPTIMIZER, + }, + }, +} = {}) => { + const { storeInteractionMeta } = createInteractionStorage(); + return createDecorateProposition( + autoCollectPropositionInteractions, + type, + propositionId, + itemId, + trackingLabel, + scopeType, + notification, + storeInteractionMeta, + ); +}; diff --git a/test/unit/specs/components/Personalization/dom-actions/dom/addCssClass.spec.js b/test/unit/specs/components/Personalization/dom-actions/dom/addCssClass.spec.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/specs/components/Personalization/dom-actions/dom/hasCssClass.spec.js b/test/unit/specs/components/Personalization/dom-actions/dom/hasCssClass.spec.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/specs/components/Personalization/handlers/createRenderedHandler.spec.js b/test/unit/specs/components/Personalization/handlers/createRenderedHandler.spec.js new file mode 100644 index 000000000..e69de29bb