/** @jest-environment jsdom */ import { useJourneyTestRuntime } from '../../__test-utils__/test-runtime'; import { exposeAppJourney, type WindowWithAppJourney } from '../../integrations/jquery'; const emitMock = jest.fn(); function createTarget(existingApp?: Record) { return { App: existingApp ? { ...existingApp } : undefined, } as any; } describe('[journey] exposeAppJourney', () => { useJourneyTestRuntime(emitMock); test('creates window.App when missing and assigns Journey', () => { const target = createTarget(); const api = exposeAppJourney(target); expect(target.App).toBeDefined(); expect(target.App.Journey).toBe(api); expect(Object.isFrozen(target.App.Journey)).toBe(true); expect(() => { target.App.Journey.defineJourney = () => { throw new Error('swapped'); }; }).toThrow(); expect(typeof api.defineJourney).toBe('function'); expect(typeof api.journeyStep).toBe('function'); expect(typeof api.firstOpenJourneyName).toBe('function'); }); test('preserves existing window.App properties', () => { const target = createTarget({ CallBoard: { Instance: {} } }); exposeAppJourney(target); expect(target.App.CallBoard).toEqual({ Instance: {} }); expect(target.App.Journey).toBeDefined(); }); describe('with defineJourney', () => { let j: ReturnType; let bookJob: ReturnType; beforeEach(() => { j = exposeAppJourney(createTarget()); bookJob = j.defineJourney({ name: 'ko-bridge-good', team: 'test', group: 'test', service: 'test', timeoutMs: 0, }); }); test('emits a good outcome via handle lifecycle', async () => { bookJob.start(); await bookJob.step('book-job', step => { step.setAttribute('call.id', 42); return 'ok'; }); bookJob.complete(); expect(emitMock).toHaveBeenCalledTimes(1); const event = emitMock.mock.calls[0][0].journey; expect(event.name).toBe('ko-bridge-good'); expect(event.outcome).toBe('good'); expect(event.steps).toHaveLength(1); expect(event.steps[0].attributes).toEqual({ 'call.id': 42 }); }); test('emits a bad outcome via handle.fail', () => { bookJob = j.defineJourney({ name: 'ko-bridge-fail', team: 'test', group: 'test', service: 'test', timeoutMs: 0, }); bookJob.start(); bookJob.fail('book-rejected'); expect(emitMock).toHaveBeenCalledTimes(1); const event = emitMock.mock.calls[0][0].journey; expect(event.outcome).toBe('bad'); expect(event.reason).toBe('book-rejected'); }); test('emits excluded via handle.exclude', () => { bookJob = j.defineJourney({ name: 'ko-bridge-exclude', team: 'test', group: 'test', service: 'test', timeoutMs: 0, }); bookJob.start(); bookJob.exclude(); expect(emitMock).toHaveBeenCalledTimes(1); expect(emitMock.mock.calls[0][0].journey.outcome).toBe('excluded'); }); test('applies setTags before complete', async () => { bookJob = j.defineJourney({ name: 'ko-bridge-tags', team: 'test', group: 'test', service: 'test', timeoutMs: 0, expected: ['load'], }); bookJob.start(); expect(j.firstOpenJourneyName(['missing', 'ko-bridge-tags'])).toBe('ko-bridge-tags'); await bookJob.step('load', step => { step.setAttribute('phase', 'load'); }); bookJob.setTags({ 'job.id': 9 }); bookJob.complete(); expect(emitMock).toHaveBeenCalledTimes(1); const event = emitMock.mock.calls[0][0].journey; expect(event.tags).toEqual({ 'job.id': 9 }); expect(event.steps[0].attributes).toEqual({ phase: 'load' }); }); }); test('journeyStep supports candidate lists without a handle', async () => { const j = exposeAppJourney(createTarget()); const primary = j.defineJourney({ name: 'ko-candidates-a', team: 'test', group: 'test', service: 'test', timeoutMs: 0, }); primary.start(); await j.journeyStep(['missing', 'ko-candidates-a'], 'shared-step', () => 'ok'); primary.complete(); expect(emitMock).toHaveBeenCalledTimes(1); expect(emitMock.mock.calls[0][0].journey.steps[0].name).toBe('shared-step'); }); test('is idempotent when called repeatedly on window', () => { const first = exposeAppJourney(window as WindowWithAppJourney); const second = exposeAppJourney(window as WindowWithAppJourney); expect(second).toBe(first); expect((window as WindowWithAppJourney).App?.Journey).toBe(first); }); });