import { afterEach, describe, expect, test } from "vitest";
import {
buildTableGridMap,
findFirstCell,
findFirstCellInRow,
findLastCell,
findLastCellInRow,
findNextFocusableCell,
getNextGridPosition,
isCellFocusable,
} from "./table-grid-nav";
let container: HTMLDivElement;
afterEach(() => {
container?.parentNode && document.body.removeChild(container);
});
function createTable(html: string): HTMLTableElement {
container = document.createElement("div");
container.innerHTML = html;
document.body.appendChild(container);
return container.querySelector("table")!;
}
describe("buildTableGridMap", () => {
test("should build grid for simple 2x2 table without spans", () => {
const table = createTable(`
`);
const { grid, positions } = buildTableGridMap(table);
expect(grid.length).toBe(2);
expect(grid[0].length).toBe(2);
expect(grid[1].length).toBe(2);
const cells = Array.from(table.querySelectorAll("td"));
expect(grid[0][0]).toBe(cells[0]);
expect(grid[0][1]).toBe(cells[1]);
expect(grid[1][0]).toBe(cells[2]);
expect(grid[1][1]).toBe(cells[3]);
expect(positions.get(cells[0])).toEqual({ x: 0, y: 0 });
expect(positions.get(cells[1])).toEqual({ x: 1, y: 0 });
expect(positions.get(cells[2])).toEqual({ x: 0, y: 1 });
expect(positions.get(cells[3])).toEqual({ x: 1, y: 1 });
});
test("should handle colspan correctly", () => {
const table = createTable(`
`);
const { grid, positions } = buildTableGridMap(table);
expect(grid.length).toBe(2);
expect(grid[0].length).toBe(2);
expect(grid[1].length).toBe(2);
const cells = Array.from(table.querySelectorAll("td"));
const cellWithColspan = cells[0];
expect(grid[0][0]).toBe(cellWithColspan);
expect(grid[0][1]).toBe(cellWithColspan);
expect(grid[1][0]).toBe(cells[1]);
expect(grid[1][1]).toBe(cells[2]);
expect(positions.get(cellWithColspan)).toEqual({ x: 0, y: 0 });
expect(positions.get(cells[1])).toEqual({ x: 0, y: 1 });
expect(positions.get(cells[2])).toEqual({ x: 1, y: 1 });
});
test("should handle rowspan correctly", () => {
const table = createTable(`
`);
const { grid, positions } = buildTableGridMap(table);
expect(grid.length).toBe(2);
expect(grid[0].length).toBe(2);
expect(grid[1].length).toBe(2);
const cells = Array.from(table.querySelectorAll("td"));
const cellWithRowspan = cells[0];
expect(grid[0][0]).toBe(cellWithRowspan);
expect(grid[0][1]).toBe(cells[1]);
expect(grid[1][0]).toBe(cellWithRowspan);
expect(grid[1][1]).toBe(cells[2]);
expect(positions.get(cellWithRowspan)).toEqual({ x: 0, y: 0 });
expect(positions.get(cells[1])).toEqual({ x: 1, y: 0 });
expect(positions.get(cells[2])).toEqual({ x: 1, y: 1 });
});
test("should handle both colspan and rowspan", () => {
const table = createTable(`
`);
const { grid, positions } = buildTableGridMap(table);
expect(grid.length).toBe(2);
expect(grid[0].length).toBe(3);
expect(grid[1].length).toBe(3);
const cells = Array.from(table.querySelectorAll("td"));
const spanningCell = cells[0];
expect(grid[0][0]).toBe(spanningCell);
expect(grid[0][1]).toBe(spanningCell);
expect(grid[0][2]).toBe(cells[1]);
expect(grid[1][0]).toBe(spanningCell);
expect(grid[1][1]).toBe(spanningCell);
expect(grid[1][2]).toBe(cells[2]);
expect(positions.get(spanningCell)).toEqual({ x: 0, y: 0 });
expect(positions.get(cells[1])).toEqual({ x: 2, y: 0 });
expect(positions.get(cells[2])).toEqual({ x: 2, y: 1 });
});
test("should handle complex table with multiple spans", () => {
const table = createTable(`
| A1 |
B1-C1 |
D1 |
| A2-A3 |
B2 |
C2 |
D2 |
| B3 |
C3-D3 |
`);
const { grid, positions } = buildTableGridMap(table);
expect(grid.length).toBe(3);
expect(grid[0].length).toBe(4);
expect(grid[1].length).toBe(4);
expect(grid[2].length).toBe(4);
const cells = Array.from(table.querySelectorAll("td"));
expect(grid[0][0]).toBe(cells[0]);
expect(grid[0][1]).toBe(cells[1]);
expect(grid[0][2]).toBe(cells[1]);
expect(grid[0][3]).toBe(cells[2]);
expect(grid[1][0]).toBe(cells[3]);
expect(grid[1][1]).toBe(cells[4]);
expect(grid[1][2]).toBe(cells[5]);
expect(grid[1][3]).toBe(cells[6]);
expect(grid[2][0]).toBe(cells[3]);
expect(grid[2][1]).toBe(cells[7]);
expect(grid[2][2]).toBe(cells[8]);
expect(grid[2][3]).toBe(cells[8]);
expect(positions.get(cells[0])).toEqual({ x: 0, y: 0 });
expect(positions.get(cells[1])).toEqual({ x: 1, y: 0 });
expect(positions.get(cells[2])).toEqual({ x: 3, y: 0 });
expect(positions.get(cells[3])).toEqual({ x: 0, y: 1 });
expect(positions.get(cells[4])).toEqual({ x: 1, y: 1 });
expect(positions.get(cells[5])).toEqual({ x: 2, y: 1 });
expect(positions.get(cells[6])).toEqual({ x: 3, y: 1 });
expect(positions.get(cells[7])).toEqual({ x: 1, y: 2 });
expect(positions.get(cells[8])).toEqual({ x: 2, y: 2 });
});
test("should handle table with thead, tbody, and tfoot", () => {
const table = createTable(`
| Header 1 |
Header 2 |
| Body 1 |
Body 2 |
| Footer 1 |
Footer 2 |
`);
const { grid } = buildTableGridMap(table);
expect(grid.length).toBe(3);
expect(grid[0].length).toBe(2);
expect(grid[1].length).toBe(2);
expect(grid[2].length).toBe(2);
const headerCells = Array.from(table.querySelectorAll("th"));
const bodyCells = Array.from(table.querySelectorAll("tbody td"));
const footerCells = Array.from(table.querySelectorAll("tfoot td"));
expect(grid[0][0]).toBe(headerCells[0]);
expect(grid[0][1]).toBe(headerCells[1]);
expect(grid[1][0]).toBe(bodyCells[0]);
expect(grid[1][1]).toBe(bodyCells[1]);
expect(grid[2][0]).toBe(footerCells[0]);
expect(grid[2][1]).toBe(footerCells[1]);
});
test("should handle empty table", () => {
const table = createTable("");
const { grid, positions } = buildTableGridMap(table);
expect(grid.length).toBe(0);
expect(positions.size).toBe(0);
});
test("should handle table with empty row", () => {
const table = createTable(`
`);
const { grid, positions } = buildTableGridMap(table);
expect(grid.length).toBe(1);
expect(grid[0].length).toBe(0);
expect(positions.size).toBe(0);
});
test("should handle colspan=0 and rowspan=0 as 1", () => {
const table = createTable(`
`);
const { grid } = buildTableGridMap(table);
const cells = Array.from(table.querySelectorAll("td"));
expect(grid[0][0]).toBe(cells[0]);
expect(grid[0][1]).toBe(cells[1]);
expect(grid[1][0]).toBe(cells[2]);
expect(grid[1][1]).toBe(cells[3]);
});
test("should handle negative span values as 1", () => {
const table = createTable(`
`);
const { grid } = buildTableGridMap(table);
const cells = Array.from(table.querySelectorAll("td"));
expect(grid[0][0]).toBe(cells[0]);
expect(grid[0][1]).toBe(cells[1]);
expect(grid[1][0]).toBe(cells[2]);
expect(grid[1][1]).toBe(cells[3]);
});
test("should handle large span values", () => {
const table = createTable(`
`);
const { grid, positions } = buildTableGridMap(table);
const cells = Array.from(table.querySelectorAll("td"));
const wideCell = cells[0];
expect(grid[0].length).toBe(5);
expect(grid[0][0]).toBe(wideCell);
expect(grid[0][1]).toBe(wideCell);
expect(grid[0][2]).toBe(wideCell);
expect(grid[0][3]).toBe(wideCell);
expect(grid[0][4]).toBe(wideCell);
expect(positions.get(wideCell)).toEqual({ x: 0, y: 0 });
});
test("should skip over slots occupied by previous spans", () => {
const table = createTable(`
`);
const { grid, positions } = buildTableGridMap(table);
const cells = Array.from(table.querySelectorAll("td"));
expect(grid[0][0]).toBe(cells[0]);
expect(grid[0][1]).toBe(cells[1]);
expect(grid[0][2]).toBe(cells[2]);
expect(grid[1][0]).toBe(cells[0]);
expect(grid[1][1]).toBe(cells[3]);
expect(grid[1][2]).toBe(cells[4]);
expect(positions.get(cells[3])).toEqual({ x: 1, y: 1 });
expect(positions.get(cells[4])).toEqual({ x: 2, y: 1 });
});
});
describe("getNextGridPosition", () => {
test("should return null when moving out of bounds", () => {
const grid = [
[undefined, undefined],
[undefined, undefined],
];
const down = getNextGridPosition(grid, { x: 0, y: 1 }, { x: 0, y: 1 });
const up = getNextGridPosition(grid, { x: 0, y: 0 }, { x: 0, y: -1 });
const right = getNextGridPosition(grid, { x: 1, y: 0 }, { x: 1, y: 0 });
const left = getNextGridPosition(grid, { x: 0, y: 0 }, { x: -1, y: 0 });
expect(down).toBeNull();
expect(up).toBeNull();
expect(right).toBeNull();
expect(left).toBeNull();
});
test("should handle empty grid", () => {
const grid: (Element | undefined)[][] = [];
const result = getNextGridPosition(grid, { x: 0, y: 0 }, { x: 1, y: 0 });
expect(result).toBeNull();
});
});
describe("isCellFocusable", () => {
test("should return false when cell is undefined", () => {
expect(isCellFocusable(undefined)).toBe(false);
});
test("should return true when cell has focusable elements", () => {
const table = createTable(`
`);
const cells = table.querySelectorAll("td");
expect(isCellFocusable(cells[1])).toBe(true);
});
});
describe("findNextFocusableCell", () => {
test("should find next focusable cell to the right", () => {
const table = createTable(`
`);
const { grid, positions } = buildTableGridMap(table);
const cells = Array.from(table.querySelectorAll("td"));
const currentPos = positions.get(cells[0])!;
const result = findNextFocusableCell(
grid,
currentPos,
{ x: 1, y: 0 },
cells[0],
);
expect(result).toBe(cells[1]);
});
test("should not skip non-focusable cells", () => {
const table = createTable(`
`);
const { grid, positions } = buildTableGridMap(table);
const cells = Array.from(table.querySelectorAll("td"));
const currentPos = positions.get(cells[0])!;
const result = findNextFocusableCell(
grid,
currentPos,
{ x: 1, y: 0 },
cells[0],
);
expect(result).toBe(cells[1]);
});
test("should return null when reaching edge of grid", () => {
const table = createTable(`
`);
const { grid, positions } = buildTableGridMap(table);
const cells = Array.from(table.querySelectorAll("td"));
const currentPos = positions.get(cells[1])!;
const result = findNextFocusableCell(
grid,
currentPos,
{ x: 1, y: 0 },
cells[1],
);
expect(result).toBeNull();
});
test("should find next focusable cell downward", () => {
const table = createTable(`
`);
const { grid, positions } = buildTableGridMap(table);
const cells = Array.from(table.querySelectorAll("td"));
const currentPos = positions.get(cells[0])!;
const result = findNextFocusableCell(
grid,
currentPos,
{ x: 0, y: 1 },
cells[0],
);
expect(result).toBe(cells[1]);
});
});
describe("findFirstCellInRow", () => {
test("should find first focusable cell in row", () => {
const table = createTable(`
`);
const { grid } = buildTableGridMap(table);
const cells = Array.from(table.querySelectorAll("td"));
const result = findFirstCellInRow(grid, 0);
expect(result).toBe(cells[0]);
});
});
describe("findLastCellInRow", () => {
test("should find last focusable cell in row", () => {
const table = createTable(`
`);
const { grid } = buildTableGridMap(table);
const cells = Array.from(table.querySelectorAll("td"));
const result = findLastCellInRow(grid, 0);
expect(result).toBe(cells[2]);
});
});
describe("findFirstCell", () => {
test("should find first focusable cell in table", () => {
const table = createTable(`
`);
const { grid } = buildTableGridMap(table);
const cells = Array.from(table.querySelectorAll("td"));
const result = findFirstCell(grid);
expect(result).toBe(cells[0]);
});
test("should skip non-focusable cells", () => {
const table = createTable(`
`);
const { grid } = buildTableGridMap(table);
const cells = Array.from(table.querySelectorAll("td"));
const result = findFirstCell(grid);
expect(result).toBe(cells[1]);
});
});
describe("findLastCell", () => {
test("should find last focusable cell in table", () => {
const table = createTable(`
`);
const { grid } = buildTableGridMap(table);
const cells = Array.from(table.querySelectorAll("td"));
const result = findLastCell(grid);
expect(result).toBe(cells[3]);
});
test("should skip non-focusable cells", () => {
const table = createTable(`
`);
const { grid } = buildTableGridMap(table);
const cells = Array.from(table.querySelectorAll("td"));
const result = findLastCell(grid);
expect(result).toBe(cells[2]);
});
});