{ "id": "item-management", "name": "품목관리 (Item Management)", "screenshotPolicy": { "onErrorOnly": true, "captureOn": ["error", "fail", "timeout", "404", "500", "blocked"] }, "description": "생산관리 - 품목관리 메뉴의 전체 기능 테스트: 품목 조회, 검색, 필터, 등록(제품/부품/소모품), 상세보기, 수정, 삭제, 페이지네이션", "priority": "High", "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": ["품목관리", "품목 관리", "생산관리"] }, "menuNavigation": { "level1": "생산관리", "level2": "품목관리", "expectedUrl": "/ko/production/screen-production", "searchWithinParent": true, "closeOtherMenus": true }, "auth": { "username": "TestUser5", "password": "password123!" }, "testData": { "testProduct": { "상품명": "테스트 프리미엄 스크린", "품목명": "TEST-SCREEN-001", "로트약자": "TSC", "품목상태": "활성", "비고": "E2E 테스트용 제품", "인정번호": "TEST-CERT-2026-001" }, "testConsumable": { "품목명": "테스트 라벨", "규격": "100x50mm", "단위": "EA", "비고": "E2E 테스트용 소모품" }, "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} ], "steps": [ { "step": 0, "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} ] }, { "step": 1, "name": "2단계 메뉴 진입: 생산관리 > 품목관리", "description": "사이드바를 스크롤하며 생산관리 > 품목관리 메뉴를 찾아 클릭", "actions": [ {"type": "menu_navigate", "level1": "생산관리", "level2": "품목관리"}, {"type": "wait", "duration": 2000}, {"type": "verify_url", "pattern": "/production/screen-production"} ] }, { "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" } ] }, { "step": 3, "name": "품목 등록 버튼 표시 확인", "phase": "VERIFY", "actions": [ { "type": "verify_element", "selector": "button", "textContains": "품목 등록", "timeout": 5000 } ] }, { "step": 4, "name": "검색 입력 필드 표시 확인", "phase": "VERIFY", "actions": [ { "type": "verify_element", "selector": "input[placeholder*='검색'], input[placeholder*='품목'], input[type='search']", "timeout": 5000 } ] }, { "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" } ] }, { "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" } ] }, { "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" } ] }, { "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" } ] }, { "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" } ] }, { "step": 10, "name": "검색 기능 테스트 - 검색어 입력", "phase": "SEARCH", "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": "fill", "selector": "input[placeholder*='검색'], input[placeholder*='품목'], input[type='search']", "value": "CS-001000" }, {"type": "wait", "duration": 1500} ] }, { "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" } ] }, { "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" } ] }, { "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": "wait", "duration": 1000} ] }, { "step": 14, "name": "탭 필터 테스트 - 제품 탭 클릭", "phase": "FILTER", "actions": [ { "type": "click", "selector": "button", "textContains": "제품", "textExact": true }, {"type": "wait", "duration": 1000} ] }, { "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" } ] }, { "step": 16, "name": "탭 필터 테스트 - 소모품 탭 클릭", "phase": "FILTER", "actions": [ { "type": "click", "selector": "button", "textContains": "소모품", "textExact": true }, {"type": "wait", "duration": 1000} ] }, { "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" } ] }, { "step": 18, "name": "탭 필터 초기화 - 전체 탭 클릭", "phase": "FILTER", "actions": [ { "type": "click", "selector": "button", "textContains": "전체", "textExact": true }, {"type": "wait", "duration": 1000} ] }, { "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} ] }, { "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" } ] }, { "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} ] }, { "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" } ] }, { "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} ] }, { "step": 24, "name": "품목 등록 페이지 이동", "phase": "CREATE", "actions": [ { "type": "click", "selector": "button, a", "textContains": "품목 등록" }, {"type": "wait", "duration": 2000} ] }, { "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" } ] }, { "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" } ] }, { "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" } ] }, { "step": 28, "name": "품목 유형 필드 확인", "phase": "CREATE", "actions": [ { "type": "verify_element", "selector": "select, [role='combobox'], button[aria-haspopup='listbox'], [class*='select']", "timeout": 5000 } ] }, { "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} ] }, { "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} ] }, { "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" } ] }, { "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}); })()" } ] }, { "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}); })()" } ] }, { "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" } ] }, { "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'}); })()" } ] }, { "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} ] }, { "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} ] }, { "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}); })()" } ] }, { "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'}); })()" } ] }, { "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" } ] }, { "step": 41, "name": "제품 등록 - URL 저장", "phase": "CREATE", "actions": [ {"type": "save_url", "variable": "url_before_product_save"} ] }, { "step": 42, "name": "제품 등록 - 저장 버튼 클릭", "phase": "CREATE", "actions": [ { "type": "click", "selector": "button", "textContains": "저장" }, {"type": "wait", "duration": 2000} ] }, { "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" } ] }, { "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" } ] }, { "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" } ] }, { "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" } ] }, { "step": 47, "name": "제품 등록 - 신규 품목 검색", "phase": "CREATE", "actions": [ { "type": "fill", "selector": "input[placeholder*='검색'], input[placeholder*='품목'], input[type='search']", "value": "TEST-SCREEN-001" }, {"type": "wait", "duration": 1500} ] }, { "step": 48, "name": "제품 등록 - 신규 품목 표시 확인", "phase": "CREATE", "actions": [ {"type": "wait", "duration": 1000} ] }, { "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" } ] }, { "step": 50, "name": "소모품 등록 테스트 시작 - 품목 등록 버튼 클릭", "phase": "CREATE", "actions": [ { "type": "click", "selector": "button, a", "textContains": "품목 등록" }, {"type": "wait", "duration": 2000} ] }, { "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} ] }, { "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} ] }, { "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" } ] }, { "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}); })()" } ] }, { "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}); })()" } ] }, { "step": 56, "name": "소모품 품목코드 자동생성 확인", "phase": "CREATE", "actions": [ { "type": "evaluate", "script": "(function(){ return JSON.stringify({pass: true, note: 'auto-generated code check'}); })()", "expected": "pass:true" } ] }, { "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} ] }, { "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} ] }, { "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}); })()" } ] }, { "step": 60, "name": "소모품 등록 - URL 저장", "phase": "CREATE", "actions": [ {"type": "save_url", "variable": "url_before_consumable_save"} ] }, { "step": 61, "name": "소모품 등록 - 저장 버튼 클릭", "phase": "CREATE", "actions": [ { "type": "click", "selector": "button", "textContains": "저장" }, {"type": "wait", "duration": 2000} ] }, { "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" } ] }, { "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" } ] }, { "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" } ] }, { "step": 65, "name": "소모품 등록 - 신규 품목 검색", "phase": "CREATE", "actions": [ { "type": "fill", "selector": "input[placeholder*='검색'], input[placeholder*='품목'], input[type='search']", "value": "테스트 라벨" }, {"type": "wait", "duration": 1500} ] }, { "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" } ] }, { "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} ] }, { "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} ] }, { "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" } ] }, { "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} ] }, { "step": 71, "name": "수정 기능 테스트 - 등록한 제품 검색", "phase": "UPDATE", "actions": [ { "type": "fill", "selector": "input[placeholder*='검색'], input[placeholder*='품목'], input[type='search']", "value": "TEST-SCREEN-001" }, {"type": "wait", "duration": 1500} ] }, { "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} ] }, { "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" } ] }, { "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" } ] }, { "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}); })()" } ] }, { "step": 76, "name": "수정 저장 - URL 저장", "phase": "UPDATE", "actions": [ {"type": "save_url", "variable": "url_before_edit_save"} ] }, { "step": 77, "name": "수정 저장 버튼 클릭", "phase": "UPDATE", "actions": [ { "type": "click", "selector": "button", "textContains": "저장" }, {"type": "wait", "duration": 2000} ] }, { "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" } ] }, { "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" } ] }, { "step": 80, "name": "수정된 데이터 확인 - 제품 검색", "phase": "UPDATE", "actions": [ { "type": "fill", "selector": "input[placeholder*='검색'], input[placeholder*='품목'], input[type='search']", "value": "TEST-SCREEN-001" }, {"type": "wait", "duration": 1500} ] }, { "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} ] }, { "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" } ] }, { "step": 83, "name": "상세 모달 닫기", "phase": "UPDATE", "actions": [ {"type": "close_modal"}, {"type": "wait", "duration": 500} ] }, { "step": 84, "name": "삭제 기능 테스트 - 소모품 검색", "phase": "DELETE", "actions": [ { "type": "fill", "selector": "input[placeholder*='검색'], input[placeholder*='품목'], input[type='search']", "value": "테스트 라벨" }, {"type": "wait", "duration": 1500} ] }, { "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} ] }, { "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" } ] }, { "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} ] }, { "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" } ] }, { "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} ] }, { "step": 90, "name": "삭제 확인 버튼 클릭", "phase": "DELETE", "actions": [ { "type": "click_dialog_confirm" }, {"type": "wait", "duration": 2000} ] }, { "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" } ] }, { "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" } ] }, { "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" } ] }, { "step": 94, "name": "제품 삭제 - 제품 검색", "phase": "DELETE", "actions": [ { "type": "fill", "selector": "input[placeholder*='검색'], input[placeholder*='품목'], input[type='search']", "value": "TEST-SCREEN-001" }, {"type": "wait", "duration": 1500} ] }, { "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} ] }, { "step": 96, "name": "제품 삭제 확인", "phase": "DELETE", "actions": [ {"type": "click_dialog_confirm"}, {"type": "wait", "duration": 2000} ] }, { "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" } ] }, { "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" } ] }, { "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" } ] }, { "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" } ] } ] }