// Component tests for usa-step-indicator
import './index.ts';
// Test data constants
const basicSteps = [
{ label: 'Personal Information', status: 'complete' },
{ label: 'Contact Details', status: 'current' },
{ label: 'Review', status: 'incomplete' },
{ label: 'Submit', status: 'incomplete' },
];
const longSteps = [
{ label: 'Step 1: Initial Setup', status: 'complete' },
{ label: 'Step 2: Configuration', status: 'complete' },
{ label: 'Step 3: Data Entry', status: 'current' },
{ label: 'Step 4: Validation', status: 'incomplete' },
{ label: 'Step 5: Review Process', status: 'incomplete' },
{ label: 'Step 6: Approval', status: 'incomplete' },
{ label: 'Step 7: Final Review', status: 'incomplete' },
{ label: 'Step 8: Submission', status: 'incomplete' },
];
describe('USA Step Indicator Component Tests', () => {
it('should render step indicator with default properties', () => {
cy.mount(``);
cy.get('usa-step-indicator').should('exist');
cy.get('usa-step-indicator').should('have.class', 'usa-step-indicator');
});
it('should render steps when provided', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('test-steps') as any;
indicator.steps = basicSteps;
});
cy.get('.usa-step-indicator__segment').should('have.length', 4);
cy.get('.usa-step-indicator__segment-label')
.first()
.should('contain.text', 'Personal Information');
cy.get('.usa-step-indicator__segment-label').last().should('contain.text', 'Submit');
});
it('should display correct step statuses', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('test-steps') as any;
indicator.steps = basicSteps;
});
// Check complete step
cy.get('.usa-step-indicator__segment')
.first()
.should('have.class', 'usa-step-indicator__segment--complete')
.find('.usa-sr-only')
.should('contain.text', 'completed');
// Check current step
cy.get('.usa-step-indicator__segment')
.eq(1)
.should('have.class', 'usa-step-indicator__segment--current')
.should('have.attr', 'aria-current', 'step');
// Check incomplete steps
cy.get('.usa-step-indicator__segment')
.eq(2)
.should('not.have.class', 'usa-step-indicator__segment--complete')
.should('not.have.class', 'usa-step-indicator__segment--current')
.find('.usa-sr-only')
.should('contain.text', 'not completed');
});
it('should handle counters variant', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('test-steps') as any;
indicator.steps = basicSteps;
});
cy.get('usa-step-indicator').should('have.class', 'usa-step-indicator--counters');
cy.get('.usa-step-indicator__segment').each(($el, index) => {
cy.wrap($el)
.find('.usa-step-indicator__segment-number')
.should('contain.text', String(index + 1));
});
});
it('should handle small counters variant', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('test-steps') as any;
indicator.steps = basicSteps;
});
cy.get('usa-step-indicator').should('have.class', 'usa-step-indicator--counters-sm');
cy.get('.usa-step-indicator__segment-number').should('exist');
});
it('should handle no labels display', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('test-steps') as any;
indicator.steps = basicSteps;
indicator.showLabels = false;
});
cy.get('.usa-step-indicator__segment-label').should('have.class', 'usa-sr-only');
});
it('should handle centered layout', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('test-steps') as any;
indicator.steps = basicSteps;
});
cy.get('usa-step-indicator').should('have.class', 'usa-step-indicator--center');
});
it('should update current step programmatically', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('test-steps') as any;
indicator.steps = basicSteps;
indicator.currentStep = 3;
});
cy.get('.usa-step-indicator__segment--current').should('contain.text', 'Review');
});
it('should emit step-change event', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('test-steps') as any;
indicator.steps = basicSteps;
const changeSpy = cy.stub();
indicator.addEventListener('usa-step-indicator:change', changeSpy);
indicator.currentStep = 3;
cy.then(() => {
expect(changeSpy).to.have.been.called;
});
});
});
it('should handle keyboard navigation', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('test-steps') as any;
indicator.steps = basicSteps;
indicator.interactive = true; // Enable keyboard navigation
});
// Focus first step
cy.get('.usa-step-indicator__segment').first().focus();
// Arrow right to next step
cy.focused().type('{rightarrow}');
cy.focused().should('contain.text', 'Contact Details');
// Arrow left to previous step
cy.focused().type('{leftarrow}');
cy.focused().should('contain.text', 'Personal Information');
});
it('should handle long lists with scrolling', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('test-steps') as any;
indicator.steps = longSteps;
});
cy.get('.usa-step-indicator__segment').should('have.length', 8);
// Current step should be visible
cy.get('.usa-step-indicator__segment--current').should('be.visible');
});
it('should be accessible', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('test-steps') as any;
indicator.steps = basicSteps;
});
// Check ARIA attributes
cy.get('usa-step-indicator').should('have.attr', 'role', 'group');
cy.get('usa-step-indicator').should('have.attr', 'aria-label');
cy.get('.usa-step-indicator__segment--current').should('have.attr', 'aria-current', 'step');
// Check screen reader text
cy.get('.usa-sr-only').should('exist');
cy.injectAxe();
cy.checkAccessibility();
});
it('should handle step headings', () => {
cy.mount(`
`);
cy.window().then((win) => {
const indicator = win.document.getElementById('test-steps') as any;
indicator.steps = basicSteps;
indicator.headingLevel = 2;
});
cy.get('.usa-step-indicator__heading').should('match', 'h2');
});
it('should update progress dynamically', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('test-steps') as any;
indicator.steps = [
{ label: 'Step 1', status: 'incomplete' },
{ label: 'Step 2', status: 'incomplete' },
{ label: 'Step 3', status: 'incomplete' },
];
});
// Initially all incomplete
cy.get('.usa-step-indicator__segment--complete').should('not.exist');
// Update first step to complete
cy.window().then((win) => {
const indicator = win.document.getElementById('test-steps') as any;
indicator.steps = [
{ label: 'Step 1', status: 'complete' },
{ label: 'Step 2', status: 'current' },
{ label: 'Step 3', status: 'incomplete' },
];
});
cy.get('.usa-step-indicator__segment--complete').should('have.length', 1);
cy.get('.usa-step-indicator__segment--current').should('have.length', 1);
});
it('should support custom CSS classes', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('test-steps') as any;
indicator.steps = basicSteps;
});
cy.get('usa-step-indicator').should('have.class', 'custom-class');
cy.get('usa-step-indicator').should('have.class', 'usa-step-indicator');
});
it('should handle RTL layout', () => {
cy.mount(`
`);
cy.window().then((win) => {
const indicator = win.document.getElementById('test-steps') as any;
indicator.steps = basicSteps;
});
// Test RTL keyboard navigation
cy.get('.usa-step-indicator__segment').first().focus();
// In RTL, arrow left should go to next step
cy.focused().type('{leftarrow}');
cy.focused().should('contain.text', 'Contact Details');
});
describe('Edge Case Testing', () => {
describe('Boundary Conditions', () => {
it('should handle extremely large number of steps', () => {
const manySteps = Array.from({ length: 100 }, (_, i) => ({
label: `Step ${i + 1}: Lorem ipsum dolor sit amet consectetur`,
status: i === 50 ? 'current' : i < 50 ? 'complete' : 'incomplete',
}));
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('many-steps') as any;
indicator.steps = manySteps;
});
cy.get('.usa-step-indicator__segment').should('have.length', 100);
cy.get('.usa-step-indicator__segment--current').should('have.length', 1);
cy.get('.usa-step-indicator__segment--complete').should('have.length', 50);
});
it('should handle empty steps array', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('empty-steps') as any;
indicator.steps = [];
});
cy.get('.usa-step-indicator__segment').should('not.exist');
cy.get('usa-step-indicator').should('exist'); // Component should still render
});
it('should handle steps with extremely long labels', () => {
const longLabelSteps = [
{
label:
'This is an extremely long step label that contains multiple sentences and should test how the component handles text overflow and wrapping behavior in various layout scenarios including mobile and desktop viewports with different screen sizes and orientations',
status: 'complete',
},
{
label:
'Another very long label with special characters: áéíóú ñ ç ü ß ø æ å 中文 日本語 العربية русский ελληνικά',
status: 'current',
},
{
label: 'HTML entities: <>&"' and Unicode: 🎉 🚀 ✨ 💡 ⚡ 🔥 📊 📈',
status: 'incomplete',
},
];
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('long-labels') as any;
indicator.steps = longLabelSteps;
});
cy.get('.usa-step-indicator__segment').should('have.length', 3);
cy.get('.usa-step-indicator__segment-label')
.first()
.should('contain.text', 'This is an extremely long');
});
it('should handle all steps with same status', () => {
const allCompleteSteps = [
{ label: 'Step 1', status: 'complete' },
{ label: 'Step 2', status: 'complete' },
{ label: 'Step 3', status: 'complete' },
{ label: 'Step 4', status: 'complete' },
];
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('all-complete') as any;
indicator.steps = allCompleteSteps;
});
cy.get('.usa-step-indicator__segment--complete').should('have.length', 4);
cy.get('.usa-step-indicator__segment--current').should('not.exist');
});
it('should handle single step', () => {
const singleStep = [{ label: 'Only Step', status: 'current' }];
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('single-step') as any;
indicator.steps = singleStep;
});
cy.get('.usa-step-indicator__segment').should('have.length', 1);
cy.get('.usa-step-indicator__segment--current').should('have.length', 1);
});
});
describe('Error Recovery', () => {
it('should handle invalid step statuses gracefully', () => {
const invalidSteps = [
{ label: 'Step 1', status: 'invalid-status' },
{ label: 'Step 2', status: null },
{ label: 'Step 3', status: undefined },
{ label: 'Step 4', status: 123 },
{ label: 'Step 5', status: 'current' },
];
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('invalid-statuses') as any;
indicator.steps = invalidSteps as any;
});
// Should not crash and should render steps
cy.get('.usa-step-indicator__segment').should('have.length', 5);
cy.get('.usa-step-indicator__segment--current').should('have.length', 1);
});
it('should handle malformed step objects', () => {
const malformedSteps = [
{ label: 'Valid Step', status: 'complete' },
{ wrongProperty: 'No label', status: 'current' },
{ label: null, status: 'incomplete' },
{ label: '', status: 'incomplete' },
null,
undefined,
'not an object',
{ label: 'Another Valid Step', status: 'incomplete' },
];
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('malformed-steps') as any;
indicator.steps = malformedSteps as any;
});
// Should handle gracefully and render valid steps
cy.get('.usa-step-indicator__segment').should('exist');
cy.get('usa-step-indicator').should('exist');
});
it('should handle rapid property updates', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('rapid-updates') as any;
// Rapid updates to test for race conditions
for (let i = 0; i < 10; i++) {
indicator.steps = [
{ label: `Iteration ${i} Step 1`, status: 'complete' },
{ label: `Iteration ${i} Step 2`, status: 'current' },
{ label: `Iteration ${i} Step 3`, status: 'incomplete' },
];
indicator.variant = i % 2 === 0 ? 'counters' : 'default';
indicator.currentStep = (i % 3) + 1;
}
});
// Should end in stable state
cy.get('.usa-step-indicator__segment').should('have.length', 3);
cy.get('.usa-step-indicator__segment--current').should('have.length', 1);
});
it('should handle DOM removal during updates', () => {
cy.mount(`
`);
cy.window().then((win) => {
const indicator = win.document.getElementById('removal-test') as any;
const container = win.document.getElementById('container');
indicator.steps = basicSteps;
// Remove from DOM during potential async operations
setTimeout(() => {
container?.removeChild(indicator);
}, 50);
});
// Should not cause JavaScript errors
cy.get('#container').should('be.empty');
});
});
describe('Performance Stress Testing', () => {
it('should handle frequent step updates efficiently', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('performance-test') as any;
const startTime = performance.now();
// Simulate 1000 step updates
for (let i = 0; i < 1000; i++) {
const currentIndex = i % 4;
indicator.steps = basicSteps.map((step, index) => ({
...step,
status:
index < currentIndex
? 'complete'
: index === currentIndex
? 'current'
: 'incomplete',
}));
}
const endTime = performance.now();
const duration = endTime - startTime;
// Should complete in reasonable time (less than 1 second)
expect(duration).to.be.lessThan(1000);
});
cy.get('.usa-step-indicator__segment').should('have.length', 4);
});
it('should handle large datasets without memory leaks', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('memory-test') as any;
// Create and destroy large datasets
for (let iteration = 0; iteration < 10; iteration++) {
const largeSteps = Array.from({ length: 500 }, (_, i) => ({
label: `Dataset ${iteration} Step ${i + 1}`,
status: i === 250 ? 'current' : i < 250 ? 'complete' : 'incomplete',
}));
indicator.steps = largeSteps;
// Clear and start over
indicator.steps = [];
}
// Set final reasonable dataset
indicator.steps = basicSteps;
});
cy.get('.usa-step-indicator__segment').should('have.length', 4);
});
it('should handle concurrent event listeners', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('event-stress') as any;
indicator.steps = basicSteps;
// Add many event listeners
for (let i = 0; i < 100; i++) {
indicator.addEventListener('usa-step-indicator:change', () => {
// Empty listener for stress testing
});
}
// Should still function normally
indicator.currentStep = 3;
indicator.currentStep = 1;
});
cy.get('.usa-step-indicator__segment--current').should('exist');
});
});
describe('Mobile Compatibility', () => {
it('should handle touch interactions on mobile', () => {
cy.viewport(375, 667); // iPhone SE dimensions
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('touch-test') as any;
indicator.steps = basicSteps;
indicator.interactive = true;
});
// Simulate touch on step
cy.get('.usa-step-indicator__segment')
.eq(2)
.trigger('touchstart', { touches: [{ clientX: 100, clientY: 100 }] })
.trigger('touchend', { changedTouches: [{ clientX: 100, clientY: 100 }] });
cy.get('.usa-step-indicator__segment').should('exist');
});
it('should adapt to small screen widths', () => {
cy.viewport(320, 568); // iPhone 5 dimensions
cy.mount(
``
);
cy.window().then((win) => {
const indicator = win.document.getElementById('small-screen') as any;
indicator.steps = longSteps;
});
cy.get('.usa-step-indicator__segment').should('be.visible');
cy.get('.usa-step-indicator__segment').first().should('be.visible');
});
it('should handle viewport orientation changes', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('orientation-test') as any;
indicator.steps = basicSteps;
});
// Portrait
cy.viewport(375, 667);
cy.get('.usa-step-indicator__segment').should('be.visible');
// Landscape
cy.viewport(667, 375);
cy.get('.usa-step-indicator__segment').should('be.visible');
// Back to portrait
cy.viewport(375, 667);
cy.get('.usa-step-indicator__segment').should('be.visible');
});
it('should handle swipe gestures for navigation', () => {
cy.viewport(375, 667);
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('swipe-test') as any;
indicator.steps = basicSteps;
indicator.interactive = true;
});
// Simulate swipe left
cy.get('usa-step-indicator')
.trigger('touchstart', { touches: [{ clientX: 200, clientY: 100 }] })
.trigger('touchmove', { touches: [{ clientX: 100, clientY: 100 }] })
.trigger('touchend', { changedTouches: [{ clientX: 100, clientY: 100 }] });
cy.get('.usa-step-indicator__segment').should('exist');
});
});
describe('Accessibility Edge Cases', () => {
it('should handle screen reader navigation with many steps', () => {
const manyAccessibleSteps = Array.from({ length: 20 }, (_, i) => ({
label: `Accessible Step ${i + 1}`,
status: i === 10 ? 'current' : i < 10 ? 'complete' : 'incomplete',
description: `Detailed description for step ${i + 1}`,
}));
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('sr-many-steps') as any;
indicator.steps = manyAccessibleSteps;
});
cy.get('.usa-step-indicator__segment').should('have.length', 20);
cy.get('.usa-sr-only').should('exist');
cy.get('[aria-current="step"]').should('have.length', 1);
});
it('should handle high contrast mode', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('contrast-test') as any;
indicator.steps = basicSteps;
});
// Simulate high contrast mode
cy.get('usa-step-indicator').invoke(
'attr',
'style',
'filter: contrast(1000%) brightness(200%) invert(1);'
);
cy.get('.usa-step-indicator__segment').should('be.visible');
cy.get('.usa-step-indicator__segment--current').should('be.visible');
});
it('should handle reduced motion preferences', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('motion-test') as any;
// Mock reduced motion preference
Object.defineProperty(win, 'matchMedia', {
writable: true,
value: cy.stub().returns({
matches: true, // prefers-reduced-motion: reduce
media: '(prefers-reduced-motion: reduce)',
onchange: null,
addListener: cy.stub(),
removeListener: cy.stub(),
addEventListener: cy.stub(),
removeEventListener: cy.stub(),
dispatchEvent: cy.stub(),
}),
});
indicator.steps = basicSteps;
});
cy.get('.usa-step-indicator__segment').should('be.visible');
});
it('should handle focus management with dynamic content', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('focus-dynamic') as any;
indicator.steps = basicSteps;
indicator.interactive = true;
});
// Focus first step
cy.get('.usa-step-indicator__segment').first().focus();
cy.focused().should('exist');
// Update steps while focused
cy.window().then((win) => {
const indicator = win.document.getElementById('focus-dynamic') as any;
indicator.steps = [...basicSteps, { label: 'New Step', status: 'incomplete' }];
});
// Focus should be maintained or properly managed
cy.get('.usa-step-indicator__segment').should('have.length', 5);
});
});
describe('Browser Compatibility', () => {
it('should handle missing modern JavaScript features', () => {
cy.window().then((win) => {
// Mock missing modern features
const originalFind = Array.prototype.find;
delete (Array.prototype as any).find;
cy.mount(``);
cy.window().then((win2) => {
const indicator = win2.document.getElementById('compat-test') as any;
// Should still function without modern Array methods
expect(() => {
indicator.steps = basicSteps;
}).not.to.throw;
// Restore method
Array.prototype.find = originalFind;
});
});
cy.get('.usa-step-indicator__segment').should('exist');
});
it('should handle CSS custom properties fallbacks', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('css-fallback') as any;
indicator.steps = basicSteps;
});
// Should be visible even without CSS custom property support
cy.get('.usa-step-indicator__segment').should('be.visible');
cy.get('.usa-step-indicator__segment--current').should('be.visible');
});
it('should handle event handling differences across browsers', () => {
cy.mount(``);
cy.window().then((win) => {
const indicator = win.document.getElementById('event-compat') as any;
indicator.steps = basicSteps;
indicator.interactive = true;
// Test different event patterns
const segment = indicator.querySelector('.usa-step-indicator__segment');
// Simulate click event in different ways
const clickEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: win,
});
expect(() => {
segment?.dispatchEvent(clickEvent);
}).not.to.throw;
});
cy.get('.usa-step-indicator__segment').should('exist');
});
});
describe('Integration Edge Cases', () => {
it('should handle step indicator within dynamic containers', () => {
cy.mount(`
`);
cy.window().then((win) => {
const indicator = win.document.getElementById('container-test') as any;
const container = win.document.getElementById('dynamic-container');
indicator.steps = basicSteps;
// Show container
if (container) container.style.display = 'block';
});
cy.get('#dynamic-container').should('be.visible');
cy.get('.usa-step-indicator__segment').should('be.visible');
});
it('should handle complex state synchronization', () => {
cy.mount(`
`);
cy.window().then((win) => {
const indicator1 = win.document.getElementById('sync-test-1') as any;
const indicator2 = win.document.getElementById('sync-test-2') as any;
// Sync both indicators
const syncSteps = (currentStep: number) => {
const steps = basicSteps.map((step, index) => ({
...step,
status:
index < currentStep - 1
? 'complete'
: index === currentStep - 1
? 'current'
: 'incomplete',
}));
indicator1.steps = steps;
indicator2.steps = steps;
};
syncSteps(1);
syncSteps(2);
syncSteps(3);
});
cy.get('#sync-test-1 .usa-step-indicator__segment--current').should('exist');
cy.get('#sync-test-2 .usa-step-indicator__segment--current').should('exist');
});
it('should handle step indicator with form integration', () => {
cy.mount(`
`);
cy.window().then((win) => {
const indicator = win.document.getElementById('form-steps') as any;
indicator.steps = basicSteps;
// Simple form navigation logic
win.document.getElementById('next-1')?.addEventListener('click', () => {
win.document.getElementById('step-1')!.style.display = 'none';
win.document.getElementById('step-2')!.style.display = 'block';
indicator.currentStep = 2;
});
});
cy.get('#next-1').click();
cy.get('#step-2').should('be.visible');
cy.get('#step-1').should('not.be.visible');
});
});
});
});