feat(stepper): implement reusable stepper component with navigation and validation
This commit is contained in:
128
src/app/shared/components/stepper/stepper.component.spec.ts
Normal file
128
src/app/shared/components/stepper/stepper.component.spec.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { Component, signal, ViewChild } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { StepperComponent } from './stepper.component';
|
||||
import { StepComponent } from './step.component';
|
||||
|
||||
@Component({
|
||||
imports: [StepperComponent, StepComponent],
|
||||
template: `
|
||||
<app-stepper #stepper>
|
||||
<app-step label="Step 1" [isValid]="step1Valid()">
|
||||
<div id="content-1">Content 1</div>
|
||||
</app-step>
|
||||
<app-step label="Step 2" [isValid]="step2Valid()">
|
||||
<div id="content-2">Content 2</div>
|
||||
</app-step>
|
||||
</app-stepper>
|
||||
`
|
||||
})
|
||||
class TestHostComponent {
|
||||
@ViewChild('stepper') stepper!: StepperComponent;
|
||||
step1Valid = signal(true);
|
||||
step2Valid = signal(true);
|
||||
}
|
||||
|
||||
describe('StepperComponent & StepComponent', () => {
|
||||
async function setup() {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TestHostComponent, StepperComponent, StepComponent]
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(TestHostComponent);
|
||||
fixture.detectChanges();
|
||||
return { fixture, component: fixture.componentInstance };
|
||||
}
|
||||
|
||||
it('renders step labels in the indicator bar', async () => {
|
||||
const { fixture } = await setup();
|
||||
const labels = fixture.nativeElement.querySelectorAll('.stepper-header__label');
|
||||
expect(labels.length).toBe(2);
|
||||
expect(labels[0].textContent.trim()).toBe('Step 1');
|
||||
expect(labels[1].textContent.trim()).toBe('Step 2');
|
||||
});
|
||||
|
||||
it('activates the first step by default and shows its content', async () => {
|
||||
const { fixture } = await setup();
|
||||
const content1 = fixture.nativeElement.querySelector('#content-1');
|
||||
const content2 = fixture.nativeElement.querySelector('#content-2');
|
||||
|
||||
expect(content1).not.toBeNull();
|
||||
expect(content2).toBeNull();
|
||||
});
|
||||
|
||||
it('advances to step 2 when next() is called and current step is valid', async () => {
|
||||
const { fixture, component } = await setup();
|
||||
|
||||
component.stepper.next();
|
||||
fixture.detectChanges();
|
||||
|
||||
const content1 = fixture.nativeElement.querySelector('#content-1');
|
||||
const content2 = fixture.nativeElement.querySelector('#content-2');
|
||||
|
||||
expect(content1).toBeNull();
|
||||
expect(content2).not.toBeNull();
|
||||
expect(component.stepper.currentStepIndex()).toBe(1);
|
||||
});
|
||||
|
||||
it('does not advance to step 2 when next() is called if current step is invalid', async () => {
|
||||
const { fixture, component } = await setup();
|
||||
component.step1Valid.set(false);
|
||||
fixture.detectChanges();
|
||||
|
||||
component.stepper.next();
|
||||
fixture.detectChanges();
|
||||
|
||||
const content1 = fixture.nativeElement.querySelector('#content-1');
|
||||
const content2 = fixture.nativeElement.querySelector('#content-2');
|
||||
|
||||
expect(content1).not.toBeNull();
|
||||
expect(content2).toBeNull();
|
||||
expect(component.stepper.currentStepIndex()).toBe(0);
|
||||
});
|
||||
|
||||
it('goes back to step 1 when previous() is called', async () => {
|
||||
const { fixture, component } = await setup();
|
||||
|
||||
component.stepper.next();
|
||||
fixture.detectChanges();
|
||||
expect(component.stepper.currentStepIndex()).toBe(1);
|
||||
|
||||
component.stepper.previous();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.stepper.currentStepIndex()).toBe(0);
|
||||
expect(fixture.nativeElement.querySelector('#content-1')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('marks the first circle as active by default', async () => {
|
||||
const { fixture } = await setup();
|
||||
const items = fixture.nativeElement.querySelectorAll('.stepper-header__item');
|
||||
expect(items[0].classList.contains('is-active')).toBe(true);
|
||||
expect(items[1].classList.contains('is-active')).toBe(false);
|
||||
});
|
||||
|
||||
it('marks step 1 as completed and step 2 as active after advancing', async () => {
|
||||
const { fixture, component } = await setup();
|
||||
|
||||
component.stepper.next();
|
||||
fixture.detectChanges();
|
||||
|
||||
const items = fixture.nativeElement.querySelectorAll('.stepper-header__item');
|
||||
expect(items[0].classList.contains('is-completed')).toBe(true);
|
||||
expect(items[1].classList.contains('is-active')).toBe(true);
|
||||
});
|
||||
|
||||
it('allows navigating back to a completed step via goToStep()', async () => {
|
||||
const { fixture, component } = await setup();
|
||||
|
||||
component.stepper.next();
|
||||
fixture.detectChanges();
|
||||
expect(component.stepper.currentStepIndex()).toBe(1);
|
||||
|
||||
component.stepper.goToStep(0);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.stepper.currentStepIndex()).toBe(0);
|
||||
expect(fixture.nativeElement.querySelector('#content-1')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user