import { expect, fixture, html, elementUpdated } from '@open-wc/testing';
import './nile-pagination';
import type { NilePagination } from './nile-pagination';
describe('NilePagination', () => {
// ═══════════════════════════════════════════════════
// RENDERING & REGISTRATION
// ═══════════════════════════════════════════════════
it('1. should render without errors', async () => {
const el = await fixture(html``);
expect(el).to.exist;
});
it('2. should have a shadow root', async () => {
const el = await fixture(html``);
expect(el.shadowRoot).to.not.be.null;
});
it('3. should be registered as a custom element', async () => {
expect(customElements.get('nile-pagination')).to.exist;
});
it('4. should have correct tag name', async () => {
const el = await fixture(html``);
expect(el.tagName.toLowerCase()).to.equal('nile-pagination');
});
it('5. should have styles defined', async () => {
const { NilePagination } = await import('./nile-pagination');
expect(NilePagination.styles).to.exist;
});
it('6. should have open shadow root mode', async () => {
const el = await fixture(html``);
expect(el.shadowRoot!.mode).to.equal('open');
});
it('7. should be connected when in DOM', async () => {
const el = await fixture(html``);
expect(el.isConnected).to.be.true;
});
it('8. should disconnect when removed', async () => {
const el = await fixture(html``);
el.remove();
expect(el.isConnected).to.be.false;
});
it('9. should render multiple instances', async () => {
const wrapper = await fixture(html`
`);
expect(wrapper.querySelectorAll('nile-pagination').length).to.equal(2);
});
it('10. should be creatable via document.createElement', async () => {
const el = document.createElement('nile-pagination') as NilePagination;
document.body.appendChild(el);
await el.updateComplete;
expect(el.shadowRoot).to.not.be.null;
document.body.removeChild(el);
});
// ═══════════════════════════════════════════════════
// DEFAULT PROPERTY VALUES
// ═══════════════════════════════════════════════════
it('11. should default totalItems to 0', async () => {
const el = await fixture(html``);
expect(el.totalItems).to.equal(0);
});
it('12. should default currentPage to 1', async () => {
const el = await fixture(html``);
expect(el.currentPage).to.equal(1);
});
it('13. should default pageSize to 50', async () => {
const el = await fixture(html``);
expect(el.pageSize).to.equal(50);
});
it('14. should default variant to fluid', async () => {
const el = await fixture(html``);
expect(el.variant).to.equal('fluid');
});
it('15. should default disabled to false', async () => {
const el = await fixture(html``);
expect(el.disabled).to.be.false;
});
it('16. should default showTitle to true', async () => {
const el = await fixture(html``);
expect(el.showTitle).to.be.true;
});
it('17. should default pageSizeOptions to [10, 25, 50, 100]', async () => {
const el = await fixture(html``);
expect(el.pageSizeOptions).to.deep.equal([10, 25, 50, 100]);
});
// ═══════════════════════════════════════════════════
// ATTRIBUTE → PROPERTY CONVERTERS (totalItems)
// ═══════════════════════════════════════════════════
it('18. should parse totalitems attribute as number', async () => {
const el = await fixture(html``);
expect(el.totalItems).to.equal(200);
});
it('19. should floor non-integer totalitems', async () => {
const el = await fixture(html``);
expect(el.totalItems).to.equal(99);
});
it('20. should default totalitems to 0 for negative values', async () => {
const el = await fixture(html``);
expect(el.totalItems).to.equal(0);
});
it('21. should default totalitems to 0 for non-numeric strings', async () => {
const el = await fixture(html``);
expect(el.totalItems).to.equal(0);
});
it('22. should default totalitems to 0 for Infinity', async () => {
const el = await fixture(html``);
expect(el.totalItems).to.equal(0);
});
it('23. should reflect totalitems attribute', async () => {
const el = await fixture(html``);
expect(el.getAttribute('totalitems')).to.equal('150');
});
// ═══════════════════════════════════════════════════
// ATTRIBUTE → PROPERTY CONVERTERS (currentPage)
// ═══════════════════════════════════════════════════
it('24. should parse currentpage attribute as number', async () => {
const el = await fixture(html``);
expect(el.currentPage).to.equal(3);
});
it('25. should floor non-integer currentpage', async () => {
const el = await fixture(html``);
expect(el.currentPage).to.equal(2);
});
it('26. should default currentpage to 1 for values < 1', async () => {
const el = await fixture(html``);
expect(el.currentPage).to.equal(1);
});
it('27. should default currentpage to 1 for negative values', async () => {
const el = await fixture(html``);
expect(el.currentPage).to.equal(1);
});
it('28. should default currentpage to 1 for non-numeric strings', async () => {
const el = await fixture(html``);
expect(el.currentPage).to.equal(1);
});
it('29. should reflect currentpage attribute', async () => {
const el = await fixture(html``);
expect(el.getAttribute('currentpage')).to.equal('5');
});
// ═══════════════════════════════════════════════════
// ATTRIBUTE → PROPERTY CONVERTERS (pageSize)
// ═══════════════════════════════════════════════════
it('30. should parse pagesize attribute as number', async () => {
const el = await fixture(html``);
expect(el.pageSize).to.equal(25);
});
it('31. should floor non-integer pagesize', async () => {
const el = await fixture(html``);
expect(el.pageSize).to.equal(10);
});
it('32. should default pagesize to 50 for values < 1', async () => {
const el = await fixture(html``);
expect(el.pageSize).to.equal(50);
});
it('33. should default pagesize to 50 for negative values', async () => {
const el = await fixture(html``);
expect(el.pageSize).to.equal(50);
});
it('34. should default pagesize to 50 for non-numeric strings', async () => {
const el = await fixture(html``);
expect(el.pageSize).to.equal(50);
});
it('35. should reflect pagesize attribute', async () => {
const el = await fixture(html``);
expect(el.getAttribute('pagesize')).to.equal('10');
});
// ═══════════════════════════════════════════════════
// ATTRIBUTE → PROPERTY CONVERTERS (pageSizeOptions)
// ═══════════════════════════════════════════════════
it('36. should parse pagesizeoptions from JSON array', async () => {
const el = await fixture(html``);
expect(el.pageSizeOptions).to.deep.equal([5, 10, 20]);
});
it('37. should fallback to defaults for invalid pagesizeoptions JSON', async () => {
const el = await fixture(html``);
expect(el.pageSizeOptions).to.deep.equal([10, 25, 50, 100]);
});
it('38. should fallback to defaults for non-number array pagesizeoptions', async () => {
const el = await fixture(html``);
expect(el.pageSizeOptions).to.deep.equal([10, 25, 50, 100]);
});
it('39. should fallback to defaults for non-array pagesizeoptions', async () => {
const el = await fixture(html``);
expect(el.pageSizeOptions).to.deep.equal([10, 25, 50, 100]);
});
// ═══════════════════════════════════════════════════
// TOTAL PAGES CALCULATION
// ═══════════════════════════════════════════════════
it('40. should calculate totalPages as 1 when totalItems is 0', async () => {
const el = await fixture(html``);
await el.updateComplete;
// With 0 items, totalPages should still be at least 1
const nav = el.shadowRoot!.querySelector('nav');
expect(nav).to.exist;
});
it('41. should calculate totalPages correctly (100 items, 10 per page = 10 pages)', async () => {
const el = await fixture(html``);
await el.updateComplete;
// Should render page buttons for 10 pages in fluid variant
const buttons = el.shadowRoot!.querySelectorAll('ul.pagination nile-button');
// 7 visible pages (1..7 since <=7) + prev + next = 9 when totalPages=10, with ellipsis
expect(buttons.length).to.be.greaterThan(2); // At least prev + next
});
it('42. should calculate totalPages correctly with remainder (105 items, 10 per page = 11 pages)', async () => {
const el = await fixture(html``);
await el.updateComplete;
const nav = el.shadowRoot!.querySelector('nav');
expect(nav).to.exist;
});
// ═══════════════════════════════════════════════════
// FLUID VARIANT RENDERING
// ═══════════════════════════════════════════════════
it('43. should render wrapper with fluid class by default', async () => {
const el = await fixture(html``);
const wrapper = el.shadowRoot!.querySelector('.pagination-wrapper');
expect(wrapper).to.exist;
expect(wrapper!.classList.contains('fluid')).to.be.true;
});
it('44. should render nav element in fluid variant', async () => {
const el = await fixture(html``);
const nav = el.shadowRoot!.querySelector('nav');
expect(nav).to.exist;
expect(nav!.getAttribute('aria-label')).to.equal('Pagination');
});
it('45. should render range text in fluid variant with showTitle', async () => {
const el = await fixture(html``);
await el.updateComplete;
const rangeText = el.shadowRoot!.querySelector('.range-text');
expect(rangeText).to.exist;
expect(rangeText!.textContent).to.contain('Showing');
});
it('46. should show correct range text for first page', async () => {
const el = await fixture(html``);
await el.updateComplete;
const rangeText = el.shadowRoot!.querySelector('.range-text');
expect(rangeText!.textContent).to.contain('1');
expect(rangeText!.textContent).to.contain('10');
expect(rangeText!.textContent).to.contain('100');
});
it('47. should show correct range text for middle page', async () => {
const el = await fixture(html``);
await el.updateComplete;
const rangeText = el.shadowRoot!.querySelector('.range-text');
expect(rangeText!.textContent).to.contain('41');
expect(rangeText!.textContent).to.contain('50');
expect(rangeText!.textContent).to.contain('100');
});
it('48. should show correct range text for last page', async () => {
const el = await fixture(html``);
await el.updateComplete;
const rangeText = el.shadowRoot!.querySelector('.range-text');
expect(rangeText!.textContent).to.contain('91');
expect(rangeText!.textContent).to.contain('95');
});
it('49. should show "Showing 0 of 0" when totalItems is 0 in fluid variant', async () => {
const el = await fixture(html``);
await el.updateComplete;
const rangeText = el.shadowRoot!.querySelector('.range-text');
expect(rangeText).to.exist;
expect(rangeText!.textContent).to.contain('0 of 0');
});
it('50. should not render range text in fluid variant when showTitle is false', async () => {
const el = await fixture(html``);
await el.updateComplete;
const rangeText = el.shadowRoot!.querySelector('.range-text');
expect(rangeText).to.be.null;
});
it('51. should render page-size-select in fluid variant', async () => {
const el = await fixture(html``);
const pageSizeSelect = el.shadowRoot!.querySelector('.page-size-select');
expect(pageSizeSelect).to.exist;
});
it('52. should render "Items per page" label in fluid variant', async () => {
const el = await fixture(html``);
const label = el.shadowRoot!.querySelector('.page-size-label');
expect(label).to.exist;
expect(label!.textContent).to.contain('Items per page');
});
it('53. should render page size dropdown showing current pageSize in fluid variant', async () => {
const el = await fixture(html``);
await el.updateComplete;
const trigger = el.shadowRoot!.querySelector('.page-size-dropdown nile-button');
expect(trigger).to.exist;
expect(trigger!.textContent).to.contain('25');
});
it('54. should render page size options as menu items in fluid variant', async () => {
const el = await fixture(html``);
await el.updateComplete;
const menuItems = el.shadowRoot!.querySelectorAll('.page-size-dropdown nile-menu-item');
expect(menuItems.length).to.equal(4); // [10, 25, 50, 100]
});
it('55. should render ul.pagination in fluid variant', async () => {
const el = await fixture(html``);
const ul = el.shadowRoot!.querySelector('ul.pagination');
expect(ul).to.exist;
});
it('56. should render prev button in fluid variant', async () => {
const el = await fixture(html``);
const prevBtn = el.shadowRoot!.querySelector('.prev-button');
expect(prevBtn).to.exist;
});
it('57. should render next button in fluid variant', async () => {
const el = await fixture(html``);
const nextBtn = el.shadowRoot!.querySelector('.next-button');
expect(nextBtn).to.exist;
});
it('58. should render page number buttons in fluid variant for <= 7 pages', async () => {
const el = await fixture(html``);
await el.updateComplete;
// 5 pages, all visible + prev + next
const buttons = el.shadowRoot!.querySelectorAll('ul.pagination > li > nile-button');
expect(buttons.length).to.equal(7); // 5 pages + prev + next
});
it('59. should render ellipsis dropdown for > 7 pages in fluid variant', async () => {
const el = await fixture(html``);
await el.updateComplete;
const ellipsis = el.shadowRoot!.querySelector('.ellipsis-dropdown');
expect(ellipsis).to.exist;
});
it('60. should highlight current page with primary variant button', async () => {
const el = await fixture(html``);
await el.updateComplete;
const pageButtons = el.shadowRoot!.querySelectorAll('ul.pagination > li > nile-button:not(.prev-button):not(.next-button)');
const currentBtn = Array.from(pageButtons).find(btn => btn.textContent!.trim() === '3');
expect(currentBtn).to.exist;
expect(currentBtn!.getAttribute('variant')).to.equal('primary');
});
it('61. should use ghost variant for non-current pages', async () => {
const el = await fixture(html``);
await el.updateComplete;
const pageButtons = el.shadowRoot!.querySelectorAll('ul.pagination > li > nile-button:not(.prev-button):not(.next-button)');
const otherBtn = Array.from(pageButtons).find(btn => btn.textContent!.trim() === '1');
expect(otherBtn).to.exist;
expect(otherBtn!.getAttribute('variant')).to.equal('ghost');
});
// ═══════════════════════════════════════════════════
// COMPACT VARIANT RENDERING
// ═══════════════════════════════════════════════════
it('62. should render wrapper with compact class', async () => {
const el = await fixture(html``);
const wrapper = el.shadowRoot!.querySelector('.pagination-wrapper.compact');
expect(wrapper).to.exist;
});
it('63. should render compact range text without "Showing" prefix', async () => {
const el = await fixture(html``);
await el.updateComplete;
const rangeText = el.shadowRoot!.querySelector('.range-text');
expect(rangeText).to.exist;
// compact trims "Showing " from the start
expect(rangeText!.textContent!.trim().startsWith('Showing')).to.be.false;
});
it('64. should render compact page size dropdown', async () => {
const el = await fixture(html``);
await el.updateComplete;
const dropdown = el.shadowRoot!.querySelector('.compact-dropdown');
expect(dropdown).to.exist;
});
it('65. should render compact pagination with current page dropdown', async () => {
const el = await fixture(html``);
await el.updateComplete;
const compactPagination = el.shadowRoot!.querySelector('.compact-pagination');
expect(compactPagination).to.exist;
});
it('66. should show current page in compact page dropdown trigger', async () => {
const el = await fixture(html``);
await el.updateComplete;
const trigger = el.shadowRoot!.querySelector('.current-page-btn');
expect(trigger).to.exist;
expect(trigger!.textContent).to.contain('3');
});
it('67. should render all page numbers in compact page dropdown', async () => {
const el = await fixture(html``);
await el.updateComplete;
const menuItems = el.shadowRoot!.querySelectorAll('.compact-scroll-wrapper1 nile-menu-item');
expect(menuItems.length).to.equal(5); // 5 total pages
});
it('68. should render prev and next buttons in compact variant', async () => {
const el = await fixture(html``);
await el.updateComplete;
const prevBtn = el.shadowRoot!.querySelector('.prev-button');
const nextBtn = el.shadowRoot!.querySelector('.next-button');
expect(prevBtn).to.exist;
expect(nextBtn).to.exist;
});
it('69. should not render page-size-select in compact variant', async () => {
const el = await fixture(html``);
await el.updateComplete;
const pageSizeSelect = el.shadowRoot!.querySelector('.page-size-select');
expect(pageSizeSelect).to.be.null;
});
it('70. should render compact page size options as menu items', async () => {
const el = await fixture(html``);
await el.updateComplete;
const menuItems = el.shadowRoot!.querySelectorAll('.compact-scroll-wrapper nile-menu-item');
expect(menuItems.length).to.equal(4); // default [10, 25, 50, 100]
});
// ═══════════════════════════════════════════════════
// MINI VARIANT RENDERING
// ═══════════════════════════════════════════════════
it('71. should render mini wrapper with mini class', async () => {
const el = await fixture(html``);
const wrapper = el.shadowRoot!.querySelector('.pagination-wrapper.mini');
expect(wrapper).to.exist;
});
it('72. should render "Showing" label in mini variant', async () => {
const el = await fixture(html``);
await el.updateComplete;
const label = el.shadowRoot!.querySelector('.mini-showing-label');
expect(label).to.exist;
expect(label!.textContent).to.contain('Showing');
});
it('73. should render "of N" label in mini variant with correct total pages', async () => {
const el = await fixture(html``);
await el.updateComplete;
const ofLabel = el.shadowRoot!.querySelector('.mini-of-label');
expect(ofLabel).to.exist;
expect(ofLabel!.textContent).to.contain('of 10');
});
it('74. should render mini page dropdown with current page', async () => {
const el = await fixture(html``);
await el.updateComplete;
const pageBtn = el.shadowRoot!.querySelector('.mini-page-btn');
expect(pageBtn).to.exist;
expect(pageBtn!.textContent).to.contain('3');
});
it('75. should render all page options in mini dropdown', async () => {
const el = await fixture(html``);
await el.updateComplete;
const menuItems = el.shadowRoot!.querySelectorAll('.mini-scroll-wrapper nile-menu-item');
expect(menuItems.length).to.equal(5); // 5 total pages
});
it('76. should render mini prev button', async () => {
const el = await fixture(html``);
await el.updateComplete;
const prevBtn = el.shadowRoot!.querySelector('.mini-prev-button');
expect(prevBtn).to.exist;
});
it('77. should render mini next button', async () => {
const el = await fixture(html``);
await el.updateComplete;
const nextBtn = el.shadowRoot!.querySelector('.mini-next-button');
expect(nextBtn).to.exist;
});
it('78. should render mini-nav container', async () => {
const el = await fixture(html``);
await el.updateComplete;
const nav = el.shadowRoot!.querySelector('.mini-nav');
expect(nav).to.exist;
});
it('79. should render mini-pagination list', async () => {
const el = await fixture(html``);
await el.updateComplete;
const ul = el.shadowRoot!.querySelector('.mini-pagination');
expect(ul).to.exist;
});
it('80. should not render pager-container in mini variant', async () => {
const el = await fixture(html``);
await el.updateComplete;
const pagerContainer = el.shadowRoot!.querySelector('.pager-container');
expect(pagerContainer).to.be.null;
});
it('81. should not render page number buttons in mini variant', async () => {
const el = await fixture(html``);
await el.updateComplete;
// mini only has prev/next, no numbered page buttons in the list
const ellipsis = el.shadowRoot!.querySelector('.ellipsis-dropdown');
expect(ellipsis).to.be.null;
});
// ═══════════════════════════════════════════════════
// NAVIGATION — PREV / NEXT BUTTONS
// ═══════════════════════════════════════════════════
it('82. should disable prev button on first page (fluid)', async () => {
const el = await fixture(html``);
await el.updateComplete;
const prevBtn = el.shadowRoot!.querySelector('.prev-button') as HTMLElement;
expect(prevBtn.hasAttribute('disabled')).to.be.true;
});
it('83. should enable prev button when not on first page (fluid)', async () => {
const el = await fixture(html``);
await el.updateComplete;
const prevBtn = el.shadowRoot!.querySelector('.prev-button') as HTMLElement;
expect(prevBtn.hasAttribute('disabled')).to.be.false;
});
it('84. should disable next button on last page (fluid)', async () => {
const el = await fixture(html``);
await el.updateComplete;
const nextBtn = el.shadowRoot!.querySelector('.next-button') as HTMLElement;
expect(nextBtn.hasAttribute('disabled')).to.be.true;
});
it('85. should enable next button when not on last page (fluid)', async () => {
const el = await fixture(html``);
await el.updateComplete;
const nextBtn = el.shadowRoot!.querySelector('.next-button') as HTMLElement;
expect(nextBtn.hasAttribute('disabled')).to.be.false;
});
it('86. should disable mini prev button on first page', async () => {
const el = await fixture(html``);
await el.updateComplete;
const prevBtn = el.shadowRoot!.querySelector('.mini-prev-button') as HTMLElement;
expect(prevBtn.hasAttribute('disabled')).to.be.true;
});
it('87. should enable mini prev button when not on first page', async () => {
const el = await fixture(html``);
await el.updateComplete;
const prevBtn = el.shadowRoot!.querySelector('.mini-prev-button') as HTMLElement;
expect(prevBtn.hasAttribute('disabled')).to.be.false;
});
it('88. should disable mini next button on last page', async () => {
const el = await fixture(html``);
await el.updateComplete;
const nextBtn = el.shadowRoot!.querySelector('.mini-next-button') as HTMLElement;
expect(nextBtn.hasAttribute('disabled')).to.be.true;
});
it('89. should enable mini next button when not on last page', async () => {
const el = await fixture(html``);
await el.updateComplete;
const nextBtn = el.shadowRoot!.querySelector('.mini-next-button') as HTMLElement;
expect(nextBtn.hasAttribute('disabled')).to.be.false;
});
// ═══════════════════════════════════════════════════
// NAVIGATION — goToPage + nile-change EVENT
// ═══════════════════════════════════════════════════
it('90. should navigate to next page on next button click (fluid)', async () => {
const el = await fixture(html``);
await el.updateComplete;
let detail: any;
el.addEventListener('nile-change', ((e: CustomEvent) => { detail = e.detail; }) as EventListener);
const nextBtn = el.shadowRoot!.querySelector('.next-button') as HTMLElement;
nextBtn.click();
await el.updateComplete;
expect(detail).to.exist;
expect(detail.page).to.equal(2);
expect(detail.previousPage).to.equal(1);
expect(detail.pageSize).to.equal(10);
});
it('91. should navigate to previous page on prev button click (fluid)', async () => {
const el = await fixture(html``);
await el.updateComplete;
let detail: any;
el.addEventListener('nile-change', ((e: CustomEvent) => { detail = e.detail; }) as EventListener);
const prevBtn = el.shadowRoot!.querySelector('.prev-button') as HTMLElement;
prevBtn.click();
await el.updateComplete;
expect(detail).to.exist;
expect(detail.page).to.equal(4);
expect(detail.previousPage).to.equal(5);
});
it('92. should navigate to clicked page number (fluid)', async () => {
const el = await fixture(html``);
await el.updateComplete;
let detail: any;
el.addEventListener('nile-change', ((e: CustomEvent) => { detail = e.detail; }) as EventListener);
// Find page 3 button
const pageButtons = el.shadowRoot!.querySelectorAll('ul.pagination > li > nile-button:not(.prev-button):not(.next-button)');
const page3Btn = Array.from(pageButtons).find(btn => btn.textContent!.trim() === '3') as HTMLElement;
expect(page3Btn).to.exist;
page3Btn!.click();
await el.updateComplete;
expect(detail.page).to.equal(3);
expect(detail.previousPage).to.equal(1);
});
it('93. should update currentPage property after navigation', async () => {
const el = await fixture(html``);
await el.updateComplete;
const nextBtn = el.shadowRoot!.querySelector('.next-button') as HTMLElement;
nextBtn.click();
await el.updateComplete;
expect(el.currentPage).to.equal(2);
});
it('94. should navigate to next page on mini next button click', async () => {
const el = await fixture(html``);
await el.updateComplete;
let detail: any;
el.addEventListener('nile-change', ((e: CustomEvent) => { detail = e.detail; }) as EventListener);
const nextBtn = el.shadowRoot!.querySelector('.mini-next-button') as HTMLElement;
nextBtn.click();
await el.updateComplete;
expect(detail.page).to.equal(2);
expect(detail.previousPage).to.equal(1);
});
it('95. should navigate to prev page on mini prev button click', async () => {
const el = await fixture(html``);
await el.updateComplete;
let detail: any;
el.addEventListener('nile-change', ((e: CustomEvent) => { detail = e.detail; }) as EventListener);
const prevBtn = el.shadowRoot!.querySelector('.mini-prev-button') as HTMLElement;
prevBtn.click();
await el.updateComplete;
expect(detail.page).to.equal(4);
expect(detail.previousPage).to.equal(5);
});
// ═══════════════════════════════════════════════════
// DISABLED STATE
// ═══════════════════════════════════════════════════
it('96. should not navigate when disabled (fluid)', async () => {
const el = await fixture(html``);
await el.updateComplete;
let eventFired = false;
el.addEventListener('nile-change', () => { eventFired = true; });
const nextBtn = el.shadowRoot!.querySelector('.next-button') as HTMLElement;
nextBtn.click();
await el.updateComplete;
expect(eventFired).to.be.false;
expect(el.currentPage).to.equal(5);
});
it('97. should disable all nav buttons when disabled is true (fluid)', async () => {
const el = await fixture(html``);
await el.updateComplete;
const prevBtn = el.shadowRoot!.querySelector('.prev-button') as HTMLElement;
const nextBtn = el.shadowRoot!.querySelector('.next-button') as HTMLElement;
expect(prevBtn.hasAttribute('disabled')).to.be.true;
expect(nextBtn.hasAttribute('disabled')).to.be.true;
});
it('98. should disable all page number buttons when disabled (fluid)', async () => {
const el = await fixture(html``);
await el.updateComplete;
const pageButtons = el.shadowRoot!.querySelectorAll('ul.pagination > li > nile-button');
pageButtons.forEach(btn => {
expect((btn as HTMLElement).hasAttribute('disabled')).to.be.true;
});
});
it('99. should disable page-size dropdown trigger when disabled (fluid)', async () => {
const el = await fixture(html``);
await el.updateComplete;
const dropdown = el.shadowRoot!.querySelector('.page-size-dropdown') as HTMLElement;
expect(dropdown.hasAttribute('disabled')).to.be.true;
});
it('100. should not navigate when disabled (mini)', async () => {
const el = await fixture(html``);
await el.updateComplete;
let eventFired = false;
el.addEventListener('nile-change', () => { eventFired = true; });
const nextBtn = el.shadowRoot!.querySelector('.mini-next-button') as HTMLElement;
nextBtn.click();
await el.updateComplete;
expect(eventFired).to.be.false;
expect(el.currentPage).to.equal(5);
});
it('101. should disable mini nav buttons when disabled', async () => {
const el = await fixture(html``);
await el.updateComplete;
const prevBtn = el.shadowRoot!.querySelector('.mini-prev-button') as HTMLElement;
const nextBtn = el.shadowRoot!.querySelector('.mini-next-button') as HTMLElement;
expect(prevBtn.hasAttribute('disabled')).to.be.true;
expect(nextBtn.hasAttribute('disabled')).to.be.true;
});
it('102. should disable compact dropdown when disabled', async () => {
const el = await fixture(html``);
await el.updateComplete;
const dropdown = el.shadowRoot!.querySelector('.compact-dropdown') as HTMLElement;
expect(dropdown.hasAttribute('disabled')).to.be.true;
});
it('103. should disable mini page dropdown when disabled', async () => {
const el = await fixture(html``);
await el.updateComplete;
const dropdown = el.shadowRoot!.querySelector('.mini-page-dropdown') as HTMLElement;
expect(dropdown.hasAttribute('disabled')).to.be.true;
});
// ═══════════════════════════════════════════════════
// PAGE SIZE CHANGE
// ═══════════════════════════════════════════════════
it('104. should emit nile-change with page=1 on page size change (fluid)', async () => {
const el = await fixture(html``);
await el.updateComplete;
let detail: any;
el.addEventListener('nile-change', ((e: CustomEvent) => { detail = e.detail; }) as EventListener);
const menuItems = el.shadowRoot!.querySelectorAll('.page-size-dropdown nile-menu-item');
// Click the first option (10)
(menuItems[0] as HTMLElement).click();
await el.updateComplete;
expect(detail).to.exist;
expect(detail.page).to.equal(1);
expect(detail.previousPage).to.equal(3);
expect(detail.pageSize).to.equal(10);
});
it('105. should reset to page 1 after page size change', async () => {
const el = await fixture(html``);
await el.updateComplete;
const menuItems = el.shadowRoot!.querySelectorAll('.page-size-dropdown nile-menu-item');
(menuItems[0] as HTMLElement).click();
await el.updateComplete;
expect(el.currentPage).to.equal(1);
expect(el.pageSize).to.equal(10);
});
it('106. should not emit event when selecting same page size', async () => {
const el = await fixture(html``);
await el.updateComplete;
let eventFired = false;
el.addEventListener('nile-change', () => { eventFired = true; });
// 50 is the 3rd option (index 2) in default [10,25,50,100]
const menuItems = el.shadowRoot!.querySelectorAll('.page-size-dropdown nile-menu-item');
(menuItems[2] as HTMLElement).click();
await el.updateComplete;
expect(eventFired).to.be.false;
});
it('107. should not change page size when disabled', async () => {
const el = await fixture(html``);
await el.updateComplete;
let eventFired = false;
el.addEventListener('nile-change', () => { eventFired = true; });
const menuItems = el.shadowRoot!.querySelectorAll('.page-size-dropdown nile-menu-item');
(menuItems[0] as HTMLElement).click();
await el.updateComplete;
expect(eventFired).to.be.false;
expect(el.pageSize).to.equal(50);
});
it('108. should emit nile-change with page=1 on compact page size change', async () => {
const el = await fixture(html``);
await el.updateComplete;
let detail: any;
el.addEventListener('nile-change', ((e: CustomEvent) => { detail = e.detail; }) as EventListener);
const menuItems = el.shadowRoot!.querySelectorAll('.compact-scroll-wrapper nile-menu-item');
(menuItems[0] as HTMLElement).click();
await el.updateComplete;
expect(detail).to.exist;
expect(detail.page).to.equal(1);
expect(detail.pageSize).to.equal(10);
});
// ═══════════════════════════════════════════════════
// DYNAMIC PROPERTY UPDATES
// ═══════════════════════════════════════════════════
it('109. should re-render when totalItems changes', async () => {
const el = await fixture(html``);
await el.updateComplete;
el.totalItems = 200;
await el.updateComplete;
// With 200 items / 10 per page = 20 pages, should show ellipsis
const ellipsis = el.shadowRoot!.querySelector('.ellipsis-dropdown');
expect(ellipsis).to.exist;
});
it('110. should re-render when currentPage changes programmatically', async () => {
const el = await fixture(html``);
await el.updateComplete;
el.currentPage = 3;
await el.updateComplete;
const pageButtons = el.shadowRoot!.querySelectorAll('ul.pagination > li > nile-button:not(.prev-button):not(.next-button)');
const currentBtn = Array.from(pageButtons).find(btn => btn.getAttribute('variant') === 'primary');
expect(currentBtn).to.exist;
expect(currentBtn!.textContent!.trim()).to.equal('3');
});
it('111. should re-render when pageSize changes programmatically', async () => {
const el = await fixture(html``);
await el.updateComplete;
el.pageSize = 25;
await el.updateComplete;
// 100/25 = 4 pages, so all pages visible (<=7)
const pageButtons = el.shadowRoot!.querySelectorAll('ul.pagination > li > nile-button:not(.prev-button):not(.next-button)');
expect(pageButtons.length).to.equal(4);
});
it('112. should re-render when variant changes', async () => {
const el = await fixture(html``);
await el.updateComplete;
expect(el.shadowRoot!.querySelector('.pagination-wrapper.fluid')).to.exist;
el.variant = 'compact';
await el.updateComplete;
expect(el.shadowRoot!.querySelector('.pagination-wrapper.compact')).to.exist;
});
it('113. should switch to mini rendering when variant changes to mini', async () => {
const el = await fixture(html``);
await el.updateComplete;
el.variant = 'mini';
await el.updateComplete;
expect(el.shadowRoot!.querySelector('.pagination-wrapper.mini')).to.exist;
expect(el.shadowRoot!.querySelector('.mini-showing-label')).to.exist;
});
it('114. should update disabled state dynamically', async () => {
const el = await fixture(html``);
await el.updateComplete;
el.disabled = true;
await el.updateComplete;
const prevBtn = el.shadowRoot!.querySelector('.prev-button') as HTMLElement;
expect(prevBtn.hasAttribute('disabled')).to.be.true;
});
// ═══════════════════════════════════════════════════
// EVENT DETAILS
// ═══════════════════════════════════════════════════
it('115. should emit nile-change event that bubbles', async () => {
const el = await fixture(html``);
await el.updateComplete;
let bubbled = false;
document.addEventListener('nile-change', () => { bubbled = true; }, { once: true });
const nextBtn = el.shadowRoot!.querySelector('.next-button') as HTMLElement;
nextBtn.click();
await el.updateComplete;
expect(bubbled).to.be.true;
});
it('116. should include page, previousPage, and pageSize in event detail', async () => {
const el = await fixture(html``);
await el.updateComplete;
let detail: any;
el.addEventListener('nile-change', ((e: CustomEvent) => { detail = e.detail; }) as EventListener);
const nextBtn = el.shadowRoot!.querySelector('.next-button') as HTMLElement;
nextBtn.click();
await el.updateComplete;
expect(detail).to.have.property('page', 3);
expect(detail).to.have.property('previousPage', 2);
expect(detail).to.have.property('pageSize', 25);
});
// ═══════════════════════════════════════════════════
// PAGINATION ITEMS (ELLIPSIS LOGIC) — FLUID
// ═══════════════════════════════════════════════════
it('117. should show all pages without ellipsis when totalPages <= 7', async () => {
const el = await fixture(html``);
await el.updateComplete;
const ellipsis = el.shadowRoot!.querySelector('.ellipsis-dropdown');
expect(ellipsis).to.be.null;
});
it('118. should show ellipsis when totalPages > 7', async () => {
const el = await fixture(html``);
await el.updateComplete;
const ellipsis = el.shadowRoot!.querySelector('.ellipsis-dropdown');
expect(ellipsis).to.exist;
});
it('119. should show right ellipsis when on early pages', async () => {
const el = await fixture(html``);
await el.updateComplete;
const ellipsisDropdowns = el.shadowRoot!.querySelectorAll('.ellipsis-dropdown');
expect(ellipsisDropdowns.length).to.be.greaterThan(0);
});
it('120. should show left ellipsis when on late pages', async () => {
const el = await fixture(html``);
await el.updateComplete;
const ellipsisDropdowns = el.shadowRoot!.querySelectorAll('.ellipsis-dropdown');
expect(ellipsisDropdowns.length).to.be.greaterThan(0);
});
it('121. should show two ellipsis when on middle pages', async () => {
const el = await fixture(html``);
await el.updateComplete;
const ellipsisDropdowns = el.shadowRoot!.querySelectorAll('.ellipsis-dropdown');
expect(ellipsisDropdowns.length).to.equal(2);
});
it('122. should render hidden pages in ellipsis dropdown', async () => {
const el = await fixture(html``);
await el.updateComplete;
const ellipsisMenuItems = el.shadowRoot!.querySelectorAll('.ellipsis-scroll-wrapper nile-menu-item');
expect(ellipsisMenuItems.length).to.be.greaterThan(0);
});
// ═══════════════════════════════════════════════════
// EDGE CASES
// ═══════════════════════════════════════════════════
it('123. should handle single page (totalItems <= pageSize)', async () => {
const el = await fixture(html``);
await el.updateComplete;
const prevBtn = el.shadowRoot!.querySelector('.prev-button') as HTMLElement;
const nextBtn = el.shadowRoot!.querySelector('.next-button') as HTMLElement;
expect(prevBtn.hasAttribute('disabled')).to.be.true;
expect(nextBtn.hasAttribute('disabled')).to.be.true;
});
it('124. should handle totalItems equal to pageSize (exactly one page)', async () => {
const el = await fixture(html``);
await el.updateComplete;
const pageButtons = el.shadowRoot!.querySelectorAll('ul.pagination > li > nile-button:not(.prev-button):not(.next-button)');
expect(pageButtons.length).to.equal(1);
});
it('125. should handle large totalItems', async () => {
const el = await fixture(html``);
await el.updateComplete;
expect(el.shadowRoot!.querySelector('nav')).to.exist;
});
it('126. should handle mini with single page', async () => {
const el = await fixture(html``);
await el.updateComplete;
const ofLabel = el.shadowRoot!.querySelector('.mini-of-label');
expect(ofLabel!.textContent).to.contain('of 1');
const prevBtn = el.shadowRoot!.querySelector('.mini-prev-button') as HTMLElement;
const nextBtn = el.shadowRoot!.querySelector('.mini-next-button') as HTMLElement;
expect(prevBtn.hasAttribute('disabled')).to.be.true;
expect(nextBtn.hasAttribute('disabled')).to.be.true;
});
it('127. should handle compact with single page', async () => {
const el = await fixture(html``);
await el.updateComplete;
const prevBtn = el.shadowRoot!.querySelector('.prev-button') as HTMLElement;
const nextBtn = el.shadowRoot!.querySelector('.next-button') as HTMLElement;
expect(prevBtn.hasAttribute('disabled')).to.be.true;
expect(nextBtn.hasAttribute('disabled')).to.be.true;
});
// ═══════════════════════════════════════════════════
// ACCESSIBILITY & PARTS
// ═══════════════════════════════════════════════════
it('128. should have aria-label="Pagination" on nav in fluid variant', async () => {
const el = await fixture(html``);
const nav = el.shadowRoot!.querySelector('nav');
expect(nav!.getAttribute('aria-label')).to.equal('Pagination');
});
it('129. should have aria-label="Pagination" on nav in compact variant', async () => {
const el = await fixture(html``);
const nav = el.shadowRoot!.querySelector('nav');
expect(nav!.getAttribute('aria-label')).to.equal('Pagination');
});
it('130. should have aria-label="Pagination" on mini nav', async () => {
const el = await fixture(html``);
const nav = el.shadowRoot!.querySelector('nav');
expect(nav!.getAttribute('aria-label')).to.equal('Pagination');
});
it('131. should expose wrapper part', async () => {
const el = await fixture(html``);
const wrapper = el.shadowRoot!.querySelector('[part="wrapper"]');
expect(wrapper).to.exist;
});
it('132. should expose range-text part in fluid variant', async () => {
const el = await fixture(html``);
await el.updateComplete;
const rangeText = el.shadowRoot!.querySelector('[part="range-text"]');
expect(rangeText).to.exist;
});
it('133. should expose pagesize-label part in fluid variant', async () => {
const el = await fixture(html``);
const label = el.shadowRoot!.querySelector('[part="pagesize-label"]');
expect(label).to.exist;
});
it('134. should expose page-list part in fluid variant', async () => {
const el = await fixture(html``);
const pageList = el.shadowRoot!.querySelector('[part="page-list"]');
expect(pageList).to.exist;
});
it('135. should expose mini parts', async () => {
const el = await fixture(html``);
await el.updateComplete;
expect(el.shadowRoot!.querySelector('[part="mini-showing-label"]')).to.exist;
expect(el.shadowRoot!.querySelector('[part="mini-page-dropdown"]')).to.exist;
expect(el.shadowRoot!.querySelector('[part="mini-of-label"]')).to.exist;
expect(el.shadowRoot!.querySelector('[part="mini-nav"]')).to.exist;
expect(el.shadowRoot!.querySelector('[part="mini-page-list"]')).to.exist;
});
// ═══════════════════════════════════════════════════
// NILE-ICON RENDERING
// ═══════════════════════════════════════════════════
it('136. should render nile-icon elements in fluid pagination', async () => {
const el = await fixture(html``);
const icons = el.shadowRoot!.querySelectorAll('nile-icon');
expect(icons.length).to.be.greaterThan(0);
});
it('137. should render chevron icon in page-size dropdown (fluid)', async () => {
const el = await fixture(html``);
const chevron = el.shadowRoot!.querySelector('.page-size-dropdown .chevron');
expect(chevron).to.exist;
});
it('138. should render chevron icon in mini page dropdown', async () => {
const el = await fixture(html``);
await el.updateComplete;
const chevron = el.shadowRoot!.querySelector('.mini-page-dropdown .chevron');
expect(chevron).to.exist;
});
it('139. should render chevron icon in compact page-size dropdown', async () => {
const el = await fixture(html``);
await el.updateComplete;
const chevron = el.shadowRoot!.querySelector('.compact-dropdown .chevron');
expect(chevron).to.exist;
});
it('140. should render chevron icon in compact page dropdown', async () => {
const el = await fixture(html``);
await el.updateComplete;
const chevron = el.shadowRoot!.querySelector('.compact-dropdown1 .chevron');
expect(chevron).to.exist;
});
// ═══════════════════════════════════════════════════
// NEGATIVE DOM CHECKS
// ═══════════════════════════════════════════════════
it('141. should not contain canvas', async () => {
const el = await fixture(html``);
expect(el.shadowRoot!.querySelector('canvas')).to.be.null;
});
it('142. should not contain video', async () => {
const el = await fixture(html``);
expect(el.shadowRoot!.querySelector('video')).to.be.null;
});
it('143. should not contain form', async () => {
const el = await fixture(html``);
expect(el.shadowRoot!.querySelector('form')).to.be.null;
});
it('144. should not contain img', async () => {
const el = await fixture(html``);
expect(el.shadowRoot!.querySelector('img')).to.be.null;
});
it('145. should not contain table', async () => {
const el = await fixture(html``);
expect(el.shadowRoot!.querySelector('table')).to.be.null;
});
it('146. should not contain nile-input', async () => {
const el = await fixture(html``);
expect(el.shadowRoot!.querySelector('nile-input')).to.be.null;
});
it('147. should not contain nile-dialog', async () => {
const el = await fixture(html``);
expect(el.shadowRoot!.querySelector('nile-dialog')).to.be.null;
});
it('148. should not contain nile-checkbox', async () => {
const el = await fixture(html``);
expect(el.shadowRoot!.querySelector('nile-checkbox')).to.be.null;
});
it('149. should not contain nile-select', async () => {
const el = await fixture(html``);
expect(el.shadowRoot!.querySelector('nile-select')).to.be.null;
});
it('150. should not contain anchor tag', async () => {
const el = await fixture(html``);
expect(el.shadowRoot!.querySelector('a')).to.be.null;
});
// ═══════════════════════════════════════════════════
// GENERIC DOM ATTRIBUTES
// ═══════════════════════════════════════════════════
it('151. should support class attribute', async () => {
const el = await fixture(html``);
expect(el.classList.contains('pg')).to.be.true;
});
it('152. should support id attribute', async () => {
const el = await fixture(html``);
expect(el.id).to.equal('pg1');
});
it('153. should support hidden attribute', async () => {
const el = await fixture(html``);
expect(el.hidden).to.be.true;
});
it('154. should support data attributes', async () => {
const el = await fixture(html``);
expect(el.dataset.test).to.equal('val');
});
it('155. should support aria-label attribute', async () => {
const el = await fixture(html``);
expect(el.getAttribute('aria-label')).to.equal('Page nav');
});
it('156. should support dir attribute', async () => {
const el = await fixture(html``);
expect(el.dir).to.equal('rtl');
});
it('157. should support style attribute', async () => {
const el = await fixture(html``);
expect(el.style.color).to.equal('red');
});
it('158. should support role attribute', async () => {
const el = await fixture(html``);
expect(el.getAttribute('role')).to.equal('navigation');
});
// ═══════════════════════════════════════════════════
// CUSTOM PAGE SIZE OPTIONS
// ═══════════════════════════════════════════════════
it('159. should render custom page size options (fluid)', async () => {
const el = await fixture(html``);
await el.updateComplete;
const menuItems = el.shadowRoot!.querySelectorAll('.page-size-dropdown nile-menu-item');
expect(menuItems.length).to.equal(3);
});
it('160. should render custom page size options (compact)', async () => {
const el = await fixture(html``);
await el.updateComplete;
const menuItems = el.shadowRoot!.querySelectorAll('.compact-scroll-wrapper nile-menu-item');
expect(menuItems.length).to.equal(3);
});
// ═══════════════════════════════════════════════════
// RE-RENDER & LIFECYCLE
// ═══════════════════════════════════════════════════
it('161. should handle multiple rapid requestUpdate calls', async () => {
const el = await fixture(html``);
for (let i = 0; i < 5; i++) {
el.requestUpdate();
await el.updateComplete;
}
expect(el.shadowRoot).to.not.be.null;
});
it('162. should have render method', async () => {
const el = await fixture(html``);
expect(el.render).to.be.a('function');
});
it('163. should resolve updateComplete', async () => {
const el = await fixture(html``);
const result = await el.updateComplete;
expect(result).to.not.be.undefined;
});
// ═══════════════════════════════════════════════════
// COMPACT VARIANT — PAGE NAVIGATION VIA DROPDOWN
// ═══════════════════════════════════════════════════
it('164. should navigate to selected page in compact dropdown', async () => {
const el = await fixture(html``);
await el.updateComplete;
let detail: any;
el.addEventListener('nile-change', ((e: CustomEvent) => { detail = e.detail; }) as EventListener);
const menuItems = el.shadowRoot!.querySelectorAll('.compact-scroll-wrapper1 nile-menu-item');
// Click page 3 (index 2)
(menuItems[2] as HTMLElement).click();
await el.updateComplete;
expect(detail).to.exist;
expect(detail.page).to.equal(3);
expect(detail.previousPage).to.equal(1);
});
// ═══════════════════════════════════════════════════
// MINI VARIANT — PAGE NAVIGATION VIA DROPDOWN
// ═══════════════════════════════════════════════════
it('165. should navigate to selected page in mini dropdown', async () => {
const el = await fixture(html``);
await el.updateComplete;
let detail: any;
el.addEventListener('nile-change', ((e: CustomEvent) => { detail = e.detail; }) as EventListener);
const menuItems = el.shadowRoot!.querySelectorAll('.mini-scroll-wrapper nile-menu-item');
// Click page 4 (index 3)
(menuItems[3] as HTMLElement).click();
await el.updateComplete;
expect(detail).to.exist;
expect(detail.page).to.equal(4);
expect(detail.previousPage).to.equal(1);
});
// ═══════════════════════════════════════════════════
// NILE-BUTTON & NILE-MENU PRESENCE
// ═══════════════════════════════════════════════════
it('166. should have nile-button in shadow root', async () => {
const el = await fixture(html``);
expect(el.shadowRoot!.querySelector('nile-button')).to.exist;
});
it('167. should have nile-menu in shadow root', async () => {
const el = await fixture(html``);
expect(el.shadowRoot!.querySelector('nile-menu')).to.exist;
});
it('168. should have nile-dropdown in shadow root', async () => {
const el = await fixture(html``);
expect(el.shadowRoot!.querySelector('nile-dropdown')).to.exist;
});
it('169. should have nile-menu-item in shadow root', async () => {
const el = await fixture(html``);
expect(el.shadowRoot!.querySelector('nile-menu-item')).to.exist;
});
it('170. should have nile-icon in shadow root', async () => {
const el = await fixture(html``);
expect(el.shadowRoot!.querySelector('nile-icon')).to.exist;
});
// ═══════════════════════════════════════════════════
// FULL INTEGRATION
// ═══════════════════════════════════════════════════
it('171. full integration — fluid variant with all props', async () => {
const el = await fixture(html`
`);
await el.updateComplete;
expect(el.totalItems).to.equal(200);
expect(el.pageSize).to.equal(25);
expect(el.currentPage).to.equal(3);
expect(el.variant).to.equal('fluid');
expect(el.id).to.equal('pg-fluid');
expect(el.shadowRoot!.querySelector('.pagination-wrapper.fluid')).to.exist;
expect(el.shadowRoot!.querySelector('nav')).to.exist;
expect(el.shadowRoot!.querySelector('.range-text')).to.exist;
expect(el.shadowRoot!.querySelector('.page-size-select')).to.exist;
});
it('172. full integration — compact variant with all props', async () => {
const el = await fixture(html`
`);
await el.updateComplete;
expect(el.totalItems).to.equal(100);
expect(el.pageSize).to.equal(10);
expect(el.currentPage).to.equal(5);
expect(el.variant).to.equal('compact');
expect(el.shadowRoot!.querySelector('.pagination-wrapper.compact')).to.exist;
expect(el.shadowRoot!.querySelector('.compact-pagination')).to.exist;
expect(el.shadowRoot!.querySelector('.current-page-btn')).to.exist;
});
it('173. full integration — mini variant with all props', async () => {
const el = await fixture(html`
`);
await el.updateComplete;
expect(el.totalItems).to.equal(100);
expect(el.pageSize).to.equal(10);
expect(el.currentPage).to.equal(5);
expect(el.variant).to.equal('mini');
expect(el.shadowRoot!.querySelector('.pagination-wrapper.mini')).to.exist;
expect(el.shadowRoot!.querySelector('.mini-showing-label')).to.exist;
expect(el.shadowRoot!.querySelector('.mini-of-label')).to.exist;
expect(el.shadowRoot!.querySelector('.mini-page-btn')).to.exist;
expect(el.shadowRoot!.querySelector('.mini-prev-button')).to.exist;
expect(el.shadowRoot!.querySelector('.mini-next-button')).to.exist;
});
});