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(`
A1 B1
A2 B2
`); 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(`
A1-B1
A2 B2
`); 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(`
A1-A2 B1
B2
`); 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(`
A1-B1-A2-B2 C1
C2
`); 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(`
A B
C D
`); 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(`
A B
C D
`); 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(`
Wide cell
A B C D E
`); 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(`
A B C
D E
`); 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(`
A
`); 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(`
B
`); 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(`
B
`); 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(`
A B
`); 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(`
D
`); const { grid } = buildTableGridMap(table); const cells = Array.from(table.querySelectorAll("td")); const result = findLastCell(grid); expect(result).toBe(cells[2]); }); });