import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import './nile-empty-state';
// nile-glyph lives in its own package and is NOT registered by nile-elements —
// the component documents it as a @dependency. Register it here or every glyph
// assertion below would be querying an element that never upgrades.
import '@aquera/nile-glyph/nile-glyph';
import type { NileEmptyState } from './nile-empty-state';
describe('NileEmptyState', () => {
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-empty-state'); });
it('4. size defaults md', async () => { const el = await fixture(html``); expect(el.size).to.equal('md'); });
it('5. variant defaults tonal', async () => { const el = await fixture(html``); expect(el.variant).to.equal('tonal'); });
it('6. text default', async () => { const el = await fixture(html``); expect(el.text).to.equal('Empty State'); });
it('7. subText default', async () => { const el = await fixture(html``); expect(el.subText).to.equal('No Data'); });
it('9. svgIconSize unset by default', async () => { const el = await fixture(html``); expect(el.svgIconSize).to.be.undefined; });
it('10. container part', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('[part="container"]')).to.exist; });
it('11. body part', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('[part="body"]')).to.exist; });
it('12. text part', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('[part="text"]')).to.exist; });
it('13. subtitle part', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('[part="subtitle"]')).to.exist; });
it('14. actions part', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('[part="actions"]')).to.exist; });
it('15. text-section part', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('[part="text-section"]')).to.exist; });
it('16. empty-state class', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('.empty-state')).to.exist; });
it('17. sm class', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('.empty-state--sm')).to.exist; });
it('18. md class', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('.empty-state--md')).to.exist; });
it('19. lg class', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('.empty-state--lg')).to.exist; });
it('20. tonal body', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('.empty-state__body--tonal')).to.exist; });
it('21. content body', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('.empty-state__body--content')).to.exist; });
it('22. flat body', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('.empty-state__body--flat')).to.exist; });
it('23. text displayed', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('.empty-state__text')!.textContent).to.contain('Nothing here'); });
it('24. subText displayed', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('.empty-state__subtext')!.textContent).to.contain('Try again'); });
it('25. nile-glyph in tonal', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('nile-glyph')).to.exist; });
it('26. nile-glyph in content', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('nile-glyph')).to.exist; });
it('27. nile-glyph in flat', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('nile-glyph')).to.exist; });
it('28. slot', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('slot')).to.exist; });
it('29. slotted content', async () => { const el = await fixture(html``); expect(el.querySelector('button')).to.exist; });
it('30. dynamic text', async () => { const el = await fixture(html``); el.text = 'New Text'; await el.updateComplete; expect(el.shadowRoot!.querySelector('.empty-state__text')!.textContent).to.contain('New Text'); });
it('31. dynamic subText', async () => { const el = await fixture(html``); el.subText = 'New Sub'; await el.updateComplete; expect(el.shadowRoot!.querySelector('.empty-state__subtext')!.textContent).to.contain('New Sub'); });
it('32. dynamic variant', async () => { const el = await fixture(html``); el.variant = 'flat'; await el.updateComplete; expect(el.shadowRoot!.querySelector('.empty-state__body--flat')).to.exist; });
it('33. dynamic size', async () => { const el = await fixture(html``); el.size = 'lg'; await el.updateComplete; expect(el.shadowRoot!.querySelector('.empty-state--lg')).to.exist; });
it('34. has styles', async () => { expect((await import('./nile-empty-state')).NileEmptyState.styles).to.exist; });
it('35. is defined', async () => { expect(customElements.get('nile-empty-state')).to.exist; });
it('36. shadow mode', async () => { const el = await fixture(html``); expect(el.shadowRoot!.mode).to.equal('open'); });
it('37. isConnected', async () => { const el = await fixture(html``); expect(el.isConnected).to.be.true; });
it('38. removal', async () => { const el = await fixture(html``); el.remove(); expect(el.isConnected).to.be.false; });
it('39. outerHTML', async () => { const el = await fixture(html``); expect(el.outerHTML).to.contain('nile-empty-state'); });
it('40. matches', async () => { const el = await fixture(html``); expect(el.matches('nile-empty-state.x')).to.be.true; });
it('41. closest', async () => { const el = await fixture(html``); expect(el.closest('nile-empty-state')).to.equal(el); });
it('42. cloneNode', async () => { const el = await fixture(html``); expect((el.cloneNode(true) as Element).tagName.toLowerCase()).to.equal('nile-empty-state'); });
it('43. dispatchEvent', async () => { const el = await fixture(html``); let f = false; el.addEventListener('c', () => (f = true)); el.dispatchEvent(new Event('c')); expect(f).to.be.true; });
it('44. updateComplete', async () => { const el = await fixture(html``); const r = await el.updateComplete; expect(r).to.not.be.undefined; });
it('45. requestUpdate', async () => { const el = await fixture(html``); el.requestUpdate(); await el.updateComplete; expect(el.shadowRoot).to.not.be.null; });
it('46. render method', async () => { const el = await fixture(html``); expect(el.render).to.be.a('function'); });
it('47. shadowRoot host', async () => { const el = await fixture(html``); expect(el.shadowRoot!.host).to.equal(el); });
it('48. class attr', async () => { const el = await fixture(html``); expect(el.classList.contains('e')).to.be.true; });
it('49. id attr', async () => { const el = await fixture(html``); expect(el.id).to.equal('e1'); });
it('50. style attr', async () => { const el = await fixture(html``); expect(el.style.color).to.equal('red'); });
it('51. data attr', async () => { const el = await fixture(html``); expect(el.getAttribute('data-x')).to.equal('1'); });
it('52. hidden', async () => { const el = await fixture(html``); expect(el.hidden).to.be.true; });
it('53. dir', async () => { const el = await fixture(html``); expect(el.dir).to.equal('rtl'); });
it('54. nodeType', async () => { const el = await fixture(html``); expect(el.nodeType).to.equal(1); });
it('55. multiple instances', async () => { const c = await fixture(html`
`); expect(c.querySelectorAll('nile-empty-state').length).to.equal(2); });
it('56. parent-child', async () => { const c = await fixture(html`
`); expect(c.querySelector('nile-empty-state')!.parentElement).to.equal(c); });
it('57. localName', async () => { const el = await fixture(html``); expect(el.localName).to.equal('nile-empty-state'); });
it('58. namespaceURI', async () => { const el = await fixture(html``); expect(el.namespaceURI).to.equal('http://www.w3.org/1999/xhtml'); });
it('59. ownerDocument', async () => { const el = await fixture(html``); expect(el.ownerDocument).to.equal(document); });
it('60. icon default', async () => { const el = await fixture(html``); expect(el.icon).to.equal('ng-x-circle'); });
it('61. custom icon', async () => { const el = await fixture(html``); expect(el.icon).to.equal('star'); });
it('62. actions class', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('.empty-state-actions')).to.exist; });
it('63. empty-state__body class', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('.empty-state__body')).to.exist; });
it('64. text-section class', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('.empty__state__text-section')).to.exist; });
it('65. sub-text attr', async () => { const el = await fixture(html``); expect(el.subText).to.equal('Nope'); });
it('66. tonal-body part', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('[part~="tonal-body"]')).to.exist; });
it('67. content-image part', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('[part="content-image"]')).to.exist; });
it('68. flat-body part', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('[part="flat-body"]')).to.exist; });
it('69. no tonal in flat', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('.empty-state__body--tonal')).to.be.null; });
it('70. no content in tonal', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('.empty-state__body--content')).to.be.null; });
it('71. no flat in content', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('.empty-state__body--flat')).to.be.null; });
it('72. icon-wrapper part content', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('[part~="icon-wrapper"]')).to.exist; });
it('73. classList add', async () => { const el = await fixture(html``); el.classList.add('z'); expect(el.classList.contains('z')).to.be.true; });
it('74. dataset', async () => { const el = await fixture(html``); expect(el.dataset.idx).to.equal('0'); });
it('75. getBoundingClientRect', async () => { const el = await fixture(html``); expect(el.getBoundingClientRect()).to.exist; });
it('76. size type string', async () => { const el = await fixture(html``); expect(typeof el.size).to.equal('string'); });
it('77. variant type string', async () => { const el = await fixture(html``); expect(typeof el.variant).to.equal('string'); });
it('79. svgIconSize type number once set', async () => { const el = await fixture(html``); expect(typeof el.svgIconSize).to.equal('number'); });
it('80. createElement', async () => { const el = document.createElement('nile-empty-state') as NileEmptyState; document.body.appendChild(el); await el.updateComplete; expect(el.shadowRoot).to.not.be.null; document.body.removeChild(el); });
it('81. aria-label', async () => { const el = await fixture(html``); expect(el.getAttribute('aria-label')).to.equal('Empty'); });
it('82. role', async () => { const el = await fixture(html``); expect(el.getAttribute('role')).to.equal('status'); });
it('83. rapid variant changes', async () => { const el = await fixture(html``); for (const v of ['flat', 'content', 'tonal'] as const) { el.variant = v; await el.updateComplete; } expect(el.variant).to.equal('tonal'); });
it('84. rapid size changes', async () => { const el = await fixture(html``); for (const s of ['sm', 'md', 'lg'] as const) { el.size = s; await el.updateComplete; } expect(el.size).to.equal('lg'); });
it('85. title attr', async () => { const el = await fixture(html``); expect(el.title).to.equal('E'); });
it('86. lang attr', async () => { const el = await fixture(html``); expect(el.lang).to.equal('en'); });
it('87. tabindex', async () => { const el = await fixture(html``); expect(el.getAttribute('tabindex')).to.equal('0'); });
it('88. no form', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('form')).to.be.null; });
it('89. no input', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('input')).to.be.null; });
it('90. flat icon class', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('.nile-flat-type-icon')).to.exist; });
it('91. content icon class', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('.empty-state__img__icon')).to.exist; });
it('92. children survived update', async () => { const el = await fixture(html``); el.text = 'Updated'; await el.updateComplete; expect(el.querySelector('#b1')).to.exist; });
it('93. dispatchCustomEvent', async () => { const el = await fixture(html``); let d: any; el.addEventListener('t', ((e: CustomEvent) => { d = e.detail; }) as EventListener); el.dispatchEvent(new CustomEvent('t', { detail: 'x' })); expect(d).to.equal('x'); });
it('94. scrollIntoView', async () => { const el = await fixture(html``); expect(el.scrollIntoView).to.be.a('function'); });
it('95. focus method', async () => { const el = await fixture(html``); expect(el.focus).to.be.a('function'); });
it('96. dynamic icon', async () => { const el = await fixture(html``); el.icon = 'star'; await el.updateComplete; const icon = el.shadowRoot!.querySelector('nile-glyph'); expect(icon!.getAttribute('name')).to.equal('star'); });
it('97. svgIconUrl renders custom', async () => { const el = await fixture(html``); await el.updateComplete; const icon = el.shadowRoot!.querySelector('nile-glyph'); expect(icon!.getAttribute('customsvgpath')).to.exist; });
it('98. svgIconSize applies', async () => { const el = await fixture(html``); await el.updateComplete; const icon = el.shadowRoot!.querySelector('nile-glyph'); expect(icon!.getAttribute('size')).to.equal('60'); });
it('99. shadow childNodes', async () => { const el = await fixture(html``); expect(el.shadowRoot!.childNodes.length).to.be.greaterThan(0); });
it('100. legacy full integration', async () => { const el = await fixture(html``); expect(el.text).to.equal('Empty'); expect(el.subText).to.equal('Nothing'); expect(el.variant).to.equal('content'); expect(el.size).to.equal('lg'); expect(el.icon).to.equal('star'); expect(el.id).to.equal('e1'); expect(el.querySelector('button')).to.exist; expect(el.shadowRoot!.querySelector('.empty-state--lg')).to.exist; });
// ── status presets ────────────────────────────────────────────────────────
const container = (el: NileEmptyState) => el.shadowRoot!.querySelector('[part="container"]')!;
const glyph = (el: NileEmptyState) => el.shadowRoot!.querySelector('nile-glyph')!;
it('101. status unset by default', async () => { const el = await fixture(html``); expect(el.status).to.be.undefined; });
it('102. status=empty copy', async () => { const el = await fixture(html``); expect(el.text).to.equal('No data yet'); expect(el.subText).to.contain('Rows appear here'); });
it('103. status=empty icon', async () => { const el = await fixture(html``); expect(el.icon).to.equal('ng-database'); });
it('104. status=no-results copy', async () => { const el = await fixture(html``); expect(el.text).to.equal('No matches'); expect(el.icon).to.equal('ng-search-lg'); });
it('105. status=not-found copy', async () => { const el = await fixture(html``); expect(el.text).to.contain(`couldn't find that`); expect(el.icon).to.equal('ng-file-06'); });
it('106. status=no-access copy', async () => { const el = await fixture(html``); expect(el.text).to.contain(`don't have access`); expect(el.icon).to.equal('ng-lock'); });
it('107. status=error copy', async () => { const el = await fixture(html``); expect(el.text).to.equal('Something went wrong'); expect(el.icon).to.equal('ng-alert-circle'); });
it('108. status=offline copy', async () => { const el = await fixture(html``); expect(el.text).to.equal('Connection lost'); expect(el.icon).to.equal('ng-cloud-off'); });
it('109. preset icon reaches the glyph', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('name')).to.equal('ng-database'); });
it('110. preset renders the copy', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('.empty-state__text')!.textContent).to.contain('No matches'); });
it('111. status reflects', async () => { const el = await fixture(html``); expect(el.getAttribute('status')).to.equal('error'); });
it('112. status change reruns the preset', async () => { const el = await fixture(html``); el.status = 'offline'; await el.updateComplete; expect(el.text).to.equal('Connection lost'); });
// ── per-property overrides beat the preset ────────────────────────────────
it('113. text overrides preset', async () => { const el = await fixture(html``); expect(el.text).to.equal('Mine'); expect(el.subText).to.contain('Rows appear here'); });
it('114. sub-text overrides preset', async () => { const el = await fixture(html``); expect(el.subText).to.equal('Mine'); expect(el.text).to.equal('No data yet'); });
it('115. icon overrides preset', async () => { const el = await fixture(html``); expect(el.icon).to.equal('star'); });
it('116. override survives a status change', async () => { const el = await fixture(html``); el.status = 'error'; await el.updateComplete; expect(el.text).to.equal('Mine'); });
// ── tone ──────────────────────────────────────────────────────────────────
it('117. tone defaults neutral without status', async () => { const el = await fixture(html``); expect(el.tone).to.equal('neutral'); });
it('118. tone defaults from status', async () => { const el = await fixture(html``); expect(el.tone).to.equal('warning'); });
it('119. error status is error tone', async () => { const el = await fixture(html``); expect(el.tone).to.equal('error'); });
it('120. tone overridable', async () => { const el = await fixture(html``); expect(el.tone).to.equal('neutral'); });
it('121. tone class on container', async () => { const el = await fixture(html``); expect(container(el).classList.contains('empty-state--tone-error')).to.be.true; });
it('122. neutral tone omits glyph color', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('color')).to.be.null; });
it('123. warning tone sets glyph color', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('color')).to.contain('fg-warning-primary'); });
// ── ARIA ──────────────────────────────────────────────────────────────────
it('124. no live region without status', async () => { const el = await fixture(html``); expect(container(el).getAttribute('role')).to.be.null; expect(container(el).getAttribute('aria-live')).to.be.null; });
it('125. status gets role=status/polite', async () => { const el = await fixture(html``); expect(container(el).getAttribute('role')).to.equal('status'); expect(container(el).getAttribute('aria-live')).to.equal('polite'); });
it('126. error gets role=alert/assertive', async () => { const el = await fixture(html``); expect(container(el).getAttribute('role')).to.equal('alert'); expect(container(el).getAttribute('aria-live')).to.equal('assertive'); });
it('127. offline gets role=alert', async () => { const el = await fixture(html``); expect(container(el).getAttribute('role')).to.equal('alert'); });
it('128. no-access does not interrupt', async () => { const el = await fixture(html``); expect(container(el).getAttribute('aria-live')).to.equal('polite'); });
// ── layout ────────────────────────────────────────────────────────────────
it('129. layout defaults stack', async () => { const el = await fixture(html``); expect(el.layout).to.equal('stack'); expect(container(el).classList.contains('empty-state--stack')).to.be.true; });
it('130. layout=inline class', async () => { const el = await fixture(html``); expect(container(el).classList.contains('empty-state--inline')).to.be.true; });
it('131. layout=page class', async () => { const el = await fixture(html``); expect(container(el).classList.contains('empty-state--page')).to.be.true; });
it('132. layout reflects', async () => { const el = await fixture(html``); expect(el.getAttribute('layout')).to.equal('page'); });
it('133. inline shrinks the glyph to 20', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('size')).to.equal('20'); });
it('134. stack keeps the 40px tonal glyph', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('size')).to.equal('40'); });
it('135. inline drops the 512px ceiling', async () => { const el = await fixture(html``); expect(getComputedStyle(container(el)).maxWidth).to.equal('none'); });
it('136. stack keeps the 512px ceiling', async () => { const el = await fixture(html``); expect(getComputedStyle(container(el)).maxWidth).to.equal('512px'); });
it('137. dynamic layout', async () => { const el = await fixture(html``); el.layout = 'inline'; await el.updateComplete; expect(container(el).classList.contains('empty-state--inline')).to.be.true; });
// ── code ──────────────────────────────────────────────────────────────────
it('138. no code part when unset', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('[part="code"]')).to.be.null; });
it('139. code renders', async () => { const el = await fixture(html``); expect(el.shadowRoot!.querySelector('[part="code"]')!.textContent).to.contain('Error 404'); });
it('140. code sits above the title', async () => { const el = await fixture(html``); const section = el.shadowRoot!.querySelector('[part="text-section"]')!; expect(section.firstElementChild!.getAttribute('part')).to.equal('code'); });
it('141. dynamic code', async () => { const el = await fixture(html``); el.code = '500'; await el.updateComplete; expect(el.shadowRoot!.querySelector('[part="code"]')).to.exist; });
// ── bordered ──────────────────────────────────────────────────────────────
it('147. bordered defaults false', async () => { const el = await fixture(html``); expect(el.bordered).to.be.false; expect(container(el).classList.contains('empty-state--bordered')).to.be.false; });
it('148. bordered class', async () => { const el = await fixture(html``); expect(container(el).classList.contains('empty-state--bordered')).to.be.true; });
// The --ng-* sheet isn't loaded here, so the token has to be supplied or the
// whole `border` shorthand is invalid at computed-value time and drops out.
it('149. bordered draws a dashed edge', async () => { const el = await fixture(html``); expect(getComputedStyle(container(el)).borderTopStyle).to.equal('dashed'); });
it('150. bordered reflects', async () => { const el = await fixture(html``); expect(el.hasAttribute('bordered')).to.be.true; });
// --bordered and --inline both set padding at one-class specificity, so on their
// own the winner would be whichever rule comes last in the stylesheet. The explicit
// .empty-state--inline.empty-state--bordered rule settles it; these pin that intent.
const SPACING = '--ng-spacing-xl:20px;--ng-spacing-lg:16px;--ng-spacing-4xl:96px;--ng-spacing-3xl:64px';
it('150b. bordered stack uses the roomy padding', async () => { const el = await fixture(html``); const cs = getComputedStyle(container(el)); expect(cs.paddingTop).to.equal('96px'); expect(cs.paddingLeft).to.equal('64px'); });
it('150c. bordered inline keeps the tight padding', async () => { const el = await fixture(html``); const cs = getComputedStyle(container(el)); expect(cs.paddingTop).to.equal('20px'); expect(cs.paddingLeft).to.equal('16px'); });
it('150d. bordered does not change inline padding', async () => { const plain = await fixture(html``); const edged = await fixture(html``); expect(getComputedStyle(container(edged)).padding).to.equal(getComputedStyle(container(plain)).padding); });
it('150e. bordered inline still draws the dashed edge', async () => { const el = await fixture(html``); expect(getComputedStyle(container(el)).borderTopStyle).to.equal('dashed'); });
// ── regressions the specimen sheet flagged ────────────────────────────────
it('151. content variant no longer forces white', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('color')).to.not.equal('white'); });
// Was a literal `white`, which punched a white chip into any dark page. Feed
// the theme token a sentinel: if the chip picks it up, it is no longer fixed.
it('152. flat chip background tracks the theme token', async () => { const el = await fixture(html``); const chip = el.shadowRoot!.querySelector('.nile-flat-type-icon')!; expect(getComputedStyle(chip).backgroundColor).to.equal('rgb(1, 2, 3)'); });
it('153. svgIconUrl still wins over a preset icon', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('customsvgpath')).to.exist; expect(glyph(el).getAttribute('name')).to.be.null; });
it('154. svgIconColor beats the tone colour', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('color')).to.equal('#123456'); });
// ── the preset glyphs must actually exist ─────────────────────────────────
// nile-glyph strips a name's dashes, looks it up, and silently renders nothing
// when it misses — so a bad preset name is invisible rather than loud. These
// assert the glyph ACTUALLY DRAWS, which is stronger than checking the name
// against a barrel: it exercises the same resolution path the browser takes,
// and it cannot drift from it.
const STATUSES = ['empty', 'no-results', 'not-found', 'no-access', 'error', 'offline'] as const;
/** The