From 4be784d958ac131330bd28a1b2742b9b7a808695 Mon Sep 17 00:00:00 2001 From: "Dave(.nl) van Hoorn" Date: Thu, 18 Apr 2024 20:36:51 +0530 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=85=20skip=20queue=20for=20zaraz.?= =?UTF-8?q?consent=20methods,=20increase=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/consent/get-all-checkboxes.spec.ts | 12 +++++++- src/consent/get-all-checkboxes.ts | 8 +++-- src/consent/get-all.spec.ts | 18 +++++++++-- src/consent/get-all.ts | 19 ++++++++++-- src/consent/get-api-ready.spec.ts | 41 ++++++++++++++++++++++++++ src/consent/get-api-ready.ts | 13 ++++++++ src/consent/get-purposes.spec.ts | 28 ++++++++++++++---- src/consent/get-purposes.ts | 18 +++++++---- src/consent/get.spec.ts | 12 +++++++- src/consent/get.ts | 4 +-- src/consent/index.spec.ts | 1 + src/consent/index.ts | 2 ++ src/consent/send-queued-event.spec.ts | 13 +++++++- src/consent/send-queued-events.ts | 2 +- src/consent/set-all-checkboxes.spec.ts | 13 +++++++- src/consent/set-all-checkboxes.ts | 2 +- src/consent/set-all.spec.ts | 12 +++++++- src/consent/set-all.ts | 2 +- src/consent/set-checkboxes.spec.ts | 13 +++++++- src/consent/set-checkboxes.ts | 2 +- src/consent/set.spec.ts | 12 +++++++- src/consent/set.ts | 13 ++++++-- src/helpers/get-zaraz.spec.ts | 10 +++++++ src/helpers/get-zaraz.ts | 16 ++++++++-- 24 files changed, 251 insertions(+), 35 deletions(-) create mode 100644 src/consent/get-api-ready.spec.ts create mode 100644 src/consent/get-api-ready.ts diff --git a/src/consent/get-all-checkboxes.spec.ts b/src/consent/get-all-checkboxes.spec.ts index 95616b0..c214fae 100644 --- a/src/consent/get-all-checkboxes.spec.ts +++ b/src/consent/get-all-checkboxes.spec.ts @@ -9,17 +9,27 @@ declare global { let windowObj: Window & typeof globalThis; +const warnSpy = jest.spyOn(console, 'warn'); + beforeAll(() => { windowObj = window; + warnSpy.mockImplementation(); }); afterAll(() => { window = windowObj; + warnSpy.mockRestore(); }); describe('getAllCheckboxes()', () => { - it('should return all checkboxes from zaraz consent', () => { + it("should return an empty object when the Zaraz consent API hasn't been initialised", () => { + const checkboxes = getAllCheckboxes(); + expect(checkboxes).toEqual({}); + }); + + it('should return an object of checkboxes when the Zaraz consent API has been initialised', () => { const checkboxesMock = { key1: true, key2: false }; + window.zaraz = { consent: { getAllCheckboxes: jest.fn().mockReturnValue(checkboxesMock), diff --git a/src/consent/get-all-checkboxes.ts b/src/consent/get-all-checkboxes.ts index e7301c4..6cbafcd 100644 --- a/src/consent/get-all-checkboxes.ts +++ b/src/consent/get-all-checkboxes.ts @@ -1,8 +1,12 @@ import { getZaraz } from '../helpers/get-zaraz'; +export type CheckboxStatuses = { + [key: string]: undefined | boolean; // The key is equal to the ID in Zaraz, e.g. "OVAL" or "DVEA". +}; + /** * Returns an object with the checkbox status of all purposes. */ -export function getAllCheckboxes(): { [key: string]: boolean } { - return getZaraz().consent.getAllCheckboxes(); +export function getAllCheckboxes(): CheckboxStatuses { + return getZaraz({ skipQueue: true })?.consent?.getAllCheckboxes() || {}; } diff --git a/src/consent/get-all.spec.ts b/src/consent/get-all.spec.ts index e808f99..1f1e9c2 100644 --- a/src/consent/get-all.spec.ts +++ b/src/consent/get-all.spec.ts @@ -9,17 +9,31 @@ declare global { let windowObj: Window & typeof globalThis; +const warnSpy = jest.spyOn(console, 'warn'); + beforeAll(() => { windowObj = window; + warnSpy.mockImplementation(); }); afterAll(() => { window = windowObj; + warnSpy.mockRestore(); }); describe('getAll()', () => { - it('should return all consents from zaraz consent', () => { - const consentsMock = { key1: true, key2: false }; + it("should return an empty object when the Zaraz consent API hasn't been initialised", () => { + const consents = getAll(); + expect(consents).toEqual({}); + }); + + it('should return an object of consents when the Zaraz consent API has been initialised', () => { + const consentsMock = { + key1: true, + key2: false, + key3: undefined, + }; + window.zaraz = { consent: { getAll: jest.fn().mockReturnValue(consentsMock), diff --git a/src/consent/get-all.ts b/src/consent/get-all.ts index 0324706..7fce28d 100644 --- a/src/consent/get-all.ts +++ b/src/consent/get-all.ts @@ -1,8 +1,21 @@ import { getZaraz } from '../helpers/get-zaraz'; +export type PurposeStatuses = { + [key: string]: undefined | boolean; // The key is equal to the ID in Zaraz, e.g. "OVAL" or "DVEA". +}; + /** - * Returns an object with the consent status of all purposes. + * Returns an object with the status of all purposes. + * + * ``` + * true: the consent has been given + * false: the consent has been denied + * undefined: the consent has never been given or seen + * ``` + * + * When Zaraz hasn't been initalised, it returns an empty object. */ -export function getAll(): { [key: string]: boolean } { - return getZaraz().consent.getAll(); + +export function getAll(): PurposeStatuses { + return getZaraz({ skipQueue: true })?.consent?.getAll() || {}; } diff --git a/src/consent/get-api-ready.spec.ts b/src/consent/get-api-ready.spec.ts new file mode 100644 index 0000000..0514132 --- /dev/null +++ b/src/consent/get-api-ready.spec.ts @@ -0,0 +1,41 @@ +import { getAPIReady } from './get-api-ready'; + +declare global { + interface Window { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + zaraz: any; + } +} + +let windowObj: Window & typeof globalThis; + +const warnSpy = jest.spyOn(console, 'warn'); + +beforeAll(() => { + windowObj = window; + warnSpy.mockImplementation(); +}); + +afterAll(() => { + window = windowObj; + warnSpy.mockRestore(); +}); + +describe('getAPIReady()', () => { + it("should return false when the Zaraz consent API hasn't been initialised", () => { + const apiReady = getAPIReady(); + expect(apiReady).toEqual(false); + }); + + it('should return true when the Zaraz consent API has been initialised', () => { + window.zaraz = { + consent: { + APIReady: true, + }, + }; + + const apiReady = getAPIReady(); + + expect(apiReady).toEqual(true); + }); +}); diff --git a/src/consent/get-api-ready.ts b/src/consent/get-api-ready.ts new file mode 100644 index 0000000..f4e4858 --- /dev/null +++ b/src/consent/get-api-ready.ts @@ -0,0 +1,13 @@ +import { getZaraz } from '../helpers/get-zaraz'; + +/** + * Indicates whether the Consent API is currently available on the page. + * + * ``` + * true: the Zaraz consent API is ready to be used with e.g. getAll(), getAllCheckboxes() etc. + * false: the Zaraz consent API is not ready to be used + * ``` + */ +export function getAPIReady(): boolean { + return getZaraz({ skipQueue: true })?.consent?.APIReady || false; +} diff --git a/src/consent/get-purposes.spec.ts b/src/consent/get-purposes.spec.ts index 63de7a4..8929de8 100644 --- a/src/consent/get-purposes.spec.ts +++ b/src/consent/get-purposes.spec.ts @@ -1,4 +1,4 @@ -import { getPurposes } from './get-purposes'; +import { getPurposes, Purposes } from './get-purposes'; declare global { interface Window { @@ -9,20 +9,38 @@ declare global { let windowObj: Window & typeof globalThis; +const warnSpy = jest.spyOn(console, 'warn'); + beforeAll(() => { windowObj = window; + warnSpy.mockImplementation(); }); afterAll(() => { window = windowObj; + warnSpy.mockRestore(); }); describe('getPurposes()', () => { - it('should return all purposes from zaraz consent', () => { - const purposesMock = { - key1: { name: 'Name 1', description: 'Description 1', order: 1 }, - key2: { name: 'Name 2', description: 'Description 2', order: 2 }, + it("should return an empty object when the Zaraz consent API isn't initialised", () => { + const purposes = getPurposes(); + expect(purposes).toEqual({}); + }); + + it('should return an object of purposes from the Zaraz consent API when Zaraz is initialised', () => { + const purposesMock: Purposes = { + ORFL: { + name: { en: 'Name 1' }, + description: { en: 'Description 1' }, + order: 100, + }, + OAVL: { + name: { en: 'Name 2' }, + description: { en: 'Description 2' }, + order: 200, + }, }; + window.zaraz = { consent: { purposes: purposesMock, diff --git a/src/consent/get-purposes.ts b/src/consent/get-purposes.ts index 382b3fa..f8aa4e2 100644 --- a/src/consent/get-purposes.ts +++ b/src/consent/get-purposes.ts @@ -1,14 +1,22 @@ import { getZaraz } from '../helpers/get-zaraz'; -type Purpose = { - name: string; - description: string; +export type Purpose = { order: number; + name: { + [key: string]: string; // the key is the language set in Zaraz (e.g. "en") + }; + description: { + [key: string]: string; // the key is the language set in Zaraz (e.g. "en") + }; +}; + +export type Purposes = { + [key: string]: Purpose; // the key is also the ID visible in Zaraz (e.g. "OrGL" or "OAVL") }; /** * An object containing all configured purposes, with their ID, name, description, and order. */ -export function getPurposes(): { [key: string]: Purpose } { - return getZaraz().consent.purposes; +export function getPurposes(): Purposes { + return getZaraz({ skipQueue: true })?.consent?.purposes || {}; } diff --git a/src/consent/get.spec.ts b/src/consent/get.spec.ts index db1b3cc..df4da94 100644 --- a/src/consent/get.spec.ts +++ b/src/consent/get.spec.ts @@ -9,17 +9,27 @@ declare global { let windowObj: Window & typeof globalThis; +const warnSpy = jest.spyOn(console, 'warn'); + beforeAll(() => { windowObj = window; + warnSpy.mockImplementation(); }); afterAll(() => { window = windowObj; + warnSpy.mockRestore(); }); describe('get()', () => { - it('should return the consent status for a purpose from zaraz consent', () => { + it("should return undefined when the Zaraz consent API hasn't been initialised", () => { + const consentStatus = get('key'); + expect(consentStatus).toBe(undefined); + }); + + it('should return the consent status for a purpose when the Zaraz consent API has been initialised', () => { const getMock = jest.fn().mockReturnValue(true); + window.zaraz = { consent: { get: getMock, diff --git a/src/consent/get.ts b/src/consent/get.ts index 7f11ebc..dbc90dd 100644 --- a/src/consent/get.ts +++ b/src/consent/get.ts @@ -1,7 +1,7 @@ import { getZaraz } from '../helpers/get-zaraz'; /** - * Get the current consent status for a purpose using the purpose ID. + * Get the consent status for a specific purpose using the purpose ID. * * ``` * true: The consent was granted. @@ -10,5 +10,5 @@ import { getZaraz } from '../helpers/get-zaraz'; * ``` */ export function get(purposeId: string): boolean | undefined { - return getZaraz().consent.get(purposeId); + return getZaraz({ skipQueue: true })?.consent?.get(purposeId) || undefined; } diff --git a/src/consent/index.spec.ts b/src/consent/index.spec.ts index 0e3271f..01d02f9 100644 --- a/src/consent/index.spec.ts +++ b/src/consent/index.spec.ts @@ -9,6 +9,7 @@ describe('consent', () => { setAll: expect.any(Function), getAllCheckboxes: expect.any(Function), getPurposes: expect.any(Function), + getAPIReady: expect.any(Function), setCheckboxes: expect.any(Function), setAllCheckboxes: expect.any(Function), sendQueuedEvents: expect.any(Function), diff --git a/src/consent/index.ts b/src/consent/index.ts index 65ff013..1fcf163 100644 --- a/src/consent/index.ts +++ b/src/consent/index.ts @@ -1,6 +1,7 @@ import { get } from './get'; import { getAll } from './get-all'; import { getAllCheckboxes } from './get-all-checkboxes'; +import { getAPIReady } from './get-api-ready'; import { getPurposes } from './get-purposes'; import { sendQueuedEvents } from './send-queued-events'; import { set } from './set'; @@ -14,6 +15,7 @@ export const consent = { getAll, setAll, getPurposes, + getAPIReady, getAllCheckboxes, setCheckboxes, setAllCheckboxes, diff --git a/src/consent/send-queued-event.spec.ts b/src/consent/send-queued-event.spec.ts index 12c6656..b7a4d62 100644 --- a/src/consent/send-queued-event.spec.ts +++ b/src/consent/send-queued-event.spec.ts @@ -9,17 +9,28 @@ declare global { let windowObj: Window & typeof globalThis; +const warnSpy = jest.spyOn(console, 'warn'); + beforeAll(() => { windowObj = window; + warnSpy.mockImplementation(); }); afterAll(() => { window = windowObj; + warnSpy.mockRestore(); }); describe('sendQueuedEvents()', () => { - it('should call sendQueuedEvents method on zaraz consent', () => { + it("should not break when the Zaraz consent API hasn't been initialised", () => { + const sendQueuedEventsMock = jest.fn(); + sendQueuedEvents(); + expect(sendQueuedEventsMock).toHaveBeenCalledTimes(0); + }); + + it('should call sendQueuedEventsMock when the Zaraz consent API has been initialised', () => { const sendQueuedEventsMock = jest.fn(); + window.zaraz = { consent: { sendQueuedEvents: sendQueuedEventsMock, diff --git a/src/consent/send-queued-events.ts b/src/consent/send-queued-events.ts index 70ef5d7..2d99b28 100644 --- a/src/consent/send-queued-events.ts +++ b/src/consent/send-queued-events.ts @@ -4,5 +4,5 @@ import { getZaraz } from '../helpers/get-zaraz'; * If some Pageview-based events were not sent due to a lack of consent, they can be sent using this method after consent was granted. */ export function sendQueuedEvents(): void { - getZaraz().consent.sendQueuedEvents(); + getZaraz({ skipQueue: true })?.consent?.sendQueuedEvents(); } diff --git a/src/consent/set-all-checkboxes.spec.ts b/src/consent/set-all-checkboxes.spec.ts index 29c3acc..67546e2 100644 --- a/src/consent/set-all-checkboxes.spec.ts +++ b/src/consent/set-all-checkboxes.spec.ts @@ -9,17 +9,28 @@ declare global { let windowObj: Window & typeof globalThis; +const warnSpy = jest.spyOn(console, 'warn'); + beforeAll(() => { windowObj = window; + warnSpy.mockImplementation(); }); afterAll(() => { window = windowObj; + warnSpy.mockRestore(); }); describe('setAllCheckboxes()', () => { - it('should call setAllCheckboxes method on zaraz consent with the correct argument', () => { + it("should not break when the Zaraz consent API hasn't been initialised", () => { + const setAllCheckboxesMock = jest.fn(); + setAllCheckboxes(true); + expect(setAllCheckboxesMock).toHaveBeenCalledTimes(0); + }); + + it('should call setAllCheckboxes method on the Zaraz consent API with the correct argument', () => { const setAllCheckboxesMock = jest.fn(); + window.zaraz = { consent: { setAllCheckboxes: setAllCheckboxesMock, diff --git a/src/consent/set-all-checkboxes.ts b/src/consent/set-all-checkboxes.ts index d238d9e..7b4f3a1 100644 --- a/src/consent/set-all-checkboxes.ts +++ b/src/consent/set-all-checkboxes.ts @@ -4,5 +4,5 @@ import { getZaraz } from '../helpers/get-zaraz'; * Set the checkboxStatus status for all purposes in the consent modal at once. */ export function setAllCheckboxes(checkboxStatus: boolean): void { - getZaraz().consent.setAllCheckboxes(checkboxStatus); + getZaraz({ skipQueue: true })?.consent?.setAllCheckboxes(checkboxStatus); } diff --git a/src/consent/set-all.spec.ts b/src/consent/set-all.spec.ts index 35e086b..adeea82 100644 --- a/src/consent/set-all.spec.ts +++ b/src/consent/set-all.spec.ts @@ -9,17 +9,27 @@ declare global { let windowObj: Window & typeof globalThis; +const warnSpy = jest.spyOn(console, 'warn'); + beforeAll(() => { windowObj = window; + warnSpy.mockImplementation(); }); afterAll(() => { window = windowObj; + warnSpy.mockRestore(); }); describe('setAll()', () => { - it('should call setAll method on zaraz consent with the correct argument', () => { + it("should not break when the Zaraz consent API hasn't been initialised", () => { + const result = setAll(true); + expect(result).toEqual(undefined); + }); + + it('should call setAll method on the Zaraz consent API with the correct argument', () => { const setAllMock = jest.fn(); + window.zaraz = { consent: { setAll: setAllMock, diff --git a/src/consent/set-all.ts b/src/consent/set-all.ts index 54e5e2b..09848b5 100644 --- a/src/consent/set-all.ts +++ b/src/consent/set-all.ts @@ -4,5 +4,5 @@ import { getZaraz } from '../helpers/get-zaraz'; * Set the consent status for all purposes at once. */ export function setAll(consentStatus: boolean): void { - getZaraz().consent.setAll(consentStatus); + getZaraz({ skipQueue: true })?.consent?.setAll(consentStatus); } diff --git a/src/consent/set-checkboxes.spec.ts b/src/consent/set-checkboxes.spec.ts index f02f2ca..f8c88bb 100644 --- a/src/consent/set-checkboxes.spec.ts +++ b/src/consent/set-checkboxes.spec.ts @@ -9,17 +9,27 @@ declare global { let windowObj: Window & typeof globalThis; +const warnSpy = jest.spyOn(console, 'warn'); + beforeAll(() => { windowObj = window; + warnSpy.mockImplementation(); }); afterAll(() => { window = windowObj; + warnSpy.mockRestore(); }); describe('setCheckboxes()', () => { - it('should call setCheckboxes method on zaraz consent with the correct argument', () => { + it("should not break when the Zaraz consent API hasn't been initialised", () => { + const result = setCheckboxes({}); + expect(result).toEqual(undefined); + }); + + it('should call setCheckboxes method on the Zaraz consent API with the correct argument', () => { const setCheckboxesMock = jest.fn(); + window.zaraz = { consent: { setCheckboxes: setCheckboxesMock, @@ -27,6 +37,7 @@ describe('setCheckboxes()', () => { }; const checkboxesStatus = { key1: true, key2: false }; + setCheckboxes(checkboxesStatus); expect(setCheckboxesMock).toHaveBeenCalledWith(checkboxesStatus); diff --git a/src/consent/set-checkboxes.ts b/src/consent/set-checkboxes.ts index b78b485..4d80e56 100644 --- a/src/consent/set-checkboxes.ts +++ b/src/consent/set-checkboxes.ts @@ -6,5 +6,5 @@ import { getZaraz } from '../helpers/get-zaraz'; export function setCheckboxes(checkboxesStatus: { [key: string]: boolean; }): void { - getZaraz().consent.setCheckboxes(checkboxesStatus); + getZaraz({ skipQueue: true })?.consent?.setCheckboxes(checkboxesStatus); } diff --git a/src/consent/set.spec.ts b/src/consent/set.spec.ts index febc75f..5c1bf75 100644 --- a/src/consent/set.spec.ts +++ b/src/consent/set.spec.ts @@ -9,17 +9,27 @@ declare global { let windowObj: Window & typeof globalThis; +const warnSpy = jest.spyOn(console, 'warn'); + beforeAll(() => { windowObj = window; + warnSpy.mockImplementation(); }); afterAll(() => { window = windowObj; + warnSpy.mockRestore(); }); describe('set()', () => { - it('should call set method on zaraz consent with the correct argument', () => { + it("should not break when the Zaraz consent API hasn't been initialised", () => { + const result = set({ key1: true, key2: false }); + expect(result).toEqual(undefined); + }); + + it('should call set method on the Zaraz consent API with the correct argument', () => { const setMock = jest.fn(); + window.zaraz = { consent: { set: setMock, diff --git a/src/consent/set.ts b/src/consent/set.ts index 0476980..4b1e2b8 100644 --- a/src/consent/set.ts +++ b/src/consent/set.ts @@ -1,8 +1,15 @@ import { getZaraz } from '../helpers/get-zaraz'; +export type ConsentPreferences = { [key: string]: boolean }; + /** - * Set the consent status for some purposes using the purpose ID. + * Set the consent status for a specific purpose using the purpose ID. + * + * ``` + * true: The consent was granted. + * false: The consent was not granted. + * ``` */ -export function set(consentPreferences: { [key: string]: boolean }): void { - getZaraz().consent.set(consentPreferences); +export function set(consentPreferences: ConsentPreferences): void { + getZaraz({ skipQueue: true })?.consent?.set(consentPreferences); } diff --git a/src/helpers/get-zaraz.spec.ts b/src/helpers/get-zaraz.spec.ts index e7a860f..fd5d864 100644 --- a/src/helpers/get-zaraz.spec.ts +++ b/src/helpers/get-zaraz.spec.ts @@ -14,11 +14,13 @@ declare global { let windowObj: Window & typeof globalThis; const logSpy = jest.spyOn(console, 'log'); +const warnSpy = jest.spyOn(console, 'warn'); beforeAll(() => { windowObj = window; logSpy.mockImplementation(); + warnSpy.mockImplementation(); }); afterAll(() => { @@ -27,9 +29,17 @@ afterAll(() => { }); logSpy.mockRestore(); + warnSpy.mockRestore(); }); describe('getZaraz()', () => { + it("should return undefined when skipQueue is true and zaraz doesn't exist on the window", () => { + expect(getZaraz({ skipQueue: true })).toEqual(undefined); + expect(warnSpy).toHaveBeenCalledWith( + `[zaraz-ts] Zaraz Web API hasn't been initialized.`, + ); + }); + it('should return zaraz when it exists on the window', () => { window.zaraz = { track: trackMock, diff --git a/src/helpers/get-zaraz.ts b/src/helpers/get-zaraz.ts index 25070a6..d31abb1 100644 --- a/src/helpers/get-zaraz.ts +++ b/src/helpers/get-zaraz.ts @@ -5,17 +5,29 @@ type QueueItem = { let queue: QueueItem[] = []; +type GetZarazOptions = { + skipQueue?: boolean; +}; + /** * A utility that checks if zaraz exists on the window object. If not it queues * the events until zaraz is initialized. If zaraz is initialized, it flushes * the queue. */ -export function getZaraz() { +export function getZaraz(options?: GetZarazOptions) { + const skipQueue = options?.skipQueue || false; + if (typeof window === 'undefined') { throw new Error(`Cannot use Zaraz Web API, because window is not defined.`); } - if (!window?.zaraz) { + if (!window?.zaraz && skipQueue) { + // eslint-disable-next-line no-console + console.warn(`[zaraz-ts] Zaraz Web API hasn't been initialized.`); + return undefined; + } + + if (!window?.zaraz && !skipQueue) { // eslint-disable-next-line no-console console.log(`Zaraz Web API is not initialized. Queueing events...`);