import { expect, fixture, html, oneEvent } from '@open-wc/testing';
import './nile-dot-stepper';
import type { NileDotStepper } from './nile-dot-stepper';
const dots = (el: NileDotStepper) =>
Array.from(
el.shadowRoot!.querySelectorAll('[part="dot"]')
);
describe('NileDotStepper', () => {
// --- Basic rendering ---
it('1. renders', async () => {
const el = await fixture(
html``
);
expect(el).to.exist;
});
it('2. shadow root', async () => {
const el = await fixture(
html``
);
expect(el.shadowRoot).to.not.be.null;
});
it('3. tag name', async () => {
const el = await fixture(
html``
);
expect(el.tagName.toLowerCase()).to.equal('nile-dot-stepper');
});
it('4. is defined', async () => {
expect(customElements.get('nile-dot-stepper')).to.exist;
});
it('5. has styles', async () => {
expect(
(await import('./nile-dot-stepper')).NileDotStepper.styles
).to.exist;
});
// --- Default property values ---
it('6. total defaults 0', async () => {
const el = await fixture(
html``
);
expect(el.total).to.equal(0);
});
it('7. current defaults 1', async () => {
const el = await fixture(
html``
);
expect(el.current).to.equal(1);
});
it('8. size defaults md', async () => {
const el = await fixture(
html``
);
expect(el.size).to.equal('md');
});
it('9. disabled defaults false', async () => {
const el = await fixture(
html``
);
expect(el.disabled).to.be.false;
});
it('10. no dots when total is 0', async () => {
const el = await fixture(
html``
);
expect(dots(el).length).to.equal(0);
});
// --- Reflected attributes ---
it('11. total reflects', async () => {
const el = await fixture(
html``
);
expect(el.getAttribute('total')).to.equal('5');
});
it('12. current reflects', async () => {
const el = await fixture(
html``
);
expect(el.getAttribute('current')).to.equal('3');
});
it('13. size reflects', async () => {
const el = await fixture(
html``
);
expect(el.getAttribute('size')).to.equal('lg');
});
it('14. disabled reflects', async () => {
const el = await fixture(
html``
);
expect(el.hasAttribute('disabled')).to.be.true;
});
// --- total renders N dots ---
it('15. total renders exactly N dots', async () => {
const el = await fixture(
html``
);
expect(dots(el).length).to.equal(5);
});
it('16. total renders single dot', async () => {
const el = await fixture(
html``
);
expect(dots(el).length).to.equal(1);
});
it('17. dynamically updating total re-renders dots', async () => {
const el = await fixture(
html``
);
expect(dots(el).length).to.equal(3);
el.total = 7;
await el.updateComplete;
expect(dots(el).length).to.equal(7);
});
// --- current marks correct active dot ---
it('18. current marks correct dot active', async () => {
const el = await fixture(
html``
);
const active = dots(el).filter((d) => d.classList.contains('dot--active'));
expect(active.length).to.equal(1);
expect(active[0].getAttribute('data-index')).to.equal('3');
});
it('19. active dot aria-selected true, others false', async () => {
const el = await fixture(
html``
);
const list = dots(el);
expect(list[1].getAttribute('aria-selected')).to.equal('true');
expect(list[0].getAttribute('aria-selected')).to.equal('false');
expect(list[2].getAttribute('aria-selected')).to.equal('false');
expect(list[3].getAttribute('aria-selected')).to.equal('false');
});
it('20. first dot active by default current=1', async () => {
const el = await fixture(
html``
);
expect(dots(el)[0].classList.contains('dot--active')).to.be.true;
});
it('21. out-of-range current (too high) clamps active to last dot', async () => {
const el = await fixture(
html``
);
const active = dots(el).filter((d) => d.classList.contains('dot--active'));
expect(active.length).to.equal(1);
expect(active[0].getAttribute('data-index')).to.equal('4');
});
it('22. out-of-range current (too low) clamps active to first dot', async () => {
const el = await fixture(
html``
);
const active = dots(el).filter((d) => d.classList.contains('dot--active'));
expect(active.length).to.equal(1);
expect(active[0].getAttribute('data-index')).to.equal('1');
});
it('23. dynamic current updates active dot', async () => {
const el = await fixture(
html``
);
el.current = 4;
await el.updateComplete;
const active = dots(el).filter((d) => d.classList.contains('dot--active'));
expect(active[0].getAttribute('data-index')).to.equal('4');
});
// --- Click interaction ---
it('24. clicking a dot sets current', async () => {
const el = await fixture(
html``
);
dots(el)[2].click();
await el.updateComplete;
expect(el.current).to.equal(3);
});
it('25. clicking a dot emits nile-step-change with 1-based value', async () => {
const el = await fixture(
html``
);
setTimeout(() => dots(el)[3].click());
const ev = await oneEvent(el, 'nile-step-change');
expect(ev.detail.value).to.equal(4);
});
it('26. clicking the first dot emits value 1', async () => {
const el = await fixture(
html``
);
setTimeout(() => dots(el)[0].click());
const ev = await oneEvent(el, 'nile-step-change');
expect(ev.detail.value).to.equal(1);
});
it('27. nile-step-change bubbles and is composed', async () => {
const el = await fixture(
html``
);
setTimeout(() => dots(el)[1].click());
const ev = await oneEvent(el, 'nile-step-change');
expect(ev.bubbles).to.be.true;
expect(ev.composed).to.be.true;
});
// --- Disabled behaviour ---
it('28. disabled blocks click (no current change)', async () => {
const el = await fixture(
html``
);
dots(el)[2].click();
await el.updateComplete;
expect(el.current).to.equal(1);
});
it('29. disabled blocks click (no event)', async () => {
const el = await fixture(
html``
);
let fired = false;
el.addEventListener('nile-step-change', () => (fired = true));
dots(el)[2].click();
await el.updateComplete;
expect(fired).to.be.false;
});
it('30. disabled sets disabled attribute on each dot', async () => {
const el = await fixture(
html``
);
expect(dots(el).every((d) => d.disabled)).to.be.true;
});
it('31. disabled blocks keyboard interaction', async () => {
const el = await fixture(
html``
);
let fired = false;
el.addEventListener('nile-step-change', () => (fired = true));
const base = el.shadowRoot!.querySelector('[part="base"]')!;
base.dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })
);
await el.updateComplete;
expect(el.current).to.equal(2);
expect(fired).to.be.false;
});
// --- Keyboard navigation ---
const dispatchKey = (el: NileDotStepper, key: string) => {
const base = el.shadowRoot!.querySelector('[part="base"]')!;
base.dispatchEvent(
new KeyboardEvent('keydown', { key, bubbles: true })
);
};
it('32. ArrowRight increments current and emits value', async () => {
const el = await fixture(
html``
);
setTimeout(() => dispatchKey(el, 'ArrowRight'));
const ev = await oneEvent(el, 'nile-step-change');
expect(ev.detail.value).to.equal(3);
expect(el.current).to.equal(3);
});
it('33. ArrowLeft decrements current and emits value', async () => {
const el = await fixture(
html``
);
setTimeout(() => dispatchKey(el, 'ArrowLeft'));
const ev = await oneEvent(el, 'nile-step-change');
expect(ev.detail.value).to.equal(2);
expect(el.current).to.equal(2);
});
it('34. ArrowRight clamps at total', async () => {
const el = await fixture(
html``
);
setTimeout(() => dispatchKey(el, 'ArrowRight'));
const ev = await oneEvent(el, 'nile-step-change');
expect(ev.detail.value).to.equal(5);
expect(el.current).to.equal(5);
});
it('35. ArrowLeft clamps at 1', async () => {
const el = await fixture(
html``
);
setTimeout(() => dispatchKey(el, 'ArrowLeft'));
const ev = await oneEvent(el, 'nile-step-change');
expect(ev.detail.value).to.equal(1);
expect(el.current).to.equal(1);
});
it('36. Home jumps to first dot', async () => {
const el = await fixture(
html``
);
setTimeout(() => dispatchKey(el, 'Home'));
const ev = await oneEvent(el, 'nile-step-change');
expect(ev.detail.value).to.equal(1);
expect(el.current).to.equal(1);
});
it('37. End jumps to last dot', async () => {
const el = await fixture(
html``
);
setTimeout(() => dispatchKey(el, 'End'));
const ev = await oneEvent(el, 'nile-step-change');
expect(ev.detail.value).to.equal(5);
expect(el.current).to.equal(5);
});
it('38. unhandled key does not change current', async () => {
const el = await fixture(
html``
);
let fired = false;
el.addEventListener('nile-step-change', () => (fired = true));
dispatchKey(el, 'Enter');
await el.updateComplete;
expect(el.current).to.equal(2);
expect(fired).to.be.false;
});
it('39. keyboard nav moves focus to the new active dot', async () => {
const el = await fixture(
html``
);
dispatchKey(el, 'ArrowRight');
await el.updateComplete;
const focused = el.shadowRoot!.activeElement as HTMLButtonElement;
expect(focused).to.exist;
expect(focused.getAttribute('data-index')).to.equal('3');
});
// --- Size variants ---
it('40. size sm applies dot--sm', async () => {
const el = await fixture(
html``
);
expect(dots(el).every((d) => d.classList.contains('dot--sm'))).to.be.true;
});
it('41. size md applies dot--md', async () => {
const el = await fixture(
html``
);
expect(dots(el).every((d) => d.classList.contains('dot--md'))).to.be.true;
});
it('42. size lg applies dot--lg', async () => {
const el = await fixture(
html``
);
expect(dots(el).every((d) => d.classList.contains('dot--lg'))).to.be.true;
});
it('43. dynamic size update changes dot class', async () => {
const el = await fixture(
html``
);
el.size = 'lg';
await el.updateComplete;
expect(dots(el).every((d) => d.classList.contains('dot--lg'))).to.be.true;
expect(dots(el).some((d) => d.classList.contains('dot--md'))).to.be.false;
});
// --- Accessibility / parts ---
it('44. base part present', async () => {
const el = await fixture(
html``
);
expect(el.shadowRoot!.querySelector('[part="base"]')).to.exist;
});
it('45. base has role tablist', async () => {
const el = await fixture(
html``
);
expect(
el.shadowRoot!.querySelector('[part="base"]')!.getAttribute('role')
).to.equal('tablist');
});
it('46. base has aria-label Progress', async () => {
const el = await fixture(
html``
);
expect(
el.shadowRoot!.querySelector('[part="base"]')!.getAttribute('aria-label')
).to.equal('Progress');
});
it('47. dot part present', async () => {
const el = await fixture(
html``
);
expect(el.shadowRoot!.querySelector('[part="dot"]')).to.exist;
});
it('48. each dot has role tab', async () => {
const el = await fixture(
html``
);
expect(dots(el).every((d) => d.getAttribute('role') === 'tab')).to.be.true;
});
it('49. dot aria-label is "Step N of M"', async () => {
const el = await fixture(
html``
);
const list = dots(el);
expect(list[0].getAttribute('aria-label')).to.equal('Step 1 of 5');
expect(list[4].getAttribute('aria-label')).to.equal('Step 5 of 5');
});
it('50. roving tabindex: active 0, others -1', async () => {
const el = await fixture(
html``
);
const list = dots(el);
expect(list[2].getAttribute('tabindex')).to.equal('0');
expect(list[0].getAttribute('tabindex')).to.equal('-1');
expect(list[1].getAttribute('tabindex')).to.equal('-1');
expect(list[3].getAttribute('tabindex')).to.equal('-1');
});
it('51. roving tabindex updates after navigation', async () => {
const el = await fixture(
html``
);
el.current = 3;
await el.updateComplete;
const list = dots(el);
expect(list[2].getAttribute('tabindex')).to.equal('0');
expect(list[0].getAttribute('tabindex')).to.equal('-1');
});
it('52. dots are button elements of type button', async () => {
const el = await fixture(
html``
);
expect(
dots(el).every(
(d) => d.tagName === 'BUTTON' && d.getAttribute('type') === 'button'
)
).to.be.true;
});
it('53. full integration', async () => {
const el = await fixture(
html``
);
expect(dots(el).length).to.equal(6);
expect(el.size).to.equal('lg');
const active = dots(el).filter((d) => d.classList.contains('dot--active'));
expect(active.length).to.equal(1);
expect(active[0].getAttribute('data-index')).to.equal('4');
setTimeout(() => dispatchKey(el, 'End'));
const ev = await oneEvent(el, 'nile-step-change');
expect(ev.detail.value).to.equal(6);
expect(el.current).to.equal(6);
});
});