/** @jest-environment jsdom */ import { baseJourneyDef, lastJourney, useJourneyTestRuntime, } from '../../__test-utils__/test-runtime'; import { completeJourney, excludeJourney, failJourney, journeyStep, setJourneyTags, } from '../../core'; import { moduleRuntime } from '../../core/runtime'; const emitMock = jest.fn(); const baseDef = { ...baseJourneyDef, name: 'api-lifecycle' }; const otherDef = { ...baseJourneyDef, name: 'api-other' }; describe('[journey] api', () => { useJourneyTestRuntime(emitMock); describe('firstOpenJourneyName', () => { const names = [baseDef.name, otherDef.name]; const subject = () => moduleRuntime.firstOpenJourneyName(names); describe('when none are open', () => { test('returns null', () => { expect(subject()).toBeNull(); }); }); describe('when only the second name is open', () => { beforeEach(() => { moduleRuntime.startJourney(otherDef); }); test('returns that name', () => { expect(subject()).toBe(otherDef.name); }); }); describe('when both are open', () => { beforeEach(() => { moduleRuntime.startJourney(otherDef); moduleRuntime.startJourney(baseDef); }); test('returns the first match in order', () => { expect(subject()).toBe(baseDef.name); expect(moduleRuntime.firstOpenJourneyName([otherDef.name, baseDef.name])).toBe( otherDef.name ); }); }); }); describe('completeJourney', () => { describe('with tags', () => { beforeEach(() => { moduleRuntime.startJourney(baseDef); }); const subject = () => completeJourney(baseDef.name, { 'lead.id': 7 }); test('emits good with tags', () => { subject(); expect(lastJourney(emitMock).outcome).toBe('good'); expect(lastJourney(emitMock).tags['lead.id']).toBe(7); }); }); describe('with null name', () => { const subject = () => completeJourney(null, { a: 1 }); test('is a no-op', () => { expect(() => subject()).not.toThrow(); expect(emitMock).not.toHaveBeenCalled(); }); }); describe('when already closed', () => { beforeEach(async () => { moduleRuntime.startJourney(baseDef); await journeyStep(baseDef.name, 'do-work', step => { step.completeJourney(); }); }); const subject = () => completeJourney(baseDef.name, { ignored: true }); test('does not emit again', () => { subject(); expect(emitMock).toHaveBeenCalledTimes(1); }); }); describe('after a fail', () => { beforeEach(async () => { moduleRuntime.startJourney(baseDef); await journeyStep(baseDef.name, 'do-work', step => { step.failJourney('validation'); }); }); const subject = () => completeJourney(baseDef.name, { ignored: true }); test('does not flip the outcome', () => { subject(); expect(emitMock).toHaveBeenCalledTimes(1); expect(lastJourney(emitMock).outcome).toBe('bad'); }); }); }); describe('startJourney', () => { describe('when restarting the same name', () => { beforeEach(() => { moduleRuntime.startJourney(baseDef); }); const subject = () => moduleRuntime.startJourney(baseDef); test('excludes the prior open journey', () => { subject(); expect(lastJourney(emitMock).outcome).toBe('excluded'); }); }); describe('when restarting after a bad close', () => { beforeEach(async () => { moduleRuntime.startJourney(baseDef); await journeyStep(baseDef.name, 'do-work', step => { step.failJourney('validation'); }); }); const subject = () => moduleRuntime.startJourney(baseDef); test('opens fresh without a second exclude', () => { subject(); expect(emitMock).toHaveBeenCalledTimes(1); completeJourney(baseDef.name); expect(lastJourney(emitMock).outcome).toBe('good'); }); }); describe('with static def tags', () => { const subject = () => { moduleRuntime.startJourney({ ...baseDef, tags: { plan: 'pro' } }); completeJourney(baseDef.name, { 'job.id': 5 }); }; test('merges into the event', () => { subject(); expect(lastJourney(emitMock).tags).toEqual({ 'plan': 'pro', 'job.id': 5 }); }); }); }); describe('failJourney', () => { beforeEach(() => { moduleRuntime.startJourney(baseDef); }); const subject = () => failJourney(baseDef.name, 'manual-bad'); test('marks bad', () => { subject(); expect(lastJourney(emitMock).outcome).toBe('bad'); expect(lastJourney(emitMock).reason).toBe('manual-bad'); }); describe('when called twice', () => { test('keeps the first reason', () => { failJourney(baseDef.name, 'first-breach'); failJourney(baseDef.name, 'second-breach'); expect(emitMock).toHaveBeenCalledTimes(1); expect(lastJourney(emitMock).reason).toBe('first-breach'); }); }); }); describe('excludeJourney', () => { describe('with a clean journey', () => { beforeEach(() => { moduleRuntime.startJourney(otherDef); }); const subject = () => excludeJourney(otherDef.name); test('marks excluded', () => { subject(); expect(lastJourney(emitMock).outcome).toBe('excluded'); }); }); describe('after a breach', () => { beforeEach(async () => { moduleRuntime.startJourney(baseDef); await journeyStep(baseDef.name, 'do-work', step => { step.failJourney('validation'); }); emitMock.mockClear(); }); const subject = () => excludeJourney(baseDef.name); test('does not emit again', () => { subject(); expect(emitMock).not.toHaveBeenCalled(); }); }); describe('when past the journey budget', () => { beforeEach(() => { jest.spyOn(globalThis.performance, 'now').mockReturnValue(1_000); moduleRuntime.startJourney({ ...baseDef, timeoutMs: 50 }); (globalThis.performance.now as jest.Mock).mockReturnValue(1_100); }); afterEach(() => { jest.restoreAllMocks(); }); const subject = () => excludeJourney(baseDef.name); test('forces bad with journey-timeout', () => { subject(); expect(lastJourney(emitMock).outcome).toBe('bad'); expect(lastJourney(emitMock).reason).toBe('journey-timeout'); }); }); }); describe('setJourneyTags', () => { describe('with null name', () => { const subject = () => setJourneyTags(null, { a: 1 }); test('is a no-op', () => { expect(() => subject()).not.toThrow(); }); }); describe('with a closed journey', () => { beforeEach(() => { moduleRuntime.startJourney(baseDef); completeJourney(baseDef.name); }); const subject = () => setJourneyTags(baseDef.name, { a: 1 }); test('is a no-op', () => { expect(() => subject()).not.toThrow(); }); }); }); });