/** * 표 객체 검증 (조사표 5절 표 · 12절 테이블 셀). * 실행: cd B07_DesignDetail/openwebcad && npx vitest run --config vitest.tmp.config.ts */ import { Point } from '@flatten-js/core'; import { describe, expect, it } from 'vitest'; import { cellAt, cellRect, normalizeCells, tableBorders, type TableCells, } from '../../../B07_DesignDetail/openwebcad/src/helpers/table-geometry'; import { TableEntity } from '../../../B07_DesignDetail/openwebcad/src/entities/TableEntity'; const origin = { x: 0, y: 100 }; const widths = [20, 30, 10]; const heights = [10, 10]; describe('표 기하', () => { it('병합이 없으면 바깥 4 + 안쪽 세로 2×2 + 안쪽 가로 3 = 11개 경계선', () => { const cells = normalizeCells([], 2, 3); expect(tableBorders(origin, widths, heights, cells)).toHaveLength(11); }); it('가로 병합은 그 자리의 세로선을 지운다', () => { const cells: TableCells = normalizeCells( [[{ text: '머리', colSpan: 3 }, null, null], [{ text: 'a' }, { text: 'b' }, { text: 'c' }]], 2, 3 ); const borders = tableBorders(origin, widths, heights, cells); // 첫 행의 세로선 2개가 사라진다 expect(borders).toHaveLength(9); const topRowVerticals = borders.filter( (border) => border.x1 === border.x2 && border.y1 === 100 && border.x1 !== 0 && border.x1 !== 60 ); expect(topRowVerticals).toHaveLength(0); }); it('세로 병합은 그 자리의 가로선을 지우고 셀 사각형이 두 행을 덮는다', () => { const cells = normalizeCells( [ [{ text: '그룹', rowSpan: 2 }, { text: 'b' }, { text: 'c' }], [null, { text: 'e' }, { text: 'f' }], ], 2, 3 ); const borders = tableBorders(origin, widths, heights, cells); const middleHorizontals = borders.filter((border) => border.y1 === 90 && border.y2 === 90); expect(middleHorizontals).toHaveLength(2); // 첫 열은 병합이라 빠진다 const rect = cellRect(origin, widths, heights, 0, 0, cells[0][0]); expect([rect.left, rect.top, rect.right, rect.bottom]).toEqual([0, 100, 20, 80]); }); it('클릭 지점의 칸을 찾고, 병합 자리는 앵커를 돌려준다', () => { const cells = normalizeCells( [ [{ text: '그룹', rowSpan: 2 }, { text: 'b' }, { text: 'c' }], [null, { text: 'e' }, { text: 'f' }], ], 2, 3 ); expect(cellAt(origin, widths, heights, cells, { x: 30, y: 95 })).toEqual({ row: 0, column: 1 }); // 아래쪽 병합 자리를 찍어도 앵커(0,0)가 나온다 expect(cellAt(origin, widths, heights, cells, { x: 10, y: 85 })).toEqual({ row: 0, column: 0 }); expect(cellAt(origin, widths, heights, cells, { x: 200, y: 95 })).toBeNull(); }); it('빈 칸을 채워도 병합은 풀리지 않는다', () => { const normalized = normalizeCells([[{ text: '머리', colSpan: 3 }]], 2, 3); expect(normalized[0][1]).toBeNull(); expect(normalized[0][2]).toBeNull(); expect(normalized[1][0]).toEqual({ text: '' }); }); }); describe('표 객체', () => { const makeTable = () => new TableEntity('layer-1', new Point(0, 100), [...widths], [...heights], [ [{ text: '머리', colSpan: 3 }, null, null], [{ text: 'a' }, { text: 'b', key: 'cut_soil' }, { text: 'c' }], ]); it('경계상자는 열 폭·행 높이의 합이다', () => { const box = makeTable().getBoundingBox(); expect([box.xmin, box.ymin, box.xmax, box.ymax]).toEqual([0, 80, 60, 100]); }); it('JSON 왕복에서 병합·칸 키가 살아남는다', async () => { const json = await makeTable().toJson(); expect(json).toBeTruthy(); if (!json) return; const restored = await TableEntity.fromJson(json as never); expect(restored.getCell(0, 0)?.colSpan).toBe(3); expect(restored.getCell(0, 1)).toBeNull(); expect(restored.getCell(1, 1)?.key).toBe('cut_soil'); const box = restored.getBoundingBox(); expect([box.xmin, box.ymin, box.xmax, box.ymax]).toEqual([0, 80, 60, 100]); }); it('행·열을 넣고 지울 수 있다', () => { const table = makeTable(); table.insertRow(2); expect(table.getRowHeights()).toHaveLength(3); table.insertColumn(0); expect(table.getColumnWidths()).toHaveLength(4); table.deleteColumn(0); table.deleteRow(2); expect(table.getColumnWidths()).toEqual(widths); expect(table.getRowHeights()).toEqual(heights); }); it('열 폭 그립으로 폭만 바뀌고 전체 폭이 따라 늘어난다', () => { const table = makeTable(); table.setColumnWidth(0, 40); const box = table.getBoundingBox(); expect(box.xmax - box.xmin).toBe(80); }); });