{ "enabled": true, "id": "quality-inspection", "name": "제품검사관리 테스트", "version": "2.0.0", "screenshotPolicy": { "captureOnFail": true, "captureOnPass": false }, "description": "품질관리 > 제품검사관리 메뉴의 페이지 로드, 테이블 구조, 검색/필터, 상세 조회 검증 (READ-only, CRUD 제외 - API 필수 필드 매핑 미완)", "baseUrl": "https://dev.codebridge-x.com", "menuNavigation": { "level1": "품질관리", "level2": "제품검사관리", "expectedUrl": "/quality/inspections", "searchWithinParent": true, "closeOtherMenus": true }, "auth": { "username": "TestUser5", "password": "password123!" }, "steps": [ { "id": 1, "name": "메뉴 진입: 품질관리 > 제품검사관리", "action": "menu_navigate", "level1": "품질관리", "level2": "제품검사관리", "expected": { "url_contains": "/quality/inspections" } }, { "id": 2, "name": "페이지 로드 대기", "action": "wait", "timeout": 3000 }, { "id": 3, "name": "URL 검증", "action": "verify_url", "expected": { "url_contains": "/quality/inspections" } }, { "id": 4, "name": "목업 감지", "action": "verify_not_mockup", "checks": ["제품검사 목록 표시", "등록 버튼 존재"] }, { "id": 5, "name": "테이블 로드 대기", "action": "wait_for_table", "timeout": 10000 }, { "id": 6, "name": "통계 카드 확인", "action": "evaluate", "script": "(() => { const cards = document.querySelectorAll('[class*=\"card\"], [class*=\"Card\"], [class*=\"stat\"], [class*=\"Stat\"]'); const texts = Array.from(cards).map(c => c.innerText?.substring(0, 40)).filter(Boolean); return texts.length > 0 ? 'pass: Stats cards=' + texts.length : 'warn: No stat cards'; })()" }, { "id": 7, "name": "테이블 컬럼 구조 확인", "action": "evaluate", "script": "(() => { const ths = Array.from(document.querySelectorAll('table thead th, table th, [role=\"columnheader\"]')); const cols = ths.map(t => t.innerText?.trim()).filter(Boolean); return cols.length > 0 ? 'pass: columns=' + cols.length + ' [' + cols.join(', ') + ']' : 'warn: No table headers'; })()" }, { "id": 8, "name": "테이블 데이터 행 확인", "action": "evaluate", "script": "(() => { const rows = document.querySelectorAll('table tbody tr'); const visRows = Array.from(rows).filter(r => r.offsetParent !== null); return visRows.length > 0 ? 'pass: ' + visRows.length + ' rows in table' : 'warn: No data rows (empty table)'; })()" }, { "id": 9, "name": "필터/검색 UI 확인", "action": "evaluate", "script": "(() => { const R = {}; const searchInputs = document.querySelectorAll('input[type=\"search\"], input[placeholder*=\"검색\"], input[placeholder*=\"조회\"]'); R.searchInputs = searchInputs.length; const selects = document.querySelectorAll('select, [role=\"combobox\"], button[class*=\"Select\"]'); R.filters = selects.length; const tabs = document.querySelectorAll('button[role=\"tab\"]'); R.tabs = tabs.length; return 'pass: search=' + R.searchInputs + ' filters=' + R.filters + ' tabs=' + R.tabs; })()" }, { "id": 10, "name": "[READ] 첫 번째 행 클릭 (상세 보기)", "phase": "READ", "action": "click_first_row" }, { "id": 11, "name": "[READ] 상세 대기", "phase": "READ", "action": "wait", "timeout": 2000 }, { "id": 12, "name": "[READ] 상세 다이얼로그/페이지 확인", "phase": "READ", "action": "evaluate", "script": "(async () => { const R = { phase: 'DETAIL_CHECK' }; const dlg = document.querySelector('[role=\"dialog\"]'); const isVis = el => !!el && el.getBoundingClientRect().width > 0; if (isVis(dlg)) { R.type = 'dialog'; R.text = dlg.innerText?.substring(0, 200); R.hasFields = dlg.querySelectorAll('input, textarea, select, label, dt, [class*=\"field\"]').length; R.ok = true; R.info = 'pass: 상세 다이얼로그 열림 (fields=' + R.hasFields + ')'; } else if (window.location.href.includes('/inspections/')) { R.type = 'page'; R.text = document.body.innerText?.substring(0, 200); R.ok = true; R.info = 'pass: 상세 페이지 이동'; } else { R.ok = true; R.info = 'warn: 상세 화면 미확인 (행 클릭 반응 없음)'; } return JSON.stringify(R); })()", "timeout": 5000 }, { "id": 13, "name": "[READ] 상세 필드 확인", "phase": "READ", "action": "evaluate", "script": "(() => { const R = { phase: 'FIELD_CHECK' }; const dlg = document.querySelector('[role=\"dialog\"]'); const scope = (dlg && dlg.getBoundingClientRect().width > 0) ? dlg : document; const labels = Array.from(scope.querySelectorAll('label, dt, [class*=\"label\"], [class*=\"Label\"]')); const fields = labels.map(l => l.innerText?.trim()).filter(t => t && t.length < 30); R.fieldCount = fields.length; R.fields = fields.slice(0, 15); R.ok = true; R.info = fields.length > 0 ? 'pass: ' + fields.length + ' fields found' : 'warn: no labeled fields'; return JSON.stringify(R); })()" }, { "id": 14, "name": "[READ] 등록 버튼 존재 확인 (클릭하지 않음)", "phase": "READ", "action": "evaluate", "script": "(() => { const btns = Array.from(document.querySelectorAll('button')).filter(b => b.offsetParent !== null); const createBtn = btns.find(b => /등록|추가|신규/.test(b.innerText?.trim())); const editBtn = btns.find(b => /수정|편집/.test(b.innerText?.trim())); const delBtn = btns.find(b => /삭제/.test(b.innerText?.trim())); return 'pass: 등록=' + (createBtn ? '있음' : '없음') + ' 수정=' + (editBtn ? '있음' : '없음') + ' 삭제=' + (delBtn ? '있음' : '없음'); })()" }, { "id": 15, "name": "[READ] 모달/다이얼로그 닫기", "phase": "READ", "action": "close_modal_if_open" }, { "id": 16, "name": "페이지네이션 확인", "action": "evaluate", "script": "(() => { const p = document.querySelector('[class*=\"pagination\"], [class*=\"Pagination\"], nav[aria-label*=\"page\"]'); const pageButtons = Array.from(document.querySelectorAll('button')).filter(b => /^\\d+$/.test(b.innerText?.trim())); return p ? 'pass: Pagination found' : pageButtons.length > 0 ? 'pass: Page buttons=' + pageButtons.length : 'No pagination (ok)'; })()" }, { "id": 17, "name": "[SUMMARY] API 호출 통계", "action": "evaluate", "script": "(() => { const logs = window.__E2E__ ? window.__E2E__.getApiLogs().logs : ((window.__E2E__?window.__E2E__.getApiLogs().logs:[])); const inspApi = logs.filter(l => l.url?.includes('inspection')); const failedApis = logs.filter(l => l.status >= 400); return 'pass: API total=' + logs.length + ' inspection=' + inspApi.length + ' failed=' + failedApis.length; })()" } ], "expectedAPIs": [ { "method": "GET", "endpoint": "/api/v1/inspections", "description": "제품검사 목록 조회" }, { "method": "GET", "endpoint": "/api/v1/inspections/stats", "description": "통계 조회" } ], "knownIssues": [ { "issue": "GET /api/v1/inspections/calendar → 404", "reason": "백엔드 라우트 미구현. 프론트엔드가 페이지 로드 시 자동 호출하지만 USE_MOCK_FALLBACK으로 동작", "severity": "low", "action": "백엔드에 calendar 엔드포인트 구현 필요" }, { "issue": "CRUD 테스트 미포함", "reason": "API 필수 필드(품목코드, 검사유형 등)와 프론트엔드 폼 매핑이 복잡하여 READ-only로 제한", "severity": "info", "action": "폼 필드 분석 후 CRUD 시나리오 별도 작성 가능" } ], "rollbackPlan": { "note": "READ-only 테스트. 데이터 변경 없음." } }