Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion schema/schema.graphql
Original file line number Diff line number Diff line change
@@ -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 ----
Expand Down
2 changes: 1 addition & 1 deletion src/features/banners/pages/banners.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
// 실제 시간도 흐르게 둬 부팅·요청은 그대로 진행되고, 시작 시각만 앞당겨 넘긴다
Expand Down
2 changes: 1 addition & 1 deletion src/features/notifications/pages/history-page.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ describe('알림 발송 이력', () => {
targetKind: null,
}),
);
}, 15_000); // 앱 부팅부터 여러 단계를 한 번에 도는 흐름이라 기본 5초가 빠듯하다
});

it('제목을 누르면 상세 시트에 본문·대상과 받지 못한 계정 ID를 보이고, 닫으면 주소에서 뺀다', async () => {
boot('/notifications');
Expand Down
2 changes: 1 addition & 1 deletion src/features/notifications/pages/send-page.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,5 +191,5 @@ describe('알림 보내기', () => {
);
expect(screen.getByLabelText('제목')).toHaveValue('');
expect(screen.queryByRole('alertdialog')).toBeNull();
}, 15_000); // 앱 부팅부터 여러 단계를 한 번에 도는 흐름이라 기본 5초가 빠듯하다
});
});
27 changes: 16 additions & 11 deletions src/features/search-chips/pages/search-chips.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -218,7 +223,7 @@ describe('검색 칩 순서 변경', () => {
return HttpResponse.json({ data: { adminReorderSearchKeywordChips: [] } });
}),
);
await screen.findByRole('cell', { name: '생일' });
await tableReady();
const handle = handleOf('생일');
expect(handle).toHaveAccessibleDescription(
'손잡이를 끌거나, 손잡이에서 ↑↓ 키로 순서를 바꿉니다.',
Expand Down Expand Up @@ -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('이미 맨 뒤입니다.');
Expand All @@ -265,7 +270,7 @@ describe('검색 칩 순서 변경', () => {

it('손잡이를 끌어 다른 칩 위에 놓으면 그 자리로 옮기고, 되돌리기로 서버 순서로 돌아간다', async () => {
boot();
await screen.findByRole('cell', { name: '생일' });
await tableReady();
const dt = dataTransfer();
// 손잡이를 누르기 전에는 행을 끌 수 없다
expect(rowOf('추석')).toHaveAttribute('draggable', 'false');
Expand Down Expand Up @@ -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('생일'));
Expand All @@ -308,15 +313,15 @@ 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();
});

it('원래 자리로 되돌려 놓으면 바뀐 것이 없어 저장 줄이 사라진다', async () => {
boot();
await screen.findByRole('cell', { name: '생일' });
await tableReady();
handleOf('생일').focus();
await userEvent.keyboard('{ArrowDown}');
expect(screen.getByText('순서를 바꿨습니다.')).toBeInTheDocument();
Expand All @@ -326,7 +331,7 @@ describe('검색 칩 순서 변경', () => {

it('저장 전에는 추가·수정·삭제를 막고 이유를 알린다', async () => {
boot();
await screen.findByRole('cell', { name: '생일' });
await tableReady();
handleOf('생일').focus();
await userEvent.keyboard('{ArrowDown}');
const reason = '바꾼 순서를 저장하거나 되돌린 뒤에 할 수 있습니다.';
Expand Down Expand Up @@ -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: '순서 저장' }));
Expand All @@ -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: '순서 저장' }));
Expand Down
12 changes: 12 additions & 0 deletions src/test/infra/sonner-style.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
13 changes: 12 additions & 1 deletion src/test/setup.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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규칙짜리 <style>을 넣는다. jsdom은 DOM이 바뀔 때마다 스타일 캐시를 버리고
// getComputedStyle에서 문서의 모든 규칙을 다시 대조하는데, 역할·이름 조회가 조회 1회에 이를 100번 넘게 부른다.
// 조작 직후 조회가 66ms → 22ms, 전체 스위트(커버리지) 합계 135초 → 99초. jsdom에서 토스트 CSS는 쓸 데가 없다
beforeEach(() => {
document.querySelectorAll('style').forEach((el) => {
if (el.textContent?.includes(SONNER_STYLE_MARKER)) el.remove();
});
});
afterEach(() => {
server.resetHandlers();
cleanup();
Expand Down
Loading