From d578fb311cc0e04bfc172368b258222a02e2513f Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:56:06 -0600 Subject: [PATCH 01/38] test(integration): migrate data collector functional specs to Vitest+Playwright+MSW --- .../Data Collector/dataCollector.spec.js | 668 ++++++++++++++++++ 1 file changed, 668 insertions(+) create mode 100644 packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js new file mode 100644 index 000000000..64e27fe33 --- /dev/null +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -0,0 +1,668 @@ +/* +Copyright 2026 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 { + test, + expect, + describe, + vi, + beforeEach, + afterEach, +} from "../../helpers/testsSetup/extend.js"; +import { sendEventHandler, setConsentHandler } from "../../helpers/mswjs/handlers.js"; +import alloyConfig from "../../helpers/alloy/config.js"; +import searchForLogMessage from "../../helpers/utils/searchForLogMessage.js"; +import { CONSENT_OUT } from "../../helpers/constants/consent.js"; + +// Poll until condition() returns truthy, then resolve. Rejects on timeout. +const waitUntil = (condition, { intervalMs = 50, timeoutMs = 3000 } = {}) => + new Promise((resolve, reject) => { + const start = Date.now(); + const poll = () => { + if (condition()) { + resolve(); + return; + } + if (Date.now() - start >= timeoutMs) { + reject(new Error("waitUntil timed out")); + return; + } + setTimeout(poll, intervalMs); + }; + poll(); + }); + +// --------------------------------------------------------------------------- +// C2592 – Event command sends a request +// --------------------------------------------------------------------------- +describe("C2592 - Event command sends a request", () => { + test("sendEvent produces an edge interact call with implementationDetails and state", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", alloyConfig); + await alloy("sendEvent"); + + const call = await networkRecorder.findCall(/edge\.adobedc\.net/); + expect(call).toBeDefined(); + expect(call.response.status).toBeGreaterThanOrEqual(200); + expect(call.response.status).toBeLessThanOrEqual(207); + + const body = call.request.body; + expect(body.events[0].xdm.implementationDetails.name).toBe( + "https://ns.adobe.com/experience/alloy", + ); + expect(body.meta.state.cookiesEnabled).toBe(true); + // domain is "" on localhost, so only check the type + expect(typeof body.meta.state.domain).toBe("string"); + }); +}); + +// --------------------------------------------------------------------------- +// C75372 – XDM and data objects passed to sendEvent are not mutated +// --------------------------------------------------------------------------- +describe("C75372 - XDM and data objects are not mutated by sendEvent", () => { + test("original xdm and data objects remain unchanged after sendEvent", async ({ + alloy, + worker, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", alloyConfig); + + const xdmDataLayer = { device: { screenHeight: 1 } }; + const nonXdmDataLayer = { baz: "quux" }; + + await alloy("sendEvent", { + xdm: xdmDataLayer, + data: nonXdmDataLayer, + mergeId: "abc", + }); + + expect(xdmDataLayer).toEqual({ device: { screenHeight: 1 } }); + expect(nonXdmDataLayer).toEqual({ baz: "quux" }); + }); +}); + +// --------------------------------------------------------------------------- +// C1715149 – onBeforeEventSend callback +// --------------------------------------------------------------------------- +describe("C1715149 - onBeforeEventSend callback", () => { + test("callback is invoked and can augment the xdm before the request fires", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + let callbackInvoked = false; + + await alloy("configure", { + ...alloyConfig, + onBeforeEventSend: (content) => { + callbackInvoked = true; + content.xdm.foo = "bar"; + }, + }); + + await alloy("sendEvent"); + + expect(callbackInvoked).toBe(true); + + const call = await networkRecorder.findCall(/edge\.adobedc\.net/); + expect(call).toBeDefined(); + expect(call.request.body.events[0].xdm.foo).toBe("bar"); + }); + + test("callback throwing causes sendEvent to reject without sending a request", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + let callbackInvoked = false; + + await alloy("configure", { + ...alloyConfig, + onBeforeEventSend: () => { + callbackInvoked = true; + throw new Error("Expected Error"); + }, + }); + + let error; + try { + await alloy("sendEvent"); + } catch (e) { + error = e; + } + + expect(callbackInvoked).toBe(true); + expect(error).toBeDefined(); + expect(error.message).toMatch(/Expected Error/); + + // No network request should have been made + const calls = networkRecorder.calls.filter((c) => + /v1\/interact/.test(c.request?.url ?? ""), + ); + expect(calls.length).toBe(0); + }); + + test("callback returning false cancels the event and sendEvent resolves with {}", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + let callbackInvoked = false; + + const consoleSpy = vi.spyOn(console, "info"); + + await alloy("configure", { + ...alloyConfig, + debugEnabled: true, + onBeforeEventSend: () => { + callbackInvoked = true; + return false; + }, + }); + + const result = await alloy("sendEvent"); + + expect(callbackInvoked).toBe(true); + expect(result).toEqual({}); + + const interactCalls = networkRecorder.calls.filter((c) => + /v1\/interact/.test(c.request?.url ?? ""), + ); + expect(interactCalls.length).toBe(0); + + expect(searchForLogMessage(consoleSpy, "Event was canceled")).toBe(true); + + consoleSpy.mockRestore(); + }); +}); + +// --------------------------------------------------------------------------- +// C8119 – Click collection disabled: link click does not send an event +// --------------------------------------------------------------------------- +describe("C8119 - Click collection disabled does not send link click events", () => { + test("clicking a link fires no edge request when clickCollectionEnabled is false", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: false, + }); + networkRecorder.reset(); + + // Add a link and click it + const link = document.createElement("a"); + link.id = "alloy-link-test"; + link.href = "#blank"; + link.textContent = "Test Link"; + document.body.appendChild(link); + + // Prevent navigation so the page stays + link.addEventListener("click", (e) => e.preventDefault()); + link.click(); + + // Fixed wait is necessary: asserting that NO interact call fires after the + // click, so there is no positive observable condition to poll on. + await new Promise((resolve) => setTimeout(resolve, 100)); + + const calls = networkRecorder.calls.filter((c) => + /v1\/interact/.test(c.request?.url ?? ""), + ); + expect(calls.length).toBe(0); + }); +}); + +// --------------------------------------------------------------------------- +// C81184 – Click collection configuration warnings +// --------------------------------------------------------------------------- +describe("C81184 - Click collection configuration warnings", () => { + let consoleSpy; + + beforeEach(() => { + consoleSpy = vi.spyOn(console, "warn"); + }); + + afterEach(() => { + consoleSpy.mockRestore(); + }); + + test("warns when onBeforeLinkClickSend configured but clickCollectionEnabled is false", async ({ + alloy, + }) => { + await alloy("configure", { + ...alloyConfig, + debugEnabled: true, + clickCollectionEnabled: false, + onBeforeLinkClickSend: () => {}, + }); + + expect( + searchForLogMessage( + consoleSpy, + "onBeforeLinkClickSend", + ), + ).toBe(true); + }); + + test("warns when downloadLinkQualifier configured but clickCollectionEnabled is false", async ({ + alloy, + }) => { + await alloy("configure", { + ...alloyConfig, + debugEnabled: true, + clickCollectionEnabled: false, + downloadLinkQualifier: "\\.pdf$", + }); + + expect( + searchForLogMessage( + consoleSpy, + "downloadLinkQualifier", + ), + ).toBe(true); + }); + + test("does not warn for default downloadLinkQualifier when clickCollectionEnabled is false", async ({ + alloy, + }) => { + await alloy("configure", { + ...alloyConfig, + debugEnabled: true, + clickCollectionEnabled: false, + }); + + expect( + searchForLogMessage( + consoleSpy, + "downloadLinkQualifier", + ), + ).toBe(false); + }); + + test("does not warn about disabled click collection when clickCollectionEnabled is true with downloadLinkQualifier", async ({ + alloy, + }) => { + // Note: onBeforeLinkClickSend is deprecated and always logs a deprecation + // warning, regardless of clickCollectionEnabled. This test uses + // clickCollection.filterClickDetails (the non-deprecated alternative) + // to verify that no "will be ignored" warning fires when clickCollectionEnabled is true. + await alloy("configure", { + ...alloyConfig, + debugEnabled: true, + clickCollectionEnabled: true, + clickCollection: { + filterClickDetails: () => true, + }, + downloadLinkQualifier: "\\.pdf$", + }); + + // The "will be ignored because clickCollectionEnabled is false" warning + // should NOT fire when clickCollectionEnabled is true + expect( + searchForLogMessage( + consoleSpy, + "will be ignored because clickCollectionEnabled is false", + ), + ).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// C81181 – onBeforeLinkClickSend callback +// --------------------------------------------------------------------------- +describe("C81181 - onBeforeLinkClickSend callback", () => { + test("returning false from onBeforeLinkClickSend cancels the link click request", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + onBeforeLinkClickSend: () => false, + }); + networkRecorder.reset(); + + const link = document.createElement("a"); + link.id = "alloy-link-test"; + link.href = "#valid"; + link.textContent = "Test Link"; + document.body.appendChild(link); + link.addEventListener("click", (e) => e.preventDefault()); + link.click(); + + // Fixed wait is necessary: asserting that NO interact call fires after the + // click, so there is no positive observable condition to poll on. + await new Promise((resolve) => setTimeout(resolve, 200)); + + const calls = networkRecorder.calls.filter((c) => + /v1\/interact/.test(c.request?.url ?? ""), + ); + expect(calls.length).toBe(0); + }); + + test("returning false from filterClickDetails cancels the link click request", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { + filterClickDetails: () => false, + }, + }); + networkRecorder.reset(); + + const link = document.createElement("a"); + link.id = "alloy-link-test"; + link.href = "#valid"; + link.textContent = "Test Link"; + document.body.appendChild(link); + link.addEventListener("click", (e) => e.preventDefault()); + link.click(); + + // Fixed wait is necessary: asserting that NO interact call fires after the + // click, so there is no positive observable condition to poll on. + await new Promise((resolve) => setTimeout(resolve, 200)); + + const calls = networkRecorder.calls.filter((c) => + /v1\/interact/.test(c.request?.url ?? ""), + ); + expect(calls.length).toBe(0); + }); + + test("onBeforeLinkClickSend can augment xdm and data before the request fires", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { + internalLinkEnabled: true, + eventGroupingEnabled: false, + }, + onBeforeLinkClickSend: (options) => { + const { xdm, data } = options; + xdm.web.webInteraction.name = "Augmented name"; + data.customField = "test123"; + return true; + }, + }); + + const link = document.createElement("a"); + link.id = "alloy-link-test"; + link.href = "#internal"; + link.textContent = "Test Link"; + document.body.appendChild(link); + link.addEventListener("click", (e) => e.preventDefault()); + link.click(); + + const call = await networkRecorder.findCall(/v1\/interact/); + expect(call).toBeDefined(); + + const event = call.request.body.events[0]; + expect(event.xdm.web.webInteraction.name).toBe("Augmented name"); + expect(event.data.customField).toBe("test123"); + }); +}); + +// --------------------------------------------------------------------------- +// C11693274 – URL query parameters do not affect exit link classification +// --------------------------------------------------------------------------- +describe("C11693274 - URL query params do not affect exit link classification", () => { + test("link to external domain with query param containing current domain is still classified as exit", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { + externalLinkEnabled: true, + eventGroupingEnabled: false, + }, + }); + + // href contains current domain only in the query string (not the host) + const link = document.createElement("a"); + link.id = "alloy-link-test"; + // Build a URL that has the test page's hostname only in a query param + const externalUrl = `https://example.com/?exclude-this=${window.location.hostname}`; + link.href = externalUrl; + link.textContent = "Test Link"; + document.body.appendChild(link); + link.addEventListener("click", (e) => e.preventDefault()); + link.click(); + + const call = await networkRecorder.findCall(/v1\/interact/); + expect(call).toBeDefined(); + + const eventXdm = call.request.body.events[0].xdm; + expect(eventXdm.eventType).toBe("web.webinteraction.linkClicks"); + expect(eventXdm.web.webInteraction.type).toBe("exit"); + expect(eventXdm.web.webInteraction.URL).toBe(externalUrl); + }); +}); + +// --------------------------------------------------------------------------- +// C225010 – Click collection handles errors when user declines consent +// --------------------------------------------------------------------------- +describe("C225010 - Click collection handles consent declined gracefully", () => { + test("warning logged and no unhandled rejection when link clicked after opt-out", async ({ + alloy, + worker, + }) => { + worker.use(setConsentHandler); + + const consoleSpy = vi.spyOn(console, "warn"); + + const unhandledRejections = []; + const rejectionHandler = (event) => { + unhandledRejections.push(event.reason); + }; + window.addEventListener("unhandledrejection", rejectionHandler); + + await alloy("configure", { + ...alloyConfig, + defaultConsent: "pending", + debugEnabled: true, + clickCollectionEnabled: true, + clickCollection: { + eventGroupingEnabled: false, + }, + }); + + await alloy("setConsent", CONSENT_OUT); + + const link = document.createElement("a"); + link.id = "alloy-link-test"; + link.href = "#foo"; + link.textContent = "Test Link"; + document.body.appendChild(link); + link.addEventListener("click", (e) => e.preventDefault()); + link.click(); + + // Poll until the "declined consent" warning log appears instead of sleeping. + await waitUntil( + () => searchForLogMessage(consoleSpy, "The user declined consent"), + { intervalMs: 50, timeoutMs: 3000 }, + ); + + expect( + searchForLogMessage(consoleSpy, "The user declined consent"), + ).toBe(true); + expect(unhandledRejections.length).toBe(0); + + window.removeEventListener("unhandledrejection", rejectionHandler); + consoleSpy.mockRestore(); + }); +}); + +// --------------------------------------------------------------------------- +// C455258 – sendEvent uses /collect when documentUnloading and identity exists +// --------------------------------------------------------------------------- +// Skipped: the /collect endpoint uses sendBeacon, which is not intercepted by +// MSW in browser mode. This test was a baseline failure in the functional suite +// ("Collect endpoint" failures). See FUNCTIONAL_MIGRATION_PLAN.md §1. +test.skip("C455258 - sendEvent sends to collect endpoint when documentUnloading=true after identity established", () => {}); + +// --------------------------------------------------------------------------- +// C8118 – Click collection sends to interact/collect depending on identity +// --------------------------------------------------------------------------- +// Skipped: tests that assert collect-vs-interact routing depend on sendBeacon +// interception via MSW, which is not reliably supported in browser mode, and +// this test was a baseline failure in the functional suite. +// See FUNCTIONAL_MIGRATION_PLAN.md §1. +test.skip("C8118 - link click routes to interact (no identity) then collect (identity established)", () => {}); + +// --------------------------------------------------------------------------- +// C9369211 – sendEvent includes a Referer header +// --------------------------------------------------------------------------- +// Skipped: the collect-endpoint portion of this test uses sendBeacon, which +// MSW cannot intercept in browser mode. The interact portion relies on +// inspecting request headers that MSW/networkRecorder may not surface +// consistently. This was a baseline failure in the functional suite. +// See FUNCTIONAL_MIGRATION_PLAN.md §1. +test.skip("C9369211 - sendEvent includes a Referer header on interact and collect requests", () => {}); + +// --------------------------------------------------------------------------- +// C81182 – onBeforeLinkClickSend with personalization metric +// --------------------------------------------------------------------------- +// Skipped: all sub-tests in the source functional file are already marked +// test.skip because they require a specific personalization response from the +// live edge that is difficult to reproduce deterministically with MSW mocks. +test.skip("C81182 - onBeforeLinkClickSend interacts with personalization metric on link (source tests skipped)", () => {}); + +// --------------------------------------------------------------------------- +// C81183 – getLinkDetails monitoring hook +// --------------------------------------------------------------------------- +describe("C81183 - getLinkDetails monitoring hook via __alloyMonitors", () => { + test("getLinkDetails returns correct link info for a visible link element", async ({ + alloy, + }) => { + // Install monitor BEFORE configure so onInstanceConfigured fires + window.__alloyMonitors = window.__alloyMonitors || []; + window.__alloyMonitors.push({ + onInstanceConfigured(data) { + window.___getLinkDetails = data.getLinkDetails; + }, + }); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + }); + + const link = document.createElement("a"); + link.id = "alloy-link-test"; + link.href = "https://example.com/valid.html"; + link.textContent = "Test Link"; + document.body.appendChild(link); + + const result = window.___getLinkDetails(link); + expect(result).toBeTruthy(); + expect(result.linkName).toBeTruthy(); + expect(result.linkUrl).toContain("example.com"); + expect(result.linkType).toBe("exit"); + }); + + test("getLinkDetails returns results even when clickCollectionEnabled is false", async ({ + alloy, + }) => { + window.__alloyMonitors = window.__alloyMonitors || []; + window.__alloyMonitors.push({ + onInstanceConfigured(data) { + window.___getLinkDetails = data.getLinkDetails; + }, + }); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: false, + }); + + const link = document.createElement("a"); + link.id = "alloy-link-test-disabled"; + link.href = "https://example.com/"; + link.textContent = "External Link"; + document.body.appendChild(link); + + const result = window.___getLinkDetails(link); + expect(result).toBeTruthy(); + expect(result.linkName).toBeTruthy(); + expect(result.linkType).toBe("exit"); + }); + + test("getLinkDetails returns falsy for an element that would not produce a click event", async ({ + alloy, + }) => { + window.__alloyMonitors = window.__alloyMonitors || []; + window.__alloyMonitors.push({ + onInstanceConfigured(data) { + window.___getLinkDetails = data.getLinkDetails; + }, + }); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + onBeforeLinkClickSend: (options) => { + const { clickedElement } = options; + if (clickedElement.id === "cancel-alloy-link-test") { + return false; + } + return true; + }, + }); + + const cancelLink = document.createElement("a"); + cancelLink.id = "cancel-alloy-link-test"; + cancelLink.href = "https://example.com/canceled.html"; + cancelLink.textContent = "Canceled Link"; + document.body.appendChild(cancelLink); + + // getLinkDetails itself doesn't invoke onBeforeLinkClickSend — it returns + // the raw link details regardless of the callback. This just proves the + // monitor hook is accessible and returns a value. + const result = window.___getLinkDetails(cancelLink); + // The result may or may not be defined depending on implementation, but + // the call itself must not throw. + expect(() => window.___getLinkDetails(cancelLink)).not.toThrow(); + }); +}); From be7f3879cd7a133da96c257dc16b3399d4e97cbe Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Fri, 12 Jun 2026 12:26:49 -0600 Subject: [PATCH 02/38] chore: apply prettier and eslint formatting Co-Authored-By: Claude Sonnet 4.6 --- .../Data Collector/dataCollector.spec.js | 34 +++++++------------ 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 64e27fe33..63be9188a 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -17,7 +17,10 @@ import { beforeEach, afterEach, } from "../../helpers/testsSetup/extend.js"; -import { sendEventHandler, setConsentHandler } from "../../helpers/mswjs/handlers.js"; +import { + sendEventHandler, + setConsentHandler, +} from "../../helpers/mswjs/handlers.js"; import alloyConfig from "../../helpers/alloy/config.js"; import searchForLogMessage from "../../helpers/utils/searchForLogMessage.js"; import { CONSENT_OUT } from "../../helpers/constants/consent.js"; @@ -259,12 +262,7 @@ describe("C81184 - Click collection configuration warnings", () => { onBeforeLinkClickSend: () => {}, }); - expect( - searchForLogMessage( - consoleSpy, - "onBeforeLinkClickSend", - ), - ).toBe(true); + expect(searchForLogMessage(consoleSpy, "onBeforeLinkClickSend")).toBe(true); }); test("warns when downloadLinkQualifier configured but clickCollectionEnabled is false", async ({ @@ -277,12 +275,7 @@ describe("C81184 - Click collection configuration warnings", () => { downloadLinkQualifier: "\\.pdf$", }); - expect( - searchForLogMessage( - consoleSpy, - "downloadLinkQualifier", - ), - ).toBe(true); + expect(searchForLogMessage(consoleSpy, "downloadLinkQualifier")).toBe(true); }); test("does not warn for default downloadLinkQualifier when clickCollectionEnabled is false", async ({ @@ -294,12 +287,9 @@ describe("C81184 - Click collection configuration warnings", () => { clickCollectionEnabled: false, }); - expect( - searchForLogMessage( - consoleSpy, - "downloadLinkQualifier", - ), - ).toBe(false); + expect(searchForLogMessage(consoleSpy, "downloadLinkQualifier")).toBe( + false, + ); }); test("does not warn about disabled click collection when clickCollectionEnabled is true with downloadLinkQualifier", async ({ @@ -524,9 +514,9 @@ describe("C225010 - Click collection handles consent declined gracefully", () => { intervalMs: 50, timeoutMs: 3000 }, ); - expect( - searchForLogMessage(consoleSpy, "The user declined consent"), - ).toBe(true); + expect(searchForLogMessage(consoleSpy, "The user declined consent")).toBe( + true, + ); expect(unhandledRejections.length).toBe(0); window.removeEventListener("unhandledrejection", rejectionHandler); From 09b467388f027894d69d818bcc07be239be0293b Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:13:32 -0600 Subject: [PATCH 03/38] Deslop the test file --- .../Data Collector/dataCollector.spec.js | 47 ------------------- 1 file changed, 47 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 63be9188a..30046b3d7 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -25,7 +25,6 @@ import alloyConfig from "../../helpers/alloy/config.js"; import searchForLogMessage from "../../helpers/utils/searchForLogMessage.js"; import { CONSENT_OUT } from "../../helpers/constants/consent.js"; -// Poll until condition() returns truthy, then resolve. Rejects on timeout. const waitUntil = (condition, { intervalMs = 50, timeoutMs = 3000 } = {}) => new Promise((resolve, reject) => { const start = Date.now(); @@ -43,9 +42,6 @@ const waitUntil = (condition, { intervalMs = 50, timeoutMs = 3000 } = {}) => poll(); }); -// --------------------------------------------------------------------------- -// C2592 – Event command sends a request -// --------------------------------------------------------------------------- describe("C2592 - Event command sends a request", () => { test("sendEvent produces an edge interact call with implementationDetails and state", async ({ alloy, @@ -72,9 +68,6 @@ describe("C2592 - Event command sends a request", () => { }); }); -// --------------------------------------------------------------------------- -// C75372 – XDM and data objects passed to sendEvent are not mutated -// --------------------------------------------------------------------------- describe("C75372 - XDM and data objects are not mutated by sendEvent", () => { test("original xdm and data objects remain unchanged after sendEvent", async ({ alloy, @@ -98,9 +91,6 @@ describe("C75372 - XDM and data objects are not mutated by sendEvent", () => { }); }); -// --------------------------------------------------------------------------- -// C1715149 – onBeforeEventSend callback -// --------------------------------------------------------------------------- describe("C1715149 - onBeforeEventSend callback", () => { test("callback is invoked and can augment the xdm before the request fires", async ({ alloy, @@ -156,7 +146,6 @@ describe("C1715149 - onBeforeEventSend callback", () => { expect(error).toBeDefined(); expect(error.message).toMatch(/Expected Error/); - // No network request should have been made const calls = networkRecorder.calls.filter((c) => /v1\/interact/.test(c.request?.url ?? ""), ); @@ -199,9 +188,6 @@ describe("C1715149 - onBeforeEventSend callback", () => { }); }); -// --------------------------------------------------------------------------- -// C8119 – Click collection disabled: link click does not send an event -// --------------------------------------------------------------------------- describe("C8119 - Click collection disabled does not send link click events", () => { test("clicking a link fires no edge request when clickCollectionEnabled is false", async ({ alloy, @@ -216,7 +202,6 @@ describe("C8119 - Click collection disabled does not send link click events", () }); networkRecorder.reset(); - // Add a link and click it const link = document.createElement("a"); link.id = "alloy-link-test"; link.href = "#blank"; @@ -238,9 +223,6 @@ describe("C8119 - Click collection disabled does not send link click events", () }); }); -// --------------------------------------------------------------------------- -// C81184 – Click collection configuration warnings -// --------------------------------------------------------------------------- describe("C81184 - Click collection configuration warnings", () => { let consoleSpy; @@ -320,9 +302,6 @@ describe("C81184 - Click collection configuration warnings", () => { }); }); -// --------------------------------------------------------------------------- -// C81181 – onBeforeLinkClickSend callback -// --------------------------------------------------------------------------- describe("C81181 - onBeforeLinkClickSend callback", () => { test("returning false from onBeforeLinkClickSend cancels the link click request", async ({ alloy, @@ -429,9 +408,6 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { }); }); -// --------------------------------------------------------------------------- -// C11693274 – URL query parameters do not affect exit link classification -// --------------------------------------------------------------------------- describe("C11693274 - URL query params do not affect exit link classification", () => { test("link to external domain with query param containing current domain is still classified as exit", async ({ alloy, @@ -452,7 +428,6 @@ describe("C11693274 - URL query params do not affect exit link classification", // href contains current domain only in the query string (not the host) const link = document.createElement("a"); link.id = "alloy-link-test"; - // Build a URL that has the test page's hostname only in a query param const externalUrl = `https://example.com/?exclude-this=${window.location.hostname}`; link.href = externalUrl; link.textContent = "Test Link"; @@ -470,9 +445,6 @@ describe("C11693274 - URL query params do not affect exit link classification", }); }); -// --------------------------------------------------------------------------- -// C225010 – Click collection handles errors when user declines consent -// --------------------------------------------------------------------------- describe("C225010 - Click collection handles consent declined gracefully", () => { test("warning logged and no unhandled rejection when link clicked after opt-out", async ({ alloy, @@ -508,7 +480,6 @@ describe("C225010 - Click collection handles consent declined gracefully", () => link.addEventListener("click", (e) => e.preventDefault()); link.click(); - // Poll until the "declined consent" warning log appears instead of sleeping. await waitUntil( () => searchForLogMessage(consoleSpy, "The user declined consent"), { intervalMs: 50, timeoutMs: 3000 }, @@ -524,26 +495,17 @@ describe("C225010 - Click collection handles consent declined gracefully", () => }); }); -// --------------------------------------------------------------------------- -// C455258 – sendEvent uses /collect when documentUnloading and identity exists -// --------------------------------------------------------------------------- // Skipped: the /collect endpoint uses sendBeacon, which is not intercepted by // MSW in browser mode. This test was a baseline failure in the functional suite // ("Collect endpoint" failures). See FUNCTIONAL_MIGRATION_PLAN.md §1. test.skip("C455258 - sendEvent sends to collect endpoint when documentUnloading=true after identity established", () => {}); -// --------------------------------------------------------------------------- -// C8118 – Click collection sends to interact/collect depending on identity -// --------------------------------------------------------------------------- // Skipped: tests that assert collect-vs-interact routing depend on sendBeacon // interception via MSW, which is not reliably supported in browser mode, and // this test was a baseline failure in the functional suite. // See FUNCTIONAL_MIGRATION_PLAN.md §1. test.skip("C8118 - link click routes to interact (no identity) then collect (identity established)", () => {}); -// --------------------------------------------------------------------------- -// C9369211 – sendEvent includes a Referer header -// --------------------------------------------------------------------------- // Skipped: the collect-endpoint portion of this test uses sendBeacon, which // MSW cannot intercept in browser mode. The interact portion relies on // inspecting request headers that MSW/networkRecorder may not surface @@ -551,17 +513,11 @@ test.skip("C8118 - link click routes to interact (no identity) then collect (ide // See FUNCTIONAL_MIGRATION_PLAN.md §1. test.skip("C9369211 - sendEvent includes a Referer header on interact and collect requests", () => {}); -// --------------------------------------------------------------------------- -// C81182 – onBeforeLinkClickSend with personalization metric -// --------------------------------------------------------------------------- // Skipped: all sub-tests in the source functional file are already marked // test.skip because they require a specific personalization response from the // live edge that is difficult to reproduce deterministically with MSW mocks. test.skip("C81182 - onBeforeLinkClickSend interacts with personalization metric on link (source tests skipped)", () => {}); -// --------------------------------------------------------------------------- -// C81183 – getLinkDetails monitoring hook -// --------------------------------------------------------------------------- describe("C81183 - getLinkDetails monitoring hook via __alloyMonitors", () => { test("getLinkDetails returns correct link info for a visible link element", async ({ alloy, @@ -650,9 +606,6 @@ describe("C81183 - getLinkDetails monitoring hook via __alloyMonitors", () => { // getLinkDetails itself doesn't invoke onBeforeLinkClickSend — it returns // the raw link details regardless of the callback. This just proves the // monitor hook is accessible and returns a value. - const result = window.___getLinkDetails(cancelLink); - // The result may or may not be defined depending on implementation, but - // the call itself must not throw. expect(() => window.___getLinkDetails(cancelLink)).not.toThrow(); }); }); From 9df82983318eac62b3d8bbd404dd8f295770c67f Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:44:13 -0600 Subject: [PATCH 04/38] refactor(integration): replace hand-rolled waitUntil with expect.poll Remove the bespoke waitUntil poller and collapse its one usage in C225010 into a single expect.poll() call, which also eliminates the redundant re-assert that followed immediately after. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Data Collector/dataCollector.spec.js | 27 +++---------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 30046b3d7..c8a86ab38 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -25,23 +25,6 @@ import alloyConfig from "../../helpers/alloy/config.js"; import searchForLogMessage from "../../helpers/utils/searchForLogMessage.js"; import { CONSENT_OUT } from "../../helpers/constants/consent.js"; -const waitUntil = (condition, { intervalMs = 50, timeoutMs = 3000 } = {}) => - new Promise((resolve, reject) => { - const start = Date.now(); - const poll = () => { - if (condition()) { - resolve(); - return; - } - if (Date.now() - start >= timeoutMs) { - reject(new Error("waitUntil timed out")); - return; - } - setTimeout(poll, intervalMs); - }; - poll(); - }); - describe("C2592 - Event command sends a request", () => { test("sendEvent produces an edge interact call with implementationDetails and state", async ({ alloy, @@ -480,14 +463,10 @@ describe("C225010 - Click collection handles consent declined gracefully", () => link.addEventListener("click", (e) => e.preventDefault()); link.click(); - await waitUntil( - () => searchForLogMessage(consoleSpy, "The user declined consent"), - { intervalMs: 50, timeoutMs: 3000 }, - ); + await expect + .poll(() => searchForLogMessage(consoleSpy, "The user declined consent")) + .toBe(true); - expect(searchForLogMessage(consoleSpy, "The user declined consent")).toBe( - true, - ); expect(unhandledRejections.length).toBe(0); window.removeEventListener("unhandledrejection", rejectionHandler); From afa8877aa5ae2083aa2f4f28bdd81b796e9f6196 Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:44:55 -0600 Subject: [PATCH 05/38] refactor(integration): centralize negative-assertion wait timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace drifting magic numbers (100ms, 200ms) with a single named NO_REQUEST_WAIT_MS constant. The fixed delay is intentional — there is no positive signal to poll on when asserting no request was fired. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../specs/Data Collector/dataCollector.spec.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index c8a86ab38..a623a309d 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -25,6 +25,10 @@ import alloyConfig from "../../helpers/alloy/config.js"; import searchForLogMessage from "../../helpers/utils/searchForLogMessage.js"; import { CONSENT_OUT } from "../../helpers/constants/consent.js"; +// Fixed wait for negative assertions ("no request fired"). There is no positive +// signal to poll on, so a fixed delay is required. +const NO_REQUEST_WAIT_MS = 200; + describe("C2592 - Event command sends a request", () => { test("sendEvent produces an edge interact call with implementationDetails and state", async ({ alloy, @@ -197,7 +201,7 @@ describe("C8119 - Click collection disabled does not send link click events", () // Fixed wait is necessary: asserting that NO interact call fires after the // click, so there is no positive observable condition to poll on. - await new Promise((resolve) => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, NO_REQUEST_WAIT_MS)); const calls = networkRecorder.calls.filter((c) => /v1\/interact/.test(c.request?.url ?? ""), @@ -310,7 +314,7 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { // Fixed wait is necessary: asserting that NO interact call fires after the // click, so there is no positive observable condition to poll on. - await new Promise((resolve) => setTimeout(resolve, 200)); + await new Promise((resolve) => setTimeout(resolve, NO_REQUEST_WAIT_MS)); const calls = networkRecorder.calls.filter((c) => /v1\/interact/.test(c.request?.url ?? ""), @@ -344,7 +348,7 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { // Fixed wait is necessary: asserting that NO interact call fires after the // click, so there is no positive observable condition to poll on. - await new Promise((resolve) => setTimeout(resolve, 200)); + await new Promise((resolve) => setTimeout(resolve, NO_REQUEST_WAIT_MS)); const calls = networkRecorder.calls.filter((c) => /v1\/interact/.test(c.request?.url ?? ""), From 328d463476e324903c6db0fa20c23fdbed69ca64 Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:46:46 -0600 Subject: [PATCH 06/38] refactor(integration): extract DOM helpers and add cleanup Add appendLink/clickLink/cleanupDom to helpers/utils/domHelpers.js and replace the ~7 duplicated createElement+appendChild+addEventListener+click blocks in the spec. A top-level afterEach calls cleanupDom() to prevent link elements from accumulating across tests, and deletes the window.___getLinkDetails global set by the C81183 monitor tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../integration/helpers/utils/domHelpers.js | 44 +++++++++ .../Data Collector/dataCollector.spec.js | 96 +++++++------------ 2 files changed, 81 insertions(+), 59 deletions(-) create mode 100644 packages/browser/test/integration/helpers/utils/domHelpers.js diff --git a/packages/browser/test/integration/helpers/utils/domHelpers.js b/packages/browser/test/integration/helpers/utils/domHelpers.js new file mode 100644 index 000000000..1f1964047 --- /dev/null +++ b/packages/browser/test/integration/helpers/utils/domHelpers.js @@ -0,0 +1,44 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const trackedElements = []; + +/** + * Creates an anchor element, appends it to document.body, and registers it + * for cleanup via cleanupDom(). + */ +export const appendLink = ({ id, href, text }) => { + const link = document.createElement("a"); + link.id = id; + link.href = href; + link.textContent = text; + document.body.appendChild(link); + trackedElements.push(link); + return link; +}; + +/** + * Simulates a non-navigating click on a link element. + */ +export const clickLink = (link) => { + link.addEventListener("click", (e) => e.preventDefault()); + link.click(); +}; + +/** + * Removes all elements appended via appendLink from the DOM. + * Call in afterEach to prevent inter-test accumulation. + */ +export const cleanupDom = () => { + trackedElements.forEach((el) => el.remove()); + trackedElements.length = 0; +}; diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index a623a309d..201153a34 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -24,11 +24,21 @@ import { import alloyConfig from "../../helpers/alloy/config.js"; import searchForLogMessage from "../../helpers/utils/searchForLogMessage.js"; import { CONSENT_OUT } from "../../helpers/constants/consent.js"; +import { + appendLink, + clickLink, + cleanupDom, +} from "../../helpers/utils/domHelpers.js"; // Fixed wait for negative assertions ("no request fired"). There is no positive // signal to poll on, so a fixed delay is required. const NO_REQUEST_WAIT_MS = 200; +afterEach(() => { + cleanupDom(); + delete window.___getLinkDetails; +}); + describe("C2592 - Event command sends a request", () => { test("sendEvent produces an edge interact call with implementationDetails and state", async ({ alloy, @@ -189,15 +199,8 @@ describe("C8119 - Click collection disabled does not send link click events", () }); networkRecorder.reset(); - const link = document.createElement("a"); - link.id = "alloy-link-test"; - link.href = "#blank"; - link.textContent = "Test Link"; - document.body.appendChild(link); - - // Prevent navigation so the page stays - link.addEventListener("click", (e) => e.preventDefault()); - link.click(); + const link = appendLink({ id: "alloy-link-test", href: "#blank", text: "Test Link" }); + clickLink(link); // Fixed wait is necessary: asserting that NO interact call fires after the // click, so there is no positive observable condition to poll on. @@ -304,13 +307,8 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { }); networkRecorder.reset(); - const link = document.createElement("a"); - link.id = "alloy-link-test"; - link.href = "#valid"; - link.textContent = "Test Link"; - document.body.appendChild(link); - link.addEventListener("click", (e) => e.preventDefault()); - link.click(); + const link = appendLink({ id: "alloy-link-test", href: "#valid", text: "Test Link" }); + clickLink(link); // Fixed wait is necessary: asserting that NO interact call fires after the // click, so there is no positive observable condition to poll on. @@ -338,13 +336,8 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { }); networkRecorder.reset(); - const link = document.createElement("a"); - link.id = "alloy-link-test"; - link.href = "#valid"; - link.textContent = "Test Link"; - document.body.appendChild(link); - link.addEventListener("click", (e) => e.preventDefault()); - link.click(); + const link = appendLink({ id: "alloy-link-test", href: "#valid", text: "Test Link" }); + clickLink(link); // Fixed wait is necessary: asserting that NO interact call fires after the // click, so there is no positive observable condition to poll on. @@ -378,13 +371,8 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { }, }); - const link = document.createElement("a"); - link.id = "alloy-link-test"; - link.href = "#internal"; - link.textContent = "Test Link"; - document.body.appendChild(link); - link.addEventListener("click", (e) => e.preventDefault()); - link.click(); + const link = appendLink({ id: "alloy-link-test", href: "#internal", text: "Test Link" }); + clickLink(link); const call = await networkRecorder.findCall(/v1\/interact/); expect(call).toBeDefined(); @@ -413,14 +401,9 @@ describe("C11693274 - URL query params do not affect exit link classification", }); // href contains current domain only in the query string (not the host) - const link = document.createElement("a"); - link.id = "alloy-link-test"; const externalUrl = `https://example.com/?exclude-this=${window.location.hostname}`; - link.href = externalUrl; - link.textContent = "Test Link"; - document.body.appendChild(link); - link.addEventListener("click", (e) => e.preventDefault()); - link.click(); + const link = appendLink({ id: "alloy-link-test", href: externalUrl, text: "Test Link" }); + clickLink(link); const call = await networkRecorder.findCall(/v1\/interact/); expect(call).toBeDefined(); @@ -459,13 +442,8 @@ describe("C225010 - Click collection handles consent declined gracefully", () => await alloy("setConsent", CONSENT_OUT); - const link = document.createElement("a"); - link.id = "alloy-link-test"; - link.href = "#foo"; - link.textContent = "Test Link"; - document.body.appendChild(link); - link.addEventListener("click", (e) => e.preventDefault()); - link.click(); + const link = appendLink({ id: "alloy-link-test", href: "#foo", text: "Test Link" }); + clickLink(link); await expect .poll(() => searchForLogMessage(consoleSpy, "The user declined consent")) @@ -518,11 +496,11 @@ describe("C81183 - getLinkDetails monitoring hook via __alloyMonitors", () => { clickCollectionEnabled: true, }); - const link = document.createElement("a"); - link.id = "alloy-link-test"; - link.href = "https://example.com/valid.html"; - link.textContent = "Test Link"; - document.body.appendChild(link); + const link = appendLink({ + id: "alloy-link-test", + href: "https://example.com/valid.html", + text: "Test Link", + }); const result = window.___getLinkDetails(link); expect(result).toBeTruthy(); @@ -546,11 +524,11 @@ describe("C81183 - getLinkDetails monitoring hook via __alloyMonitors", () => { clickCollectionEnabled: false, }); - const link = document.createElement("a"); - link.id = "alloy-link-test-disabled"; - link.href = "https://example.com/"; - link.textContent = "External Link"; - document.body.appendChild(link); + const link = appendLink({ + id: "alloy-link-test-disabled", + href: "https://example.com/", + text: "External Link", + }); const result = window.___getLinkDetails(link); expect(result).toBeTruthy(); @@ -580,11 +558,11 @@ describe("C81183 - getLinkDetails monitoring hook via __alloyMonitors", () => { }, }); - const cancelLink = document.createElement("a"); - cancelLink.id = "cancel-alloy-link-test"; - cancelLink.href = "https://example.com/canceled.html"; - cancelLink.textContent = "Canceled Link"; - document.body.appendChild(cancelLink); + const cancelLink = appendLink({ + id: "cancel-alloy-link-test", + href: "https://example.com/canceled.html", + text: "Canceled Link", + }); // getLinkDetails itself doesn't invoke onBeforeLinkClickSend — it returns // the raw link details regardless of the callback. This just proves the From 4f7e6623a5e05fd1bdc81488269e66ba407d2c3d Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:47:37 -0600 Subject: [PATCH 07/38] refactor(integration): extract interactCalls helper to deduplicate filter Replace 5 identical inline networkRecorder.calls.filter blocks with a named interactCalls(networkRecorder) helper. Kept as a synchronous filter rather than findCalls() to avoid its retry/wait behavior, which would produce incorrect results when asserting no request was fired. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Data Collector/dataCollector.spec.js | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 201153a34..4d6962655 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -34,6 +34,13 @@ import { // signal to poll on, so a fixed delay is required. const NO_REQUEST_WAIT_MS = 200; +// Synchronous filter — intentionally NOT findCalls(), which retries/waits and +// would give false negatives on assertions that no request was fired. +const interactCalls = (networkRecorder) => + networkRecorder.calls.filter((c) => + /v1\/interact/.test(c.request?.url ?? ""), + ); + afterEach(() => { cleanupDom(); delete window.___getLinkDetails; @@ -143,10 +150,7 @@ describe("C1715149 - onBeforeEventSend callback", () => { expect(error).toBeDefined(); expect(error.message).toMatch(/Expected Error/); - const calls = networkRecorder.calls.filter((c) => - /v1\/interact/.test(c.request?.url ?? ""), - ); - expect(calls.length).toBe(0); + expect(interactCalls(networkRecorder).length).toBe(0); }); test("callback returning false cancels the event and sendEvent resolves with {}", async ({ @@ -174,10 +178,7 @@ describe("C1715149 - onBeforeEventSend callback", () => { expect(callbackInvoked).toBe(true); expect(result).toEqual({}); - const interactCalls = networkRecorder.calls.filter((c) => - /v1\/interact/.test(c.request?.url ?? ""), - ); - expect(interactCalls.length).toBe(0); + expect(interactCalls(networkRecorder).length).toBe(0); expect(searchForLogMessage(consoleSpy, "Event was canceled")).toBe(true); @@ -206,10 +207,7 @@ describe("C8119 - Click collection disabled does not send link click events", () // click, so there is no positive observable condition to poll on. await new Promise((resolve) => setTimeout(resolve, NO_REQUEST_WAIT_MS)); - const calls = networkRecorder.calls.filter((c) => - /v1\/interact/.test(c.request?.url ?? ""), - ); - expect(calls.length).toBe(0); + expect(interactCalls(networkRecorder).length).toBe(0); }); }); @@ -314,10 +312,7 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { // click, so there is no positive observable condition to poll on. await new Promise((resolve) => setTimeout(resolve, NO_REQUEST_WAIT_MS)); - const calls = networkRecorder.calls.filter((c) => - /v1\/interact/.test(c.request?.url ?? ""), - ); - expect(calls.length).toBe(0); + expect(interactCalls(networkRecorder).length).toBe(0); }); test("returning false from filterClickDetails cancels the link click request", async ({ @@ -343,10 +338,7 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { // click, so there is no positive observable condition to poll on. await new Promise((resolve) => setTimeout(resolve, NO_REQUEST_WAIT_MS)); - const calls = networkRecorder.calls.filter((c) => - /v1\/interact/.test(c.request?.url ?? ""), - ); - expect(calls.length).toBe(0); + expect(interactCalls(networkRecorder).length).toBe(0); }); test("onBeforeLinkClickSend can augment xdm and data before the request fires", async ({ From daf4951fb32da5ebc31a2681aa2d2b36ab052841 Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:48:12 -0600 Subject: [PATCH 08/38] refactor(integration): replace try/catch with rejects.toThrow In the C1715149 'callback throwing' test, replace the manual error-capture try/catch pattern with the idiomatic vitest expect().rejects.toThrow(). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../specs/Data Collector/dataCollector.spec.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 4d6962655..007862fd0 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -139,16 +139,8 @@ describe("C1715149 - onBeforeEventSend callback", () => { }, }); - let error; - try { - await alloy("sendEvent"); - } catch (e) { - error = e; - } - + await expect(alloy("sendEvent")).rejects.toThrow(/Expected Error/); expect(callbackInvoked).toBe(true); - expect(error).toBeDefined(); - expect(error.message).toMatch(/Expected Error/); expect(interactCalls(networkRecorder).length).toBe(0); }); From 892ab37138268f39182eef155c8b37b03bd347d2 Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:48:54 -0600 Subject: [PATCH 09/38] refactor(integration): replace callbackInvoked booleans with vi.fn() spies Use vi.fn() for the three onBeforeEventSend callbacks in C1715149 tests and assert with toHaveBeenCalled(), removing the manual boolean tracking. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Data Collector/dataCollector.spec.js | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 007862fd0..21e8c6ca4 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -103,19 +103,15 @@ describe("C1715149 - onBeforeEventSend callback", () => { }) => { worker.use(sendEventHandler); - let callbackInvoked = false; - - await alloy("configure", { - ...alloyConfig, - onBeforeEventSend: (content) => { - callbackInvoked = true; - content.xdm.foo = "bar"; - }, + const onBeforeEventSend = vi.fn((content) => { + content.xdm.foo = "bar"; }); + await alloy("configure", { ...alloyConfig, onBeforeEventSend }); + await alloy("sendEvent"); - expect(callbackInvoked).toBe(true); + expect(onBeforeEventSend).toHaveBeenCalled(); const call = await networkRecorder.findCall(/edge\.adobedc\.net/); expect(call).toBeDefined(); @@ -129,18 +125,14 @@ describe("C1715149 - onBeforeEventSend callback", () => { }) => { worker.use(sendEventHandler); - let callbackInvoked = false; - - await alloy("configure", { - ...alloyConfig, - onBeforeEventSend: () => { - callbackInvoked = true; - throw new Error("Expected Error"); - }, + const onBeforeEventSend = vi.fn(() => { + throw new Error("Expected Error"); }); + await alloy("configure", { ...alloyConfig, onBeforeEventSend }); + await expect(alloy("sendEvent")).rejects.toThrow(/Expected Error/); - expect(callbackInvoked).toBe(true); + expect(onBeforeEventSend).toHaveBeenCalled(); expect(interactCalls(networkRecorder).length).toBe(0); }); @@ -152,22 +144,18 @@ describe("C1715149 - onBeforeEventSend callback", () => { }) => { worker.use(sendEventHandler); - let callbackInvoked = false; - + const onBeforeEventSend = vi.fn(() => false); const consoleSpy = vi.spyOn(console, "info"); await alloy("configure", { ...alloyConfig, debugEnabled: true, - onBeforeEventSend: () => { - callbackInvoked = true; - return false; - }, + onBeforeEventSend, }); const result = await alloy("sendEvent"); - expect(callbackInvoked).toBe(true); + expect(onBeforeEventSend).toHaveBeenCalled(); expect(result).toEqual({}); expect(interactCalls(networkRecorder).length).toBe(0); From bbe3b5833fc155ac7bb684c93f2fe43d7638f54e Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:50:54 -0600 Subject: [PATCH 10/38] refactor(integration): add restoreMocks and remove manual mockRestore Add restoreMocks: true to the browser/integration vitest project so all vi.spyOn() calls are automatically restored after each test. Remove the now-redundant manual consoleSpy.mockRestore() calls in two C1715149 and C225010 tests, and the afterEach in C81184 that did the same. Verified the full integration suite (64 tests) still passes. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../integration/specs/Data Collector/dataCollector.spec.js | 7 ------- packages/browser/vitest.projects.js | 1 + 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 21e8c6ca4..1bccc7f7b 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -161,8 +161,6 @@ describe("C1715149 - onBeforeEventSend callback", () => { expect(interactCalls(networkRecorder).length).toBe(0); expect(searchForLogMessage(consoleSpy, "Event was canceled")).toBe(true); - - consoleSpy.mockRestore(); }); }); @@ -198,10 +196,6 @@ describe("C81184 - Click collection configuration warnings", () => { consoleSpy = vi.spyOn(console, "warn"); }); - afterEach(() => { - consoleSpy.mockRestore(); - }); - test("warns when onBeforeLinkClickSend configured but clickCollectionEnabled is false", async ({ alloy, }) => { @@ -424,7 +418,6 @@ describe("C225010 - Click collection handles consent declined gracefully", () => expect(unhandledRejections.length).toBe(0); window.removeEventListener("unhandledrejection", rejectionHandler); - consoleSpy.mockRestore(); }); }); diff --git a/packages/browser/vitest.projects.js b/packages/browser/vitest.projects.js index d6e5d3346..638c0aabd 100644 --- a/packages/browser/vitest.projects.js +++ b/packages/browser/vitest.projects.js @@ -61,6 +61,7 @@ export const browserTestProjects = [ "packages/browser/test/integration/**/*.{test,spec}.?(c|m)[jt]s?(x)", ], isolate: false, + restoreMocks: true, browser: createBrowserMode(), coverage: packageCoverage, }, From 62f1319001dbf3e6bfeee80417064ed2ddb47da6 Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:54:27 -0600 Subject: [PATCH 11/38] refactor(integration): remove redundant networkRecorder.reset() calls The mid-test reset() calls existed to drop configure-time requests before the link click, but configure uses /identity/acquire not /v1/interact. Since interactCalls() filters specifically for /v1/interact, configure cannot produce false positives and the resets are redundant. Verified by running the suite without them: all 18 tests still pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../integration/specs/Data Collector/dataCollector.spec.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 1bccc7f7b..9faa1ca0c 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -176,7 +176,6 @@ describe("C8119 - Click collection disabled does not send link click events", () ...alloyConfig, clickCollectionEnabled: false, }); - networkRecorder.reset(); const link = appendLink({ id: "alloy-link-test", href: "#blank", text: "Test Link" }); clickLink(link); @@ -277,7 +276,6 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { clickCollectionEnabled: true, onBeforeLinkClickSend: () => false, }); - networkRecorder.reset(); const link = appendLink({ id: "alloy-link-test", href: "#valid", text: "Test Link" }); clickLink(link); @@ -303,7 +301,6 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { filterClickDetails: () => false, }, }); - networkRecorder.reset(); const link = appendLink({ id: "alloy-link-test", href: "#valid", text: "Test Link" }); clickLink(link); From 3b1ec4ed4d00510aae744c21e47fecd7d4e5debc Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:56:21 -0600 Subject: [PATCH 12/38] fix(integration): restore C81183 getLinkDetails(null) assertion The third C81183 test only asserted not.toThrow(), silently dropping the coverage from original test 2 which verified getLinkDetails returns no meaningful data for a non-existent (null) element. Restore it: call getLinkDetails(null) and assert linkName, linkType, and linkUrl are all undefined, matching the original eql({linkName: undefined, ...}) assertion. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Data Collector/dataCollector.spec.js | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 9faa1ca0c..ae3507e33 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -498,7 +498,7 @@ describe("C81183 - getLinkDetails monitoring hook via __alloyMonitors", () => { expect(result.linkType).toBe("exit"); }); - test("getLinkDetails returns falsy for an element that would not produce a click event", async ({ + test("getLinkDetails returns no link data for a null element", async ({ alloy, }) => { window.__alloyMonitors = window.__alloyMonitors || []; @@ -511,24 +511,13 @@ describe("C81183 - getLinkDetails monitoring hook via __alloyMonitors", () => { await alloy("configure", { ...alloyConfig, clickCollectionEnabled: true, - onBeforeLinkClickSend: (options) => { - const { clickedElement } = options; - if (clickedElement.id === "cancel-alloy-link-test") { - return false; - } - return true; - }, - }); - - const cancelLink = appendLink({ - id: "cancel-alloy-link-test", - href: "https://example.com/canceled.html", - text: "Canceled Link", }); - // getLinkDetails itself doesn't invoke onBeforeLinkClickSend — it returns - // the raw link details regardless of the callback. This just proves the - // monitor hook is accessible and returns a value. - expect(() => window.___getLinkDetails(cancelLink)).not.toThrow(); + // Mirrors original C81183 test 2: passing null (a non-existent element) + // should return an object with no meaningful link data, not throw. + const result = window.___getLinkDetails(null); + expect(result.linkName).toBeUndefined(); + expect(result.linkType).toBeUndefined(); + expect(result.linkUrl).toBeUndefined(); }); }); From d95d7c269ebfc4aa38962af5c1472f5394121dd2 Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:57:09 -0600 Subject: [PATCH 13/38] test(integration): add skip stubs for 5 dropped C81181 tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original C81181 functional file had 8 tests; the initial migration included only 3. The 5 dropped tests do not depend on sendBeacon/collect endpoint interception — they are deferred scope, not blocked. Add test.skip stubs so the gap is documented rather than silently blessed. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../specs/Data Collector/dataCollector.spec.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index ae3507e33..83311484a 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -344,6 +344,15 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { expect(event.xdm.web.webInteraction.name).toBe("Augmented name"); expect(event.data.customField).toBe("test123"); }); + + // These five tests from the original C81181 functional file were not included + // in the initial migration. None depend on sendBeacon/collect-endpoint + // interception, so they are deferred scope rather than blocked. + test.skip("C81181 - link click is collected when clickCollectionEnabled and no callback defined", () => {}); + test.skip("C81181 - onBeforeLinkClickSend cancels a request conditionally based on clickedElement.id", () => {}); + test.skip("C81181 - filterClickDetails cancels a request conditionally based on clickedElement.id", () => {}); + test.skip("C81181 - filterClickDetails can augment xdm and data before the request fires", () => {}); + test.skip("C81181 - clickCollection.sessionStorageEnabled:false still captures link click data", () => {}); }); describe("C11693274 - URL query params do not affect exit link classification", () => { From d938f77236b3c09955bf79a6f73fe9bd8c7916e4 Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:57:32 -0600 Subject: [PATCH 14/38] test(integration): migrate C455258 collect routing via sendBeacon recorder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a permanent navigator.sendBeacon recorder (installed before the library loads, since alloy binds sendBeacon once at instance creation) so collect-vs- interact routing can be observed in-page — the same approach the functional suite used. Keeps the suite hermetic: the recorder returns true and no request hits the network. Adds sendEventWithIdentityHandler to establish identity from the interact response so the routing transition can be exercised. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../helpers/alloy/setupBaseCode.js | 3 ++ .../integration/helpers/mswjs/handlers.js | 22 ++++++++++ .../integration/helpers/utils/sendBeacon.js | 39 +++++++++++++++++ .../Data Collector/dataCollector.spec.js | 43 +++++++++++++++++-- 4 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 packages/browser/test/integration/helpers/utils/sendBeacon.js diff --git a/packages/browser/test/integration/helpers/alloy/setupBaseCode.js b/packages/browser/test/integration/helpers/alloy/setupBaseCode.js index bd041474d..b3ecea356 100644 --- a/packages/browser/test/integration/helpers/alloy/setupBaseCode.js +++ b/packages/browser/test/integration/helpers/alloy/setupBaseCode.js @@ -11,6 +11,7 @@ governing permissions and limitations under the License. // eslint-disable-next-line import/no-unresolved import { server } from "vitest/browser"; +import { installSendBeaconRecorder } from "../utils/sendBeacon.js"; const { readFile } = server.commands; @@ -19,6 +20,8 @@ export default async () => { `${server.config.root}/packages/browser/distTest/baseCode.min.js`, ); + installSendBeaconRecorder(); + document.body.innerHTML = "Alloy Test Page"; // Monkeypatch document.addEventListener once per page lifetime to track click listeners. diff --git a/packages/browser/test/integration/helpers/mswjs/handlers.js b/packages/browser/test/integration/helpers/mswjs/handlers.js index 6c559cfd6..7f9b0e30c 100644 --- a/packages/browser/test/integration/helpers/mswjs/handlers.js +++ b/packages/browser/test/integration/helpers/mswjs/handlers.js @@ -58,6 +58,28 @@ export const sendEventWithIdentityHandler = http.post( }, ); +export const sendEventWithIdentityCookieHandler = http.post( + /https:\/\/edge.adobedc.net\/ee\/.*\/?v1\/interact/, + + async (req) => { + const url = new URL(req.request.url); + const configId = url.searchParams.get("configId"); + + if ( + configId && + configId.startsWith("bc1a10e0-aee4-4e0e-ac5b-cdbb9abbec83") + ) { + return HttpResponse.text( + await readFile( + `${server.config.root}/packages/browser/test/integration/helpers/mocks/sendEventWithIdentityCookieResponse.json`, + ), + ); + } + + throw new Error("Handler not configured properly"); + }, +); + export const sendEventErrorHandler = http.post( /https:\/\/edge.adobedc.net\/ee\/.*\/?v1\/interact/, diff --git a/packages/browser/test/integration/helpers/utils/sendBeacon.js b/packages/browser/test/integration/helpers/utils/sendBeacon.js new file mode 100644 index 000000000..96ea56472 --- /dev/null +++ b/packages/browser/test/integration/helpers/utils/sendBeacon.js @@ -0,0 +1,39 @@ +/* +Copyright 2026 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. +*/ + +// Alloy binds navigator.sendBeacon once at instance creation (see +// createBrowserNetworkService), and MSW's service worker does not surface +// sendBeacon for assertion. So the only way to observe collect-vs-interact +// routing is to record sendBeacon calls in the page — the same approach the +// functional suite took (sendBeaconMock.js). The patch must be installed +// before the library script loads, so it lives in setupBaseCode alongside the +// permanent addEventListener patch: installed once per page lifetime, with the +// captured calls reset per test. Returning true keeps the suite hermetic (no +// stray real beacon) and tells alloy the beacon succeeded so it doesn't fall +// back to fetch. + +export const installSendBeaconRecorder = () => { + if (!window.__alloySendBeaconInstalled) { + window.__alloySendBeaconInstalled = true; + window.navigator.sendBeacon = (url, data) => { + window.__alloySendBeaconCalls.push({ url, data }); + return true; + }; + } + window.__alloySendBeaconCalls = []; +}; + +export const sendBeaconCalls = () => window.__alloySendBeaconCalls ?? []; + +export const resetSendBeaconCalls = () => { + window.__alloySendBeaconCalls = []; +}; diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 83311484a..c67188f0d 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -19,6 +19,7 @@ import { } from "../../helpers/testsSetup/extend.js"; import { sendEventHandler, + sendEventWithIdentityCookieHandler, setConsentHandler, } from "../../helpers/mswjs/handlers.js"; import alloyConfig from "../../helpers/alloy/config.js"; @@ -29,6 +30,10 @@ import { clickLink, cleanupDom, } from "../../helpers/utils/domHelpers.js"; +import { + sendBeaconCalls, + resetSendBeaconCalls, +} from "../../helpers/utils/sendBeacon.js"; // Fixed wait for negative assertions ("no request fired"). There is no positive // signal to poll on, so a fixed delay is required. @@ -427,10 +432,40 @@ describe("C225010 - Click collection handles consent declined gracefully", () => }); }); -// Skipped: the /collect endpoint uses sendBeacon, which is not intercepted by -// MSW in browser mode. This test was a baseline failure in the functional suite -// ("Collect endpoint" failures). See FUNCTIONAL_MIGRATION_PLAN.md §1. -test.skip("C455258 - sendEvent sends to collect endpoint when documentUnloading=true after identity established", () => {}); +describe("C455258 - sendEvent routes to collect via sendBeacon once identity is established", () => { + test("documentUnloading uses interact before identity, collect after, and interact when not unloading", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventWithIdentityCookieHandler); + + await alloy("configure", alloyConfig); + + // No identity established yet: even with documentUnloading, the request + // goes to interact (fetch) so the response can establish an identity. + await alloy("sendEvent", { documentUnloading: true }); + expect(interactCalls(networkRecorder).length).toBe(1); + expect(sendBeaconCalls().length).toBe(0); + + networkRecorder.reset(); + resetSendBeaconCalls(); + + // Identity is now established: documentUnloading routes to collect via + // sendBeacon, not interact. + await alloy("sendEvent", { documentUnloading: true }); + expect(sendBeaconCalls().length).toBe(1); + expect(interactCalls(networkRecorder).length).toBe(0); + + networkRecorder.reset(); + resetSendBeaconCalls(); + + // Without documentUnloading the request goes to interact regardless. + await alloy("sendEvent"); + expect(interactCalls(networkRecorder).length).toBe(1); + expect(sendBeaconCalls().length).toBe(0); + }); +}); // Skipped: tests that assert collect-vs-interact routing depend on sendBeacon // interception via MSW, which is not reliably supported in browser mode, and From 3efde597fc214887584a1a9df31fdd1693ba78aa Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:59:23 -0600 Subject: [PATCH 15/38] docs(integration): record why C9369211 Referer test stays skipped Verified empirically that networkRecorder never captures the Referer header: it is a browser-set forbidden header added after MSW's service worker sees the request. Replaces the speculative skip rationale with the confirmed reason. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../specs/Data Collector/dataCollector.spec.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index c67188f0d..16eb7e42f 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -473,11 +473,12 @@ describe("C455258 - sendEvent routes to collect via sendBeacon once identity is // See FUNCTIONAL_MIGRATION_PLAN.md §1. test.skip("C8118 - link click routes to interact (no identity) then collect (identity established)", () => {}); -// Skipped: the collect-endpoint portion of this test uses sendBeacon, which -// MSW cannot intercept in browser mode. The interact portion relies on -// inspecting request headers that MSW/networkRecorder may not surface -// consistently. This was a baseline failure in the functional suite. -// See FUNCTIONAL_MIGRATION_PLAN.md §1. +// Skipped: this test asserts the Referer header on the interact request, but +// Referer is a browser-set forbidden header added by the network layer after +// MSW's service worker sees the request, so networkRecorder never captures it +// (verified: request.headers.referer is undefined). The collect-side referer +// assertion was already a commented-out TODO in the functional source. Without +// the Referer header there is nothing this test covers beyond C2592. test.skip("C9369211 - sendEvent includes a Referer header on interact and collect requests", () => {}); // Skipped: all sub-tests in the source functional file are already marked From 049e0870e32a57e1a32011a59b9e22512d5d64c9 Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:07:52 -0600 Subject: [PATCH 16/38] docs(integration): tighten comments in dataCollector spec Removes comments that duplicated the NO_REQUEST_WAIT_MS constant's rationale or restated their assertion, condenses the verbose ones, and corrects the C8118 skip note (collect routing is now feasible via the sendBeacon recorder, not blocked as the stale comment claimed). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Data Collector/dataCollector.spec.js | 59 +++++++------------ 1 file changed, 20 insertions(+), 39 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 16eb7e42f..8ab86fb63 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -35,12 +35,10 @@ import { resetSendBeaconCalls, } from "../../helpers/utils/sendBeacon.js"; -// Fixed wait for negative assertions ("no request fired"). There is no positive -// signal to poll on, so a fixed delay is required. +// A request that must NOT fire has no signal to poll on, so a fixed wait is required. const NO_REQUEST_WAIT_MS = 200; -// Synchronous filter — intentionally NOT findCalls(), which retries/waits and -// would give false negatives on assertions that no request was fired. +// Synchronous, unlike findCalls() which retries/waits — wrong for "no request fired". const interactCalls = (networkRecorder) => networkRecorder.calls.filter((c) => /v1\/interact/.test(c.request?.url ?? ""), @@ -185,8 +183,6 @@ describe("C8119 - Click collection disabled does not send link click events", () const link = appendLink({ id: "alloy-link-test", href: "#blank", text: "Test Link" }); clickLink(link); - // Fixed wait is necessary: asserting that NO interact call fires after the - // click, so there is no positive observable condition to poll on. await new Promise((resolve) => setTimeout(resolve, NO_REQUEST_WAIT_MS)); expect(interactCalls(networkRecorder).length).toBe(0); @@ -243,10 +239,8 @@ describe("C81184 - Click collection configuration warnings", () => { test("does not warn about disabled click collection when clickCollectionEnabled is true with downloadLinkQualifier", async ({ alloy, }) => { - // Note: onBeforeLinkClickSend is deprecated and always logs a deprecation - // warning, regardless of clickCollectionEnabled. This test uses - // clickCollection.filterClickDetails (the non-deprecated alternative) - // to verify that no "will be ignored" warning fires when clickCollectionEnabled is true. + // onBeforeLinkClickSend always logs a deprecation warning, so use + // filterClickDetails to isolate the "will be ignored" warning under test. await alloy("configure", { ...alloyConfig, debugEnabled: true, @@ -257,8 +251,6 @@ describe("C81184 - Click collection configuration warnings", () => { downloadLinkQualifier: "\\.pdf$", }); - // The "will be ignored because clickCollectionEnabled is false" warning - // should NOT fire when clickCollectionEnabled is true expect( searchForLogMessage( consoleSpy, @@ -285,8 +277,6 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { const link = appendLink({ id: "alloy-link-test", href: "#valid", text: "Test Link" }); clickLink(link); - // Fixed wait is necessary: asserting that NO interact call fires after the - // click, so there is no positive observable condition to poll on. await new Promise((resolve) => setTimeout(resolve, NO_REQUEST_WAIT_MS)); expect(interactCalls(networkRecorder).length).toBe(0); @@ -310,8 +300,6 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { const link = appendLink({ id: "alloy-link-test", href: "#valid", text: "Test Link" }); clickLink(link); - // Fixed wait is necessary: asserting that NO interact call fires after the - // click, so there is no positive observable condition to poll on. await new Promise((resolve) => setTimeout(resolve, NO_REQUEST_WAIT_MS)); expect(interactCalls(networkRecorder).length).toBe(0); @@ -350,9 +338,8 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { expect(event.data.customField).toBe("test123"); }); - // These five tests from the original C81181 functional file were not included - // in the initial migration. None depend on sendBeacon/collect-endpoint - // interception, so they are deferred scope rather than blocked. + // Five C81181 functional tests are interact link-XDM variations, deferred as + // scope — not blocked (no sendBeacon dependency). test.skip("C81181 - link click is collected when clickCollectionEnabled and no callback defined", () => {}); test.skip("C81181 - onBeforeLinkClickSend cancels a request conditionally based on clickedElement.id", () => {}); test.skip("C81181 - filterClickDetails cancels a request conditionally based on clickedElement.id", () => {}); @@ -442,8 +429,8 @@ describe("C455258 - sendEvent routes to collect via sendBeacon once identity is await alloy("configure", alloyConfig); - // No identity established yet: even with documentUnloading, the request - // goes to interact (fetch) so the response can establish an identity. + // Before identity, documentUnloading still uses interact so the response + // can establish one. await alloy("sendEvent", { documentUnloading: true }); expect(interactCalls(networkRecorder).length).toBe(1); expect(sendBeaconCalls().length).toBe(0); @@ -451,8 +438,7 @@ describe("C455258 - sendEvent routes to collect via sendBeacon once identity is networkRecorder.reset(); resetSendBeaconCalls(); - // Identity is now established: documentUnloading routes to collect via - // sendBeacon, not interact. + // With identity established, documentUnloading routes to collect (sendBeacon). await alloy("sendEvent", { documentUnloading: true }); expect(sendBeaconCalls().length).toBe(1); expect(interactCalls(networkRecorder).length).toBe(0); @@ -460,30 +446,25 @@ describe("C455258 - sendEvent routes to collect via sendBeacon once identity is networkRecorder.reset(); resetSendBeaconCalls(); - // Without documentUnloading the request goes to interact regardless. + // Without documentUnloading it always uses interact. await alloy("sendEvent"); expect(interactCalls(networkRecorder).length).toBe(1); expect(sendBeaconCalls().length).toBe(0); }); }); -// Skipped: tests that assert collect-vs-interact routing depend on sendBeacon -// interception via MSW, which is not reliably supported in browser mode, and -// this test was a baseline failure in the functional suite. -// See FUNCTIONAL_MIGRATION_PLAN.md §1. +// Skipped: 15 functional sub-tests. The one collect-routing case is now doable +// via the sendBeacon recorder (see C455258); the other 14 are interact link-XDM +// variations deferred as scope, like the C81181 five. Not yet migrated. test.skip("C8118 - link click routes to interact (no identity) then collect (identity established)", () => {}); -// Skipped: this test asserts the Referer header on the interact request, but -// Referer is a browser-set forbidden header added by the network layer after -// MSW's service worker sees the request, so networkRecorder never captures it -// (verified: request.headers.referer is undefined). The collect-side referer -// assertion was already a commented-out TODO in the functional source. Without -// the Referer header there is nothing this test covers beyond C2592. +// Skipped: asserts the Referer header, but it's a browser-set forbidden header +// added after MSW's service worker sees the request, so networkRecorder can't +// capture it (verified). Nothing else here isn't already covered by C2592. test.skip("C9369211 - sendEvent includes a Referer header on interact and collect requests", () => {}); -// Skipped: all sub-tests in the source functional file are already marked -// test.skip because they require a specific personalization response from the -// live edge that is difficult to reproduce deterministically with MSW mocks. +// Skipped: every sub-test is test.skip in the functional source too — they need +// a specific live-edge personalization response that's hard to mock deterministically. test.skip("C81182 - onBeforeLinkClickSend interacts with personalization metric on link (source tests skipped)", () => {}); describe("C81183 - getLinkDetails monitoring hook via __alloyMonitors", () => { @@ -558,8 +539,8 @@ describe("C81183 - getLinkDetails monitoring hook via __alloyMonitors", () => { clickCollectionEnabled: true, }); - // Mirrors original C81183 test 2: passing null (a non-existent element) - // should return an object with no meaningful link data, not throw. + // An absent/non-clickable element yields a details object with undefined + // fields, not a throw or falsy — mirrors original C81183 test 2. const result = window.___getLinkDetails(null); expect(result.linkName).toBeUndefined(); expect(result.linkType).toBeUndefined(); From 111c2d12f1edba3733c63324a31dc648c8b62999 Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:27:12 -0600 Subject: [PATCH 17/38] test(integration): restore C2592 configOverrides assertion The functional original sent a datasetId and asserted meta.configOverrides, but its `.event` key was undefined so the check was a no-op. Restores the meaningful assertion: the datasetId propagates to meta.configOverrides.com_adobe_experience_platform.datasets.event.datasetId. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../specs/Data Collector/dataCollector.spec.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 8ab86fb63..e6d5121e1 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -57,8 +57,10 @@ describe("C2592 - Event command sends a request", () => { }) => { worker.use(sendEventHandler); + const datasetId = "6335faf30f5a161c0b4b1444"; + await alloy("configure", alloyConfig); - await alloy("sendEvent"); + await alloy("sendEvent", { datasetId }); const call = await networkRecorder.findCall(/edge\.adobedc\.net/); expect(call).toBeDefined(); @@ -69,6 +71,10 @@ describe("C2592 - Event command sends a request", () => { expect(body.events[0].xdm.implementationDetails.name).toBe( "https://ns.adobe.com/experience/alloy", ); + expect( + body.meta.configOverrides.com_adobe_experience_platform.datasets.event + .datasetId, + ).toBe(datasetId); expect(body.meta.state.cookiesEnabled).toBe(true); // domain is "" on localhost, so only check the type expect(typeof body.meta.state.domain).toBe("string"); From 5f5933bd5042bd71fcf8628bf577e41b16f41436 Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:31:00 -0600 Subject: [PATCH 18/38] test(integration): assert full webInteraction in C11693274 Match the functional original, which pinned the entire webInteraction object; the migration had dropped name, region, and linkClicks. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../specs/Data Collector/dataCollector.spec.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index e6d5121e1..476f14ab9 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -380,8 +380,13 @@ describe("C11693274 - URL query params do not affect exit link classification", const eventXdm = call.request.body.events[0].xdm; expect(eventXdm.eventType).toBe("web.webinteraction.linkClicks"); - expect(eventXdm.web.webInteraction.type).toBe("exit"); - expect(eventXdm.web.webInteraction.URL).toBe(externalUrl); + expect(eventXdm.web.webInteraction).toEqual({ + name: "Test Link", + region: "BODY", + type: "exit", + URL: externalUrl, + linkClicks: { value: 1 }, + }); }); }); From 20198567ea68f39343ac779150f2fcdfaa82fa3c Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:31:51 -0600 Subject: [PATCH 19/38] test(integration): assert full webInteraction in C81181 augment test The functional original pinned the whole webInteraction object; the migration checked only the augmented name. Adds region/type/linkClicks and a loose URL origin check (the URL resolves against the localhost test page). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../specs/Data Collector/dataCollector.spec.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 476f14ab9..19b86747b 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -340,7 +340,13 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { expect(call).toBeDefined(); const event = call.request.body.events[0]; - expect(event.xdm.web.webInteraction.name).toBe("Augmented name"); + const { webInteraction } = event.xdm.web; + expect(webInteraction.name).toBe("Augmented name"); + expect(webInteraction.region).toBe("BODY"); + expect(webInteraction.type).toBe("other"); + expect(webInteraction.linkClicks).toEqual({ value: 1 }); + // URL resolves against the localhost test page, so only check the origin. + expect(webInteraction.URL).toContain(window.location.origin); expect(event.data.customField).toBe("test123"); }); From e44d43db8e82b62a412cf5434e81f94cc62c55c6 Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:44:03 -0600 Subject: [PATCH 20/38] docs(integration): sharpen C9369211 skip rationale Note that the collect-side referer was already a TODO in the functional source and that collect routing is covered by C455258, rather than implying C2592 covers everything. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../integration/specs/Data Collector/dataCollector.spec.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 19b86747b..c1fb01dea 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -477,7 +477,8 @@ test.skip("C8118 - link click routes to interact (no identity) then collect (ide // Skipped: asserts the Referer header, but it's a browser-set forbidden header // added after MSW's service worker sees the request, so networkRecorder can't -// capture it (verified). Nothing else here isn't already covered by C2592. +// capture it (verified). The collect-side referer was already a commented-out +// TODO in the functional source, and collect routing is covered by C455258. test.skip("C9369211 - sendEvent includes a Referer header on interact and collect requests", () => {}); // Skipped: every sub-test is test.skip in the functional source too — they need From a25269218ba562b54c2ffb90e41f92f7fe37a7c5 Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:46:06 -0600 Subject: [PATCH 21/38] refactor(integration): centralize test lifecycle in the alloy fixture Moves DOM cleanup and the sendBeacon-recorder reset out of the spec and into the shared alloy fixture, and the ___getLinkDetails teardown into cleanAlloy (next to __alloyMonitors). installSendBeaconRecorder now only installs; the fixture owns the per-test reset. Removes the unusual top-level afterEach from the spec and documents why the recorder install lives in setupBaseCode. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../browser/test/integration/helpers/alloy/clean.js | 1 + .../test/integration/helpers/alloy/setupBaseCode.js | 3 +++ .../test/integration/helpers/testsSetup/extend.js | 4 ++++ .../test/integration/helpers/utils/sendBeacon.js | 13 +++++++------ .../specs/Data Collector/dataCollector.spec.js | 12 +----------- 5 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/browser/test/integration/helpers/alloy/clean.js b/packages/browser/test/integration/helpers/alloy/clean.js index bcfb1ab8f..c30f16145 100644 --- a/packages/browser/test/integration/helpers/alloy/clean.js +++ b/packages/browser/test/integration/helpers/alloy/clean.js @@ -17,6 +17,7 @@ export default () => { window.__alloyClickListeners = []; } delete window.__alloyMonitors; + delete window.___getLinkDetails; delete window.__alloyNS; delete window.alloy; }; diff --git a/packages/browser/test/integration/helpers/alloy/setupBaseCode.js b/packages/browser/test/integration/helpers/alloy/setupBaseCode.js index b3ecea356..1add8176d 100644 --- a/packages/browser/test/integration/helpers/alloy/setupBaseCode.js +++ b/packages/browser/test/integration/helpers/alloy/setupBaseCode.js @@ -20,6 +20,9 @@ export default async () => { `${server.config.root}/packages/browser/distTest/baseCode.min.js`, ); + // Must run before the alloy script is injected below: createBrowserNetworkService + // binds navigator.sendBeacon once at instance creation, so the recorder has to + // already be in place. Per-test reset lives in the extend.js alloy fixture. installSendBeaconRecorder(); document.body.innerHTML = "Alloy Test Page"; diff --git a/packages/browser/test/integration/helpers/testsSetup/extend.js b/packages/browser/test/integration/helpers/testsSetup/extend.js index 64c4c11f6..d394806f9 100644 --- a/packages/browser/test/integration/helpers/testsSetup/extend.js +++ b/packages/browser/test/integration/helpers/testsSetup/extend.js @@ -17,6 +17,8 @@ import { networkRecorder } from "../mswjs/networkRecorder.js"; import setupAlloy from "../alloy/setup.js"; import setupBaseCode from "../alloy/setupBaseCode.js"; import cleanAlloy from "../alloy/clean.js"; +import { cleanupDom } from "../utils/domHelpers.js"; +import { resetSendBeaconCalls } from "../utils/sendBeacon.js"; const worker = createWorker(); @@ -63,11 +65,13 @@ export const test = baseTest.extend({ await setupBaseCode(); const alloy = await setupAlloy(); + resetSendBeaconCalls(); // Make alloy available in the test context await use(alloy); cleanAlloy(); + cleanupDom(); }, { auto: true }, // Apply to all tests even if not explicitly using alloy ], diff --git a/packages/browser/test/integration/helpers/utils/sendBeacon.js b/packages/browser/test/integration/helpers/utils/sendBeacon.js index 96ea56472..a26c1dd19 100644 --- a/packages/browser/test/integration/helpers/utils/sendBeacon.js +++ b/packages/browser/test/integration/helpers/utils/sendBeacon.js @@ -22,14 +22,15 @@ governing permissions and limitations under the License. // back to fetch. export const installSendBeaconRecorder = () => { - if (!window.__alloySendBeaconInstalled) { - window.__alloySendBeaconInstalled = true; - window.navigator.sendBeacon = (url, data) => { - window.__alloySendBeaconCalls.push({ url, data }); - return true; - }; + if (window.__alloySendBeaconInstalled) { + return; } + window.__alloySendBeaconInstalled = true; window.__alloySendBeaconCalls = []; + window.navigator.sendBeacon = (url, data) => { + window.__alloySendBeaconCalls.push({ url, data }); + return true; + }; }; export const sendBeaconCalls = () => window.__alloySendBeaconCalls ?? []; diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index c1fb01dea..9f2669ed3 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -15,7 +15,6 @@ import { describe, vi, beforeEach, - afterEach, } from "../../helpers/testsSetup/extend.js"; import { sendEventHandler, @@ -25,11 +24,7 @@ import { import alloyConfig from "../../helpers/alloy/config.js"; import searchForLogMessage from "../../helpers/utils/searchForLogMessage.js"; import { CONSENT_OUT } from "../../helpers/constants/consent.js"; -import { - appendLink, - clickLink, - cleanupDom, -} from "../../helpers/utils/domHelpers.js"; +import { appendLink, clickLink } from "../../helpers/utils/domHelpers.js"; import { sendBeaconCalls, resetSendBeaconCalls, @@ -44,11 +39,6 @@ const interactCalls = (networkRecorder) => /v1\/interact/.test(c.request?.url ?? ""), ); -afterEach(() => { - cleanupDom(); - delete window.___getLinkDetails; -}); - describe("C2592 - Event command sends a request", () => { test("sendEvent produces an edge interact call with implementationDetails and state", async ({ alloy, From 4f4af2a2f9d23be1281abcc0b27bcd35a56ff680 Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:47:15 -0600 Subject: [PATCH 22/38] refactor(integration): hoist C81183 monitor setup into beforeEach The __alloyMonitors install was copy-pasted in all three C81183 tests. A beforeEach installs it once; cleanAlloy already clears it between tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Data Collector/dataCollector.spec.js | 34 +++++++------------ 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 9f2669ed3..070bf33e6 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -476,17 +476,21 @@ test.skip("C9369211 - sendEvent includes a Referer header on interact and collec test.skip("C81182 - onBeforeLinkClickSend interacts with personalization metric on link (source tests skipped)", () => {}); describe("C81183 - getLinkDetails monitoring hook via __alloyMonitors", () => { + // Install the monitor before configure so onInstanceConfigured fires and + // exposes getLinkDetails. + beforeEach(() => { + window.__alloyMonitors = [ + { + onInstanceConfigured(data) { + window.___getLinkDetails = data.getLinkDetails; + }, + }, + ]; + }); + test("getLinkDetails returns correct link info for a visible link element", async ({ alloy, }) => { - // Install monitor BEFORE configure so onInstanceConfigured fires - window.__alloyMonitors = window.__alloyMonitors || []; - window.__alloyMonitors.push({ - onInstanceConfigured(data) { - window.___getLinkDetails = data.getLinkDetails; - }, - }); - await alloy("configure", { ...alloyConfig, clickCollectionEnabled: true, @@ -508,13 +512,6 @@ describe("C81183 - getLinkDetails monitoring hook via __alloyMonitors", () => { test("getLinkDetails returns results even when clickCollectionEnabled is false", async ({ alloy, }) => { - window.__alloyMonitors = window.__alloyMonitors || []; - window.__alloyMonitors.push({ - onInstanceConfigured(data) { - window.___getLinkDetails = data.getLinkDetails; - }, - }); - await alloy("configure", { ...alloyConfig, clickCollectionEnabled: false, @@ -535,13 +532,6 @@ describe("C81183 - getLinkDetails monitoring hook via __alloyMonitors", () => { test("getLinkDetails returns no link data for a null element", async ({ alloy, }) => { - window.__alloyMonitors = window.__alloyMonitors || []; - window.__alloyMonitors.push({ - onInstanceConfigured(data) { - window.___getLinkDetails = data.getLinkDetails; - }, - }); - await alloy("configure", { ...alloyConfig, clickCollectionEnabled: true, From 7802a88547644b14b13495b83e41702fbb1de505 Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:49:49 -0600 Subject: [PATCH 23/38] refactor(integration): tidy skip stubs and reuse waitFor Groups the three loose top-level test.skip stubs into a "Not migrated" describe so they read as intentional, and replaces the inline setTimeout promises with the existing waitFor util. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Data Collector/dataCollector.spec.js | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 070bf33e6..335f3011b 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -29,6 +29,7 @@ import { sendBeaconCalls, resetSendBeaconCalls, } from "../../helpers/utils/sendBeacon.js"; +import waitFor from "../../helpers/utils/waitFor.js"; // A request that must NOT fire has no signal to poll on, so a fixed wait is required. const NO_REQUEST_WAIT_MS = 200; @@ -179,7 +180,7 @@ describe("C8119 - Click collection disabled does not send link click events", () const link = appendLink({ id: "alloy-link-test", href: "#blank", text: "Test Link" }); clickLink(link); - await new Promise((resolve) => setTimeout(resolve, NO_REQUEST_WAIT_MS)); + await waitFor(NO_REQUEST_WAIT_MS); expect(interactCalls(networkRecorder).length).toBe(0); }); @@ -273,7 +274,7 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { const link = appendLink({ id: "alloy-link-test", href: "#valid", text: "Test Link" }); clickLink(link); - await new Promise((resolve) => setTimeout(resolve, NO_REQUEST_WAIT_MS)); + await waitFor(NO_REQUEST_WAIT_MS); expect(interactCalls(networkRecorder).length).toBe(0); }); @@ -296,7 +297,7 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { const link = appendLink({ id: "alloy-link-test", href: "#valid", text: "Test Link" }); clickLink(link); - await new Promise((resolve) => setTimeout(resolve, NO_REQUEST_WAIT_MS)); + await waitFor(NO_REQUEST_WAIT_MS); expect(interactCalls(networkRecorder).length).toBe(0); }); @@ -460,20 +461,22 @@ describe("C455258 - sendEvent routes to collect via sendBeacon once identity is }); }); -// Skipped: 15 functional sub-tests. The one collect-routing case is now doable -// via the sendBeacon recorder (see C455258); the other 14 are interact link-XDM -// variations deferred as scope, like the C81181 five. Not yet migrated. -test.skip("C8118 - link click routes to interact (no identity) then collect (identity established)", () => {}); - -// Skipped: asserts the Referer header, but it's a browser-set forbidden header -// added after MSW's service worker sees the request, so networkRecorder can't -// capture it (verified). The collect-side referer was already a commented-out -// TODO in the functional source, and collect routing is covered by C455258. -test.skip("C9369211 - sendEvent includes a Referer header on interact and collect requests", () => {}); - -// Skipped: every sub-test is test.skip in the functional source too — they need -// a specific live-edge personalization response that's hard to mock deterministically. -test.skip("C81182 - onBeforeLinkClickSend interacts with personalization metric on link (source tests skipped)", () => {}); +describe("Not migrated (see per-test rationale)", () => { + // 15 functional sub-tests. The one collect-routing case is now doable via the + // sendBeacon recorder (see C455258); the other 14 are interact link-XDM + // variations deferred as scope, like the C81181 five. + test.skip("C8118 - link click routes to interact (no identity) then collect (identity established)", () => {}); + + // Asserts the Referer header, but it's a browser-set forbidden header added + // after MSW's service worker sees the request, so networkRecorder can't + // capture it (verified). The collect-side referer was already a commented-out + // TODO in the functional source, and collect routing is covered by C455258. + test.skip("C9369211 - sendEvent includes a Referer header on interact and collect requests", () => {}); + + // Every sub-test is test.skip in the functional source too — they need a + // specific live-edge personalization response that's hard to mock deterministically. + test.skip("C81182 - onBeforeLinkClickSend interacts with personalization metric on link (source tests skipped)", () => {}); +}); describe("C81183 - getLinkDetails monitoring hook via __alloyMonitors", () => { // Install the monitor before configure so onInstanceConfigured fires and From f8d378717770bcea185ffd80dbf07e71e2cf361f Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:51:46 -0600 Subject: [PATCH 24/38] format --- .../Data Collector/dataCollector.spec.js | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 335f3011b..9a2149350 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -177,7 +177,11 @@ describe("C8119 - Click collection disabled does not send link click events", () clickCollectionEnabled: false, }); - const link = appendLink({ id: "alloy-link-test", href: "#blank", text: "Test Link" }); + const link = appendLink({ + id: "alloy-link-test", + href: "#blank", + text: "Test Link", + }); clickLink(link); await waitFor(NO_REQUEST_WAIT_MS); @@ -271,7 +275,11 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { onBeforeLinkClickSend: () => false, }); - const link = appendLink({ id: "alloy-link-test", href: "#valid", text: "Test Link" }); + const link = appendLink({ + id: "alloy-link-test", + href: "#valid", + text: "Test Link", + }); clickLink(link); await waitFor(NO_REQUEST_WAIT_MS); @@ -294,7 +302,11 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { }, }); - const link = appendLink({ id: "alloy-link-test", href: "#valid", text: "Test Link" }); + const link = appendLink({ + id: "alloy-link-test", + href: "#valid", + text: "Test Link", + }); clickLink(link); await waitFor(NO_REQUEST_WAIT_MS); @@ -324,7 +336,11 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { }, }); - const link = appendLink({ id: "alloy-link-test", href: "#internal", text: "Test Link" }); + const link = appendLink({ + id: "alloy-link-test", + href: "#internal", + text: "Test Link", + }); clickLink(link); const call = await networkRecorder.findCall(/v1\/interact/); @@ -369,7 +385,11 @@ describe("C11693274 - URL query params do not affect exit link classification", // href contains current domain only in the query string (not the host) const externalUrl = `https://example.com/?exclude-this=${window.location.hostname}`; - const link = appendLink({ id: "alloy-link-test", href: externalUrl, text: "Test Link" }); + const link = appendLink({ + id: "alloy-link-test", + href: externalUrl, + text: "Test Link", + }); clickLink(link); const call = await networkRecorder.findCall(/v1\/interact/); @@ -414,7 +434,11 @@ describe("C225010 - Click collection handles consent declined gracefully", () => await alloy("setConsent", CONSENT_OUT); - const link = appendLink({ id: "alloy-link-test", href: "#foo", text: "Test Link" }); + const link = appendLink({ + id: "alloy-link-test", + href: "#foo", + text: "Test Link", + }); clickLink(link); await expect From e51bbe7ecf110a13f866d1d34b332189d8a01b09 Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:27:17 -0600 Subject: [PATCH 25/38] test(integration): verify C455258 beacon URL and body The migration only counted one sendBeacon call. Restore the original's intent by asserting the beacon hit /v1/collect and carries a valid events payload. Co-Authored-By: Claude Opus 4.8 --- .../specs/Data Collector/dataCollector.spec.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 9a2149350..ad2421c28 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -472,9 +472,15 @@ describe("C455258 - sendEvent routes to collect via sendBeacon once identity is // With identity established, documentUnloading routes to collect (sendBeacon). await alloy("sendEvent", { documentUnloading: true }); - expect(sendBeaconCalls().length).toBe(1); + const beacons = sendBeaconCalls(); + expect(beacons.length).toBe(1); expect(interactCalls(networkRecorder).length).toBe(0); + expect(beacons[0].url).toMatch(/\/v1\/collect\?configId=/); + const beaconBody = JSON.parse(await beacons[0].data.text()); + expect(Array.isArray(beaconBody.events)).toBe(true); + expect(beaconBody.events.length).toBeGreaterThanOrEqual(1); + networkRecorder.reset(); resetSendBeaconCalls(); From 0e57080c6f01f070404509d570400e9794e9d5b9 Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:28:15 -0600 Subject: [PATCH 26/38] test(integration): pin interactCalls to full edge host and path The loose /v1\/interact/ match would count any URL containing that substring. Anchor it to the edge host and the configId query so a wrong host or malformed path is not mistaken for a valid interact call. Co-Authored-By: Claude Opus 4.8 --- .../specs/Data Collector/dataCollector.spec.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index ad2421c28..625b0fcbc 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -34,10 +34,14 @@ import waitFor from "../../helpers/utils/waitFor.js"; // A request that must NOT fire has no signal to poll on, so a fixed wait is required. const NO_REQUEST_WAIT_MS = 200; -// Synchronous, unlike findCalls() which retries/waits — wrong for "no request fired". +// Pin the full edge host and path so a wrong host or malformed path is not +// counted as an interact call. Synchronous, unlike findCalls() which +// retries/waits — wrong for "no request fired". +const EDGE_INTERACT = + /^https:\/\/edge\.adobedc\.net\/ee\/(?:[^?]*\/)?v1\/interact\?configId=/; const interactCalls = (networkRecorder) => networkRecorder.calls.filter((c) => - /v1\/interact/.test(c.request?.url ?? ""), + EDGE_INTERACT.test(c.request?.url ?? ""), ); describe("C2592 - Event command sends a request", () => { From f42ab13344850af6513f779a6d619af41529f3d0 Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:29:00 -0600 Subject: [PATCH 27/38] test(integration): extend domHelpers for verbatim link ports Add appendHtmlToBody so anchor structures from the functional originals (spans, download/data-* attributes, custom-region wrappers) can be built as-is, and a preventNavigation option on clickLink so a test can let real hash navigation proceed. Default click behavior is unchanged. Co-Authored-By: Claude Opus 4.8 --- .../integration/helpers/utils/domHelpers.js | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/browser/test/integration/helpers/utils/domHelpers.js b/packages/browser/test/integration/helpers/utils/domHelpers.js index 1f1964047..20049f0ba 100644 --- a/packages/browser/test/integration/helpers/utils/domHelpers.js +++ b/packages/browser/test/integration/helpers/utils/domHelpers.js @@ -27,10 +27,31 @@ export const appendLink = ({ id, href, text }) => { }; /** - * Simulates a non-navigating click on a link element. + * Appends arbitrary HTML to document.body and registers the inserted top-level + * elements for cleanup. Mirrors the functional suite's addHtmlToBody so anchor + * structures (spans, download/data-* attributes, custom-region wrappers) can be + * built verbatim from the original tests. Returns the inserted elements. */ -export const clickLink = (link) => { - link.addEventListener("click", (e) => e.preventDefault()); +export const appendHtmlToBody = (html) => { + const container = document.createElement("div"); + container.innerHTML = html.trim(); + const elements = [...container.children]; + elements.forEach((el) => { + document.body.appendChild(el); + trackedElements.push(el); + }); + return elements; +}; + +/** + * Simulates a click on a link element. By default the click's default action is + * prevented so the test page does not navigate away; pass + * { preventNavigation: false } to let real hash navigation proceed. + */ +export const clickLink = (link, { preventNavigation = true } = {}) => { + if (preventNavigation) { + link.addEventListener("click", (e) => e.preventDefault()); + } link.click(); }; From 7a132774eca378d1f6b6b4aef528831e3297adde Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:29:07 -0600 Subject: [PATCH 28/38] test(integration): assert C225010 click still navigates to #foo The migration clicked via the navigation-preventing clickLink, so a regression that blocked default navigation would have passed. Let the click navigate and assert the hash changed, matching the original's getLocation().contains(#foo). Co-Authored-By: Claude Opus 4.8 --- .../integration/specs/Data Collector/dataCollector.spec.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 625b0fcbc..2e3700091 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -443,7 +443,10 @@ describe("C225010 - Click collection handles consent declined gracefully", () => href: "#foo", text: "Test Link", }); - clickLink(link); + // Let the click navigate so a regression that blocks default navigation + // (the original C225010 concern) would be caught. + clickLink(link, { preventNavigation: false }); + expect(window.location.href).toContain("#foo"); await expect .poll(() => searchForLogMessage(consoleSpy, "The user declined consent")) @@ -452,6 +455,7 @@ describe("C225010 - Click collection handles consent declined gracefully", () => expect(unhandledRejections.length).toBe(0); window.removeEventListener("unhandledrejection", rejectionHandler); + window.location.hash = ""; }); }); From baf0339f33f08957477dfa1b8ba1d5b9b0cbb433 Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:31:21 -0600 Subject: [PATCH 29/38] test(integration): restore exact C81183 getLinkDetails assertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The migration loosened these to truthy/contains and changed the link to external (exit). Restore the original's exact internal-link pinning — linkName, linkRegion, linkType:other, pageIDType, resolved linkUrl and pageName (adapted to the localhost page) — and assert all five fields are undefined for a null element. Co-Authored-By: Claude Opus 4.8 --- .../Data Collector/dataCollector.spec.js | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 2e3700091..88480c953 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -529,28 +529,44 @@ describe("C81183 - getLinkDetails monitoring hook via __alloyMonitors", () => { ]; }); - test("getLinkDetails returns correct link info for a visible link element", async ({ + // The functional original pinned every field for an internal link. URLs are + // adapted to the localhost test page: linkUrl resolves the relative href and + // pageName is the page's own location. + const expectedInternalLinkDetails = () => ({ + linkName: "Test Link", + linkRegion: "BODY", + linkType: "other", + linkUrl: new URL("valid.html", window.location.href).href, + pageName: window.location.href, + pageIDType: 0, + }); + + test("getLinkDetails returns the resolved details even when onBeforeLinkClickSend augments the xdm", async ({ alloy, }) => { await alloy("configure", { ...alloyConfig, clickCollectionEnabled: true, + onBeforeLinkClickSend: (options) => { + options.xdm.web.webInteraction.name = "augmented name"; + options.data.customField = "test123"; + return true; + }, }); const link = appendLink({ id: "alloy-link-test", - href: "https://example.com/valid.html", + href: "valid.html", text: "Test Link", }); const result = window.___getLinkDetails(link); - expect(result).toBeTruthy(); - expect(result.linkName).toBeTruthy(); - expect(result.linkUrl).toContain("example.com"); - expect(result.linkType).toBe("exit"); + // linkName stays "Test Link": the activity-map link (unchanged by the + // callback) takes priority over the augmented webInteraction name. + expect(result).toEqual(expectedInternalLinkDetails()); }); - test("getLinkDetails returns results even when clickCollectionEnabled is false", async ({ + test("getLinkDetails returns the full internal-link details even when clickCollectionEnabled is false", async ({ alloy, }) => { await alloy("configure", { @@ -559,15 +575,13 @@ describe("C81183 - getLinkDetails monitoring hook via __alloyMonitors", () => { }); const link = appendLink({ - id: "alloy-link-test-disabled", - href: "https://example.com/", - text: "External Link", + id: "alloy-link-test", + href: "valid.html", + text: "Test Link", }); const result = window.___getLinkDetails(link); - expect(result).toBeTruthy(); - expect(result.linkName).toBeTruthy(); - expect(result.linkType).toBe("exit"); + expect(result).toEqual(expectedInternalLinkDetails()); }); test("getLinkDetails returns no link data for a null element", async ({ @@ -582,7 +596,9 @@ describe("C81183 - getLinkDetails monitoring hook via __alloyMonitors", () => { // fields, not a throw or falsy — mirrors original C81183 test 2. const result = window.___getLinkDetails(null); expect(result.linkName).toBeUndefined(); + expect(result.linkRegion).toBeUndefined(); expect(result.linkType).toBeUndefined(); expect(result.linkUrl).toBeUndefined(); + expect(result.pageName).toBeUndefined(); }); }); From 50031bff085021660e2079cee7f8b22b789146cd Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:33:43 -0600 Subject: [PATCH 30/38] test(integration): assert eventType and activity map in C81181 augment The migration only checked the augmented webInteraction name plus customField. Restore the original's full coverage: eventType, the complete webInteraction object, and the activity-map data (whose link stays "Test Link" since the callback augmented only the xdm). Adds reusable link-assertion helpers. Co-Authored-By: Claude Opus 4.8 --- .../Data Collector/dataCollector.spec.js | 48 +++++++++++++++---- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 88480c953..16c8d1a2e 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -44,6 +44,32 @@ const interactCalls = (networkRecorder) => EDGE_INTERACT.test(c.request?.url ?? ""), ); +const firstEvent = (call) => call.request.body.events[0]; + +// eslint-disable-next-line no-underscore-dangle +const activityMap = (event) => + event.data.__adobe.analytics.contextData.a.activitymap; + +// Functional originals pinned absolute alloyio.com URLs; on localhost the +// relative hrefs resolve against the test page, so derive the expected URL. +const absoluteUrl = (href) => + /^https?:\/\//.test(href) ? href : new URL(href, window.location.href).href; + +const expectedWebInteraction = ({ name, type, href, region = "BODY" }) => ({ + name, + region, + type, + URL: absoluteUrl(href), + linkClicks: { value: 1 }, +}); + +const expectedActivityMap = ({ link, region = "BODY", pageIDType = 0 }) => ({ + page: window.location.href, + link, + region, + pageIDType, +}); + describe("C2592 - Event command sends a request", () => { test("sendEvent produces an edge interact call with implementationDetails and state", async ({ alloy, @@ -342,7 +368,7 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { const link = appendLink({ id: "alloy-link-test", - href: "#internal", + href: "valid.html", text: "Test Link", }); clickLink(link); @@ -350,14 +376,18 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { const call = await networkRecorder.findCall(/v1\/interact/); expect(call).toBeDefined(); - const event = call.request.body.events[0]; - const { webInteraction } = event.xdm.web; - expect(webInteraction.name).toBe("Augmented name"); - expect(webInteraction.region).toBe("BODY"); - expect(webInteraction.type).toBe("other"); - expect(webInteraction.linkClicks).toEqual({ value: 1 }); - // URL resolves against the localhost test page, so only check the origin. - expect(webInteraction.URL).toContain(window.location.origin); + const event = firstEvent(call); + expect(event.xdm.eventType).toBe("web.webinteraction.linkClicks"); + expect(event.xdm.web.webInteraction).toEqual( + expectedWebInteraction({ + name: "Augmented name", + type: "other", + href: "valid.html", + }), + ); + // The activity-map link is unchanged: the callback only augmented the xdm + // webInteraction name, not the data. + expect(activityMap(event)).toEqual(expectedActivityMap({ link: "Test Link" })); expect(event.data.customField).toBe("test123"); }); From f7506a7d86e4dcd0e0066adb1f6f0a681f86b991 Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:35:02 -0600 Subject: [PATCH 31/38] test(integration): implement the 5 dropped C81181 link-click tests These were stubbed as skips; none depends on sendBeacon, so port them: no-callback collection, conditional cancel via onBeforeLinkClickSend and via filterClickDetails, filterClickDetails augmentation, and sessionStorageEnabled:false. Each pins the full webInteraction (and activity map where the original did), adapted to localhost URLs. Co-Authored-By: Claude Opus 4.8 --- .../Data Collector/dataCollector.spec.js | 196 +++++++++++++++++- 1 file changed, 189 insertions(+), 7 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 16c8d1a2e..e09498ad5 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -391,13 +391,195 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { expect(event.data.customField).toBe("test123"); }); - // Five C81181 functional tests are interact link-XDM variations, deferred as - // scope — not blocked (no sendBeacon dependency). - test.skip("C81181 - link click is collected when clickCollectionEnabled and no callback defined", () => {}); - test.skip("C81181 - onBeforeLinkClickSend cancels a request conditionally based on clickedElement.id", () => {}); - test.skip("C81181 - filterClickDetails cancels a request conditionally based on clickedElement.id", () => {}); - test.skip("C81181 - filterClickDetails can augment xdm and data before the request fires", () => {}); - test.skip("C81181 - clickCollection.sessionStorageEnabled:false still captures link click data", () => {}); + test("link click is collected with full XDM when no callback is defined", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { + eventGroupingEnabled: false, + }, + }); + + const link = appendLink({ + id: "alloy-link-test", + href: "valid.html", + text: "Test Link", + }); + clickLink(link); + + const call = await networkRecorder.findCall(/v1\/interact/); + expect(call).toBeDefined(); + + const event = firstEvent(call); + expect(event.xdm.eventType).toBe("web.webinteraction.linkClicks"); + expect(event.xdm.web.webInteraction).toEqual( + expectedWebInteraction({ name: "Test Link", type: "other", href: "valid.html" }), + ); + }); + + test("onBeforeLinkClickSend cancels conditionally based on clickedElement.id", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { + eventGroupingEnabled: false, + }, + onBeforeLinkClickSend: (options) => + options.clickedElement.id !== "canceled-alloy-link-test", + }); + + const canceledLink = appendLink({ + id: "canceled-alloy-link-test", + href: "canceled.html", + text: "Canceled Link", + }); + clickLink(canceledLink); + await waitFor(NO_REQUEST_WAIT_MS); + expect(interactCalls(networkRecorder).length).toBe(0); + + const link = appendLink({ + id: "alloy-link-test", + href: "valid.html", + text: "Test Link", + }); + clickLink(link); + + const call = await networkRecorder.findCall(/v1\/interact/); + expect(call).toBeDefined(); + expect(firstEvent(call).xdm.web.webInteraction).toEqual( + expectedWebInteraction({ name: "Test Link", type: "other", href: "valid.html" }), + ); + }); + + test("filterClickDetails cancels conditionally based on clickedElement.id", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { + eventGroupingEnabled: false, + filterClickDetails: (props) => + props.clickedElement.id !== "canceled-alloy-link-test", + }, + }); + + const canceledLink = appendLink({ + id: "canceled-alloy-link-test", + href: "canceled.html", + text: "Canceled Link", + }); + clickLink(canceledLink); + await waitFor(NO_REQUEST_WAIT_MS); + expect(interactCalls(networkRecorder).length).toBe(0); + + const link = appendLink({ + id: "alloy-link-test", + href: "valid.html", + text: "Test Link", + }); + clickLink(link); + + const call = await networkRecorder.findCall(/v1\/interact/); + expect(call).toBeDefined(); + expect(firstEvent(call).xdm.web.webInteraction).toEqual( + expectedWebInteraction({ name: "Test Link", type: "other", href: "valid.html" }), + ); + }); + + test("filterClickDetails can augment xdm and data before the request fires", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { + eventGroupingEnabled: false, + filterClickDetails: (props) => { + if (props.clickedElement.id === "alloy-link-test") { + props.linkName = "Augmented name"; + return true; + } + return false; + }, + }, + }); + + const link = appendLink({ + id: "alloy-link-test", + href: "valid.html", + text: "Test Link", + }); + clickLink(link); + + const call = await networkRecorder.findCall(/v1\/interact/); + expect(call).toBeDefined(); + + const event = firstEvent(call); + expect(event.xdm.eventType).toBe("web.webinteraction.linkClicks"); + // filterClickDetails mutates linkName, so both the webInteraction name and + // the activity-map link reflect the augmented value. + expect(event.xdm.web.webInteraction).toEqual( + expectedWebInteraction({ + name: "Augmented name", + type: "other", + href: "valid.html", + }), + ); + expect(activityMap(event)).toEqual( + expectedActivityMap({ link: "Augmented name" }), + ); + }); + + test("link click data is captured when clickCollection.sessionStorageEnabled is false", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { + eventGroupingEnabled: false, + sessionStorageEnabled: false, + }, + }); + + const link = appendLink({ + id: "alloy-link-test", + href: "valid.html", + text: "Test Link", + }); + clickLink(link); + + const call = await networkRecorder.findCall(/v1\/interact/); + expect(call).toBeDefined(); + expect(firstEvent(call).xdm.web.webInteraction).toEqual( + expectedWebInteraction({ name: "Test Link", type: "other", href: "valid.html" }), + ); + }); }); describe("C11693274 - URL query params do not affect exit link classification", () => { From 1519a38f0838c4cd1e27da00d03b5dc3e57ea36d Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:39:02 -0600 Subject: [PATCH 32/38] test(integration): migrate the C8118 link-click suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the wholesale C8118 skip with the full suite: the interact-then-collect routing case (via the sendBeacon recorder once the interact response establishes identity) plus the 14 interact-only cases — enabled/disabled download, internal and external links, event grouping, cached click on page view, custom region, custom link type, custom activity map, multiple grouped clicks, and custom XDM. URLs are adapted to the localhost page; the grouped-clicks payload shape stays tolerant, matching the original's hedge. Co-Authored-By: Claude Opus 4.8 --- .../Data Collector/dataCollector.spec.js | 446 +++++++++++++++++- 1 file changed, 440 insertions(+), 6 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index e09498ad5..b735f1a5e 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -24,7 +24,11 @@ import { import alloyConfig from "../../helpers/alloy/config.js"; import searchForLogMessage from "../../helpers/utils/searchForLogMessage.js"; import { CONSENT_OUT } from "../../helpers/constants/consent.js"; -import { appendLink, clickLink } from "../../helpers/utils/domHelpers.js"; +import { + appendLink, + appendHtmlToBody, + clickLink, +} from "../../helpers/utils/domHelpers.js"; import { sendBeaconCalls, resetSendBeaconCalls, @@ -711,12 +715,442 @@ describe("C455258 - sendEvent routes to collect via sendBeacon once identity is }); }); -describe("Not migrated (see per-test rationale)", () => { - // 15 functional sub-tests. The one collect-routing case is now doable via the - // sendBeacon recorder (see C455258); the other 14 are interact link-XDM - // variations deferred as scope, like the C81181 five. - test.skip("C8118 - link click routes to interact (no identity) then collect (identity established)", () => {}); +describe("C8118 - Collects and sends link click information", () => { + const clickById = (id = "alloy-link-test") => + clickLink(document.getElementById(id)); + + const waitForIdentityCookie = () => + expect + .poll(async () => + (await cookieStore.getAll()).some((c) => c.name.endsWith("_identity")), + ) + .toBe(true); + + test("link click uses interact before identity and collect (sendBeacon) after", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventWithIdentityHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { eventGroupingEnabled: false }, + }); + + appendLink({ id: "alloy-link-test", href: "blank.html", text: "Test Link" }); + clickById(); + + const call = await networkRecorder.findCall(/v1\/interact/); + expect(call).toBeDefined(); + expect(firstEvent(call).xdm.web.webInteraction).toEqual( + expectedWebInteraction({ name: "Test Link", type: "other", href: "blank.html" }), + ); + expect(sendBeaconCalls().length).toBe(0); + + // The interact response establishes an identity; wait for the cookie before + // the second click so it can route to collect. + await waitForIdentityCookie(); + networkRecorder.reset(); + resetSendBeaconCalls(); + + clickById(); + await expect.poll(() => sendBeaconCalls().length).toBe(1); + expect(interactCalls(networkRecorder).length).toBe(0); + expect(sendBeaconCalls()[0].url).toMatch(/\/v1\/collect\?configId=/); + }); + + test("download link click is not sent when downloadLinkEnabled is false", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { downloadLinkEnabled: false, eventGroupingEnabled: false }, + }); + + appendHtmlToBody( + `Download Zip File`, + ); + clickById(); + + await waitFor(NO_REQUEST_WAIT_MS); + expect(interactCalls(networkRecorder).length).toBe(0); + expect(sendBeaconCalls().length).toBe(0); + }); + + test("download link click is sent with download type when downloadLinkEnabled is true", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { downloadLinkEnabled: true, eventGroupingEnabled: false }, + }); + appendHtmlToBody( + `Download Zip File`, + ); + clickById(); + + const call = await networkRecorder.findCall(/v1\/interact/); + const event = firstEvent(call); + expect(event.xdm.web.webInteraction).toEqual( + expectedWebInteraction({ + name: "Download Zip File", + type: "download", + href: "example.zip", + }), + ); + expect(activityMap(event)).toEqual( + expectedActivityMap({ link: "Download Zip File" }), + ); + }); + + test("internal link click is not sent when internalLinkEnabled is false", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { internalLinkEnabled: false, eventGroupingEnabled: false }, + }); + + appendLink({ id: "alloy-link-test", href: "blank.html", text: "Test Link" }); + clickById(); + + await waitFor(NO_REQUEST_WAIT_MS); + expect(interactCalls(networkRecorder).length).toBe(0); + expect(sendBeaconCalls().length).toBe(0); + }); + + test("internal link click is sent with full XDM when internalLinkEnabled is true", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { internalLinkEnabled: true, eventGroupingEnabled: false }, + }); + + appendLink({ id: "alloy-link-test", href: "blank.html", text: "Internal Link" }); + clickById(); + + const call = await networkRecorder.findCall(/v1\/interact/); + const event = firstEvent(call); + expect(event.xdm.web.webInteraction).toEqual( + expectedWebInteraction({ + name: "Internal Link", + type: "other", + href: "blank.html", + }), + ); + expect(activityMap(event)).toEqual( + expectedActivityMap({ link: "Internal Link" }), + ); + }); + + test("external link click is not sent when externalLinkEnabled is false", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { externalLinkEnabled: false, eventGroupingEnabled: false }, + }); + + appendLink({ + id: "alloy-link-test", + href: "https://example.com/", + text: "External Link", + }); + clickById(); + + await waitFor(NO_REQUEST_WAIT_MS); + expect(interactCalls(networkRecorder).length).toBe(0); + expect(sendBeaconCalls().length).toBe(0); + }); + + test("external link click is sent with exit type when externalLinkEnabled is true", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { externalLinkEnabled: true, eventGroupingEnabled: false }, + }); + + appendLink({ + id: "alloy-link-test", + href: "https://example.com/", + text: "External Link", + }); + clickById(); + + const call = await networkRecorder.findCall(/v1\/interact/); + const event = firstEvent(call); + expect(event.xdm.web.webInteraction).toEqual( + expectedWebInteraction({ + name: "External Link", + type: "exit", + href: "https://example.com/", + }), + ); + expect(activityMap(event)).toEqual( + expectedActivityMap({ link: "External Link" }), + ); + }); + + test("internal link click is not sent immediately when event grouping is enabled", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { internalLinkEnabled: true, eventGroupingEnabled: true }, + }); + + appendLink({ id: "alloy-link-test", href: "blank.html", text: "Test Link" }); + clickById(); + + await waitFor(NO_REQUEST_WAIT_MS); + expect(interactCalls(networkRecorder).length).toBe(0); + expect(sendBeaconCalls().length).toBe(0); + }); + + test("cached internal link click is sent on the next page view event", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { internalLinkEnabled: true, eventGroupingEnabled: true }, + }); + + appendLink({ id: "alloy-link-test", href: "blank.html", text: "Test Link" }); + clickById(); + await waitFor(NO_REQUEST_WAIT_MS); + expect(interactCalls(networkRecorder).length).toBe(0); + + networkRecorder.reset(); + + await alloy("sendEvent", { + xdm: { + web: { + webPageDetails: { name: "Test Page", pageViews: { value: 1 } }, + }, + }, + }); + + const call = await networkRecorder.findCall(/v1\/interact/); + const event = firstEvent(call); + expect(event.xdm.web.webInteraction).toEqual( + expectedWebInteraction({ name: "Test Link", type: "other", href: "blank.html" }), + ); + expect(event.xdm.web.webPageDetails.name).toBe("Test Page"); + expect(event.xdm.web.webPageDetails.pageViews).toEqual({ value: 1 }); + expect(activityMap(event)).toEqual(expectedActivityMap({ link: "Test Link" })); + }); + + test("internal link click data with custom region", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { internalLinkEnabled: true, eventGroupingEnabled: false }, + }); + + appendHtmlToBody( + ``, + ); + clickById(); + + const call = await networkRecorder.findCall(/v1\/interact/); + expect(firstEvent(call).xdm.web.webInteraction.region).toBe("custom-region"); + }); + + test("external link click data with custom link type", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { externalLinkEnabled: true, eventGroupingEnabled: false }, + }); + + appendHtmlToBody( + `External Link`, + ); + clickById(); + + const call = await networkRecorder.findCall(/v1\/interact/); + expect(firstEvent(call).xdm.web.webInteraction.type).toBe("exit"); + }); + + test("link click with custom activity map data", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { internalLinkEnabled: true, eventGroupingEnabled: false }, + }); + + appendHtmlToBody( + ``, + ); + clickById(); + + const call = await networkRecorder.findCall(/v1\/interact/); + expect(activityMap(firstEvent(call))).toEqual( + expectedActivityMap({ + link: "Custom Activity Map Link", + region: "custom-region", + }), + ); + }); + + test("multiple grouped link clicks surface on the next page view event", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { internalLinkEnabled: true, eventGroupingEnabled: true }, + }); + + appendLink({ id: "alloy-link-test-1", href: "blank.html", text: "Link 1" }); + appendLink({ id: "alloy-link-test-2", href: "blank.html", text: "Link 2" }); + clickById("alloy-link-test-1"); + clickById("alloy-link-test-2"); + + await waitFor(NO_REQUEST_WAIT_MS); + expect(interactCalls(networkRecorder).length).toBe(0); + + networkRecorder.reset(); + + await alloy("sendEvent", { + xdm: { + web: { + webPageDetails: { name: "Test Page", pageViews: { value: 1 } }, + }, + }, + }); + + const call = await networkRecorder.findCall(/v1\/interact/); + const { webInteraction } = firstEvent(call).xdm.web; + expect(webInteraction).toBeDefined(); + // Event grouping caches one click at a time, so the last click wins. The + // payload shape is not guaranteed, so accept either an array or an object. + if (Array.isArray(webInteraction)) { + expect(webInteraction.at(-1).name).toBe("Link 2"); + } else { + expect(webInteraction.name).toBe("Link 2"); + } + }); + + test("link click with custom XDM data via onBeforeLinkClickSend", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { internalLinkEnabled: true, eventGroupingEnabled: false }, + onBeforeLinkClickSend: (options) => { + options.xdm.customField = "customValue"; + return true; + }, + }); + + appendLink({ id: "alloy-link-test", href: "blank.html", text: "Internal Link" }); + clickById(); + + const call = await networkRecorder.findCall(/v1\/interact/); + expect(firstEvent(call).xdm.customField).toBe("customValue"); + }); + + test("page view name is stored and the grouped click surfaces with it", async ({ + alloy, + worker, + networkRecorder, + }) => { + worker.use(sendEventHandler); + + await alloy("configure", { + ...alloyConfig, + clickCollectionEnabled: true, + clickCollection: { eventGroupingEnabled: true }, + }); + + appendLink({ id: "alloy-link-test", href: "blank.html", text: "Test Link" }); + clickById(); + + await alloy("sendEvent", { + xdm: { web: { webPageDetails: { name: "Test Page" } } }, + }); + + const call = await networkRecorder.findCall(/v1\/interact/); + const { web } = firstEvent(call).xdm; + expect(web.webPageDetails.name).toBe("Test Page"); + expect(web.webInteraction).toBeDefined(); + }); +}); + +describe("Not migrated (see per-test rationale)", () => { // Asserts the Referer header, but it's a browser-set forbidden header added // after MSW's service worker sees the request, so networkRecorder can't // capture it (verified). The collect-side referer was already a commented-out From 8237cf38635cd78c1fd27e2da9c678615758dec9 Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:43:23 -0600 Subject: [PATCH 33/38] docs(integration): explain why the sendBeacon recorder must be global Reviewer flagged the global sendBeacon override as potentially masking collect/fallback behavior in unrelated specs. Opt-in per spec was attempted and verified to fail: alloy binds navigator.sendBeacon by value when it creates its network service at bundle load, before any per-test beforeEach runs, so a later swap is ignored (the routing assertions saw zero beacons). Document that the global install is therefore required and that always returning true is the deliberate hermetic default mirroring the functional suite. Co-Authored-By: Claude Opus 4.8 --- .../helpers/alloy/setupBaseCode.js | 7 ++--- .../integration/helpers/utils/sendBeacon.js | 26 ++++++++++++------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/packages/browser/test/integration/helpers/alloy/setupBaseCode.js b/packages/browser/test/integration/helpers/alloy/setupBaseCode.js index 1add8176d..ae65541aa 100644 --- a/packages/browser/test/integration/helpers/alloy/setupBaseCode.js +++ b/packages/browser/test/integration/helpers/alloy/setupBaseCode.js @@ -20,9 +20,10 @@ export default async () => { `${server.config.root}/packages/browser/distTest/baseCode.min.js`, ); - // Must run before the alloy script is injected below: createBrowserNetworkService - // binds navigator.sendBeacon once at instance creation, so the recorder has to - // already be in place. Per-test reset lives in the extend.js alloy fixture. + // Must run before the alloy script is injected below: alloy creates its + // instance (and its network service) at bundle load, and that service + // binds navigator.sendBeacon by value at that moment, so a later swap would + // be ignored. Per-test reset lives in the extend.js alloy fixture. installSendBeaconRecorder(); document.body.innerHTML = "Alloy Test Page"; diff --git a/packages/browser/test/integration/helpers/utils/sendBeacon.js b/packages/browser/test/integration/helpers/utils/sendBeacon.js index a26c1dd19..a03231539 100644 --- a/packages/browser/test/integration/helpers/utils/sendBeacon.js +++ b/packages/browser/test/integration/helpers/utils/sendBeacon.js @@ -10,16 +10,22 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -// Alloy binds navigator.sendBeacon once at instance creation (see -// createBrowserNetworkService), and MSW's service worker does not surface -// sendBeacon for assertion. So the only way to observe collect-vs-interact -// routing is to record sendBeacon calls in the page — the same approach the -// functional suite took (sendBeaconMock.js). The patch must be installed -// before the library script loads, so it lives in setupBaseCode alongside the -// permanent addEventListener patch: installed once per page lifetime, with the -// captured calls reset per test. Returning true keeps the suite hermetic (no -// stray real beacon) and tells alloy the beacon succeeded so it doesn't fall -// back to fetch. +// MSW's service worker does not surface sendBeacon for assertion, so the only +// way to observe collect-vs-interact routing is to record sendBeacon calls in +// the page — the same approach the functional suite took (sendBeaconMock.js). +// +// The recorder is necessarily global (installed in setupBaseCode before the +// alloy script is injected) rather than opt-in per spec: alloy creates its +// network service at bundle load, and createBrowserNetworkService binds +// navigator.sendBeacon *by value* (.bind) at that moment. A per-test beforeEach +// runs after the bundle has already loaded, so its swap would be ignored — +// verified empirically (the routing assertions saw zero recorded beacons). +// +// Returning true is the deliberate hermetic default, matching the functional +// suite: it suppresses any stray real beacon and tells alloy the beacon +// succeeded so it doesn't fall back to fetch. Specs that don't assert routing +// are unaffected, and any that need the fetch-fallback path could simulate it +// by recording false here. export const installSendBeaconRecorder = () => { if (window.__alloySendBeaconInstalled) { From 2ed63e09a51a2547e179dd461fc51500dbabf85a Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:44:24 -0600 Subject: [PATCH 34/38] format --- .../Data Collector/dataCollector.spec.js | 159 ++++++++++++++---- 1 file changed, 127 insertions(+), 32 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index b735f1a5e..b0c2a1b8d 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -44,9 +44,7 @@ const NO_REQUEST_WAIT_MS = 200; const EDGE_INTERACT = /^https:\/\/edge\.adobedc\.net\/ee\/(?:[^?]*\/)?v1\/interact\?configId=/; const interactCalls = (networkRecorder) => - networkRecorder.calls.filter((c) => - EDGE_INTERACT.test(c.request?.url ?? ""), - ); + networkRecorder.calls.filter((c) => EDGE_INTERACT.test(c.request?.url ?? "")); const firstEvent = (call) => call.request.body.events[0]; @@ -391,7 +389,9 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { ); // The activity-map link is unchanged: the callback only augmented the xdm // webInteraction name, not the data. - expect(activityMap(event)).toEqual(expectedActivityMap({ link: "Test Link" })); + expect(activityMap(event)).toEqual( + expectedActivityMap({ link: "Test Link" }), + ); expect(event.data.customField).toBe("test123"); }); @@ -423,7 +423,11 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { const event = firstEvent(call); expect(event.xdm.eventType).toBe("web.webinteraction.linkClicks"); expect(event.xdm.web.webInteraction).toEqual( - expectedWebInteraction({ name: "Test Link", type: "other", href: "valid.html" }), + expectedWebInteraction({ + name: "Test Link", + type: "other", + href: "valid.html", + }), ); }); @@ -463,7 +467,11 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { const call = await networkRecorder.findCall(/v1\/interact/); expect(call).toBeDefined(); expect(firstEvent(call).xdm.web.webInteraction).toEqual( - expectedWebInteraction({ name: "Test Link", type: "other", href: "valid.html" }), + expectedWebInteraction({ + name: "Test Link", + type: "other", + href: "valid.html", + }), ); }); @@ -503,7 +511,11 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { const call = await networkRecorder.findCall(/v1\/interact/); expect(call).toBeDefined(); expect(firstEvent(call).xdm.web.webInteraction).toEqual( - expectedWebInteraction({ name: "Test Link", type: "other", href: "valid.html" }), + expectedWebInteraction({ + name: "Test Link", + type: "other", + href: "valid.html", + }), ); }); @@ -581,7 +593,11 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { const call = await networkRecorder.findCall(/v1\/interact/); expect(call).toBeDefined(); expect(firstEvent(call).xdm.web.webInteraction).toEqual( - expectedWebInteraction({ name: "Test Link", type: "other", href: "valid.html" }), + expectedWebInteraction({ + name: "Test Link", + type: "other", + href: "valid.html", + }), ); }); }); @@ -739,13 +755,21 @@ describe("C8118 - Collects and sends link click information", () => { clickCollection: { eventGroupingEnabled: false }, }); - appendLink({ id: "alloy-link-test", href: "blank.html", text: "Test Link" }); + appendLink({ + id: "alloy-link-test", + href: "blank.html", + text: "Test Link", + }); clickById(); const call = await networkRecorder.findCall(/v1\/interact/); expect(call).toBeDefined(); expect(firstEvent(call).xdm.web.webInteraction).toEqual( - expectedWebInteraction({ name: "Test Link", type: "other", href: "blank.html" }), + expectedWebInteraction({ + name: "Test Link", + type: "other", + href: "blank.html", + }), ); expect(sendBeaconCalls().length).toBe(0); @@ -771,7 +795,10 @@ describe("C8118 - Collects and sends link click information", () => { await alloy("configure", { ...alloyConfig, clickCollectionEnabled: true, - clickCollection: { downloadLinkEnabled: false, eventGroupingEnabled: false }, + clickCollection: { + downloadLinkEnabled: false, + eventGroupingEnabled: false, + }, }); appendHtmlToBody( @@ -794,7 +821,10 @@ describe("C8118 - Collects and sends link click information", () => { await alloy("configure", { ...alloyConfig, clickCollectionEnabled: true, - clickCollection: { downloadLinkEnabled: true, eventGroupingEnabled: false }, + clickCollection: { + downloadLinkEnabled: true, + eventGroupingEnabled: false, + }, }); appendHtmlToBody( @@ -826,10 +856,17 @@ describe("C8118 - Collects and sends link click information", () => { await alloy("configure", { ...alloyConfig, clickCollectionEnabled: true, - clickCollection: { internalLinkEnabled: false, eventGroupingEnabled: false }, + clickCollection: { + internalLinkEnabled: false, + eventGroupingEnabled: false, + }, }); - appendLink({ id: "alloy-link-test", href: "blank.html", text: "Test Link" }); + appendLink({ + id: "alloy-link-test", + href: "blank.html", + text: "Test Link", + }); clickById(); await waitFor(NO_REQUEST_WAIT_MS); @@ -847,10 +884,17 @@ describe("C8118 - Collects and sends link click information", () => { await alloy("configure", { ...alloyConfig, clickCollectionEnabled: true, - clickCollection: { internalLinkEnabled: true, eventGroupingEnabled: false }, + clickCollection: { + internalLinkEnabled: true, + eventGroupingEnabled: false, + }, }); - appendLink({ id: "alloy-link-test", href: "blank.html", text: "Internal Link" }); + appendLink({ + id: "alloy-link-test", + href: "blank.html", + text: "Internal Link", + }); clickById(); const call = await networkRecorder.findCall(/v1\/interact/); @@ -877,7 +921,10 @@ describe("C8118 - Collects and sends link click information", () => { await alloy("configure", { ...alloyConfig, clickCollectionEnabled: true, - clickCollection: { externalLinkEnabled: false, eventGroupingEnabled: false }, + clickCollection: { + externalLinkEnabled: false, + eventGroupingEnabled: false, + }, }); appendLink({ @@ -902,7 +949,10 @@ describe("C8118 - Collects and sends link click information", () => { await alloy("configure", { ...alloyConfig, clickCollectionEnabled: true, - clickCollection: { externalLinkEnabled: true, eventGroupingEnabled: false }, + clickCollection: { + externalLinkEnabled: true, + eventGroupingEnabled: false, + }, }); appendLink({ @@ -936,10 +986,17 @@ describe("C8118 - Collects and sends link click information", () => { await alloy("configure", { ...alloyConfig, clickCollectionEnabled: true, - clickCollection: { internalLinkEnabled: true, eventGroupingEnabled: true }, + clickCollection: { + internalLinkEnabled: true, + eventGroupingEnabled: true, + }, }); - appendLink({ id: "alloy-link-test", href: "blank.html", text: "Test Link" }); + appendLink({ + id: "alloy-link-test", + href: "blank.html", + text: "Test Link", + }); clickById(); await waitFor(NO_REQUEST_WAIT_MS); @@ -957,10 +1014,17 @@ describe("C8118 - Collects and sends link click information", () => { await alloy("configure", { ...alloyConfig, clickCollectionEnabled: true, - clickCollection: { internalLinkEnabled: true, eventGroupingEnabled: true }, + clickCollection: { + internalLinkEnabled: true, + eventGroupingEnabled: true, + }, }); - appendLink({ id: "alloy-link-test", href: "blank.html", text: "Test Link" }); + appendLink({ + id: "alloy-link-test", + href: "blank.html", + text: "Test Link", + }); clickById(); await waitFor(NO_REQUEST_WAIT_MS); expect(interactCalls(networkRecorder).length).toBe(0); @@ -978,11 +1042,17 @@ describe("C8118 - Collects and sends link click information", () => { const call = await networkRecorder.findCall(/v1\/interact/); const event = firstEvent(call); expect(event.xdm.web.webInteraction).toEqual( - expectedWebInteraction({ name: "Test Link", type: "other", href: "blank.html" }), + expectedWebInteraction({ + name: "Test Link", + type: "other", + href: "blank.html", + }), ); expect(event.xdm.web.webPageDetails.name).toBe("Test Page"); expect(event.xdm.web.webPageDetails.pageViews).toEqual({ value: 1 }); - expect(activityMap(event)).toEqual(expectedActivityMap({ link: "Test Link" })); + expect(activityMap(event)).toEqual( + expectedActivityMap({ link: "Test Link" }), + ); }); test("internal link click data with custom region", async ({ @@ -995,7 +1065,10 @@ describe("C8118 - Collects and sends link click information", () => { await alloy("configure", { ...alloyConfig, clickCollectionEnabled: true, - clickCollection: { internalLinkEnabled: true, eventGroupingEnabled: false }, + clickCollection: { + internalLinkEnabled: true, + eventGroupingEnabled: false, + }, }); appendHtmlToBody( @@ -1004,7 +1077,9 @@ describe("C8118 - Collects and sends link click information", () => { clickById(); const call = await networkRecorder.findCall(/v1\/interact/); - expect(firstEvent(call).xdm.web.webInteraction.region).toBe("custom-region"); + expect(firstEvent(call).xdm.web.webInteraction.region).toBe( + "custom-region", + ); }); test("external link click data with custom link type", async ({ @@ -1017,7 +1092,10 @@ describe("C8118 - Collects and sends link click information", () => { await alloy("configure", { ...alloyConfig, clickCollectionEnabled: true, - clickCollection: { externalLinkEnabled: true, eventGroupingEnabled: false }, + clickCollection: { + externalLinkEnabled: true, + eventGroupingEnabled: false, + }, }); appendHtmlToBody( @@ -1039,7 +1117,10 @@ describe("C8118 - Collects and sends link click information", () => { await alloy("configure", { ...alloyConfig, clickCollectionEnabled: true, - clickCollection: { internalLinkEnabled: true, eventGroupingEnabled: false }, + clickCollection: { + internalLinkEnabled: true, + eventGroupingEnabled: false, + }, }); appendHtmlToBody( @@ -1066,7 +1147,10 @@ describe("C8118 - Collects and sends link click information", () => { await alloy("configure", { ...alloyConfig, clickCollectionEnabled: true, - clickCollection: { internalLinkEnabled: true, eventGroupingEnabled: true }, + clickCollection: { + internalLinkEnabled: true, + eventGroupingEnabled: true, + }, }); appendLink({ id: "alloy-link-test-1", href: "blank.html", text: "Link 1" }); @@ -1109,14 +1193,21 @@ describe("C8118 - Collects and sends link click information", () => { await alloy("configure", { ...alloyConfig, clickCollectionEnabled: true, - clickCollection: { internalLinkEnabled: true, eventGroupingEnabled: false }, + clickCollection: { + internalLinkEnabled: true, + eventGroupingEnabled: false, + }, onBeforeLinkClickSend: (options) => { options.xdm.customField = "customValue"; return true; }, }); - appendLink({ id: "alloy-link-test", href: "blank.html", text: "Internal Link" }); + appendLink({ + id: "alloy-link-test", + href: "blank.html", + text: "Internal Link", + }); clickById(); const call = await networkRecorder.findCall(/v1\/interact/); @@ -1136,7 +1227,11 @@ describe("C8118 - Collects and sends link click information", () => { clickCollection: { eventGroupingEnabled: true }, }); - appendLink({ id: "alloy-link-test", href: "blank.html", text: "Test Link" }); + appendLink({ + id: "alloy-link-test", + href: "blank.html", + text: "Test Link", + }); clickById(); await alloy("sendEvent", { From e2d5bddc64264efca33de154ee195c301c60a2b3 Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:49:53 -0600 Subject: [PATCH 35/38] test(integration): pin full webPageDetails in C8118 cached test For consistency with the other restored assertions, pin the whole webPageDetails object (URL, name, pageViews) instead of just name and pageViews. webPageDetails.URL is window.location.href, the same value already used for the activity-map page. Co-Authored-By: Claude Opus 4.8 --- .../integration/specs/Data Collector/dataCollector.spec.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index b0c2a1b8d..21025315d 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -1048,8 +1048,11 @@ describe("C8118 - Collects and sends link click information", () => { href: "blank.html", }), ); - expect(event.xdm.web.webPageDetails.name).toBe("Test Page"); - expect(event.xdm.web.webPageDetails.pageViews).toEqual({ value: 1 }); + expect(event.xdm.web.webPageDetails).toEqual({ + URL: window.location.href, + name: "Test Page", + pageViews: { value: 1 }, + }); expect(activityMap(event)).toEqual( expectedActivityMap({ link: "Test Link" }), ); From c03cbf1560931f2998399e1761eeff162943f60b Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:04:27 -0600 Subject: [PATCH 36/38] test(integration): poll longer for fire-and-forget link-click requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI failed 10 link-click assertions with findCall returning undefined. Unlike the sendEvent tests, which await the command (so the call is already complete), clickLink returns before the request is sent, so findCall must outwait the click → event → fetch → MSW round-trip. Its default (5 retries × 10ms ≈ 50ms) is too short under CI load — the latent race only surfaced once this PR added ~20 more click tests. Route all click-driven interact lookups through a findInteractCall helper that polls up to ~2s, returning as soon as the call completes. Co-Authored-By: Claude Opus 4.8 --- .../Data Collector/dataCollector.spec.js | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 21025315d..22e752eef 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -48,6 +48,13 @@ const interactCalls = (networkRecorder) => const firstEvent = (call) => call.request.body.events[0]; +// Link clicks are fire-and-forget: clickLink returns before the request is sent, +// so findCall must outwait the click → event → fetch → MSW round-trip. The +// default (5 × 10ms) is too short under CI load; poll up to ~2s, returning as +// soon as the call completes. +const findInteractCall = (networkRecorder) => + networkRecorder.findCall(/v1\/interact/, { retries: 40, delayMs: 50 }); + // eslint-disable-next-line no-underscore-dangle const activityMap = (event) => event.data.__adobe.analytics.contextData.a.activitymap; @@ -375,7 +382,7 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { }); clickLink(link); - const call = await networkRecorder.findCall(/v1\/interact/); + const call = await findInteractCall(networkRecorder); expect(call).toBeDefined(); const event = firstEvent(call); @@ -417,7 +424,7 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { }); clickLink(link); - const call = await networkRecorder.findCall(/v1\/interact/); + const call = await findInteractCall(networkRecorder); expect(call).toBeDefined(); const event = firstEvent(call); @@ -464,7 +471,7 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { }); clickLink(link); - const call = await networkRecorder.findCall(/v1\/interact/); + const call = await findInteractCall(networkRecorder); expect(call).toBeDefined(); expect(firstEvent(call).xdm.web.webInteraction).toEqual( expectedWebInteraction({ @@ -508,7 +515,7 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { }); clickLink(link); - const call = await networkRecorder.findCall(/v1\/interact/); + const call = await findInteractCall(networkRecorder); expect(call).toBeDefined(); expect(firstEvent(call).xdm.web.webInteraction).toEqual( expectedWebInteraction({ @@ -548,7 +555,7 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { }); clickLink(link); - const call = await networkRecorder.findCall(/v1\/interact/); + const call = await findInteractCall(networkRecorder); expect(call).toBeDefined(); const event = firstEvent(call); @@ -590,7 +597,7 @@ describe("C81181 - onBeforeLinkClickSend callback", () => { }); clickLink(link); - const call = await networkRecorder.findCall(/v1\/interact/); + const call = await findInteractCall(networkRecorder); expect(call).toBeDefined(); expect(firstEvent(call).xdm.web.webInteraction).toEqual( expectedWebInteraction({ @@ -628,7 +635,7 @@ describe("C11693274 - URL query params do not affect exit link classification", }); clickLink(link); - const call = await networkRecorder.findCall(/v1\/interact/); + const call = await findInteractCall(networkRecorder); expect(call).toBeDefined(); const eventXdm = call.request.body.events[0].xdm; @@ -762,7 +769,7 @@ describe("C8118 - Collects and sends link click information", () => { }); clickById(); - const call = await networkRecorder.findCall(/v1\/interact/); + const call = await findInteractCall(networkRecorder); expect(call).toBeDefined(); expect(firstEvent(call).xdm.web.webInteraction).toEqual( expectedWebInteraction({ @@ -832,7 +839,7 @@ describe("C8118 - Collects and sends link click information", () => { ); clickById(); - const call = await networkRecorder.findCall(/v1\/interact/); + const call = await findInteractCall(networkRecorder); const event = firstEvent(call); expect(event.xdm.web.webInteraction).toEqual( expectedWebInteraction({ @@ -897,7 +904,7 @@ describe("C8118 - Collects and sends link click information", () => { }); clickById(); - const call = await networkRecorder.findCall(/v1\/interact/); + const call = await findInteractCall(networkRecorder); const event = firstEvent(call); expect(event.xdm.web.webInteraction).toEqual( expectedWebInteraction({ @@ -962,7 +969,7 @@ describe("C8118 - Collects and sends link click information", () => { }); clickById(); - const call = await networkRecorder.findCall(/v1\/interact/); + const call = await findInteractCall(networkRecorder); const event = firstEvent(call); expect(event.xdm.web.webInteraction).toEqual( expectedWebInteraction({ @@ -1039,7 +1046,7 @@ describe("C8118 - Collects and sends link click information", () => { }, }); - const call = await networkRecorder.findCall(/v1\/interact/); + const call = await findInteractCall(networkRecorder); const event = firstEvent(call); expect(event.xdm.web.webInteraction).toEqual( expectedWebInteraction({ @@ -1079,7 +1086,7 @@ describe("C8118 - Collects and sends link click information", () => { ); clickById(); - const call = await networkRecorder.findCall(/v1\/interact/); + const call = await findInteractCall(networkRecorder); expect(firstEvent(call).xdm.web.webInteraction.region).toBe( "custom-region", ); @@ -1106,7 +1113,7 @@ describe("C8118 - Collects and sends link click information", () => { ); clickById(); - const call = await networkRecorder.findCall(/v1\/interact/); + const call = await findInteractCall(networkRecorder); expect(firstEvent(call).xdm.web.webInteraction.type).toBe("exit"); }); @@ -1131,7 +1138,7 @@ describe("C8118 - Collects and sends link click information", () => { ); clickById(); - const call = await networkRecorder.findCall(/v1\/interact/); + const call = await findInteractCall(networkRecorder); expect(activityMap(firstEvent(call))).toEqual( expectedActivityMap({ link: "Custom Activity Map Link", @@ -1174,7 +1181,7 @@ describe("C8118 - Collects and sends link click information", () => { }, }); - const call = await networkRecorder.findCall(/v1\/interact/); + const call = await findInteractCall(networkRecorder); const { webInteraction } = firstEvent(call).xdm.web; expect(webInteraction).toBeDefined(); // Event grouping caches one click at a time, so the last click wins. The @@ -1213,7 +1220,7 @@ describe("C8118 - Collects and sends link click information", () => { }); clickById(); - const call = await networkRecorder.findCall(/v1\/interact/); + const call = await findInteractCall(networkRecorder); expect(firstEvent(call).xdm.customField).toBe("customValue"); }); @@ -1241,7 +1248,7 @@ describe("C8118 - Collects and sends link click information", () => { xdm: { web: { webPageDetails: { name: "Test Page" } } }, }); - const call = await networkRecorder.findCall(/v1\/interact/); + const call = await findInteractCall(networkRecorder); const { web } = firstEvent(call).xdm; expect(web.webPageDetails.name).toBe("Test Page"); expect(web.webInteraction).toBeDefined(); From 575b7e5e1856cc3ca3318046ddd4eac3899ccce4 Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:14:36 -0600 Subject: [PATCH 37/38] test(integration): wait for link-click requests by event, not polling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the retry-tuned findCall with an event-driven networkRecorder.waitForCall: it resolves the instant the matching response is captured (driven by the MSW request/response events the recorder already listens to), so there is no polling interval to tune and no CI-load race. Link clicks are fire-and-forget — clickLink returns before the request is sent — so this is the right tool where there is no command promise to await. Falls back to undefined after a safety timeout so presence is still asserted with toBeDefined(). Co-Authored-By: Claude Opus 4.8 --- .../helpers/mswjs/networkRecorder.js | 63 +++++++++++++++++++ .../Data Collector/dataCollector.spec.js | 8 +-- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/packages/browser/test/integration/helpers/mswjs/networkRecorder.js b/packages/browser/test/integration/helpers/mswjs/networkRecorder.js index 28a306c1f..650a06ac7 100644 --- a/packages/browser/test/integration/helpers/mswjs/networkRecorder.js +++ b/packages/browser/test/integration/helpers/mswjs/networkRecorder.js @@ -32,6 +32,29 @@ class NetworkRecorder { constructor() { /** @type {NetworkCall[]} */ this.calls = []; + /** @type {{ pattern: RegExp, resolve: (call: NetworkCall) => void, timer: ReturnType }[]} */ + this.waiters = []; + } + + /** + * Resolves any pending waitForCall promises whose pattern matches a call that + * now has both a request and a response. Called from both capture methods so + * resolution does not depend on which of request:start / response:* settles + * last. + * @param {NetworkCall} call + */ + notifyWaiters(call) { + if (!call.request || !call.response) { + return; + } + this.waiters = this.waiters.filter((waiter) => { + if (waiter.pattern.test(call.request.url)) { + clearTimeout(waiter.timer); + waiter.resolve(call); + return false; + } + return true; + }); } /** @@ -72,6 +95,8 @@ class NetworkRecorder { timestamp: Date.now(), body, }; + + this.notifyWaiters(call); } /** @@ -112,6 +137,8 @@ class NetworkRecorder { body, timestamp: Date.now(), }; + + this.notifyWaiters(call); } /** @@ -172,7 +199,43 @@ class NetworkRecorder { return calls.length > 0 ? calls[0] : undefined; } + /** + * Resolves with the first complete call matching the pattern. Unlike findCall, + * this is event-driven: if no match exists yet it resolves the moment a + * matching response is captured, rather than retry-polling. This is the right + * tool for fire-and-forget requests (e.g. link clicks) where the triggering + * call does not return a promise to await. Resolves with undefined if no match + * arrives within timeoutMs, so callers can assert presence with toBeDefined(). + * @param {RegExp|string} pattern + * @param {Object} [options] + * @param {number} [options.timeoutMs=5000] + * @returns {Promise} + */ + waitForCall(pattern, { timeoutMs = 5000 } = {}) { + if (typeof pattern === "string") { + pattern = new RegExp(`/${pattern}/`, "i"); + } + + const existing = this.calls.find( + (call) => call.request && call.response && pattern.test(call.request.url), + ); + if (existing) { + return Promise.resolve(existing); + } + + return new Promise((resolve) => { + const waiter = { pattern, resolve, timer: undefined }; + waiter.timer = setTimeout(() => { + this.waiters = this.waiters.filter((w) => w !== waiter); + resolve(undefined); + }, timeoutMs); + this.waiters.push(waiter); + }); + } + reset() { + this.waiters.forEach((waiter) => clearTimeout(waiter.timer)); + this.waiters = []; this.calls = []; } } diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 22e752eef..161b33ab0 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -49,11 +49,11 @@ const interactCalls = (networkRecorder) => const firstEvent = (call) => call.request.body.events[0]; // Link clicks are fire-and-forget: clickLink returns before the request is sent, -// so findCall must outwait the click → event → fetch → MSW round-trip. The -// default (5 × 10ms) is too short under CI load; poll up to ~2s, returning as -// soon as the call completes. +// so there is no promise to await. waitForCall resolves the moment the matching +// response is captured (no polling), which findCall's short retry window can +// miss under CI load. const findInteractCall = (networkRecorder) => - networkRecorder.findCall(/v1\/interact/, { retries: 40, delayMs: 50 }); + networkRecorder.waitForCall(/v1\/interact/); // eslint-disable-next-line no-underscore-dangle const activityMap = (event) => From 268976434f1f4d7e1b7cd60c5b77e59f2461124a Mon Sep 17 00:00:00 2001 From: Carter McBride <18412686+carterworks@users.noreply.github.com> Date: Fri, 26 Jun 2026 14:31:15 -0600 Subject: [PATCH 38/38] test(integration): point C8118 at renamed identity-cookie handler Rebasing onto 00-infra collided 07's sendEventWithIdentityHandler with the identically named handler main added (used by platformServicesCookieWiring). 07's handler was renamed to sendEventWithIdentityCookieHandler during conflict resolution; the C8118 link-click test still referenced the old name. --- .../test/integration/specs/Data Collector/dataCollector.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js index 161b33ab0..6a5dbe367 100644 --- a/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js +++ b/packages/browser/test/integration/specs/Data Collector/dataCollector.spec.js @@ -754,7 +754,7 @@ describe("C8118 - Collects and sends link click information", () => { worker, networkRecorder, }) => { - worker.use(sendEventWithIdentityHandler); + worker.use(sendEventWithIdentityCookieHandler); await alloy("configure", { ...alloyConfig,