diff --git a/packages/browser/test/FUNCTIONAL_MIGRATION_PLAN.md b/packages/browser/test/FUNCTIONAL_MIGRATION_PLAN.md new file mode 100644 index 000000000..319fadcf6 --- /dev/null +++ b/packages/browser/test/FUNCTIONAL_MIGRATION_PLAN.md @@ -0,0 +1,195 @@ +# Functional → Integration Test Migration Plan + +Migrating the atrophied TestCafe **functional** suite +(`packages/browser/test/functional`) into the Vitest + Playwright + MSW +**integration** suite (`packages/browser/test/integration`). + +Status: **migration in progress — all 18 categories have integration spec files.** + +### Current integration test counts (2026-06-11) +- **43 integration spec files** across all categories +- **163 passing, ~29 failing (being fixed), 39 skipped** +- All 170 functional specs have been assessed; ~130 translated to integration tests, + the remainder skipped with documented rationale (page-reload, Visitor.js dependency, + live-edge-only, shadow DOM, known baseline failures). + +### New handlers / fixtures added +- `handlers.js`: `acquireHandler` (identity/acquire endpoint) +- `mocks/acquireResponse.json`, `identityAcquireResponse.json` +- `mocks/sendEventWithIdentityCookieResponse.json` +- `mocks/personalizationFormBasedResponse.json`, `personalizationSetHtmlResponse.json`, `personalizationSpaResponse.json` +- `helpers/constants/consent.js`: consent constants (CONSENT_IN/OUT, ADOBE2_IN/OUT, IAB_*) +- `helpers/utils/legacyCookies.js`: AMCV cookie helpers for migration tests + +--- + +## 1. How to run the functional suite (parity baseline) + +The functional tests *do* run; they were just hard to invoke in a fresh/VPN'd +environment. Two gotchas, both solved: + +| Symptom | Cause | Fix | +| --- | --- | --- | +| All edge tests fail instantly, `Network request failed` | `edge.adobedc.net` was DNS‑blackholed to `0.0.0.0` off‑VPN | Connect to VPN (edge now resolves to a real IP) | +| `browser disconnected` at ~6 min / `page.goto Timeout` to a `192.168.x.x` URL | TestCafe binds its browser proxy to the machine's LAN IP, which the headless browser can't reach (worse under VPN) | Pass **`--hostname localhost`** | + +**Canonical command** (run from `packages/browser`): + +```bash +EDGE_BASE_PATH="ee-pre-prd" ALLOY_ENV="int" \ + npx testcafe --hostname localhost playwright:chromium:headless \ + "test/functional/specs/**/*.js" +``` + +Verified: `Command Logic` category = **12 passed in 4s**; the edge‑dependent +`C11634155` (sendEvent + edge assertion) = **3 passed in 2s**. + +### Full-suite baseline (VPN on, `--hostname localhost`) + +**245 tests → 228 passed, 17 failed, 16 skipped (4m09s).** The 17 failures are +the "before" state — parity work must account for these (some are likely +atrophied/flaky, some env-specific like the `collect` endpoint / `RequestMock +CORS validation failed`): + +- BrandConcierge `sendConversationalEvent` ×5 (`C2590433–437`) — `ClientFunction` errors +- Collect endpoint: `C455258`, `C8118`, `C9369211` +- Identity/ECID/cookie/protobuf ×4 (CORE identity from cookie, ECID after collect beacon, base64/protobuf fallbacks) +- `C2589` getLibraryInfo +- `C21886916` shadow-DOM click tracking +- Personalization `C5298194` ×2, `C5805676` + +> Triage these before/while migrating their categories — don't blindly port a +> failing test. Re-run in CI for an authoritative baseline. + +> The package script `pnpm test:functional` should be updated to add +> `--hostname localhost` so it works regardless of network interface. + +### One pre-existing breakage already fixed +`functional/specs/Personalization/C17409728.js` imported +`createDecorateProposition.js` and `initDomActionsModules.js` from +`core/src/...`, but that whole Personalization component **moved to +`browser/src/...`**. This was a hard compile error that aborted the *entire* +TestCafe run. Imports repointed to `../../../../src/...` (constants such as +`decisionProvider`/`propositionInteractionType` legitimately remain in `core`). + +--- + +## 2. Architecture: functional vs integration + +| Concern | Functional (TestCafe) | Integration (Vitest + Playwright + MSW) | +| --- | --- | --- | +| Runner | `testcafe` | `vitest` browser mode (`@vitest/browser-playwright`) | +| Test code location | runs in **Node**, marshals into browser via `ClientFunction`/`t.eval` | runs **in the browser** directly | +| Page under test | remote `https://alloyio.com/functional-test/testPage.html` | blank Vitest page; library injected via `setupBaseCode` + `setupAlloy` | +| Alloy command call | `createAlloyProxy()` marshalling wrapper | `window.alloy("command", opts)` directly | +| Network | **live int edge** (`edge.adobedc.net`) | **MSW mocks** (`helpers/mswjs/handlers.js`) | +| Request inspection | `RequestLogger` / `networkLogger` | `networkRecorder.findCall(pattern)` | +| Response control | whatever the live edge returns | deterministic JSON fixtures in `helpers/mocks/` | +| Console assertions | `createConsoleLogger().warn.expectMessageMatching` | `vi.spyOn(console, …)` + `searchForLogMessage` | +| Setup/teardown | `createFixture` per file | auto fixtures in `helpers/testsSetup/extend.js` (`worker`, `networkRecorder`, `alloy`) | + +**Key consequence:** integration tests are *hermetic* — no live edge, no VPN, +deterministic. This is the entire reason for the migration. The functional +suite remains the **behavioral source of truth**; we translate intent, not +transport. + +--- + +## 3. Parity matrix (functional categories → existing integration coverage) + +170 functional spec files. ~150 (88%) touch the edge and need MSW mocks. + +| Functional category | # specs | Integration today | Gap | +| --- | ---: | --- | --- | +| Audiences | 3 | `Audiences/` (2) | small | +| BrandConcierge | 1 | — | full | +| CNAME | 1 | `CNAME/cname` (1) | likely done — verify | +| Command Logic | 10 | `Command Logic/` (7) | mostly done — verify gaps | +| Config Overrides | 4 | `Command Logic/configOverrides` (1) | partial | +| Consent | 32 | — | **full (largest gap)** | +| Context | 7 | — | full | +| Data Collector | 13 | `Advertising/` (7)* | partial — map carefully | +| ID Migration | 7 | — | full | +| Identity | 20 | `Personalization/identityMapPersistence`* | mostly full | +| Install SDK | 3 | — | full | +| LibraryInfo | 1 | — | full | +| Location Hints | 2 | — | full | +| Logging | 4 | — | full | +| MediaCollection | 3 | `StreamingMedia/mediaEvents` (1) | partial | +| Migration | 9 | — | full | +| Personalization | 43 | `Personalization/applyPropositions`, `Target/`, `AJO/` (4)* | **partial (largest category)** | +| RulesEngine | 3 | — | full | +| Visitor | 4 | — | full | + +\* Integration uses topic-oriented folders (`Advertising`, `AJO`, `Target`, +`StreamingMedia`) that don't map 1:1 to functional folder names. A first task is +a **spec-level** crosswalk (by test-case ID / behavior), not folder-level. + +--- + +## 4. Helper translation layer (build once, reuse everywhere) + +Before bulk migration, port the functional helper idioms to integration +equivalents so specs translate mechanically. Most already exist; the gaps: + +| Functional helper | Integration equivalent | Action | +| --- | --- | --- | +| `createAlloyProxy()` (`alloy.sendEvent(opts)`) | direct `window.alloy("sendEvent", opts)` | none — inline | +| `…Async` / `…ErrorMessage` proxy variants | `await`/`try-catch` on the real promise | document pattern | +| `createConsoleLogger().lvl.expectMessageMatching` | `vi.spyOn(console,lvl)` + `searchForLogMessage` | extend `searchForLogMessage` to cover regex + "no message" cases | +| `networkLogger.Logs` (RequestLogger) | `networkRecorder.findCall(regex)` | none — exists | +| `responseStatus(requests, [200,207])` assertion | assert on `call.response.status` | port `assertions/responseStatus` | +| `createCollectEndpointAsserter` | — | port if any migrated spec needs `/collect` | +| `assertions/advertising.js` | — | port for Data Collector/Advertising specs | +| `constants/configParts/*` (compose, orgMainConfigMain, debugEnabled, consent…) | `helpers/alloy/config.js` (single object) | port the `configParts` building blocks the specs actually use | +| `cookies.js`, `setLegacyIdentityCookie`, `createAdobeMC` | `helpers/utils/deleteCookies` + new | port for Identity/Migration/Visitor | +| `dom/addHtmlToBody`, `preventLinkNavigation` | DOM is real in browser mode | port small DOM utils for Personalization | +| MSW response fixtures (`helpers/mocks/*.json`) | — | **biggest new work**: capture/author a fixture per edge behavior | + +**MSW fixtures are the crux.** Each edge‑dependent spec needs a handler + +response fixture that reproduces the relevant slice of the live response +(destinations, propositions, consent handles, identity, media, etc.). Strategy: +use `networkRecorder` against the *live* functional run (now that it works) to +capture real int responses, then sanitize them into `helpers/mocks/`. + +--- + +## 5. Execution sequence + +Per your direction, full plan first; then (separately) a **pilot category** +before bulk work. + +1. **Baseline capture** (in progress): run the full functional suite with the + canonical command; record pass/fail per spec as the "before" state. Re-run in + CI for an authoritative baseline. +2. **Crosswalk**: produce a spec-level map (functional file → existing + integration spec or "to migrate"), so we don't re-migrate already-covered + behavior (Command Logic, CNAME, parts of Audiences/Personalization). +3. **Helper layer**: port the helpers in §4; add a fixture-capture script that + drives the live edge via `networkRecorder` and writes sanitized mocks. +4. **Pilot**: migrate one self-contained category end-to-end for review. + Recommend **Consent** (32 specs, zero integration coverage, well-bounded + behavior, exercises set-consent + queueing — proves the fixture pipeline) or + **Command Logic** (finish the last 3 — fastest win, validates the pattern). +5. **Scale** category-by-category, largest-gap first: Consent → Identity → + Personalization → Data Collector → Migration → Context → the long tail. +6. **Decommission**: once a category reaches parity, delete the functional + specs + now-unused functional helpers; update `pnpm test:functional` / + `.testcaferc.json` and CI. Remove TestCafe deps when the last spec is gone. + +--- + +## 6. Open questions / risks + +- **Fixture fidelity**: live int responses vary (timestamps, ECIDs, ordering). + Fixtures must capture only assertion-relevant fields; over-fitting causes + brittle tests. Need a sanitization convention. +- **Behaviors that need a real edge**: some specs may assert on round-trip + semantics hard to mock faithfully (e.g., real identity stitching, CNAME TLS). + Flag these; a few may justifiably *stay* functional or move to a thin + smoke-suite. +- **Already-migrated coverage**: confirm the existing integration specs truly + cover their functional counterparts before deleting anything. +- **Topic vs ID folder naming**: agree on integration folder/naming convention + (keep topic-oriented like `Advertising`/`AJO`, or mirror functional categories). + diff --git a/packages/browser/test/functional/specs/Personalization/C17409728.js b/packages/browser/test/functional/specs/Personalization/C17409728.js index 2eca14dbe..2e7f67e8c 100644 --- a/packages/browser/test/functional/specs/Personalization/C17409728.js +++ b/packages/browser/test/functional/specs/Personalization/C17409728.js @@ -25,14 +25,14 @@ import addHtmlToBody from "../../helpers/dom/addHtmlToBody.js"; import { CLICK_LABEL_DATA_ATTRIBUTE, INTERACT_ID_DATA_ATTRIBUTE, -} from "../../../../../core/src/components/Personalization/handlers/createDecorateProposition.js"; +} from "../../../../src/components/Personalization/handlers/createDecorateProposition.js"; import { ADOBE_JOURNEY_OPTIMIZER } from "../../../../../core/src/constants/decisionProvider.js"; import { ALWAYS, DECORATED_ELEMENTS_ONLY, NEVER, } from "../../../../../core/src/constants/propositionInteractionType.js"; -import { DOM_ACTION_COLLECT_INTERACTIONS } from "../../../../../core/src/components/Personalization/dom-actions/initDomActionsModules.js"; +import { DOM_ACTION_COLLECT_INTERACTIONS } from "../../../../src/components/Personalization/dom-actions/initDomActionsModules.js"; /* eslint-enable import/no-relative-packages */ import { responseStatus } from "../../helpers/assertions/index.js"; diff --git a/packages/browser/test/integration/helpers/alloy/clean.js b/packages/browser/test/integration/helpers/alloy/clean.js index 5c98f1e92..bcfb1ab8f 100644 --- a/packages/browser/test/integration/helpers/alloy/clean.js +++ b/packages/browser/test/integration/helpers/alloy/clean.js @@ -10,6 +10,12 @@ governing permissions and limitations under the License. */ export default () => { + if (window.__alloyClickListeners) { + window.__alloyClickListeners.forEach(({ handler, rest }) => { + document.removeEventListener("click", handler, ...rest); + }); + window.__alloyClickListeners = []; + } delete window.__alloyMonitors; 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 eb29b9d1c..bd041474d 100644 --- a/packages/browser/test/integration/helpers/alloy/setupBaseCode.js +++ b/packages/browser/test/integration/helpers/alloy/setupBaseCode.js @@ -21,6 +21,21 @@ export default async () => { document.body.innerHTML = "Alloy Test Page"; + // Monkeypatch document.addEventListener once per page lifetime to track click listeners. + // Paired with helpers/alloy/clean.js, which removes stale alloy click listeners between + // tests to prevent cross-test leakage. The patch is intentionally permanent (never restored) + // and only tracks "click" events — all other event types are passed through untouched. + if (!window.__alloyClickListeners) { + window.__alloyClickListeners = []; + const originalAddEventListener = document.addEventListener.bind(document); + document.addEventListener = (type, handler, ...rest) => { + if (type === "click") { + window.__alloyClickListeners.push({ handler, rest }); + } + return originalAddEventListener(type, handler, ...rest); + }; + } + const alloyBaseCodeScriptTag = document.createElement("script"); alloyBaseCodeScriptTag.textContent = alloyBaseCode; diff --git a/packages/browser/test/integration/helpers/constants/consent.js b/packages/browser/test/integration/helpers/constants/consent.js new file mode 100644 index 000000000..7328dd138 --- /dev/null +++ b/packages/browser/test/integration/helpers/constants/consent.js @@ -0,0 +1,146 @@ +/* +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. +*/ +export const CONSENT_IN = { + consent: [ + { + standard: "Adobe", + version: "1.0", + value: { + general: "in", + }, + }, + ], +}; + +export const CONSENT_OUT = { + consent: [ + { + standard: "Adobe", + version: "1.0", + value: { + general: "out", + }, + }, + ], +}; + +export const IAB_CONSENT_IN = { + consent: [ + { + standard: "IAB TCF", + version: "2.0", + value: "CO052l-O052l-DGAMBFRACBgAIBAAAAAAIYgEawAQEagAAAA", + gdprApplies: true, + }, + ], +}; + +export const IAB_CONSENT_IN_PERSONAL_DATA = { + consent: [ + { + standard: "IAB TCF", + version: "2.0", + value: "CO052l-O052l-DGAMBFRACBgAIBAAAAAAIYgEawAQEagAAAA", + gdprApplies: true, + gdprContainsPersonalData: true, + }, + ], +}; + +export const IAB_CONSENT_IN_NO_GDPR = { + consent: [ + { + standard: "IAB TCF", + version: "2.0", + value: "CO052l-O052l-DGAMBFRACBgAIBAAAAAAIYgEawAQEagAAAA", + gdprApplies: false, + }, + ], +}; + +export const IAB_NO_PURPOSE_ONE = { + consent: [ + { + standard: "IAB TCF", + version: "2.0", + value: "CO052oTO052oTDGAMBFRACBgAABAAAAAAIYgEawAQEagAAAA", + gdprApplies: true, + }, + ], +}; + +export const IAB_NO_PURPOSE_ONE_NO_GDPR = { + consent: [ + { + standard: "IAB TCF", + version: "2.0", + value: "CO052oTO052oTDGAMBFRACBgAABAAAAAAIYgEawAQEagAAAA", + gdprApplies: false, + }, + ], +}; + +export const IAB_NO_PURPOSE_TEN = { + consent: [ + { + standard: "IAB TCF", + version: "2.0", + value: "CO052kIO052kIDGAMBFRACBgAIAAAAAAAIYgEawAQEagAAAA", + gdprApplies: true, + }, + ], +}; + +export const IAB_NO_ADOBE_VENDOR = { + consent: [ + { + standard: "IAB TCF", + version: "2.0", + value: "CO052qdO052qdDGAMBFRACBgAIBAAAAAAIYgAAoAAAAA", + gdprApplies: true, + }, + ], +}; + +export const ADOBE2_IN = { + consent: [ + { + standard: "Adobe", + version: "2.0", + value: { + collect: { + val: "y", + }, + metadata: { + time: "2019-01-01T15:52:25+00:00", + }, + }, + }, + ], +}; + +export const ADOBE2_OUT = { + consent: [ + { + standard: "Adobe", + version: "2.0", + value: { + collect: { + val: "n", + }, + metadata: { + time: "2019-01-01T15:52:25+00:00", + }, + }, + }, + ], +}; diff --git a/packages/browser/test/integration/helpers/mocks/acquireResponse.json b/packages/browser/test/integration/helpers/mocks/acquireResponse.json new file mode 100644 index 000000000..9a9c792a2 --- /dev/null +++ b/packages/browser/test/integration/helpers/mocks/acquireResponse.json @@ -0,0 +1,26 @@ +{ + "requestId": "acquire-request-id-1234", + "handle": [ + { + "payload": [ + { + "id": "41861666193140161934276845651148876988", + "namespace": { + "code": "ECID" + } + } + ], + "type": "identity:result" + }, + { + "payload": [ + { + "key": "kndctr_5BFE274A5F6980A50A495C08_AdobeOrg_cluster", + "value": "or2", + "maxAge": 1800 + } + ], + "type": "state:store" + } + ] +} diff --git a/packages/browser/test/integration/helpers/mocks/identityAcquireResponse.json b/packages/browser/test/integration/helpers/mocks/identityAcquireResponse.json new file mode 100644 index 000000000..feb3dc251 --- /dev/null +++ b/packages/browser/test/integration/helpers/mocks/identityAcquireResponse.json @@ -0,0 +1,54 @@ +{ + "requestId": "f0e1d2c3-b4a5-9687-fedc-ba9876543210", + "handle": [ + { + "payload": [ + { + "id": "41861666193140161934276845651148876988", + "namespace": { + "code": "ECID" + } + } + ], + "type": "identity:result" + }, + { + "payload": [ + { + "scope": "Target", + "hint": "35", + "ttlSeconds": 1800 + }, + { + "scope": "AAM", + "hint": "9", + "ttlSeconds": 1800 + }, + { + "scope": "EdgeNetwork", + "hint": "or2", + "ttlSeconds": 1800 + } + ], + "type": "locationHint:result" + }, + { + "payload": [ + { + "key": "kndctr_5BFE274A5F6980A50A495C08_AdobeOrg_identity", + "value": "CiY0MTg2MTY2NjE5MzE0MDE2MTkzNDI3Njg0NTY1MTE0ODg3Njk4OFIQCM68vcXoMhgBKgNPUjIwAaAB0ry9xegysAHCqAHwAc68vcXoMg==", + "maxAge": 34128000, + "attrs": { + "SameSite": "None" + } + }, + { + "key": "kndctr_5BFE274A5F6980A50A495C08_AdobeOrg_cluster", + "value": "or2", + "maxAge": 1800 + } + ], + "type": "state:store" + } + ] +} diff --git a/packages/browser/test/integration/helpers/mocks/personalizationFormBasedResponse.json b/packages/browser/test/integration/helpers/mocks/personalizationFormBasedResponse.json new file mode 100644 index 000000000..66636ec43 --- /dev/null +++ b/packages/browser/test/integration/helpers/mocks/personalizationFormBasedResponse.json @@ -0,0 +1,99 @@ +{ + "requestId": "pers-form-based-001-requestid", + "handle": [ + { + "payload": [ + { + "id": "56475161841051406291527557158775615545", + "namespace": { + "code": "ECID" + } + } + ], + "type": "identity:result" + }, + { + "payload": [ + { + "id": "AT:eyJhY3Rpdml0eUlkIjoiMTI2NDg2IiwiZXhwZXJpZW5jZUlkIjoiMCJ9", + "scope": "__view__", + "scopeDetails": { + "decisionProvider": "TGT", + "activity": { + "id": "126486" + }, + "experience": { + "id": "0" + }, + "characteristics": { + "eventToken": "page-wide-event-token" + }, + "correlationID": "126486:0:0" + }, + "items": [ + { + "id": "0", + "schema": "https://ns.adobe.com/personalization/default-content-item", + "meta": {} + } + ] + }, + { + "id": "AT:eyJhY3Rpdml0eUlkIjoiMTI2NDg2IiwiZXhwZXJpZW5jZUlkIjoiMCJ9", + "scope": "alloy-test-scope-1", + "scopeDetails": { + "decisionProvider": "TGT", + "activity": { + "id": "126486" + }, + "experience": { + "id": "0" + }, + "characteristics": { + "eventToken": "form-based-event-token" + }, + "correlationID": "126486:0:0" + }, + "items": [ + { + "id": "126486-item", + "schema": "https://ns.adobe.com/personalization/html-content-item", + "data": { + "content": "

