fix(listings): ToolsTab 제출·disclaimer 테스트 추가 + 수집 완료 폴링
최종 리뷰 반영: (1) ToolsTab 제출→중첩객체·disclaimer 렌더 경로 테스트 2건 추가(최고위험 미검증 경로), (2) handleCollect의 고정 3초 타임아웃을 collected_at 변경 기반 폴링(최대 60s)으로 교체 — 진행 중을 완료로 오표시 하지 않도록. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHXzpsZQxKG9hQmNRfZjRS
This commit is contained in:
@@ -68,13 +68,30 @@ const ListingsTab = () => {
|
||||
|
||||
const handleCollect = async () => {
|
||||
setCollecting(true);
|
||||
setError('');
|
||||
const prevAt = collectStatus?.collected_at ?? null;
|
||||
try {
|
||||
await collectListings();
|
||||
setTimeout(() => { loadStatus(); load(); setCollecting(false); }, 3000);
|
||||
} catch (e) {
|
||||
setError(e?.message ?? String(e));
|
||||
setCollecting(false);
|
||||
return;
|
||||
}
|
||||
let tries = 0;
|
||||
const poll = async () => {
|
||||
tries += 1;
|
||||
let st = null;
|
||||
try { st = await getListingsCollectStatus(); } catch { /* noop */ }
|
||||
if (st) setCollectStatus(st);
|
||||
const done = st && (st.collected_at !== prevAt || st.error);
|
||||
if (done || tries >= 12) {
|
||||
setCollecting(false);
|
||||
load();
|
||||
return;
|
||||
}
|
||||
window.setTimeout(poll, 5000);
|
||||
};
|
||||
window.setTimeout(poll, 5000);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
|
||||
vi.mock('../../../api', () => ({ safetyCheck: vi.fn(), budgetCalc: vi.fn() }));
|
||||
import { safetyCheck, budgetCalc } from '../../../api';
|
||||
import ToolsTab from './ToolsTab.jsx';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
@@ -14,4 +15,30 @@ describe('ToolsTab', () => {
|
||||
// 제출 버튼 존재
|
||||
expect(screen.getAllByRole('button', { name: /판정|계산/ }).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('안전마진 제출 → tier·비율·disclaimer 렌더 (객체 아님)', async () => {
|
||||
safetyCheck.mockResolvedValue({ median: 45000, ratio: 0.72, tier: '안전', sample: 7, is_toheo: false, disclaimer: '등기부 선순위·근사치 주의' });
|
||||
render(<ToolsTab />);
|
||||
fireEvent.change(screen.getByPlaceholderText(/면적/), { target: { value: '84' } });
|
||||
fireEvent.change(screen.getByPlaceholderText(/금액/), { target: { value: '45000' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '판정' }));
|
||||
await waitFor(() => expect(screen.getByText(/등기부 선순위/)).toBeInTheDocument());
|
||||
// "안전마진 체커" 헤딩과 구분하기 위해 "마진"이 뒤따르지 않는 "안전"만 매칭
|
||||
expect(screen.getByText(/안전(?!마진)/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('예산 제출 → 전세/매매 금액·disclaimer 렌더 (중첩 객체 필드별 렌더)', async () => {
|
||||
budgetCalc.mockResolvedValue({
|
||||
jeonse: { loan_limit: 30000, max_deposit: 40000, notes: '' },
|
||||
purchase: { ltv_pct: 70, loan_cap: 50000, max_price: 90000, dsr_note: 'DSR 40%', regulation_flags: ['규제지역'] },
|
||||
region: { is_toheo: true, is_regulated: true, notes: '토허 구역' },
|
||||
disclaimer: '정책 수치는 근사치입니다',
|
||||
});
|
||||
render(<ToolsTab />);
|
||||
fireEvent.change(screen.getByPlaceholderText(/자기자본/), { target: { value: '20000' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '계산' }));
|
||||
await waitFor(() => expect(screen.getByText(/정책 수치는 근사치/)).toBeInTheDocument());
|
||||
// purchase.regulation_flags 와 region.is_regulated 플래그 둘 다 "규제지역" 텍스트를 렌더함
|
||||
expect(screen.getAllByText('규제지역').length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user