import { aTimeout, expect, fixture, html } from '@open-wc/testing'; import { ifDefined } from 'lit/directives/if-defined.js'; import './nile-nav-tab-group'; import '../nile-nav-tab/nile-nav-tab'; import '../nile-nav-tab-panel/nile-nav-tab-panel'; import type { NileNavTabGroup } from './nile-nav-tab-group'; import type { NileNavTab } from '../nile-nav-tab/nile-nav-tab'; import type { NileNavTabPanel } from '../nile-nav-tab-panel/nile-nav-tab-panel'; /** Use real Lit bindings — raw strings like `placement="bottom"` in the tag do not apply attributes. */ type MakeGroupOpts = { placement?: NileNavTabGroup['placement']; variant?: NileNavTabGroup['variant']; value?: string; noTrack?: boolean; fullWidth?: boolean; centered?: boolean; showIndicatorOnHover?: boolean; indicatorPlacement?: string; width?: string; }; const makeGroup = async (opts: MakeGroupOpts = {}) => fixture(html` General Custom Disabled General panel Custom panel Disabled panel `); const getTabs = (el: NileNavTabGroup) => Array.from(el.querySelectorAll('nile-nav-tab')); const getPanels = (el: NileNavTabGroup) => Array.from(el.querySelectorAll('nile-nav-tab-panel')); describe('NileNavTabGroup', () => { // ---- Rendering ---- it('renders with nav and body slots', async () => { const el = await makeGroup(); expect(el).to.exist; expect(el.shadowRoot).to.not.be.null; expect(el.shadowRoot!.querySelector('slot[name="nav"]')).to.exist; expect(el.shadowRoot!.querySelector('slot:not([name])')).to.exist; }); it('renders base wrapper with default top placement class', async () => { const el = await makeGroup(); const base = el.shadowRoot!.querySelector('.nav-tab-group'); expect(base).to.exist; expect(base!.classList.contains('nav-tab-group--top')).to.be.true; }); it('applies placement bottom class', async () => { const el = await makeGroup({ placement: 'bottom' }); const base = el.shadowRoot!.querySelector('.nav-tab-group')!; expect(base.classList.contains('nav-tab-group--bottom')).to.be.true; }); it('applies placement start class', async () => { const el = await makeGroup({ placement: 'start' }); const base = el.shadowRoot!.querySelector('.nav-tab-group')!; expect(base.classList.contains('nav-tab-group--start')).to.be.true; }); it('applies placement end class', async () => { const el = await makeGroup({ placement: 'end' }); const base = el.shadowRoot!.querySelector('.nav-tab-group')!; expect(base.classList.contains('nav-tab-group--end')).to.be.true; }); it('renders indicator by default (underline variant)', async () => { const el = await makeGroup(); expect(el.shadowRoot!.querySelector('.nav-tab-group__indicator')).to.exist; }); it('renders pill and no indicator for filled variant', async () => { const el = await makeGroup({ variant: 'filled' }); expect(el.shadowRoot!.querySelector('.nav-tab-group__pill')).to.exist; expect(el.shadowRoot!.querySelector('.nav-tab-group__indicator')).to.be.null; }); it('renders pill for neutral-filled variant', async () => { const el = await makeGroup({ variant: 'neutral-filled' }); expect(el.shadowRoot!.querySelector('.nav-tab-group__pill')).to.exist; }); it('toggle variant renders toggle frame and pill not underline indicator', async () => { const el = await makeGroup({ variant: 'toggle' }); expect(el.shadowRoot!.querySelector('.nav-tab-group__toggle-frame')).to.exist; expect(el.shadowRoot!.querySelector('.nav-tab-group__pill')).to.exist; expect(el.shadowRoot!.querySelector('.nav-tab-group__indicator')).to.be.null; }); it('toggle-button variant renders pill without underline indicator', async () => { const el = await makeGroup({ variant: 'toggle-button' }); expect(el.shadowRoot!.querySelector('.nav-tab-group__pill')).to.exist; expect(el.shadowRoot!.querySelector('.nav-tab-group__indicator')).to.be.null; }); it('tab list has role tablist', async () => { const el = await makeGroup(); const list = el.shadowRoot?.querySelector('.nav-tab-group__tabs'); expect(list?.getAttribute('role')).to.equal('tablist'); }); it('nav region exposes part nav on container', async () => { const el = await makeGroup(); const nav = el.shadowRoot!.querySelector('.nav-tab-group__nav-container'); expect(nav!.getAttribute('part')).to.equal('nav'); }); it('body slot exposes part body', async () => { const el = await makeGroup(); const body = el.shadowRoot!.querySelector('.nav-tab-group__body'); expect(body!.getAttribute('part')).to.equal('body'); }); it('no-track adds hide__track on base', async () => { const el = await makeGroup({ noTrack: true }); const base = el.shadowRoot!.querySelector('.nav-tab-group')!; expect(base.classList.contains('hide__track')).to.be.true; }); it('fullWidth reflects to attribute and tabs stretch', async () => { const el = await makeGroup({ fullWidth: true }); expect(el.hasAttribute('fullwidth')).to.be.true; const base = el.shadowRoot!.querySelector('.nav-tab-group')!; const tabs = base.querySelector('.nav-tab-group__tabs'); expect(tabs).to.exist; }); it('showIndicatorOnHover adds hover indicator node for underline', async () => { const el = await makeGroup({ showIndicatorOnHover: true }); expect(el.shadowRoot!.querySelector('.nav-tab-group__hover-indicator')).to.exist; }); it('indicatorPlacement left applies alongside placement start', async () => { const el = await makeGroup({ placement: 'start', indicatorPlacement: 'left', value: 'general', }); await aTimeout(0); expect(el.indicatorPlacement).to.equal('left'); expect(el.placement).to.equal('start'); }); // ---- Defaults & reflection ---- it('has expected defaults', async () => { const el = await makeGroup(); expect(el.placement).to.equal('top'); expect(el.activeTabProp).to.equal(''); expect(el.noTrack).to.be.false; expect(el.noScrollControls).to.be.false; expect(el.centered).to.be.false; expect(el.fullWidth).to.be.false; expect(el.variant).to.equal('underline'); }); it('reflects `value` attribute to activeTabProp', async () => { const el = await makeGroup({ value: 'custom' }); await el.updateComplete; expect(el.activeTabProp).to.equal('custom'); }); it('reflects centered attribute', async () => { const el = await makeGroup({ centered: true }); expect(el.centered).to.be.true; }); it('localName is nile-nav-tab-group', async () => { const el = await makeGroup(); expect(el.localName).to.equal('nile-nav-tab-group'); }); it('shadow root is open', async () => { const el = await makeGroup(); expect(el.shadowRoot!.mode).to.equal('open'); }); it('exposes static styles and custom element registration', async () => { const mod = await import('./nile-nav-tab-group'); expect(mod.NileNavTabGroup.styles).to.exist; expect(customElements.get('nile-nav-tab-group')).to.exist; }); it('default export matches named class', async () => { const mod = await import('./nile-nav-tab-group'); expect(mod.default).to.equal(mod.NileNavTabGroup); }); // ---- Selection behavior ---- it('selects tab/panel from value at startup', async () => { const el = await makeGroup({ value: 'custom' }); await aTimeout(0); await el.updateComplete; const tabs = getTabs(el); const panels = getPanels(el); const customTab = tabs.find(t => t.getAttribute('panel') === 'custom')!; const generalTab = tabs.find(t => t.getAttribute('panel') === 'general')!; const customPanel = panels.find(p => p.getAttribute('name') === 'custom')!; const generalPanel = panels.find(p => p.getAttribute('name') === 'general')!; expect(customTab.hasAttribute('active')).to.be.true; expect(generalTab.hasAttribute('active')).to.be.false; expect(customPanel.hasAttribute('active')).to.be.true; expect(generalPanel.hasAttribute('active')).to.be.false; }); it('updates selected tab/panel when activeTabProp changes', async () => { const el = await makeGroup({ value: 'general' }); el.activeTabProp = 'custom'; await aTimeout(0); await el.updateComplete; const tabs = getTabs(el); const panels = getPanels(el); expect(tabs.find(t => t.getAttribute('panel') === 'custom')!.hasAttribute('active')).to.be.true; expect(panels.find(p => p.getAttribute('name') === 'custom')!.hasAttribute('active')).to.be.true; }); it('clicking an enabled tab changes activeTabName', async () => { const el = await makeGroup({ value: 'general' }); const customTab = getTabs(el).find(t => t.getAttribute('panel') === 'custom')!; customTab.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true })); await aTimeout(0); await el.updateComplete; expect(el.activeTabName).to.equal('custom'); expect(document.activeElement).to.equal(customTab); }); it('clicking a disabled tab does not change activeTabName', async () => { const el = await makeGroup({ value: 'general' }); const disabledTab = getTabs(el).find(t => t.getAttribute('panel') === 'disabled')!; disabledTab.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true })); await aTimeout(0); await el.updateComplete; expect(el.activeTabName).to.equal('general'); }); it('modifier click on tab does not change activeTabName (native link / group both ignore)', async () => { const el = await makeGroup({ value: 'general' }); const customTab = getTabs(el).find(t => t.getAttribute('panel') === 'custom'); expect(customTab).to.exist; customTab?.dispatchEvent( new MouseEvent('click', { bubbles: true, composed: true, ctrlKey: true, button: 0 }) ); await aTimeout(0); await el.updateComplete; expect(el.activeTabName).to.equal('general'); }); it('emits nile-tab-change with details when selection changes', async () => { const el = await makeGroup({ value: 'general' }); let detail: any; el.addEventListener('nile-tab-change', (e: Event) => { detail = (e as CustomEvent).detail; }); const customTab = getTabs(el).find(t => t.getAttribute('panel') === 'custom')!; customTab.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true })); await aTimeout(0); await el.updateComplete; expect(detail).to.exist; expect(detail.value).to.equal('custom'); expect(detail.previousValue).to.equal('general'); expect(detail.index).to.equal(1); }); it('nile-tab-change detail includes link when selected tab has link', async () => { const el = await fixture(html` A B `); await aTimeout(0); let detail: any; el.addEventListener('nile-tab-change', (e: Event) => { detail = (e as CustomEvent).detail; }); getTabs(el) .find(t => t.getAttribute('panel') === 'a')! .dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true })); await aTimeout(0); await el.updateComplete; expect(detail.value).to.equal('a'); expect(detail.link).to.equal('https://u.example/a'); }); it('re-emits nile-close from child tab with panel detail', async () => { const el = await makeGroup(); const generalTab = getTabs(el).find(t => t.getAttribute('panel') === 'general')!; let closeDetail: any; el.addEventListener('nile-close', (e: Event) => { closeDetail = (e as CustomEvent).detail; }); generalTab.dispatchEvent(new CustomEvent('nile-close', { bubbles: true, composed: true })); await el.updateComplete; expect(closeDetail).to.deep.equal({ panel: 'general' }); }); it('centered distributes to slotted nav tabs', async () => { const el = await makeGroup({ centered: true, value: 'general' }); await aTimeout(0); const first = getTabs(el)[0]; expect(first?.centered).to.be.true; }); // ---- Keyboard navigation ---- it('ArrowRight moves focus across enabled tabs', async () => { const el = await makeGroup({ value: 'general' }); const tabs = getTabs(el); const first = tabs.find(t => t.getAttribute('panel') === 'general')!; const second = tabs.find(t => t.getAttribute('panel') === 'custom')!; first.focus(); first.dispatchEvent( new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true, composed: true, cancelable: true, }) ); await aTimeout(0); expect(document.activeElement).to.equal(second); }); it('ArrowLeft wraps from first enabled tab to last enabled', async () => { const el = await makeGroup({ value: 'general' }); const tabs = getTabs(el); const first = tabs.find(t => t.getAttribute('panel') === 'general')!; const second = tabs.find(t => t.getAttribute('panel') === 'custom')!; first.focus(); first.dispatchEvent( new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true, composed: true, cancelable: true, }) ); await aTimeout(0); expect(document.activeElement).to.equal(second); }); it('RTL horizontal: ArrowLeft moves focus like LTR ArrowRight', async () => { const el = await makeGroup({ value: 'general' }); el.setAttribute('dir', 'rtl'); const tabs = getTabs(el); const first = tabs.find(t => t.getAttribute('panel') === 'general')!; const second = tabs.find(t => t.getAttribute('panel') === 'custom')!; first.focus(); first.dispatchEvent( new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true, composed: true, cancelable: true, }) ); await aTimeout(0); expect(document.activeElement).to.equal(second); }); it('RTL horizontal: ArrowRight wraps from first like LTR ArrowLeft', async () => { const el = await makeGroup({ value: 'general' }); el.setAttribute('dir', 'rtl'); const tabs = getTabs(el); const first = tabs.find(t => t.getAttribute('panel') === 'general')!; const second = tabs.find(t => t.getAttribute('panel') === 'custom')!; first.focus(); first.dispatchEvent( new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true, composed: true, cancelable: true, }) ); await aTimeout(0); expect(document.activeElement).to.equal(second); }); it('Home focuses first enabled tab', async () => { const el = await makeGroup({ value: 'custom' }); const tabs = getTabs(el); const custom = tabs.find(t => t.getAttribute('panel') === 'custom')!; const general = tabs.find(t => t.getAttribute('panel') === 'general')!; custom.focus(); custom.dispatchEvent( new KeyboardEvent('keydown', { key: 'Home', bubbles: true, composed: true, cancelable: true, }) ); await aTimeout(0); expect(document.activeElement).to.equal(general); }); it('End focuses last enabled tab', async () => { const el = await makeGroup({ value: 'general' }); const tabs = getTabs(el); const custom = tabs.find(t => t.getAttribute('panel') === 'custom')!; const general = tabs.find(t => t.getAttribute('panel') === 'general')!; general.focus(); general.dispatchEvent( new KeyboardEvent('keydown', { key: 'End', bubbles: true, composed: true, cancelable: true, }) ); await aTimeout(0); expect(document.activeElement).to.equal(custom); }); it('Enter selects the focused tab', async () => { const el = await makeGroup({ value: 'general' }); const customTab = getTabs(el).find(t => t.getAttribute('panel') === 'custom')!; customTab.focus(); customTab.dispatchEvent( new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, composed: true, cancelable: true, }) ); await aTimeout(0); await el.updateComplete; expect(el.activeTabName).to.equal('custom'); }); it('Space selects the focused tab', async () => { const el = await makeGroup({ value: 'general' }); const customTab = getTabs(el).find(t => t.getAttribute('panel') === 'custom')!; customTab.focus(); customTab.dispatchEvent( new KeyboardEvent('keydown', { key: ' ', bubbles: true, composed: true, cancelable: true, }) ); await aTimeout(0); await el.updateComplete; expect(el.activeTabName).to.equal('custom'); }); it('ArrowDown moves roving focus when placement is start', async () => { const el = await makeGroup({ placement: 'start', value: 'general' }); const tabs = getTabs(el); const general = tabs.find(t => t.getAttribute('panel') === 'general')!; const custom = tabs.find(t => t.getAttribute('panel') === 'custom')!; general.focus(); general.dispatchEvent( new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, composed: true, cancelable: true, }) ); await aTimeout(0); expect(document.activeElement).to.equal(custom); }); it('ArrowUp moves roving focus when placement is start', async () => { const el = await makeGroup({ placement: 'start', value: 'custom' }); const tabs = getTabs(el); const general = tabs.find(t => t.getAttribute('panel') === 'general')!; const custom = tabs.find(t => t.getAttribute('panel') === 'custom')!; custom.focus(); custom.dispatchEvent( new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true, composed: true, cancelable: true, }) ); await aTimeout(0); expect(document.activeElement).to.equal(general); }); it('keydown on disabled tab calls preventDefault', async () => { const el = await makeGroup({ value: 'general' }); const disabled = getTabs(el).find(t => t.getAttribute('panel') === 'disabled')!; disabled.focus(); const ev = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, composed: true, cancelable: true, }); disabled.dispatchEvent(ev); expect(ev.defaultPrevented).to.be.true; }); // ---- A11y sync ---- it('sets aria-posinset and aria-setsize on enabled tabs only', async () => { const el = await makeGroup(); await aTimeout(0); const tabs = getTabs(el); expect(tabs[0].getAttribute('aria-posinset')).to.equal('1'); expect(tabs[1].getAttribute('aria-posinset')).to.equal('2'); expect(tabs[2].hasAttribute('aria-posinset')).to.be.false; expect(tabs[0].getAttribute('aria-setsize')).to.equal('2'); expect(tabs[1].getAttribute('aria-setsize')).to.equal('2'); expect(tabs[2].hasAttribute('aria-setsize')).to.be.false; expect(tabs[2].getAttribute('aria-hidden')).to.equal('true'); }); it('wires aria-controls and aria-labelledby between tab and panel', async () => { const el = await makeGroup(); await el.updateComplete; await Promise.all(getTabs(el).map(tab => tab.updateComplete)); await Promise.all(getPanels(el).map(panel => panel.updateComplete)); const tab = getTabs(el).find(t => t.getAttribute('panel') === 'general')!; const panel = getPanels(el).find(p => p.getAttribute('name') === 'general')!; expect(tab.getAttribute('aria-controls')).to.equal(panel.id); expect(panel.getAttribute('aria-labelledby')).to.equal(tab.id); }); // ---- Scroll controls ---- it('setScrollControls sets hasScrollControls for horizontal overflow', async () => { const el = await makeGroup(); const nav = el.shadowRoot!.querySelector('.nav-tab-group__nav') as HTMLElement; Object.defineProperty(nav, 'scrollWidth', { configurable: true, value: 500 }); Object.defineProperty(nav, 'clientWidth', { configurable: true, value: 100 }); (el as any).setScrollControls(); expect(el.hasScrollControls).to.be.true; el.noScrollControls = true; (el as any).setScrollControls(); expect(el.hasScrollControls).to.be.false; }); it('hasScrollControls stays false for vertical placement even when overflow math holds', async () => { const el = await makeGroup({ placement: 'start' }); const nav = el.shadowRoot!.querySelector('.nav-tab-group__nav') as HTMLElement; Object.defineProperty(nav, 'scrollWidth', { configurable: true, value: 500 }); Object.defineProperty(nav, 'clientWidth', { configurable: true, value: 100 }); (el as any).setScrollControls(); expect(el.hasScrollControls).to.be.false; }); it('renders scroll buttons when hasScrollControls becomes true', async () => { const el = await makeGroup(); el.hasScrollControls = true; await el.updateComplete; const buttons = el.shadowRoot!.querySelectorAll('nile-icon-button.nav-tab-group__scroll-button'); expect(buttons.length).to.equal(2); }); // ---- Lifecycle ---- it('disconnecting the group does not throw', async () => { const el = await makeGroup(); expect(() => el.remove()).to.not.throw(); }); it('updateComplete resolves', async () => { const el = await makeGroup(); expect(await el.updateComplete).to.be.true; }); it('width property applies inline style token on base', async () => { const el = await makeGroup({ width: '120px' }); await el.updateComplete; const base = el.shadowRoot!.querySelector('.nav-tab-group') as HTMLElement; expect(base.style.getPropertyValue('--nav-tab-item-width').trim()).to.equal('120px'); }); // ---- Extended coverage ---- it('noScrollControls property default false', async () => { const el = await makeGroup(); expect(el.noScrollControls).to.be.false; }); it('noScrollControls can be set true', async () => { const el = await makeGroup(); el.noScrollControls = true; await el.updateComplete; expect(el.noScrollControls).to.be.true; }); it('activeTabName matches value after programmatic value set', async () => { const el = await makeGroup({ value: 'general' }); await aTimeout(0); el.activeTabProp = 'custom'; await el.updateComplete; await aTimeout(0); expect(el.activeTabName).to.equal('custom'); }); it('clicking already selected tab keeps activeTabName', async () => { const el = await makeGroup({ value: 'custom' }); await aTimeout(0); const tab = getTabs(el).find(t => t.getAttribute('panel') === 'custom')!; tab.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true })); await el.updateComplete; expect(el.activeTabName).to.equal('custom'); }); it('nile-tab-change includes previousIndex when moving from first', async () => { const el = await makeGroup({ value: 'general' }); await aTimeout(0); let detail: any; el.addEventListener('nile-tab-change', (e: Event) => { detail = (e as CustomEvent).detail; }); getTabs(el) .find(t => t.getAttribute('panel') === 'custom')! .dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true })); await el.updateComplete; expect(detail.previousIndex).to.equal(0); expect(detail.index).to.equal(1); }); it('setAttribute value updates selection after tick', async () => { const el = await makeGroup({ value: 'general' }); await aTimeout(0); el.setAttribute('value', 'custom'); await aTimeout(0); await el.updateComplete; expect(getTabs(el).find(t => t.getAttribute('panel') === 'custom')!.active).to.be.true; }); it('ArrowRight from custom wraps to general', async () => { const el = await makeGroup({ value: 'custom' }); await aTimeout(0); const tabs = getTabs(el); const custom = tabs.find(t => t.getAttribute('panel') === 'custom')!; const general = tabs.find(t => t.getAttribute('panel') === 'general')!; custom.focus(); custom.dispatchEvent( new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true, composed: true, cancelable: true }) ); await aTimeout(0); expect(document.activeElement).to.equal(general); }); it('re-emits nile-close for custom panel id', async () => { const el = await makeGroup(); let d: any; el.addEventListener('nile-close', (e: Event) => { d = (e as CustomEvent).detail; }); getTabs(el) .find(t => t.getAttribute('panel') === 'custom')! .dispatchEvent(new CustomEvent('nile-close', { bubbles: true, composed: true })); await el.updateComplete; expect(d).to.deep.equal({ panel: 'custom' }); }); it('indicator part exists for underline variant', async () => { const el = await makeGroup(); expect(el.shadowRoot!.querySelector('[part~="active-tab-indicator"]')).to.exist; }); it('pill part exists for filled variant', async () => { const el = await makeGroup({ variant: 'filled' }); expect(el.shadowRoot!.querySelector('[part~="active-tab-pill"]')).to.exist; }); it('body part remains when no panels rendered in shadow alone', async () => { const el = await makeGroup(); expect(el.shadowRoot!.querySelector('[part="body"]')).to.exist; }); it('base part on wrapper exists', async () => { const el = await makeGroup(); expect(el.shadowRoot!.querySelector('[part="base"]')).to.exist; }); it('tabs part exists on list', async () => { const el = await makeGroup(); expect(el.shadowRoot!.querySelector('[part="tabs"]')).to.exist; }); it('centered false does not set centered on first tab', async () => { const el = await makeGroup({ centered: false, value: 'general' }); await aTimeout(0); expect(getTabs(el)[0].centered).to.be.false; }); it('hasScrollControls reflects to attribute', async () => { const el = await makeGroup(); el.hasScrollControls = true; await el.updateComplete; expect(el.hasAttribute('hasscrollcontrols')).to.be.true; }); it('variant underline is default string', async () => { const el = await makeGroup(); expect(el.variant).to.equal('underline'); }); it('variant neutral-filled string', async () => { const el = await makeGroup({ variant: 'neutral-filled' }); expect(el.variant).to.equal('neutral-filled'); }); it('keydown Tab key does not change selection', async () => { const el = await makeGroup({ value: 'general' }); await aTimeout(0); getTabs(el)[0].focus(); getTabs(el)[0].dispatchEvent( new KeyboardEvent('keydown', { key: 'Tab', bubbles: true, composed: true, cancelable: true }) ); await el.updateComplete; expect(el.activeTabName).to.equal('general'); }); it('keydown unrelated key a does not change selection', async () => { const el = await makeGroup({ value: 'general' }); await aTimeout(0); getTabs(el)[0].focus(); getTabs(el)[0].dispatchEvent( new KeyboardEvent('keydown', { key: 'a', bubbles: true, composed: true, cancelable: true }) ); await el.updateComplete; expect(el.activeTabName).to.equal('general'); }); it('placement bottom still has tablist role', async () => { const el = await makeGroup({ placement: 'bottom' }); expect(el.shadowRoot?.querySelector('.nav-tab-group__tabs')?.getAttribute('role')).to.equal('tablist'); }); it('ArrowLeft from second tab moves to first', async () => { const el = await makeGroup({ value: 'custom' }); await aTimeout(0); const tabs = getTabs(el); const custom = tabs.find(t => t.getAttribute('panel') === 'custom')!; const general = tabs.find(t => t.getAttribute('panel') === 'general')!; custom.focus(); custom.dispatchEvent( new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true, composed: true, cancelable: true }) ); await aTimeout(0); expect(document.activeElement).to.equal(general); }); it('End from first tab focuses last enabled tab', async () => { const el = await makeGroup({ value: 'general' }); await aTimeout(0); const general = getTabs(el).find(t => t.getAttribute('panel') === 'general')!; const custom = getTabs(el).find(t => t.getAttribute('panel') === 'custom')!; general.focus(); general.dispatchEvent( new KeyboardEvent('keydown', { key: 'End', bubbles: true, composed: true, cancelable: true }) ); await aTimeout(0); expect(document.activeElement).to.equal(custom); }); it('fullWidth false by default', async () => { const el = await makeGroup(); expect(el.fullWidth).to.be.false; }); it('noTrack false by default', async () => { const el = await makeGroup(); expect(el.noTrack).to.be.false; }); it('showIndicatorOnHover false by default', async () => { const el = await makeGroup(); expect(el.showIndicatorOnHover).to.be.false; }); it('width defaults empty string', async () => { const el = await makeGroup(); expect(el.width).to.equal(''); }); it('indicatorPlacement defaults empty', async () => { const el = await makeGroup(); expect(el.indicatorPlacement).to.equal(''); }); it('getTabs returns three slotted nav tabs', async () => { const el = await makeGroup(); await aTimeout(0); expect(getTabs(el).length).to.equal(3); }); it('getPanels returns three panels', async () => { const el = await makeGroup(); await aTimeout(0); expect(getPanels(el).length).to.equal(3); }); it('removing value attribute clears value attr on host', async () => { const el = await makeGroup({ value: 'general' }); await aTimeout(0); el.removeAttribute('value'); await el.updateComplete; await aTimeout(0); expect(el.hasAttribute('value')).to.be.false; }); it('tagName lowercases correctly', async () => { const el = await makeGroup(); expect(el.tagName.toLowerCase()).to.equal('nile-nav-tab-group'); }); it('scroll buttons have label attributes', async () => { const el = await makeGroup(); el.hasScrollControls = true; await el.updateComplete; const btns = el.shadowRoot!.querySelectorAll('nile-icon-button.nav-tab-group__scroll-button'); expect(btns.length).to.equal(2); expect(btns[0]?.getAttribute('label')).to.exist; expect(btns[1]?.getAttribute('label')).to.exist; }); it('toggle variant still exposes tablist', async () => { const el = await makeGroup({ variant: 'toggle' }); expect(el.shadowRoot!.querySelector('[role="tablist"]')).to.exist; }); it('Enter after focus general keeps general selected', async () => { const el = await makeGroup({ value: 'general' }); await aTimeout(0); const g = getTabs(el).find(t => t.getAttribute('panel') === 'general')!; g.focus(); g.dispatchEvent( new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, composed: true, cancelable: true }) ); await el.updateComplete; expect(el.activeTabName).to.equal('general'); }); it('Space on general keeps general', async () => { const el = await makeGroup({ value: 'general' }); await aTimeout(0); const g = getTabs(el).find(t => t.getAttribute('panel') === 'general')!; g.focus(); g.dispatchEvent( new KeyboardEvent('keydown', { key: ' ', bubbles: true, composed: true, cancelable: true }) ); await el.updateComplete; expect(el.activeTabName).to.equal('general'); }); it('fullWidth and centered can both be true', async () => { const el = await makeGroup({ fullWidth: true, centered: true, value: 'general' }); await aTimeout(0); expect(el.fullWidth).to.be.true; expect(el.centered).to.be.true; expect(getTabs(el)[0].centered).to.be.true; }); it('emits a single nile-tab-change for one user click', async () => { const el = await makeGroup({ value: 'general' }); await aTimeout(0); let n = 0; el.addEventListener('nile-tab-change', () => n++); getTabs(el) .find(t => t.getAttribute('panel') === 'custom')! .dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true })); await el.updateComplete; expect(n).to.equal(1); }); it('createElement upgrades nile-nav-tab-group', async () => { const el = document.createElement('nile-nav-tab-group') as NileNavTabGroup; document.body.appendChild(el); await el.updateComplete; expect(el.shadowRoot).to.not.be.null; el.remove(); }); it('nav container has nav-container class', async () => { const el = await makeGroup(); expect(el.shadowRoot!.querySelector('.nav-tab-group__nav-container')).to.exist; }); it('underline variant exposes indicator path part', async () => { const el = await makeGroup(); expect(el.shadowRoot!.querySelector('[part~="active-tab-indicator-path"]')).to.exist; }); /** * 100 generated cases — `yarn tsc` emits dist/src/nile-nav-tab-group/nile-nav-tab-group.test.js. */ describe('bulk generated coverage (100 cases)', () => { for (let i = 0; i < 100; i++) { it(`bulk case ${i + 1}/100 (mode ${i % 10})`, async () => { const mode = i % 10; if (mode === 0) { const placements: NileNavTabGroup['placement'][] = ['top', 'bottom', 'start', 'end']; const p = placements[i % 4]!; const el = await makeGroup({ placement: p, value: 'general' }); await aTimeout(0); const base = el.shadowRoot!.querySelector('.nav-tab-group')!; expect(base.classList.contains(`nav-tab-group--${p}`)).to.be.true; } else if (mode === 1) { const variants: NileNavTabGroup['variant'][] = [ 'underline', 'filled', 'toggle', 'neutral-filled', 'toggle-button', ]; const v = variants[i % 5]!; const el = await makeGroup({ variant: v }); await aTimeout(0); if (v === 'underline') { expect(el.shadowRoot!.querySelector('.nav-tab-group__indicator')).to.exist; } else { expect(el.shadowRoot!.querySelector('.nav-tab-group__pill')).to.exist; } } else if (mode === 2) { const el = await makeGroup({ value: 'custom' }); await aTimeout(0); expect(getTabs(el).find(t => t.getAttribute('panel') === 'custom')!.active).to.be.true; } else if (mode === 3) { const el = await makeGroup({ value: 'general' }); await aTimeout(0); getTabs(el) .find(t => t.getAttribute('panel') === 'custom')! .dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true })); await el.updateComplete; expect(el.activeTabName).to.equal('custom'); } else if (mode === 4) { const el = await makeGroup({ centered: true, value: 'general' }); await aTimeout(0); expect(getTabs(el).every(t => t.centered)).to.be.true; } else if (mode === 5) { const el = await makeGroup({ noTrack: true }); await aTimeout(0); expect(el.shadowRoot!.querySelector('.nav-tab-group')!.classList.contains('hide__track')).to.be.true; } else if (mode === 6) { const el = await makeGroup({ fullWidth: true }); await aTimeout(0); expect(el.fullWidth).to.be.true; } else if (mode === 7) { const el = await makeGroup(); await aTimeout(0); expect(el.shadowRoot!.querySelector('[role="tablist"]')).to.exist; } else if (mode === 8) { const el = await makeGroup(); await aTimeout(0); expect(getTabs(el).length).to.equal(3); expect(getPanels(el).length).to.equal(3); } else { const w = `${40 + (i % 20)}px`; const el = await makeGroup({ width: w, value: 'general' }); await aTimeout(0); const base = el.shadowRoot!.querySelector('.nav-tab-group') as HTMLElement; expect(base.style.getPropertyValue('--nav-tab-item-width').trim()).to.equal(w); } }); } }); });