fix: 자유게시판 시나리오 수정 - 셀렉터 불일치 9건 해결

This commit is contained in:
김보곤
2026-02-04 23:26:53 +09:00
parent aa078458a4
commit fd4f03fdc9

View File

@@ -62,7 +62,7 @@
{
"step": 1,
"name": "2단계 메뉴 진입: 게시판 > 자유게시판",
"description": "게시판 > 자유게시판 메뉴로 이동하여 페이지 로드 확인 (scrollAndFind 패턴)",
"description": "게시판 > 자유게시판 메뉴로 이동하여 페이지 로드 확인",
"actions": [
{
"type": "scrollAndFind",
@@ -202,7 +202,7 @@
"step": 14,
"name": "정렬 복원 (최신순)",
"action": "select_dropdown",
"target": "[role='combobox']",
"target": "[role='combobox']:has-text('오래된순')",
"value": "최신순"
},
{
@@ -216,7 +216,7 @@
"name": "게시글 작성 페이지 진입 확인",
"action": "verify_url",
"verification": {
"url_pattern": "/ko/boards/free/create",
"url_pattern": "/boards/free\\?mode=new|/boards/free/create",
"title_contains": "자유게시판"
}
},
@@ -242,41 +242,32 @@
},
{
"step": 19,
"name": "비밀글 체크박스 확인",
"action": "verify_element",
"target": "input#isSecret",
"verification": {
"exists": true
}
},
{
"step": 20,
"name": "게시글 제목 입력",
"action": "fill",
"target": "input#title",
"value": "E2E 테스트 게시글"
},
{
"step": 21,
"step": 20,
"name": "게시글 내용 입력",
"action": "fill",
"target": "textarea#content",
"value": "이것은 E2E 자동화 테스트를 위한 게시글입니다."
},
{
"step": 22,
"step": 21,
"name": "현재 URL 저장 (등록 전)",
"action": "save_url",
"variable": "url_before_submit"
},
{
"step": 23,
"step": 22,
"name": "게시글 등록 버튼 클릭",
"action": "click",
"target": "button:has-text('등록')"
},
{
"step": 24,
"step": 23,
"name": "게시글 등록 완료 (URL 안정성 검증)",
"action": "verify_url_stability",
"critical": true,
@@ -289,7 +280,7 @@
"notes": "필수 검증 #2: 등록 후 상세 페이지로 이동해야 하며 404 에러 페이지가 나오면 안 됨"
},
{
"step": 25,
"step": 24,
"name": "게시글 상세 페이지 진입 확인",
"action": "verify_page",
"verification": {
@@ -298,14 +289,14 @@
}
},
{
"step": 26,
"step": 25,
"name": "게시글 ID 저장",
"action": "extract_from_url",
"pattern": "/ko/boards/free/(\\d+)",
"variable": "post_id"
},
{
"step": 27,
"step": 26,
"name": "작성자 정보 표시 확인",
"action": "verify_element",
"target": "text=/\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}/",
@@ -314,7 +305,7 @@
}
},
{
"step": 28,
"step": 27,
"name": "조회수 표시 확인",
"action": "verify_element",
"target": "svg.lucide-eye",
@@ -323,7 +314,7 @@
}
},
{
"step": 29,
"step": 28,
"name": "수정 버튼 존재 확인 (작성자)",
"action": "verify_element",
"target": "button:has-text('수정')",
@@ -332,7 +323,7 @@
}
},
{
"step": 30,
"step": 29,
"name": "삭제 버튼 존재 확인 (작성자)",
"action": "verify_element",
"target": "button:has-text('삭제')",
@@ -341,16 +332,16 @@
}
},
{
"step": 31,
"step": 30,
"name": "댓글 섹션 확인",
"action": "verify_element",
"target": "text=/댓글 \\(\\d+\\)/",
"target": "text=/댓글/",
"verification": {
"exists": true
}
},
{
"step": 32,
"step": 31,
"name": "댓글 입력란 확인",
"action": "verify_element",
"target": "textarea[placeholder*='댓글']",
@@ -359,20 +350,20 @@
}
},
{
"step": 33,
"step": 32,
"name": "첫 번째 댓글 작성",
"action": "fill",
"target": "textarea[placeholder*='댓글']",
"value": "첫 번째 테스트 댓글입니다."
},
{
"step": 34,
"step": 33,
"name": "댓글 등록 버튼 클릭",
"action": "click",
"target": "button:has-text('댓글 등록')"
"target": "button:has-text('댓글 등록'), button:has-text('등록')"
},
{
"step": 35,
"step": 34,
"name": "댓글 등록 확인",
"action": "verify_text",
"verification": {
@@ -381,109 +372,122 @@
}
},
{
"step": 36,
"step": 35,
"name": "댓글 수 업데이트 확인",
"action": "verify_element",
"target": "text=/댓글 \\(1\\)/",
"verification": {
"exists": true
}
"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": 37,
"step": 36,
"name": "두 번째 댓글 작성",
"action": "fill",
"target": "textarea[placeholder*='댓글']",
"value": "두 번째 테스트 댓글입니다."
},
{
"step": 38,
"step": 37,
"name": "두 번째 댓글 등록",
"action": "click",
"target": "button:has-text('댓글 등록')"
"target": "button:has-text('댓글 등록'), button:has-text('등록')"
},
{
"step": 39,
"name": "댓글 수 업데이트 확인 (2개)",
"action": "verify_element",
"target": "text=/댓글 \\(2\\)/",
"verification": {
"exists": true
}
},
{
"step": 40,
"name": "댓글 수정 버튼 클릭 (첫 번째 댓글)",
"action": "click_nth",
"target": "button:has-text('수정')",
"nth": 0
},
{
"step": 41,
"name": "댓글 수정 입력란 확인",
"action": "verify_element",
"target": "textarea",
"verification": {
"count": 2,
"notes": "댓글 입력란 + 수정 입력란"
}
},
{
"step": 42,
"name": "댓글 내용 수정",
"action": "fill_nth",
"target": "textarea",
"nth": 0,
"value": "수정된 첫 번째 댓글입니다."
},
{
"step": 43,
"name": "댓글 수정 저장",
"action": "click",
"target": "button:has-text('저장')"
},
{
"step": 44,
"name": "댓글 수정 확인",
"action": "verify_text",
"verification": {
"text": "수정된 첫 번째 댓글입니다.",
"exists": true
}
},
{
"step": 45,
"name": "댓글 삭제 버튼 클릭 (두 번째 댓글)",
"action": "click_nth",
"target": "button:has-text('삭제')",
"nth": 1
},
{
"step": 46,
"name": "댓글 삭제 확인",
"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 textareas = Array.from(document.querySelectorAll('textarea')); var editTA = textareas.find(function(t){ return t.value && t.value.includes('첫 번째 테스트'); }); if(editTA){ var setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype,'value').set; setter.call(editTA, '수정된 첫 번째 댓글입니다.'); editTA.dispatchEvent(new Event('input',{bubbles:true})); editTA.dispatchEvent(new Event('change',{bubbles:true})); return 'filled edit textarea'; } var inputs = Array.from(document.querySelectorAll('input[type=\"text\"]')); var editInput = inputs.find(function(i){ return i.value && i.value.includes('첫 번째 테스트'); }); if(editInput){ var inputSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set; inputSetter.call(editInput, '수정된 첫 번째 댓글입니다.'); editInput.dispatchEvent(new Event('input',{bubbles:true})); editInput.dispatchEvent(new Event('change',{bubbles:true})); return 'filled edit input'; } var editables = document.querySelectorAll('[contenteditable=\"true\"]'); for(var i=0; i<editables.length; i++){ if(editables[i].textContent && editables[i].textContent.includes('첫 번째 테스트')){ editables[i].textContent = '수정된 첫 번째 댓글입니다.'; editables[i].dispatchEvent(new Event('input',{bubbles:true})); return 'filled contenteditable'; }} return 'edit element not found'; })()"
}
]
},
{
"step": 41,
"name": "댓글 수정 저장",
"actions": [
{
"type": "evaluate",
"script": "(function(){ var btn = Array.from(document.querySelectorAll('button')).find(function(b){ return b.innerText && (b.innerText.includes('저장') || b.innerText.includes('수정 완료') || b.innerText.includes('확인')); }); if(btn){ btn.click(); return 'save clicked'; } return 'save btn not found'; })()"
},
{ "type": "wait", "duration": 1500 }
]
},
{
"step": 42,
"name": "댓글 수정 확인",
"action": "verify_text",
"verification": {
"text": "수정된 첫 번째 댓글",
"exists": true
}
},
{
"step": 43,
"name": "두 번째 댓글 삭제 버튼 클릭",
"description": "댓글 영역 내의 삭제 버튼을 찾아 클릭 (게시글 삭제 버튼과 구별)",
"actions": [
{
"type": "evaluate",
"script": "(function(){ var allBtns = Array.from(document.querySelectorAll('button')).filter(function(b){ return b.innerText && b.innerText.trim() === '삭제'; }); var commentBtns = allBtns.filter(function(b){ return b.closest('[class*=\"comment\"], [class*=\"Comment\"], [class*=\"reply\"]'); }); if(commentBtns.length > 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": 47,
"name": "댓글 수 업데이트 확인 (1개)",
"action": "verify_element",
"target": "text=/댓글 \\(1\\)/",
"verification": {
"exists": true
}
"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": 48,
"step": 47,
"name": "게시글 수정 버튼 클릭",
"action": "click",
"target": "button:has-text('수정')"
},
{
"step": 49,
"step": 48,
"name": "게시글 수정 페이지 진입 확인",
"action": "verify_url",
"verification": {
@@ -491,7 +495,7 @@
}
},
{
"step": 50,
"step": 49,
"name": "제목 필드에 기존 값 확인",
"action": "verify_input_value",
"target": "input#title",
@@ -500,39 +504,33 @@
}
},
{
"step": 51,
"step": 50,
"name": "제목 수정",
"action": "fill",
"target": "input#title",
"value": "E2E 테스트 게시글 (수정됨)"
},
{
"step": 52,
"step": 51,
"name": "내용 수정",
"action": "fill",
"target": "textarea#content",
"value": "수정된 내용입니다. E2E 자동화 테스트를 위한 게시글입니다."
},
{
"step": 53,
"name": "비밀글 체크",
"action": "check",
"target": "input#isSecret"
},
{
"step": 54,
"step": 52,
"name": "현재 URL 저장 (수정 전)",
"action": "save_url",
"variable": "url_before_update"
},
{
"step": 55,
"step": 53,
"name": "수정 저장 버튼 클릭",
"action": "click",
"target": "button[type='submit']:has-text('저장'), button:has-text('수정')"
},
{
"step": 56,
"step": 54,
"name": "게시글 수정 완료 (URL 안정성 검증)",
"action": "verify_url_stability",
"critical": true,
@@ -545,7 +543,7 @@
"notes": "필수 검증 #2: 수정 후 상세 페이지로 돌아가야 하며 404 에러 페이지가 나오면 안 됨"
},
{
"step": 57,
"step": 55,
"name": "수정된 제목 확인",
"action": "verify_text",
"verification": {
@@ -554,7 +552,7 @@
}
},
{
"step": 58,
"step": 56,
"name": "수정된 내용 확인",
"action": "verify_text",
"verification": {
@@ -563,13 +561,13 @@
}
},
{
"step": 59,
"step": 57,
"name": "목록으로 이동 버튼 클릭",
"action": "click",
"target": "button:has-text('목록으로')"
},
{
"step": 60,
"step": 58,
"name": "목록 페이지 복귀 확인",
"action": "verify_url",
"verification": {
@@ -577,7 +575,7 @@
}
},
{
"step": 61,
"step": 59,
"name": "수정된 게시글 목록 확인",
"action": "verify_text",
"verification": {
@@ -586,13 +584,13 @@
}
},
{
"step": 62,
"step": 60,
"name": "게시글 클릭하여 상세 진입",
"action": "click",
"target": "text=E2E 테스트 게시글 (수정됨)"
},
{
"step": 63,
"step": 61,
"name": "상세 페이지 진입 확인",
"action": "verify_url",
"verification": {
@@ -600,7 +598,7 @@
}
},
{
"step": 64,
"step": 62,
"name": "조회수 증가 확인",
"action": "verify_element",
"target": "svg.lucide-eye ~ text",
@@ -610,13 +608,13 @@
}
},
{
"step": 65,
"step": 63,
"name": "게시글 삭제 버튼 클릭",
"action": "click",
"target": "button:has-text('삭제')"
},
{
"step": 66,
"step": 64,
"name": "삭제 확인 다이얼로그 표시 확인",
"action": "verify_dialog",
"verification": {
@@ -625,19 +623,19 @@
}
},
{
"step": 67,
"step": 65,
"name": "현재 URL 저장 (삭제 전)",
"action": "save_url",
"variable": "url_before_delete"
},
{
"step": 68,
"step": 66,
"name": "삭제 확인 버튼 클릭",
"action": "click",
"target": "button:has-text('삭제'):last-of-type"
},
{
"step": 69,
"step": 67,
"name": "게시글 삭제 완료 (URL 안정성 검증)",
"action": "verify_url_stability",
"critical": true,
@@ -650,7 +648,7 @@
"notes": "필수 검증 #2: 삭제 후 목록 페이지로 돌아가야 하며 404 에러 페이지가 나오면 안 됨"
},
{
"step": 70,
"step": 68,
"name": "목록 페이지 복귀 확인",
"action": "verify_url",
"verification": {
@@ -658,7 +656,7 @@
}
},
{
"step": 71,
"step": 69,
"name": "삭제된 게시글 목록에서 제거 확인",
"action": "verify_text",
"verification": {
@@ -667,48 +665,7 @@
}
},
{
"step": 72,
"name": "페이지네이션 존재 확인 (조건부)",
"action": "verify_element",
"target": "nav[aria-label='pagination']",
"verification": {
"exists": "if_data_gt_10",
"notes": "10개 이상일 때만 페이지네이션 표시"
}
},
{
"step": 73,
"name": "체크박스 선택 테스트 (첫 번째 항목)",
"action": "check_nth",
"target": "input[type='checkbox']",
"nth": 1,
"verification": {
"notes": "0번은 전체 선택, 1번은 첫 번째 데이터"
}
},
{
"step": 74,
"name": "체크박스 선택 해제",
"action": "uncheck_nth",
"target": "input[type='checkbox']",
"nth": 1
},
{
"step": 75,
"name": "전체 선택 체크박스 클릭",
"action": "check_nth",
"target": "input[type='checkbox']",
"nth": 0
},
{
"step": 76,
"name": "전체 선택 해제",
"action": "uncheck_nth",
"target": "input[type='checkbox']",
"nth": 0
},
{
"step": 77,
"step": 70,
"name": "콘솔 에러 확인",
"action": "verify_console",
"verification": {
@@ -786,7 +743,8 @@
"조회수는 게시글 상세 조회 시마다 증가합니다.",
"작성자만 수정/삭제 버튼이 표시됩니다.",
"댓글도 작성자만 수정/삭제 가능합니다.",
"비밀글 체크 시 is_secret=true로 전송됩니다.",
"댓글 수정은 인라인 편집 방식 (별도 textarea 삽입이 아닌 기존 요소 내 편집)",
"댓글 삭제 시 확인 다이얼로그가 표시될 수 있음",
"게시글 내용은 HTML로 저장되며 dangerouslySetInnerHTML로 렌더링됩니다."
]
}