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); }); });