diff --git a/CHANGELOG.md b/CHANGELOG.md index 0493da17d64..0b57dc5ca9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ The types of changes are: - Cleans up CSS for fidesEmbed mode [#4306](https://github.com/ethyca/fides/pull/4306) ### Added -- Added a `FidesPreferenceToggled` event to Fides.js to track when user preferences change without being saved [#4253](https://github.com/ethyca/fides/pull/4253) +- Added a `FidesUIChanged` event to Fides.js to track when user preferences change without being saved [#4314](https://github.com/ethyca/fides/pull/4314) and [#4253](https://github.com/ethyca/fides/pull/4253) - Add AC Systems to the TCF Overlay under Vendor Consents section [#4266](https://github.com/ethyca/fides/pull/4266/) ### Changed diff --git a/clients/fides-js/src/components/notices/NoticeOverlay.tsx b/clients/fides-js/src/components/notices/NoticeOverlay.tsx index 8f72865fd93..3e61f37b714 100644 --- a/clients/fides-js/src/components/notices/NoticeOverlay.tsx +++ b/clients/fides-js/src/components/notices/NoticeOverlay.tsx @@ -142,11 +142,7 @@ const NoticeOverlay: FunctionComponent = ({ enabledNoticeKeys={draftEnabledNoticeKeys} onChange={(updatedKeys) => { setDraftEnabledNoticeKeys(updatedKeys); - dispatchFidesEvent( - "FidesPreferenceToggled", - cookie, - options.debug - ); + dispatchFidesEvent("FidesUIChanged", cookie, options.debug); }} /> diff --git a/clients/fides-js/src/components/tcf/TcfOverlay.tsx b/clients/fides-js/src/components/tcf/TcfOverlay.tsx index 82b54e6112c..65da78ed5e6 100644 --- a/clients/fides-js/src/components/tcf/TcfOverlay.tsx +++ b/clients/fides-js/src/components/tcf/TcfOverlay.tsx @@ -320,11 +320,7 @@ const TcfOverlay: FunctionComponent = ({ enabledIds={draftIds} onChange={(updatedIds) => { setDraftIds(updatedIds); - dispatchFidesEvent( - "FidesPreferenceToggled", - cookie, - options.debug - ); + dispatchFidesEvent("FidesUIChanged", cookie, options.debug); }} activeTabIndex={activeTabIndex} onTabChange={setActiveTabIndex} diff --git a/clients/fides-js/src/fides-tcf.ts b/clients/fides-js/src/fides-tcf.ts index a8dc055326a..0915817b8b7 100644 --- a/clients/fides-js/src/fides-tcf.ts +++ b/clients/fides-js/src/fides-tcf.ts @@ -101,7 +101,8 @@ declare global { parameter?: number | string ) => void; config: { - fides: OverrideOptions; + // DEFER (PROD-1243): support a configurable "custom options" path + tc_info: OverrideOptions; }; } } diff --git a/clients/fides-js/src/fides.ts b/clients/fides-js/src/fides.ts index 5fbbb07b5a2..d7d3770951b 100644 --- a/clients/fides-js/src/fides.ts +++ b/clients/fides-js/src/fides.ts @@ -77,7 +77,8 @@ declare global { interface Window { Fides: Fides; config: { - fides: OverrideOptions; + // DEFER (PROD-1243): support a configurable "custom options" path + tc_info: OverrideOptions; }; } } diff --git a/clients/fides-js/src/lib/events.ts b/clients/fides-js/src/lib/events.ts index 392124ca5a5..d619588074f 100644 --- a/clients/fides-js/src/lib/events.ts +++ b/clients/fides-js/src/lib/events.ts @@ -6,15 +6,15 @@ import { debugLog } from "./consent-utils"; * - FidesInitialized: dispatched when initialization is complete, from Fides.init() * - FidesUpdated: dispatched when preferences are updated, from updateConsentPreferences() or Fides.init() * - FidesUIShown: dispatched when either the banner or modal is shown to the user + * - FidesUIChanged: dispatched when preferences are changed but not saved, i.e. "dirty". * - FidesModalClosed: dispatched when the modal is closed - * - FidesPreferenceToggled: dispatched when preferences are toggled but not saved, i.e. "dirty". */ export type FidesEventType = | "FidesInitialized" | "FidesUpdated" | "FidesUIShown" - | "FidesModalClosed" - | "FidesPreferenceToggled"; + | "FidesUIChanged" + | "FidesModalClosed"; // Bonus points: update the WindowEventMap interface with our custom event types declare global { @@ -22,8 +22,8 @@ declare global { FidesInitialized: FidesEvent; FidesUpdated: FidesEvent; FidesUIShown: FidesEvent; + FidesUIChanged: FidesEvent; FidesModalClosed: FidesEvent; - FidesPreferenceToggled: FidesEvent; } } diff --git a/clients/fides-js/src/lib/initialize.ts b/clients/fides-js/src/lib/initialize.ts index fcfdf4c3fb3..64cbe72283b 100644 --- a/clients/fides-js/src/lib/initialize.ts +++ b/clients/fides-js/src/lib/initialize.ts @@ -148,21 +148,33 @@ const automaticallyApplyGPCPreferences = ({ /** * Gets and validates Fides override options provided through URL query params, cookie or window obj. + * + * If the same override option is provided in multiple ways, load the value in this order: + * 1) query param (top priority) + * 2) window obj (second priority) + * 3) cookie value (last priority) */ export const getOverrideFidesOptions = (): Partial => { const overrideOptions: Partial = {}; if (typeof window !== "undefined") { - const params = new URLSearchParams(document.location.search); + // Grab query params if provided in the URL (e.g. "?fides_string=123...") + const queryParams = new URLSearchParams(window.location.search); + // Grab global window object if provided (e.g. window.config.tc_info = { fides_string: "123..." }) + // DEFER (PROD-1243): support a configurable "custom options" path + const windowObj = window.config?.tc_info; + + // Look for each of the override options in all three locations: query params, window object, cookie FIDES_OVERRIDE_OPTIONS_VALIDATOR_MAP.forEach( ({ fidesOption, fidesOptionType, fidesOverrideKey, validationRegex }) => { - // look for override options on URL query params, window obj, and cookie - const queryParamOverride: string | null = params.get(fidesOverrideKey); - const windowObjOverride: string | boolean | undefined = window.config - ?.fides - ? window.config?.fides[fidesOverrideKey] + const queryParamOverride: string | null = + queryParams.get(fidesOverrideKey); + const windowObjOverride: string | boolean | undefined = windowObj + ? windowObj[fidesOverrideKey] : undefined; const cookieOverride: string | undefined = getCookieByName(fidesOverrideKey); + + // Load the override option value, respecting the order of precedence (query params > window object > cookie) const value = queryParamOverride || windowObjOverride || cookieOverride; if (value && validationRegex.test(value.toString())) { // coerce to expected type in FidesOptions diff --git a/clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts b/clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts index 50b4265b8cb..6f79f3397f5 100644 --- a/clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts +++ b/clients/privacy-center/cypress/e2e/consent-banner-tcf.cy.ts @@ -374,33 +374,33 @@ describe("Fides-js TCF", () => { }); }); - it("can group toggle and fire FidesPreferenceToggled events", () => { + it("can group toggle and fire FidesUIChanged events", () => { // Toggle just legitimate interests cy.getByTestId("toggle-Purposes").click(); cy.getByTestId(`toggle-${PURPOSE_2.name}`).within(() => { cy.get("input").should("not.be.checked"); }); - cy.get("@FidesPreferenceToggled").its("callCount").should("equal", 1); + cy.get("@FidesUIChanged").its("callCount").should("equal", 1); // Toggle a child back on cy.getByTestId(`toggle-${PURPOSE_2.name}`).click(); cy.getByTestId("toggle-Purposes").within(() => { cy.get("input").should("be.checked"); }); - cy.get("@FidesPreferenceToggled").its("callCount").should("equal", 2); + cy.get("@FidesUIChanged").its("callCount").should("equal", 2); // Do the same for consent column cy.getByTestId("toggle-all-Purposes-consent").click(); cy.getByTestId(`toggle-${PURPOSE_4.name}-consent`).within(() => { cy.get("input").should("not.be.checked"); }); - cy.get("@FidesPreferenceToggled").its("callCount").should("equal", 3); + cy.get("@FidesUIChanged").its("callCount").should("equal", 3); // Toggle back on cy.getByTestId("toggle-all-Purposes-consent").click(); cy.getByTestId(`toggle-${PURPOSE_4.name}-consent`).within(() => { cy.get("input").should("be.checked"); }); - cy.get("@FidesPreferenceToggled").its("callCount").should("equal", 4); + cy.get("@FidesUIChanged").its("callCount").should("equal", 4); // Try the all on/all off button cy.get("button").contains("All off").click(); @@ -410,7 +410,7 @@ describe("Fides-js TCF", () => { cy.getByTestId(`toggle-${PURPOSE_4.name}-consent`).within(() => { cy.get("input").should("not.be.checked"); }); - cy.get("@FidesPreferenceToggled").its("callCount").should("equal", 5); + cy.get("@FidesUIChanged").its("callCount").should("equal", 5); }); it("can handle group toggle empty states", () => { @@ -490,7 +490,7 @@ describe("Fides-js TCF", () => { cy.getByTestId(`toggle-${VENDOR_1.name}-consent`).within(() => { cy.get("input").should("not.be.checked"); }); - cy.get("@FidesPreferenceToggled").should("have.been.calledOnce"); + cy.get("@FidesUIChanged").should("have.been.calledOnce"); }); }); }); @@ -503,7 +503,7 @@ describe("Fides-js TCF", () => { cy.getByTestId("consent-modal").within(() => { cy.get("button").contains("Opt in to all").click(); cy.wait("@patchPrivacyPreference").then((interception) => { - cy.get("@FidesPreferenceToggled").should("not.have.been.called"); + cy.get("@FidesUIChanged").should("not.have.been.called"); const { body } = interception.request; expect(body.purpose_consent_preferences).to.eql([ { id: PURPOSE_4.id, preference: "opt_in" }, @@ -576,7 +576,7 @@ describe("Fides-js TCF", () => { cy.getByTestId("consent-modal").within(() => { cy.get("button").contains("Opt out of all").click(); cy.wait("@patchPrivacyPreference").then((interception) => { - cy.get("@FidesPreferenceToggled").should("not.have.been.called"); + cy.get("@FidesUIChanged").should("not.have.been.called"); const { body } = interception.request; expect(body.purpose_consent_preferences).to.eql([ { id: PURPOSE_4.id, preference: "opt_out" }, @@ -653,7 +653,7 @@ describe("Fides-js TCF", () => { cy.get("#fides-tab-Vendors").click(); cy.getByTestId(`toggle-${SYSTEM_1.name}`).click(); cy.get("button").contains("Save").click(); - cy.get("@FidesPreferenceToggled").its("callCount").should("equal", 3); + cy.get("@FidesUIChanged").its("callCount").should("equal", 3); cy.wait("@patchPrivacyPreference").then((interception) => { const { body } = interception.request; expect(body.purpose_consent_preferences).to.eql([ @@ -722,6 +722,7 @@ describe("Fides-js TCF", () => { ).to.eql(true); }); }); + it("skips saving preferences to API when disable save is set", () => { cy.fixture("consent/experience_tcf.json").then((experience) => { stubConfig({ @@ -790,6 +791,7 @@ describe("Fides-js TCF", () => { }); }); }); + it("skips saving preferences to API when disable save is set via cookie", () => { cy.getCookie(CONSENT_COOKIE_NAME).should("not.exist"); cy.getCookie("fides_disable_save_api").should("not.exist"); @@ -822,6 +824,7 @@ describe("Fides-js TCF", () => { }); }); }); + it("skips saving preferences to API when disable save is set via query param", () => { cy.getCookie("fides_string").should("not.exist"); cy.fixture("consent/experience_tcf.json").then((experience) => { @@ -857,6 +860,7 @@ describe("Fides-js TCF", () => { }); }); }); + it("skips saving preferences to API when disable save is set via window obj", () => { cy.getCookie("fides_string").should("not.exist"); cy.fixture("consent/experience_tcf.json").then((experience) => { @@ -1112,7 +1116,7 @@ describe("Fides-js TCF", () => { }); }); - describe("cmp api", () => { + describe("CMP API", () => { beforeEach(() => { cy.getCookie(CONSENT_COOKIE_NAME).should("not.exist"); cy.fixture("consent/experience_tcf.json").then((experience) => { @@ -1237,25 +1241,30 @@ describe("Fides-js TCF", () => { }); }); - describe("User preference sources of truth for UI and CMP API", () => { + /** + * There are the following potential sources of user preferences: + * 1) fides_string override option (via config.options.fidesString) + * 2) DEFER: preferences API (via a custom function) + * 3) local cookie (via fides_consent cookie) + * 4) "prefetched" experience (via config.options.experience) + * 5) experience API (via GET /privacy-experience) + * + * These specs test various combinations of those sources of truth and ensure + * that Fides loads the correct preferences in each case. + */ + describe("user preferences overrides", () => { beforeEach(() => { cy.getCookie(CONSENT_COOKIE_NAME).should("not.exist"); }); - it("prefers preferences from a cookie when both cookie and experience exist", () => { - /** - * The default from the fixture is that - * - all purposes are opted in - * - all special purposes are opted in - * - feature 1 is opted out, feature 2 has no preference - * - all vendors are opted in - * - all systems are opted in - * - * We'll change at least one value from each entity type in the cookie - */ + + /** + * Configure a valid fides_consent cookie with previously saved preferences + */ + const setFidesCookie = () => { const uuid = "4fbb6edf-34f6-4717-a6f1-541fd1e5d585"; const CREATED_DATE = "2022-12-24T12:00:00.000Z"; const UPDATED_DATE = "2022-12-25T12:00:00.000Z"; - const cookie = { + const cookie: FidesCookie = { identity: { fides_user_device_id: uuid }, fides_meta: { version: "0.9.0", @@ -1272,9 +1281,23 @@ describe("Fides-js TCF", () => { system_legitimate_interests_preferences: { [SYSTEM_1.id]: false }, vendor_consent_preferences: { [VENDOR_1.id]: true }, }, - tc_string: "CPziCYAPziCYAGXABBENATEIAACAAAAAAAAAABEAAAAA.IABE", + fides_string: "CPziCYAPziCYAGXABBENATEIAACAAAAAAAAAABEAAAAA.IABE", }; cy.setCookie(CONSENT_COOKIE_NAME, JSON.stringify(cookie)); + }; + + /** + * TEST CASE #1: + * ❌ 1) fides_string override option (via config.options.fidesString) + * ❌ 2) DEFER: preferences API (via a custom function) + * ✅ 3) local cookie (via fides_consent cookie) + * ✅ 4) "prefetched" experience (via config.options.experience) + * ❌ 5) experience API (via GET /privacy-experience) + * + * EXPECTED RESULT: use preferences from local cookie + */ + it("prefers preferences from a cookie when both cookie and experience exist", () => { + setFidesCookie(); cy.fixture("consent/experience_tcf.json").then((experience) => { stubConfig({ options: { @@ -1349,34 +1372,19 @@ describe("Fides-js TCF", () => { }); }); }); - it("does nothing when cookie exists and experience is not provided", () => { - /** - * An experience is required to serve the CMP API, since the GVL is on the experience - */ - const uuid = "4fbb6edf-34f6-4717-a6f1-541fd1e5d585"; - const CREATED_DATE = "2022-12-24T12:00:00.000Z"; - const UPDATED_DATE = "2022-12-25T12:00:00.000Z"; - - const cookie = { - identity: { fides_user_device_id: uuid }, - fides_meta: { - version: "0.9.0", - createdAt: CREATED_DATE, - updatedAt: UPDATED_DATE, - }, - consent: {}, - tcf_consent: { - // We hard-code 2 because purpose_2 references a tcf_purpose_legitimate_interest in the experience - // and we wish to refer to a purpose_consent_preference here - purpose_consent_preferences: { 2: false, [PURPOSE_4.id]: true }, - special_feature_preferences: { [SPECIAL_FEATURE_1.id]: true }, - system_legitimate_interests_preferences: { [SYSTEM_1.id]: false }, - vendor_consent_preferences: { [VENDOR_1.id]: false }, - }, - tc_string: "CPzbcgAPzbcgAGXABBENATEIAACAAAAAAAAAABEAAAAA.IABE", - }; - cy.setCookie(CONSENT_COOKIE_NAME, JSON.stringify(cookie)); + /** + * TEST CASE #2: + * ❌ 1) fides_string override option (via config.options.fidesString) + * ❌ 2) DEFER: preferences API (via a custom function) + * ✅ 3) local cookie (via fides_consent cookie) + * ❌ 4) "prefetched" experience (via config.options.experience) + * ❌ 5) experience API (via GET /privacy-experience) + * + * EXPECTED RESULT: ignore all preferences, do not load TCF experience + */ + it("does nothing when cookie exists but no experience is provided (neither prefetch nor API)", () => { + setFidesCookie(); stubConfig( { options: { @@ -1385,14 +1393,26 @@ describe("Fides-js TCF", () => { fidesString: undefined, }, experience: OVERRIDE.UNDEFINED, - // the below ensures we do not fetch experience client-side either }, OVERRIDE.UNDEFINED, - OVERRIDE.UNDEFINED + OVERRIDE.EMPTY ); - cy.get("#fides-modal-link").should("not.be.visible"); + cy.waitUntilFidesInitialized().then(() => { + cy.get("#fides-modal-link").should("not.be.visible"); + }); }); - it("does nothing when we have neither cookie, experience, nor tc string", () => { + + /** + * TEST CASE #3: + * ❌ 1) fides_string override option (via config.options.fidesString) + * ❌ 2) DEFER: preferences API (via a custom function) + * ❌ 3) local cookie (via fides_consent cookie) + * ❌ 4) "prefetched" experience (via config.options.experience) + * ❌ 5) experience API (via GET /privacy-experience) + * + * EXPECTED RESULT: ignore all preferences, do not load TCF experience + */ + it("does nothing when nothing is provided (neither cookie, nor experience, nor fides_string option)", () => { stubConfig( { options: { @@ -1403,48 +1423,34 @@ describe("Fides-js TCF", () => { experience: OVERRIDE.UNDEFINED, }, OVERRIDE.UNDEFINED, - OVERRIDE.UNDEFINED + OVERRIDE.EMPTY ); - cy.get("#fides-modal-link").should("not.be.visible"); + cy.waitUntilFidesInitialized().then(() => { + cy.get("#fides-modal-link").should("not.be.visible"); + }); }); - it("prefers preferences from a TC string when tc string, experience, and cookie exist", () => { - /** - * The default from the fixture is that - * - all purposes are opted in - * - all special purposes are opted in - * - feature 1 is opted out, feature 2 has no preference - * - all vendors are opted in - * - all systems are opted in - * - * We'll change at least one value from each entity type in the cookie - */ - const uuid = "4fbb6edf-34f6-4717-a6f1-541fd1e5d585"; - const CREATED_DATE = "2022-12-24T12:00:00.000Z"; - const UPDATED_DATE = "2022-12-25T12:00:00.000Z"; - const cookie = { - identity: { fides_user_device_id: uuid }, - fides_meta: { - version: "0.9.0", - createdAt: CREATED_DATE, - updatedAt: UPDATED_DATE, - }, - consent: {}, - tcf_consent: { - purpose_preferences: { 2: false, [PURPOSE_4.id]: true }, - special_feature_preferences: { [SPECIAL_FEATURE_1.id]: true }, - system_legitimate_interests_preferences: { [SYSTEM_1.id]: false }, - vendor_consent_preferences: { [VENDOR_1.id]: false }, - }, - tc_string: "CPzbcgAPzbcgAGXABBENATEIAACAAAAAAAAAABEAAAAA.IABE", - }; - cy.setCookie(CONSENT_COOKIE_NAME, JSON.stringify(cookie)); + + /** + * TEST CASE #4: + * ✅ 1) fides_string override option (via config.options.fidesString) + * ❌ 2) DEFER: preferences API (via a custom function) + * ✅ 3) local cookie (via fides_consent cookie) + * ✅ 4) "prefetched" experience (via config.options.experience) + * ❌ 5) experience API (via GET /privacy-experience) + * + * EXPECTED RESULT: use preferences from fides_string option + */ + it("prefers preferences from fides_string option when fides_string, experience, and cookie exist", () => { + setFidesCookie(); + const fidesStringOverride = + "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA.IABE"; + const expectedTCString = "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA"; // without disclosed vendors cy.fixture("consent/experience_tcf.json").then((experience) => { stubConfig({ options: { isOverlayEnabled: true, tcfEnabled: true, - // this TC string sets purpose 4 to false and purpose 7 to true - fidesString: "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA.IABE", + fidesString: fidesStringOverride, }, experience: experience.items[0], }); @@ -1458,12 +1464,12 @@ describe("Fides-js TCF", () => { // Verify the toggles // Purposes cy.getByTestId(`toggle-${PURPOSE_2.name}`).within(() => { - // this purpose is set to true in the experience, but since it was not defined in the TC string, + // this purpose is set to true in the experience, but since it was not defined in the fides_string, // it should use the default preference set in the experience which is true cy.get("input").should("be.checked"); }); cy.getByTestId(`toggle-${PURPOSE_4.name}-consent`).within(() => { - // this purpose was previously set to true from the experience, but it is overridden by the TC string + // this purpose was previously set to true from the experience, but it is overridden by the fides_string cy.get("input").should("not.be.checked"); }); cy.getByTestId(`toggle-${PURPOSE_6.name}-consent`).within(() => { @@ -1473,7 +1479,7 @@ describe("Fides-js TCF", () => { cy.get("input").should("be.checked"); }); cy.getByTestId(`toggle-${PURPOSE_9.name}-consent`).within(() => { - // this purpose is set to true in the experience, but since it was not defined in the TC string, + // this purpose is set to true in the experience, but since it was not defined in the fides_string, // it should use the default preference set in the experience which is false cy.get("input").should("not.be.checked"); }); @@ -1483,7 +1489,7 @@ describe("Fides-js TCF", () => { cy.get("input").should("be.checked"); }); // Vendors - // this purpose is set to true in the experience, but since it was not defined in the TC string, + // this purpose is set to true in the experience, but since it was not defined in the fides_string, // it should use the default preference set in the experience which is true cy.get("#fides-tab-Vendors").click(); cy.getByTestId(`toggle-${SYSTEM_1.name}`).within(() => { @@ -1498,6 +1504,7 @@ describe("Fides-js TCF", () => { .its("lastCall.args") .then(([tcData, success]) => { expect(success).to.eql(true); + expect(tcData.tcString).to.eql(expectedTCString); expect(tcData.eventStatus).to.eql("cmpuishown"); expect(tcData.purpose.consents).to.eql({ [PURPOSE_2.id]: false, @@ -1514,24 +1521,27 @@ describe("Fides-js TCF", () => { expect(tcData.vendor.legitimateInterests).to.eql({}); }); }); - it("prefers preferences from a TC string when both tc string and experience is provided and cookie does not exist", () => { - /** - * The default from the fixture is that - * - all purposes are opted in - * - all special purposes are opted in - * - feature 1 is opted out, feature 2 has no preference - * - all vendors are opted in - * - all systems are opted in - * - * We'll change at least one value from each entity type in the cookie - */ + + /** + * TEST CASE #5: + * ✅ 1) fides_string override option (via config.options.fidesString) + * ❌ 2) DEFER: preferences API (via a custom function) + * ❌ 3) local cookie (via fides_consent cookie) + * ✅ 4) "prefetched" experience (via config.options.experience) + * ❌ 5) experience API (via GET /privacy-experience) + * + * EXPECTED RESULT: use preferences from fides_string option + */ + it("prefers preferences from fides_string option when both fides_string and experience is provided and cookie does not exist", () => { + const fidesStringOverride = + "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA.IABE"; + const expectedTCString = "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA"; // without disclosed vendors cy.fixture("consent/experience_tcf.json").then((experience) => { stubConfig({ options: { isOverlayEnabled: true, tcfEnabled: true, - // this TC string sets purpose 4 to false and purpose 7 to true - fidesString: "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA.IABE", + fidesString: fidesStringOverride, }, experience: experience.items[0], }); @@ -1545,12 +1555,12 @@ describe("Fides-js TCF", () => { // Verify the toggles // Purposes cy.getByTestId(`toggle-${PURPOSE_2.name}`).within(() => { - // this purpose is set to true in the experience, but since it was not defined in the TC string, + // this purpose is set to true in the experience, but since it was not defined in the fides_string, // it should use the default preference set in the experience which is true cy.get("input").should("be.checked"); }); cy.getByTestId(`toggle-${PURPOSE_4.name}-consent`).within(() => { - // this purpose was previously set to true from the experience, but it is overridden by the TC string + // this purpose was previously set to true from the experience, but it is overridden by the fides_string cy.get("input").should("not.be.checked"); }); cy.getByTestId(`toggle-${PURPOSE_6.name}-consent`).within(() => { @@ -1560,7 +1570,7 @@ describe("Fides-js TCF", () => { cy.get("input").should("be.checked"); }); cy.getByTestId(`toggle-${PURPOSE_9.name}-consent`).within(() => { - // this purpose is set to true in the experience, but since it was not defined in the TC string, + // this purpose is set to true in the experience, but since it was not defined in the fides_string, // it should use the default preference set in the experience which is false cy.get("input").should("not.be.checked"); }); @@ -1570,7 +1580,7 @@ describe("Fides-js TCF", () => { cy.get("input").should("be.checked"); }); // Vendors - // this purpose is set to true in the experience, but since it was not defined in the TC string, + // this purpose is set to true in the experience, but since it was not defined in the fides_string, // it should use the default preference set in the experience which is true cy.get("#fides-tab-Vendors").click(); cy.getByTestId(`toggle-${SYSTEM_1.name}`).within(() => { @@ -1585,6 +1595,7 @@ describe("Fides-js TCF", () => { .its("lastCall.args") .then(([tcData, success]) => { expect(success).to.eql(true); + expect(tcData.tcString).to.eql(expectedTCString); expect(tcData.eventStatus).to.eql("cmpuishown"); expect(tcData.purpose.consents).to.eql({ [PURPOSE_2.id]: false, @@ -1601,69 +1612,53 @@ describe("Fides-js TCF", () => { expect(tcData.vendor.legitimateInterests).to.eql({}); }); }); - it("does nothing when tc string and cookie exist but experience is not provided", () => { - const uuid = "4fbb6edf-34f6-4717-a6f1-541fd1e5d585"; - const CREATED_DATE = "2022-12-24T12:00:00.000Z"; - const UPDATED_DATE = "2022-12-25T12:00:00.000Z"; - const cookie = { - identity: { fides_user_device_id: uuid }, - fides_meta: { - version: "0.9.0", - createdAt: CREATED_DATE, - updatedAt: UPDATED_DATE, - }, - consent: {}, - tcf_consent: { - purpose_consent_preferences: { - [PURPOSE_2.id]: false, - [PURPOSE_4.id]: true, - }, - special_feature_preferences: { [SPECIAL_FEATURE_1.id]: true }, - system_legitimate_interests_preferences: { [SYSTEM_1.id]: false }, - vendor_consent_preferences: { [VENDOR_1.id]: false }, - }, - tc_string: "CPzbcgAPzbcgAGXABBENATEIAACAAAAAAAAAABEAAAAA.IABE", - }; - cy.setCookie(CONSENT_COOKIE_NAME, JSON.stringify(cookie)); + + /** + * TEST CASE #6: + * ✅ 1) fides_string override option (via config.options.fidesString) + * ❌ 2) DEFER: preferences API (via a custom function) + * ✅ 3) local cookie (via fides_consent cookie) + * ❌ 4) "prefetched" experience (via config.options.experience) + * ❌ 5) experience API (via GET /privacy-experience) + * + * EXPECTED RESULT: ignore all preferences, do not load TCF experience + */ + it("does nothing when fides_string option when both fides_string option and cookie exist but no experience exists (neither prefetch nor API)", () => { + const fidesStringOverride = + "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA.IABE"; + setFidesCookie(); stubConfig( { options: { isOverlayEnabled: true, tcfEnabled: true, - // this TC string sets purpose 4 to false and purpose 7 to true - fidesString: "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA.IABE", + fidesString: fidesStringOverride, }, experience: OVERRIDE.UNDEFINED, }, OVERRIDE.UNDEFINED, - OVERRIDE.UNDEFINED + OVERRIDE.EMPTY // return no experience ); - cy.get("#fides-modal-link").should("not.be.visible"); + cy.waitUntilFidesInitialized().then(() => { + cy.get("#fides-modal-link").should("not.be.visible"); + }); }); - it("Prefers prefs on tc string when both tc string and cookie exist and client-side experience is fetched", () => { - const uuid = "4fbb6edf-34f6-4717-a6f1-541fd1e5d585"; - const CREATED_DATE = "2022-12-24T12:00:00.000Z"; - const UPDATED_DATE = "2022-12-25T12:00:00.000Z"; - const cookie = { - identity: { fides_user_device_id: uuid }, - fides_meta: { - version: "0.9.0", - createdAt: CREATED_DATE, - updatedAt: UPDATED_DATE, - }, - consent: {}, - tcf_consent: { - purpose_consent_preferences: { - [PURPOSE_2.id]: false, - [PURPOSE_4.id]: true, - }, - special_feature_preferences: { [SPECIAL_FEATURE_1.id]: true }, - system_legitimate_interests_preferences: { [SYSTEM_1.id]: false }, - vendor_consent_preferences: { [VENDOR_1.id]: false }, - }, - tc_string: "CPzbcgAPzbcgAGXABBENATEIAACAAAAAAAAAABEAAAAA.IABE", - }; - cy.setCookie(CONSENT_COOKIE_NAME, JSON.stringify(cookie)); + + /** + * TEST CASE #7: + * ✅ 1) fides_string override option (via config.options.fidesString) + * ❌ 2) DEFER: preferences API (via a custom function) + * ✅ 3) local cookie (via fides_consent cookie) + * ❌ 4) "prefetched" experience (via config.options.experience) + * ✅ 5) experience API (via GET /privacy-experience) + * + * EXPECTED RESULT: use preferences from fides_string option + */ + it("prefers preferences from fides_string option when both fides_string option and cookie exist and experience is fetched from API", () => { + const fidesStringOverride = + "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA.IABE"; + const expectedTCString = "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA"; // without disclosed vendors + setFidesCookie(); cy.fixture("consent/experience_tcf.json").then((experience) => { cy.fixture("consent/geolocation_tcf.json").then((geo) => { stubConfig( @@ -1671,9 +1666,7 @@ describe("Fides-js TCF", () => { options: { isOverlayEnabled: true, tcfEnabled: true, - // this TC string sets purpose 4 to false and purpose 7 to true - fidesString: - "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA.IABE", + fidesString: fidesStringOverride, }, experience: OVERRIDE.UNDEFINED, }, @@ -1694,12 +1687,12 @@ describe("Fides-js TCF", () => { // Verify the toggles // Purposes cy.getByTestId(`toggle-${PURPOSE_2.name}`).within(() => { - // this purpose is set to true in the experience, but since it was not defined in the TC string, + // this purpose is set to true in the experience, but since it was not defined in the fides_string, // it should use the default preference set in the experience which is true cy.get("input").should("be.checked"); }); cy.getByTestId(`toggle-${PURPOSE_4.name}-consent`).within(() => { - // this purpose was previously set to true from the experience, but it is overridden by the TC string + // this purpose was previously set to true from the experience, but it is overridden by the fides_string cy.get("input").should("not.be.checked"); }); cy.getByTestId(`toggle-${PURPOSE_6.name}-consent`).within(() => { @@ -1709,7 +1702,7 @@ describe("Fides-js TCF", () => { cy.get("input").should("be.checked"); }); cy.getByTestId(`toggle-${PURPOSE_9.name}-consent`).within(() => { - // this purpose is set to true in the experience, but since it was not defined in the TC string, + // this purpose is set to true in the experience, but since it was not defined in the fides_string, // it should use the default preference set in the experience which is false cy.get("input").should("not.be.checked"); }); @@ -1719,7 +1712,7 @@ describe("Fides-js TCF", () => { cy.get("input").should("be.checked"); }); // Vendors - // this purpose is set to true in the experience, but since it was not defined in the TC string, + // this purpose is set to true in the experience, but since it was not defined in the fides_string, // it should use the default preference set in the experience which is true cy.get("#fides-tab-Vendors").click(); cy.getByTestId(`toggle-${SYSTEM_1.name}`).within(() => { @@ -1734,6 +1727,7 @@ describe("Fides-js TCF", () => { .its("lastCall.args") .then(([tcData, success]) => { expect(success).to.eql(true); + expect(tcData.tcString).to.eql(expectedTCString); expect(tcData.eventStatus).to.eql("cmpuishown"); expect(tcData.purpose.consents).to.eql({ [PURPOSE_2.id]: false, @@ -1752,14 +1746,13 @@ describe("Fides-js TCF", () => { }); }); - describe("fides string override options", () => { - it("uses TC string when set via cookie", () => { + describe("fides_string override options", () => { + it("uses fides_string when set via cookie", () => { + const fidesStringOverride = + "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA.IABE"; + const expectedTCString = "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA"; // without disclosed vendors cy.getCookie("fides_string").should("not.exist"); - // this TC string sets purpose 4 to false and purpose 7 to true - cy.setCookie( - "fides_string", - "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA.IABE" - ); + cy.setCookie("fides_string", fidesStringOverride); cy.fixture("consent/experience_tcf.json").then((experience) => { stubConfig({ options: { @@ -1780,6 +1773,7 @@ describe("Fides-js TCF", () => { .its("lastCall.args") .then(([tcData, success]) => { expect(success).to.eql(true); + expect(tcData.tcString).to.eql(expectedTCString); expect(tcData.eventStatus).to.eql("cmpuishown"); expect(tcData.purpose.consents).to.eql({ [PURPOSE_2.id]: false, @@ -1796,7 +1790,11 @@ describe("Fides-js TCF", () => { expect(tcData.vendor.legitimateInterests).to.eql({}); }); }); - it("uses TC string when set via query param", () => { + + it("uses fides_string when set via query param", () => { + const fidesStringOverride = + "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA.IABE"; + const expectedTCString = "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA"; // without disclosed vendors cy.getCookie("fides_string").should("not.exist"); cy.fixture("consent/experience_tcf.json").then((experience) => { stubConfig( @@ -1809,8 +1807,7 @@ describe("Fides-js TCF", () => { }, null, null, - // this TC string sets purpose 4 to false and purpose 7 to true - { fides_string: "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA.IABE" } + { fides_string: fidesStringOverride } ); }); cy.window().then((win) => { @@ -1824,6 +1821,7 @@ describe("Fides-js TCF", () => { .its("lastCall.args") .then(([tcData, success]) => { expect(success).to.eql(true); + expect(tcData.tcString).to.eql(expectedTCString); expect(tcData.eventStatus).to.eql("cmpuishown"); expect(tcData.purpose.consents).to.eql({ [PURPOSE_2.id]: false, @@ -1840,7 +1838,11 @@ describe("Fides-js TCF", () => { expect(tcData.vendor.legitimateInterests).to.eql({}); }); }); - it("uses TC string when set via window obj", () => { + + it("uses fides_string when set via window obj", () => { + const fidesStringOverride = + "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA.IABE"; + const expectedTCString = "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA"; // without disclosed vendors cy.getCookie("fides_string").should("not.exist"); cy.fixture("consent/experience_tcf.json").then((experience) => { stubConfig( @@ -1854,8 +1856,7 @@ describe("Fides-js TCF", () => { null, null, null, - // this TC string sets purpose 4 to false and purpose 7 to true - { fides_string: "CPzevcAPzevcAGXABBENATEIAAIAAAAAAAAAAAAAAAAA.IABE" } + { fides_string: fidesStringOverride } ); }); cy.window().then((win) => { @@ -1869,6 +1870,7 @@ describe("Fides-js TCF", () => { .its("lastCall.args") .then(([tcData, success]) => { expect(success).to.eql(true); + expect(tcData.tcString).to.eql(expectedTCString); expect(tcData.eventStatus).to.eql("cmpuishown"); expect(tcData.purpose.consents).to.eql({ [PURPOSE_2.id]: false, @@ -2012,7 +2014,7 @@ describe("Fides-js TCF", () => { .then(([tcData, success]) => { expect(success).to.eql(true); expect(tcData.eventStatus).to.eql("useractioncomplete"); - // This TC string should not be a composite—should just be the tc string + // This fides_string should not be a composite—should just be the tc string const { tcString } = tcData; const parts = tcString.split(","); expect(parts.length).to.eql(1); diff --git a/clients/privacy-center/cypress/e2e/consent-banner.cy.ts b/clients/privacy-center/cypress/e2e/consent-banner.cy.ts index 3fbf5c8d4f4..4815813f577 100644 --- a/clients/privacy-center/cypress/e2e/consent-banner.cy.ts +++ b/clients/privacy-center/cypress/e2e/consent-banner.cy.ts @@ -1110,13 +1110,13 @@ describe("Consent banner", () => { [PRIVACY_NOTICE_KEY_1]: false, [PRIVACY_NOTICE_KEY_2]: true, }); - cy.get("@FidesPreferenceToggled").should("not.have.been.called"); + cy.get("@FidesUIChanged").should("not.have.been.called"); }); describe("when preferences are changed / saved", () => { it("emits another FidesUpdated event when reject all is clicked", () => { cy.contains("button", "Reject Test").should("be.visible").click(); - cy.get("@FidesPreferenceToggled").should("not.have.been.called"); + cy.get("@FidesUIChanged").should("not.have.been.called"); cy.get("@FidesUpdated") .should("have.been.calledTwice") // First call should be from initialization, before the user rejects all @@ -1136,7 +1136,7 @@ describe("Consent banner", () => { it("emits another FidesUpdated event when accept all is clicked", () => { cy.contains("button", "Accept Test").should("be.visible").click(); - cy.get("@FidesPreferenceToggled").should("not.have.been.called"); + cy.get("@FidesUIChanged").should("not.have.been.called"); cy.get("@FidesUpdated") .should("have.been.calledTwice") // First call should be from initialization, before the user accepts all @@ -1154,13 +1154,13 @@ describe("Consent banner", () => { }); }); - it("emits a FidesPreferenceToggled event when preferences are changed and a FidesUpdated event when preferences are saved", () => { + it("emits a FidesUIChanged event when preferences are changed and a FidesUpdated event when preferences are saved", () => { cy.contains("button", "Manage preferences") .should("be.visible") .click(); cy.getByTestId("toggle-Test privacy notice").click(); cy.getByTestId("consent-modal").contains("Save").click(); - cy.get("@FidesPreferenceToggled").should("have.been.calledOnce"); + cy.get("@FidesUIChanged").should("have.been.calledOnce"); cy.get("@FidesUpdated") .should("have.been.calledTwice") // First call should be from initialization, before the user saved preferences diff --git a/clients/privacy-center/cypress/support/commands.ts b/clients/privacy-center/cypress/support/commands.ts index 0f6b0f24339..f30fa42829c 100644 --- a/clients/privacy-center/cypress/support/commands.ts +++ b/clients/privacy-center/cypress/support/commands.ts @@ -68,7 +68,8 @@ Cypress.Commands.add( // @ts-ignore // eslint-disable-next-line no-param-reassign win.config = { - fides: windowParams, + // DEFER (PROD-1243): support a configurable "custom options" path + tc_info: windowParams, }; } @@ -79,10 +80,7 @@ Cypress.Commands.add( ); win.addEventListener("FidesUpdated", cy.stub().as("FidesUpdated")); win.addEventListener("FidesUIShown", cy.stub().as("FidesUIShown")); - win.addEventListener( - "FidesPreferenceToggled", - cy.stub().as("FidesPreferenceToggled") - ); + win.addEventListener("FidesUIChanged", cy.stub().as("FidesUIChanged")); // Add GTM stub // eslint-disable-next-line no-param-reassign diff --git a/clients/privacy-center/pages/api/fides-js.ts b/clients/privacy-center/pages/api/fides-js.ts index a1a4850ded9..d09431aa748 100644 --- a/clients/privacy-center/pages/api/fides-js.ts +++ b/clients/privacy-center/pages/api/fides-js.ts @@ -237,6 +237,7 @@ async function fetchCustomFidesCss( const data = await response.text(); if (!response.ok) { + // eslint-disable-next-line no-console console.error( "Error fetching custom-fides.css:", response.status, @@ -250,6 +251,7 @@ async function fetchCustomFidesCss( throw new Error("No data returned by the server"); } + // eslint-disable-next-line no-console console.log("Successfully retrieved custom-fides.css"); autoRefresh = true; cachedCustomFidesCss = data; @@ -257,8 +259,10 @@ async function fetchCustomFidesCss( } catch (error) { autoRefresh = false; // /custom-asset endpoint unreachable stop auto-refresh if (error instanceof Error) { + // eslint-disable-next-line no-console console.error("Error during fetch operation:", error.message); } else { + // eslint-disable-next-line no-console console.error("Unknown error occurred:", error); } }