/// /// /* eslint-disable no-undef */ import { Fingerprinter } from "@zywave/zywave-analytics/dist/fingerprinter.js"; import { assert } from "@esm-bundle/chai"; import type { FingerprintAttributes } from "@zywave/zywave-analytics/dist/fingerprinter.js"; async function sha256(source: string) { const sourceBytes = new TextEncoder().encode(source); const digest = await crypto.subtle.digest("SHA-256", sourceBytes); const resultBytes = [...new Uint8Array(digest)]; return resultBytes.map((x) => x.toString(16).padStart(2, "0")).join(""); } function buildTestCanvas() { const canvas = document.createElement("canvas"); const context = canvas.getContext("2d")!; const txt = "ZYWAVE"; context.textBaseline = "top"; context.textBaseline = "top"; context.font = "14px 'Arial'"; context.textBaseline = "alphabetic"; context.fillStyle = "#f60"; context.fillRect(125, 1, 62, 20); context.fillStyle = "#069"; context.fillText(txt, 2, 15); context.fillStyle = "rgba(102, 204, 0, 0.7)"; context.fillText(txt, 4, 17); return { canvas, context }; } suite("Fingerprinter", () => { let attributes: FingerprintAttributes; const storageTtlDays = 1; const cookieDomain = ".example.com"; setup(async () => { attributes = await Fingerprinter.getAttributes({ storageTtlDays, cookieDomain }); }); test("attributes are defined", () => { assert.isDefined(attributes.canvas); assert.isDefined(attributes.storage); assert.isDefined(attributes.screenWidth); assert.isDefined(attributes.screenHeight); assert.isDefined(attributes.timeZone); assert.isDefined(attributes.doNotTrack); assert.isDefined(attributes.gpc); assert.isDefined(attributes.prefersReducedMotion); assert.isDefined(attributes.prefersColorScheme); }); test("canvas fingerprint is expected canvas drawing", async () => { const { canvas } = buildTestCanvas(); const dataUrl = canvas.toDataURL(); const data = dataUrl.split(",")[1]; const expectedCanvasFingerprint = `v1|${await sha256(data)}`; assert.equal(attributes.canvas, expectedCanvasFingerprint); }); // localhost testing and Secure cookies don't mix well test.skip("storage fingerprint sets cookie and localStorage", () => { const key = "zapi:bf"; const value = `; ${document.cookie}`; const parts = value.split(`; ${key}=`); const cookieValue = parts.length === 2 ? parts.pop()?.split(";").shift() : null; assert.equal(cookieValue, attributes.storage); const localStorageValue = localStorage.getItem(key); assert.isNotNull(localStorageValue); assert.isDefined(localStorageValue); const localStorageParsedValue = JSON.parse(localStorageValue!) as ExpirableLocalStorageItem; assert.isDefined(localStorageParsedValue.expires); assert.equal(localStorageParsedValue.value, attributes.storage); }); test("timeZone is pulled from Intl", () => { const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; assert.equal(attributes.timeZone, timeZone); }); test("screenWidth and screenHeight are pulled from Screen", () => { assert.equal(attributes.screenWidth, window.screen.width); assert.equal(attributes.screenHeight, window.screen.height); }); test("doNotTrack is pulled from Navigator", () => { assert.equal(attributes.doNotTrack, navigator.doNotTrack); }); test("gpc is pulled from Navigator", () => { assert.equal(attributes.gpc, navigator.globalPrivacyControl); }); test("prefersReducedMotion is pulled from matchMedia", () => { assert.equal(attributes.prefersReducedMotion, window.matchMedia("(prefers-reduced-motion: reduce)").matches); }); test("prefersColorScheme is pulled from matchMedia", () => { const prefersColorScheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; assert.equal(attributes.prefersColorScheme, prefersColorScheme); }); }); type ExpirableLocalStorageItem = { value: string; expires: number; };