diff --git a/frontend/src/App.coverage.test.tsx b/frontend/src/App.coverage.test.tsx index 0b9a20aa8..d5332b4b2 100644 --- a/frontend/src/App.coverage.test.tsx +++ b/frontend/src/App.coverage.test.tsx @@ -610,6 +610,7 @@ describe('App orchestration coverage', () => { it('logs auto-layout failures and preserves nodes added after the undo snapshot', async () => { await renderReadyApp() fireEvent.click(screen.getByRole('button', { name: '다이어그램' })) + await waitFor(() => expect(screen.getAllByRole('button', { name: '열기' }).length).toBeGreaterThan(0)) vi.useFakeTimers() fireEvent.click(screen.getAllByRole('button', { name: '열기' })[0]!) await act(async () => { @@ -641,6 +642,7 @@ describe('App orchestration coverage', () => { .mockRejectedValueOnce(new Error('terminal refresh down')) await renderReadyApp() fireEvent.click(screen.getByRole('button', { name: '다이어그램' })) + await waitFor(() => expect(screen.getAllByRole('button', { name: '열기' }).length).toBeGreaterThan(0)) vi.useFakeTimers() fireEvent.click(screen.getAllByRole('button', { name: '열기' })[0]!) await act(async () => { @@ -744,6 +746,7 @@ describe('App orchestration coverage', () => { })) await renderReadyApp() fireEvent.click(screen.getByRole('button', { name: '다이어그램' })) + await waitFor(() => expect(screen.getAllByRole('button', { name: '열기' }).length).toBeGreaterThan(0)) vi.useFakeTimers() fireEvent.click(screen.getAllByRole('button', { name: '열기' })[0]!) await act(async () => { @@ -784,6 +787,7 @@ describe('App orchestration coverage', () => { }) await renderReadyApp() fireEvent.click(screen.getByRole('button', { name: '다이어그램' })) + await waitFor(() => expect(screen.getAllByRole('button', { name: '열기' }).length).toBeGreaterThan(0)) vi.useFakeTimers() fireEvent.click(screen.getAllByRole('button', { name: '열기' })[0]!) await act(async () => { diff --git a/frontend/src/erd/handleUtils.ts b/frontend/src/erd/handleUtils.ts index 054d5ab2a..4dd087aac 100644 --- a/frontend/src/erd/handleUtils.ts +++ b/frontend/src/erd/handleUtils.ts @@ -1,16 +1,28 @@ +/** + * Encode a persisted column name into the canonical React Flow handle payload. + * The code-point format is a compatibility contract shared by graph rendering + * and exporters, so equivalent refactors must preserve the exact bytes. + */ export function sanitizeHandleId(columnName: string): string { - const encoded = Array.from(columnName, (char) => { - // Array.from only yields non-empty Unicode scalars, so codePointAt(0) is defined. - return char.codePointAt(0)!.toString(16).padStart(4, '0') - }).join('-') + if (!columnName) return 'c-empty'; - return `c-${encoded || 'empty'}` + let encoded = ''; + for (const char of columnName) { + if (encoded.length > 0) { + encoded += '-'; + } + encoded += char.codePointAt(0)!.toString(16).padStart(4, '0'); + } + + return `c-${encoded}`; } +/** Return the canonical source-endpoint handle for a persisted column name. */ export function sourceColumnHandleId(columnName: string): string { return `src-${sanitizeHandleId(columnName)}` } +/** Return the canonical target-endpoint handle for a persisted column name. */ export function targetColumnHandleId(columnName: string): string { return `tgt-${sanitizeHandleId(columnName)}` }