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 nile-glyph drew, once it has resolved the name. */ const drawnSvg = async (el: NileEmptyState): Promise => { const g = glyph(el); await waitUntil( () => !!g.shadowRoot?.querySelector('svg'), `glyph "${el.icon}" never drew — the name does not resolve in @aquera/nile-glyph`, ); return g.shadowRoot!.querySelector('svg')!; }; STATUSES.forEach((status, i) => { it(`${155 + i}. status=${status} names a glyph that actually draws`, async () => { const el = await fixture(html``); expect(await drawnSvg(el)).to.exist; }); }); // nile-glyph resolves the var expression and reflects the resolved value back, // so these assert the paint method that actually reaches the SVG. Every glyph the // component draws is stroked now — preset or not, either theme. it('161. preset glyphs are stroked, not filled', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('method')).to.equal('stroke'); }); it('162. preset glyph artwork is an outline', async () => { const el = await fixture(html``); expect((await drawnSvg(el)).getAttribute('fill')).to.equal('none'); }); it('163. a call site with no status is stroked too', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('method')).to.equal('stroke'); }); it('163b. content variant strokes a preset glyph', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('method')).to.equal('stroke'); }); it('163c. content variant strokes a caller icon as well', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('method')).to.equal('stroke'); }); it('164. overriding the preset icon does not change the paint method', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('method')).to.equal('stroke'); }); // ── nile-glyph + ::part(icon-chrome) ────────────────────────────────────── // // The component draws through nile-glyph, not nile-icon: nile-icon resolves // against a vendored subset and renders nothing when a name misses it, which // is what held the presets on near-miss names. const SHAPES: ReadonlyArray = [ ['variant', 'tonal'], ['variant', 'flat'], ['variant', 'content'], ['layout', 'inline'], ]; const partsOf = (el: NileEmptyState, sel: string) => (el.shadowRoot!.querySelector(sel)!.getAttribute('part') ?? '').split(/\s+/); it('166. never renders a nile-icon in any shape', async () => { for (const [name, value] of SHAPES) { const el = await fixture(html``); el.setAttribute(name, value); await el.updateComplete; expect(el.shadowRoot!.querySelector('nile-icon'), `${name}="${value}"`).to.be.null; } }); it('167. renders exactly one glyph per shape', async () => { for (const [name, value] of SHAPES) { const el = await fixture(html``); el.setAttribute(name, value); await el.updateComplete; expect(el.shadowRoot!.querySelectorAll('[part="body"] nile-glyph').length, `${name}="${value}"`).to.equal(1); } }); it('169. every shape exposes exactly one icon-chrome element', async () => { for (const [name, value] of SHAPES) { const el = await fixture(html``); el.setAttribute(name, value); await el.updateComplete; expect(el.shadowRoot!.querySelectorAll('[part~="icon-chrome"]').length, `${name}="${value}"`).to.equal(1); } }); it('170. tonal disc carries icon-chrome alongside tonal-body', async () => { const el = await fixture(html``); const p = partsOf(el, '.empty-state__body--tonal'); expect(p).to.include('icon-chrome'); expect(p).to.include('tonal-body'); }); it('171. flat tile carries icon-chrome, its wrapper does not', async () => { const el = await fixture(html``); expect(partsOf(el, '.nile-flat-type-icon')).to.include('icon-chrome'); expect(partsOf(el, '.empty-state__body--flat')).to.not.include('icon-chrome'); }); it('172. content icon box carries icon-chrome alongside icon-wrapper', async () => { const el = await fixture(html``); const p = partsOf(el, '.empty-state__img__icon'); expect(p).to.include('icon-chrome'); expect(p).to.include('icon-wrapper'); }); it('173. inline exposes a bare glyph box, no variant chrome', async () => { for (const v of ['tonal', 'flat', 'content'] as const) { const el = await fixture(html``); const p = partsOf(el, '.empty-state__body--inline'); expect(p, v).to.include('inline-icon'); expect(p, v).to.include('icon-chrome'); expect(el.shadowRoot!.querySelector('.empty-state__body--tonal'), v).to.be.null; expect(el.shadowRoot!.querySelector('.nile-flat-type-icon'), v).to.be.null; expect(el.shadowRoot!.querySelector('.empty-state__img__icon'), v).to.be.null; } }); it('174. inline still honours the status preset', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('name')).to.equal('ng-search-lg'); expect(container(el).getAttribute('role')).to.equal('status'); }); it('175. the inert nile-icon variant attribute is gone from the flat tile', async () => { const el = await fixture(html``); expect(glyph(el).hasAttribute('variant')).to.be.false; }); // ── svgIconColor beats the tone, so chrome and glyph can differ ─────────── it('176. svgIconColor colours a catalogue glyph', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('color')).to.equal('#6941C6'); }); it('177. svgIconColor beats the tone colour', async () => { const el = await fixture(html``); expect(el.tone).to.equal('error'); expect(glyph(el).getAttribute('color')).to.equal('#6941C6'); }); it('178. without svgIconColor the tone still decides', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('color')).to.contain('--ng-colors-fg-error-primary'); }); it('179. svgIconColor applies in every shape', async () => { for (const [name, value] of SHAPES) { const el = await fixture(html``); el.setAttribute(name, value); await el.updateComplete; expect(glyph(el).getAttribute('color'), `${name}="${value}"`).to.equal('#6941C6'); } }); // The resolving accessors must report a resolved oldValue: Lit reads the new value // back through the getter, so capturing the raw backing field would compare an // undefined against a resolved value. it('180. assigning the already-resolved value schedules nothing', async () => { const el = await fixture(html``); el.icon = el.icon; expect((el as any).isUpdatePending).to.be.false; }); it('181. same for text and tone', async () => { const el = await fixture(html``); el.text = el.text; el.tone = el.tone; expect((el as any).isUpdatePending).to.be.false; }); it('182. a real change still schedules', async () => { const el = await fixture(html``); el.icon = 'star'; expect((el as any).isUpdatePending).to.be.true; await el.updateComplete; expect(glyph(el).getAttribute('name')).to.equal('star'); }); it('183. changedProperties reports the resolved old value', async () => { const el = await fixture(html``); let seen: unknown = 'unset'; (el as any).willUpdate = (c: Map) => { if (c.has('icon')) seen = c.get('icon'); }; el.icon = 'star'; await el.updateComplete; expect(seen).to.equal('ng-database'); }); it('184. removing the attribute falls back to the preset', async () => { const el = await fixture(html``); el.removeAttribute('icon'); await el.updateComplete; expect(el.icon).to.equal('ng-database'); }); // svgIconSize overrides the layout-driven size rather than replacing it, so a // custom SVG tracks the shape it is drawn in unless the caller says otherwise. it('185. custom SVG follows the inline size', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('size')).to.equal('20'); }); it('186. custom SVG follows the tonal size', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('size')).to.equal('40'); }); it('187. custom SVG follows the flat size', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('size')).to.equal('24'); }); it('188. custom SVG follows the flat lg size', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('size')).to.equal('28'); }); it('189. custom SVG follows the content size', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('size')).to.equal('24'); }); it('190. svgIconSize overrides the layout size', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('size')).to.equal('60'); }); it('191. svgIconSize does not affect a catalogue glyph', async () => { const el = await fixture(html``); expect(glyph(el).getAttribute('size')).to.equal('20'); }); it('165. full new-API integration', async () => { const el = await fixture(html``); expect(el.text).to.contain(`couldn't find that`); expect(el.tone).to.equal('warning'); expect(container(el).getAttribute('role')).to.equal('status'); expect(container(el).classList.contains('empty-state--page')).to.be.true; expect(container(el).classList.contains('empty-state--bordered')).to.be.true; expect(el.shadowRoot!.querySelector('[part="code"]')!.textContent).to.contain('Error 404'); expect(el.querySelector('button')).to.exist; }); });