welcome to TARGET AWESOME WORLD!!!

", + "format": "text/html", + "id": "126486-item" + } + } + ] + } + ], + "type": "personalization:decisions", + "eventIndex": 0 + }, + { + "payload": [ + { + "scope": "Target", + "hint": "35", + "ttlSeconds": 1800 + }, + { + "scope": "EdgeNetwork", + "hint": "or2", + "ttlSeconds": 1800 + } + ], + "type": "locationHint:result" + }, + { + "payload": [ + { + "key": "kndctr_5BFE274A5F6980A50A495C08_AdobeOrg_cluster", + "value": "or2", + "maxAge": 1800 + } + ], + "type": "state:store" + } + ] +} diff --git a/packages/browser/test/integration/helpers/mocks/personalizationSetHtmlResponse.json b/packages/browser/test/integration/helpers/mocks/personalizationSetHtmlResponse.json new file mode 100644 index 000000000..a4fc6cf04 --- /dev/null +++ b/packages/browser/test/integration/helpers/mocks/personalizationSetHtmlResponse.json @@ -0,0 +1,84 @@ +{ + "requestId": "pers-sethtml-001-requestid", + "handle": [ + { + "payload": [ + { + "id": "56475161841051406291527557158775615545", + "namespace": { + "code": "ECID" + } + } + ], + "type": "identity:result" + }, + { + "payload": [ + { + "id": "AT:eyJhY3Rpdml0eUlkIjoiMTAwMDAxIiwiZXhwZXJpZW5jZUlkIjoiMCJ9", + "scope": "__view__", + "scopeDetails": { + "decisionProvider": "TGT", + "activity": { + "id": "100001" + }, + "experience": { + "id": "0" + }, + "characteristics": { + "eventToken": "sethtml-event-token" + }, + "correlationID": "100001:0:0" + }, + "items": [ + { + "id": "100001-item", + "schema": "https://ns.adobe.com/personalization/dom-action", + "meta": { + "activity.id": "100001", + "activity.name": "Integration Test: setHtml", + "experience.id": "0", + "offer.id": "100001-item", + "offer.name": "setHtml offer" + }, + "data": { + "type": "setHtml", + "format": "application/vnd.adobe.target.dom-action", + "content": "
Personalized Content
", + "selector": "#target-container", + "prehidingSelector": "#target-container" + } + } + ] + } + ], + "type": "personalization:decisions", + "eventIndex": 0 + }, + { + "payload": [ + { + "scope": "Target", + "hint": "35", + "ttlSeconds": 1800 + }, + { + "scope": "EdgeNetwork", + "hint": "or2", + "ttlSeconds": 1800 + } + ], + "type": "locationHint:result" + }, + { + "payload": [ + { + "key": "kndctr_5BFE274A5F6980A50A495C08_AdobeOrg_cluster", + "value": "or2", + "maxAge": 1800 + } + ], + "type": "state:store" + } + ] +} diff --git a/packages/browser/test/integration/helpers/mocks/personalizationSpaResponse.json b/packages/browser/test/integration/helpers/mocks/personalizationSpaResponse.json new file mode 100644 index 000000000..cfcf29af6 --- /dev/null +++ b/packages/browser/test/integration/helpers/mocks/personalizationSpaResponse.json @@ -0,0 +1,139 @@ +{ + "requestId": "pers-spa-001-requestid", + "handle": [ + { + "payload": [ + { + "id": "56475161841051406291527557158775615545", + "namespace": { + "code": "ECID" + } + } + ], + "type": "identity:result" + }, + { + "payload": [ + { + "id": "AT:pagewide-spa-proposition", + "scope": "__view__", + "scopeDetails": { + "decisionProvider": "TGT", + "activity": { + "id": "200001" + }, + "experience": { + "id": "0" + }, + "characteristics": { + "eventToken": "pagewide-spa-event-token" + }, + "correlationID": "200001:0:0" + }, + "items": [ + { + "id": "200001-pagewide-item", + "schema": "https://ns.adobe.com/personalization/dom-action", + "data": { + "type": "setHtml", + "format": "application/vnd.adobe.target.dom-action", + "content": "test for a page wide scope", + "selector": "#pageWideScope", + "prehidingSelector": "#pageWideScope" + } + } + ] + }, + { + "id": "AT:products-view-proposition", + "scope": "products", + "scopeDetails": { + "decisionProvider": "TGT", + "activity": { + "id": "200001" + }, + "experience": { + "id": "0" + }, + "characteristics": { + "eventToken": "products-view-event-token", + "scopeType": "view" + }, + "correlationID": "200001:0:0" + }, + "items": [ + { + "id": "200001-products-item", + "schema": "https://ns.adobe.com/personalization/dom-action", + "data": { + "type": "setHtml", + "format": "application/vnd.adobe.target.dom-action", + "content": "This is product view", + "selector": "#personalization-products-container", + "prehidingSelector": "#personalization-products-container" + } + } + ] + }, + { + "id": "AT:cart-view-proposition", + "scope": "cart", + "scopeDetails": { + "decisionProvider": "TGT", + "activity": { + "id": "200001" + }, + "experience": { + "id": "0" + }, + "characteristics": { + "eventToken": "cart-view-event-token", + "scopeType": "view" + }, + "correlationID": "200001:0:0" + }, + "items": [ + { + "id": "200001-cart-item", + "schema": "https://ns.adobe.com/personalization/dom-action", + "data": { + "type": "setHtml", + "format": "application/vnd.adobe.target.dom-action", + "content": "This is cart view", + "selector": "#personalization-cart-container", + "prehidingSelector": "#personalization-cart-container" + } + } + ] + } + ], + "type": "personalization:decisions", + "eventIndex": 0 + }, + { + "payload": [ + { + "scope": "Target", + "hint": "35", + "ttlSeconds": 1800 + }, + { + "scope": "EdgeNetwork", + "hint": "or2", + "ttlSeconds": 1800 + } + ], + "type": "locationHint:result" + }, + { + "payload": [ + { + "key": "kndctr_5BFE274A5F6980A50A495C08_AdobeOrg_cluster", + "value": "or2", + "maxAge": 1800 + } + ], + "type": "state:store" + } + ] +} diff --git a/packages/browser/test/integration/helpers/mocks/rulesEngineEvaluateResponse.json b/packages/browser/test/integration/helpers/mocks/rulesEngineEvaluateResponse.json new file mode 100644 index 000000000..0d442cdf5 --- /dev/null +++ b/packages/browser/test/integration/helpers/mocks/rulesEngineEvaluateResponse.json @@ -0,0 +1,137 @@ +{ + "requestId": "rules-engine-evaluate-test-fixture", + "handle": [ + { + "payload": [ + { + "id": "rules-engine-evaluate-payload", + "scope": "web://testing.alloy.adobe.com/", + "scopeDetails": { + "decisionProvider": "AJO", + "correlationID": "rules-engine-evaluate-correlation", + "characteristics": { + "eventToken": "rules-engine-evaluate-token" + }, + "rank": 1, + "activity": { + "id": "rules-engine-evaluate-activity", + "priority": 0, + "matchedSurfaces": ["web://testing.alloy.adobe.com/"] + } + }, + "items": [ + { + "id": "rules-engine-evaluate-item", + "schema": "https://ns.adobe.com/personalization/ruleset-item", + "data": { + "version": 1, + "rules": [ + { + "condition": { + "definition": { + "conditions": [ + { + "definition": { + "key": "~type", + "matcher": "eq", + "values": ["com.adobe.eventType.rulesEngine"] + }, + "type": "matcher" + } + ], + "logic": "and" + }, + "type": "group" + }, + "consequences": [ + { + "id": "rules-engine-evaluate-consequence", + "type": "schema", + "detail": { + "id": "rules-engine-evaluate-consequence", + "schema": "https://ns.adobe.com/personalization/message/in-app", + "data": { + "content": "
Rules Engine Test
", + "contentType": "text/html", + "mobileParameters": { + "verticalAlign": "center", + "dismissAnimation": "bottom", + "verticalInset": 0, + "backdropOpacity": 0.2, + "cornerRadius": 15, + "gestures": {}, + "horizontalInset": 0, + "uiTakeover": true, + "horizontalAlign": "center", + "width": 55, + "displayAnimation": "bottom", + "backdropColor": "#000000", + "height": 55 + }, + "webParameters": { + "alloy-content-iframe": { + "style": { + "border": "none", + "height": "100%", + "width": "100%" + }, + "params": { + "enabled": true, + "insertionMethod": "appendChild", + "parentElement": "#alloy-messaging-container" + } + }, + "alloy-messaging-container": { + "style": { + "backgroundColor": "#000000", + "border": "none", + "borderRadius": "15px", + "height": "55vh", + "overflow": "hidden", + "position": "fixed", + "width": "55%", + "left": "50%", + "transform": "translateX(-50%) translateY(-50%)", + "top": "50%" + }, + "params": { + "enabled": true, + "insertionMethod": "appendChild", + "parentElement": "body" + } + }, + "alloy-overlay-container": { + "style": { + "position": "fixed", + "top": "0", + "left": "0", + "width": "100%", + "height": "100%", + "background": "transparent", + "opacity": 0.2, + "backgroundColor": "#000000" + }, + "params": { + "enabled": true, + "insertionMethod": "appendChild", + "parentElement": "body" + } + } + }, + "publishedDate": 1746806494 + } + } + } + ] + } + ] + } + } + ] + } + ], + "type": "personalization:decisions", + "eventIndex": 0 + } + ] +} diff --git a/packages/browser/test/integration/helpers/mocks/sendEventWithIdentityCookieResponse.json b/packages/browser/test/integration/helpers/mocks/sendEventWithIdentityCookieResponse.json new file mode 100644 index 000000000..9b2bda514 --- /dev/null +++ b/packages/browser/test/integration/helpers/mocks/sendEventWithIdentityCookieResponse.json @@ -0,0 +1,54 @@ +{ + "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "handle": [ + { + "payload": [ + { + "id": "41861666193140161934276845651148876988", + "namespace": { + "code": "ECID" + } + } + ], + "type": "identity:result" + }, + { + "payload": [ + { + "scope": "Target", + "hint": "35", + "ttlSeconds": 1800 + }, + { + "scope": "AAM", + "hint": "9", + "ttlSeconds": 1800 + }, + { + "scope": "EdgeNetwork", + "hint": "or2", + "ttlSeconds": 1800 + } + ], + "type": "locationHint:result" + }, + { + "payload": [ + { + "key": "kndctr_5BFE274A5F6980A50A495C08_AdobeOrg_identity", + "value": "CiY0MTg2MTY2NjE5MzE0MDE2MTkzNDI3Njg0NTY1MTE0ODg3Njk4OFIQCM68vcXoMhgBKgNPUjIwAaAB0ry9xegysAHCqAHwAc68vcXoMg==", + "maxAge": 34128000, + "attrs": { + "SameSite": "None" + } + }, + { + "key": "kndctr_5BFE274A5F6980A50A495C08_AdobeOrg_cluster", + "value": "or2", + "maxAge": 1800 + } + ], + "type": "state:store" + } + ] +} diff --git a/packages/browser/test/integration/helpers/mswjs/handlers.js b/packages/browser/test/integration/helpers/mswjs/handlers.js index 3bc1c4727..6c559cfd6 100644 --- a/packages/browser/test/integration/helpers/mswjs/handlers.js +++ b/packages/browser/test/integration/helpers/mswjs/handlers.js @@ -24,7 +24,10 @@ export const sendEventHandler = http.post( const url = new URL(req.request.url); const configId = url.searchParams.get("configId"); - if (configId === "bc1a10e0-aee4-4e0e-ac5b-cdbb9abbec83") { + if ( + configId && + configId.startsWith("bc1a10e0-aee4-4e0e-ac5b-cdbb9abbec83") + ) { return HttpResponse.text( await readFile( `${server.config.root}/packages/browser/test/integration/helpers/mocks/sendEventResponse.json`, @@ -203,7 +206,38 @@ export const setConsentHandler = http.post( const url = new URL(req.request.url); const configId = url.searchParams.get("configId"); - if (configId === "bc1a10e0-aee4-4e0e-ac5b-cdbb9abbec83") { + if ( + configId && + configId.startsWith("bc1a10e0-aee4-4e0e-ac5b-cdbb9abbec83") + ) { + const body = await req.request.json().catch(() => ({})); + const consentOptions = body?.consent ?? []; + + const IAB_OUT_STRINGS = [ + "CO052oTO052oTDGAMBFRACBgAABAAAAAAIYgEawAQEagAAAA", // no Purpose 1 + "CO052qdO052qdDGAMBFRACBgAIBAAAAAAIYgAAoAAAAA", // no Adobe vendor + ]; + + let generalConsent = "in"; + for (const option of consentOptions) { + if (option.standard === "Adobe" && option.version === "1.0") { + if (option.value?.general === "out") { + generalConsent = "out"; + } + } else if (option.standard === "Adobe" && option.version === "2.0") { + if (option.value?.collect?.val === "n") { + generalConsent = "out"; + } + } else if (option.standard === "IAB TCF") { + if ( + option.gdprApplies !== false && + IAB_OUT_STRINGS.includes(option.value) + ) { + generalConsent = "out"; + } + } + } + return HttpResponse.json({ requestId: "consent-request-id", handle: [ @@ -212,7 +246,7 @@ export const setConsentHandler = http.post( payload: [ { key: "kndctr_5BFE274A5F6980A50A495C08_AdobeOrg_consent", - value: "general=in", + value: `general=${generalConsent}`, maxAge: 15552000, }, ], @@ -225,6 +259,29 @@ export const setConsentHandler = http.post( }, ); +export const acquireHandler = http.post( + /https:\/\/edge\.adobedc\.net\/ee\/.*\/?v1\/identity\/acquire/, + + async (req) => { + const url = new URL(req.request.url); + const configId = url.searchParams.get("configId"); + + if ( + configId && + (configId === "bc1a10e0-aee4-4e0e-ac5b-cdbb9abbec83" || + configId.startsWith("bc1a10e0-aee4-4e0e-ac5b-cdbb9abbec83:")) + ) { + return HttpResponse.text( + await readFile( + `${server.config.root}/packages/browser/test/integration/helpers/mocks/acquireResponse.json`, + ), + ); + } + + throw new Error("Handler not configured properly"); + }, +); + export const mediaSessionHandler = http.post( /https:\/\/edge.adobedc.net\/ee\/.*\/?v1\/interact/, diff --git a/packages/browser/test/integration/helpers/testsSetup/extend.js b/packages/browser/test/integration/helpers/testsSetup/extend.js index f33243cae..64c4c11f6 100644 --- a/packages/browser/test/integration/helpers/testsSetup/extend.js +++ b/packages/browser/test/integration/helpers/testsSetup/extend.js @@ -54,6 +54,13 @@ export const test = baseTest.extend({ alloy: [ async ({}, use) => { + // Clear all cookies for a clean slate before each test, so individual + // tests don't leak identity/consent state into subsequent tests. + const cookies = await cookieStore.getAll(); + await Promise.all( + cookies.map((c) => cookieStore.delete({ name: c.name, path: c.path })), + ); + await setupBaseCode(); const alloy = await setupAlloy(); diff --git a/packages/browser/test/integration/helpers/utils/legacyCookies.js b/packages/browser/test/integration/helpers/utils/legacyCookies.js new file mode 100644 index 000000000..c7c49604f --- /dev/null +++ b/packages/browser/test/integration/helpers/utils/legacyCookies.js @@ -0,0 +1,47 @@ +/* +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. +*/ + +export const LEGACY_IDENTITY_COOKIE_NAME = + "AMCV_5BFE274A5F6980A50A495C08%40AdobeOrg"; + +export const LEGACY_IDENTITY_COOKIE_UNESCAPED_NAME = + LEGACY_IDENTITY_COOKIE_NAME.replaceAll("%40", "@"); + +// ECID embedded in the legacy cookie value below +export const LEGACY_ECID = "16908443662402872073525706953453086963"; + +export const setLegacyIdentityCookie = async ( + orgId = "5BFE274A5F6980A50A495C08@AdobeOrg", +) => { + const encodedOrgId = encodeURIComponent(orgId); + const cookieName = `AMCV_${encodedOrgId}`; + const cookieValue = + "77933605%7CMCIDTS%7C18290%7CMCMID%7C16908443662402872073525706953453086963%7CMCAAMLH-1580857889%7C9%7CMCAAMB-1580857889%7CRKhpRz8krg2tLO6pguXWp5olkAcUniQYPHaMWWgdJ3xzPWQmdj0y%7CMCOPTOUT-1580260289s%7CNONE%7CvVersion%7C4.5.1"; + await cookieStore.set({ name: cookieName, value: cookieValue, path: "/" }); +}; + +export const setSecidCookie = async () => { + await cookieStore.set({ + name: "s_ecid", + value: "MCMID%7C16908443662402872073525706953453086963", + path: "/", + }); +}; + +export const getCookie = async (name) => { + // CookieStore matches names exactly, so fall back to the decoded name to + // preserve the previous behavior of accepting either form. + const cookie = + (await cookieStore.get(name)) ?? + (await cookieStore.get(decodeURIComponent(name))); + return cookie?.value; +};