import { StateGraph } from '../../src/core/StateGraph'; import { IState, IAction } from '../../src/core/interfaces'; function makeState(id: string, url: string, visitCount = 0): IState { return { id, url, title: `Page ${id}`, timestamp: 1000, domSnapshot: '', visitCount, }; } function makeAction(id: string, stateId: string): IAction { return { id, type: 'click', selector: '#btn', timestamp: 2000, seed: 42, stateId, }; } describe('StateGraph', () => { describe('addState', () => { it('adds a new state', () => { const graph = new StateGraph(); const state = makeState('s1', 'http://localhost/'); graph.addState(state); expect(graph.hasState('s1')).toBe(true); }); it('does not duplicate states with the same id', () => { const graph = new StateGraph(); graph.addState(makeState('s1', 'http://localhost/')); graph.addState(makeState('s1', 'http://localhost/')); expect(graph.getAllStates()).toHaveLength(1); }); it('increments visitCount when adding existing state id', () => { const graph = new StateGraph(); graph.addState(makeState('s1', 'http://localhost/', 0)); graph.addState(makeState('s1', 'http://localhost/', 0)); const state = graph.getState('s1')!; expect(state.visitCount).toBe(1); }); }); describe('hasState', () => { it('returns false for unknown state', () => { const graph = new StateGraph(); expect(graph.hasState('nonexistent')).toBe(false); }); }); describe('recordTransition', () => { it('records a transition between states', () => { const graph = new StateGraph(); graph.addState(makeState('s1', '/')); graph.addState(makeState('s2', '/about')); const action = makeAction('a1', 's1'); graph.recordTransition('s1', action, 's2'); const transitions = graph.getTransitions(); expect(transitions).toHaveLength(1); expect(transitions[0].fromId).toBe('s1'); expect(transitions[0].toId).toBe('s2'); expect(transitions[0].action.id).toBe('a1'); }); it('records multiple transitions', () => { const graph = new StateGraph(); graph.addState(makeState('s1', '/')); graph.addState(makeState('s2', '/a')); graph.addState(makeState('s3', '/b')); graph.recordTransition('s1', makeAction('a1', 's1'), 's2'); graph.recordTransition('s1', makeAction('a2', 's1'), 's3'); expect(graph.getTransitions()).toHaveLength(2); }); }); describe('getUnvisited', () => { it('returns states with visitCount === 0', () => { const graph = new StateGraph(); graph.addState(makeState('s1', '/', 0)); graph.addState(makeState('s2', '/a', 1)); graph.addState(makeState('s3', '/b', 0)); const unvisited = graph.getUnvisited(); expect(unvisited.map((s) => s.id)).toEqual(['s1', 's3']); }); it('returns empty when all states visited', () => { const graph = new StateGraph(); graph.addState(makeState('s1', '/', 1)); expect(graph.getUnvisited()).toHaveLength(0); }); }); describe('getNextToExplore', () => { it('returns oldest unvisited state (BFS order)', () => { const graph = new StateGraph(); graph.addState(makeState('s1', '/')); graph.addState(makeState('s2', '/a')); const next = graph.getNextToExplore(); expect(next?.id).toBe('s1'); }); it('returns null when no unvisited states remain', () => { const graph = new StateGraph(); graph.addState(makeState('s1', '/', 1)); expect(graph.getNextToExplore()).toBeNull(); }); it('skips visited states', () => { const graph = new StateGraph(); graph.addState(makeState('s1', '/')); graph.addState(makeState('s2', '/a')); graph.incrementVisit('s1'); const next = graph.getNextToExplore(); expect(next?.id).toBe('s2'); }); }); describe('toJSON', () => { it('produces a serializable object', () => { const graph = new StateGraph(); graph.addState(makeState('s1', '/')); graph.recordTransition('s1', makeAction('a1', 's1'), 's1'); const json = graph.toJSON() as any; expect(json.stateCount).toBe(1); expect(json.transitionCount).toBe(1); expect(JSON.parse(JSON.stringify(json))).toEqual(json); }); }); });