Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d578fb3
test(integration): migrate data collector functional specs to Vitest+…
carterworks Jun 12, 2026
be7f387
chore: apply prettier and eslint formatting
carterworks Jun 12, 2026
09b4673
Deslop the test file
carterworks Jun 16, 2026
9df8298
refactor(integration): replace hand-rolled waitUntil with expect.poll
carterworks Jun 16, 2026
afa8877
refactor(integration): centralize negative-assertion wait timeout
carterworks Jun 16, 2026
328d463
refactor(integration): extract DOM helpers and add cleanup
carterworks Jun 16, 2026
4f7e662
refactor(integration): extract interactCalls helper to deduplicate fi…
carterworks Jun 16, 2026
daf4951
refactor(integration): replace try/catch with rejects.toThrow
carterworks Jun 16, 2026
892ab37
refactor(integration): replace callbackInvoked booleans with vi.fn() …
carterworks Jun 16, 2026
bbe3b58
refactor(integration): add restoreMocks and remove manual mockRestore
carterworks Jun 16, 2026
62f1319
refactor(integration): remove redundant networkRecorder.reset() calls
carterworks Jun 16, 2026
3b1ec4e
fix(integration): restore C81183 getLinkDetails(null) assertion
carterworks Jun 16, 2026
d95d7c2
test(integration): add skip stubs for 5 dropped C81181 tests
carterworks Jun 16, 2026
d938f77
test(integration): migrate C455258 collect routing via sendBeacon rec…
carterworks Jun 18, 2026
3efde59
docs(integration): record why C9369211 Referer test stays skipped
carterworks Jun 18, 2026
049e087
docs(integration): tighten comments in dataCollector spec
carterworks Jun 18, 2026
111c2d1
test(integration): restore C2592 configOverrides assertion
carterworks Jun 18, 2026
5f5933b
test(integration): assert full webInteraction in C11693274
carterworks Jun 18, 2026
2019856
test(integration): assert full webInteraction in C81181 augment test
carterworks Jun 18, 2026
e44d43d
docs(integration): sharpen C9369211 skip rationale
carterworks Jun 18, 2026
a252692
refactor(integration): centralize test lifecycle in the alloy fixture
carterworks Jun 18, 2026
4f4af2a
refactor(integration): hoist C81183 monitor setup into beforeEach
carterworks Jun 18, 2026
7802a88
refactor(integration): tidy skip stubs and reuse waitFor
carterworks Jun 18, 2026
f8d3787
format
carterworks Jun 18, 2026
e51bbe7
test(integration): verify C455258 beacon URL and body
carterworks Jun 19, 2026
0e57080
test(integration): pin interactCalls to full edge host and path
carterworks Jun 19, 2026
f42ab13
test(integration): extend domHelpers for verbatim link ports
carterworks Jun 19, 2026
7a13277
test(integration): assert C225010 click still navigates to #foo
carterworks Jun 19, 2026
baf0339
test(integration): restore exact C81183 getLinkDetails assertions
carterworks Jun 19, 2026
50031bf
test(integration): assert eventType and activity map in C81181 augment
carterworks Jun 19, 2026
f7506a7
test(integration): implement the 5 dropped C81181 link-click tests
carterworks Jun 19, 2026
1519a38
test(integration): migrate the C8118 link-click suite
carterworks Jun 19, 2026
8237cf3
docs(integration): explain why the sendBeacon recorder must be global
carterworks Jun 19, 2026
2ed63e0
format
carterworks Jun 19, 2026
e2d5bdd
test(integration): pin full webPageDetails in C8118 cached test
carterworks Jun 19, 2026
c03cbf1
test(integration): poll longer for fire-and-forget link-click requests
carterworks Jun 19, 2026
575b7e5
test(integration): wait for link-click requests by event, not polling
carterworks Jun 19, 2026
2689764
test(integration): point C8118 at renamed identity-cookie handler
carterworks Jun 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/browser/test/integration/helpers/alloy/clean.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default () => {
window.__alloyClickListeners = [];
}
delete window.__alloyMonitors;
delete window.___getLinkDetails;
delete window.__alloyNS;
delete window.alloy;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -19,6 +20,12 @@ export default async () => {
`${server.config.root}/packages/browser/distTest/baseCode.min.js`,
);

// 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";

// Monkeypatch document.addEventListener once per page lifetime to track click listeners.
Expand Down
22 changes: 22 additions & 0 deletions packages/browser/test/integration/helpers/mswjs/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/,

Expand Down
63 changes: 63 additions & 0 deletions packages/browser/test/integration/helpers/mswjs/networkRecorder.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,29 @@ class NetworkRecorder {
constructor() {
/** @type {NetworkCall[]} */
this.calls = [];
/** @type {{ pattern: RegExp, resolve: (call: NetworkCall) => void, timer: ReturnType<typeof setTimeout> }[]} */
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;
});
}

/**
Expand Down Expand Up @@ -72,6 +95,8 @@ class NetworkRecorder {
timestamp: Date.now(),
body,
};

this.notifyWaiters(call);
}

/**
Expand Down Expand Up @@ -112,6 +137,8 @@ class NetworkRecorder {
body,
timestamp: Date.now(),
};

this.notifyWaiters(call);
}

/**
Expand Down Expand Up @@ -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<NetworkCall | undefined>}
*/
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 = [];
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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
],
Expand Down
65 changes: 65 additions & 0 deletions packages/browser/test/integration/helpers/utils/domHelpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
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;
};

/**
* 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 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();
};

/**
* 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;
};
46 changes: 46 additions & 0 deletions packages/browser/test/integration/helpers/utils/sendBeacon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
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.
*/

// 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) {
return;
}
window.__alloySendBeaconInstalled = true;
window.__alloySendBeaconCalls = [];
window.navigator.sendBeacon = (url, data) => {
window.__alloySendBeaconCalls.push({ url, data });
return true;
};
};

export const sendBeaconCalls = () => window.__alloySendBeaconCalls ?? [];

export const resetSendBeaconCalls = () => {
window.__alloySendBeaconCalls = [];
};
Loading
Loading