///
///
/* 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;
};
};