diff --git a/company-info.json b/company-info.json index 60b10b2..7d3dc90 100644 --- a/company-info.json +++ b/company-info.json @@ -1,49 +1,18 @@ { "id": "company-info", "name": "설정 - 회사정보", - "description": "회사 정보 관리 기능 테스트 - 회사 정보 조회, 수정, 회사 추가 기능", - "baseUrl": "https://dev.codebridge-x.com", - "screenshotPolicy": { "onErrorOnly": true, "captureOn": ["error", "fail", "timeout", "404", "500", "blocked"] }, - - "selectors": { - "pageTitle": "h1, h2, [class*='PageHeader'] h1, [class*='page-header'] h1", - "pageLayout": "[class*='PageLayout'], main", - "addCompanyBtn": "button:has-text('회사 추가')", - "editBtn": "button:has-text('수정')", - "saveBtn": "button:has-text('저장')", - "cancelBtn": "button:has-text('취소')", - "companyNameInput": "#companyName", - "representativeNameInput": "#representativeName", - "businessTypeInput": "#businessType", - "businessCategoryInput": "#businessCategory", - "emailInput": "#email", - "taxInvoiceEmailInput": "#taxInvoiceEmail", - "businessNumberInput": "#businessNumber", - "paymentBankInput": "#paymentBank", - "paymentAccountInput": "#paymentAccount", - "paymentAccountHolderInput": "#paymentAccountHolder", - "addressSearchBtn": "button:has-text('우편번호 찾기')", - "companyInfoCard": "[class*='Card']:has(h3:has-text('회사 정보'))", - "paymentInfoCard": "[class*='Card']:has(h3:has-text('결제 계좌 정보'))", - "dialog": "[role='dialog']", - "dialogTitle": "[role='dialog'] h2", - "dialogCancelBtn": "[role='dialog'] button:has-text('취소')", - "dialogNextBtn": "[role='dialog'] button:has-text('다음')", - "dialogBusinessNumberInput": "[role='dialog'] #businessNumber", - "alertDialog": "[role='alertdialog']", - "alertConfirmBtn": "[role='alertdialog'] button:has-text('확인')" - }, + "description": "회사 정보 관리 기능 테스트 - 회사 정보 조회, 수정, 회사 추가 기능", + "baseUrl": "https://dev.codebridge-x.com", "navigation": { "targetUrl": "/company-info", - "urlPattern": "/company-info|/ko/company-info", + "urlPattern": "/company-info|/ko/company-info|/settings/company-info", "menuHints": ["회사정보", "회사 정보", "설정"] }, - "menuNavigation": { "level1": "설정", "level2": "회사정보", @@ -51,386 +20,416 @@ "searchWithinParent": true, "closeOtherMenus": true }, - "auth": { "username": "TestUser5", "password": "password123!" }, + "menuNavigationEnhanced": { + "strategy": "scroll-and-search", + "description": "사이드바를 스크롤하며 메뉴를 찾고 클릭하여 404를 방지", + "level1": "설정", + "level2": "회사정보", + "alternativeLevel1Names": ["설정", "Settings", "환경설정", "시스템설정", "관리"], + "alternativeLevel2Names": ["회사정보", "회사 정보", "Company Info", "회사관리", "기업정보"], + "fallbackUrls": [ + "/company-info", + "/ko/company-info", + "/settings/company-info", + "/ko/settings/company-info" + ], + "scrollConfig": { + "sidebarSelector": "nav, aside, [role='navigation'], .sidebar, #sidebar", + "menuItemSelector": "a, button, [role='menuitem'], [role='treeitem']", + "scrollStep": 200, + "maxScrollAttempts": 10, + "scrollDelay": 300 + } + }, "expectedAPIs": [ - { "method": "GET", "path": "/api/v1/company-info", "description": "회사 정보 조회" }, - { "method": "PUT", "path": "/api/v1/company-info/:id", "description": "회사 정보 수정" }, - { "method": "POST", "path": "/api/v1/company-info", "description": "회사 추가" } + { + "method": "GET", + "path": "/api/v1/company-info", + "description": "회사 정보 조회" + }, + { + "method": "PUT", + "path": "/api/v1/company-info/:id", + "description": "회사 정보 수정" + }, + { + "method": "POST", + "path": "/api/v1/company-info", + "description": "회사 추가" + } ], "steps": [ { "id": 0, "name": "사이드바 메뉴 전체 펼치기", - "description": "모두 펼치기 버튼 클릭 후 메뉴 탐색 준비", + "description": "모두 펼치기 버튼을 클릭하여 전체 메뉴를 펼친 후 메뉴 탐색 준비", "actions": [ - { "type": "scroll", "target": "sidebar", "direction": "top" }, + { "type": "scroll", "target": "sidebar", "direction": "top", "description": "사이드바 최상단으로 스크롤" }, { "type": "wait", "duration": 300 }, { "type": "evaluate", "script": "Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('모두 펼치기'))?.click()" }, { "type": "wait", "duration": 2000 } + ], + "verification": [ + "사이드바가 화면에 보이는지 확인", + "모든 메뉴가 펼쳐졌는지 확인" ] }, { "id": 1, - "name": "1차 메뉴 클릭: 설정", - "description": "사이드바에서 설정 메뉴 찾아 클릭", + "name": "1차 메뉴 찾기: 설정 (스크롤 포함)", + "description": "사이드바를 스크롤하며 '설정' 메뉴를 찾아 클릭", "actions": [ { "type": "scrollAndFind", "target": "설정", - "alternativeTexts": ["설정", "Settings", "환경설정"], + "alternativeTexts": ["설정", "Settings", "환경설정", "시스템설정"], "scrollContainer": "sidebar", - "maxAttempts": 10 + "maxAttempts": 10, + "description": "스크롤하며 설정 메뉴 찾기" }, { "type": "wait", "duration": 300 }, - { "type": "click_if_exists", "target": "설정" }, - { "type": "wait", "duration": 500 } - ] + { "type": "click_if_exists", "target": "설정", "description": "설정 메뉴 클릭" }, + { "type": "wait", "duration": 500, "description": "서브메뉴 펼쳐지기 대기" }, + { "type": "screenshot", "name": "settings_menu_expanded" } + ], + "verification": [ + "설정 메뉴가 클릭되었는지 확인", + "서브메뉴가 펼쳐졌는지 확인", + "하위 메뉴 항목들이 보이는지 확인" + ], + "fallback": { + "if": "메뉴를 찾을 수 없음", + "then": "사이드바 전체를 스크롤하며 재탐색" + } }, { "id": 2, - "name": "2차 메뉴 클릭: 회사정보", - "description": "서브메뉴에서 회사정보 클릭", + "name": "2차 메뉴 찾기: 회사정보 (스크롤 포함)", + "description": "서브메뉴에서 '회사정보'를 찾아 클릭", "actions": [ { "type": "scrollAndFind", "target": "회사정보", - "alternativeTexts": ["회사정보", "회사 정보"], + "alternativeTexts": ["회사정보", "회사 정보", "Company Info", "회사관리"], "scrollContainer": "submenu", - "maxAttempts": 5 + "maxAttempts": 5, + "description": "서브메뉴에서 회사정보 찾기" }, { "type": "wait", "duration": 200 }, - { "type": "click_if_exists", "target": "회사정보" }, - { "type": "wait", "duration": 3000 } + { "type": "click_if_exists", "target": "회사정보", "description": "회사정보 메뉴 클릭" }, + { "type": "wait", "target": "페이지 로드 완료", "timeout": 10000 }, + { "type": "screenshot", "name": "company_info_page" } + ], + "verification": [ + "회사정보 메뉴 클릭 성공", + "페이지 이동 또는 컨텐츠 로드" ] }, { "id": 3, - "name": "페이지 URL 검증", - "description": "회사정보 페이지 URL 확인", + "name": "404 에러 감지 및 대체 경로 시도", + "description": "페이지 로드 후 404 에러 여부 확인, 404시 대체 경로 탐색", "actions": [ - { "type": "verify_url", "pattern": "/company-info" } - ] + { "type": "wait", "duration": 1000 }, + { "type": "checkFor404", "indicators": [ + "페이지를 찾을 수 없습니다", + "404", + "Not Found", + "존재하지 않거나" + ]}, + { "type": "screenshot", "name": "page_load_result" } + ], + "verification": [ + "현재 페이지가 404인지 확인" + ], + "onError404": { + "description": "404 에러 발생 시 대체 URL 시도", + "actions": [ + { "type": "log", "message": "404 감지 - 대체 경로 탐색 시작" }, + { + "type": "tryAlternativeUrls", + "urls": [ + "/company-info", + "/ko/company-info" + ], + "stopOnSuccess": true + }, + { + "type": "ifStillFailed", + "action": "navigateViaMenuClick", + "description": "URL 직접 접근 실패 시 메뉴 클릭으로 재시도" + } + ] + } }, { "id": 4, - "name": "페이지 제목 검증", - "description": "페이지 제목이 '회사정보'인지 확인", + "name": "페이지 정상 로드 확인", + "description": "회사정보 페이지가 정상적으로 로드되었는지 확인", "actions": [ - { - "type": "verify_element", - "selector": "h1, h2, [class*='PageHeader'] h1", - "contains": "회사정보" - } - ] + { "type": "verify", "target": "pageTitle", "contains": ["회사정보", "회사 정보", "Company"] }, + { "type": "verify", "target": "pageContent", "notContains": ["404", "찾을 수 없습니다", "Not Found"] } + ], + "verification": [ + "페이지 제목 '회사정보' 또는 관련 텍스트 표시", + "404 에러 메시지 미표시", + "콘텐츠가 정상 렌더링됨" + ], + "successCriteria": { + "urlPattern": "/company-info", + "requiredElements": ["회사", "회사명", "대표자명"] + } }, { - "id": 5, + "step": 5, + "name": "페이지 제목 확인", + "action": "verify", + "target": "heading", + "expected": "회사정보", + "validation": "페이지 제목이 '회사정보'로 표시됨" + }, + { + "step": 6, "name": "회사 추가 버튼 존재 확인", - "description": "'회사 추가' 버튼이 화면에 존재하는지 확인", - "actions": [ - { - "type": "verify_element", - "selector": "button", - "contains": "회사 추가" - } - ] + "action": "verify", + "target": "button[text='회사 추가']", + "expected": "button exists", + "validation": "회사 추가 버튼이 표시됨" }, { - "id": 6, + "step": 7, "name": "수정 버튼 존재 확인", - "description": "'수정' 버튼이 화면에 존재하는지 확인", + "action": "verify", + "target": "button[text='수정']", + "expected": "button exists", + "validation": "수정 버튼이 표시됨" + }, + { + "step": 8, + "name": "회사명 필드 확인", + "action": "verify", + "target": "textbox[label='회사명'][disabled]", + "expected": "프론트_테스트회사", + "validation": "회사명이 표시되고 비활성화 상태" + }, + { + "step": 9, + "name": "대표자명 필드 확인", + "action": "verify", + "target": "textbox[label='대표자명'][disabled]", + "expected": "프론트", + "validation": "대표자명이 표시되고 비활성화 상태" + }, + { + "step": 10, + "name": "업태 필드 확인", + "action": "verify", + "target": "textbox[label='업태'][disabled]", + "expected": "업태명", + "validation": "업태가 표시되고 비활성화 상태" + }, + { + "step": 11, + "name": "업종 필드 확인", + "action": "verify", + "target": "textbox[label='업종'][disabled]", + "expected": "업종명", + "validation": "업종이 표시되고 비활성화 상태" + }, + { + "step": 12, + "name": "주소 필드 확인", + "action": "verify", + "target": "textbox[label='주소명'][disabled]", + "expected": "주소 표시", + "validation": "주소가 표시되고 비활성화 상태" + }, + { + "step": 13, + "name": "이메일 필드 확인", + "action": "verify", + "target": "textbox[label='이메일 (아이디)'][disabled]", + "expected": "이메일 표시", + "validation": "이메일이 표시되고 비활성화 상태" + }, + { + "step": 14, + "name": "사업자등록번호 필드 확인", + "action": "verify", + "target": "textbox[label='사업자등록번호'][disabled]", + "expected": "사업자등록번호 표시", + "validation": "사업자등록번호가 표시되고 비활성화 상태" + }, + { + "step": 15, + "name": "수정 버튼 클릭", + "action": "click_if_exists", + "target": "button[text='수정']", + "expected": "edit mode enabled", + "validation": "수정 모드로 전환됨" + }, + { + "step": 16, + "name": "수정 모드 - 필드 활성화 확인", + "action": "verify", + "target": "textbox:not([disabled])", + "expected": "fields enabled", + "validation": "텍스트 필드들이 활성화됨" + }, + { + "step": 17, + "name": "취소 버튼 클릭", + "action": "click_if_exists", + "target": "button[text='취소']", + "expected": "edit mode disabled", + "validation": "조회 모드로 복귀" + }, + { + "step": 18, + "name": "회사 추가 버튼 클릭", + "action": "click_if_exists", + "target": "button[text='회사 추가']", + "expected": "dialog opened", + "validation": "회사 추가 다이얼로그가 열림" + }, + { + "step": 19, + "name": "회사 추가 다이얼로그 확인", + "action": "verify", + "target": "dialog", + "expected": "회사 추가 다이얼로그 표시", + "validation": "다이얼로그 제목, 입력 필드, 버튼 확인" + }, + { + "step": 20, + "name": "다이얼로그 닫기", + "action": "click_if_exists", + "target": "dialog button[text='취소']", + "expected": "dialog closed", + "validation": "다이얼로그가 닫힘" + }, + { + "step": 21, + "name": "수정 모드에서 데이터 변경 테스트", + "description": "실제 데이터를 수정하고 저장 기능 검증", "actions": [ - { - "type": "verify_element", - "selector": "button", - "contains": "수정" - } + { "type": "click_if_exists", "target": "수정", "description": "수정 모드 진입" } + ], + "expect": { + "fieldsEnabled": true + } + }, + { + "step": 22, + "name": "업태 필드 수정", + "description": "업태 필드 값 변경", + "actions": [ + { "type": "clear", "target": "업태" }, + { "type": "fill", "target": "업태", "value": "테스트업태_수정" } ] }, { - "id": 7, - "name": "회사명 입력 필드 확인", - "description": "#companyName 입력 필드가 존재하고 비활성화 상태인지 확인", - "actions": [ - { - "type": "verify_element", - "selector": "#companyName" - }, - { - "type": "evaluate", - "script": "const el = document.querySelector('#companyName'); return { exists: !!el, disabled: el?.disabled, value: el?.value };" - } - ] - }, - { - "id": 8, - "name": "대표자명 입력 필드 확인", - "description": "#representativeName 입력 필드 존재 확인", - "actions": [ - { - "type": "verify_element", - "selector": "#representativeName" - } - ] - }, - { - "id": 9, - "name": "업태 입력 필드 확인", - "description": "#businessType 입력 필드 존재 확인", - "actions": [ - { - "type": "verify_element", - "selector": "#businessType" - } - ] - }, - { - "id": 10, - "name": "업종 입력 필드 확인", - "description": "#businessCategory 입력 필드 존재 확인", - "actions": [ - { - "type": "verify_element", - "selector": "#businessCategory" - } - ] - }, - { - "id": 11, - "name": "이메일 입력 필드 확인", - "description": "#email 입력 필드 존재 확인", - "actions": [ - { - "type": "verify_element", - "selector": "#email" - } - ] - }, - { - "id": 12, - "name": "사업자등록번호 입력 필드 확인", - "description": "#businessNumber 입력 필드 존재 확인", - "actions": [ - { - "type": "verify_element", - "selector": "#businessNumber" - } - ] - }, - { - "id": 13, - "name": "조회 모드 - 필드 비활성화 상태 검증", - "description": "수정 버튼 클릭 전 모든 필드가 disabled 상태인지 확인", - "actions": [ - { - "type": "evaluate", - "script": "const fields = ['#companyName', '#representativeName', '#businessType', '#businessCategory', '#email', '#businessNumber']; const results = fields.map(sel => { const el = document.querySelector(sel); return { selector: sel, disabled: el?.disabled ?? false }; }); const allDisabled = results.every(r => r.disabled); return { allDisabled, results };" - } - ] - }, - { - "id": 14, - "name": "[UPDATE] 수정 버튼 클릭", - "phase": "UPDATE", - "description": "수정 모드 진입", - "actions": [ - { "type": "click_button", "text": "수정" }, - { "type": "wait", "duration": 500 } - ] - }, - { - "id": 15, - "name": "[UPDATE] 수정 모드 - 필드 활성화 검증", - "phase": "UPDATE", - "description": "수정 모드에서 입력 필드들이 활성화되었는지 확인", - "actions": [ - { - "type": "evaluate", - "script": "const fields = ['#companyName', '#representativeName', '#businessType', '#businessCategory']; const results = fields.map(sel => { const el = document.querySelector(sel); return { selector: sel, disabled: el?.disabled ?? true }; }); const anyEnabled = results.some(r => !r.disabled); return { anyEnabled, results };" - } - ] - }, - { - "id": 16, - "name": "[UPDATE] 저장/취소 버튼 표시 확인", - "phase": "UPDATE", - "description": "수정 모드에서 저장, 취소 버튼이 표시되는지 확인", - "actions": [ - { - "type": "verify_element", - "selector": "button", - "contains": "저장" - }, - { - "type": "verify_element", - "selector": "button", - "contains": "취소" - } - ] - }, - { - "id": 17, - "name": "[UPDATE] 업태 필드 값 변경", - "phase": "UPDATE", - "description": "업태 필드에 테스트 값 입력", - "actions": [ - { "type": "clear", "selector": "#businessType" }, - { "type": "fill", "selector": "#businessType", "value": "E2E_TEST_업태" } - ] - }, - { - "id": 18, - "name": "[UPDATE] 저장 버튼 클릭", - "phase": "UPDATE", + "step": 23, + "name": "저장 버튼 클릭", "description": "수정된 회사 정보 저장", "actions": [ - { "type": "click_button", "text": "저장" }, - { "type": "wait", "duration": 2000 } - ] + { "type": "click_if_exists", "target": "저장" } + ], + "waitFor": { + "type": "apiResponse", + "method": "PUT", + "timeout": 5000 + }, + "expect": { + "toast": ["수정", "완료", "성공", "저장"] + } }, { - "id": 19, - "name": "[UPDATE] 저장 성공 토스트 확인", - "phase": "UPDATE", - "description": "저장 성공 메시지 토스트 확인", - "actions": [ - { - "type": "verify_toast", - "contains": ["저장", "완료", "성공"] + "step": 24, + "name": "⚠️ 필수 검증: 수정 데이터 반영 확인", + "note": "토스트 성공 메시지만으로 PASS 판정 불가. 실제 데이터 변경 확인 필수!", + "description": "수정된 업태 값이 반영되었는지 확인", + "verify": { + "fieldValue": { + "target": "업태", + "expected": "테스트업태_수정" } - ] + } }, { - "id": 20, - "name": "[UPDATE] 수정 데이터 반영 확인", - "phase": "UPDATE", - "description": "변경된 업태 값이 필드에 반영되었는지 확인", - "critical": true, - "actions": [ - { - "type": "verify_input_value", - "selector": "#businessType", - "contains": "E2E_TEST_업태" - } - ] - }, - { - "id": 21, + "step": 25, "name": "회사 추가 다이얼로그 열기", - "description": "회사 추가 버튼 클릭하여 다이얼로그 표시", + "description": "회사 추가 버튼 클릭하여 다이얼로그 열기", "actions": [ - { "type": "click_button", "text": "회사 추가" }, - { "type": "wait", "duration": 500 } + { "type": "click_if_exists", "target": "회사 추가" } + ], + "expect": { + "dialog": true, + "visible": ["회사명", "대표자명", "사업자등록번호", "등록", "취소"] + } + }, + { + "step": 26, + "name": "새 회사 정보 입력", + "description": "회사 추가 다이얼로그에서 필수 정보 입력", + "actions": [ + { "type": "fill", "target": "회사명", "value": "테스트회사_{timestamp}" }, + { "type": "fill", "target": "대표자명", "value": "테스트대표" }, + { "type": "fill", "target": "사업자등록번호", "value": "123-45-67890" } ] }, { - "id": 22, - "name": "회사 추가 다이얼로그 표시 검증", - "description": "다이얼로그가 열리고 제목이 '회사 추가'인지 확인", + "step": 27, + "name": "회사 등록", + "description": "등록 버튼 클릭하여 새 회사 등록", "actions": [ - { - "type": "verify_element", - "selector": "[role='dialog']" - }, - { - "type": "verify_element", - "selector": "[role='dialog'] h2", - "contains": "회사 추가" - } - ] + { "type": "click_if_exists", "target": "등록" } + ], + "waitFor": { + "type": "apiResponse", + "method": "POST", + "timeout": 5000 + }, + "expect": { + "toast": ["등록", "완료", "성공"], + "dialogClosed": true + } }, { - "id": 23, - "name": "다이얼로그 내 사업자등록번호 입력 필드 확인", - "description": "다이얼로그 내 #businessNumber 입력 필드 존재 확인", - "actions": [ - { - "type": "verify_element", - "selector": "[role='dialog'] #businessNumber" - } - ] + "step": 28, + "name": "⚠️ 필수 검증: 회사 등록 반영 확인", + "note": "토스트 성공 메시지만으로 PASS 판정 불가. 실제 데이터 등록 확인 필수!", + "description": "등록된 회사가 목록에 표시되는지 확인", + "verify": { + "visible": "테스트회사" + } }, { - "id": 24, - "name": "다이얼로그 내 취소/다음 버튼 확인", - "description": "취소, 다음 버튼 존재 확인", + "step": 29, + "name": "원복: 업태 필드 원래 값으로 복구", + "description": "테스트 후 원래 값으로 복구", "actions": [ - { - "type": "evaluate", - "script": "const dialog = document.querySelector('[role=\"dialog\"]'); if (!dialog) return { error: 'Dialog not found' }; const buttons = Array.from(dialog.querySelectorAll('button')).map(b => b.innerText.trim()); return { buttons, hasCancel: buttons.includes('취소'), hasNext: buttons.includes('다음') };" - } - ] - }, - { - "id": 25, - "name": "다이얼로그 사업자등록번호 입력", - "description": "10자리 사업자등록번호 입력", - "actions": [ - { "type": "fill", "selector": "[role='dialog'] #businessNumber", "value": "1234567890" }, - { "type": "wait", "duration": 300 } - ] - }, - { - "id": 26, - "name": "다이얼로그 다음 버튼 활성화 확인", - "description": "10자리 입력 후 다음 버튼이 활성화되는지 확인", - "actions": [ - { - "type": "evaluate", - "script": "const dialog = document.querySelector('[role=\"dialog\"]'); const nextBtn = Array.from(dialog?.querySelectorAll('button') || []).find(b => b.innerText.includes('다음')); return { disabled: nextBtn?.disabled ?? true, text: nextBtn?.innerText };" - } - ] - }, - { - "id": 27, - "name": "다이얼로그 취소 클릭", - "description": "취소 버튼 클릭하여 다이얼로그 닫기", - "actions": [ - { - "type": "evaluate", - "script": "const dialog = document.querySelector('[role=\"dialog\"]'); const cancelBtn = Array.from(dialog?.querySelectorAll('button') || []).find(b => b.innerText.includes('취소')); cancelBtn?.click(); return { clicked: !!cancelBtn };" - }, - { "type": "wait", "duration": 500 } - ] - }, - { - "id": 28, - "name": "다이얼로그 닫힘 확인", - "description": "다이얼로그가 닫혔는지 확인", - "actions": [ - { - "type": "evaluate", - "script": "const dialog = document.querySelector('[role=\"dialog\"]'); return { dialogClosed: !dialog || dialog.offsetParent === null };" - } - ] - }, - { - "id": 29, - "name": "[CLEANUP] 업태 필드 원래 값 복구", - "phase": "CLEANUP", - "description": "테스트 데이터 정리 - 업태 필드 원복", - "actions": [ - { "type": "click_button", "text": "수정" }, - { "type": "wait", "duration": 500 }, - { "type": "clear", "selector": "#businessType" }, - { "type": "fill", "selector": "#businessType", "value": "업태명" }, - { "type": "click_button", "text": "저장" }, - { "type": "wait", "duration": 2000 } - ] + { "type": "click_if_exists", "target": "수정" }, + { "type": "clear", "target": "업태" }, + { "type": "fill", "target": "업태", "value": "업태명" }, + { "type": "click_if_exists", "target": "저장" } + ], + "expect": { + "toast": ["수정", "완료", "성공", "저장"] + } } ], "notes": [ "직접 URL 접근 금지: 반드시 메뉴 클릭으로 페이지 진입 (404 방지)", "스크롤 필수: 사이드바가 길 경우 메뉴가 화면 밖에 있을 수 있음", - "메뉴 계층: 설정 > 회사정보", - "주요 필드 ID: #companyName, #representativeName, #businessType, #businessCategory, #email, #businessNumber", - "수정 모드: 수정 버튼 클릭 시 필드 활성화, 저장/취소 버튼 표시" + "대체 경로: 메뉴명이 변경되었을 수 있으므로 다양한 이름으로 탐색", + "메뉴 계층: 설정 > 회사정보" ] } diff --git a/draft-box.json b/draft-box.json index 9e9a0d6..ba37992 100644 --- a/draft-box.json +++ b/draft-box.json @@ -3,44 +3,17 @@ "name": "기안함 테스트", "screenshotPolicy": { "onErrorOnly": true, - "captureOn": ["error", "fail", "timeout", "404", "500", "blocked"] + "captureOn": [ + "error", + "fail", + "timeout", + "404", + "500", + "blocked" + ] }, "description": "결재관리 > 기안함 메뉴의 문서 목록 조회, 검색, 필터, 정렬, 문서 상세, 상신, 삭제 기능 테스트", "baseUrl": "https://dev.codebridge-x.com", - "selectors": { - "sidebar": "nav, aside, [role='navigation'], .sidebar, #sidebar, .sidebar-scroll", - "pageTitle": "h1, h2, [class*='page-title'], [class*='PageTitle'], .text-2xl, .text-xl", - "pageDescription": "[class*='description'], [class*='subtitle'], .text-muted-foreground", - "statCards": "[class*='stat-card'], [class*='StatCard'], [class*='card']:has([class*='stat']), .grid > div:has(.text-2xl)", - "statCardItem": "[class*='stat-card'], [class*='card'], .rounded-lg.border", - "searchInput": "input[type='search'], input[placeholder*='검색'], input[name='search'], [class*='search'] input", - "filterSelect": "select[name*='status'], select[name*='filter'], [class*='filter'] select, button[role='combobox']:has-text('전체'), button[role='combobox']:has-text('상태')", - "sortSelect": "select[name*='sort'], [class*='sort'] select, button[role='combobox']:has-text('최신순'), button[role='combobox']:has-text('정렬')", - "dataTable": "table, [role='table'], [class*='table'], [class*='Table']", - "tableHeader": "thead, [role='rowgroup']:first-child, table tr:first-child", - "tableHeaderCell": "th, [role='columnheader']", - "tableBody": "tbody, [role='rowgroup']:last-child", - "tableRow": "tbody tr, [role='row']", - "tableCell": "td, [role='cell']", - "checkbox": "input[type='checkbox'], [role='checkbox'], button[role='checkbox']", - "headerCheckbox": "thead input[type='checkbox'], thead [role='checkbox'], th input[type='checkbox']", - "rowCheckbox": "tbody input[type='checkbox'], tbody [role='checkbox'], td input[type='checkbox']", - "pagination": "[class*='pagination'], [class*='Pagination'], nav[aria-label*='pagination'], .flex:has(button[aria-label*='page'])", - "paginationButton": "[class*='pagination'] button, nav button, button[aria-label*='page']", - "modal": "[role='dialog'], [aria-modal='true'], [class*='modal'], [class*='Modal'], [class*='Dialog']", - "modalCloseBtn": "[role='dialog'] button[class*='close'], [aria-label='닫기'], [aria-label='Close'], [role='dialog'] button:has(svg)", - "createBtn": "button:has-text('문서 작성'), button:has-text('작성'), button:has(svg[class*='plus']), button:has-text('등록')", - "submitBtn": "button:has-text('상신'), button:has(svg[class*='send'])", - "deleteBtn": "button:has-text('삭제'), button[class*='destructive']:has(svg)", - "editBtn": "button:has-text('수정'), button:has(svg[class*='pencil'])", - "copyBtn": "button:has-text('복제'), button:has(svg[class*='copy'])", - "dateRangeSelector": "[class*='date-range'], [class*='DateRange'], input[type='date'], [class*='calendar']", - "badge": "[class*='badge'], [class*='Badge'], span[class*='rounded-full']", - "loadingIndicator": "[class*='loading'], [class*='spinner'], [class*='Spinner'], [aria-busy='true']", - "emptyMessage": "[class*='empty'], [class*='no-data'], td[colspan]:has-text('데이터'), .text-center:has-text('없습니다')", - "toast": "[class*='toast'], [class*='Toast'], [role='alert']", - "actionColumn": "td:last-child, [class*='action']" - }, "testFocus": { "primary": "기안 문서 목록 관리 및 결재 상신 프로세스 검증", "description": "기안함 목록 표시, 통계 카드, 검색/필터/정렬, 체크박스 선택, 상신/삭제 버튼, 문서 상세 모달, 페이지네이션 동작 확인" @@ -48,7 +21,11 @@ "navigation": { "targetUrl": "/approval/draft", "urlPattern": "/approval/draft|/ko/approval/draft", - "menuHints": ["기안함", "기안 함", "결재관리"] + "menuHints": [ + "기안함", + "기안 함", + "결재관리" + ] }, "menuNavigation": { "level1": "결재관리", @@ -61,464 +38,1303 @@ "username": "TestUser5", "password": "password123!" }, + "menuNavigationEnhanced": { + "strategy": "scroll-and-search", + "description": "사이드바를 스크롤하며 메뉴를 찾고 클릭하여 404를 방지", + "level1": "결재관리", + "level2": "기안함", + "alternativeLevel1Names": [ + "결재관리", + "결재 관리", + "Approval", + "전자결재" + ], + "alternativeLevel2Names": [ + "기안함", + "기안 함", + "Draft", + "기안문서", + "내 기안" + ], + "scrollConfig": { + "sidebarSelector": "nav, aside, [role='navigation'], .sidebar, #sidebar", + "menuItemSelector": "a, button, [role='menuitem'], [role='treeitem']", + "scrollStep": 200, + "maxScrollAttempts": 10, + "scrollDelay": 300 + } + }, "prerequisites": { "authentication": true, "testData": { - "description": "결재 문서 데이터가 최소 1개 이상 존재해야 함" + "description": "결재 문서 데이터가 최소 1개 이상 존재해야 함 (다양한 상태: 임시저장, 결재대기, 진행중, 완료, 반려)" } }, + "expectedAPIs": [ + { + "method": "GET", + "endpoint": "/api/v1/approvals/drafts", + "params": "page=1&per_page=20&sort_by=created_at&sort_dir=desc", + "description": "기안함 목록 조회 (페이지네이션, 검색, 필터, 정렬)" + }, + { + "method": "GET", + "endpoint": "/api/v1/approvals/drafts/summary", + "params": "", + "description": "기안함 통계 카드 (전체, 진행, 완료, 반려, 임시저장 건수)" + }, + { + "method": "GET", + "endpoint": "/api/v1/approvals/{id}", + "params": "", + "description": "결재 문서 상세 조회 (content 포함)" + }, + { + "method": "POST", + "endpoint": "/api/v1/approvals/{id}/submit", + "params": "", + "description": "결재 상신" + }, + { + "method": "DELETE", + "endpoint": "/api/v1/approvals/{id}", + "params": "", + "description": "결재 문서 삭제 (임시저장 상태만)" + } + ], "steps": [ { - "id": 0, + "id": "step-0", "name": "사이드바 메뉴 전체 펼치기", - "phase": "SETUP", + "description": "모두 펼치기 버튼을 클릭하여 전체 메뉴를 펼친 후 메뉴 탐색 준비", "actions": [ { - "type": "evaluate", - "script": "document.querySelector('.sidebar-scroll, nav, aside')?.scrollTo({top: 0, behavior: 'instant'})" + "type": "scroll", + "target": "sidebar", + "direction": "top", + "description": "사이드바 최상단으로 스크롤" + }, + { + "type": "wait", + "duration": 300 }, - { "type": "wait", "duration": 300 }, { "type": "evaluate", - "script": "Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('모두 펼치기'))?.click(); 'clicked'" + "script": "Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('모두 펼치기'))?.click()" }, - { "type": "wait", "duration": 1500 } + { + "type": "wait", + "duration": 2000 + } ] }, { - "id": 1, + "id": "step-1", "name": "2단계 메뉴 진입: 결재관리 > 기안함", - "phase": "SETUP", - "critical": true, + "description": "사이드바를 스크롤하며 결재관리 > 기안함 메뉴를 찾아 클릭", "actions": [ { - "type": "menu_navigate", - "level1": "결재관리", - "level2": "기안함", - "alternativeLevel1": ["결재관리", "결재 관리", "Approval", "전자결재"], - "alternativeLevel2": ["기안함", "기안 함", "Draft", "내 기안"] - }, - { "type": "wait", "duration": 2000 }, - { - "type": "verify_url", - "pattern": "/approval/draft", - "timeout": 5000 - } - ] - }, - { - "id": 2, - "name": "페이지 타이틀 확인", - "phase": "READ", - "actions": [ - { - "type": "verify_element", - "selector": "h1, h2, [class*='page-title'], .text-2xl", - "timeout": 3000 + "type": "scrollAndFind", + "target": "결재관리", + "alternativeTexts": [ + "결재관리", + "결재 관리", + "Approval", + "전자결재" + ], + "scrollContainer": "sidebar", + "maxAttempts": 10, + "description": "스크롤하며 결재관리 메뉴 찾기" }, { - "type": "evaluate", - "script": "(() => { const title = document.querySelector('h1, h2, [class*=\"page-title\"], .text-2xl'); return title && title.innerText.includes('기안함') ? 'PASS: 기안함 타이틀 확인' : 'FAIL: 타이틀 없음 또는 불일치'; })()" + "type": "click_if_exists", + "target": "결재관리", + "description": "결재관리 메뉴 클릭" + }, + { + "type": "wait", + "duration": 500, + "description": "서브메뉴 펼쳐지기 대기" + }, + { + "type": "scrollAndFind", + "target": "기안함", + "alternativeTexts": [ + "기안함", + "기안 함", + "Draft", + "내 기안" + ], + "scrollContainer": "submenu", + "maxAttempts": 5, + "description": "서브메뉴에서 기안함 찾기" + }, + { + "type": "click_if_exists", + "target": "기안함", + "description": "기안함 메뉴 클릭" + }, + { + "type": "wait", + "target": "페이지 로드 완료", + "timeout": 10000 } + ], + "expected": { + "url": "/ko/approval/draft", + "pageTitle": "기안함", + "elements": [ + "통계 카드", + "검색바", + "테이블", + "페이지네이션" + ] + }, + "verification": [ + "결재관리 메뉴가 펼쳐졌는지 확인", + "기안함 서브메뉴 클릭 성공", + "404 에러 없이 페이지 로드 완료" ] }, { - "id": 3, - "name": "통계 카드 영역 존재 확인", - "phase": "READ", + "id": "step-2", + "name": "페이지 구조 확인", + "description": "페이지 타이틀, 설명, 통계 카드, 헤더 액션 버튼 확인", "actions": [ { - "type": "evaluate", - "script": "(() => { const cards = document.querySelectorAll('[class*=\"card\"], .rounded-lg.border, [class*=\"stat\"]'); const statTexts = ['진행', '완료', '반려', '임시']; let found = 0; cards.forEach(c => { if (statTexts.some(t => c.innerText?.includes(t))) found++; }); return found >= 2 ? `PASS: 통계 카드 ${found}개 발견` : 'FAIL: 통계 카드 부족'; })()" + "type": "verify", + "target": "페이지 구조" } - ] + ], + "expected": { + "pageTitle": "기안함", + "pageDescription": "작성한 결재 문서를 관리합니다", + "icon": "FileText", + "statCards": [ + "진행", + "완료", + "반려", + "임시 저장" + ], + "headerActions": [ + "날짜 범위 선택", + "문서 작성 버튼" + ] + } }, { - "id": 4, + "id": "step-3", + "name": "통계 카드 표시 확인", + "description": "4개의 통계 카드(진행, 완료, 반려, 임시 저장) 표시 및 건수 확인", + "actions": [ + { + "type": "verify", + "target": "통계 카드" + } + ], + "expected": { + "statCards": [ + { + "label": "진행", + "format": "N건", + "icon": "FileText", + "color": "blue" + }, + { + "label": "완료", + "format": "N건", + "icon": "FileText", + "color": "green" + }, + { + "label": "반려", + "format": "N건", + "icon": "FileText", + "color": "red" + }, + { + "label": "임시 저장", + "format": "N건", + "icon": "FileText", + "color": "gray" + } + ], + "apiCalled": "GET /api/v1/approvals/drafts/summary" + } + }, + { + "id": "step-4", "name": "테이블 컬럼 구조 확인", - "phase": "READ", + "description": "기안함 테이블의 컬럼 헤더 확인 (8개 컬럼)", "actions": [ { - "type": "verify_element", - "selector": "table, [role='table'], [class*='Table']", - "timeout": 3000 - }, - { - "type": "evaluate", - "script": "(() => { const headers = document.querySelectorAll('th, [role=\"columnheader\"]'); const texts = Array.from(headers).map(h => h.innerText?.trim()); const expected = ['번호', '문서번호', '문서유형', '제목', '결재자', '기안일시', '상태']; const found = expected.filter(e => texts.some(t => t?.includes(e))); return found.length >= 4 ? `PASS: 컬럼 ${found.length}개 확인 (${found.join(', ')})` : `FAIL: 컬럼 부족 (발견: ${texts.join(', ')})`; })()" + "type": "verify", + "target": "table columns" } - ] + ], + "expected": { + "columns": [ + "번호", + "문서번호", + "문서유형", + "제목", + "결재자", + "기안일시", + "상태", + "작업" + ], + "hasCheckboxColumn": true + } }, { - "id": 5, - "name": "테이블 데이터 로드 확인", - "phase": "READ", + "id": "step-5", + "name": "데이터 로드 확인", + "description": "기안 문서 데이터가 테이블에 표시되는지 확인", "actions": [ { - "type": "wait_for_element", - "selector": "tbody tr, [role='row'], [class*='empty'], [class*='no-data']", - "timeout": 5000 - }, - { - "type": "evaluate", - "script": "(() => { const rows = document.querySelectorAll('tbody tr, table [role=\"row\"]:not(:first-child)'); const empty = document.querySelector('[class*=\"empty\"], [class*=\"no-data\"], td[colspan]'); if (rows.length > 0) return `PASS: 데이터 ${rows.length}행 로드됨`; if (empty) return 'PASS: 빈 데이터 메시지 표시'; return 'FAIL: 데이터 로드 실패'; })()" + "type": "verify", + "target": "table data" } - ] + ], + "expected": { + "dataExists": "데이터 행 존재 또는 '데이터가 없습니다' 메시지", + "apiCalled": "GET /api/v1/approvals/drafts?page=1&per_page=20", + "defaultSort": "최신순 (created_at desc)", + "defaultFilter": "전체" + } }, { - "id": 6, + "id": "step-6", "name": "문서번호 형식 확인", - "phase": "READ", + "description": "문서번호가 정상적으로 표시되는지 확인", "actions": [ { - "type": "evaluate", - "script": "(() => { const cells = document.querySelectorAll('tbody td, [role=\"cell\"]'); for (const cell of cells) { const text = cell.innerText?.trim(); if (text && /^[A-Z]{2,}-\\d{4}-\\d+$/.test(text)) return `PASS: 문서번호 형식 확인 (${text})`; if (text && text.includes('-') && /\\d{4}/.test(text)) return `PASS: 문서번호 발견 (${text})`; } return 'WARN: 문서번호 형식 확인 불가 (데이터 없거나 다른 형식)'; })()" + "type": "verify", + "target": "document number format" } - ] + ], + "expected": { + "format": "문서번호 형식 (예: DR-2026-001)", + "column": "문서번호" + } }, { - "id": 7, - "name": "상태 뱃지 표시 확인", - "phase": "READ", + "id": "step-7", + "name": "문서유형 뱃지 표시 확인", + "description": "문서유형이 뱃지 형태로 표시되는지 확인", "actions": [ { - "type": "evaluate", - "script": "(() => { const badges = document.querySelectorAll('[class*=\"badge\"], [class*=\"Badge\"], span[class*=\"rounded\"]'); const statusTexts = ['임시저장', '결재대기', '진행중', '완료', '반려', '대기', '진행']; let found = []; badges.forEach(b => { const t = b.innerText?.trim(); if (statusTexts.some(s => t?.includes(s))) found.push(t); }); return found.length > 0 ? `PASS: 상태 뱃지 발견 (${[...new Set(found)].join(', ')})` : 'WARN: 상태 뱃지 없음 (데이터 없을 수 있음)'; })()" + "type": "verify", + "target": "document type badge" } - ] + ], + "expected": { + "displayFormat": "Badge (outline)", + "possibleValues": [ + "품의서", + "지출결의서", + "예상지출내역" + ] + } }, { - "id": 8, - "name": "검색 입력 필드 존재 확인", - "phase": "READ", + "id": "step-8", + "name": "결재자 표시 형식 확인", + "description": "결재자가 '이름 외 N명' 형식으로 표시되는지 확인", "actions": [ { - "type": "verify_element", - "selector": "input[type='search'], input[placeholder*='검색'], input[name='search'], [class*='search'] input", - "timeout": 3000 + "type": "verify", + "target": "approvers format" } - ] + ], + "expected": { + "format": "단일: '홍길동', 복수: '홍길동 외 2명'", + "emptyFormat": "-" + } }, { - "id": 9, + "id": "step-9", + "name": "상태 뱃지 색상 확인", + "description": "문서 상태별로 다른 색상의 뱃지가 표시되는지 확인", + "actions": [ + { + "type": "verify", + "target": "status badge colors" + } + ], + "expected": { + "statusColors": { + "임시저장": "gray", + "결재대기": "yellow", + "진행중": "blue", + "완료": "green", + "반려": "red" + } + } + }, + { + "id": "step-10", "name": "검색 기능 테스트", - "phase": "READ", + "description": "검색바에 키워드 입력 후 필터링 확인", "actions": [ { - "type": "fill", - "selector": "input[type='search'], input[placeholder*='검색'], input[name='search'], [class*='search'] input", + "type": "click_if_exists", + "target": "검색 입력 필드", "value": "테스트" }, - { "type": "wait", "duration": 1500 }, { - "type": "evaluate", - "script": "(() => { const input = document.querySelector('input[type=\"search\"], input[placeholder*=\"검색\"]'); return input && input.value === '테스트' ? 'PASS: 검색어 입력 완료' : 'FAIL: 검색어 입력 실패'; })()" + "type": "wait", + "target": "검색 결과 로드" } - ] + ], + "expected": { + "searchPlaceholder": "문서번호, 제목, 기안자 검색...", + "apiCalled": "GET /api/v1/approvals/drafts?search=테스트", + "dataFiltered": "검색어 포함된 문서만 표시", + "pageReset": "1페이지로 초기화" + } }, { - "id": 10, + "id": "step-11", "name": "검색어 초기화", - "phase": "READ", + "description": "검색어를 지우고 전체 목록으로 복귀", "actions": [ { - "type": "clear", - "selector": "input[type='search'], input[placeholder*='검색'], input[name='search']" + "type": "click_if_exists", + "target": "검색 입력 필드" }, - { "type": "wait", "duration": 1000 } - ] - }, - { - "id": 11, - "name": "필터 드롭다운 존재 확인", - "phase": "READ", - "actions": [ { - "type": "evaluate", - "script": "(() => { const selects = document.querySelectorAll('select, button[role=\"combobox\"], [class*=\"select\"] button'); for (const s of selects) { const text = s.innerText || s.value || ''; if (['전체', '상태', '임시저장', '결재대기'].some(t => text.includes(t))) return `PASS: 필터 드롭다운 발견 (${text.substring(0,20)})`; } return 'WARN: 필터 드롭다운 미발견 (다른 UI일 수 있음)'; })()" + "type": "wait", + "target": "데이터 로드" } - ] + ], + "expected": { + "dataRestored": "전체 목록 표시", + "apiCalled": "GET /api/v1/approvals/drafts?page=1" + } }, { - "id": 12, - "name": "정렬 드롭다운 존재 확인", - "phase": "READ", + "id": "step-12", + "name": "필터 셀렉트박스 존재 확인", + "description": "상태 필터 드롭다운이 표시되는지 확인", "actions": [ { - "type": "evaluate", - "script": "(() => { const selects = document.querySelectorAll('select, button[role=\"combobox\"], [class*=\"select\"] button'); for (const s of selects) { const text = s.innerText || s.value || ''; if (['최신순', '오래된순', '정렬', '제목'].some(t => text.includes(t))) return `PASS: 정렬 드롭다운 발견 (${text.substring(0,20)})`; } return 'WARN: 정렬 드롭다운 미발견 (다른 UI일 수 있음)'; })()" + "type": "verify", + "target": "filter select" } - ] + ], + "expected": { + "selectExists": true, + "defaultValue": "전체", + "options": [ + "전체", + "임시저장", + "결재대기", + "진행중", + "완료", + "반려" + ] + } }, { - "id": 13, - "name": "체크박스 존재 확인 (헤더)", - "phase": "READ", + "id": "step-13", + "name": "필터 적용 테스트 (임시저장)", + "description": "필터를 '임시저장'으로 변경하여 필터링 확인", "actions": [ { - "type": "evaluate", - "script": "(() => { const headerCb = document.querySelector('thead input[type=\"checkbox\"], thead [role=\"checkbox\"], th input[type=\"checkbox\"], th button[role=\"checkbox\"]'); return headerCb ? 'PASS: 헤더 체크박스 발견' : 'WARN: 헤더 체크박스 미발견'; })()" - } - ] - }, - { - "id": 14, - "name": "체크박스 존재 확인 (행)", - "phase": "READ", - "actions": [ - { - "type": "evaluate", - "script": "(() => { const rowCbs = document.querySelectorAll('tbody input[type=\"checkbox\"], tbody [role=\"checkbox\"], td input[type=\"checkbox\"], td button[role=\"checkbox\"]'); return rowCbs.length > 0 ? `PASS: 행 체크박스 ${rowCbs.length}개 발견` : 'WARN: 행 체크박스 미발견 (데이터 없을 수 있음)'; })()" - } - ] - }, - { - "id": 15, - "name": "첫 번째 행 체크박스 클릭", - "phase": "READ", - "actions": [ - { - "type": "evaluate", - "script": "(() => { const cb = document.querySelector('tbody input[type=\"checkbox\"], tbody [role=\"checkbox\"], tbody button[role=\"checkbox\"]'); if (cb) { cb.click(); return 'PASS: 체크박스 클릭'; } return 'SKIP: 체크박스 없음'; })()" + "type": "click_if_exists", + "target": "필터 셀렉트박스", + "value": "임시저장" }, - { "type": "wait", "duration": 500 } - ] + { + "type": "wait", + "target": "데이터 로드" + } + ], + "expected": { + "apiCalled": "GET /api/v1/approvals/drafts?status=draft", + "dataFiltered": "임시저장 상태만 표시", + "pageReset": "1페이지로 초기화" + } }, { - "id": 16, - "name": "선택 시 액션 버튼 표시 확인", - "phase": "READ", + "id": "step-14", + "name": "필터 초기화", + "description": "필터를 '전체'로 변경하여 전체 목록 표시", "actions": [ { - "type": "evaluate", - "script": "(() => { const btns = document.querySelectorAll('button'); let found = []; btns.forEach(b => { const t = b.innerText?.trim(); if (['상신', '삭제'].some(a => t?.includes(a))) found.push(t); }); return found.length > 0 ? `PASS: 액션 버튼 발견 (${found.join(', ')})` : 'WARN: 액션 버튼 미표시 (선택 상태 또는 권한에 따라 다를 수 있음)'; })()" + "type": "click_if_exists", + "target": "필터 셀렉트박스", + "value": "전체" + }, + { + "type": "wait", + "target": "데이터 로드" } - ] + ], + "expected": { + "apiCalled": "GET /api/v1/approvals/drafts?page=1", + "dataRestored": "전체 상태 표시" + } }, { - "id": 17, + "id": "step-15", + "name": "정렬 셀렉트박스 존재 확인", + "description": "정렬 옵션 드롭다운이 표시되는지 확인", + "actions": [ + { + "type": "verify", + "target": "sort select" + } + ], + "expected": { + "selectExists": true, + "defaultValue": "최신순", + "options": [ + "최신순", + "오래된순", + "제목 오름차순", + "제목 내림차순" + ] + } + }, + { + "id": "step-16", + "name": "정렬 변경 테스트 (제목 오름차순)", + "description": "정렬을 '제목 오름차순'으로 변경", + "actions": [ + { + "type": "click_if_exists", + "target": "정렬 셀렉트박스", + "value": "제목 오름차순" + }, + { + "type": "wait", + "target": "데이터 로드" + } + ], + "expected": { + "apiCalled": "GET /api/v1/approvals/drafts?sort_by=title&sort_dir=asc", + "dataSorted": "제목 알파벳 순서로 정렬", + "pageReset": "1페이지로 초기화" + } + }, + { + "id": "step-17", + "name": "정렬 초기화", + "description": "정렬을 '최신순'으로 복귀", + "actions": [ + { + "type": "click_if_exists", + "target": "정렬 셀렉트박스", + "value": "최신순" + }, + { + "type": "wait", + "target": "데이터 로드" + } + ], + "expected": { + "apiCalled": "GET /api/v1/approvals/drafts?sort_by=created_at&sort_dir=desc", + "dataRestored": "최신순 정렬" + } + }, + { + "id": "step-18", + "name": "체크박스 선택 (단일)", + "description": "첫 번째 문서의 체크박스 선택", + "actions": [ + { + "type": "click_if_exists", + "target": "첫 번째 행 체크박스" + } + ], + "expected": { + "checkboxChecked": true, + "selectedCount": 1, + "actionButtonsVisible": "상신, 삭제 버튼 표시" + } + }, + { + "id": "step-19", + "name": "임시저장 문서 수정/삭제 버튼 표시 확인", + "description": "임시저장 상태 문서 선택 시 작업 컬럼에 수정/삭제 버튼 표시", + "actions": [ + { + "type": "verify", + "target": "action buttons for draft status" + } + ], + "expected": { + "condition": "status === 'draft' && isSelected", + "buttonsVisible": [ + "수정 (Pencil 아이콘)", + "삭제 (Trash2 아이콘)" + ], + "buttonColors": { + "수정": "gray", + "삭제": "red" + } + } + }, + { + "id": "step-20", "name": "체크박스 해제", - "phase": "READ", + "description": "선택한 체크박스를 다시 클릭하여 해제", "actions": [ { - "type": "evaluate", - "script": "(() => { const cb = document.querySelector('tbody input[type=\"checkbox\"]:checked, tbody [role=\"checkbox\"][data-state=\"checked\"], tbody button[role=\"checkbox\"][aria-checked=\"true\"]'); if (cb) { cb.click(); return 'PASS: 체크박스 해제'; } return 'SKIP: 선택된 체크박스 없음'; })()" - }, - { "type": "wait", "duration": 300 } - ] - }, - { - "id": 18, - "name": "문서 작성 버튼 존재 확인", - "phase": "READ", - "actions": [ - { - "type": "evaluate", - "script": "(() => { const btns = document.querySelectorAll('button, a'); for (const b of btns) { const t = b.innerText?.trim(); if (t && ['문서 작성', '작성', '새 문서', '등록'].some(k => t.includes(k))) return `PASS: 문서 작성 버튼 발견 (${t})`; } return 'WARN: 문서 작성 버튼 미발견'; })()" + "type": "click_if_exists", + "target": "첫 번째 행 체크박스" } - ] + ], + "expected": { + "checkboxChecked": false, + "selectedCount": 0, + "actionButtonsHidden": "상신, 삭제 버튼 숨김" + } }, { - "id": 19, - "name": "날짜 범위 선택기 존재 확인", - "phase": "READ", - "actions": [ - { - "type": "evaluate", - "script": "(() => { const dateInputs = document.querySelectorAll('input[type=\"date\"], [class*=\"date\"], [class*=\"calendar\"], button:has-text(\"2025\"), button:has-text(\"2026\")'); return dateInputs.length > 0 ? `PASS: 날짜 선택기 ${dateInputs.length}개 발견` : 'WARN: 날짜 선택기 미발견'; })()" - } - ] - }, - { - "id": 20, - "name": "페이지네이션 존재 확인", - "phase": "READ", - "actions": [ - { - "type": "evaluate", - "script": "(() => { const pag = document.querySelector('[class*=\"pagination\"], [class*=\"Pagination\"], nav[aria-label*=\"pagination\"], .flex:has(button[aria-label*=\"page\"])'); if (pag) return 'PASS: 페이지네이션 발견'; const pageInfo = document.body.innerText.match(/(\\d+)\\s*[/~-]\\s*(\\d+)\\s*(페이지|page)/i); if (pageInfo) return `PASS: 페이지 정보 발견 (${pageInfo[0]})`; return 'WARN: 페이지네이션 미발견 (1페이지만 있을 수 있음)'; })()" - } - ] - }, - { - "id": 21, - "name": "테이블 행 클릭 - 문서 상세 모달 열기", - "phase": "READ", - "actions": [ - { - "type": "evaluate", - "script": "(() => { const row = document.querySelector('tbody tr:not(:has(td[colspan])), table [role=\"row\"]:not(:first-child)'); if (row) { row.click(); return 'PASS: 테이블 행 클릭'; } return 'SKIP: 클릭할 행 없음'; })()" - }, - { "type": "wait", "duration": 1500 } - ] - }, - { - "id": 22, - "name": "모달 열림 확인", - "phase": "READ", - "actions": [ - { - "type": "evaluate", - "script": "(() => { const modal = document.querySelector('[role=\"dialog\"], [aria-modal=\"true\"], [class*=\"modal\"][class*=\"open\"], [class*=\"Modal\"], [class*=\"Dialog\"], [data-state=\"open\"]'); if (modal && modal.offsetParent !== null) return 'PASS: 모달 열림 확인'; return 'WARN: 모달 미열림 (임시저장 문서는 수정 페이지로 이동할 수 있음)'; })()" - } - ] - }, - { - "id": 23, - "name": "모달 내용 확인", - "phase": "READ", - "actions": [ - { - "type": "evaluate", - "script": "(() => { const modal = document.querySelector('[role=\"dialog\"], [aria-modal=\"true\"], [class*=\"Modal\"], [class*=\"Dialog\"]'); if (!modal) return 'SKIP: 모달 없음'; const text = modal.innerText || ''; const checks = ['문서번호', '기안', '결재', '제목'].filter(k => text.includes(k)); return checks.length >= 2 ? `PASS: 모달 내용 확인 (${checks.join(', ')})` : 'WARN: 모달 내용 부족'; })()" - } - ] - }, - { - "id": 24, - "name": "모달 내 버튼 확인 (수정/복제)", - "phase": "READ", - "actions": [ - { - "type": "evaluate", - "script": "(() => { const modal = document.querySelector('[role=\"dialog\"], [aria-modal=\"true\"], [class*=\"Modal\"]'); if (!modal) return 'SKIP: 모달 없음'; const btns = modal.querySelectorAll('button'); const found = []; btns.forEach(b => { const t = b.innerText?.trim(); if (['수정', '복제', '상신', '닫기'].some(k => t?.includes(k))) found.push(t); }); return found.length > 0 ? `PASS: 모달 버튼 발견 (${found.join(', ')})` : 'WARN: 모달 버튼 미발견'; })()" - } - ] - }, - { - "id": 25, - "name": "모달 닫기", - "phase": "READ", - "actions": [ - { - "type": "close_modal" - }, - { "type": "wait", "duration": 500 } - ] - }, - { - "id": 26, - "name": "모달 닫힘 확인", - "phase": "READ", - "actions": [ - { - "type": "evaluate", - "script": "(() => { const modal = document.querySelector('[role=\"dialog\"][data-state=\"open\"], [aria-modal=\"true\"]:not([hidden])'); if (!modal || modal.offsetParent === null) return 'PASS: 모달 닫힘 확인'; return 'WARN: 모달이 아직 열려있음'; })()" - } - ] - }, - { - "id": 27, + "id": "step-21", "name": "전체 선택 체크박스 클릭", - "phase": "READ", + "description": "테이블 헤더의 전체 선택 체크박스 클릭", "actions": [ { - "type": "evaluate", - "script": "(() => { const headerCb = document.querySelector('thead input[type=\"checkbox\"], thead [role=\"checkbox\"], th input[type=\"checkbox\"], th button[role=\"checkbox\"]'); if (headerCb) { headerCb.click(); return 'PASS: 전체 선택 체크박스 클릭'; } return 'SKIP: 전체 선택 체크박스 없음'; })()" - }, - { "type": "wait", "duration": 500 } - ] - }, - { - "id": 28, - "name": "전체 선택 결과 확인", - "phase": "READ", - "actions": [ - { - "type": "evaluate", - "script": "(() => { const checkedCbs = document.querySelectorAll('tbody input[type=\"checkbox\"]:checked, tbody [role=\"checkbox\"][data-state=\"checked\"], tbody button[role=\"checkbox\"][aria-checked=\"true\"]'); return checkedCbs.length > 0 ? `PASS: ${checkedCbs.length}개 행 선택됨` : 'WARN: 선택된 행 없음'; })()" + "type": "click_if_exists", + "target": "헤더 체크박스 (전체 선택)" } - ] + ], + "expected": { + "allCheckboxesChecked": true, + "selectedCount": "현재 페이지의 모든 행 수", + "actionButtonsVisible": "상신, 삭제 버튼 표시" + } }, { - "id": 29, + "id": "step-22", "name": "전체 선택 해제", - "phase": "READ", + "description": "전체 선택 체크박스를 다시 클릭하여 모두 해제", "actions": [ { - "type": "evaluate", - "script": "(() => { const headerCb = document.querySelector('thead input[type=\"checkbox\"]:checked, thead [role=\"checkbox\"][data-state=\"checked\"], thead button[role=\"checkbox\"][aria-checked=\"true\"]'); if (headerCb) { headerCb.click(); return 'PASS: 전체 선택 해제'; } return 'SKIP: 체크된 전체 선택 없음'; })()" - }, - { "type": "wait", "duration": 300 } - ] + "type": "click_if_exists", + "target": "헤더 체크박스 (전체 선택)" + } + ], + "expected": { + "allCheckboxesUnchecked": true, + "selectedCount": 0, + "actionButtonsHidden": "상신, 삭제 버튼 숨김" + } }, { - "id": 30, + "id": "step-23", + "name": "문서 작성 버튼 확인", + "description": "헤더 액션에 '문서 작성' 버튼이 표시되는지 확인", + "actions": [ + { + "type": "verify", + "target": "문서 작성 버튼" + } + ], + "expected": { + "buttonExists": true, + "buttonText": "문서 작성", + "icon": "Plus" + } + }, + { + "id": "step-24", + "name": "문서 클릭 (임시저장)", + "description": "임시저장 상태의 문서 행 클릭 (수정 모드로 이동)", + "actions": [ + { + "type": "click_if_exists", + "target": "임시저장 상태의 문서 행" + } + ], + "expected": { + "urlChange": "/ko/approval/draft/new?id={id}&mode=edit", + "behavior": "문서 작성 페이지로 이동 (수정 모드)" + } + }, + { + "id": "step-25", + "name": "기안함으로 복귀", + "description": "문서 작성 페이지에서 기안함으로 돌아오기", + "actions": [ + { + "type": "navigate", + "target": "/ko/approval/draft" + } + ], + "expected": { + "url": "/ko/approval/draft", + "dataReloaded": "목록 재로드" + } + }, + { + "id": "step-26", + "name": "문서 클릭 (결재대기/진행중/완료)", + "description": "임시저장이 아닌 문서 행 클릭 (상세 모달 오픈)", + "actions": [ + { + "type": "click_if_exists", + "target": "결재대기/진행중/완료 상태의 문서 행" + }, + { + "type": "wait", + "target": "모달 오픈 및 상세 데이터 로드" + } + ], + "expected": { + "modalOpened": true, + "apiCalled": "GET /api/v1/approvals/{id}", + "modalTitle": "문서 상세" + } + }, + { + "id": "step-27", + "name": "문서 상세 모달 구조 확인", + "description": "문서 상세 모달의 구조 및 내용 확인", + "actions": [ + { + "type": "verify", + "target": "document detail modal" + } + ], + "expected": { + "modalContent": [ + "문서번호", + "기안일시", + "결재자 목록 (최대 3명)", + "문서 내용 (문서 유형에 따라 다름)" + ], + "documentTypes": [ + "품의서 (proposal)", + "지출결의서 (expenseReport)", + "예상지출내역 (expenseEstimate)" + ] + } + }, + { + "id": "step-28", + "name": "모달 수정 버튼 확인", + "description": "모달 하단에 수정 버튼이 표시되는지 확인", + "actions": [ + { + "type": "verify", + "target": "modal edit button" + } + ], + "expected": { + "buttonExists": true, + "buttonText": "수정", + "behavior": "클릭 시 문서 작성 페이지로 이동 (수정 모드)" + } + }, + { + "id": "step-29", + "name": "모달 복제 버튼 확인", + "description": "모달 하단에 복제 버튼이 표시되는지 확인", + "actions": [ + { + "type": "verify", + "target": "modal copy button" + } + ], + "expected": { + "buttonExists": true, + "buttonText": "복제", + "behavior": "클릭 시 문서 작성 페이지로 이동 (복제 모드, copyFrom 파라미터)" + } + }, + { + "id": "step-30", + "name": "모달 상신 버튼 확인 (임시저장 시)", + "description": "임시저장 문서의 모달에서 상신 버튼 확인", + "actions": [ + { + "type": "verify", + "target": "modal submit button" + } + ], + "expected": { + "buttonExists": "임시저장 상태일 때만", + "buttonText": "상신", + "behavior": "클릭 시 결재 상신 (POST /api/v1/approvals/{id}/submit)" + } + }, + { + "id": "step-31", + "name": "모달 닫기", + "description": "문서 상세 모달을 닫기", + "actions": [ + { + "type": "click_if_exists", + "target": "모달 외부 또는 닫기 버튼" + } + ], + "expected": { + "modalClosed": true, + "returnToList": "기안함 목록으로 복귀" + } + }, + { + "id": "step-31-pdf-1", + "name": "⚠️ 필수 검증: PDF 다운로드 전 모달 스크린샷", + "description": "PDF 생성 전 모달 상태를 스크린샷으로 캡처하여 CSS 문제 감지용 기준 이미지 확보", + "prerequisite": "step-26의 문서 상세 모달이 열려있는 상태에서 실행", + "actions": [ + { + "type": "click_if_exists", + "target": "결재대기/진행중/완료 상태의 문서 행", + "description": "모달 다시 열기" + }, + { + "type": "wait", + "duration": 1000, + "description": "모달 로드 대기" + }, + { + "type": "screenshot", + "name": "pdf-preview-before-download-draft-box", + "fullPage": false, + "selector": "[role='dialog'], .modal, [data-state='open']", + "savePath": "tests/e2e/results/hotfix/screenshots/", + "description": "PDF 생성 대상 모달 전체 캡처" + } + ], + "verify": { + "screenshotCaptured": true, + "purpose": "PDF CSS 문제 감지를 위한 기준 이미지" + } + }, + { + "id": "step-31-pdf-2", + "name": "⚠️ 필수 검증: PDF 다운로드 실행 및 파일 보관", + "description": "PDF 다운로드 후 파일을 지정 폴더에 보관하여 수동 검증 가능하게 함", + "actions": [ + { + "type": "verify", + "target": "PDF 버튼 존재", + "selector": "button:has-text('PDF'), [aria-label*='PDF']", + "description": "PDF 다운로드 버튼 존재 확인" + }, + { + "type": "expectResponse", + "id": "pdf-download-response-draft-box", + "urlPattern": "/api/v1/approvals/*/pdf", + "description": "PDF 다운로드 API 응답 대기 설정" + }, + { + "type": "click_if_exists", + "target": "PDF 버튼", + "selector": "button:has-text('PDF')", + "description": "PDF 다운로드 버튼 클릭" + }, + { + "type": "wait", + "duration": 3000, + "description": "PDF 생성 및 다운로드 대기" + }, + { + "type": "assertResponse", + "id": "pdf-download-response-draft-box", + "checks": { + "status": 200, + "contentType": "application/pdf" + } + }, + { + "type": "saveDownloadedFile", + "targetPath": "tests/e2e/results/hotfix/pdf-samples/", + "fileNamePattern": "draft-box-{timestamp}.pdf", + "description": "다운로드된 PDF 파일을 지정 폴더에 보관" + } + ], + "verify": { + "apiSuccess": true, + "fileDownloaded": true, + "fileSaved": "tests/e2e/results/hotfix/pdf-samples/" + } + }, + { + "id": "step-31-pdf-3", + "name": "⚠️ PDF 파일 유효성 검증", + "description": "다운로드된 PDF 파일의 기본 유효성 검사", + "actions": [ + { + "type": "verifyDownloadedFile", + "checks": { + "fileExists": true, + "fileSize": "> 1024", + "pdfSignature": "%PDF-", + "description": "PDF 파일 헤더 검증" + } + } + ], + "verify": { + "pdfValid": true, + "minFileSize": "1KB 이상" + } + }, + { + "id": "step-31-pdf-4", + "name": "📋 PDF 스타일 수동 확인 체크리스트", + "type": "manualVerification", + "description": "개발자가 다운로드된 PDF를 열어 시각적으로 확인해야 하는 항목", + "manualChecklist": [ + { + "id": "css-1", + "item": "테이블 경계선이 올바르게 표시되는가?", + "category": "테이블 스타일" + }, + { + "id": "css-2", + "item": "한글 폰트가 깨지지 않고 정상 표시되는가?", + "category": "폰트" + }, + { + "id": "css-3", + "item": "숫자/금액 정렬이 올바른가? (우측 정렬)", + "category": "정렬" + }, + { + "id": "css-4", + "item": "여백(margin/padding)이 적절한가?", + "category": "레이아웃" + }, + { + "id": "css-5", + "item": "헤더/푸터가 각 페이지에 올바르게 표시되는가?", + "category": "페이지 구조" + }, + { + "id": "css-6", + "item": "로고/이미지가 정상 표시되는가?", + "category": "이미지" + }, + { + "id": "css-7", + "item": "페이지 나눔(page break)이 적절한 위치에서 발생하는가?", + "category": "페이지 나눔" + }, + { + "id": "css-8", + "item": "배경색/강조색이 올바르게 적용되었는가?", + "category": "색상" + }, + { + "id": "css-9", + "item": "텍스트가 잘리거나 겹치지 않는가?", + "category": "오버플로우" + }, + { + "id": "css-10", + "item": "결재선 정보가 정상적으로 표시되는가?", + "category": "결재선" + } + ], + "outputFiles": { + "screenshot": "tests/e2e/results/hotfix/screenshots/pdf-preview-before-download-draft-box-*.png", + "pdfFile": "tests/e2e/results/hotfix/pdf-samples/draft-box-*.pdf" + }, + "reportFlag": { + "requiresManualReview": true, + "message": "⚠️ PDF 스타일 수동 확인 필요 - 위 체크리스트 항목을 PDF 파일에서 직접 확인하세요" + } + }, + { + "id": "step-31-pdf-5", + "name": "모달 닫기 (PDF 테스트 후)", + "description": "PDF 테스트 완료 후 모달 닫기", + "actions": [ + { + "type": "click_if_exists", + "target": "모달 외부 또는 닫기 버튼" + } + ], + "expected": { + "modalClosed": true + } + }, + { + "id": "step-32", + "name": "날짜 범위 선택 확인", + "description": "헤더 액션에 날짜 범위 선택 컴포넌트가 표시되는지 확인", + "actions": [ + { + "type": "verify", + "target": "date range selector" + } + ], + "expected": { + "componentExists": true, + "defaultStartDate": "2025-01-01", + "defaultEndDate": "2025-12-31", + "inputs": [ + "시작일 입력", + "종료일 입력" + ] + } + }, + { + "id": "step-33", + "name": "페이지네이션 존재 확인", + "description": "테이블 하단에 페이지네이션이 표시되는지 확인", + "actions": [ + { + "type": "verify", + "target": "pagination component" + } + ], + "expected": { + "paginationExists": true, + "showsCurrentPage": "현재 페이지 번호", + "showsTotalPages": "전체 페이지 수", + "showsTotalItems": "전체 항목 수", + "itemsPerPage": 20 + } + }, + { + "id": "step-34", + "name": "페이지네이션 이동 테스트", + "description": "2페이지가 있는 경우 페이지 이동 테스트", + "actions": [ + { + "type": "click_if_exists", + "target": "페이지 2 버튼 (또는 다음 버튼)" + }, + { + "type": "wait", + "target": "데이터 로드" + } + ], + "expected": { + "currentPage": 2, + "apiCalled": "GET /api/v1/approvals/drafts?page=2", + "dataChanged": "2페이지 데이터 표시", + "scrollToTop": "페이지 상단으로 스크롤" + } + }, + { + "id": "step-35", + "name": "1페이지로 복귀", + "description": "페이지네이션에서 1페이지로 이동", + "actions": [ + { + "type": "click_if_exists", + "target": "페이지 1 버튼" + }, + { + "type": "wait", + "target": "데이터 로드" + } + ], + "expected": { + "currentPage": 1, + "apiCalled": "GET /api/v1/approvals/drafts?page=1", + "dataRestored": "1페이지 데이터 표시" + } + }, + { + "id": "step-36", "name": "테이블 hover 효과 확인", - "phase": "READ", + "description": "테이블 행에 마우스 오버 시 배경색 변경 확인", "actions": [ { "type": "hover", - "selector": "tbody tr:first-child, table [role='row']:nth-child(2)" - }, - { - "type": "evaluate", - "script": "(() => { const row = document.querySelector('tbody tr:first-child'); if (row) { const style = getComputedStyle(row); return 'PASS: hover 동작 확인 (시각적 효과는 스크린샷으로 확인)'; } return 'SKIP: hover 대상 없음'; })()" + "target": "첫 번째 테이블 행" } - ] + ], + "expected": { + "hoverEffect": "hover:bg-muted/50", + "backgroundChanges": true, + "cursorPointer": true + } }, { - "id": 31, - "name": "빈 검색 결과 테스트", - "phase": "READ", + "id": "step-37", + "name": "로딩 상태 확인", + "description": "데이터 로드 중 로딩 인디케이터 표시 확인", "actions": [ { - "type": "fill", - "selector": "input[type='search'], input[placeholder*='검색'], input[name='search']", - "value": "ZZZZNOTEXIST99999" - }, - { "type": "wait", "duration": 1500 }, - { - "type": "evaluate", - "script": "(() => { const empty = document.querySelector('[class*=\"empty\"], [class*=\"no-data\"], td[colspan]'); const rows = document.querySelectorAll('tbody tr:not(:has(td[colspan]))'); if (empty || rows.length === 0) return 'PASS: 빈 결과 표시 확인'; return 'WARN: 빈 결과 메시지 미표시'; })()" + "type": "verify", + "target": "loading state" } - ] + ], + "expected": { + "loadingIndicator": "스피너 또는 로딩 메시지", + "isLoading": "true during data fetch" + } }, { - "id": 32, - "name": "검색어 최종 초기화", - "phase": "READ", + "id": "step-38", + "name": "빈 상태 메시지 확인", + "description": "검색/필터 결과가 없을 때 빈 상태 메시지 표시", "actions": [ { - "type": "clear", - "selector": "input[type='search'], input[placeholder*='검색'], input[name='search']" + "type": "click_if_exists", + "target": "검색 입력 필드", + "value": "존재하지않는문서번호999999" }, - { "type": "wait", "duration": 1000 } - ] + { + "type": "wait", + "target": "검색 결과" + } + ], + "expected": { + "emptyMessage": "데이터가 없습니다.", + "messagePosition": "테이블 중앙" + } }, { - "id": 33, + "id": "step-39", + "name": "검색어 초기화 (빈 상태 해제)", + "description": "검색어를 지워서 전체 목록으로 복귀", + "actions": [ + { + "type": "click_if_exists", + "target": "검색 입력 필드" + }, + { + "type": "wait", + "target": "데이터 로드" + } + ], + "expected": { + "dataRestored": "전체 목록 표시" + } + }, + { + "id": "step-40", "name": "콘솔 에러 확인", - "phase": "READ", + "description": "페이지 동작 중 콘솔에 에러가 발생하지 않는지 확인", "actions": [ { - "type": "evaluate", - "script": "(() => { return 'PASS: 콘솔 에러 확인 (step-executor API 로그에서 확인)'; })()" + "type": "verify", + "target": "console errors" } - ] + ], + "expected": { + "noErrors": "콘솔 에러 없음", + "warningsAcceptable": "경고는 허용" + } }, { - "id": 34, - "name": "API 호출 요약 확인", - "phase": "READ", + "id": "step-41", + "name": "반응형 레이아웃 확인", + "description": "모바일 뷰에서 카드 레이아웃으로 표시되는지 확인", "actions": [ { - "type": "evaluate", - "script": "(() => { const logs = window.__API_LOGS__ || []; const approvalLogs = logs.filter(l => l.url?.includes('approval')); return approvalLogs.length > 0 ? `PASS: API 호출 ${approvalLogs.length}건 (approval 관련)` : 'WARN: approval API 호출 미감지'; })()" + "type": "verify", + "target": "mobile card layout" } - ] + ], + "expected": { + "mobileCardExists": "화면 크기에 따라", + "cardTitle": "문서 제목", + "cardFields": [ + "문서번호", + "기안일자", + "기안자", + "결재자" + ] + } }, { - "id": 35, - "name": "최종 URL 확인", - "phase": "READ", + "id": "step-42", + "name": "모바일 카드 액션 버튼 확인", + "description": "모바일 카드에서 임시저장 문서 선택 시 수정/삭제 버튼 표시", "actions": [ { - "type": "verify_url", - "pattern": "/approval/draft", - "timeout": 3000 + "type": "verify", + "target": "mobile card actions" } - ] + ], + "expected": { + "condition": "status === 'draft' && isSelected", + "buttons": [ + "수정", + "삭제" + ], + "buttonIcons": [ + "Pencil", + "Trash2" + ] + } + }, + { + "id": "step-43", + "name": "통계 카드 실시간 업데이트 확인", + "description": "문서 상신/삭제 후 통계 카드가 업데이트되는지 확인", + "actions": [ + { + "type": "verify", + "target": "stat cards update after action" + } + ], + "expected": { + "updateTriggers": [ + "상신 성공", + "삭제 성공" + ], + "apiCalled": "GET /api/v1/approvals/drafts/summary" + } + }, + { + "id": "step-44", + "name": "IntegratedListTemplateV2 사용 확인", + "description": "IntegratedListTemplateV2 템플릿 컴포넌트 사용 확인", + "actions": [ + { + "type": "verify", + "target": "template component" + } + ], + "expected": { + "templateComponent": "IntegratedListTemplateV2", + "responsive": "모바일/데스크톱 대응" + } + }, + { + "id": "step-45", + "name": "상신 버튼 조건부 표시 확인", + "description": "항목 선택 시에만 상신 버튼이 표시되는지 확인", + "actions": [ + { + "type": "verify", + "target": "submit button visibility" + } + ], + "expected": { + "condition": "selectedItems.size > 0", + "buttonVisible": true, + "buttonText": "상신", + "icon": "Send" + } + }, + { + "id": "step-46", + "name": "삭제 버튼 조건부 표시 확인", + "description": "항목 선택 시에만 삭제 버튼이 표시되는지 확인", + "actions": [ + { + "type": "verify", + "target": "delete button visibility" + } + ], + "expected": { + "condition": "selectedItems.size > 0", + "buttonVisible": true, + "buttonText": "삭제", + "icon": "Trash2", + "variant": "destructive (red)" + } + }, + { + "id": "step-47", + "name": "결재자 상태 뱃지 확인", + "description": "모달 내 결재자 목록의 상태 뱃지 색상 확인", + "actions": [ + { + "type": "verify", + "target": "approver status badges in modal" + } + ], + "expected": { + "statusColors": { + "none": "gray", + "pending": "yellow", + "approved": "green", + "rejected": "red" + } + } + }, + { + "id": "step-48", + "name": "문서 유형별 모달 내용 확인", + "description": "문서 유형(품의서, 지출결의서, 예상지출내역)에 따라 다른 내용 표시", + "actions": [ + { + "type": "verify", + "target": "document type specific content" + } + ], + "expected": { + "proposal": [ + "거래처", + "거래처 지급일", + "제목", + "내용", + "사유", + "예상금액", + "첨부파일" + ], + "expenseReport": [ + "신청일", + "지급일", + "지출 내역", + "카드 정보", + "총액", + "첨부파일" + ], + "expenseEstimate": [ + "예상지급일", + "카테고리", + "금액", + "거래처", + "계좌", + "총 지출", + "계좌 잔액", + "최종 차액" + ] + } + }, + { + "id": "step-49", + "name": "API 응답 구조 확인", + "description": "기안함 목록 API 응답이 올바른 구조인지 확인", + "actions": [ + { + "type": "verify", + "target": "API response structure" + } + ], + "expected": { + "responseStructure": { + "success": true, + "data": { + "current_page": "number", + "data": "Array", + "total": "number", + "per_page": "number", + "last_page": "number" + } + } + } + }, + { + "id": "step-50", + "name": "데이터 변환 확인", + "description": "API 데이터가 프론트엔드 형식으로 변환되는지 확인", + "actions": [ + { + "type": "verify", + "target": "data transformation" + } + ], + "expected": { + "apiFormat": "snake_case (document_number, created_at, approval_steps)", + "frontendFormat": "camelCase (documentNo, draftDate, approvers)", + "transformFunction": "transformApiToFrontend", + "statusMapping": { + "draft": "draft", + "pending": "pending", + "in_progress": "inProgress", + "approved": "approved", + "rejected": "rejected" + } + } } ], "cleanup": { @@ -534,6 +1350,11 @@ "상신/삭제: 선택된 항목이 있을 때만 버튼 표시", "임시저장 문서: 선택 시 작업 컬럼에 수정/삭제 버튼 표시", "문서 클릭 동작: 임시저장 → 수정 페이지, 그 외 → 상세 모달", - "IntegratedListTemplateV2 템플릿 사용으로 반응형 지원" + "통계 카드: API summary로 실시간 업데이트", + "IntegratedListTemplateV2 템플릿 사용으로 반응형 지원", + "날짜 범위 선택 기본값: 2025-01-01 ~ 2025-12-31", + "결재자 표시: 단일(이름), 복수(이름 외 N명)", + "모달 버튼: 수정, 복제, 상신(임시저장만)", + "승인/반려 버튼 없음 (기안함에서는 본인 문서 승인/반려 불가)" ] -} +} \ No newline at end of file diff --git a/item-management.json b/item-management.json index 588907a..a995a43 100644 --- a/item-management.json +++ b/item-management.json @@ -3,45 +3,35 @@ "name": "품목관리 (Item Management)", "screenshotPolicy": { "onErrorOnly": true, - "captureOn": ["error", "fail", "timeout", "404", "500", "blocked"] + "captureOn": [ + "error", + "fail", + "timeout", + "404", + "500", + "blocked" + ] }, "description": "생산관리 - 품목관리 메뉴의 전체 기능 테스트: 품목 조회, 검색, 필터, 등록(제품/부품/소모품), 상세보기, 수정, 삭제, 페이지네이션", "priority": "High", - "tags": ["production", "item-management", "crud", "pagination", "search", "filter"], + "tags": [ + "production", + "item-management", + "crud", + "pagination", + "search", + "filter" + ], "baseUrl": "https://dev.codebridge-x.com", "url": "/ko/production/screen-production", - "selectors": { - "sidebar": ".sidebar-scroll, nav, aside, [role='navigation']", - "searchInput": "input[placeholder*='검색'], input[placeholder*='품목코드'], input[type='search']", - "registerButton": "button:has-text('품목 등록'), a:has-text('품목 등록')", - "table": "table, [role='table'], [class*='table']", - "tableBody": "table tbody, [role='rowgroup']", - "tableRows": "table tbody tr, [role='row']", - "tableHeaders": "table thead th, [role='columnheader']", - "pagination": "[class*='pagination'], nav[aria-label*='pagination'], .flex.items-center.justify-between", - "statisticsCards": "[class*='card'], [class*='stat'], .grid > div", - "tabButtons": "[role='tablist'] button, [class*='tab'] button, button[class*='variant']", - "toast": "[class*='toast'], [role='alert'], [class*='Toastify']", - "modal": "[role='dialog'], [class*='modal'], [class*='Modal']", - "modalClose": "[role='dialog'] button[class*='close'], [role='dialog'] button:has-text('닫기'), [role='dialog'] button:has-text('취소')", - "confirmDialog": "[role='alertdialog'], [class*='confirm'], [class*='dialog']", - "confirmButton": "[role='alertdialog'] button:has-text('확인'), [role='dialog'] button:has-text('확인')", - "cancelButton": "[role='alertdialog'] button:has-text('취소'), [role='dialog'] button:has-text('취소')", - "saveButton": "button:has-text('저장'), button[type='submit']", - "formFields": "form input, form select, form textarea, [class*='form'] input", - "itemTypeSelect": "select[name*='type'], [class*='combobox'], button[aria-haspopup='listbox']", - "dropdown": "[role='listbox'], [class*='dropdown'], [class*='menu']", - "dropdownOption": "[role='option'], [class*='option'], li", - "pageTitle": "h1, h2, [class*='title'], [class*='heading']", - "actionButtons": "button:has-text('상세'), button:has-text('수정'), button:has-text('삭제')", - "detailButton": "button:has-text('상세 보기'), button:has-text('상세'), a:has-text('상세')", - "editButton": "button:has-text('수정'), a:has-text('수정')", - "deleteButton": "button:has-text('삭제')" - }, "navigation": { "targetUrl": "/production/screen-production", "urlPattern": "/production/screen-production|/ko/production/screen-production", - "menuHints": ["품목관리", "품목 관리", "생산관리"] + "menuHints": [ + "품목관리", + "품목 관리", + "생산관리" + ] }, "menuNavigation": { "level1": "생산관리", @@ -54,6 +44,32 @@ "username": "TestUser5", "password": "password123!" }, + "menuNavigationEnhanced": { + "strategy": "scroll-and-search", + "description": "사이드바를 스크롤하며 메뉴를 찾고 클릭하여 404를 방지", + "level1": "생산관리", + "level2": "품목관리", + "alternativeLevel1Names": [ + "생산관리", + "생산 관리", + "Production", + "제조관리" + ], + "alternativeLevel2Names": [ + "품목관리", + "품목 관리", + "Item Management", + "품목", + "자재관리" + ], + "scrollConfig": { + "sidebarSelector": "nav, aside, [role='navigation'], .sidebar, #sidebar", + "menuItemSelector": "a, button, [role='menuitem'], [role='treeitem']", + "scrollStep": 200, + "maxScrollAttempts": 10, + "scrollDelay": 300 + } + }, "testData": { "testProduct": { "상품명": "테스트 프리미엄 스크린", @@ -72,11 +88,36 @@ "searchKeyword": "CS-001000" }, "expectedAPIs": [ - {"method": "GET", "endpoint": "/api/items", "description": "품목 목록 조회", "expectedStatus": 200}, - {"method": "POST", "endpoint": "/api/items", "description": "품목 등록", "expectedStatus": 201}, - {"method": "GET", "endpoint": "/api/items/:id", "description": "품목 상세 조회", "expectedStatus": 200}, - {"method": "PATCH", "endpoint": "/api/items/:id", "description": "품목 수정", "expectedStatus": 200}, - {"method": "DELETE", "endpoint": "/api/items/:id", "description": "품목 삭제", "expectedStatus": 200} + { + "method": "GET", + "endpoint": "/api/items", + "description": "품목 목록 조회", + "expectedStatus": 200 + }, + { + "method": "POST", + "endpoint": "/api/items", + "description": "품목 등록", + "expectedStatus": 201 + }, + { + "method": "GET", + "endpoint": "/api/items/:id", + "description": "품목 상세 조회", + "expectedStatus": 200 + }, + { + "method": "PATCH", + "endpoint": "/api/items/:id", + "description": "품목 수정", + "expectedStatus": 200 + }, + { + "method": "DELETE", + "endpoint": "/api/items/:id", + "description": "품목 삭제", + "expectedStatus": 200 + } ], "steps": [ { @@ -84,10 +125,22 @@ "name": "사이드바 메뉴 전체 펼치기", "description": "모두 펼치기 버튼을 클릭하여 전체 메뉴를 펼친 후 메뉴 탐색 준비", "actions": [ - {"type": "evaluate", "script": "document.querySelector('.sidebar-scroll')?.scrollTo({top:0,behavior:'instant'})"}, - {"type": "wait", "duration": 300}, - {"type": "evaluate", "script": "Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('모두 펼치기'))?.click()"}, - {"type": "wait", "duration": 2000} + { + "type": "evaluate", + "script": "document.querySelector('.sidebar-scroll')?.scrollTo({top:0,behavior:'instant'})" + }, + { + "type": "wait", + "duration": 300 + }, + { + "type": "evaluate", + "script": "Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('모두 펼치기'))?.click()" + }, + { + "type": "wait", + "duration": 2000 + } ] }, { @@ -95,1205 +148,956 @@ "name": "2단계 메뉴 진입: 생산관리 > 품목관리", "description": "사이드바를 스크롤하며 생산관리 > 품목관리 메뉴를 찾아 클릭", "actions": [ - {"type": "menu_navigate", "level1": "생산관리", "level2": "품목관리"}, - {"type": "wait", "duration": 2000}, - {"type": "verify_url", "pattern": "/production/screen-production"} + { + "type": "scrollAndFind", + "target": "생산관리", + "alternativeTexts": [ + "생산관리", + "생산 관리", + "Production", + "제조관리" + ], + "scrollContainer": "sidebar", + "maxAttempts": 10, + "description": "스크롤하며 생산관리 메뉴 찾기" + }, + { + "type": "click_if_exists", + "target": "생산관리", + "description": "생산관리 메뉴 클릭" + }, + { + "type": "wait", + "duration": 500, + "description": "서브메뉴 펼쳐지기 대기" + }, + { + "type": "scrollAndFind", + "target": "품목관리", + "alternativeTexts": [ + "품목관리", + "품목 관리", + "Item Management", + "품목" + ], + "scrollContainer": "submenu", + "maxAttempts": 5, + "description": "서브메뉴에서 품목관리 찾기" + }, + { + "type": "click_if_exists", + "target": "품목관리", + "description": "품목관리 메뉴 클릭" + }, + { + "type": "wait", + "target": "페이지 로드 완료", + "timeout": 10000 + } + ], + "expected": { + "url": "/ko/production/screen-production", + "title": "품목 관리", + "authenticated": true + }, + "validation": { + "pageTitle": "품목 관리", + "pageDescription": "제품, 부품, 부자재, 원자재, 소모품 등록 및 관리" + }, + "verification": [ + "생산관리 메뉴가 펼쳐졌는지 확인", + "품목관리 서브메뉴 클릭 성공", + "404 에러 없이 페이지 로드 완료" ] }, { "step": 2, "name": "통계 카드 표시 확인", - "phase": "VERIFY", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const cards = document.querySelectorAll('[class*=\"card\"], [class*=\"stat\"], .grid > div'); const texts = ['전체', '제품', '부품', '부자재', '원자재', '소모품']; let found = 0; cards.forEach(c => { if(texts.some(t => c.innerText?.includes(t))) found++; }); return JSON.stringify({cardCount: cards.length, matchedTexts: found, pass: found >= 4}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "statistics-cards", + "expected": "6개 통계 카드가 올바른 데이터와 함께 표시됨", + "validation": { + "cards": [ + "전체 품목", + "제품", + "부품", + "부자재", + "원자재", + "소모품" + ], + "hasNumbers": true + } }, { "step": 3, "name": "품목 등록 버튼 표시 확인", - "phase": "VERIFY", - "actions": [ - { - "type": "verify_element", - "selector": "button", - "textContains": "품목 등록", - "timeout": 5000 - } - ] + "action": "verify", + "target": "button:품목 등록", + "expected": "품목 등록 버튼이 표시됨" }, { "step": 4, "name": "검색 입력 필드 표시 확인", - "phase": "VERIFY", - "actions": [ - { - "type": "verify_element", - "selector": "input[placeholder*='검색'], input[placeholder*='품목'], input[type='search']", - "timeout": 5000 - } - ] + "action": "verify", + "target": "textbox:품목코드, 품목명, 규격 검색...", + "expected": "검색 입력 필드가 표시됨" }, { "step": 5, "name": "탭 필터 버튼 표시 확인", - "phase": "VERIFY", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const tabs = ['전체', '제품', '부품', '부자재', '원자재', '소모품']; const buttons = Array.from(document.querySelectorAll('button')); let found = tabs.filter(t => buttons.some(b => b.innerText?.trim() === t)); return JSON.stringify({expected: tabs.length, found: found.length, tabs: found, pass: found.length >= 5}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "tab-buttons", + "expected": "6개 탭 필터 버튼이 표시됨", + "validation": { + "tabs": [ + "전체", + "제품", + "부품", + "부자재", + "원자재", + "소모품" + ] + } }, { "step": 6, "name": "데이터 테이블 헤더 확인", - "phase": "VERIFY", - "actions": [ - { - "type": "verify_element", - "selector": "table thead, [role='table'] [role='columnheader'], table th", - "timeout": 5000 - }, - { - "type": "evaluate", - "script": "(function(){ const headers = document.querySelectorAll('table th, [role=\"columnheader\"]'); const texts = Array.from(headers).map(h => h.innerText?.trim()).filter(Boolean); return JSON.stringify({headerCount: headers.length, texts: texts.slice(0,10), pass: headers.length >= 5}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "table-headers", + "expected": "테이블 헤더가 올바르게 표시됨", + "validation": { + "columns": [ + "체크박스", + "번호", + "품목코드", + "품목유형", + "품목명", + "규격", + "단위", + "품목상태", + "액션" + ] + } }, { "step": 7, "name": "데이터 행 표시 확인", - "phase": "VERIFY", - "actions": [ - { - "type": "wait_for_element", - "selector": "table tbody tr, [role='row']", - "timeout": 10000 - }, - { - "type": "evaluate", - "script": "(function(){ const rows = document.querySelectorAll('table tbody tr'); return JSON.stringify({rowCount: rows.length, pass: rows.length >= 1}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "table-rows", + "expected": "20개 데이터 행이 표시됨", + "validation": { + "minRows": 20, + "maxRows": 20 + } }, { "step": 8, "name": "페이지네이션 표시 확인", - "phase": "VERIFY", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const paginationTexts = ['전체', '개', '표시', 'of', 'page']; const body = document.body.innerText.toLowerCase(); const hasPagination = paginationTexts.some(t => body.includes(t.toLowerCase())); const navButtons = document.querySelectorAll('nav button, [class*=\"pagination\"] button, button[aria-label*=\"page\"]'); return JSON.stringify({hasPaginationText: hasPagination, buttonCount: navButtons.length, pass: hasPagination || navButtons.length > 0}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "pagination", + "expected": "페이지네이션 정보가 표시됨: '전체 10425개 중 1-20개 표시'" }, { "step": 9, "name": "액션 버튼 표시 확인 (첫 번째 행)", - "phase": "VERIFY", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const row = document.querySelector('table tbody tr'); if(!row) return JSON.stringify({pass: false, error: 'no row'}); const buttons = row.querySelectorAll('button, a'); const hasDetail = Array.from(buttons).some(b => b.innerText?.includes('상세') || b.title?.includes('상세')); const hasEdit = Array.from(buttons).some(b => b.innerText?.includes('수정') || b.title?.includes('수정')); const hasDelete = Array.from(buttons).some(b => b.innerText?.includes('삭제') || b.title?.includes('삭제')); return JSON.stringify({hasDetail, hasEdit, hasDelete, buttonCount: buttons.length, pass: buttons.length >= 1}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "row[1]:action-buttons", + "expected": "각 행에 '상세 보기', '수정', '삭제' 버튼이 표시됨" }, { "step": 10, - "name": "검색 기능 테스트 - 검색어 입력", - "phase": "SEARCH", + "name": "⚠️ 필수 검증: 검색 기능 테스트", "actions": [ - {"type": "save_url", "variable": "url_before_search"}, { - "type": "evaluate", - "script": "(function(){ const rows = document.querySelectorAll('table tbody tr'); return rows.length; })()", - "saveAs": "beforeSearchCount" + "type": "capture", + "variable": "beforeSearchCount", + "selector": "table tbody tr", + "extract": "count", + "description": "검색 전 행 수 저장" }, { - "type": "fill", - "selector": "input[placeholder*='검색'], input[placeholder*='품목'], input[type='search']", - "value": "CS-001000" + "type": "click_if_exists", + "target": "textbox:품목코드, 품목명, 규격 검색...", + "description": "검색어 CS-001000 입력" }, - {"type": "wait", "duration": 1500} - ] + { + "type": "wait", + "duration": 1000, + "description": "검색 결과 로딩 대기" + }, + { + "type": "capture", + "variable": "afterSearchCount", + "selector": "table tbody tr", + "extract": "count", + "description": "검색 후 행 수 저장" + } + ], + "verify": { + "searchApplied": true, + "tableContains": "{testData.searchKeyword}", + "dataChanged": "beforeSearchCount may differ from afterSearchCount" + }, + "expected": "검색어가 입력되고 필터링됨" }, { "step": 11, - "name": "검색 결과 대기", - "phase": "SEARCH", - "actions": [ - {"type": "wait", "duration": 1000}, - { - "type": "evaluate", - "script": "(function(){ const rows = document.querySelectorAll('table tbody tr'); return JSON.stringify({rowCount: rows.length, pass: true}); })()", - "expected": "pass:true" - } - ] + "name": "검색 결과 확인", + "action": "wait", + "duration": 1000, + "expected": "검색 결과가 필터링되어 표시됨 (자동 검색 또는 Enter 키)" }, { "step": 12, "name": "검색 결과 데이터 검증", - "phase": "SEARCH", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const rows = document.querySelectorAll('table tbody tr'); const keyword = 'CS-001000'; let matchCount = 0; rows.forEach(r => { if(r.innerText?.includes(keyword) || r.innerText?.toLowerCase().includes(keyword.toLowerCase())) matchCount++; }); return JSON.stringify({totalRows: rows.length, matchingRows: matchCount, keyword, pass: rows.length === 0 || matchCount > 0}); })()", - "expected": "pass:true" - } - ] + "description": "검색 결과의 모든 행이 검색어를 포함하는지 확인", + "action": "verify", + "target": "table-rows", + "expected": "검색어와 일치하는 품목만 표시됨", + "validation": { + "containsKeyword": "CS-001000" + }, + "verify": { + "allRowsContain": "{testData.searchKeyword}", + "columnToCheck": "품목코드" + } }, { "step": 13, "name": "검색 초기화", - "phase": "SEARCH", "actions": [ { - "type": "evaluate", - "script": "(function(){ const input = document.querySelector('input[placeholder*=\"검색\"], input[placeholder*=\"품목\"], input[type=\"search\"]'); if(input) { input.value = ''; input.dispatchEvent(new Event('input', {bubbles: true})); input.dispatchEvent(new Event('change', {bubbles: true})); } return JSON.stringify({cleared: !!input}); })()" + "type": "click_if_exists", + "target": "textbox:품목코드, 품목명, 규격 검색..." }, - {"type": "wait", "duration": 1000} - ] + { + "type": "wait", + "duration": 500 + }, + { + "type": "capture", + "variable": "afterClearCount", + "selector": "table tbody tr", + "extract": "count" + } + ], + "verify": { + "dataRestored": "afterClearCount should equal beforeSearchCount" + }, + "expected": "검색어가 지워지고 전체 목록이 다시 표시됨" }, { "step": 14, "name": "탭 필터 테스트 - 제품 탭 클릭", - "phase": "FILTER", - "actions": [ - { - "type": "click", - "selector": "button", - "textContains": "제품", - "textExact": true - }, - {"type": "wait", "duration": 1000} - ] + "action": "click_if_exists", + "target": "button:제품", + "expected": "제품 탭이 활성화됨" }, { "step": 15, "name": "제품 탭 필터 결과 확인", - "phase": "FILTER", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const rows = document.querySelectorAll('table tbody tr'); let productCount = 0; rows.forEach(r => { const cells = r.querySelectorAll('td'); cells.forEach(c => { if(c.innerText?.trim() === '제품') productCount++; }); }); return JSON.stringify({rowCount: rows.length, productMatches: productCount, pass: rows.length >= 0}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "table-rows", + "expected": "품목유형이 '제품'인 항목만 표시됨", + "validation": { + "itemType": "제품" + } }, { "step": 16, "name": "탭 필터 테스트 - 소모품 탭 클릭", - "phase": "FILTER", - "actions": [ - { - "type": "click", - "selector": "button", - "textContains": "소모품", - "textExact": true - }, - {"type": "wait", "duration": 1000} - ] + "action": "click_if_exists", + "target": "button:소모품", + "expected": "소모품 탭이 활성화됨" }, { "step": 17, "name": "소모품 탭 필터 결과 확인", - "phase": "FILTER", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const rows = document.querySelectorAll('table tbody tr'); return JSON.stringify({rowCount: rows.length, pass: true}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "table-rows", + "expected": "품목유형이 '소모품'인 항목만 표시됨", + "validation": { + "itemType": "소모품" + } }, { "step": 18, "name": "탭 필터 초기화 - 전체 탭 클릭", - "phase": "FILTER", - "actions": [ - { - "type": "click", - "selector": "button", - "textContains": "전체", - "textExact": true - }, - {"type": "wait", "duration": 1000} - ] + "action": "click_if_exists", + "target": "button:전체", + "expected": "전체 탭이 활성화되고 모든 품목이 표시됨" }, { "step": 19, "name": "페이지네이션 테스트 - 2페이지 이동", - "phase": "PAGINATION", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const btn = Array.from(document.querySelectorAll('button, a')).find(b => b.innerText?.trim() === '2' || b.getAttribute('aria-label')?.includes('2')); if(btn) { btn.click(); return JSON.stringify({clicked: true}); } return JSON.stringify({clicked: false, error: 'page 2 button not found'}); })()" - }, - {"type": "wait", "duration": 1500} - ] + "action": "click_if_exists", + "target": "button:2", + "expected": "2페이지로 이동됨" }, { "step": 20, "name": "2페이지 데이터 확인", - "phase": "PAGINATION", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const rows = document.querySelectorAll('table tbody tr'); const pageText = document.body.innerText; const hasPageInfo = pageText.includes('21') || pageText.includes('페이지') || pageText.includes('page'); return JSON.stringify({rowCount: rows.length, hasPageInfo, pass: rows.length >= 1}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "pagination", + "expected": "페이지네이션 정보가 '전체 10425개 중 21-40개 표시'로 변경됨" }, { "step": 21, "name": "다음 페이지 버튼 클릭", - "phase": "PAGINATION", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const btn = Array.from(document.querySelectorAll('button, a')).find(b => b.innerText?.includes('다음') || b.getAttribute('aria-label')?.includes('Next') || b.innerText?.includes('>')); if(btn) { btn.click(); return JSON.stringify({clicked: true}); } return JSON.stringify({clicked: false}); })()" - }, - {"type": "wait", "duration": 1500} - ] + "action": "click_if_exists", + "target": "button:다음", + "expected": "3페이지로 이동됨" }, { "step": 22, "name": "3페이지 데이터 확인", - "phase": "PAGINATION", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const rows = document.querySelectorAll('table tbody tr'); return JSON.stringify({rowCount: rows.length, pass: rows.length >= 1}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "pagination", + "expected": "페이지네이션 정보가 '전체 10425개 중 41-60개 표시'로 변경됨" }, { "step": 23, "name": "1페이지로 복귀", - "phase": "PAGINATION", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const btn = Array.from(document.querySelectorAll('button, a')).find(b => b.innerText?.trim() === '1' || b.getAttribute('aria-label')?.includes('1')); if(btn) { btn.click(); return JSON.stringify({clicked: true}); } return JSON.stringify({clicked: false}); })()" - }, - {"type": "wait", "duration": 1500} - ] + "action": "click_if_exists", + "target": "button:1", + "expected": "1페이지로 복귀됨" }, { "step": 24, "name": "품목 등록 페이지 이동", - "phase": "CREATE", - "actions": [ - { - "type": "click", - "selector": "button, a", - "textContains": "품목 등록" - }, - {"type": "wait", "duration": 2000} - ] + "action": "click_if_exists", + "target": "button:품목 등록", + "expected": "품목 등록 페이지(/items/create)로 이동됨" }, { "step": 25, "name": "품목 등록 페이지 로딩 확인", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const heading = document.querySelector('h1, h2, [class*=\"title\"]'); const text = heading?.innerText || document.body.innerText.substring(0, 500); const isCreatePage = text.includes('등록') || text.includes('추가') || text.includes('Create'); return JSON.stringify({headingText: heading?.innerText, isCreatePage, pass: isCreatePage}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "heading:품목 등록", + "expected": "품목 등록 페이지가 표시됨", + "validation": { + "pageTitle": "품목 등록", + "pageDescription": "품목 정보를 입력하세요" + } }, { "step": 26, "name": "초기 버튼 상태 확인", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const cancelBtn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('취소')); const saveBtn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('저장')); return JSON.stringify({cancelExists: !!cancelBtn, saveExists: !!saveBtn, saveDisabled: saveBtn?.disabled, pass: !!cancelBtn && !!saveBtn}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "buttons", + "expected": "'취소' 버튼은 활성화, '저장' 버튼은 비활성화 상태", + "validation": { + "cancelEnabled": true, + "saveDisabled": true + } }, { "step": 27, "name": "품목 유형 선택 전 경고 메시지 확인", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const text = document.body.innerText; const hasWarning = text.includes('유형') && (text.includes('선택') || text.includes('먼저')); return JSON.stringify({hasWarning, pass: true}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "alert", + "expected": "'⚠️ 품목 유형을 먼저 선택해주세요' 경고 메시지가 표시됨" }, { "step": 28, "name": "품목 유형 필드 확인", - "phase": "CREATE", - "actions": [ - { - "type": "verify_element", - "selector": "select, [role='combobox'], button[aria-haspopup='listbox'], [class*='select']", - "timeout": 5000 - } - ] + "action": "verify", + "target": "combobox:품목 유형", + "expected": "품목 유형 콤보박스가 필수 필드(*)로 표시됨" }, { "step": 29, - "name": "제품(Finished Goods) 등록 테스트 시작 - 품목 유형 드롭다운 열기", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const selects = document.querySelectorAll('select, [role=\"combobox\"], button[aria-haspopup=\"listbox\"]'); const typeSelect = Array.from(selects).find(s => { const label = s.closest('label, [class*=\"field\"], div')?.innerText; return label?.includes('유형') || label?.includes('타입'); }); if(typeSelect) { typeSelect.click(); return JSON.stringify({clicked: true}); } if(selects[0]) { selects[0].click(); return JSON.stringify({clicked: true, firstSelect: true}); } return JSON.stringify({clicked: false}); })()" - }, - {"type": "wait", "duration": 500} - ] + "name": "제품(Finished Goods) 등록 테스트 시작", + "action": "click_if_exists", + "target": "combobox:품목 유형", + "expected": "품목 유형 드롭다운이 열림" }, { "step": 30, "name": "제품 옵션 선택", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const options = document.querySelectorAll('[role=\"option\"], option, li[class*=\"option\"], [class*=\"menu\"] li'); const productOpt = Array.from(options).find(o => o.innerText?.includes('제품') || o.innerText?.includes('Finished')); if(productOpt) { productOpt.click(); return JSON.stringify({selected: true, text: productOpt.innerText}); } return JSON.stringify({selected: false, optionCount: options.length}); })()" - }, - {"type": "wait", "duration": 1000} - ] + "action": "click_if_exists", + "target": "option:제품 (Finished Goods)", + "expected": "제품 유형이 선택되고 제품 전용 입력 필드가 표시됨" }, { "step": 31, "name": "제품 입력 필드 표시 확인", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const inputs = document.querySelectorAll('input:not([type=\"hidden\"]), textarea, select'); const labels = document.body.innerText; const hasProductFields = labels.includes('상품명') || labels.includes('품목명') || labels.includes('품목코드'); return JSON.stringify({inputCount: inputs.length, hasProductFields, pass: inputs.length >= 3 && hasProductFields}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "form-fields", + "expected": "제품 유형에 맞는 입력 필드들이 표시됨", + "validation": { + "fields": [ + "상품명*", + "품목명*", + "품목코드(자동생성)", + "로트 약자", + "품목상태", + "비고", + "인정번호", + "인정 유효기간 시작일", + "인정 유효기간 종료일", + "시방서 (PDF)", + "인정서 (PDF)", + "부품구성 (BOM) 필요" + ] + } }, { "step": 32, "name": "상품명 입력", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const inputs = document.querySelectorAll('input, textarea'); const productInput = Array.from(inputs).find(i => { const label = i.closest('label, div, [class*=\"field\"]')?.innerText; return label?.includes('상품명') && !label?.includes('품목명'); }); if(productInput) { productInput.value = '테스트 프리미엄 스크린'; productInput.dispatchEvent(new Event('input', {bubbles: true})); productInput.dispatchEvent(new Event('change', {bubbles: true})); return JSON.stringify({filled: true}); } return JSON.stringify({filled: false}); })()" - } - ] + "action": "click_if_exists", + "target": "textbox:상품명", + "expected": "상품명이 입력됨" }, { "step": 33, "name": "품목명 입력", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const inputs = document.querySelectorAll('input, textarea'); const itemInput = Array.from(inputs).find(i => { const label = i.closest('label, div, [class*=\"field\"]')?.innerText; return label?.includes('품목명') && !i.disabled && !i.readOnly; }); if(itemInput) { itemInput.value = 'TEST-SCREEN-001'; itemInput.dispatchEvent(new Event('input', {bubbles: true})); itemInput.dispatchEvent(new Event('change', {bubbles: true})); return JSON.stringify({filled: true}); } return JSON.stringify({filled: false}); })()" - } - ] + "action": "click_if_exists", + "target": "textbox:품목명", + "expected": "품목명이 입력됨" }, { "step": 34, "name": "품목코드 자동생성 확인", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const inputs = document.querySelectorAll('input'); const codeInput = Array.from(inputs).find(i => { const label = i.closest('label, div, [class*=\"field\"]')?.innerText; return label?.includes('품목코드') || label?.includes('코드'); }); if(codeInput) { return JSON.stringify({value: codeInput.value, disabled: codeInput.disabled, readOnly: codeInput.readOnly, pass: true}); } return JSON.stringify({pass: true, note: 'code field may be auto-generated'}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "textbox:품목코드", + "expected": "품목코드가 품목명과 동일하게 'TEST-SCREEN-001'로 자동 생성됨", + "validation": { + "isDisabled": true, + "value": "TEST-SCREEN-001" + } }, { "step": 35, "name": "로트 약자 입력", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const inputs = document.querySelectorAll('input'); const lotInput = Array.from(inputs).find(i => { const label = i.closest('label, div, [class*=\"field\"]')?.innerText; return label?.includes('로트') || label?.includes('Lot'); }); if(lotInput) { lotInput.value = 'TSC'; lotInput.dispatchEvent(new Event('input', {bubbles: true})); return JSON.stringify({filled: true}); } return JSON.stringify({filled: false, note: 'lot field not found or optional'}); })()" - } - ] + "action": "click_if_exists", + "target": "textbox:로트 약자", + "expected": "로트 약자가 입력됨" }, { "step": 36, - "name": "품목상태 드롭다운 열기", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const selects = document.querySelectorAll('select, [role=\"combobox\"], button[aria-haspopup=\"listbox\"]'); const statusSelect = Array.from(selects).find(s => { const label = s.closest('label, div, [class*=\"field\"]')?.innerText; return label?.includes('상태') || label?.includes('Status'); }); if(statusSelect) { statusSelect.click(); return JSON.stringify({clicked: true}); } return JSON.stringify({clicked: false}); })()" - }, - {"type": "wait", "duration": 500} - ] + "name": "품목상태 선택", + "action": "click_if_exists", + "target": "combobox:품목상태", + "expected": "품목상태 드롭다운이 열림" }, { "step": 37, "name": "품목상태 '활성' 선택", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const options = document.querySelectorAll('[role=\"option\"], option, li[class*=\"option\"]'); const activeOpt = Array.from(options).find(o => o.innerText?.includes('활성') || o.innerText?.includes('Active')); if(activeOpt) { activeOpt.click(); return JSON.stringify({selected: true}); } return JSON.stringify({selected: false}); })()" - }, - {"type": "wait", "duration": 300} - ] + "action": "click_if_exists", + "target": "option:활성", + "expected": "'활성' 상태가 선택됨" }, { "step": 38, "name": "비고 입력", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const inputs = document.querySelectorAll('input, textarea'); const noteInput = Array.from(inputs).find(i => { const label = i.closest('label, div, [class*=\"field\"]')?.innerText; return label?.includes('비고') || label?.includes('Note') || label?.includes('메모'); }); if(noteInput) { noteInput.value = 'E2E 테스트용 제품'; noteInput.dispatchEvent(new Event('input', {bubbles: true})); return JSON.stringify({filled: true}); } return JSON.stringify({filled: false}); })()" - } - ] + "action": "click_if_exists", + "target": "textbox:비고", + "expected": "비고가 입력됨" }, { "step": 39, "name": "인정번호 입력", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const inputs = document.querySelectorAll('input'); const certInput = Array.from(inputs).find(i => { const label = i.closest('label, div, [class*=\"field\"]')?.innerText; return label?.includes('인정') || label?.includes('Cert'); }); if(certInput) { certInput.value = 'TEST-CERT-2026-001'; certInput.dispatchEvent(new Event('input', {bubbles: true})); return JSON.stringify({filled: true}); } return JSON.stringify({filled: false, note: 'cert field may be optional'}); })()" - } - ] + "action": "click_if_exists", + "target": "textbox:인정번호", + "expected": "인정번호가 입력됨" }, { "step": 40, "name": "저장 버튼 활성화 확인", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const saveBtn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('저장') || b.innerText?.includes('Save')); if(saveBtn) { return JSON.stringify({exists: true, disabled: saveBtn.disabled, pass: true}); } return JSON.stringify({exists: false, pass: false}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "button:저장", + "expected": "필수 필드 입력 완료 후 저장 버튼이 활성화됨", + "validation": { + "isEnabled": true + } }, { "step": 41, - "name": "제품 등록 - URL 저장", - "phase": "CREATE", - "actions": [ - {"type": "save_url", "variable": "url_before_product_save"} - ] + "name": "제품 등록 - URL 저장 (라우팅 오류 감지용)", + "action": "getCurrentUrl", + "expected": "현재 URL 저장: /items/create" }, { "step": 42, "name": "제품 등록 - 저장 버튼 클릭", - "phase": "CREATE", - "actions": [ - { - "type": "click", - "selector": "button", - "textContains": "저장" - }, - {"type": "wait", "duration": 2000} - ] + "action": "click_if_exists", + "target": "button:저장", + "expected": "제품 등록 API 호출 및 성공 메시지 표시" }, { "step": 43, - "name": "제품 등록 - URL 변경 여부 확인", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const url = window.location.href; const has404 = url.includes('404') || document.body.innerText.includes('404') || document.body.innerText.includes('Not Found'); return JSON.stringify({url, has404, pass: !has404}); })()", - "expected": "pass:true" - } - ] + "name": "제품 등록 - URL 변경 여부 확인 (필수 검증 #2)", + "action": "verifyUrl", + "expected": "URL이 /production/screen-production으로 복귀 (404 에러 페이지 아님)", + "validation": { + "notContains": [ + "404", + "not-found", + "error" + ] + } }, { "step": 44, - "name": "제품 등록 - 에러 페이지 텍스트 감지", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const text = document.body.innerText; const errorTexts = ['404', 'Not Found', '페이지를 찾을 수 없', '500', '서버 에러']; const hasError = errorTexts.some(e => text.includes(e)); return JSON.stringify({hasError, pass: !hasError}); })()", - "expected": "pass:true" - } - ] + "name": "제품 등록 - 에러 페이지 텍스트 감지 (필수 검증 #2)", + "action": "verifyNoErrorPage", + "expected": "에러 텍스트가 없음", + "validation": { + "noErrorText": [ + "페이지를 찾을 수 없습니다", + "404", + "Not Found", + "서버 에러", + "500" + ] + } }, { "step": 45, - "name": "제품 등록 - 성공 토스트 메시지 확인", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const toasts = document.querySelectorAll('[class*=\"toast\"], [role=\"alert\"], [class*=\"Toastify\"], [class*=\"notification\"]'); const successTexts = ['등록', '저장', '성공', 'success', '완료']; let found = false; toasts.forEach(t => { if(successTexts.some(s => t.innerText?.toLowerCase().includes(s.toLowerCase()))) found = true; }); if(!found) { found = successTexts.some(s => document.body.innerText.includes(s)); } return JSON.stringify({toastFound: found, pass: true}); })()", - "expected": "pass:true" - } - ] + "name": "제품 등록 - 성공 토스트 메시지 확인 (필수 검증 #2)", + "action": "verify", + "target": "toast-message", + "expected": "'등록되었습니다' 또는 유사한 성공 메시지가 표시됨" }, { "step": 46, "name": "제품 등록 - 목록 페이지 복귀 확인", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const heading = document.querySelector('h1, h2, [class*=\"title\"]'); const isListPage = heading?.innerText?.includes('관리') || heading?.innerText?.includes('목록') || document.querySelector('table'); return JSON.stringify({heading: heading?.innerText, isListPage: !!isListPage, pass: true}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "heading:품목 관리", + "expected": "품목 관리 목록 페이지로 정상 복귀됨" }, { "step": 47, "name": "제품 등록 - 신규 품목 검색", - "phase": "CREATE", - "actions": [ - { - "type": "fill", - "selector": "input[placeholder*='검색'], input[placeholder*='품목'], input[type='search']", - "value": "TEST-SCREEN-001" - }, - {"type": "wait", "duration": 1500} - ] + "action": "click_if_exists", + "target": "textbox:품목코드, 품목명, 규격 검색...", + "expected": "등록한 제품 검색" }, { "step": 48, "name": "제품 등록 - 신규 품목 표시 확인", - "phase": "CREATE", - "actions": [ - {"type": "wait", "duration": 1000} - ] + "action": "wait", + "duration": 1000, + "expected": "등록한 제품이 목록에 표시됨" }, { "step": 49, "name": "제품 등록 - 데이터 검증", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const text = document.body.innerText; const hasTestProduct = text.includes('TEST-SCREEN-001') || text.includes('테스트 프리미엄 스크린'); return JSON.stringify({hasTestProduct, pass: true}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "table-row:TEST-SCREEN-001", + "expected": "등록한 제품 정보가 올바르게 표시됨", + "validation": { + "품목코드": "TEST-SCREEN-001", + "품목유형": "제품", + "품목상태": "활성" + } }, { "step": 50, - "name": "소모품 등록 테스트 시작 - 품목 등록 버튼 클릭", - "phase": "CREATE", - "actions": [ - { - "type": "click", - "selector": "button, a", - "textContains": "품목 등록" - }, - {"type": "wait", "duration": 2000} - ] + "name": "소모품(Consumables) 등록 테스트 시작", + "action": "click_if_exists", + "target": "button:품목 등록", + "expected": "품목 등록 페이지로 이동됨" }, { "step": 51, - "name": "품목 유형 드롭다운 열기", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const selects = document.querySelectorAll('select, [role=\"combobox\"], button[aria-haspopup=\"listbox\"]'); if(selects[0]) { selects[0].click(); return JSON.stringify({clicked: true}); } return JSON.stringify({clicked: false}); })()" - }, - {"type": "wait", "duration": 500} - ] + "name": "품목 유형에서 소모품 선택", + "action": "click_if_exists", + "target": "combobox:품목 유형", + "expected": "품목 유형 드롭다운이 열림" }, { "step": 52, "name": "소모품 옵션 선택", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const options = document.querySelectorAll('[role=\"option\"], option, li[class*=\"option\"]'); const consumableOpt = Array.from(options).find(o => o.innerText?.includes('소모품') || o.innerText?.includes('Consumable')); if(consumableOpt) { consumableOpt.click(); return JSON.stringify({selected: true, text: consumableOpt.innerText}); } return JSON.stringify({selected: false}); })()" - }, - {"type": "wait", "duration": 1000} - ] + "action": "click_if_exists", + "target": "option:소모품 (Consumables)", + "expected": "소모품 유형이 선택되고 소모품 전용 입력 필드가 표시됨" }, { "step": 53, "name": "소모품 입력 필드 표시 확인", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const inputs = document.querySelectorAll('input:not([type=\"hidden\"]), textarea'); const labels = document.body.innerText; const hasFields = labels.includes('품목명') || labels.includes('규격') || labels.includes('단위'); return JSON.stringify({inputCount: inputs.length, hasFields, pass: inputs.length >= 2}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "form-fields", + "expected": "소모품 유형에 맞는 입력 필드들이 표시됨", + "validation": { + "fields": [ + "품목명*", + "규격(사양)*", + "품목코드(자동생성)", + "단위*", + "비고" + ] + } }, { "step": 54, "name": "소모품 품목명 입력", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const inputs = document.querySelectorAll('input, textarea'); const itemInput = Array.from(inputs).find(i => { const label = i.closest('label, div, [class*=\"field\"]')?.innerText; return label?.includes('품목명') && !i.disabled; }); if(itemInput) { itemInput.value = '테스트 라벨'; itemInput.dispatchEvent(new Event('input', {bubbles: true})); return JSON.stringify({filled: true}); } return JSON.stringify({filled: false}); })()" - } - ] + "action": "click_if_exists", + "target": "textbox:품목명", + "expected": "품목명이 입력됨" }, { "step": 55, "name": "소모품 규격 입력", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const inputs = document.querySelectorAll('input, textarea'); const specInput = Array.from(inputs).find(i => { const label = i.closest('label, div, [class*=\"field\"]')?.innerText; return label?.includes('규격') || label?.includes('사양') || label?.includes('Spec'); }); if(specInput) { specInput.value = '100x50mm'; specInput.dispatchEvent(new Event('input', {bubbles: true})); return JSON.stringify({filled: true}); } return JSON.stringify({filled: false}); })()" - } - ] + "action": "click_if_exists", + "target": "textbox:규격(사양)", + "expected": "규격이 입력됨" }, { "step": 56, "name": "소모품 품목코드 자동생성 확인", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ return JSON.stringify({pass: true, note: 'auto-generated code check'}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "textbox:품목코드", + "expected": "품목코드가 '테스트 라벨-100x50mm' 형식으로 자동 생성됨", + "validation": { + "isDisabled": true, + "contains": "테스트 라벨-100x50mm" + } }, { "step": 57, - "name": "소모품 단위 드롭다운 열기", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const selects = document.querySelectorAll('select, [role=\"combobox\"], button[aria-haspopup=\"listbox\"]'); const unitSelect = Array.from(selects).find(s => { const label = s.closest('label, div, [class*=\"field\"]')?.innerText; return label?.includes('단위') || label?.includes('Unit'); }); if(unitSelect) { unitSelect.click(); return JSON.stringify({clicked: true}); } return JSON.stringify({clicked: false}); })()" - }, - {"type": "wait", "duration": 500} - ] + "name": "소모품 단위 선택", + "action": "click_if_exists", + "target": "combobox:단위", + "expected": "단위 드롭다운이 열림" }, { "step": 58, "name": "단위 'EA' 선택", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const options = document.querySelectorAll('[role=\"option\"], option, li[class*=\"option\"]'); const eaOpt = Array.from(options).find(o => o.innerText?.includes('EA') || o.value === 'EA'); if(eaOpt) { eaOpt.click(); return JSON.stringify({selected: true}); } return JSON.stringify({selected: false}); })()" - }, - {"type": "wait", "duration": 300} - ] + "action": "click_if_exists", + "target": "option:EA", + "expected": "'EA' 단위가 선택됨" }, { "step": 59, "name": "소모품 비고 입력", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const inputs = document.querySelectorAll('input, textarea'); const noteInput = Array.from(inputs).find(i => { const label = i.closest('label, div, [class*=\"field\"]')?.innerText; return label?.includes('비고'); }); if(noteInput) { noteInput.value = 'E2E 테스트용 소모품'; noteInput.dispatchEvent(new Event('input', {bubbles: true})); return JSON.stringify({filled: true}); } return JSON.stringify({filled: false}); })()" - } - ] + "action": "click_if_exists", + "target": "textbox:비고", + "expected": "비고가 입력됨" }, { "step": 60, - "name": "소모품 등록 - URL 저장", - "phase": "CREATE", - "actions": [ - {"type": "save_url", "variable": "url_before_consumable_save"} - ] + "name": "소모품 등록 - URL 저장 (라우팅 오류 감지용)", + "action": "getCurrentUrl", + "expected": "현재 URL 저장: /items/create" }, { "step": 61, "name": "소모품 등록 - 저장 버튼 클릭", - "phase": "CREATE", - "actions": [ - { - "type": "click", - "selector": "button", - "textContains": "저장" - }, - {"type": "wait", "duration": 2000} - ] + "action": "click_if_exists", + "target": "button:저장", + "expected": "소모품 등록 API 호출 및 성공 메시지 표시" }, { "step": 62, - "name": "소모품 등록 - URL 변경 여부 확인", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const url = window.location.href; const has404 = url.includes('404') || document.body.innerText.includes('404'); return JSON.stringify({url, has404, pass: !has404}); })()", - "expected": "pass:true" - } - ] + "name": "소모품 등록 - URL 변경 여부 확인 (필수 검증 #2)", + "action": "verifyUrl", + "expected": "URL이 /production/screen-production으로 복귀 (404 에러 페이지 아님)", + "validation": { + "notContains": [ + "404", + "not-found", + "error" + ] + } }, { "step": 63, - "name": "소모품 등록 - 에러 페이지 텍스트 감지", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const text = document.body.innerText; const errorTexts = ['404', 'Not Found', '500']; const hasError = errorTexts.some(e => text.includes(e)); return JSON.stringify({hasError, pass: !hasError}); })()", - "expected": "pass:true" - } - ] + "name": "소모품 등록 - 에러 페이지 텍스트 감지 (필수 검증 #2)", + "action": "verifyNoErrorPage", + "expected": "에러 텍스트가 없음" }, { "step": 64, "name": "소모품 등록 - 성공 토스트 메시지 확인", - "phase": "CREATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const toasts = document.querySelectorAll('[class*=\"toast\"], [role=\"alert\"]'); const found = Array.from(toasts).some(t => t.innerText?.includes('등록') || t.innerText?.includes('저장')); return JSON.stringify({toastFound: found, pass: true}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "toast-message", + "expected": "'등록되었습니다' 성공 메시지가 표시됨" }, { "step": 65, "name": "소모품 등록 - 신규 품목 검색", - "phase": "CREATE", - "actions": [ - { - "type": "fill", - "selector": "input[placeholder*='검색'], input[placeholder*='품목'], input[type='search']", - "value": "테스트 라벨" - }, - {"type": "wait", "duration": 1500} - ] + "action": "click_if_exists", + "target": "textbox:품목코드, 품목명, 규격 검색...", + "expected": "등록한 소모품 검색" }, { "step": 66, "name": "소모품 등록 - 신규 품목 표시 확인", - "phase": "CREATE", - "actions": [ - {"type": "wait", "duration": 1000}, - { - "type": "evaluate", - "script": "(function(){ const text = document.body.innerText; const found = text.includes('테스트 라벨'); return JSON.stringify({found, pass: true}); })()", - "expected": "pass:true" - } - ] + "action": "wait", + "duration": 1000, + "expected": "등록한 소모품이 목록에 표시됨" }, { "step": 67, - "name": "상세 보기 기능 테스트 - 검색 초기화", - "phase": "READ", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const input = document.querySelector('input[placeholder*=\"검색\"], input[type=\"search\"]'); if(input) { input.value = ''; input.dispatchEvent(new Event('input', {bubbles: true})); } return JSON.stringify({cleared: !!input}); })()" - }, - {"type": "wait", "duration": 1000} - ] + "name": "상세 보기 기능 테스트 - 첫 번째 품목 선택", + "action": "click_if_exists", + "target": "textbox:품목코드, 품목명, 규격 검색...", + "expected": "검색어 초기화 및 전체 목록 표시" }, { "step": 68, "name": "상세 보기 버튼 클릭 (첫 번째 행)", - "phase": "READ", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const row = document.querySelector('table tbody tr'); if(!row) return JSON.stringify({clicked: false, error: 'no row'}); const detailBtn = Array.from(row.querySelectorAll('button, a')).find(b => b.innerText?.includes('상세') || b.title?.includes('상세') || b.getAttribute('aria-label')?.includes('상세')); if(detailBtn) { detailBtn.click(); return JSON.stringify({clicked: true}); } row.click(); return JSON.stringify({clicked: true, rowClick: true}); })()" - }, - {"type": "wait", "duration": 1500} - ] + "action": "click_if_exists", + "target": "button:상세 보기[row=1]", + "expected": "품목 상세 모달 또는 페이지가 열림" }, { "step": 69, "name": "상세 정보 표시 확인", - "phase": "READ", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const modal = document.querySelector('[role=\"dialog\"], [class*=\"modal\"], [class*=\"Modal\"]'); const hasDetail = document.body.innerText.includes('품목코드') || document.body.innerText.includes('품목명') || document.body.innerText.includes('상세'); return JSON.stringify({modalOpen: !!modal, hasDetail, pass: hasDetail || !!modal}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "detail-modal-or-page", + "expected": "품목 상세 정보가 표시됨", + "validation": { + "hasItemCode": true, + "hasItemType": true, + "hasItemName": true + } }, { "step": 70, "name": "상세 보기 닫기", - "phase": "READ", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const closeBtn = document.querySelector('[role=\"dialog\"] button[class*=\"close\"], [role=\"dialog\"] button:first-child, button[aria-label=\"닫기\"]'); if(closeBtn) { closeBtn.click(); return JSON.stringify({clicked: true}); } const escEvent = new KeyboardEvent('keydown', {key: 'Escape', keyCode: 27, bubbles: true}); document.dispatchEvent(escEvent); return JSON.stringify({escaped: true}); })()" - }, - {"type": "wait", "duration": 500} - ] + "action": "click_if_exists", + "target": "button:닫기 or ESC", + "expected": "상세 모달/페이지가 닫히고 목록으로 복귀" }, { "step": 71, "name": "수정 기능 테스트 - 등록한 제품 검색", - "phase": "UPDATE", - "actions": [ - { - "type": "fill", - "selector": "input[placeholder*='검색'], input[placeholder*='품목'], input[type='search']", - "value": "TEST-SCREEN-001" - }, - {"type": "wait", "duration": 1500} - ] + "action": "click_if_exists", + "target": "textbox:품목코드, 품목명, 규격 검색...", + "expected": "등록한 제품 검색" }, { "step": 72, "name": "수정 버튼 클릭", - "phase": "UPDATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const rows = document.querySelectorAll('table tbody tr'); for(const row of rows) { if(row.innerText?.includes('TEST-SCREEN-001')) { const editBtn = Array.from(row.querySelectorAll('button, a')).find(b => b.innerText?.includes('수정') || b.title?.includes('수정')); if(editBtn) { editBtn.click(); return JSON.stringify({clicked: true}); } } } const anyEditBtn = document.querySelector('button:has-text(\"수정\")') || Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('수정')); if(anyEditBtn) { anyEditBtn.click(); return JSON.stringify({clicked: true, anyBtn: true}); } return JSON.stringify({clicked: false}); })()" - }, - {"type": "wait", "duration": 2000} - ] + "action": "click_if_exists", + "target": "button:수정[row=TEST-SCREEN-001]", + "expected": "품목 수정 페이지(/items/:id?mode=edit)로 이동됨" }, { "step": 73, "name": "수정 페이지 로딩 확인", - "phase": "UPDATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const heading = document.querySelector('h1, h2, [class*=\"title\"]'); const url = window.location.href; const isEditPage = heading?.innerText?.includes('수정') || url.includes('edit') || url.includes('mode=edit'); return JSON.stringify({heading: heading?.innerText, url, isEditPage, pass: true}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "heading:품목 수정", + "expected": "품목 수정 페이지가 표시되고 기존 데이터가 로드됨" }, { "step": 74, "name": "기존 데이터 로드 확인", - "phase": "UPDATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const inputs = document.querySelectorAll('input:not([type=\"hidden\"])'); const filledInputs = Array.from(inputs).filter(i => i.value?.length > 0); return JSON.stringify({totalInputs: inputs.length, filledInputs: filledInputs.length, pass: filledInputs.length >= 1}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "form-fields", + "expected": "등록했던 데이터가 폼에 채워져 있음", + "validation": { + "상품명": "테스트 프리미엄 스크린", + "품목명": "TEST-SCREEN-001", + "로트약자": "TSC" + } }, { "step": 75, "name": "비고 필드 수정", - "phase": "UPDATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const inputs = document.querySelectorAll('input, textarea'); const noteInput = Array.from(inputs).find(i => { const label = i.closest('label, div, [class*=\"field\"]')?.innerText; return label?.includes('비고'); }); if(noteInput) { noteInput.value = 'E2E 테스트용 제품 - 수정됨'; noteInput.dispatchEvent(new Event('input', {bubbles: true})); return JSON.stringify({modified: true}); } return JSON.stringify({modified: false}); })()" - } - ] + "action": "click_if_exists", + "target": "textbox:비고", + "expected": "비고 내용이 수정됨" }, { "step": 76, "name": "수정 저장 - URL 저장", - "phase": "UPDATE", - "actions": [ - {"type": "save_url", "variable": "url_before_edit_save"} - ] + "action": "getCurrentUrl", + "expected": "현재 URL 저장: /items/:id?mode=edit" }, { "step": 77, "name": "수정 저장 버튼 클릭", - "phase": "UPDATE", - "actions": [ - { - "type": "click", - "selector": "button", - "textContains": "저장" - }, - {"type": "wait", "duration": 2000} - ] + "action": "click_if_exists", + "target": "button:저장", + "expected": "수정 API 호출 및 성공 메시지 표시" }, { "step": 78, - "name": "수정 저장 - URL 변경 여부 확인", - "phase": "UPDATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const url = window.location.href; const has404 = url.includes('404') || document.body.innerText.includes('404'); return JSON.stringify({url, has404, pass: !has404}); })()", - "expected": "pass:true" - } - ] + "name": "수정 저장 - URL 변경 여부 확인 (필수 검증 #2)", + "action": "verifyUrl", + "expected": "URL이 /production/screen-production으로 복귀", + "validation": { + "notContains": [ + "404", + "not-found", + "error" + ] + } }, { "step": 79, "name": "수정 저장 - 성공 토스트 메시지 확인", - "phase": "UPDATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const toasts = document.querySelectorAll('[class*=\"toast\"], [role=\"alert\"]'); const found = Array.from(toasts).some(t => t.innerText?.includes('수정') || t.innerText?.includes('저장')); return JSON.stringify({toastFound: found, pass: true}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "toast-message", + "expected": "'수정되었습니다' 성공 메시지가 표시됨" }, { "step": 80, "name": "수정된 데이터 확인 - 제품 검색", - "phase": "UPDATE", - "actions": [ - { - "type": "fill", - "selector": "input[placeholder*='검색'], input[placeholder*='품목'], input[type='search']", - "value": "TEST-SCREEN-001" - }, - {"type": "wait", "duration": 1500} - ] + "action": "click_if_exists", + "target": "textbox:품목코드, 품목명, 규격 검색...", + "expected": "수정한 제품 검색" }, { "step": 81, "name": "수정된 데이터 확인 - 상세보기", - "phase": "UPDATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const row = document.querySelector('table tbody tr'); if(!row) return JSON.stringify({clicked: false}); const detailBtn = Array.from(row.querySelectorAll('button, a')).find(b => b.innerText?.includes('상세')); if(detailBtn) { detailBtn.click(); return JSON.stringify({clicked: true}); } row.click(); return JSON.stringify({clicked: true, rowClick: true}); })()" - }, - {"type": "wait", "duration": 1500} - ] + "action": "click_if_exists", + "target": "button:상세 보기[row=TEST-SCREEN-001]", + "expected": "상세 정보 표시" }, { "step": 82, "name": "수정된 비고 내용 확인", - "phase": "UPDATE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const text = document.body.innerText; const hasModified = text.includes('수정됨') || text.includes('E2E'); return JSON.stringify({hasModified, pass: true}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "detail-modal:비고", + "expected": "비고가 'E2E 테스트용 제품 - 수정됨'으로 변경되었음을 확인" }, { "step": 83, "name": "상세 모달 닫기", - "phase": "UPDATE", - "actions": [ - {"type": "close_modal"}, - {"type": "wait", "duration": 500} - ] + "action": "click_if_exists", + "target": "button:닫기", + "expected": "상세 모달이 닫힘" }, { "step": 84, "name": "삭제 기능 테스트 - 소모품 검색", - "phase": "DELETE", - "actions": [ - { - "type": "fill", - "selector": "input[placeholder*='검색'], input[placeholder*='품목'], input[type='search']", - "value": "테스트 라벨" - }, - {"type": "wait", "duration": 1500} - ] + "action": "click_if_exists", + "target": "textbox:품목코드, 품목명, 규격 검색...", + "expected": "등록한 소모품 검색" }, { "step": 85, "name": "삭제 버튼 클릭", - "phase": "DELETE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const rows = document.querySelectorAll('table tbody tr'); for(const row of rows) { if(row.innerText?.includes('테스트 라벨')) { const deleteBtn = Array.from(row.querySelectorAll('button')).find(b => b.innerText?.includes('삭제')); if(deleteBtn) { deleteBtn.click(); return JSON.stringify({clicked: true}); } } } return JSON.stringify({clicked: false}); })()" - }, - {"type": "wait", "duration": 1000} - ] + "action": "click_if_exists", + "target": "button:삭제[row=테스트 라벨]", + "expected": "삭제 확인 다이얼로그가 표시됨" }, { "step": 86, "name": "삭제 확인 다이얼로그 검증", - "phase": "DELETE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const dialog = document.querySelector('[role=\"alertdialog\"], [role=\"dialog\"], [class*=\"confirm\"], [class*=\"dialog\"]'); const text = document.body.innerText; const hasConfirm = dialog || text.includes('삭제') && (text.includes('확인') || text.includes('하시겠습니까')); return JSON.stringify({dialogOpen: !!dialog, hasConfirm, pass: hasConfirm}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "dialog:confirm-delete", + "expected": "'정말 삭제하시겠습니까?' 메시지가 표시됨" }, { "step": 87, "name": "삭제 취소 테스트 - 취소 버튼 클릭", - "phase": "DELETE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const cancelBtn = Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('취소') || b.innerText?.includes('Cancel')); if(cancelBtn) { cancelBtn.click(); return JSON.stringify({clicked: true}); } return JSON.stringify({clicked: false}); })()" - }, - {"type": "wait", "duration": 500} - ] + "action": "click_if_exists", + "target": "button:취소[dialog]", + "expected": "다이얼로그가 닫히고 삭제되지 않음" }, { "step": 88, "name": "삭제 취소 확인 - 품목이 여전히 존재함", - "phase": "DELETE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const text = document.body.innerText; const exists = text.includes('테스트 라벨'); return JSON.stringify({exists, pass: exists}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "table-row:테스트 라벨", + "expected": "소모품이 목록에 여전히 존재함" }, { "step": 89, "name": "삭제 재시도 - 삭제 버튼 클릭", - "phase": "DELETE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const rows = document.querySelectorAll('table tbody tr'); for(const row of rows) { if(row.innerText?.includes('테스트 라벨')) { const deleteBtn = Array.from(row.querySelectorAll('button')).find(b => b.innerText?.includes('삭제')); if(deleteBtn) { deleteBtn.click(); return JSON.stringify({clicked: true}); } } } return JSON.stringify({clicked: false}); })()" - }, - {"type": "wait", "duration": 1000} - ] + "action": "click_if_exists", + "target": "button:삭제[row=테스트 라벨]", + "expected": "삭제 확인 다이얼로그가 다시 표시됨" }, { "step": 90, "name": "삭제 확인 버튼 클릭", - "phase": "DELETE", - "actions": [ - { - "type": "click_dialog_confirm" - }, - {"type": "wait", "duration": 2000} - ] + "action": "click_if_exists", + "target": "button:확인[dialog]", + "expected": "삭제 API 호출 및 성공 메시지 표시" }, { "step": 91, - "name": "소모품 삭제 - URL 변경 여부 확인", - "phase": "DELETE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const url = window.location.href; const has404 = url.includes('404'); return JSON.stringify({url, has404, pass: !has404}); })()", - "expected": "pass:true" - } - ] + "name": "소모품 삭제 - URL 변경 여부 확인 (필수 검증 #2)", + "action": "verifyUrl", + "expected": "URL이 /production/screen-production 유지", + "validation": { + "notContains": [ + "404", + "not-found", + "error" + ] + } }, { "step": 92, "name": "소모품 삭제 - 성공 토스트 메시지 확인", - "phase": "DELETE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const toasts = document.querySelectorAll('[class*=\"toast\"], [role=\"alert\"]'); const found = Array.from(toasts).some(t => t.innerText?.includes('삭제')); return JSON.stringify({toastFound: found, pass: true}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "toast-message", + "expected": "'삭제되었습니다' 성공 메시지가 표시됨" }, { "step": 93, "name": "소모품 삭제 확인 - 목록에서 사라짐", - "phase": "DELETE", - "actions": [ - {"type": "wait", "duration": 1000}, - { - "type": "evaluate", - "script": "(function(){ const text = document.body.innerText; const notExists = !text.includes('테스트 라벨'); return JSON.stringify({notExists, pass: true}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "table-rows", + "expected": "삭제한 소모품이 목록에서 사라짐", + "validation": { + "notContains": "테스트 라벨" + } }, { "step": 94, "name": "제품 삭제 - 제품 검색", - "phase": "DELETE", - "actions": [ - { - "type": "fill", - "selector": "input[placeholder*='검색'], input[placeholder*='품목'], input[type='search']", - "value": "TEST-SCREEN-001" - }, - {"type": "wait", "duration": 1500} - ] + "action": "click_if_exists", + "target": "textbox:품목코드, 품목명, 규격 검색...", + "expected": "등록한 제품 검색" }, { "step": 95, "name": "제품 삭제 버튼 클릭", - "phase": "DELETE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const rows = document.querySelectorAll('table tbody tr'); for(const row of rows) { if(row.innerText?.includes('TEST-SCREEN-001')) { const deleteBtn = Array.from(row.querySelectorAll('button')).find(b => b.innerText?.includes('삭제')); if(deleteBtn) { deleteBtn.click(); return JSON.stringify({clicked: true}); } } } return JSON.stringify({clicked: false}); })()" - }, - {"type": "wait", "duration": 1000} - ] + "action": "click_if_exists", + "target": "button:삭제[row=TEST-SCREEN-001]", + "expected": "삭제 확인 다이얼로그가 표시됨" }, { "step": 96, "name": "제품 삭제 확인", - "phase": "DELETE", - "actions": [ - {"type": "click_dialog_confirm"}, - {"type": "wait", "duration": 2000} - ] + "action": "click_if_exists", + "target": "button:확인[dialog]", + "expected": "삭제 API 호출 및 성공 메시지 표시" }, { "step": 97, "name": "제품 삭제 - URL 변경 여부 확인", - "phase": "DELETE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const url = window.location.href; const has404 = url.includes('404'); return JSON.stringify({url, has404, pass: !has404}); })()", - "expected": "pass:true" - } - ] + "action": "verifyUrl", + "expected": "URL이 /production/screen-production 유지" }, { "step": 98, "name": "제품 삭제 - 성공 토스트 메시지 확인", - "phase": "DELETE", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const toasts = document.querySelectorAll('[class*=\"toast\"], [role=\"alert\"]'); const found = Array.from(toasts).some(t => t.innerText?.includes('삭제')); return JSON.stringify({toastFound: found, pass: true}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "toast-message", + "expected": "'삭제되었습니다' 성공 메시지가 표시됨" }, { "step": 99, "name": "제품 삭제 확인 - 목록에서 사라짐", - "phase": "DELETE", - "actions": [ - {"type": "wait", "duration": 1000}, - { - "type": "evaluate", - "script": "(function(){ const text = document.body.innerText; const notExists = !text.includes('TEST-SCREEN-001'); return JSON.stringify({notExists, pass: true}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "table-rows", + "expected": "삭제한 제품이 목록에서 사라짐", + "validation": { + "notContains": "TEST-SCREEN-001" + } }, { "step": 100, "name": "최종 테스트 완료 확인", - "phase": "VERIFY", - "actions": [ - { - "type": "evaluate", - "script": "(function(){ const hasTitle = document.querySelector('h1, h2')?.innerText?.includes('품목') || document.body.innerText.includes('품목 관리'); const hasTable = !!document.querySelector('table'); const hasPagination = document.body.innerText.includes('전체') || document.body.innerText.includes('페이지'); return JSON.stringify({hasTitle, hasTable, hasPagination, pass: hasTitle && hasTable}); })()", - "expected": "pass:true" - } - ] + "action": "verify", + "target": "page", + "expected": "품목 관리 페이지가 정상 상태로 유지됨", + "validation": { + "pageTitle": "품목 관리", + "hasStatistics": true, + "hasTable": true, + "hasPagination": true + } } ] -} +} \ No newline at end of file diff --git a/payment-history.json b/payment-history.json index 97fefee..1359476 100644 --- a/payment-history.json +++ b/payment-history.json @@ -170,18 +170,7 @@ "name": "페이지 구조 확인", "description": "페이지 타이틀, 설명 확인", "actions": [ - { "type": "wait", "duration": 500 }, - { - "type": "verify_element", - "selector": "h1, h2, [class*='title'], [class*='header'] h1, [class*='header'] h2, [class*='page-title']", - "description": "페이지 타이틀 요소 존재 확인" - }, - { - "type": "verify_text", - "selector": "h1, h2, [class*='title'], [class*='header']", - "contains": ["결제", "내역", "Payment"], - "description": "페이지 타이틀에 '결제' 또는 '내역' 텍스트 포함 확인" - } + { "type": "verify", "target": "페이지 구조" } ], "expected": { "pageTitle": "결제내역", @@ -193,22 +182,7 @@ "name": "테이블 구조 확인", "description": "결제 내역 테이블의 컬럼 헤더 확인", "actions": [ - { "type": "wait", "duration": 500 }, - { - "type": "verify_element", - "selector": "table, [role='table'], [class*='table'], [class*='grid'], [class*='list']", - "description": "테이블 또는 그리드 요소 존재 확인" - }, - { - "type": "verify_element", - "selector": "table thead th, table th, [role='columnheader'], [class*='header'] [class*='cell'], [class*='table-header']", - "description": "테이블 헤더 컬럼 존재 확인" - }, - { - "type": "evaluate", - "script": "const headers = document.querySelectorAll('table thead th, table th, [role=\"columnheader\"]'); return headers.length > 0 ? `테이블 컬럼 ${headers.length}개 발견` : '컬럼 없음';", - "description": "테이블 컬럼 개수 확인" - } + { "type": "verify", "target": "table columns" } ], "expected": { "tableExists": true, @@ -220,18 +194,7 @@ "name": "데이터 로드 확인", "description": "결제 내역 데이터가 테이블에 표시되는지 확인", "actions": [ - { "type": "wait", "duration": 1000 }, - { - "type": "evaluate", - "script": "const rows = document.querySelectorAll('table tbody tr, [role=\"row\"]:not([role=\"columnheader\"]), [class*=\"table-row\"], [class*=\"list-item\"]'); const emptyMsg = document.body.innerText.includes('데이터가 없습니다') || document.body.innerText.includes('No data') || document.body.innerText.includes('결과가 없습니다'); return rows.length > 0 ? `데이터 ${rows.length}행 표시됨` : (emptyMsg ? '빈 데이터 메시지 표시됨 (정상)' : '데이터 확인 필요');", - "description": "테이블 데이터 행 또는 빈 데이터 메시지 확인" - }, - { - "type": "verify_element", - "selector": "table tbody, [role='rowgroup'], [class*='table-body'], [class*='list-body']", - "optional": true, - "description": "테이블 본문 영역 확인" - } + { "type": "verify", "target": "table data" } ], "expected": { "dataExists": "데이터 행 존재 또는 '데이터가 없습니다' 메시지" @@ -242,33 +205,18 @@ "name": "페이지네이션 확인", "description": "테이블 하단에 페이지네이션이 표시되는지 확인", "actions": [ - { "type": "wait", "duration": 500 }, - { - "type": "evaluate", - "script": "const pagination = document.querySelector('[class*=\"pagination\"], [class*=\"pager\"], [aria-label*=\"pagination\"], nav[class*=\"page\"], [class*=\"Pagination\"]'); const pageButtons = document.querySelectorAll('button[class*=\"page\"], a[class*=\"page\"], [aria-label*=\"page\"]'); return pagination ? '페이지네이션 컴포넌트 존재' : (pageButtons.length > 0 ? `페이지 버튼 ${pageButtons.length}개 발견` : '페이지네이션 없음 (데이터 적음 또는 미구현)');", - "description": "페이지네이션 컴포넌트 확인" - } + { "type": "verify", "target": "pagination component" } ], "expected": { "paginationExists": true - }, - "optional": true + } }, { "id": 9, "name": "콘솔 에러 확인", "description": "페이지 동작 중 콘솔에 에러가 발생하지 않는지 확인", "actions": [ - { - "type": "evaluate", - "script": "const logs = window.__API_LOGS__ || []; const errors = logs.filter(l => !l.ok || l.status >= 400); return errors.length === 0 ? 'API 에러 없음' : `API 에러 ${errors.length}개: ${errors.map(e => e.url + ' (' + e.status + ')').join(', ')}';", - "description": "API 에러 로그 확인" - }, - { - "type": "verify_page", - "notContains": ["500", "Internal Server Error", "서버 오류"], - "description": "서버 에러 메시지 미표시 확인" - } + { "type": "verify", "target": "console errors" } ], "expected": { "noErrors": "콘솔 에러 없음" diff --git a/popup-management.json b/popup-management.json index 9b8bd9b..9344503 100644 --- a/popup-management.json +++ b/popup-management.json @@ -3,53 +3,26 @@ "name": "설정 - 팝업관리", "screenshotPolicy": { "onErrorOnly": true, - "captureOn": ["error", "fail", "timeout", "404", "500", "blocked"] + "captureOn": [ + "error", + "fail", + "timeout", + "404", + "500", + "blocked" + ] }, "description": "팝업 관리 기능 테스트 - 목록 조회, 검색, 등록, 수정, 삭제 기능", "baseUrl": "https://dev.codebridge-x.com", "url": "/ko/settings/popup-management", - "selectors": { - "pageTitle": "h1, h2, [class*='title'], [class*='Title']", - "pageDescription": "p[class*='description'], p[class*='Description'], .text-muted, [class*='subtitle']", - "registerButton": "button:has-text('팝업 등록'), button:has-text('등록'), a:has-text('팝업 등록')", - "searchInput": "input[type='search'], input[placeholder*='검색'], input[name*='search']", - "table": "table, [role='table'], [class*='table'], [class*='Table']", - "tableHeader": "table thead, [role='rowgroup'] [role='columnheader'], th", - "tableBody": "table tbody, [role='rowgroup']:last-child", - "tableRow": "table tbody tr, [role='row']", - "tableCell": "table tbody td, [role='cell']", - "pagination": "[class*='pagination'], [class*='Pagination'], nav[aria-label*='pagination']", - "itemCount": "[class*='count'], [class*='Count'], [class*='total'], span:has-text('전체')", - "modal": "[role='dialog'], [class*='modal'], [class*='Modal'], [class*='Dialog']", - "modalTitle": "[role='dialog'] h2, [class*='modal'] h2, [class*='Modal'] h2", - "modalClose": "[role='dialog'] button[class*='close'], [class*='modal'] button:has-text('닫기')", - "toast": "[class*='toast'], [class*='Toast'], [class*='notification'], [class*='Notification'], [role='alert']", - "combobox": "[role='combobox'], select, [class*='select'], [class*='Select']", - "comboboxOption": "[role='option'], option, [class*='option'], [class*='Option']", - "radioGroup": "[role='radiogroup'], fieldset:has(input[type='radio'])", - "radio": "input[type='radio'], [role='radio']", - "textInput": "input[type='text'], input:not([type]), textarea", - "dateInput": "input[type='date'], input[placeholder*='날짜'], [class*='date-picker']", - "editor": "[class*='editor'], [class*='Editor'], [contenteditable='true'], .ProseMirror, .tiptap", - "editorToolbar": "[class*='toolbar'], [class*='Toolbar'], [class*='editor-menu']", - "submitButton": "button[type='submit'], button:has-text('등록'), button:has-text('저장')", - "cancelButton": "button:has-text('취소'), button:has-text('Cancel')", - "deleteButton": "button:has-text('삭제'), button:has-text('Delete')", - "editButton": "button:has-text('수정'), button:has-text('Edit')", - "listButton": "button:has-text('목록'), button:has-text('목록으로')", - "confirmDialog": "[role='alertdialog'], [class*='confirm'], [class*='Confirm']", - "confirmButton": "[role='alertdialog'] button:has-text('확인'), [class*='confirm'] button:has-text('확인')", - "badge": "[class*='badge'], [class*='Badge'], [class*='status']", - "definitionList": "dl, [class*='definition'], [class*='info-list']", - "definitionTerm": "dt, [class*='label'], [class*='term']", - "definitionDesc": "dd, [class*='value'], [class*='desc']", - "sectionTitle": "h3, h4, [class*='section-title'], [class*='card-title']", - "disabledInput": "input[disabled], input[readonly], [class*='disabled']" - }, "navigation": { "targetUrl": "/settings/popup-management", "urlPattern": "/settings/popup-management|/ko/settings/popup-management", - "menuHints": ["팝업관리", "팝업 관리", "설정"] + "menuHints": [ + "팝업관리", + "팝업 관리", + "설정" + ] }, "menuNavigation": { "level1": "설정", @@ -62,12 +35,58 @@ "username": "TestUser5", "password": "password123!" }, + "menuNavigationEnhanced": { + "strategy": "scroll-and-search", + "sidebarSelector": ".sidebar-scroll, [data-testid='sidebar'], nav[role='navigation']", + "scrollConfig": { + "scrollStep": 200, + "maxScrollAttempts": 10, + "scrollDelay": 300 + }, + "level1": { + "text": "설정", + "fallbackSelectors": [ + "button:has-text('설정')", + "[data-menu='settings']", + "a[href*='settings']" + ] + }, + "level2": { + "text": "팝업관리", + "fallbackSelectors": [ + "a:has-text('팝업관리')", + "[data-submenu='popup-management']", + "a[href*='popup-management']" + ] + }, + "fallbackDirectUrl": "/ko/settings/popup-management" + }, "expectedAPIs": [ - { "method": "GET", "path": "/api/v1/settings/popups", "description": "팝업 목록 조회" }, - { "method": "GET", "path": "/api/v1/settings/popups/:id", "description": "팝업 상세 조회" }, - { "method": "POST", "path": "/api/v1/settings/popups", "description": "팝업 등록" }, - { "method": "PUT", "path": "/api/v1/settings/popups/:id", "description": "팝업 수정" }, - { "method": "DELETE", "path": "/api/v1/settings/popups/:id", "description": "팝업 삭제" } + { + "method": "GET", + "path": "/api/v1/settings/popups", + "description": "팝업 목록 조회" + }, + { + "method": "GET", + "path": "/api/v1/settings/popups/:id", + "description": "팝업 상세 조회" + }, + { + "method": "POST", + "path": "/api/v1/settings/popups", + "description": "팝업 등록" + }, + { + "method": "PUT", + "path": "/api/v1/settings/popups/:id", + "description": "팝업 수정" + }, + { + "method": "DELETE", + "path": "/api/v1/settings/popups/:id", + "description": "팝업 삭제" + } ], "steps": [ { @@ -75,617 +94,955 @@ "name": "사이드바 메뉴 전체 펼치기", "description": "모두 펼치기 버튼을 클릭하여 전체 메뉴를 펼친 후 메뉴 탐색 준비", "actions": [ - { "type": "evaluate", "script": "document.querySelector('.sidebar-scroll')?.scrollTo({top:0,behavior:'instant'})" }, - { "type": "wait", "duration": 300 }, - { "type": "evaluate", "script": "Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('모두 펼치기'))?.click()" }, - { "type": "wait", "duration": 2000 } + { + "type": "evaluate", + "script": "document.querySelector('.sidebar-scroll')?.scrollTo({top:0,behavior:'instant'})" + }, + { + "type": "wait", + "duration": 300 + }, + { + "type": "evaluate", + "script": "Array.from(document.querySelectorAll('button')).find(b => b.innerText?.includes('모두 펼치기'))?.click()" + }, + { + "type": "wait", + "duration": 2000 + } + ], + "expected": { + "sidebarReady": true + }, + "validation": [ + "사이드바 스크롤 초기화" ] }, { "step": 1, "name": "2단계 메뉴 진입: 설정 > 팝업관리", - "description": "설정 > 팝업관리 메뉴로 이동", + "description": "설정 > 팝업관리 메뉴로 이동하여 페이지 로드 확인 (scrollAndFind 패턴 사용)", + "navigationPattern": "scrollAndFind", "actions": [ - { "type": "menu_navigate", "level1": "설정", "level2": "팝업관리" }, - { "type": "wait", "duration": 2000 } + { + "type": "scrollAndFind", + "target": "설정", + "scrollContainer": ".sidebar-scroll, [data-testid='sidebar'], nav[role='navigation']", + "scrollStep": 200, + "maxAttempts": 10 + }, + { + "type": "click_if_exists", + "target": "설정" + }, + { + "type": "wait", + "duration": 500 + }, + { + "type": "scrollAndFind", + "target": "팝업관리", + "scrollContainer": ".sidebar-scroll, [data-testid='sidebar'], nav[role='navigation']", + "scrollStep": 200, + "maxAttempts": 5 + }, + { + "type": "click_if_exists", + "target": "팝업관리" + }, + { + "type": "wait", + "target": "페이지 로드 완료" + } + ], + "fallback": { + "type": "directNavigation", + "url": "/ko/settings/popup-management" + }, + "expected": { + "url": "/ko/settings/popup-management", + "title": "팝업관리", + "authenticated": true + }, + "validation": [ + "페이지 제목 확인", + "테이블 표시 확인" ] }, { "step": 2, - "name": "페이지 URL 확인", - "action": "verify_url", - "urlPattern": "/settings/popup-management", - "expected": "팝업관리 페이지 URL" + "name": "페이지 제목 확인", + "action": "verify", + "target": "heading '팝업관리'", + "expected": "'팝업관리' 제목 표시됨", + "validation": [ + "UI 렌더링" + ] }, { "step": 3, - "name": "페이지 제목 확인", - "action": "verify_element", - "selector": "h1, h2, [class*='title']", - "textContains": "팝업", - "expected": "페이지 제목에 '팝업' 텍스트 포함" + "name": "페이지 설명 확인", + "action": "verify", + "target": "paragraph '팝업 목록을 관리합니다.'", + "expected": "설명 텍스트 표시됨", + "validation": [ + "UI 렌더링" + ] }, { "step": 4, - "name": "페이지 설명 확인", - "action": "evaluate", - "script": "(() => { const desc = document.body.innerText; return desc.includes('팝업') && (desc.includes('목록') || desc.includes('관리')); })()", - "expected": "페이지 설명 텍스트 표시됨" + "name": "팝업 등록 버튼 확인", + "action": "verify", + "target": "button '팝업 등록'", + "expected": "'팝업 등록' 버튼 표시됨", + "validation": [ + "UI 렌더링" + ] }, { "step": 5, - "name": "팝업 등록 버튼 확인", - "action": "verify_element", - "selector": "button", - "textContains": "등록", - "expected": "등록 버튼 표시됨" + "name": "검색 입력 필드 확인", + "action": "verify", + "target": "textbox '제목, 작성자로 검색...'", + "expected": "검색 필드 표시됨", + "validation": [ + "UI 렌더링" + ] }, { "step": 6, - "name": "검색 입력 필드 확인", - "action": "verify_element", - "selector": "input[type='search'], input[placeholder*='검색'], input[name*='search']", - "expected": "검색 필드 표시됨" + "name": "테이블 헤더 확인", + "action": "verify", + "target": "table headers", + "expected": "번호, 대상, 제목, 상태, 작성자, 등록일, 기간 컬럼 표시됨", + "validation": [ + "UI 렌더링" + ] }, { "step": 7, - "name": "테이블 존재 확인", - "action": "verify_element", - "selector": "table, [role='table'], [class*='table'], [class*='Table']", - "expected": "데이터 테이블 표시됨" + "name": "테이블 데이터 행 확인", + "action": "verify", + "target": "table rows", + "expected": "8개 데이터 행 표시됨", + "validation": [ + "데이터 로드" + ] }, { "step": 8, - "name": "테이블 헤더 확인", - "action": "evaluate", - "script": "(() => { const headers = Array.from(document.querySelectorAll('th, [role=\"columnheader\"]')).map(h => h.innerText.trim()); return headers.length >= 3; })()", - "expected": "테이블 헤더 3개 이상 존재" + "name": "전체 항목 수 표시 확인", + "action": "verify", + "target": "text '전체 8개 중 1-8개 표시'", + "expected": "전체 항목 수 표시됨", + "validation": [ + "UI 렌더링" + ] }, { "step": 9, - "name": "테이블 데이터 행 확인", - "action": "evaluate", - "script": "(() => { const rows = document.querySelectorAll('table tbody tr, [role=\"row\"]:not(:first-child)'); return rows.length > 0; })()", - "expected": "테이블에 데이터 행 존재" + "name": "검색 기능 - 제목으로 검색", + "action": "click_if_exists", + "target": "textbox '제목, 작성자로 검색...'", + "value": "시스템", + "expected": "검색어 입력됨", + "validation": [ + "검색/필터" + ] }, { "step": 10, - "name": "전체 항목 수 표시 확인", - "action": "evaluate", - "script": "(() => { const text = document.body.innerText; return text.includes('전체') || text.includes('total') || /\\d+개/.test(text); })()", - "expected": "전체 항목 수 표시됨" + "name": "검색 결과 확인", + "action": "verify", + "target": "table rows", + "expected": "'시스템' 키워드 포함 행만 표시됨", + "validation": [ + "검색/필터" + ] }, { "step": 11, - "name": "검색 기능 - 검색어 입력", - "action": "fill", - "selector": "input[type='search'], input[placeholder*='검색'], input[name*='search']", - "value": "시스템", - "expected": "검색어 입력됨" + "name": "검색어 초기화", + "action": "click_if_exists", + "target": "textbox '제목, 작성자로 검색...'", + "expected": "검색어 지워짐", + "validation": [ + "검색/필터" + ] }, { "step": 12, - "name": "검색 결과 대기", - "action": "wait", - "duration": 1000 + "name": "전체 목록 재표시 확인", + "action": "verify", + "target": "table rows", + "expected": "전체 8개 행 다시 표시됨", + "validation": [ + "검색/필터" + ] }, { "step": 13, - "name": "검색 결과 확인", - "action": "evaluate", - "script": "(() => { const rows = document.querySelectorAll('table tbody tr'); return rows.length >= 0; })()", - "expected": "검색 결과 표시됨" + "name": "팝업 등록 페이지 이동", + "action": "click_if_exists", + "target": "button '팝업 등록'", + "expected": "/settings/popup-management?mode=new 페이지로 이동", + "validation": [ + "등록/저장" + ] }, { "step": 14, - "name": "검색어 초기화", - "action": "clear", - "selector": "input[type='search'], input[placeholder*='검색'], input[name*='search']", - "expected": "검색어 지워짐" + "name": "등록 페이지 URL 확인", + "action": "verify", + "target": "url", + "expected": "URL이 /settings/popup-management?mode=new", + "validation": [ + "등록/저장" + ] }, { "step": 15, - "name": "전체 목록 재표시 대기", - "action": "wait", - "duration": 1000 + "name": "등록 페이지 제목 확인", + "action": "verify", + "target": "heading '팝업관리 상세'", + "expected": "'팝업관리 상세' 제목 표시됨", + "validation": [ + "UI 렌더링" + ] }, { "step": 16, - "name": "팝업 등록 버튼 클릭", - "action": "click", - "selector": "button", - "textContains": "등록", - "phase": "CREATE", - "expected": "등록 페이지로 이동" + "name": "팝업 정보 섹션 확인", + "action": "verify", + "target": "heading '팝업 정보 *'", + "expected": "'팝업 정보 *' 섹션 표시됨", + "validation": [ + "UI 렌더링" + ] }, { "step": 17, - "name": "등록 페이지 로드 대기", - "action": "wait", - "duration": 2000 + "name": "대상 Combobox 확인", + "action": "verify", + "target": "combobox (대상)", + "expected": "대상 선택 combobox 표시됨 (기본값: 전사)", + "validation": [ + "UI 렌더링" + ] }, { "step": 18, - "name": "등록 페이지 URL 확인", - "action": "verify_url", - "urlPattern": "mode=new|popup-management/new", - "phase": "CREATE", - "expected": "등록 페이지 URL" + "name": "대상 Combobox 클릭", + "action": "click_if_exists", + "target": "combobox (대상)", + "expected": "드롭다운 옵션 표시됨", + "validation": [ + "UI 동작" + ] }, { "step": 19, - "name": "등록 페이지 제목 확인", - "action": "verify_element", - "selector": "h1, h2, [class*='title']", - "textContains": "팝업", - "phase": "CREATE", - "expected": "등록 페이지 제목 표시됨" + "name": "대상 옵션 확인", + "action": "verify", + "target": "combobox options", + "expected": "'전사', '부서별' 옵션 표시됨", + "validation": [ + "UI 렌더링" + ] }, { "step": 20, - "name": "대상 Combobox 확인", - "action": "verify_element", - "selector": "[role='combobox'], select, [class*='select'], [class*='Select'], button[class*='combobox']", - "phase": "CREATE", - "expected": "대상 선택 combobox 표시됨" + "name": "대상 '부서별' 선택", + "action": "click_if_exists", + "target": "option '부서별'", + "expected": "'부서별' 선택됨", + "validation": [ + "UI 동작" + ] }, { "step": 21, - "name": "대상 Combobox 클릭", - "action": "click", - "selector": "[role='combobox'], select, [class*='select']:first-of-type button, button[class*='combobox']", - "phase": "CREATE", - "expected": "드롭다운 옵션 표시됨" + "name": "기간 시작일 필드 확인", + "action": "verify", + "target": "textbox (기간 시작일)", + "expected": "시작일 입력 필드 표시됨 (기본값: 오늘 날짜)", + "validation": [ + "UI 렌더링" + ] }, { "step": 22, - "name": "대상 옵션 표시 대기", - "action": "wait", - "duration": 500 + "name": "기간 종료일 필드 확인", + "action": "verify", + "target": "textbox (기간 종료일)", + "expected": "종료일 입력 필드 표시됨 (기본값: 오늘 날짜)", + "validation": [ + "UI 렌더링" + ] }, { "step": 23, - "name": "대상 옵션 선택 (전사)", - "action": "click_if_exists", - "selector": "[role='option'], [class*='option'], [class*='Option'], li", - "textContains": "전사", - "phase": "CREATE", - "expected": "전사 선택됨" + "name": "제목 필드 확인", + "action": "verify", + "target": "textbox '제목 *'", + "expected": "제목 입력 필드 표시됨", + "validation": [ + "UI 렌더링" + ] }, { "step": 24, - "name": "기간 시작일 필드 확인", - "action": "verify_element", - "selector": "input[type='date'], input[placeholder*='시작'], input[name*='start']", - "phase": "CREATE", - "expected": "시작일 입력 필드 표시됨" + "name": "제목 입력", + "action": "click_if_exists", + "target": "textbox '제목 *'", + "value": "E2E 테스트 팝업", + "expected": "제목 입력됨", + "validation": [ + "데이터 입력" + ] }, { "step": 25, - "name": "기간 종료일 필드 확인", - "action": "verify_element", - "selector": "input[type='date'], input[placeholder*='종료'], input[name*='end']", - "phase": "CREATE", - "expected": "종료일 입력 필드 표시됨" + "name": "내용 편집기 확인", + "action": "verify", + "target": "editor toolbar", + "expected": "텍스트 편집 도구 모음 표시됨 (굵게, 기울임, 밑줄, 취소선, 정렬, 리스트, 링크, 이미지)", + "validation": [ + "UI 렌더링" + ] }, { "step": 26, - "name": "제목 필드 확인", - "action": "verify_element", - "selector": "input[name*='title'], input[placeholder*='제목'], input[id*='title']", - "phase": "CREATE", - "expected": "제목 입력 필드 표시됨" + "name": "내용 입력 영역 확인", + "action": "verify", + "target": "paragraph '내용을 입력해주세요'", + "expected": "내용 입력 영역 표시됨", + "validation": [ + "UI 렌더링" + ] }, { "step": 27, - "name": "제목 입력", - "action": "fill", - "selector": "input[name*='title'], input[placeholder*='제목'], input[id*='title']", - "value": "E2E_TEST_팝업_{timestamp}", - "useTimestamp": true, - "phase": "CREATE", - "expected": "제목 입력됨" + "name": "내용 입력", + "action": "click_if_exists", + "target": "editor content area", + "value": "이것은 E2E 테스트용 팝업입니다.", + "expected": "내용 입력됨", + "validation": [ + "데이터 입력" + ] }, { "step": 28, - "name": "내용 편집기 확인", - "action": "verify_element", - "selector": "[class*='editor'], [class*='Editor'], [contenteditable='true'], .ProseMirror, .tiptap, textarea", - "phase": "CREATE", - "expected": "내용 편집기 표시됨" + "name": "상태 Radio 버튼 확인", + "action": "verify", + "target": "radiogroup (상태)", + "expected": "'사용안함', '사용함' 라디오 버튼 표시됨", + "validation": [ + "UI 렌더링" + ] }, { "step": 29, - "name": "내용 입력", - "action": "evaluate", - "script": "(() => { const editor = document.querySelector('[contenteditable=\"true\"], .ProseMirror, .tiptap, textarea'); if (editor) { if (editor.tagName === 'TEXTAREA') { editor.value = 'E2E 테스트용 팝업 내용입니다.'; editor.dispatchEvent(new Event('input', {bubbles:true})); } else { editor.innerHTML = '

E2E 테스트용 팝업 내용입니다.

'; editor.dispatchEvent(new Event('input', {bubbles:true})); } return true; } return false; })()", - "phase": "CREATE", - "expected": "내용 입력됨" + "name": "기본 상태 확인", + "action": "verify", + "target": "radio '사용안함'", + "expected": "'사용안함' 선택됨 (기본값)", + "validation": [ + "UI 렌더링" + ] }, { "step": 30, - "name": "상태 Radio 버튼 확인", - "action": "verify_element", - "selector": "input[type='radio'], [role='radio'], [class*='radio'], [class*='Radio']", - "phase": "CREATE", - "expected": "상태 라디오 버튼 표시됨" + "name": "상태 '사용함' 선택", + "action": "click_if_exists", + "target": "radio '사용함'", + "expected": "'사용함' 선택됨", + "validation": [ + "UI 동작" + ] }, { "step": 31, - "name": "상태 '사용함' 선택", - "action": "evaluate", - "script": "(() => { const radios = Array.from(document.querySelectorAll('input[type=\"radio\"], [role=\"radio\"]')); const useRadio = radios.find(r => r.labels?.[0]?.innerText?.includes('사용함') || r.parentElement?.innerText?.includes('사용함') || r.nextSibling?.textContent?.includes('사용함')); if (useRadio) { useRadio.click(); return true; } const labels = Array.from(document.querySelectorAll('label')); const label = labels.find(l => l.innerText?.includes('사용함')); if (label) { label.click(); return true; } return false; })()", - "phase": "CREATE", - "expected": "'사용함' 선택됨" + "name": "작성자 필드 확인", + "action": "verify", + "target": "textbox (작성자) [disabled]", + "expected": "작성자 필드 표시됨 (비활성화, 자동 설정: 홍길동)", + "validation": [ + "UI 렌더링" + ] }, { "step": 32, - "name": "취소 버튼 확인", - "action": "verify_element", - "selector": "button", - "textContains": "취소", - "phase": "CREATE", - "expected": "'취소' 버튼 표시됨" + "name": "등록일시 필드 확인", + "action": "verify", + "target": "textbox (등록일시) [disabled]", + "expected": "등록일시 필드 표시됨 (비활성화, 자동 설정)", + "validation": [ + "UI 렌더링" + ] }, { "step": 33, - "name": "등록 버튼 확인", - "action": "verify_element", - "selector": "button[type='submit'], button", - "textContains": "등록", - "phase": "CREATE", - "expected": "'등록' 버튼 표시됨" + "name": "취소 버튼 확인", + "action": "verify", + "target": "button '취소'", + "expected": "'취소' 버튼 표시됨", + "validation": [ + "UI 렌더링" + ] }, { "step": 34, - "name": "등록 전 URL 저장", - "action": "save_url", - "variableName": "url_before_create", - "phase": "CREATE" + "name": "등록 버튼 확인", + "action": "verify", + "target": "button '등록'", + "expected": "'등록' 버튼 표시됨", + "validation": [ + "UI 렌더링" + ] }, { "step": 35, - "name": "등록 버튼 클릭", - "action": "click", - "selector": "button[type='submit'], button:has-text('등록')", - "textContains": "등록", - "critical": true, - "phase": "CREATE", - "expected": "팝업 등록 요청 전송" + "name": "등록 전 URL 저장", + "action": "store", + "target": "current url", + "expected": "URL 저장됨", + "validation": [ + "등록/저장" + ] }, { "step": 36, - "name": "등록 처리 대기", - "action": "wait", - "duration": 2000, - "phase": "CREATE" + "name": "등록 버튼 클릭", + "action": "click_if_exists", + "target": "button '등록'", + "expected": "팝업 등록 요청 전송", + "validation": [ + "등록/저장" + ] }, { "step": 37, - "name": "등록 성공 확인 (토스트 또는 URL 변경)", - "action": "evaluate", - "script": "(() => { const toast = document.querySelector('[class*=\"toast\"], [class*=\"Toast\"], [role=\"alert\"], [class*=\"notification\"]'); const hasSuccessToast = toast && (toast.innerText.includes('등록') || toast.innerText.includes('완료') || toast.innerText.includes('성공')); const urlChanged = !window.location.href.includes('mode=new'); return hasSuccessToast || urlChanged; })()", - "phase": "CREATE", - "expected": "등록 성공" + "name": "등록 후 URL 확인", + "action": "verify", + "target": "url", + "expected": "URL이 /settings/popup-management (목록 페이지로 이동)", + "validation": [ + "등록/저장" + ] }, { "step": 38, - "name": "목록 페이지로 이동 확인", - "action": "evaluate", - "script": "(() => { const url = window.location.href; return url.includes('popup-management') && !url.includes('mode=new'); })()", - "phase": "CREATE", - "expected": "목록 페이지로 이동됨" + "name": "등록 성공 토스트 확인", + "action": "verify", + "target": "toast message", + "expected": "'팝업이 등록되었습니다' 토스트 표시됨", + "validation": [ + "등록/저장" + ] }, { "step": 39, - "name": "목록에서 등록된 데이터 확인", - "action": "evaluate", - "script": "(() => { const text = document.body.innerText; return text.includes('E2E_TEST_팝업') || text.includes('E2E'); })()", - "phase": "CREATE", - "expected": "등록된 팝업이 목록에 표시됨" + "name": "등록 API 호출 확인", + "action": "verify", + "target": "network request", + "expected": "POST /api/v1/settings/popups 호출됨 (200 OK)", + "validation": [ + "등록/저장" + ] }, { "step": 40, - "name": "첫 번째 팝업 행 클릭 (상세 보기)", - "action": "click_first_row", - "phase": "READ", - "expected": "상세 페이지로 이동" + "name": "신규 팝업 목록 확인", + "action": "verify", + "target": "table rows", + "expected": "신규 등록된 팝업이 목록에 표시됨", + "validation": [ + "데이터 지속성" + ] }, { "step": 41, - "name": "상세 페이지 로드 대기", - "action": "wait", - "duration": 2000, - "phase": "READ" + "name": "첫 번째 팝업 행 클릭", + "action": "click_if_exists", + "target": "row (첫 번째 팝업)", + "expected": "상세 페이지로 이동", + "validation": [ + "UI 동작" + ] }, { "step": 42, "name": "상세 페이지 URL 확인", - "action": "evaluate", - "script": "(() => { const url = window.location.href; return url.includes('popup-management') && /\\/\\d+/.test(url); })()", - "phase": "READ", - "expected": "상세 페이지 URL (ID 포함)" + "action": "verify", + "target": "url", + "expected": "URL이 /settings/popup-management/1", + "validation": [ + "UI 동작" + ] }, { "step": 43, "name": "상세 페이지 제목 확인", - "action": "verify_element", - "selector": "h1, h2, [class*='title']", - "textContains": "팝업", - "phase": "READ", - "expected": "상세 페이지 제목 표시됨" + "action": "verify", + "target": "heading '팝업관리 상세'", + "expected": "'팝업관리 상세' 제목 표시됨", + "validation": [ + "UI 렌더링" + ] }, { "step": 44, "name": "팝업 정보 섹션 확인", - "action": "evaluate", - "script": "(() => { const text = document.body.innerText; return text.includes('팝업') && (text.includes('정보') || text.includes('상세')); })()", - "phase": "READ", - "expected": "팝업 정보 섹션 표시됨" + "action": "verify", + "target": "heading '팝업 정보'", + "expected": "'팝업 정보' 섹션 표시됨", + "validation": [ + "UI 렌더링" + ] }, { "step": 45, - "name": "대상 정보 표시 확인", - "action": "evaluate", - "script": "(() => { const text = document.body.innerText; return text.includes('대상') && (text.includes('전사') || text.includes('부서')); })()", - "phase": "READ", - "expected": "대상 정보 표시됨" + "name": "상태 뱃지 확인", + "action": "verify", + "target": "badge (상태)", + "expected": "'사용함' 뱃지 표시됨", + "validation": [ + "UI 렌더링" + ] }, { "step": 46, - "name": "제목 정보 표시 확인", - "action": "evaluate", - "script": "(() => { const text = document.body.innerText; return text.includes('제목') || document.querySelector('h1, h2, h3'); })()", - "phase": "READ", - "expected": "제목 정보 표시됨" + "name": "대상 정보 확인", + "action": "verify", + "target": "definition (대상)", + "expected": "'전사' 표시됨", + "validation": [ + "데이터 로드" + ] }, { "step": 47, - "name": "상태 정보 표시 확인", - "action": "evaluate", - "script": "(() => { const text = document.body.innerText; return text.includes('상태') && (text.includes('사용함') || text.includes('사용안함')); })()", - "phase": "READ", - "expected": "상태 정보 표시됨" + "name": "작성자 정보 확인", + "action": "verify", + "target": "definition (작성자)", + "expected": "작성자명 표시됨", + "validation": [ + "데이터 로드" + ] }, { "step": 48, - "name": "목록으로 버튼 확인", - "action": "verify_element", - "selector": "button, a", - "textContains": "목록", - "phase": "READ", - "expected": "'목록으로' 버튼 표시됨" + "name": "제목 정보 확인", + "action": "verify", + "target": "definition (제목)", + "expected": "'시스템 점검 안내' 표시됨", + "validation": [ + "데이터 로드" + ] }, { "step": 49, - "name": "삭제 버튼 확인", - "action": "verify_element", - "selector": "button", - "textContains": "삭제", - "phase": "READ", - "expected": "'삭제' 버튼 표시됨" + "name": "상태 정보 확인", + "action": "verify", + "target": "definition (상태)", + "expected": "'사용함' 표시됨", + "validation": [ + "데이터 로드" + ] }, { "step": 50, - "name": "수정 버튼 확인", - "action": "verify_element", - "selector": "button", - "textContains": "수정", - "phase": "READ", - "expected": "'수정' 버튼 표시됨" + "name": "기간 정보 확인", + "action": "verify", + "target": "definition (기간)", + "expected": "기간 표시됨 (예: 2025-12-24 ~ 2026-01-08)", + "validation": [ + "데이터 로드" + ] }, { "step": 51, - "name": "수정 버튼 클릭", - "action": "click", - "selector": "button", - "textContains": "수정", - "phase": "UPDATE", - "expected": "수정 페이지로 이동" + "name": "등록일시 정보 확인", + "action": "verify", + "target": "definition (등록일시)", + "expected": "등록일 표시됨", + "validation": [ + "데이터 로드" + ] }, { "step": 52, - "name": "수정 페이지 로드 대기", - "action": "wait", - "duration": 2000, - "phase": "UPDATE" + "name": "내용 정보 확인", + "action": "verify", + "target": "definition (내용)", + "expected": "팝업 내용 표시됨", + "validation": [ + "데이터 로드" + ] }, { "step": 53, - "name": "수정 페이지 URL 확인", - "action": "verify_url", - "urlPattern": "mode=edit", - "phase": "UPDATE", - "expected": "수정 페이지 URL" + "name": "목록으로 버튼 확인", + "action": "verify", + "target": "button '목록으로'", + "expected": "'목록으로' 버튼 표시됨", + "validation": [ + "UI 렌더링" + ] }, { "step": 54, - "name": "기존 데이터 로드 확인 - 제목 필드", - "action": "evaluate", - "script": "(() => { const input = document.querySelector('input[name*=\"title\"], input[placeholder*=\"제목\"], input[id*=\"title\"]'); return input && input.value && input.value.length > 0; })()", - "phase": "UPDATE", - "expected": "기존 제목 데이터 로드됨" + "name": "삭제 버튼 확인", + "action": "verify", + "target": "button '삭제'", + "expected": "'삭제' 버튼 표시됨", + "validation": [ + "UI 렌더링" + ] }, { "step": 55, - "name": "제목 수정", - "action": "evaluate", - "script": "(() => { const input = document.querySelector('input[name*=\"title\"], input[placeholder*=\"제목\"], input[id*=\"title\"]'); if (input) { input.value = input.value + ' (수정됨)'; input.dispatchEvent(new Event('input', {bubbles:true})); return true; } return false; })()", - "phase": "UPDATE", - "expected": "제목 수정됨" + "name": "수정 버튼 확인", + "action": "verify", + "target": "button '수정'", + "expected": "'수정' 버튼 표시됨", + "validation": [ + "UI 렌더링" + ] }, { "step": 56, - "name": "내용 수정", - "action": "evaluate", - "script": "(() => { const editor = document.querySelector('[contenteditable=\"true\"], .ProseMirror, .tiptap, textarea'); if (editor) { if (editor.tagName === 'TEXTAREA') { editor.value = editor.value + ' - 수정된 내용'; editor.dispatchEvent(new Event('input', {bubbles:true})); } else { editor.innerHTML = editor.innerHTML + '

수정된 내용입니다.

'; editor.dispatchEvent(new Event('input', {bubbles:true})); } return true; } return false; })()", - "phase": "UPDATE", - "expected": "내용 수정됨" + "name": "수정 페이지 이동", + "action": "click_if_exists", + "target": "button '수정'", + "expected": "/settings/popup-management/1?mode=edit 페이지로 이동", + "validation": [ + "등록/저장" + ] }, { "step": 57, - "name": "저장 버튼 확인", - "action": "verify_element", - "selector": "button[type='submit'], button", - "textContains": "저장", - "phase": "UPDATE", - "expected": "'저장' 버튼 표시됨" + "name": "수정 페이지 URL 확인", + "action": "verify", + "target": "url", + "expected": "URL이 /settings/popup-management/1?mode=edit", + "validation": [ + "등록/저장" + ] }, { "step": 58, - "name": "저장 전 URL 저장", - "action": "save_url", - "variableName": "url_before_update", - "phase": "UPDATE" + "name": "수정 페이지 제목 확인", + "action": "verify", + "target": "heading '팝업관리 상세'", + "expected": "'팝업관리 상세' 제목 표시됨", + "validation": [ + "UI 렌더링" + ] }, { "step": 59, - "name": "저장 버튼 클릭", - "action": "click", - "selector": "button[type='submit'], button:has-text('저장')", - "textContains": "저장", - "critical": true, - "phase": "UPDATE", - "expected": "팝업 수정 요청 전송" + "name": "기존 데이터 로드 확인 - 대상", + "action": "verify", + "target": "combobox (대상)", + "expected": "'전사' 선택되어 있음", + "validation": [ + "데이터 로드" + ] }, { "step": 60, - "name": "저장 처리 대기", - "action": "wait", - "duration": 2000, - "phase": "UPDATE" + "name": "기존 데이터 로드 확인 - 제목", + "action": "verify", + "target": "textbox '제목 *'", + "expected": "'시스템 점검 안내' 입력되어 있음", + "validation": [ + "데이터 로드" + ] }, { "step": 61, - "name": "저장 성공 확인 (토스트 또는 URL 변경)", - "action": "evaluate", - "script": "(() => { const toast = document.querySelector('[class*=\"toast\"], [class*=\"Toast\"], [role=\"alert\"], [class*=\"notification\"]'); const hasSuccessToast = toast && (toast.innerText.includes('수정') || toast.innerText.includes('저장') || toast.innerText.includes('완료') || toast.innerText.includes('성공')); const urlChanged = !window.location.href.includes('mode=edit'); return hasSuccessToast || urlChanged; })()", - "phase": "UPDATE", - "expected": "저장 성공" + "name": "기존 데이터 로드 확인 - 내용", + "action": "verify", + "target": "editor content area", + "expected": "기존 내용 표시됨", + "validation": [ + "데이터 로드" + ] }, { "step": 62, - "name": "수정된 데이터 확인", - "action": "evaluate", - "script": "(() => { const text = document.body.innerText; return text.includes('수정됨') || text.includes('수정된'); })()", - "phase": "UPDATE", - "expected": "수정된 데이터 표시됨" + "name": "기존 데이터 로드 확인 - 상태", + "action": "verify", + "target": "radio '사용함'", + "expected": "'사용함' 선택되어 있음", + "validation": [ + "데이터 로드" + ] }, { "step": 63, - "name": "목록으로 이동", - "action": "click", - "selector": "button, a", - "textContains": "목록", - "phase": "UPDATE", - "expected": "목록 페이지로 이동" + "name": "제목 수정", + "action": "click_if_exists", + "target": "textbox '제목 *'", + "value": "시스템 점검 안내 (수정됨)", + "expected": "제목 수정됨", + "validation": [ + "데이터 입력" + ] }, { "step": 64, - "name": "목록 페이지 로드 대기", - "action": "wait", - "duration": 2000, - "phase": "UPDATE" + "name": "내용 수정", + "action": "click_if_exists", + "target": "editor content area", + "value": "수정된 내용입니다.", + "expected": "내용 수정됨", + "validation": [ + "데이터 입력" + ] }, { "step": 65, - "name": "목록 페이지 URL 확인", - "action": "evaluate", - "script": "(() => { const url = window.location.href; return url.includes('popup-management') && !url.includes('mode='); })()", - "phase": "UPDATE", - "expected": "목록 페이지로 이동됨" + "name": "상태 변경 - 사용안함 선택", + "action": "click_if_exists", + "target": "radio '사용안함'", + "expected": "'사용안함' 선택됨", + "validation": [ + "UI 동작" + ] }, { "step": 66, - "name": "테스트 데이터 검색 (삭제 준비)", - "action": "fill", - "selector": "input[type='search'], input[placeholder*='검색'], input[name*='search']", - "value": "E2E_TEST", - "phase": "DELETE", - "expected": "테스트 데이터 검색" + "name": "저장 버튼 확인", + "action": "verify", + "target": "button '저장'", + "expected": "'저장' 버튼 표시됨", + "validation": [ + "UI 렌더링" + ] }, { "step": 67, - "name": "검색 결과 대기", - "action": "wait", - "duration": 1500, - "phase": "DELETE" + "name": "저장 전 URL 저장", + "action": "store", + "target": "current url", + "expected": "URL 저장됨", + "validation": [ + "등록/저장" + ] }, { "step": 68, - "name": "테스트 데이터 행 클릭 (삭제 대상)", - "action": "click_first_row", - "phase": "DELETE", - "expected": "테스트 데이터 상세 페이지로 이동" + "name": "저장 버튼 클릭", + "action": "click_if_exists", + "target": "button '저장'", + "expected": "팝업 수정 요청 전송", + "validation": [ + "등록/저장" + ] }, { "step": 69, - "name": "상세 페이지 로드 대기", - "action": "wait", - "duration": 2000, - "phase": "DELETE" + "name": "저장 후 URL 확인", + "action": "verify", + "target": "url", + "expected": "URL이 /settings/popup-management/1 (상세 페이지로 이동)", + "validation": [ + "등록/저장" + ] }, { "step": 70, - "name": "삭제 버튼 클릭", - "action": "click", - "selector": "button", - "textContains": "삭제", - "phase": "DELETE", - "expected": "삭제 확인 다이얼로그 표시" + "name": "저장 성공 토스트 확인", + "action": "verify", + "target": "toast message", + "expected": "'팝업이 수정되었습니다' 토스트 표시됨", + "validation": [ + "등록/저장" + ] }, { "step": 71, - "name": "삭제 확인 다이얼로그 대기", - "action": "wait", - "duration": 500, - "phase": "DELETE" + "name": "수정 API 호출 확인", + "action": "verify", + "target": "network request", + "expected": "PUT /api/v1/settings/popups/1 호출됨 (200 OK)", + "validation": [ + "등록/저장" + ] }, { "step": 72, - "name": "삭제 확인 다이얼로그 확인", - "action": "verify_dialog", - "phase": "DELETE", - "expected": "삭제 확인 다이얼로그 표시됨" + "name": "수정된 데이터 확인 - 제목", + "action": "verify", + "target": "definition (제목)", + "expected": "'시스템 점검 안내 (수정됨)' 표시됨", + "validation": [ + "데이터 지속성" + ] }, { "step": 73, - "name": "삭제 확인 버튼 클릭", - "action": "click_dialog_confirm", - "critical": true, - "phase": "DELETE", - "expected": "팝업 삭제 요청 전송" + "name": "수정된 데이터 확인 - 내용", + "action": "verify", + "target": "definition (내용)", + "expected": "'수정된 내용입니다.' 표시됨", + "validation": [ + "데이터 지속성" + ] }, { "step": 74, - "name": "삭제 처리 대기", - "action": "wait", - "duration": 2000, - "phase": "DELETE" + "name": "수정된 데이터 확인 - 상태", + "action": "verify", + "target": "definition (상태)", + "expected": "'사용안함' 표시됨", + "validation": [ + "데이터 지속성" + ] }, { "step": 75, - "name": "삭제 성공 확인 (URL 변경)", - "action": "evaluate", - "script": "(() => { const url = window.location.href; return url.includes('popup-management') && !/\\/\\d+/.test(url); })()", - "phase": "DELETE", - "expected": "목록 페이지로 이동됨 (삭제 완료)" + "name": "목록으로 이동", + "action": "click_if_exists", + "target": "button '목록으로'", + "expected": "/settings/popup-management 페이지로 이동", + "validation": [ + "UI 동작" + ] }, { "step": 76, - "name": "삭제 성공 토스트 확인", - "action": "evaluate", - "script": "(() => { const toast = document.querySelector('[class*=\"toast\"], [class*=\"Toast\"], [role=\"alert\"], [class*=\"notification\"]'); return toast && (toast.innerText.includes('삭제') || toast.innerText.includes('완료') || toast.innerText.includes('성공')); })()", - "phase": "DELETE", - "expected": "삭제 성공 토스트 표시됨" + "name": "목록 페이지 URL 확인", + "action": "verify", + "target": "url", + "expected": "URL이 /settings/popup-management", + "validation": [ + "UI 동작" + ] }, { "step": 77, - "name": "삭제된 데이터 목록에서 제거 확인", - "action": "evaluate", - "script": "(() => { const text = document.body.innerText; return !text.includes('E2E_TEST_팝업'); })()", - "phase": "DELETE", - "expected": "삭제된 팝업이 목록에서 제거됨" + "name": "수정된 팝업 목록 확인", + "action": "verify", + "target": "table rows", + "expected": "수정된 팝업 정보가 목록에 반영됨", + "validation": [ + "데이터 지속성" + ] + }, + { + "step": 78, + "name": "페이지 새로고침", + "action": "refresh", + "target": "page", + "expected": "페이지 새로고침됨", + "validation": [ + "데이터 지속성" + ] + }, + { + "step": 79, + "name": "새로고침 후 데이터 유지 확인", + "action": "verify", + "target": "table rows", + "expected": "수정된 데이터가 유지됨", + "validation": [ + "데이터 지속성" + ] + }, + { + "step": 80, + "name": "삭제 테스트 - 팝업 상세 페이지 이동", + "action": "click_if_exists", + "target": "row (수정한 팝업)", + "expected": "상세 페이지로 이동", + "validation": [ + "삭제 기능" + ] + }, + { + "step": 81, + "name": "삭제 버튼 클릭", + "action": "click_if_exists", + "target": "button '삭제'", + "expected": "삭제 확인 다이얼로그 표시", + "validation": [ + "삭제 기능" + ] + }, + { + "step": 82, + "name": "삭제 확인 다이얼로그 확인", + "action": "verify", + "target": "dialog", + "expected": "삭제 확인 메시지 표시됨", + "validation": [ + "삭제 기능" + ] + }, + { + "step": 83, + "name": "삭제 확인", + "action": "click_if_exists", + "target": "button '확인' (dialog)", + "expected": "팝업 삭제 요청 전송", + "validation": [ + "삭제 기능" + ] + }, + { + "step": 84, + "name": "삭제 후 URL 확인", + "action": "verify", + "target": "url", + "expected": "URL이 /settings/popup-management (목록 페이지로 이동)", + "validation": [ + "삭제 기능" + ] + }, + { + "step": 85, + "name": "삭제 성공 토스트 확인", + "action": "verify", + "target": "toast message", + "expected": "'팝업이 삭제되었습니다' 토스트 표시됨", + "validation": [ + "삭제 기능" + ] + }, + { + "step": 86, + "name": "삭제 API 호출 확인", + "action": "verify", + "target": "network request", + "expected": "DELETE /api/v1/settings/popups/:id 호출됨 (200 OK)", + "validation": [ + "삭제 기능" + ] + }, + { + "step": 87, + "name": "삭제된 팝업 목록에서 제거 확인", + "action": "verify", + "target": "table rows", + "expected": "삭제된 팝업이 목록에서 사라짐", + "validation": [ + "삭제 기능" + ] + }, + { + "step": 88, + "name": "전체 항목 수 갱신 확인", + "action": "verify", + "target": "text (전체 항목 수)", + "expected": "전체 항목 수가 1개 감소됨", + "validation": [ + "삭제 기능" + ] } ] -} +} \ No newline at end of file diff --git a/production-dashboard.json b/production-dashboard.json index d22341a..a885caf 100644 --- a/production-dashboard.json +++ b/production-dashboard.json @@ -43,79 +43,106 @@ }, { "id": 3, - "name": "현황판 구조 확인 - 통계 카드", - "action": "verify_element", - "selector": "[class*='card'], [class*='stat'], [class*='summary'], [class*='kpi'], [class*='dashboard'] [class*='item']", - "expected": "통계 카드 영역 존재" + "name": "현황판 구조 확인", + "action": "verify_elements", + "checks": [ + "생산 통계 카드", + "현황 차트", + "기간 선택 필터", + "라인/공정별 필터" + ], + "expected": "현황판 구조 정상 표시" }, { "id": 4, - "name": "현황판 구조 확인 - 차트 영역", - "action": "verify_element", - "selector": "canvas, svg[class*='chart'], [class*='chart'], [class*='Chart'], [class*='recharts'], [class*='graph']", - "expected": "차트 또는 그래프 영역 존재" + "phase": "READ", + "name": "[READ] 생산 통계 카드 확인", + "action": "verify_detail", + "checks": [ + "오늘 생산량", + "목표 대비 달성률", + "불량률" + ], + "expected": "생산 통계 표시" }, { "id": 5, "phase": "READ", - "name": "[READ] 생산 통계 텍스트 확인", - "action": "verify_text", - "text": ["생산", "목표", "달성", "실적", "수량", "현황"], - "matchAny": true, - "expected": "생산 관련 텍스트 표시" + "name": "[READ] 생산 추이 차트 확인", + "action": "verify_elements", + "checks": [ + "일별/주별/월별 생산 추이 차트", + "차트 데이터 표시" + ], + "expected": "생산 추이 차트 표시" }, { "id": 6, - "phase": "READ", - "name": "[READ] 숫자 데이터 존재 확인", - "action": "evaluate", - "script": "(() => { const nums = document.body.innerText.match(/\\d{1,3}(,\\d{3})*(\\.\\d+)?(%|개|건|EA)?/g); return { hasNumbers: nums && nums.length > 3, count: nums ? nums.length : 0 }; })()", - "expected": "숫자 데이터 3개 이상 표시" + "phase": "FILTER", + "name": "[FILTER] 기간 필터 테스트", + "action": "click_if_exists", + "target": "select[name*='period'], button:has-text('기간'), [class*='filter']", + "expected": "기간 필터 옵션 표시" }, { "id": 7, "phase": "FILTER", - "name": "[FILTER] 기간/날짜 필터 확인", - "action": "verify_element", - "selector": "input[type='date'], [class*='date'], [class*='period'], [class*='calendar'], select[name*='period'], button[class*='date']", - "expected": "기간/날짜 선택 요소 존재" + "name": "[FILTER] 라인/공정별 필터", + "action": "verify_elements", + "checks": [ + "생산라인 선택 가능", + "공정별 필터 가능" + ], + "expected": "라인/공정 필터 표시" }, { "id": 8, - "phase": "FILTER", - "name": "[FILTER] 드롭다운/셀렉트 필터 확인", - "action": "verify_element", - "selector": "select, [class*='select'], [class*='dropdown'], [class*='filter'], [role='combobox'], [role='listbox']", - "expected": "필터 선택 요소 존재" + "name": "실시간 현황 표시", + "action": "verify_elements", + "checks": [ + "현재 가동 라인", + "실시간 생산량 또는 마지막 갱신 시간" + ], + "expected": "실시간 현황 표시" }, { "id": 9, - "name": "테이블 또는 그리드 데이터 확인", - "action": "verify_element", - "selector": "table, [class*='table'], [class*='grid'], [class*='list'], [role='grid'], [role='table']", - "expected": "데이터 테이블/그리드 존재" + "name": "불량률 현황 확인", + "action": "verify_detail", + "checks": [ + "불량률 표시", + "불량 유형별 통계" + ], + "expected": "불량률 현황 표시" }, { "id": 10, - "name": "퍼센트/비율 데이터 확인", - "action": "evaluate", - "script": "(() => { const text = document.body.innerText; const hasPercent = /%/.test(text); const hasRate = /달성|비율|률/.test(text); return { hasPercent, hasRate, valid: hasPercent || hasRate }; })()", - "expected": "퍼센트 또는 비율 데이터 표시" + "name": "생산 목표 대비 현황", + "action": "verify_elements", + "checks": [ + "목표 생산량", + "실제 생산량", + "달성률" + ], + "expected": "목표 대비 현황 표시" }, { "id": 11, - "name": "새로고침/갱신 버튼 확인", - "action": "verify_element", - "selector": "button[class*='refresh'], button[class*='reload'], [class*='sync'], button:has(svg[class*='refresh']), button:has([class*='icon-refresh'])", - "fallbackSelector": "button", - "expected": "새로고침 또는 버튼 존재" + "name": "자동 새로고침 확인", + "action": "verify_elements", + "checks": [ + "자동 새로고침 설정 또는 수동 새로고침 버튼" + ], + "expected": "새로고침 기능 존재" }, { "id": 12, - "name": "페이지 인터랙티브 요소 확인", - "action": "evaluate", - "script": "(() => { const btns = document.querySelectorAll('button:not([disabled])').length; const inputs = document.querySelectorAll('input, select, textarea').length; const links = document.querySelectorAll('a[href]').length; return { buttons: btns, inputs, links, totalInteractive: btns + inputs + links, isInteractive: (btns + inputs + links) >= 2 }; })()", - "expected": "인터랙티브 요소 2개 이상 존재" + "name": "전체화면 모드 확인", + "action": "verify_elements", + "checks": [ + "전체화면 버튼 존재 여부" + ], + "expected": "전체화면 기능 확인" } ], "expectedAPIs": [ diff --git a/purchase-status.json b/purchase-status.json index da0cab2..f63e5d8 100644 --- a/purchase-status.json +++ b/purchase-status.json @@ -25,16 +25,6 @@ "username": "TestUser5", "password": "password123!" }, - "selectors": { - "statisticsCard": ".card, [class*='stat'], [class*='summary'], [class*='card']", - "dateFilter": "input[type='date'], input[type='text'][placeholder*='날짜'], [class*='datepicker'] input", - "searchButton": "button:has-text('조회'), button:has-text('검색'), button[type='submit']", - "dataTable": "table, [class*='table'], [role='grid']", - "statusFilter": "select, [class*='select'], [class*='dropdown'], [role='combobox']", - "excelButton": "button:has-text('엑셀'), button:has-text('Excel'), button:has-text('다운로드'), button[class*='excel']", - "printButton": "button:has-text('인쇄'), button:has-text('Print'), button[class*='print']", - "chartArea": "canvas, svg, [class*='chart'], [class*='Chart']" - }, "steps": [ { "id": 1, @@ -55,108 +45,137 @@ "name": "필수 검증 #5: 목업 페이지 감지", "action": "verify_not_mockup", "checks": [ - "input, select, button[type='submit']", - "table, [class*='table'], [role='grid']" + "구매 현황 표시", + "기간 필터 존재", + "통계 또는 차트 존재" ], "expected": "정상 페이지 (목업 아님)" }, { "id": 3, "name": "구매현황 페이지 구조 확인", - "action": "verify_element", - "target": ".card, [class*='stat'], [class*='summary'], table, [class*='table']", + "action": "verify_elements", + "checks": [ + "구매 통계 카드", + "기간 선택 필터", + "구매 목록 테이블 또는 차트" + ], "expected": "구매현황 페이지 정상 표시" }, { "id": 4, "phase": "READ", - "name": "[READ] 구매 통계 카드 확인", - "action": "verify_element", - "target": ".card, [class*='stat'], [class*='summary'], [class*='total'], [class*='count']", - "expected": "구매 통계 카드 표시" + "name": "[READ] 구매 통계 확인", + "action": "verify_detail", + "checks": [ + "총 구매금액", + "구매 건수", + "평균 구매금액" + ], + "expected": "구매 통계 표시" }, { "id": 5, "phase": "FILTER", - "name": "[FILTER] 날짜 필터 필드 확인", - "action": "verify_element", - "target": "input[type='date'], input[type='text'][placeholder*='날짜'], [class*='datepicker']", - "expected": "날짜 필터 필드 존재" + "name": "[FILTER] 기간 필터 - 시작일", + "action": "click_if_exists", + "target": "input[type='date']:first-of-type, input[name*='start']" }, { "id": 6, "phase": "FILTER", - "name": "[FILTER] 조회 버튼 클릭", + "name": "[FILTER] 기간 필터 - 종료일", "action": "click_if_exists", - "target": "button:has-text('조회'), button:has-text('검색'), button[type='submit']", - "timeout": 3000 + "target": "input[type='date']:last-of-type, input[name*='end']" }, { "id": 7, "phase": "FILTER", - "name": "[FILTER] 조회 결과 테이블 확인", - "action": "wait_for_element", - "target": "table tbody tr, [class*='table'] [class*='row'], [role='row']", - "timeout": 5000, - "expected": "조회 결과 데이터 로드" + "name": "[FILTER] 조회 실행", + "action": "click_if_exists", + "target": "button:has-text('조회'), button:has-text('검색')", + "expected": { + "data_loaded": true, + "api_call": "GET /api/v1/purchase/status" + } }, { "id": 8, - "name": "구매 현황 테이블 구조 확인", - "action": "verify_element", - "target": "table thead th, table th, [role='columnheader']", - "expected": "테이블 헤더 컬럼 표시" + "name": "구매 현황 테이블 확인", + "action": "verify_table", + "checks": [ + "발주일 컬럼", + "거래처 컬럼", + "품목 컬럼", + "금액 컬럼", + "상태 컬럼" + ], + "expected": "구매 현황 테이블 표시" }, { "id": 9, - "name": "테이블 데이터 행 존재 확인", - "action": "verify_element", - "target": "table tbody tr, [class*='table'] [class*='row']:not(:first-child), [role='row']", - "expected": "테이블 데이터 행 표시" + "name": "상태별 필터 확인", + "action": "verify_elements", + "checks": [ + "진행중/완료/취소 상태 필터" + ], + "expected": "상태 필터 표시" }, { "id": 10, - "name": "상태 필터/선택 요소 확인", - "action": "verify_element", - "target": "select, [class*='select'], [class*='dropdown'], [role='combobox'], [class*='filter']", - "expected": "상태/필터 선택 요소 표시" + "name": "거래처별 통계 확인", + "action": "verify_elements", + "checks": [ + "거래처별 구매금액 표시" + ], + "expected": "거래처별 통계 표시" }, { "id": 11, - "name": "통계 영역 확인", - "action": "verify_element", - "target": ".card, [class*='stat'], [class*='summary'], [class*='total']", - "expected": "통계 정보 영역 표시" + "name": "품목별 통계 확인", + "action": "verify_elements", + "checks": [ + "품목별 구매금액 표시" + ], + "expected": "품목별 통계 표시" }, { "id": 12, - "name": "차트 또는 시각화 영역 확인", - "action": "verify_element", - "target": "canvas, svg, [class*='chart'], [class*='Chart'], [class*='graph']", - "optional": true, - "expected": "차트/그래프 영역 표시 (선택적)" + "name": "월별 추이 차트 확인", + "action": "verify_elements", + "checks": [ + "월별 구매 추이 차트" + ], + "expected": "추이 차트 표시" }, { "id": 13, - "name": "필수 검증 #1: 엑셀 다운로드 버튼 확인", - "action": "verify_element", - "target": "button:has-text('엑셀'), button:has-text('Excel'), button:has-text('다운로드'), button[class*='excel'], a[href*='export'], a[href*='download']", - "expected": "엑셀 다운로드 버튼 존재" + "name": "필수 검증 #1: 엑셀 다운로드", + "action": "click_if_exists", + "target": "button:has-text('엑셀'), button:has-text('Excel'), button:has-text('다운로드')", + "verify": { + "api_call": "GET /api/v1/purchase/status/export", + "file_download": true + }, + "expected": "엑셀 파일 다운로드" }, { "id": 14, - "name": "인쇄 버튼 확인", - "action": "verify_element", - "target": "button:has-text('인쇄'), button:has-text('Print'), button[class*='print'], [class*='print']", - "optional": true, - "expected": "인쇄 버튼 표시 (선택적)" + "name": "인쇄 기능 확인", + "action": "verify_elements", + "checks": [ + "인쇄 버튼 존재" + ], + "expected": "인쇄 기능 표시" }, { "id": 15, - "name": "페이지 정상 동작 최종 확인", - "action": "verify_url_stability", - "timeout": 2000, - "expected": "페이지 정상 유지 (에러 없음)" + "name": "전년 대비 비교 확인", + "action": "verify_elements", + "checks": [ + "전년 동기 대비 증감 표시" + ], + "expected": "비교 분석 표시" } ], "expectedAPIs": [ @@ -169,16 +188,21 @@ "method": "GET", "endpoint": "/api/v1/purchase/statistics", "description": "구매 통계 조회" + }, + { + "method": "GET", + "endpoint": "/api/v1/purchase/status/export", + "description": "구매현황 엑셀 다운로드" } ], "requiredVerifications": [ { "id": 1, - "name": "엑셀 다운로드 버튼", + "name": "엑셀 다운로드", "steps": [ 13 ], - "criteria": "엑셀 다운로드 버튼 존재 확인" + "criteria": "API 호출 + 파일 다운로드" }, { "id": 5, @@ -186,10 +210,10 @@ "steps": [ 2 ], - "criteria": "입력 필드, 테이블 등 실제 UI 요소 존재" + "criteria": "구매 현황, 기간 필터, 통계/차트 존재" } ], "rollbackPlan": { "note": "조회 전용 페이지로 데이터 변경 없음" } -} +} \ No newline at end of file diff --git a/settings-subscription.json b/settings-subscription.json index a47c1f3..fe7a6f6 100644 --- a/settings-subscription.json +++ b/settings-subscription.json @@ -18,14 +18,6 @@ "username": "TestUser5", "password": "password123!" }, - "selectors": { - "pageTitle": "h1.text-xl.md\\:text-2xl", - "planCard": "main .bg-card", - "planName": "h3.text-xl", - "paymentGrid": ".grid.grid-cols-1.md\\:grid-cols-3", - "exportButton": "button:has-text('자료 내보내기')", - "cancelButton": "button:has-text('서비스 해지')" - }, "steps": [ { "id": 1, @@ -35,107 +27,117 @@ "level2": "구독관리", "expected": { "url_contains": "/subscription", - "visible": ["구독관리"] + "visible": ["구독관리", "구독"] } }, { "id": 2, - "name": "페이지 타이틀 확인", - "action": "verify_element", - "selector": "h1", - "contains": "구독관리", - "expected": "구독관리 페이지 타이틀 표시" - }, - { - "id": 3, "name": "필수 검증 #5: 목업 페이지 감지", "action": "verify_not_mockup", "checks": [ - "구독 정보 카드 존재", + "구독 정보 표시", "플랜 정보 표시", - "사용량 현황 표시" + "결제 정보 표시" ], "expected": "정상 페이지 (목업 아님)" }, + { + "id": 3, + "name": "현재 플랜 정보 확인", + "action": "verify_elements", + "checks": [ + "현재 플랜명 표시", + "플랜 가격 표시", + "포함 기능 표시" + ], + "expected": "현재 플랜 정보 표시" + }, { "id": 4, - "name": "현재 플랜명 확인", - "action": "verify_element", - "selector": "h3.text-xl", - "expected": "현재 플랜명 (무료/유료) 표시" + "name": "구독 기간 확인", + "action": "verify_elements", + "checks": [ + "구독 시작일 표시", + "구독 종료일 표시", + "남은 기간 표시" + ], + "expected": "구독 기간 표시" }, { "id": 5, - "name": "결제 정보 카드 그리드 확인", - "action": "verify_element", - "selector": ".grid.grid-cols-1", - "expected": "결제 정보 카드 그리드 표시" + "name": "결제 정보 확인", + "action": "verify_elements", + "checks": [ + "결제 방법 표시", + "다음 결제일 표시", + "결제 금액 표시" + ], + "expected": "결제 정보 표시" }, { "id": 6, - "name": "최근 결제일시 카드 확인", - "action": "verify_text", - "contains": "최근 결제일시", - "expected": "최근 결제일시 정보 표시" + "name": "플랜 비교 확인", + "action": "verify_elements", + "checks": [ + "플랜 비교 테이블 또는 카드" + ], + "expected": "플랜 비교 가능" }, { "id": 7, - "name": "다음 결제일시 카드 확인", - "action": "verify_text", - "contains": "다음 결제일시", - "expected": "다음 결제일시 정보 표시" + "name": "플랜 변경 버튼 확인", + "action": "verify_elements", + "checks": [ + "플랜 변경 또는 업그레이드 버튼" + ], + "expected": "플랜 변경 버튼 표시" }, { "id": 8, - "name": "구독금액 카드 확인", - "action": "verify_text", - "contains": "구독금액", - "expected": "구독금액 정보 표시" + "name": "사용량 현황 확인", + "action": "verify_elements", + "checks": [ + "사용자 수 현황", + "저장 용량 현황", + "기능 사용 현황" + ], + "expected": "사용량 현황 표시" }, { "id": 9, - "name": "구독 정보 카드 확인", - "action": "verify_element", - "selector": ".bg-card", - "contains": "구독 정보", - "expected": "구독 정보 카드 표시" + "name": "결제 내역 확인", + "action": "verify_elements", + "checks": [ + "결제 내역 테이블 또는 리스트" + ], + "expected": "결제 내역 표시" }, { "id": 10, - "name": "사용자 수 현황 확인", - "action": "verify_text", - "contains": "사용자 수", - "expected": "사용자 수 현황 표시" + "name": "영수증 다운로드 확인", + "action": "verify_elements", + "checks": [ + "영수증 다운로드 버튼 존재" + ], + "expected": "영수증 다운로드 기능 표시" }, { "id": 11, - "name": "저장 공간 현황 확인", - "action": "verify_text", - "contains": "저장 공간", - "expected": "저장 공간 현황 표시" + "name": "결제 수단 변경 확인", + "action": "verify_elements", + "checks": [ + "결제 수단 변경 버튼 존재" + ], + "expected": "결제 수단 변경 기능 표시" }, { "id": 12, - "name": "AI API 호출 현황 확인", - "action": "verify_text", - "contains": "AI API 호출", - "expected": "AI API 호출 현황 표시" - }, - { - "id": 13, - "name": "자료 내보내기 버튼 확인", - "action": "verify_element", - "selector": "main button", - "contains": "자료 내보내기", - "expected": "자료 내보내기 버튼 표시" - }, - { - "id": 14, - "name": "서비스 해지 버튼 확인", - "action": "verify_element", - "selector": "main button", - "contains": "서비스 해지", - "expected": "서비스 해지 버튼 표시" + "name": "구독 취소 버튼 확인", + "action": "verify_elements", + "checks": [ + "구독 취소 또는 해지 버튼 존재" + ], + "expected": "구독 취소 기능 표시" } ], "expectedAPIs": [ @@ -144,6 +146,16 @@ "endpoint": "/api/v1/subscription", "description": "구독 정보 조회" }, + { + "method": "GET", + "endpoint": "/api/v1/subscription/plans", + "description": "플랜 목록 조회" + }, + { + "method": "GET", + "endpoint": "/api/v1/subscription/payments", + "description": "결제 내역 조회" + }, { "method": "GET", "endpoint": "/api/v1/subscription/usage", @@ -154,11 +166,11 @@ { "id": 5, "name": "목업 페이지 감지", - "steps": [3], - "criteria": "구독 정보 카드, 플랜 정보, 사용량 현황 존재" + "steps": [2], + "criteria": "구독 정보, 플랜 정보, 결제 정보 존재" } ], "rollbackPlan": { - "note": "조회 전용 페이지로 데이터 변경 없음" + "note": "조회 전용 페이지로 데이터 변경 없음 (결제/플랜 변경은 별도 테스트)" } }