Files
Aislo/B07_DesignDetail/openwebcad/src/integration/aislo-table-bridge.test.ts
T
eomsangdonandClaude Sonnet 5.5 5638bd5101 feat(M02): 면적 값 고침이 저장 전 도면 패널 표에 바로 비침 · 이름 · 보내기 취소는 [저장] 때 서버가 기록(112-1·112-2)
- 112-1 면적 표 변환을 공용 파일(spreadsheet_area_table.ts)로 빼 서버 Node 와 브라우저가 한 벌로 씀 · 도면 패널이 칸 고침마다(값 바뀐 표만 · 펼친 동안만) CAD 연결 표 갈기를 보기 전용 갈음(preview)으로 보냄
- 112-2 보낸 범위 이름은 도면 [저장] 요청 안에서 서버가 면적 문서에 둠(놓기만 하고 저장 안 하면 안 생김) · [보내기 취소]는 화면에서만 빼고 면적 [저장] 때 서버가 연결표 줄 · 이름을 뺌 · /name · /unlink 길 삭제

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
2026-10-08 17:36:44 +09:00

192 lines
6.7 KiB
TypeScript

import { Point } from '@flatten-js/core';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { Actor } from 'xstate';
import { TableEntity } from '../entities/TableEntity.ts';
import { ActorEvent } from '../tools/tool.types.ts';
import {
getEntities,
isDrawingDirty,
setEntities,
setHostReadOnly,
setLayers,
} from '../state.ts';
import {
getPendingTable,
placeTableToolStateMachine,
setPendingTable,
tableAt,
} from '../tools/annotate/place-table-tool.ts';
import {
AISLO_PLACE_CANCELLED_MESSAGE,
AISLO_PLACE_TABLE_MESSAGE,
AISLO_TABLE_UPDATED_MESSAGE,
AISLO_UPDATE_TABLE_MESSAGE,
handleTableMessage,
updateLinkedTables,
} from './aislo-table-bridge.ts';
const run = vi.hoisted(() => vi.fn());
vi.mock('../commands/run-command.ts', () => ({ runCommandInput: run }));
const layer = { id: 'layer-1', isLocked: false, isVisible: true, name: '작업' };
function table(link?: string): TableEntity {
return new TableEntity(
layer.id,
new Point(10, 20),
[30, 15],
[5, 5],
[
[{ text: '항목', colSpan: 2, key: 'S1!A1' }, null],
[
{ text: '면적', key: 'S1!A2' },
{ text: '12.5', align: 'right', key: 'S1!B2' },
],
],
{ fontSize: 2.5, ...(link ? { 연결: link } : {}) }
);
}
beforeEach(() => {
setLayers([layer]);
setEntities([]);
setPendingTable(null);
run.mockClear();
});
describe('표 놓기(PLACETABLE)', () => {
it('미리보기 · 놓을 표는 커서 자리(왼쪽 위)에 같은 칸 · 크기 · 연결로', () => {
const placed = tableAt(table('T1'), new Point(100, 200), layer.id);
expect([placed.getOrigin().x, placed.getOrigin().y]).toEqual([100, 200]);
expect(placed.getColumnWidths()).toEqual([30, 15]);
expect(placed.getCell(1, 1)?.text).toBe('12.5');
expect(placed.getStyle().연결).toBe('T1');
});
it('place-table 메시지 = 연결을 단 표를 맡기고 PLACETABLE 을 켬', async () => {
const notify = vi.fn();
const handled = await handleTableMessage(
{
type: AISLO_PLACE_TABLE_MESSAGE,
entity: { ...(await table().toJson()), lineColor: '#123456' },
link: 'T7',
},
notify
);
expect(handled).toBe(true);
expect(run).toHaveBeenCalledWith('PLACETABLE');
expect(getPendingTable()?.getStyle().연결).toBe('T7');
expect(getPendingTable()?.getCell(0, 0)?.colSpan).toBe(2);
expect(getPendingTable()?.lineColor).toBe('#123456');
expect(notify).not.toHaveBeenCalled();
});
it('놓지 않고 Esc = place-cancelled 를 한 번 부모에 알리고 맡긴 표를 비움', async () => {
const notify = vi.fn();
await handleTableMessage(
{ type: AISLO_PLACE_TABLE_MESSAGE, entity: await table().toJson(), link: 'T3' },
notify
);
const actor = new Actor(placeTableToolStateMachine);
actor.start();
actor.send({ type: ActorEvent.ESC });
actor.send({ type: ActorEvent.ESC });
expect(notify).toHaveBeenCalledTimes(1);
expect(notify).toHaveBeenCalledWith(AISLO_PLACE_CANCELLED_MESSAGE, {});
expect(getPendingTable()).toBeNull();
});
it('옮기기 · 복사(clone) · JSON 왕복에도 style.연결이 남음', async () => {
const linked = table('T1');
expect(linked.clone().getStyle().연결).toBe('T1');
const json = await linked.toJson();
if (!json) throw new Error('표 JSON 없음');
const back = await TableEntity.fromJson(json);
expect(back.getStyle().연결).toBe('T1');
});
});
describe('선 색 없는 표(서버 표 변환 답 · 43-6)', () => {
it('id · 선 색 · 굵기가 없으면 기본값(자동색 #fff · 1 · 새 id)으로 채워 놓고 저장 JSON 에도 남음', async () => {
const json = await table('T8').toJson();
if (!json) throw new Error('표 JSON 없음');
const { id: _id, lineColor: _color, lineWidth: _width, layerId: _layer, ...bare } = json;
await handleTableMessage(
{ type: AISLO_PLACE_TABLE_MESSAGE, entity: bare as typeof json, link: 'T8' },
vi.fn()
);
const pending = getPendingTable();
expect(pending?.lineColor).toBe('#fff');
expect(pending?.lineWidth).toBe(1);
const placed = tableAt(pending as TableEntity, new Point(0, 0), layer.id);
placed.lineColor = pending?.lineColor ?? '';
const saved = await placed.toJson();
expect(saved?.lineColor).toBe('#fff');
const back = await TableEntity.fromJson(bare as typeof json);
expect(back.id).toBeTruthy();
expect(back.lineColor).toBe('#fff');
});
});
describe('표 칸 갈기(update-table)', () => {
it('연결이 같은 표의 칸만 갈고 자리 · 크기는 그대로 · 되돌리기 한 걸음', () => {
const linked = table('T1');
const other = table('T2');
setEntities([linked, other]);
const count = updateLinkedTables('T1', [
[{ text: '항목(새)', colSpan: 2 }, null],
[{ text: '면적' }, { text: '99.0', align: 'right' }],
]);
expect(count).toBe(1);
expect(linked.getCell(1, 1)?.text).toBe('99.0');
expect(linked.getCell(0, 1)).toBeNull(); // 병합 그대로
expect(other.getCell(1, 1)?.text).toBe('12.5');
expect([linked.getOrigin().x, linked.getColumnWidths()]).toEqual([10, [30, 15]]);
expect(isDrawingDirty()).toBe(true);
});
it('넘치는 칸은 버리고 모자란 칸은 빈 칸', () => {
const linked = table('T1');
setEntities([linked]);
updateLinkedTables('T1', [[{ text: 'a' }, { text: 'b' }, { text: '넘침' }]]);
expect(linked.getCells().map((row) => row.map((cell) => cell?.text ?? null))).toEqual([
['a', 'b'],
['', ''],
]);
});
it('update-table 메시지 = table-updated{link, count} · 없는 연결은 0 · 도면 그대로', async () => {
setEntities([table('T1')]);
const before = getEntities();
const notify = vi.fn();
await handleTableMessage({ type: AISLO_UPDATE_TABLE_MESSAGE, link: 'T9', cells: [] }, notify);
expect(notify).toHaveBeenCalledWith(AISLO_TABLE_UPDATED_MESSAGE, { link: 'T9', count: 0 });
expect(getEntities()).toBe(before);
});
it('보기 전용 도면은 막고(0) · preview 로 온 갈기만 보기 전용에서도 갈음(112-1)', async () => {
const linked = table('T1');
setEntities([linked]);
setHostReadOnly(true);
const cells = [
[{ text: '항목' }, null],
[{ text: '면적' }, { text: '77.0' }],
];
const notify = vi.fn();
await handleTableMessage({ type: AISLO_UPDATE_TABLE_MESSAGE, link: 'T1', cells }, notify);
expect(notify).toHaveBeenLastCalledWith(AISLO_TABLE_UPDATED_MESSAGE, { link: 'T1', count: 0 });
expect(linked.getCell(1, 1)?.text).toBe('12.5');
await handleTableMessage(
{ type: AISLO_UPDATE_TABLE_MESSAGE, link: 'T1', cells, preview: true },
notify
);
expect(notify).toHaveBeenLastCalledWith(AISLO_TABLE_UPDATED_MESSAGE, { link: 'T1', count: 1 });
expect(linked.getCell(1, 1)?.text).toBe('77.0');
setHostReadOnly(false);
});
it('표 메시지가 아니면 false', async () => {
expect(await handleTableMessage({ type: 'aislo:b08:save-request' }, vi.fn())).toBe(false);
});
});