/// /// /* eslint-disable no-undef */ import { ZywaveAnalyticsElement } from "@zywave/zywave-analytics"; import { mockFetch } from "../../../../test/src/util/mock-fetch"; import user from "./fixtures/user-info"; import analytics from "./fixtures/analytics-info"; import { awaitEvent, randObject, randString, sleep } from "../../../../test/src/util/helpers"; import { assert } from "@esm-bundle/chai"; import type { HeapGlobal, AppcuesGlobal } from "@zywave/zywave-analytics/dist/analytics-configuration"; const TEST_API_BASE_URL = `${window.location.origin}/`; const path = window.location.href.substring(0, window.location.href.lastIndexOf("/")); const CDN_HOST_URL = `${path}/node_modules/@zywave/zywave-analytics/test/cdn/`; const mockedFetch = mockFetch() .get( (url) => url.toString().startsWith(`${TEST_API_BASE_URL}userinfo`), new Promise((resolve) => sleep(10).then(() => resolve(user))), ) .get( (url) => url.toString().startsWith(`${TEST_API_BASE_URL}shell/v2.0/analyticsinfo`), new Promise((resolve) => sleep(10).then(() => resolve(analytics))), ) .post( (url) => url.toString().startsWith(`${TEST_API_BASE_URL}shell/v2.0/analyticsinfo/track`), new Promise((resolve) => sleep(10).then(() => resolve(new Response(null, { status: 204 })))), ); const HEAP_APP_ID = "test"; const APPCUES_ACCOUNT_ID = "test"; suite("zywave-analytics", () => { let element: ZywaveAnalyticsElement; setup(() => { element = document.createElement("zywave-analytics"); element.setAttribute("cdn-host", CDN_HOST_URL); element.setAttribute("api-base-url", TEST_API_BASE_URL); document.body.appendChild(element); }); teardown(() => { element.remove(); const analyticsScriptElements = document.querySelectorAll("script[src*='test/cdn/']"); for (const script of analyticsScriptElements) { script.remove(); } window.heap = undefined; window.Appcues = undefined; }); test("initializes as a zywave-analytics", () => { assert.instanceOf(element, ZywaveAnalyticsElement); }); test("fires load event", async () => { element.setAttribute("bearer-token", randString()); await awaitEvent(element, "load"); assert.isTrue(true, "load event not fired"); }); test("merges user properties", async () => { element.setAttribute("bearer-token", randString()); const userProp = {}; const key = randString(); userProp[key] = randString(); userProp["givenName"] = randString(); element.setAttribute("user-properties", JSON.stringify(userProp)); await awaitEvent(element, "load"); const userProperties = (element as any)._userProperties; assert.exists(userProperties); assert.equal(userProperties[key], userProp[key]); assert.equal(userProperties["givenName"], user.given_name); assert.equal(userProperties["familyName"], user.family_name); }); test("heap-app-id configures global heap", async () => { element.setAttribute("heap-app-id", HEAP_APP_ID); await awaitEvent(element, "load"); assert.isDefined(window.heap, "heap is not defined"); assert.equal(window.heap?.appid, HEAP_APP_ID); }); test("heap-app-id and identity configure global heap with identify param", async () => { const userProp = {}; const key = randString(); userProp[key] = randString(); const identity = "me"; element.setAttribute("heap-app-id", HEAP_APP_ID); element.setAttribute("identity", identity); element.setAttribute("user-properties", JSON.stringify(userProp)); await awaitEvent(element, "load"); assert.isDefined(window.heap, "heap is not defined"); const identify = window.heap?.find((x) => x[0] === "identify"); assert.isDefined(identify, "identify not in heap array"); assert.equal(identify![1], identity); const addedUserProperties = window.heap?.find((x) => x[0] === "addUserProperties")?.[1]; assert.exists(addedUserProperties, "addUserProperties not in heap array"); assert.equal(addedUserProperties[key], userProp[key]); assert.exists(addedUserProperties["languages"], "languages not in addedUserProperties"); for (const language of window.navigator.languages) { assert.include(addedUserProperties["languages"], `[${language.toLowerCase()}]`); } }); test("heap configures event properties", async () => { const identity = "me"; element.setAttribute("heap-app-id", HEAP_APP_ID); element.setAttribute("identity", identity); await awaitEvent(element, "load"); assert.isDefined(window.heap, "heap is not defined"); const identify = window.heap?.find((x) => x[0] === "identify"); assert.isDefined(identify, "identify not in heap array"); assert.equal(identify![1], identity); const addEventProperties = window.heap?.find((x) => x[0] === "addEventProperties"); assert.isArray(addEventProperties); const props = addEventProperties![1]; assert.exists(props); assert.equal(props.screenHeight, window.screen.height); assert.equal(props.screenWidth, window.screen.width); assert.equal(props.screenResolution, `${window.screen.width}x${window.screen.height}`); if (window.navigator.connection) { const connection = window.navigator.connection; assert.equal(props.networkConnectionType, connection.type === "unknown" ? undefined : connection.type); assert.equal(props.networkReducedData, connection.saveData ?? false); assert.equal(props.networkRtt, connection.rtt); assert.equal(props.networkDownlink, connection.downlink); assert.equal(props.networkDownlinkMax, connection.downlinkMax); } }); test("appcues-account-id loads appcues script", async () => { element.setAttribute("appcues-account-id", APPCUES_ACCOUNT_ID); await awaitEvent(element, "load"); const appcuesScript = document.querySelector("script[src*='fast.appcues.com']"); assert.isNotNull(appcuesScript); assert.isDefined(appcuesScript); }); test("appcues skipAMD flag set", async () => { element.setAttribute("appcues-account-id", APPCUES_ACCOUNT_ID); await awaitEvent(element, "load"); assert.isNotNull(window.AppcuesSettings); assert.isDefined(window.AppcuesSettings); assert.isTrue(window.AppcuesSettings?.skipAMD); }); test("pushState is tracked as page views", async () => { element.setAttribute("heap-app-id", HEAP_APP_ID); element.setAttribute("appcues-account-id", APPCUES_ACCOUNT_ID); element.setAttribute("identity", randString()); await awaitEvent(element, "load"); const timeout = 1_000; const heapEmit = awaitEvent((window.heap as HeapGlobal & TestHook).zywave.publisher, "track", timeout); const appcuesEmit = awaitEvent((window.Appcues as AppcuesGlobal & TestHook).zywave.publisher, "page", timeout); window.dispatchEvent(new CustomEvent("zapiPushState")); const [heapResult, appcuesResult] = await Promise.allSettled([heapEmit, appcuesEmit]); assert.equal(heapResult.status, "fulfilled", "Heap not tracked"); assert.equal(appcuesResult.status, "fulfilled", "AppCues not tracked"); }); test("replaceState is tracked as page views", async () => { element.setAttribute("heap-app-id", HEAP_APP_ID); element.setAttribute("appcues-account-id", APPCUES_ACCOUNT_ID); element.setAttribute("identity", randString()); await awaitEvent(element, "load"); const timeout = 1_000; const heapEmit = awaitEvent((window.heap as HeapGlobal & TestHook).zywave.publisher, "track", timeout); const appcuesEmit = awaitEvent((window.Appcues as AppcuesGlobal & TestHook).zywave.publisher, "page", timeout); window.dispatchEvent(new CustomEvent("zapiReplaceState")); const [heapResult, appcuesResult] = await Promise.allSettled([heapEmit, appcuesEmit]); assert.equal(heapResult.status, "fulfilled", "Heap not tracked"); assert.equal(appcuesResult.status, "fulfilled", "AppCues not tracked"); }); test("setting bearer-token loads userinfo", async () => { element.setAttribute("bearer-token", randString()); const { id, oldId } = mockedFetch.freshRun(); await awaitEvent(element, "load"); const userInfoRequest = mockedFetch.requests.find((x) => { if (x.id !== id) { return false; } let url: URL; if (typeof x.request === "string") { url = new URL(x.request); } else { url = new URL(x.request.url); } return url.toString().startsWith(`${TEST_API_BASE_URL}userinfo`); }); assert.exists(userInfoRequest, "API call to load user info not made"); mockedFetch.freshRun(oldId); }); test("track method propagates to analytics scripts", async () => { element.setAttribute("heap-app-id", HEAP_APP_ID); element.setAttribute("appcues-account-id", APPCUES_ACCOUNT_ID); const identity = "me"; element.setAttribute("identity", identity); await awaitEvent(element, "load"); const eventName = randString(); const payload = randObject(); element.track(eventName, payload); await sleep(10); const heapData = (window.heap as HeapGlobal & TestHook).zywave.data.at(-1); assert.exists(heapData); assert.equal(heapData![0], eventName, "Event name not propagated to Heap"); assert.equal(heapData![1], payload, "Event payload not propagated to Heap"); }); }); declare global { interface Window { heap?: HeapGlobal; Appcues?: AppcuesGlobal; AppcuesSettings?: { skipAMD: boolean }; } } type TestHook = { zywave: { data: unknown[][]; reset: () => void; publisher: EventTarget; }; };