import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { hasLead, rememberLead, isValidEmail, shouldAskForEmail, submitLead, } from './leadStore'; import type { WidgetConfig } from '../types'; const demo: WidgetConfig = { apiBaseUrl: 'https://brain.example/api/storefront/tryon', publishableKey: 'pk_test', productImage: 'https://shop.example/garment.jpg', productTitle: 'Amalfi Linen Dress', productId: '12', demo: true, demoLeadUrl: '/wp-json/vela-demo/v1/lead', }; beforeEach( () => localStorage.clear() ); describe( 'shouldAskForEmail', () => { it( 'leaves the first try-on free', () => { expect( shouldAskForEmail( demo, 0 ) ).toBe( false ); } ); it( 'asks from the second one on', () => { expect( shouldAskForEmail( demo, 1 ) ).toBe( true ); expect( shouldAskForEmail( demo, 4 ) ).toBe( true ); } ); // The gate is a demo funnel. It must never appear on a merchant's storefront. it( 'never asks on a merchant store', () => { expect( shouldAskForEmail( { ...demo, demo: false }, 3 ) ).toBe( false ); } ); it( 'never asks when there is nowhere to send the lead', () => { expect( shouldAskForEmail( { ...demo, demoLeadUrl: undefined }, 3 ) ).toBe( false ); } ); it( 'stops asking once the address has been given', () => { rememberLead( 'buyer@store.co' ); expect( hasLead() ).toBe( true ); expect( shouldAskForEmail( demo, 3 ) ).toBe( false ); } ); // Declining is a real way out, not a permanent skip: the visitor keeps what // they made, and the ask returns only when they come back for another render. // If declining silenced the gate for good, the gate would be theatre. it( 'asks again the next time a visitor who declined wants another render', () => { expect( shouldAskForEmail( demo, 1 ) ).toBe( true ); expect( shouldAskForEmail( demo, 1 ) ).toBe( true ); } ); } ); describe( 'isValidEmail', () => { it( 'accepts an ordinary address', () => { expect( isValidEmail( 'buyer@store.co' ) ).toBe( true ); } ); it( 'ignores surrounding whitespace', () => { expect( isValidEmail( ' buyer@store.co ' ) ).toBe( true ); } ); it( 'rejects the obvious typos', () => { expect( isValidEmail( '' ) ).toBe( false ); expect( isValidEmail( 'buyer' ) ).toBe( false ); expect( isValidEmail( 'buyer@store' ) ).toBe( false ); expect( isValidEmail( 'buyer store.co' ) ).toBe( false ); } ); } ); describe( 'submitLead', () => { afterEach( () => { Reflect.deleteProperty( globalThis, 'fetch' ); } ); it( 'posts the address and the consent as JSON', async () => { const fetchMock = vi.fn().mockResolvedValue( { ok: true } ); globalThis.fetch = fetchMock as unknown as typeof fetch; await expect( submitLead( '/wp-json/vela-demo/v1/lead', { email: 'buyer@store.co', consent: true, productId: '12', } ) ).resolves.toBe( true ); const [ url, init ] = fetchMock.mock.calls[ 0 ]; expect( url ).toBe( '/wp-json/vela-demo/v1/lead' ); expect( init.method ).toBe( 'POST' ); expect( JSON.parse( init.body ) ).toMatchObject( { email: 'buyer@store.co', consent: true, productId: '12', } ); } ); it( 'reports failure on a refused request rather than a phantom success', async () => { globalThis.fetch = vi.fn().mockResolvedValue( { ok: false } ) as unknown as typeof fetch; await expect( submitLead( '/lead', { email: 'buyer@store.co', consent: true } ) ).resolves.toBe( false ); } ); it( 'reports failure when the network is gone', async () => { globalThis.fetch = vi.fn().mockRejectedValue( new Error( 'offline' ) ) as unknown as typeof fetch; await expect( submitLead( '/lead', { email: 'buyer@store.co', consent: true } ) ).resolves.toBe( false ); } ); } );