import { beforeEach, describe, expect, it, vi } from 'vitest';
import { KTDataTable } from '../datatable';
type DataRow = {
id: string;
name: string;
status: string;
};
const rows: DataRow[] = [
{ id: '1', name: 'Alpha', status: 'Active' },
{ id: '2', name: 'Beta', status: 'Pending' },
{ id: '3', name: 'Gamma', status: 'Disabled' },
];
const createDatatableFixture = () => {
const wrapper = document.createElement('div');
wrapper.className = 'kt-table-wrapper';
const container = document.createElement('div');
container.id = 'kt_datatable_locked_layout';
container.setAttribute('data-kt-datatable', 'true');
const table = document.createElement('table');
table.setAttribute('data-kt-datatable-table', 'true');
const thead = document.createElement('thead');
thead.innerHTML = `
| ID |
Name |
Status |
`;
table.appendChild(thead);
const tbody = document.createElement('tbody');
rows.forEach((row) => {
const tr = document.createElement('tr');
tr.innerHTML = `${row.id} | ${row.name} | ${row.status} | `;
tbody.appendChild(tr);
});
table.appendChild(tbody);
wrapper.appendChild(table);
container.appendChild(wrapper);
const info = document.createElement('span');
info.setAttribute('data-kt-datatable-info', 'true');
const size = document.createElement('select');
size.setAttribute('data-kt-datatable-size', 'true');
const pagination = document.createElement('div');
pagination.setAttribute('data-kt-datatable-pagination', 'true');
container.appendChild(info);
container.appendChild(size);
container.appendChild(pagination);
document.body.appendChild(container);
const sizedCells = table.querySelectorAll('th, td');
sizedCells.forEach((cell) => {
Object.defineProperty(cell, 'getBoundingClientRect', {
value: () =>
({
width: 120,
height: 40,
top: 0,
left: 0,
right: 0,
bottom: 0,
x: 0,
y: 0,
toJSON: () => ({}),
}) as DOMRect,
});
});
return { container, table };
};
describe('KTDataTable locked layout plugin', () => {
beforeEach(() => {
vi.useFakeTimers();
});
it('applies locked header, rows and columns in local mode and keeps them after sorting', async () => {
const { container, table } = createDatatableFixture();
const datatable = new KTDataTable(container, {
stateSave: false,
columns: {
id: { title: 'ID' },
name: { title: 'Name' },
status: { title: 'Status' },
},
lockedLayout: {
stickyHeader: true,
stickyRows: { top: 1, bottom: 1 },
stickyColumns: { left: ['id'], right: ['status'] },
},
});
await vi.runAllTimersAsync();
expect(
table.querySelectorAll('.kt-datatable-locked-header').length,
).toBeGreaterThan(0);
expect(
table.querySelectorAll('.kt-datatable-locked-top-row').length,
).toBeGreaterThan(0);
expect(
table.querySelectorAll('.kt-datatable-locked-bottom-row').length,
).toBeGreaterThan(0);
expect(
table.querySelectorAll('.kt-datatable-locked-left').length,
).toBeGreaterThan(0);
expect(
table.querySelectorAll('.kt-datatable-locked-right').length,
).toBeGreaterThan(0);
datatable.sort('name');
await vi.runAllTimersAsync();
expect(
table.querySelectorAll('.kt-datatable-locked-cell').length,
).toBeGreaterThan(0);
});
it('reapplies locked layout after remote fetch redraw', async () => {
const { container, table } = createDatatableFixture();
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response(JSON.stringify({ data: rows, totalCount: rows.length }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
);
new KTDataTable(container, {
stateSave: false,
apiEndpoint: 'https://example.test/datatable',
requestMethod: 'GET',
mapResponse: (data) => data,
lockedLayout: {
stickyHeader: true,
stickyColumns: { left: ['id'] },
},
});
await vi.runAllTimersAsync();
expect(fetchMock).toHaveBeenCalled();
expect(
table.querySelectorAll('.kt-datatable-locked-header').length,
).toBeGreaterThan(0);
expect(
table.querySelectorAll('.kt-datatable-locked-left').length,
).toBeGreaterThan(0);
});
it('keeps pagination state when using locked columns in local mode', async () => {
const { container } = createDatatableFixture();
const datatable = new KTDataTable(container, {
stateSave: false,
pageSize: 2,
columns: {
id: { title: 'ID' },
name: { title: 'Name' },
status: { title: 'Status' },
},
lockedLayout: {
stickyHeader: true,
stickyColumns: { left: ['id'] },
},
});
await vi.runAllTimersAsync();
expect(datatable.getState().totalPages).toBe(2);
datatable.goPage(2);
await vi.runAllTimersAsync();
expect(datatable.getState().page).toBe(2);
expect(datatable.getState().totalPages).toBe(2);
expect(datatable.getState().totalItems).toBe(rows.length);
});
it('uses collapsed table borders for row-only locked layout', async () => {
const { container, table } = createDatatableFixture();
new KTDataTable(container, {
stateSave: false,
lockedLayout: {
stickyHeader: true,
stickyRows: { top: 1, bottom: 1 },
},
});
await vi.runAllTimersAsync();
expect(table.classList.contains('kt-datatable-locked-layout')).toBe(true);
expect(
table.classList.contains('kt-datatable-locked-layout-separate'),
).toBe(false);
expect(table.style.borderCollapse).not.toBe('separate');
expect(
table.tHead?.classList.contains('kt-datatable-locked-header-section'),
).toBe(true);
const stickyHeaderCell = table.querySelector(
'th.kt-datatable-locked-header',
) as HTMLTableCellElement | null;
expect(stickyHeaderCell?.style.position).not.toBe('sticky');
});
it('uses separate table borders when sticky columns are enabled', async () => {
const { container, table } = createDatatableFixture();
new KTDataTable(container, {
stateSave: false,
lockedLayout: {
stickyHeader: true,
stickyColumns: { left: ['id'] },
},
});
await vi.runAllTimersAsync();
expect(
table.classList.contains('kt-datatable-locked-layout-separate'),
).toBe(true);
expect(table.style.borderCollapse).toBe('separate');
});
it('does not set inline background color for locked header columns', async () => {
const { container, table } = createDatatableFixture();
new KTDataTable(container, {
stateSave: false,
columns: {
id: { title: 'ID' },
name: { title: 'Name' },
status: { title: 'Status' },
},
lockedLayout: {
stickyHeader: true,
stickyColumns: { left: ['id'] },
},
});
await vi.runAllTimersAsync();
const lockedHeaderCell = table.querySelector(
'th.kt-datatable-locked-left',
) as HTMLTableCellElement | null;
expect(lockedHeaderCell).not.toBeNull();
expect(lockedHeaderCell?.style.backgroundColor).toBe('');
});
});