diff --git a/schema/schema.graphql b/schema/schema.graphql index e129e94..342fd26 100644 --- a/schema/schema.graphql +++ b/schema/schema.graphql @@ -1,4 +1,4 @@ -# 생성 파일 — 수정하지 않는다. caquick-be local@9c15e94d1458101007ed17b4462ec131daa6a4dc 의 src/features/**/*.graphql 64개를 경로순으로 합쳤다. +# 생성 파일 — 수정하지 않는다. caquick-be local@234063bf62d2bf40170c1b3bae454081977ce981 의 src/features/**/*.graphql 64개를 경로순으로 합쳤다. # 갱신: pnpm schema:pull [ref] # ---- src/features/audit-log/audit-log.types.graphql ---- diff --git a/src/features/banners/pages/banners.spec.tsx b/src/features/banners/pages/banners.spec.tsx index 85be30e..75e8cac 100644 --- a/src/features/banners/pages/banners.spec.tsx +++ b/src/features/banners/pages/banners.spec.tsx @@ -115,7 +115,7 @@ describe('배너', () => { await userEvent.click(screen.getByRole('button', { name: '가을 한정 삭제' })); await userEvent.click(await screen.findByRole('button', { name: '삭제' })); await vi.waitFor(() => expect(deleted).toEqual({ bannerId: '5' })); - }, 15_000); // 앱 부팅부터 여러 단계를 한 번에 도는 흐름이라 기본 5초가 빠듯하다 + }); it('목록을 켜 둔 채 시작 시각이 되면 예약이 노출 중·현재 노출로 바뀐다', async () => { // 실제 시간도 흐르게 둬 부팅·요청은 그대로 진행되고, 시작 시각만 앞당겨 넘긴다 diff --git a/src/features/notifications/pages/history-page.spec.tsx b/src/features/notifications/pages/history-page.spec.tsx index 8a008de..09ea8e8 100644 --- a/src/features/notifications/pages/history-page.spec.tsx +++ b/src/features/notifications/pages/history-page.spec.tsx @@ -165,7 +165,7 @@ describe('알림 발송 이력', () => { targetKind: null, }), ); - }, 15_000); // 앱 부팅부터 여러 단계를 한 번에 도는 흐름이라 기본 5초가 빠듯하다 + }); it('제목을 누르면 상세 시트에 본문·대상과 받지 못한 계정 ID를 보이고, 닫으면 주소에서 뺀다', async () => { boot('/notifications'); diff --git a/src/features/notifications/pages/send-page.spec.tsx b/src/features/notifications/pages/send-page.spec.tsx index a02a181..172fc9c 100644 --- a/src/features/notifications/pages/send-page.spec.tsx +++ b/src/features/notifications/pages/send-page.spec.tsx @@ -191,5 +191,5 @@ describe('알림 보내기', () => { ); expect(screen.getByLabelText('제목')).toHaveValue(''); expect(screen.queryByRole('alertdialog')).toBeNull(); - }, 15_000); // 앱 부팅부터 여러 단계를 한 번에 도는 흐름이라 기본 5초가 빠듯하다 + }); }); diff --git a/src/features/search-chips/pages/search-chips.spec.tsx b/src/features/search-chips/pages/search-chips.spec.tsx index 440de23..c34b80c 100644 --- a/src/features/search-chips/pages/search-chips.spec.tsx +++ b/src/features/search-chips/pages/search-chips.spec.tsx @@ -84,9 +84,14 @@ const keywordsInTable = () => .getAllByRole('row') .slice(1) .map((r) => within(r).getAllByRole('cell')[1]!.textContent); -const rowOf = (keyword: string) => screen.getByRole('cell', { name: keyword }).closest('tr')!; const handleOf = (keyword: string) => screen.getByRole('button', { name: `${keyword} 순서 바꾸기` }); +// 셀 이름 조회(getByRole('cell', { name }))는 모든 셀의 접근 가능한 이름을 계산해 회당 수십 ms가 든다 — +// aria-label로 바로 찾히는 손잡이에서 행을 잡는다(전체 실행 부하에서 드래그 흐름이 5초를 넘던 원인) +const rowOf = (keyword: string) => handleOf(keyword).closest('tr')!; +/** 표가 그려질 때까지 기다린다 — 셀 이름 대신 손잡이 이름으로(같은 이유) */ +const tableReady = (keyword = '생일') => + screen.findByRole('button', { name: `${keyword} 순서 바꾸기` }); const dataTransfer = () => ({ effectAllowed: 'none', dropEffect: 'none', @@ -125,7 +130,7 @@ describe('검색 칩 목록', () => { { keyword: '도시락 케이크', status: '숨김', period: '제한 없음' }, ])('$keyword: $status · $period', async ({ keyword, status, period }) => { boot(); - await screen.findByRole('cell', { name: '생일' }); + await tableReady(); const cells = within(rowOf(keyword)).getAllByRole('cell'); expect(cells[0]).toHaveTextContent(String(CHIPS.findIndex((c) => c.keyword === keyword) + 1)); expect(cells[2]).toHaveTextContent(status); @@ -218,7 +223,7 @@ describe('검색 칩 순서 변경', () => { return HttpResponse.json({ data: { adminReorderSearchKeywordChips: [] } }); }), ); - await screen.findByRole('cell', { name: '생일' }); + await tableReady(); const handle = handleOf('생일'); expect(handle).toHaveAccessibleDescription( '손잡이를 끌거나, 손잡이에서 ↑↓ 키로 순서를 바꿉니다.', @@ -256,7 +261,7 @@ describe('검색 칩 순서 변경', () => { it('맨 뒤 칩에서 ↓를 누르면 맨 뒤라고 알리고 순서는 그대로다', async () => { boot(); - await screen.findByRole('cell', { name: '생일' }); + await tableReady(); handleOf('도시락 케이크').focus(); await userEvent.keyboard('{ArrowDown}'); expect(screen.getByRole('status')).toHaveTextContent('이미 맨 뒤입니다.'); @@ -265,7 +270,7 @@ describe('검색 칩 순서 변경', () => { it('손잡이를 끌어 다른 칩 위에 놓으면 그 자리로 옮기고, 되돌리기로 서버 순서로 돌아간다', async () => { boot(); - await screen.findByRole('cell', { name: '생일' }); + await tableReady(); const dt = dataTransfer(); // 손잡이를 누르기 전에는 행을 끌 수 없다 expect(rowOf('추석')).toHaveAttribute('draggable', 'false'); @@ -299,7 +304,7 @@ describe('검색 칩 순서 변경', () => { it('손잡이를 눌렀다 끌지 않고 떼면 행은 다시 끌 수 없다', async () => { boot(); - await screen.findByRole('cell', { name: '생일' }); + await tableReady(); fireEvent.pointerDown(handleOf('생일')); expect(rowOf('생일')).toHaveAttribute('draggable', 'true'); fireEvent.pointerUp(handleOf('생일')); @@ -308,7 +313,7 @@ describe('검색 칩 순서 변경', () => { it('끌지 않은 채 들어온 드래그(파일 등)는 무시한다', async () => { boot(); - await screen.findByRole('cell', { name: '생일' }); + await tableReady(); fireEvent.drop(rowOf('생일'), { dataTransfer: dataTransfer() }); expect(keywordsInTable()[0]).toBe('생일'); expect(screen.queryByText('순서를 바꿨습니다.')).not.toBeInTheDocument(); @@ -316,7 +321,7 @@ describe('검색 칩 순서 변경', () => { it('원래 자리로 되돌려 놓으면 바뀐 것이 없어 저장 줄이 사라진다', async () => { boot(); - await screen.findByRole('cell', { name: '생일' }); + await tableReady(); handleOf('생일').focus(); await userEvent.keyboard('{ArrowDown}'); expect(screen.getByText('순서를 바꿨습니다.')).toBeInTheDocument(); @@ -326,7 +331,7 @@ describe('검색 칩 순서 변경', () => { it('저장 전에는 추가·수정·삭제를 막고 이유를 알린다', async () => { boot(); - await screen.findByRole('cell', { name: '생일' }); + await tableReady(); handleOf('생일').focus(); await userEvent.keyboard('{ArrowDown}'); const reason = '바꾼 순서를 저장하거나 되돌린 뒤에 할 수 있습니다.'; @@ -355,7 +360,7 @@ describe('검색 칩 순서 변경', () => { statusCode: 400, }), ); - await screen.findByRole('cell', { name: '생일' }); + await tableReady(); handleOf('생일').focus(); await userEvent.keyboard('{ArrowDown}'); await userEvent.click(screen.getByRole('button', { name: '순서 저장' })); @@ -381,7 +386,7 @@ describe('검색 칩 순서 변경', () => { statusCode: 400, }), ); - await screen.findByRole('cell', { name: '생일' }); + await tableReady(); handleOf('생일').focus(); await userEvent.keyboard('{ArrowDown}'); await userEvent.click(screen.getByRole('button', { name: '순서 저장' })); diff --git a/src/test/infra/sonner-style.spec.ts b/src/test/infra/sonner-style.spec.ts new file mode 100644 index 0000000..616dded --- /dev/null +++ b/src/test/infra/sonner-style.spec.ts @@ -0,0 +1,12 @@ +import { toast } from 'sonner'; + +/** 테스트 설정(setup.ts)이 sonner의 주입 스타일시트를 걷어내는지 고정한다 — 남으면 조작 직후 역할 조회가 약 3배 느려진다 */ +describe('테스트 환경의 sonner 스타일시트', () => { + it('sonner를 불러와도 테스트 시작 시점의 문서에는 토스트 스타일시트가 없다', () => { + expect(typeof toast).toBe('function'); + const injected = [...document.querySelectorAll('style')].filter((el) => + el.textContent?.includes('data-sonner-toaster'), + ); + expect(injected).toHaveLength(0); + }); +}); diff --git a/src/test/setup.ts b/src/test/setup.ts index 970813e..8849785 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -1,6 +1,6 @@ import '@testing-library/jest-dom/vitest'; import { cleanup, configure } from '@testing-library/react'; -import { afterAll, afterEach, beforeAll } from 'vitest'; +import { afterAll, afterEach, beforeAll, beforeEach } from 'vitest'; import { installZodKorean } from '@/shared/lib/zod-locale'; @@ -32,7 +32,18 @@ proto.setPointerCapture ??= () => undefined; proto.releasePointerCapture ??= () => undefined; proto.scrollIntoView ??= () => undefined; +/** sonner가 import될 때 문서에 넣는 토스트 스타일시트를 찾는 표식 */ +const SONNER_STYLE_MARKER = 'data-sonner-toaster'; + beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); +// sonner는 import 시점에 97규칙짜리