{ "id": "free-board", "name": "자유게시판 E2E 테스트", "screenshotPolicy": { "onErrorOnly": true, "captureOn": ["error", "fail", "timeout", "404", "500", "blocked"] }, "description": "자유게시판의 목록, 게시글 작성, 상세, 수정, 삭제, 댓글 CRUD 전체 워크플로우 테스트", "url": "/ko/boards/free", "navigation": { "targetUrl": "/boards/free", "urlPattern": "/boards/free|/ko/boards/free", "menuHints": ["자유게시판", "자유 게시판", "게시판"] }, "menuNavigation": { "level1": "게시판", "level2": "자유게시판", "expectedUrl": "/ko/boards/free", "searchWithinParent": true, "closeOtherMenus": true }, "auth": { "username": "TestUser5", "password": "password123!" }, "menuNavigationEnhanced": { "strategy": "scroll-and-search", "level1": { "text": "게시판", "scrollContainer": ".sidebar-scroll, [data-sidebar='content'], nav", "maxScrollAttempts": 5, "scrollStep": 200 }, "level2": { "text": "자유게시판", "waitAfterLevel1Click": 500 }, "expectedUrl": "/ko/boards/free", "fallbackUrl": "/ko/boards/free" }, "steps": [ { "step": 0, "name": "사이드바 메뉴 전체 펼치기", "description": "모두 펼치기 버튼을 클릭하여 전체 메뉴를 펼친 후 메뉴 탐색 준비", "actions": [ { "type": "evaluate", "script": "document.querySelector('.sidebar-scroll, [data-sidebar=\"content\"], nav')?.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": "scrollAndFind", "target": "게시판", "container": ".sidebar-scroll, [data-sidebar='content'], nav", "maxAttempts": 5, "scrollStep": 200 }, { "type": "click_if_exists", "target": "게시판" }, { "type": "wait", "duration": 500 }, { "type": "click_if_exists", "target": "자유게시판" }, { "type": "wait", "target": "페이지 로드 완료" } ], "verification": { "title_contains": "자유게시판", "elements": ["table", "button:has-text('글쓰기')"] } }, { "step": 2, "name": "초기 게시글 목록 확인", "action": "verify_table_structure", "verification": { "columns": ["No.", "제목", "작성자", "조회수", "상태", "등록일"], "min_rows": 0 } }, { "step": 3, "name": "게시글 총 건수 확인", "action": "verify_text", "verification": { "text_pattern": "총 \\d+건" } }, { "step": 4, "name": "검색 기능 확인 (검색창 존재)", "action": "verify_element", "target": "input[placeholder*='제목']", "verification": { "exists": true } }, { "step": 5, "name": "필터 드롭다운 확인 (상태)", "action": "verify_element", "target": "select, [role='combobox']:has-text('상태')", "verification": { "exists": true } }, { "step": 6, "name": "정렬 드롭다운 확인", "action": "verify_element", "target": "select, [role='combobox']:has-text('최신순')", "verification": { "exists": true } }, { "step": 7, "name": "날짜 범위 선택기 확인", "action": "verify_element", "target": "input[type='date']", "verification": { "count": 2 } }, { "step": 8, "name": "검색 테스트 (제목)", "action": "fill_and_wait", "target": "input[placeholder*='제목']", "value": "테스트", "verification": { "wait_for_data_change": true } }, { "step": 9, "name": "검색 결과 확인", "action": "verify_table_data", "verification": { "filtered": true } }, { "step": 10, "name": "검색어 초기화", "action": "fill", "target": "input[placeholder*='제목']", "value": "" }, { "step": 11, "name": "상태 필터 테스트 (게시됨)", "action": "select_dropdown", "target": "[role='combobox']:has-text('전체')", "value": "게시됨", "verification": { "wait_for_data_change": true } }, { "step": 12, "name": "상태 필터 초기화 (전체)", "action": "select_dropdown", "target": "[role='combobox']", "value": "전체" }, { "step": 13, "name": "정렬 변경 (오래된순)", "action": "select_dropdown", "target": "[role='combobox']:has-text('최신순')", "value": "오래된순", "verification": { "wait_for_data_change": true } }, { "step": 14, "name": "정렬 복원 (최신순)", "action": "select_dropdown", "target": "[role='combobox']:has-text('오래된순')", "value": "최신순" }, { "step": 15, "name": "글쓰기 버튼 클릭", "action": "click_if_exists", "target": "button:has-text('글쓰기')" }, { "step": 16, "name": "게시글 작성 페이지 진입 확인", "action": "verify_url", "verification": { "url_pattern": "/boards/free\\?mode=new|/boards/free/create", "title_contains": "자유게시판" } }, { "step": 17, "name": "제목 필드 확인", "action": "verify_element", "target": "input#title", "verification": { "exists": true, "required": true } }, { "step": 18, "name": "내용 필드 확인", "action": "verify_element", "target": "textarea#content", "verification": { "exists": true, "required": true } }, { "step": 19, "name": "게시글 제목 입력", "action": "fill", "target": "input#title", "value": "E2E 테스트 게시글" }, { "step": 20, "name": "게시글 내용 입력", "action": "fill", "target": "textarea#content", "value": "이것은 E2E 자동화 테스트를 위한 게시글입니다." }, { "step": 21, "name": "현재 URL 저장 (등록 전)", "action": "save_url", "variable": "url_before_submit" }, { "step": 22, "name": "게시글 등록 버튼 클릭", "action": "click_if_exists", "target": "button:has-text('등록')" }, { "step": 23, "name": "게시글 등록 완료 (URL 안정성 검증)", "action": "verify_url_stability", "verification": { "expected_url_pattern": "/boards/free/\\d+", "no_404": true, "no_error_page": true, "success_condition": "url_changed_to_detail" }, "notes": "필수 검증 #2: 등록 후 상세 페이지로 이동해야 하며 404 에러 페이지가 나오면 안 됨" }, { "step": 24, "name": "게시글 상세 페이지 진입 확인", "action": "verify_page", "verification": { "title": "E2E 테스트 게시글", "content_contains": "E2E 자동화 테스트" } }, { "step": 25, "name": "게시글 ID 저장", "action": "extract_from_url", "pattern": "/ko/boards/free/(\\d+)", "variable": "post_id" }, { "step": 26, "name": "작성자 정보 표시 확인", "action": "verify_element", "target": "text=/\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}/", "verification": { "exists": true } }, { "step": 27, "name": "조회수 표시 확인", "action": "verify_element", "target": "svg.lucide-eye", "verification": { "exists": true } }, { "step": 28, "name": "수정 버튼 존재 확인 (작성자)", "action": "verify_element", "target": "button:has-text('수정')", "verification": { "exists": true } }, { "step": 29, "name": "삭제 버튼 존재 확인 (작성자)", "action": "verify_element", "target": "button:has-text('삭제')", "verification": { "exists": true } }, { "step": 30, "name": "댓글 섹션 확인", "action": "verify_element", "target": "text=/댓글/", "verification": { "exists": true } }, { "step": 31, "name": "댓글 입력란 확인", "action": "verify_element", "target": "textarea[placeholder*='댓글']", "verification": { "exists": true } }, { "step": 32, "name": "첫 번째 댓글 작성", "action": "fill", "target": "textarea[placeholder*='댓글']", "value": "첫 번째 테스트 댓글입니다." }, { "step": 33, "name": "댓글 등록 버튼 클릭", "action": "click_if_exists", "target": "button:has-text('댓글 등록'), button:has-text('등록')" }, { "step": 34, "name": "댓글 등록 확인", "action": "verify_text", "verification": { "text": "첫 번째 테스트 댓글입니다.", "exists": true } }, { "step": 35, "name": "댓글 수 업데이트 확인", "action": "evaluate", "script": "(function(){ var t = document.body.innerText; var m = t.match(/댓글[\\s(]*\\d+/); return m ? m[0] : 'comment section found: ' + (t.includes('댓글') ? 'yes' : 'no'); })()" }, { "step": 36, "name": "두 번째 댓글 작성", "action": "fill", "target": "textarea[placeholder*='댓글']", "value": "두 번째 테스트 댓글입니다." }, { "step": 37, "name": "두 번째 댓글 등록", "action": "click_if_exists", "target": "button:has-text('댓글 등록'), button:has-text('등록')" }, { "step": 38, "name": "두 번째 댓글 등록 확인", "action": "verify_text", "verification": { "text": "두 번째 테스트 댓글입니다.", "exists": true } }, { "step": 39, "name": "첫 번째 댓글 수정 버튼 클릭", "description": "댓글 영역 내의 수정 버튼을 찾아 클릭 (게시글 수정 버튼과 구별)", "actions": [ { "type": "evaluate", "script": "(function(){ var allBtns = Array.from(document.querySelectorAll('button')).filter(function(b){ return b.innerText && b.innerText.trim() === '수정'; }); var commentBtn = allBtns.filter(function(b){ return b.closest('[class*=\"comment\"], [class*=\"Comment\"], [class*=\"reply\"]'); }); if(commentBtn.length > 0){ commentBtn[0].click(); return 'clicked comment edit btn'; } if(allBtns.length >= 2){ allBtns[allBtns.length - 1].click(); return 'clicked last edit btn (assumed comment)'; } return 'no comment edit btn found'; })()" }, { "type": "wait", "duration": 1000 } ] }, { "step": 40, "name": "댓글 수정 내용 입력", "description": "인라인 편집 또는 별도 textarea에 수정 내용 입력", "actions": [ { "type": "evaluate", "script": "(function(){ var newVal = '수정된 첫 번째 댓글입니다.'; var textareas = Array.from(document.querySelectorAll('textarea')); var editTA = textareas.find(function(t){ return t.value && t.value.includes('첫 번째 테스트'); }); if(editTA){ var rk = Object.keys(editTA).find(function(k){ return k.indexOf('__reactProps$')===0; }); if(rk && editTA[rk] && typeof editTA[rk].onChange==='function'){ var setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,'value').set; setter.call(editTA, newVal); editTA[rk].onChange({target:editTA,currentTarget:editTA}); return 'filled via reactProps (value='+editTA.value+')'; } editTA.focus(); editTA.select(); document.execCommand('insertText',false,newVal); return 'filled via execCommand (value='+editTA.value+')'; } var inputs = Array.from(document.querySelectorAll('input[type=\"text\"]')); var editInput = inputs.find(function(i){ return i.value && i.value.includes('첫 번째 테스트'); }); if(editInput){ var rk2 = Object.keys(editInput).find(function(k){ return k.indexOf('__reactProps$')===0; }); if(rk2 && editInput[rk2] && typeof editInput[rk2].onChange==='function'){ var setter2 = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set; setter2.call(editInput, newVal); editInput[rk2].onChange({target:editInput,currentTarget:editInput}); return 'filled input via reactProps (value='+editInput.value+')'; } editInput.focus(); editInput.select(); document.execCommand('insertText',false,newVal); return 'filled input via execCommand (value='+editInput.value+')'; } var editables = document.querySelectorAll('[contenteditable=\"true\"]'); for(var i=0; i 0){ commentBtns[commentBtns.length-1].click(); return 'clicked last comment delete btn'; } if(allBtns.length >= 2){ allBtns[allBtns.length-1].click(); return 'clicked last delete btn (assumed comment)'; } return 'no comment delete btn found'; })()" }, { "type": "wait", "duration": 500 } ] }, { "step": 44, "name": "댓글 삭제 확인 다이얼로그 처리", "actions": [ { "type": "evaluate", "script": "(function(){ var dialog = document.querySelector('[role=\"dialog\"], [role=\"alertdialog\"], [class*=\"modal\"]:not([class*=\"tooltip\"])'); if(dialog && dialog.offsetParent !== null){ var confirmBtn = Array.from(dialog.querySelectorAll('button')).find(function(b){ return ['확인','삭제','예','OK','Yes'].some(function(t){ return b.innerText && b.innerText.includes(t); }); }); if(confirmBtn){ confirmBtn.click(); return 'confirmed delete dialog'; } } return 'no dialog or auto-handled'; })()" }, { "type": "wait", "duration": 1500 } ] }, { "step": 45, "name": "댓글 삭제 확인", "action": "verify_text", "verification": { "text": "두 번째 테스트 댓글", "exists": false } }, { "step": 46, "name": "댓글 수 확인 (삭제 후)", "action": "evaluate", "script": "(function(){ var t = document.body.innerText; var m = t.match(/댓글[\\s(]*\\d+/); return m ? m[0] : 'comment section: ' + (t.includes('댓글') ? 'exists' : 'not found'); })()" }, { "step": 47, "name": "게시글 수정 버튼 클릭", "action": "click_if_exists", "target": "button:has-text('수정')" }, { "step": 48, "name": "게시글 수정 페이지 진입 확인", "action": "verify_url", "verification": { "url_pattern": "/(ko/)?boards/free/\\d+\\?mode=edit" } }, { "step": 49, "name": "제목 필드에 기존 값 확인", "action": "verify_input_value", "target": "input#title", "verification": { "value": "E2E 테스트 게시글" } }, { "step": 50, "name": "제목 수정", "action": "fill", "target": "input#title", "value": "E2E 테스트 게시글 (수정됨)" }, { "step": 51, "name": "내용 수정", "action": "fill", "target": "textarea#content", "value": "수정된 내용입니다. E2E 자동화 테스트를 위한 게시글입니다." }, { "step": 52, "name": "현재 URL 저장 (수정 전)", "action": "save_url", "variable": "url_before_update" }, { "step": 53, "name": "수정 저장 버튼 클릭", "action": "click_if_exists", "target": "button[type='submit']:has-text('저장'), button:has-text('수정')" }, { "step": 54, "name": "게시글 수정 완료 (URL 안정성 검증)", "action": "verify_url_stability", "verification": { "expected_url_pattern": "/boards/free/\\d+", "no_404": true, "no_error_page": true, "success_condition": "url_back_to_detail" }, "notes": "필수 검증 #2: 수정 후 상세 페이지로 돌아가야 하며 404 에러 페이지가 나오면 안 됨" }, { "step": 55, "name": "수정된 제목 확인", "action": "verify_text", "verification": { "text": "E2E 테스트 게시글 (수정됨)", "exists": true } }, { "step": 56, "name": "수정된 내용 확인", "action": "verify_text", "verification": { "text": "수정된 내용입니다", "exists": true } }, { "step": 57, "name": "목록으로 이동 버튼 클릭", "action": "click_if_exists", "target": "button:has-text('목록으로')" }, { "step": 58, "name": "목록 페이지 복귀 확인", "action": "verify_url", "verification": { "url": "/boards/free" } }, { "step": 59, "name": "수정된 게시글 목록 확인", "action": "verify_text", "verification": { "text": "E2E 테스트 게시글 (수정됨)", "exists": true } }, { "step": 60, "name": "게시글 클릭하여 상세 진입", "action": "click_if_exists", "target": "text=E2E 테스트 게시글 (수정됨)" }, { "step": 61, "name": "상세 페이지 진입 확인", "action": "verify_url", "verification": { "url_pattern": "/(ko/)?boards/free/\\d+" } }, { "step": 62, "name": "조회수 증가 확인", "action": "verify_element", "target": "svg.lucide-eye ~ text", "verification": { "text_pattern": "\\d+", "notes": "조회수가 표시되는지 확인" } }, { "step": 63, "name": "게시글 삭제 버튼 클릭", "action": "click_if_exists", "target": "button:has-text('삭제')" }, { "step": 64, "name": "삭제 확인 다이얼로그 표시 확인", "action": "verify_dialog", "verification": { "title": "게시글 삭제", "content_contains": "삭제하시겠습니까" } }, { "step": 65, "name": "현재 URL 저장 (삭제 전)", "action": "save_url", "variable": "url_before_delete" }, { "step": 66, "name": "삭제 확인 버튼 클릭", "action": "click_if_exists", "target": "button:has-text('삭제'):last-of-type" }, { "step": 67, "name": "게시글 삭제 완료 (URL 안정성 검증)", "action": "verify_url_stability", "verification": { "expected_url": "/boards/free", "no_404": true, "no_error_page": true, "success_condition": "url_back_to_list" }, "notes": "필수 검증 #2: 삭제 후 목록 페이지로 돌아가야 하며 404 에러 페이지가 나오면 안 됨" }, { "step": 68, "name": "목록 페이지 복귀 확인", "action": "verify_url", "verification": { "url": "/boards/free" } }, { "step": 69, "name": "삭제된 게시글 목록에서 제거 확인", "action": "verify_text", "verification": { "text": "E2E 테스트 게시글 (수정됨)", "exists": false } }, { "step": 70, "name": "콘솔 에러 확인", "action": "verify_element", "target": "body", "verification": { "no_errors": true } } ], "expectedAPIs": [ { "endpoint": "GET /api/v1/boards/free", "description": "자유게시판 정보 조회" }, { "endpoint": "GET /api/v1/boards/free/posts", "description": "게시글 목록 조회 (per_page=100)" }, { "endpoint": "POST /api/v1/boards/free/posts", "description": "게시글 등록", "payload": { "title": "string", "content": "string", "is_secret": "boolean" } }, { "endpoint": "GET /api/v1/boards/free/posts/{id}", "description": "게시글 상세 조회 (조회수 증가)" }, { "endpoint": "GET /api/v1/boards/free/posts/{id}/comments", "description": "댓글 목록 조회" }, { "endpoint": "POST /api/v1/boards/free/posts/{id}/comments", "description": "댓글 등록", "payload": { "content": "string" } }, { "endpoint": "PUT /api/v1/boards/free/posts/{id}/comments/{commentId}", "description": "댓글 수정", "payload": { "content": "string" } }, { "endpoint": "DELETE /api/v1/boards/free/posts/{id}/comments/{commentId}", "description": "댓글 삭제" }, { "endpoint": "PUT /api/v1/boards/free/posts/{id}", "description": "게시글 수정", "payload": { "title": "string", "content": "string", "is_secret": "boolean" } }, { "endpoint": "DELETE /api/v1/boards/free/posts/{id}", "description": "게시글 삭제" } ], "notes": [ "자유게시판은 boardCode='free'를 사용하는 동적 게시판입니다.", "게시글 등록/수정/삭제 시 반드시 URL 안정성 검증 수행 (필수 검증 #2)", "댓글 CRUD 기능 모두 테스트해야 합니다.", "IntegratedListTemplateV2 템플릿 사용으로 반응형 디자인 (데스크톱/모바일)", "페이지네이션은 10개 단위로 동작 (10개 미만 시 미표시)", "검색은 제목, 작성자명으로 필터링됩니다.", "상태 필터: 전체, 게시됨, 임시저장", "정렬: 최신순, 오래된순", "조회수는 게시글 상세 조회 시마다 증가합니다.", "작성자만 수정/삭제 버튼이 표시됩니다.", "댓글도 작성자만 수정/삭제 가능합니다.", "댓글 수정은 인라인 편집 방식 (별도 textarea 삽입이 아닌 기존 요소 내 편집)", "댓글 삭제 시 확인 다이얼로그가 표시될 수 있음", "게시글 내용은 HTML로 저장되며 dangerouslySetInnerHTML로 렌더링됩니다." ] }