Migrated IAB TCF consent functional tests to Vitest+Playwright+MSW#1543
Migrated IAB TCF consent functional tests to Vitest+Playwright+MSW#1543carterworks wants to merge 4 commits into
Conversation
|
|
| Filename | Overview |
|---|---|
| packages/browser/test/integration/specs/Consent/iab.spec.js | New integration test file covering 9 IAB TCF consent scenarios using Vitest+Playwright+MSW; two tests share the C224671 ID, a verifiable cookie assertion from the originals is silently dropped in the C224671 opt-out tests, and some negative-path tests use a raw synchronous filter inconsistent with the rest of the file. |
Sequence Diagram
sequenceDiagram
participant Test as Vitest Test
participant Alloy as Alloy SDK (browser)
participant MSW as MSW Worker
participant NR as NetworkRecorder
Test->>Alloy: configure(pendingConfig)
Test->>Alloy: "setConsent(IAB_CONSENT_*)"
Alloy->>MSW: POST /v1/privacy/set-consent
MSW->>NR: captureRequest
MSW-->>Alloy: "{ state:store [general=in|out] }"
MSW->>NR: captureResponse
Test->>NR: findCalls(/set-consent/)
NR-->>Test: [call with request+response]
Test->>Alloy: sendEvent()
alt "consent = in"
Alloy->>MSW: POST /v1/interact
MSW->>NR: captureRequest + captureResponse
Test->>NR: findCalls(/v1\/interact/)
NR-->>Test: [1 call]
else "consent = out"
Note over Alloy: Alloy blocks call (no request sent)
Test->>NR: calls.filter(v1/interact)
NR-->>Test: [] (zero calls)
end
Reviews (1): Last reviewed commit: "test(integration): migrate iab functiona..." | Re-trigger Greptile
| test("C224671: opt out of IAB with no Purpose 1; subsequent sendEvent is blocked", async ({ | ||
| alloy, | ||
| worker, | ||
| networkRecorder, | ||
| }) => { | ||
| worker.use(setConsentHandler); | ||
|
|
||
| await alloy("configure", pendingConfig); | ||
| await alloy("setConsent", IAB_NO_PURPOSE_ONE); | ||
|
|
||
| const consentCalls = await networkRecorder.findCalls( | ||
| /v1\/privacy\/set-consent/, | ||
| ); | ||
| expect(consentCalls.length).toBe(1); | ||
| expect(consentCalls[0].response.status).toBeGreaterThanOrEqual(200); | ||
| expect(consentCalls[0].response.status).toBeLessThanOrEqual(207); | ||
|
|
||
| // After opting out, sendEvent should not fire | ||
| await alloy("sendEvent"); | ||
| expect( | ||
| networkRecorder.calls.filter((c) => | ||
| /v1\/interact/.test(c.request?.url ?? ""), | ||
| ).length, | ||
| ).toBe(0); | ||
| }); | ||
|
|
||
| // C224671: Opt out of IAB — no Adobe vendor | ||
| test("C224671: opt out of IAB with no Adobe vendor; subsequent sendEvent is blocked", async ({ |
There was a problem hiding this comment.
Duplicate test ID for two distinct scenarios
Both the "no Purpose 1" test (line 68) and the "no Adobe vendor" test (line 95) share the identifier C224671. In the original TestCafe file both variants ran inside a forEach loop under one test.meta({ ID: "C224671" }) declaration, but in Vitest each test() call registers an independent entry. Test-reporting tools and CI systems that track coverage by case ID will see two separate C224671 results that may overwrite each other or trigger duplicate-ID warnings. Consider differentiating the names, e.g., C224671-no-purpose-one and C224671-no-adobe-vendor, or C224671 (No Purpose 1) / C224671 (No Adobe vendor).
| // After opting out, sendEvent should not fire | ||
| await alloy("sendEvent"); | ||
| expect( | ||
| networkRecorder.calls.filter((c) => | ||
| /v1\/interact/.test(c.request?.url ?? ""), | ||
| ).length, | ||
| ).toBe(0); | ||
| }); | ||
|
|
||
| // C224671: Opt out of IAB — no Adobe vendor | ||
| test("C224671: opt out of IAB with no Adobe vendor; subsequent sendEvent is blocked", async ({ | ||
| alloy, | ||
| worker, | ||
| networkRecorder, | ||
| }) => { | ||
| worker.use(setConsentHandler); | ||
|
|
||
| await alloy("configure", pendingConfig); | ||
| await alloy("setConsent", IAB_NO_ADOBE_VENDOR); | ||
|
|
||
| const consentCalls = await networkRecorder.findCalls( | ||
| /v1\/privacy\/set-consent/, | ||
| ); | ||
| expect(consentCalls.length).toBe(1); | ||
| expect(consentCalls[0].response.status).toBeGreaterThanOrEqual(200); | ||
| expect(consentCalls[0].response.status).toBeLessThanOrEqual(207); | ||
|
|
||
| // After opting out, sendEvent should not fire | ||
| await alloy("sendEvent"); | ||
| expect( | ||
| networkRecorder.calls.filter((c) => | ||
| /v1\/interact/.test(c.request?.url ?? ""), | ||
| ).length, | ||
| ).toBe(0); |
There was a problem hiding this comment.
Inconsistent pattern for asserting zero network calls
Both C224671 opt-out tests assert no v1/interact call by directly filtering the synchronous networkRecorder.calls array, while every other test in the file uses the async networkRecorder.findCalls() helper. Although the synchronous check is safe here (when consent is out, Alloy never enqueues the request so there is nothing to wait for), the inconsistency could mislead future contributors into thinking a raw .filter is the accepted way to check for absent calls, which would be incorrect in cases where the request could arrive with a delay. Using findCalls with retries: 0 (or a dedicated assertNoCalls helper) would keep the pattern uniform.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| // C224671: Opt out of IAB using the setConsent command | ||
| // IAB_NO_PURPOSE_ONE — no Purpose 1, results in general=out | ||
| test("C224671: opt out of IAB with no Purpose 1; subsequent sendEvent is blocked", async ({ | ||
| alloy, | ||
| worker, | ||
| networkRecorder, | ||
| }) => { | ||
| worker.use(setConsentHandler); | ||
|
|
||
| await alloy("configure", pendingConfig); | ||
| await alloy("setConsent", IAB_NO_PURPOSE_ONE); | ||
|
|
||
| const consentCalls = await networkRecorder.findCalls( | ||
| /v1\/privacy\/set-consent/, | ||
| ); | ||
| expect(consentCalls.length).toBe(1); | ||
| expect(consentCalls[0].response.status).toBeGreaterThanOrEqual(200); | ||
| expect(consentCalls[0].response.status).toBeLessThanOrEqual(207); | ||
|
|
||
| // After opting out, sendEvent should not fire | ||
| await alloy("sendEvent"); | ||
| expect( | ||
| networkRecorder.calls.filter((c) => | ||
| /v1\/interact/.test(c.request?.url ?? ""), | ||
| ).length, | ||
| ).toBe(0); | ||
| }); | ||
|
|
||
| // C224671: Opt out of IAB — no Adobe vendor | ||
| test("C224671: opt out of IAB with no Adobe vendor; subsequent sendEvent is blocked", async ({ | ||
| alloy, | ||
| worker, | ||
| networkRecorder, | ||
| }) => { | ||
| worker.use(setConsentHandler); | ||
|
|
||
| await alloy("configure", pendingConfig); | ||
| await alloy("setConsent", IAB_NO_ADOBE_VENDOR); | ||
|
|
||
| const consentCalls = await networkRecorder.findCalls( | ||
| /v1\/privacy\/set-consent/, | ||
| ); | ||
| expect(consentCalls.length).toBe(1); | ||
| expect(consentCalls[0].response.status).toBeGreaterThanOrEqual(200); | ||
| expect(consentCalls[0].response.status).toBeLessThanOrEqual(207); | ||
|
|
||
| // After opting out, sendEvent should not fire | ||
| await alloy("sendEvent"); | ||
| expect( | ||
| networkRecorder.calls.filter((c) => | ||
| /v1\/interact/.test(c.request?.url ?? ""), | ||
| ).length, | ||
| ).toBe(0); | ||
| }); |
There was a problem hiding this comment.
C224671: verifiable consent-cookie assertion is silently dropped
The original C224671 functional test verified that after opting out the consent cookie equalled "general=out". The setConsentHandler in this PR already returns { type: "state:store", payload: [{ key: "kndctr_…_consent", value: "general=out" }] }, so Alloy will write that cookie during the test. The assertion is therefore feasible in the mock environment but was dropped without an explanation comment (unlike the well-documented skip in C224675 and the limitation notes in C224678). Adding a expect(document.cookie).toContain("general=out") check after setConsent would make these tests equivalent to the originals on the cookie dimension.
f4ee395 to
ea1897b
Compare
bd904d8 to
86474d3
Compare
There was a problem hiding this comment.
carterworks has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
ea1897b to
f9440d5
Compare
86474d3 to
7208bec
Compare
There was a problem hiding this comment.
carterworks has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
carterworks has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
c941202 to
eeef5eb
Compare
eeef5eb to
6c320f2
Compare
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6c320f2 to
882177b
Compare
…tegration suite Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Integration migration review — Consent/IAB (
|
| Old file | Migrated | Subtests old → new | Notes |
|---|---|---|---|
| C224670 | ✅ | 1 → 1 | |
| C224671 | ✅ | 2 → 2 (forEach expanded) | Both IAB_NO_PURPOSE_ONE and IAB_NO_ADOBE_VENDOR as separate test() blocks |
| C224672 | ✅ | 1 → 1 | New adds gdprContainsPersonalData body assertion — improvement |
| C224673 | ✅ | 1 → 1 | |
| C224674 | ✅ | 1 → 1 | Old typo IAB_NO_PURPOSE_ONE_NO_GRPR corrected to IAB_NO_PURPOSE_ONE_NO_GDPR |
| C224675 | ⏭️ skipped | — | See §1 |
| C224676 | ✅ | 1 → 1 | New adds request-body XDM assertion — improvement |
| C224677 | ✅ | 1 → 1 | Warning-not-present assertion dropped; see §3 |
| C224678 | ✅ | 1 → 1 | Scope narrowed; see §4 |
1. Skip justification — C224675 🟢
Justified and double-covered. The old test asserts the server rejects four invalid IAB strings with specific error codes: EXEG-0102-400, EXEG-0103-400, EXEG-0104-422. These are server-side TCF string validation errors. The MSW setConsentHandler matches only on configId and always returns a 200 state:store — faithfully simulating per-request validation would require a full TCF TC string decoder. The skip comment at iab.spec.js:199-202 says exactly this.
Bonus: C224675.js was not deleted in this PR and remains in the functional suite, so server-validation coverage is preserved end-to-end.
2. Dropped assertions: ECID and cookie 🟡
setConsent-path tests (C224670–C224674, C224677):
Old tests extracted identity:result from the set-consent response and asserted ECID was present. The setConsentHandler (handlers.js:241-255) returns only a state:store payload — no identity:result — so the ECID assertion is genuinely non-replicable for these tests. 🟢 env-forced.
Cookie assertions (consentCookieValue === "general=in/out") are also dropped for these tests. The handler does return a state:store value that alloy uses to write the cookie (e.g. general=out for IAB_NO_PURPOSE_ONE at handlers.js:217-218). The behavioral signal that replaces it — whether a subsequent sendEvent fires or not — is a stronger proxy. 🟢 acceptable trade.
sendEvent-path tests (C224676, C224678):
sendEventResponse.json (lines 5-14) does include an identity:result handle with ECID. The ECID assertion was droppable-but-portable for these two tests and was dropped by choice. Worth a comment if ECID presence is a stated requirement of these cases. 🟡 minor.
3. C224677: warning-not-present assertion dropped 🟡
iab.spec.js:251-273 — the old test (C224677.js:60-72) verified that with purpose 10 false (but purpose 1 true), alloy does not record an EXEG-0301-200 opt-out warning:
const warningTypes = eventResponse.getWarnings().map((w) => w.type);
await t.expect(warningTypes).notContains("https://ns.adobe.com/aep/errors/EXEG-0301-200");sendEventResponse.json has no warnings handle, so this would vacuously pass — but it's worth a comment noting the assertion was skipped because the mock response has no warnings array, rather than silently omitting it.
4. C224678: second-event-blocked assertion dropped 🟡
iab.spec.js:280-316 — the old test (C224678.js:85-96) called alloy.sendEvent() twice: first with a no-Purpose-1 consent string, then bare — and verified the second event was blocked (total interact count stayed 1, opt-out state was set by the server's EXEG-0301-200 warning).
The sendEventResponse.json mock has no EXEG-0301-200 warning payload, so the SDK never flips its internal consent state — a second sendEvent in the new test would succeed, making the assertion unreplicable as-is. The comment at iab.spec.js:277-279 explains the cookie/warning gap but doesn't mention the second-event-block assertion specifically. Consider adding that to the comment.
The fix exists if needed: setConsentHandler already has the pattern — it reads the IAB string from the body and returns a consent-state response. A sendEventOptOutHandler that checks for the known no-Purpose-1 string (CO052oTO052oTDGAMBFRACBgAABAAAAAAIYgEawAQEagAAAA) and returns a warnings handle with EXEG-0301-200 would restore both the warning assertion and the second-event-block.
Also dropped (not mentioned in comments): the old test asserted that activation:push, identity:exchange, and personalization:decisions handles were absent from the opt-out response. sendEventResponse.json includes activation:push and personalization:decisions payloads, so this couldn't be asserted against the mock. Worth documenting.
Isolation / hygiene 🟢
worker.resetHandlers()fires after every test via theworkerfixture (extend.js:41).networkRecorder.reset()fires before and after every test (extend.js:47-50).- All cookies are cleared before each test via
cookieStore.getAll()/cookieStore.delete()(extend.js:59-62). - License header present (
iab.spec.js:1-11), year 2026 correct. iab.spec.jspicked up byvitest.projects.js:62glob (packages/browser/test/integration/**/*.spec.js) — no registration needed.- No
test.metatags in the new spec — consistent with other migrated specs in this harness.
Negative-case timing / flake 🟢
Both C224671 opt-out tests (iab.spec.js:87-91, 114-118) assert zero interact calls using a synchronous networkRecorder.calls.filter(...) snapshot rather than findCalls. Because alloy("sendEvent") resolves immediately when consent is out (SDK blocks before hitting the network), no poll is needed. Correct.
Deletions alignment 🟢
Eight files deleted (C224670–C224674, C224676–C224678), one retained (C224675). Matches the new spec's coverage exactly.
Reviewed against: packages/browser/test/integration/specs/Consent/iab.spec.js (new), packages/browser/test/functional/specs/Consent/IAB/C224670–C224678.js (old, via migrate-integration/00-infra), helpers/mswjs/handlers.js, helpers/mswjs/networkRecorder.js, helpers/constants/consent.js, helpers/testsSetup/extend.js, helpers/mocks/sendEventResponse.json.
Keep the original testcafe functional specs alongside the new Vitest+Playwright+MSW integration suite until these migration branches merge, so reviewers retain the pre-migration signal.
Changed Packages
Description
Migrates the IAB TCF consent functional tests to the new Vitest+Playwright+MSW harness. C224675 (server-side 400/422 validation) is preserved as test.skip (requires live edge endpoint).
Related Issue
Part of the functional test → integration test migration. See
packages/browser/test/FUNCTIONAL_MIGRATION_PLAN.md.Motivation and Context
The existing TestCafe functional test suite is being migrated to Vitest+Playwright+MSW to enable faster, more reliable CI testing without a running server. This PR is part of a stacked series — each PR migrates one test file.
Functional tests replaced:
packages/browser/test/functional/specs/Consent/IAB/C224670.jspackages/browser/test/functional/specs/Consent/IAB/C224671.jspackages/browser/test/functional/specs/Consent/IAB/C224672.jspackages/browser/test/functional/specs/Consent/IAB/C224673.jspackages/browser/test/functional/specs/Consent/IAB/C224674.jspackages/browser/test/functional/specs/Consent/IAB/C224675.jspackages/browser/test/functional/specs/Consent/IAB/C224676.jspackages/browser/test/functional/specs/Consent/IAB/C224677.jspackages/browser/test/functional/specs/Consent/IAB/C224678.jsTypes of changes
Checklist:
Stack