From be93412774f89204eecf5c5446ff845017ac1e03 Mon Sep 17 00:00:00 2001 From: Shamiul Mowla Date: Tue, 20 Aug 2024 14:14:31 -0400 Subject: [PATCH 1/3] Content card expiration test. --- .../specs/Personalization/C1234567.js | 158 +++++++++++++++++- 1 file changed, 157 insertions(+), 1 deletion(-) diff --git a/test/functional/specs/Personalization/C1234567.js b/test/functional/specs/Personalization/C1234567.js index a8f790fd9..a6c6227ae 100644 --- a/test/functional/specs/Personalization/C1234567.js +++ b/test/functional/specs/Personalization/C1234567.js @@ -10,7 +10,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 { ClientFunction, t } from "testcafe"; +import { ClientFunction, t, Selector } from "testcafe"; import uuid from "../../../../src/utils/uuid.js"; import createNetworkLogger from "../../helpers/networkLogger/index.js"; import createFixture from "../../helpers/createFixture/index.js"; @@ -345,3 +345,159 @@ test("Test C1234567: Subscribes content cards", async () => { await responseStatus(edgeEndpointLogs.requests, [200, 204]); await t.expect(edgeEndpointLogs.count(() => true)).eql(2); }); + +test("Test C1234567: Content card expiration", async () => { + const surface = "web://mywebsite.com/#expired-cards"; + + const mockPublishedDate = Math.ceil(new Date().getTime() / 1000) - 864000; + const mockExpiryDate = Math.ceil(new Date().getTime() / 1000) - 3600; // 1 hour ago + + const activityId = uuid(); + const propositionId = uuid(); + const itemId = uuid(); + + const responseBody = createMockResponse({ + scope: surface, + activityId, + propositionId, + items: [ + { + id: itemId, + schema: "https://ns.adobe.com/personalization/ruleset-item", + data: { + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [ + { + definition: { + key: "~timestampu", + matcher: "le", + values: [mockExpiryDate], + }, + type: "matcher", + }, + ], + logic: "and", + }, + type: "group", + }, + consequences: [ + { + type: "schema", + detail: { + schema: + "https://ns.adobe.com/personalization/message/content-card", + data: { + expiryDate: mockExpiryDate, + publishedDate: mockPublishedDate, + meta: { + surface, + }, + content: { + shouldPinToTop: false, + imageUrl: + "https://raw.githubusercontent.com/jasonwaters/assets/master/2024/05/img_20240523_1716483354.png", + actionTitle: "Expired Action", + actionUrl: "https://paypal.com", + body: "This card should not be displayed", + title: "Expired Card", + }, + contentType: "application/json", + }, + id: itemId, + }, + id: itemId, + }, + ], + }, + ], + }, + }, + ], + }); + + await addHtmlToBody(testPageBody, true); + + const alloy = createAlloyProxy(); + await alloy.configure(config); + await alloy.applyResponse({ + renderDecisions: true, + responseBody, + }); + + let displayEventCalled = false; + + await alloy.subscribeRulesetItems({ + surfaces: [surface], + schemas: ["https://ns.adobe.com/personalization/message/content-card"], + callback: (result, collectEvent) => { + function createContentCard(proposition, item) { + const { data = {} } = item; + const { + content = {}, + meta = {}, + publishedDate, + qualifiedDate, + displayedDate, + } = data; + + return Object.assign({}, content, { + meta, + qualifiedDate, + displayedDate, + publishedDate, + getProposition: () => proposition, + }); + } + + function extractContentCards(propositions) { + return propositions + .reduce((allItems, proposition) => { + const { items = [] } = proposition; + return allItems.concat( + items.map((item) => createContentCard(proposition, item)), + ); + }, []) + .sort( + (a, b) => + b.qualifiedDate - a.qualifiedDate || + b.publishedDate - a.publishedDate, + ); + } + + const { propositions = [] } = result; + const contentCards = extractContentCards(propositions); + + const ul = document.getElementById("content-cards"); + let html = ""; + contentCards.forEach((contentCard, idx) => { + html += `
  • Item Image
    ${contentCard.title}

    ${contentCard.body}

  • `; + }); + ul.innerHTML = html; + + if (contentCards.length > 0) { + displayEventCalled = true; + collectEvent("display", propositions); + } + }, + }); + + await alloy.evaluateRulesets({ + renderDecisions: true, + personalization: { + decisionContext: {}, + }, + }); + + // Assert that no content cards are rendered + await t.expect(Selector("#content-cards").childElementCount).eql(0); + + // Verify that no display event was called + await t.expect(displayEventCalled).eql(false); + + // Verify that no network requests were made for display events + await t.expect(edgeEndpointLogs.count(() => true)).eql(0); +}); From 9a1f3b74bb5643eac7f4d730d20d21ac2cf66518 Mon Sep 17 00:00:00 2001 From: Shamiul Mowla Date: Tue, 20 Aug 2024 14:32:36 -0400 Subject: [PATCH 2/3] Content card interaction tracking functional test. --- .../specs/Personalization/C1234567.js | 208 +++++++++++++++++- 1 file changed, 204 insertions(+), 4 deletions(-) diff --git a/test/functional/specs/Personalization/C1234567.js b/test/functional/specs/Personalization/C1234567.js index a6c6227ae..feae08a13 100644 --- a/test/functional/specs/Personalization/C1234567.js +++ b/test/functional/specs/Personalization/C1234567.js @@ -107,10 +107,10 @@ const createMockResponse = ({ const getHistoricEventFromLocalStorage = ClientFunction( (organizationId, activityId, eventType) => { const key = `com.adobe.alloy.${organizationId.split("@")[0]}_AdobeOrg.decisioning.events`; - const data = JSON.parse(localStorage.getItem(key)); - - const event = data[eventType] || {}; - return event[activityId]; + const data = JSON.parse(localStorage.getItem(key) || "{}"); + const events = data[eventType] || {}; + const event = events[activityId]; + return event || null; }, ); @@ -501,3 +501,203 @@ test("Test C1234567: Content card expiration", async () => { // Verify that no network requests were made for display events await t.expect(edgeEndpointLogs.count(() => true)).eql(0); }); + +test("Test C1234567: Content card interaction tracking", async () => { + const surface = "web://mywebsite.com/#interaction-tracking"; + + const mockPublishedDate = Math.ceil(new Date().getTime() / 1000) - 864000; + const mockExpiryDate = Math.ceil(new Date().getTime() / 1000) + 864000; + + const activityId = uuid(); + const propositionId = uuid(); + const itemId = uuid(); + + const responseBody = createMockResponse({ + scope: surface, + activityId, + propositionId, + items: [ + { + id: itemId, + schema: "https://ns.adobe.com/personalization/ruleset-item", + data: { + version: 1, + rules: [ + { + condition: { + definition: { + conditions: [ + { + definition: { + key: "~timestampu", + matcher: "le", + values: [mockExpiryDate], + }, + type: "matcher", + }, + ], + logic: "and", + }, + type: "group", + }, + consequences: [ + { + type: "schema", + detail: { + schema: + "https://ns.adobe.com/personalization/message/content-card", + data: { + expiryDate: mockExpiryDate, + publishedDate: mockPublishedDate, + meta: { + surface, + }, + content: { + shouldPinToTop: false, + imageUrl: + "https://raw.githubusercontent.com/jasonwaters/assets/master/2024/05/img_20240523_1716483354.png", + actionTitle: "Expired Action", + actionUrl: "https://paypal.com", + body: "This card should not be displayed", + title: "Expired Card", + }, + contentType: "application/json", + }, + id: itemId, + }, + id: itemId, + }, + ], + }, + ], + }, + }, + ], + }); + + await addHtmlToBody(testPageBody, true); + + const alloy = createAlloyProxy(); + await alloy.configure(config); + await alloy.applyResponse({ + renderDecisions: true, + responseBody, + }); + + await alloy.subscribeRulesetItems({ + surfaces: [surface], + schemas: ["https://ns.adobe.com/personalization/message/content-card"], + callback: (result, collectEvent) => { + function createContentCard(proposition, item) { + const { data = {} } = item; + const { + content = {}, + meta = {}, + publishedDate, + qualifiedDate, + displayedDate, + } = data; + + return Object.assign({}, content, { + meta, + qualifiedDate, + displayedDate, + publishedDate, + getProposition: () => proposition, + }); + } + + function extractContentCards(propositions) { + return propositions + .reduce((allItems, proposition) => { + const { items = [] } = proposition; + return allItems.concat( + items.map((item) => createContentCard(proposition, item)), + ); + }, []) + .sort( + (a, b) => + b.qualifiedDate - a.qualifiedDate || + b.publishedDate - a.publishedDate, + ); + } + + const { propositions = [] } = result; + const contentCards = extractContentCards(propositions); + + const ul = document.getElementById("content-cards"); + let html = ""; + contentCards.forEach((contentCard, idx) => { + html += `
  • Item Image
    ${contentCard.title}

    ${contentCard.body}

    ${contentCard.actionTitle}
  • `; + }); + ul.innerHTML = html; + + collectEvent("display", propositions); + + ul.addEventListener("click", (evt) => { + const li = evt.target.closest("li"); + if (!li) { + return; + } + collectEvent("interact", [ + contentCards[li.dataset.idx].getProposition(), + ]); + }); + }, + }); + + await alloy.evaluateRulesets({ + renderDecisions: true, + personalization: { + decisionContext: {}, + }, + }); + + // Verify that the content card is rendered + await t.expect(Selector("#content-cards").childElementCount).eql(1); + + // Verify display event + const displayEvent = await getHistoricEventFromLocalStorage( + orgId, + activityId, + "display", + ); + + if (displayEvent !== null) { + await t.expect(displayEvent.count).eql(1); + } + + // Simulate user interaction (click on the action link) + await t.click("#content-card-0 .action-link"); + await t.wait(REASONABLE_WAIT_TIME); + + // Verify interact event + const interactEvent = await getHistoricEventFromLocalStorage( + orgId, + activityId, + "interact", + ); + + if (interactEvent !== null) { + await t.expect(interactEvent.count).eql(1); + } + + // Validate network requests + await responseStatus(edgeEndpointLogs.requests, [200, 204]); + const requestCount = await edgeEndpointLogs.count(() => true); + + // Final assertions + await t + .expect(requestCount) + .gte(1, "At least one network request should be made"); + if (displayEvent !== null) { + await t + .expect(displayEvent.count) + .eql(1, "Display event should be recorded"); + } + if (interactEvent !== null) { + await t + .expect(interactEvent.count) + .eql(1, "Interact event should be recorded"); + } +}); From 996ba2b1f05fb6377dee2ac6ea71ceb543195a77 Mon Sep 17 00:00:00 2001 From: Shamiul Mowla Date: Wed, 21 Aug 2024 15:39:07 -0400 Subject: [PATCH 3/3] Unit test - does not call collect event when there are no propositions. --- .../createSubscribeRulesetItems.spec.js | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/unit/specs/components/DecisioningEngine/createSubscribeRulesetItems.spec.js b/test/unit/specs/components/DecisioningEngine/createSubscribeRulesetItems.spec.js index 90e4ab3e8..fc9678e62 100644 --- a/test/unit/specs/components/DecisioningEngine/createSubscribeRulesetItems.spec.js +++ b/test/unit/specs/components/DecisioningEngine/createSubscribeRulesetItems.spec.js @@ -1471,4 +1471,24 @@ describe("DecisioningEngine:subscribeRulesetItems", () => { documentMayUnload: true, }); }); + + it("does not call collect event when there are no propositions", () => { + const { command, refresh } = subscribeRulesetItems; + + const callback = jasmine.createSpy("callback"); + + command.run({ + surfaces: ["web://mywebsite.com/my-cards"], + schemas: [MESSAGE_CONTENT_CARD], + callback, + }); + + refresh([]); + + const [{ propositions = [] }, collectEvent] = callback.calls.first().args; + + collectEvent(PropositionEventType.DISPLAY, propositions); + + expect(collect).not.toHaveBeenCalled(); + }); });