/** @vitest-environment jsdom */ /** * Replay actually starts when an already-consented shopper opens the modal. * * This is the ordinary case — most shoppers on a store whose CMP already * granted never fire a consent-change event at all — and it is the one no * existing test covered. `replay.test.ts` drives `startReplay` directly with an * artificial `stillAllowed: () => true`, which is precisely the condition the * widget was failing to satisfy. * * The real module is used here, not a mock: what has to be proved is that * `startSessionRecording` is *reached*, and only once the modal overlay exists. * Recording before the overlay is in the DOM would take a snapshot in which the * confinement target does not exist — every node blocked, a recording of * nothing, and no error anywhere. */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, cleanup, fireEvent, waitFor } from '@testing-library/react'; import { App } from './App'; import { MODAL_OVERLAY_CLASS, isRecording, resetReplayForTests } from './replay'; import { en } from './i18n/en'; import type { WidgetConfig } from './types'; vi.mock( './imageUtils', () => ( { fetchImageAsBase64: vi.fn().mockResolvedValue( 'data:image/png;base64,G' ), downloadImage: vi.fn(), fileToBase64: vi.fn(), resizeAvatarImage: vi.fn(), isValidImageFile: () => true, } ) ); const config = { apiBaseUrl: 'https://brain.example.com/api/storefront/tryon', publishableKey: 'pk_test', productImage: 'https://cdn.example.com/shirt.jpg', productTitle: 'Linen Shirt', productId: '555', locale: 'en', analytics: { replayKey: 'phc_browser', ingestHost: 'https://ingest.aisthetix.fyi', replayEnabled: true, environment: 'staging', storeId: 'store_42', }, } as unknown as WidgetConfig; interface FakeSdk { init: ReturnType< typeof vi.fn >; startSessionRecording: ReturnType< typeof vi.fn >; stopSessionRecording: ReturnType< typeof vi.fn >; register: ReturnType< typeof vi.fn >; opt_out_capturing: ReturnType< typeof vi.fn >; opt_in_capturing: ReturnType< typeof vi.fn >; } let sdk: FakeSdk; /** Whether the overlay was in the DOM at the moment recording started. */ let overlayAtStart: boolean | null; /** The injected script tag, so the test decides when the SDK "arrives". */ let pending: HTMLScriptElement | null; function interceptScriptInjection() { pending = null; const original = document.head.appendChild.bind( document.head ); vi.spyOn( document.head, 'appendChild' ).mockImplementation( ( ( node: Node ) => { if ( node instanceof HTMLScriptElement ) { pending = node; return node; } return original( node ); } ) as typeof document.head.appendChild ); } function deliverSdk() { ( window as unknown as { posthog?: FakeSdk } ).posthog = sdk; pending!.onload?.( new Event( 'load' ) ); } /** The bootstrap's answer, which is the only consent signal this widget has. */ function setConsent( allowed: boolean ) { ( window as unknown as { aisthetixProductAnalyticsAllowed?: () => boolean } ) .aisthetixProductAnalyticsAllowed = () => allowed; } beforeEach( () => { localStorage.clear(); sessionStorage.clear(); document.body.innerHTML = ''; resetReplayForTests(); delete ( window as unknown as { posthog?: unknown } ).posthog; overlayAtStart = null; sdk = { // posthog-js's `init(key, options, name)` returns the NAMED instance we // record through — never the merchant's default one. init: vi.fn( () => sdk ), startSessionRecording: vi.fn( () => { overlayAtStart = document.querySelector( `.${ MODAL_OVERLAY_CLASS }` ) !== null; } ), stopSessionRecording: vi.fn(), register: vi.fn(), opt_out_capturing: vi.fn(), opt_in_capturing: vi.fn(), }; setConsent( true ); } ); afterEach( () => { cleanup(); document.body.innerHTML = ''; delete ( window as unknown as { aisthetixProductAnalyticsAllowed?: unknown } ) .aisthetixProductAnalyticsAllowed; vi.restoreAllMocks(); } ); async function openModal() { const trigger = document.createElement( 'button' ); trigger.id = 'aisthetix-tryon-trigger'; document.body.appendChild( trigger ); render( ); fireEvent.click( trigger ); // The overlay is the widget being open, and it is also what the recording is // confined to — so waiting on it is waiting on exactly the right thing. await waitFor( () => expect( document.querySelector( `.${ MODAL_OVERLAY_CLASS }` ) ).not.toBeNull() ); await waitFor( () => expect( screen.getAllByText( en.upload.title ).length ).toBeGreaterThan( 0 ) ); } describe( 'an already-consented shopper opening the modal', () => { it( 'starts recording — the case no consent-change event ever announces', async () => { interceptScriptInjection(); await openModal(); await waitFor( () => expect( pending, 'the recorder was never even requested' ).not.toBeNull() ); deliverSdk(); await waitFor( () => expect( sdk.startSessionRecording ).toHaveBeenCalledTimes( 1 ) ); expect( isRecording() ).toBe( true ); } ); it( 'does not record before the overlay it is confined to exists', async () => { interceptScriptInjection(); await openModal(); await waitFor( () => expect( pending ).not.toBeNull() ); deliverSdk(); await waitFor( () => expect( sdk.startSessionRecording ).toHaveBeenCalled() ); expect( overlayAtStart, 'the confinement target must be in the DOM first' ).toBe( true ); } ); it( 'does not start when the shopper closes the modal before the SDK arrives', async () => { interceptScriptInjection(); await openModal(); await waitFor( () => expect( pending ).not.toBeNull() ); fireEvent.click( screen.getByLabelText( 'Close' ) ); await waitFor( () => expect( document.querySelector( `.${ MODAL_OVERLAY_CLASS }` ) ).toBeNull() ); deliverSdk(); await new Promise( ( resolve ) => setTimeout( resolve, 0 ) ); expect( sdk.startSessionRecording ).not.toHaveBeenCalled(); expect( isRecording() ).toBe( false ); } ); it( 'requests nothing at all when the shopper has not consented', async () => { setConsent( false ); interceptScriptInjection(); await openModal(); await new Promise( ( resolve ) => setTimeout( resolve, 0 ) ); expect( pending, 'loading the SDK is itself a request they never agreed to' ).toBeNull(); } ); } );