Skip to main content
Glama
step-history.test.ts12 kB
/** * Step History Tests * * @package WP_Navigator_Pro * @since 2.5.0 */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { createStepHistory, type StepHistoryEntry } from './step-history.js'; describe('Step History', () => { describe('createStepHistory', () => { it('should create an empty history', () => { const history = createStepHistory(); expect(history.size()).toBe(0); expect(history.isEmpty()).toBe(true); }); }); describe('push', () => { it('should add entries to history', () => { const history = createStepHistory(); history.push({ stepNumber: 1, stepName: 'Welcome', data: {} }); expect(history.size()).toBe(1); expect(history.isEmpty()).toBe(false); history.push({ stepNumber: 2, stepName: 'Site URL', data: { url: 'https://example.com' } }); expect(history.size()).toBe(2); }); it('should add timestamp automatically', () => { const history = createStepHistory(); const before = new Date(); history.push({ stepNumber: 1, stepName: 'Welcome', data: {} }); const after = new Date(); const entry = history.peek(); expect(entry).toBeDefined(); expect(entry!.timestamp).toBeInstanceOf(Date); expect(entry!.timestamp.getTime()).toBeGreaterThanOrEqual(before.getTime()); expect(entry!.timestamp.getTime()).toBeLessThanOrEqual(after.getTime()); }); it('should deep clone data to prevent external mutations', () => { const history = createStepHistory(); const data = { nested: { value: 'original' } }; history.push({ stepNumber: 1, stepName: 'Test', data }); // Mutate original data data.nested.value = 'mutated'; // History should still have original value const entry = history.peek(); expect((entry!.data.nested as { value: string }).value).toBe('original'); }); it('should respect maxSize option', () => { const history = createStepHistory({ maxSize: 2 }); history.push({ stepNumber: 1, stepName: 'Step1', data: {} }); history.push({ stepNumber: 2, stepName: 'Step2', data: {} }); history.push({ stepNumber: 3, stepName: 'Step3', data: {} }); expect(history.size()).toBe(2); // Should have removed oldest entry (Step1) const all = history.getAll(); expect(all[0].stepName).toBe('Step2'); expect(all[1].stepName).toBe('Step3'); }); }); describe('pop', () => { it('should return undefined for empty history', () => { const history = createStepHistory(); expect(history.pop()).toBeUndefined(); }); it('should remove and return most recent entry', () => { const history = createStepHistory(); history.push({ stepNumber: 1, stepName: 'Step1', data: { a: 1 } }); history.push({ stepNumber: 2, stepName: 'Step2', data: { b: 2 } }); const popped = history.pop(); expect(popped).toBeDefined(); expect(popped!.stepNumber).toBe(2); expect(popped!.stepName).toBe('Step2'); expect(popped!.data).toEqual({ b: 2 }); expect(history.size()).toBe(1); }); it('should return deep clone to prevent mutations', () => { const history = createStepHistory(); history.push({ stepNumber: 1, stepName: 'Test', data: { value: 'original' } }); const popped = history.pop(); popped!.data.value = 'mutated'; // Push back and check (though history should be empty now) history.push({ stepNumber: 1, stepName: 'Test', data: { value: 'new' } }); const newEntry = history.peek(); expect(newEntry!.data.value).toBe('new'); }); }); describe('peek', () => { it('should return undefined for empty history', () => { const history = createStepHistory(); expect(history.peek()).toBeUndefined(); }); it('should return most recent entry without removing it', () => { const history = createStepHistory(); history.push({ stepNumber: 1, stepName: 'Step1', data: {} }); history.push({ stepNumber: 2, stepName: 'Step2', data: {} }); const peeked = history.peek(); expect(peeked).toBeDefined(); expect(peeked!.stepNumber).toBe(2); expect(history.size()).toBe(2); // Size unchanged }); it('should return deep clone to prevent mutations', () => { const history = createStepHistory(); history.push({ stepNumber: 1, stepName: 'Test', data: { value: 'original' } }); const peeked = history.peek(); peeked!.data.value = 'mutated'; // Original in history should be unchanged const peekedAgain = history.peek(); expect(peekedAgain!.data.value).toBe('original'); }); }); describe('get', () => { it('should return undefined for invalid index', () => { const history = createStepHistory(); history.push({ stepNumber: 1, stepName: 'Test', data: {} }); expect(history.get(-1)).toBeUndefined(); expect(history.get(1)).toBeUndefined(); expect(history.get(100)).toBeUndefined(); }); it('should return entry at specified index (oldest first)', () => { const history = createStepHistory(); history.push({ stepNumber: 1, stepName: 'Step1', data: {} }); history.push({ stepNumber: 2, stepName: 'Step2', data: {} }); history.push({ stepNumber: 3, stepName: 'Step3', data: {} }); expect(history.get(0)?.stepName).toBe('Step1'); expect(history.get(1)?.stepName).toBe('Step2'); expect(history.get(2)?.stepName).toBe('Step3'); }); }); describe('clear', () => { it('should remove all entries', () => { const history = createStepHistory(); history.push({ stepNumber: 1, stepName: 'Step1', data: {} }); history.push({ stepNumber: 2, stepName: 'Step2', data: {} }); expect(history.size()).toBe(2); history.clear(); expect(history.size()).toBe(0); expect(history.isEmpty()).toBe(true); }); }); describe('getAll', () => { it('should return empty array for empty history', () => { const history = createStepHistory(); expect(history.getAll()).toEqual([]); }); it('should return all entries in order (oldest first)', () => { const history = createStepHistory(); history.push({ stepNumber: 1, stepName: 'Step1', data: { a: 1 } }); history.push({ stepNumber: 2, stepName: 'Step2', data: { b: 2 } }); const all = history.getAll(); expect(all.length).toBe(2); expect(all[0].stepName).toBe('Step1'); expect(all[1].stepName).toBe('Step2'); }); it('should return deep clones', () => { const history = createStepHistory(); history.push({ stepNumber: 1, stepName: 'Test', data: { value: 'original' } }); const all = history.getAll(); all[0].data.value = 'mutated'; const allAgain = history.getAll(); expect(allAgain[0].data.value).toBe('original'); }); }); describe('getStepData', () => { it('should return undefined for non-existent step', () => { const history = createStepHistory(); history.push({ stepNumber: 1, stepName: 'Step1', data: {} }); expect(history.getStepData('NonExistent')).toBeUndefined(); }); it('should return data for existing step by name', () => { const history = createStepHistory(); history.push({ stepNumber: 1, stepName: 'Welcome', data: { mode: 'guided' } }); history.push({ stepNumber: 2, stepName: 'Site URL', data: { url: 'https://example.com' }, }); const welcomeData = history.getStepData('Welcome'); expect(welcomeData).toEqual({ mode: 'guided' }); const urlData = history.getStepData('Site URL'); expect(urlData).toEqual({ url: 'https://example.com' }); }); }); describe('getAccumulatedData', () => { it('should return empty object for empty history', () => { const history = createStepHistory(); expect(history.getAccumulatedData()).toEqual({}); }); it('should merge data from all steps', () => { const history = createStepHistory(); history.push({ stepNumber: 1, stepName: 'Step1', data: { a: 1, b: 2 } }); history.push({ stepNumber: 2, stepName: 'Step2', data: { c: 3, d: 4 } }); const accumulated = history.getAccumulatedData(); expect(accumulated).toEqual({ a: 1, b: 2, c: 3, d: 4 }); }); it('should let later steps override earlier steps for same keys', () => { const history = createStepHistory(); history.push({ stepNumber: 1, stepName: 'Step1', data: { shared: 'first', a: 1 } }); history.push({ stepNumber: 2, stepName: 'Step2', data: { shared: 'second', b: 2 } }); const accumulated = history.getAccumulatedData(); expect(accumulated).toEqual({ shared: 'second', a: 1, b: 2 }); }); }); describe('hasStep', () => { it('should return false for empty history', () => { const history = createStepHistory(); expect(history.hasStep('Anything')).toBe(false); }); it('should return true for existing step', () => { const history = createStepHistory(); history.push({ stepNumber: 1, stepName: 'Welcome', data: {} }); expect(history.hasStep('Welcome')).toBe(true); }); it('should return false for non-existent step', () => { const history = createStepHistory(); history.push({ stepNumber: 1, stepName: 'Welcome', data: {} }); expect(history.hasStep('Goodbye')).toBe(false); }); }); describe('typical wizard flow', () => { it('should support forward navigation', () => { const history = createStepHistory(); // User progresses through wizard history.push({ stepNumber: 1, stepName: 'Welcome', data: { mode: 'guided' } }); history.push({ stepNumber: 2, stepName: 'Site URL', data: { url: 'https://test.com' } }); history.push({ stepNumber: 3, stepName: 'Credentials', data: { user: 'admin' } }); expect(history.size()).toBe(3); expect(history.getAccumulatedData()).toEqual({ mode: 'guided', url: 'https://test.com', user: 'admin', }); }); it('should support back navigation with data preservation', () => { const history = createStepHistory(); // User progresses to step 3 history.push({ stepNumber: 1, stepName: 'Welcome', data: { mode: 'guided' } }); history.push({ stepNumber: 2, stepName: 'Site URL', data: { url: 'https://test.com' } }); history.push({ stepNumber: 3, stepName: 'Credentials', data: { user: 'admin' } }); // User goes back const step3 = history.pop(); expect(step3?.stepName).toBe('Credentials'); expect(history.size()).toBe(2); // Previous data still accessible const currentData = history.getAccumulatedData(); expect(currentData).toEqual({ mode: 'guided', url: 'https://test.com', }); // User can see current step data const currentStep = history.peek(); expect(currentStep?.stepName).toBe('Site URL'); expect(currentStep?.data.url).toBe('https://test.com'); }); it('should allow editing previous step and continuing', () => { const history = createStepHistory(); // User completes steps 1-2 history.push({ stepNumber: 1, stepName: 'Welcome', data: { mode: 'guided' } }); history.push({ stepNumber: 2, stepName: 'Site URL', data: { url: 'https://wrong-url.com' }, }); // User goes back to fix URL history.pop(); // User re-enters step 2 with correct URL history.push({ stepNumber: 2, stepName: 'Site URL', data: { url: 'https://correct-url.com' }, }); // Continue to step 3 history.push({ stepNumber: 3, stepName: 'Credentials', data: { user: 'admin' } }); const accumulated = history.getAccumulatedData(); expect(accumulated.url).toBe('https://correct-url.com'); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/littlebearapps/wp-navigator-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server