/**
* multi-row-headers.test.ts
* Tests for datatable column count with multi-row (grouped) headers.
*
* Spec: openspec/changes/fix-datatable-multi-row-header-column-count
* Requirement: Multi-Row (Grouped) Header Column Count
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { KTDataTable } from '../datatable';
describe('KTDataTable - Multi-row header column count', () => {
let container: HTMLElement;
let tableElement: HTMLTableElement;
/**
* Create a table with multi-row thead (no data-kt-datatable-column) and 17 data columns.
* Row 1: Person (rowspan=2), Backlog (colspan=3), Floater2 (colspan=3), Floater1 (colspan=3), CL2025 (colspan=3), LWP (colspan=3), Action (rowspan=2) = 7 th
* Row 2: 15 leaf th (Assigned, Used, Balance × 5)
* Total 22 th in DOM but only 17 data columns.
*/
const createMultiRowHeaderTable = (bodyRowCount: number = 2) => {
container = document.createElement('div');
container.id = 'kt_datatable_multirow';
container.setAttribute('data-kt-datatable', 'true');
tableElement = document.createElement('table');
tableElement.setAttribute('data-kt-datatable-table', 'true');
const thead = document.createElement('thead');
const row1 = document.createElement('tr');
row1.innerHTML = `
Person |
Backlog |
Floater (2) 2025 |
Floater (1) 2025 |
CL2025 |
LWP |
Action |
`;
thead.appendChild(row1);
const row2 = document.createElement('tr');
row2.innerHTML = `
Assigned | Used | Balance |
Assigned | Used | Balance |
Assigned | Used | Balance |
Assigned | Used | Balance |
Assigned | Used | Balance |
`;
thead.appendChild(row2);
tableElement.appendChild(thead);
const tbody = document.createElement('tbody');
const tdCount = 17;
for (let r = 0; r < bodyRowCount; r++) {
const tr = document.createElement('tr');
for (let c = 0; c < tdCount; c++) {
const td = document.createElement('td');
td.textContent = c === 0 ? `Person ${r + 1}` : String(c);
tr.appendChild(td);
}
tbody.appendChild(tr);
}
tableElement.appendChild(tbody);
const wrapper = document.createElement('div');
wrapper.appendChild(tableElement);
const infoElement = document.createElement('span');
infoElement.setAttribute('data-kt-datatable-info', 'true');
const sizeElement = document.createElement('select');
sizeElement.setAttribute('data-kt-datatable-size', 'true');
const paginationElement = document.createElement('div');
paginationElement.setAttribute('data-kt-datatable-pagination', 'true');
container.appendChild(wrapper);
container.appendChild(infoElement);
container.appendChild(sizeElement);
container.appendChild(paginationElement);
document.body.appendChild(container);
return { container, tableElement, tbody };
};
beforeEach(() => {
vi.useFakeTimers();
});
it('should render exactly 17 columns when thead has multi-row headers and no data-kt-datatable-column', async () => {
createMultiRowHeaderTable(2);
new KTDataTable(container, { stateSave: false });
await vi.runAllTimersAsync();
const tbody = tableElement.tBodies[0];
expect(tbody).toBeDefined();
const rows = tbody.querySelectorAll('tr');
// Two data rows
expect(rows.length).toBe(2);
rows.forEach((row) => {
const cells = row.querySelectorAll('td');
expect(cells.length).toBe(17);
});
});
it('should use logical column count for empty-state row colspan', async () => {
createMultiRowHeaderTable(0);
const tbody = tableElement.querySelector('tbody');
expect(tbody).toBeDefined();
new KTDataTable(container, { stateSave: false });
await vi.runAllTimersAsync();
const noticeRow = tableElement.tBodies[0].querySelector('tr');
expect(noticeRow).toBeDefined();
const cell = noticeRow?.querySelector('td');
expect(cell).toBeDefined();
// Should span 17 (logical columns from originalData) or 1 if no data; after extract we have 0 rows so logicalCount could be 0 -> we use 1
// With 0 body rows we never have originalData, so _getLogicalColumnCount() returns first tbody row td count (0) or 0; we set colspan to 1
expect(cell!.colSpan).toBeGreaterThanOrEqual(1);
});
});