feat: 전역 모달 처리 설정 파일 추가

This commit is contained in:
light
2026-01-31 08:51:06 +09:00
parent cff20a6c0e
commit bd41ff1c12

224
_global-modal-config.json Normal file
View File

@@ -0,0 +1,224 @@
{
"$schema": "E2E Global Modal Handling Configuration",
"version": "1.0.0",
"description": "모든 E2E 테스트 시나리오에 적용되는 전역 모달/팝업 처리 설정",
"lastUpdated": "2026-01-31",
"modalDetection": {
"description": "모달/팝업 감지를 위한 셀렉터",
"selectors": [
"[role='dialog']",
"[role='alertdialog']",
"[data-state='open']",
".modal",
"[class*='modal']",
"[class*='Modal']",
"[class*='dialog']",
"[class*='Dialog']",
"[class*='popup']",
"[class*='Popup']",
"[class*='overlay'][class*='open']",
"[aria-modal='true']"
],
"excludeSelectors": [
"[class*='tooltip']",
"[class*='Tooltip']",
"[class*='dropdown']",
"[role='menu']"
],
"checkScript": "(function() { const selectors = [\"[role='dialog']\", \"[role='alertdialog']\", \"[data-state='open']\", \".modal\", \"[class*='modal']\", \"[class*='Modal']\", \"[class*='Dialog']\", \"[aria-modal='true']\"]; for (const sel of selectors) { const el = document.querySelector(sel); if (el && el.offsetParent !== null && !el.className?.includes('tooltip') && !el.className?.includes('dropdown')) return { isOpen: true, selector: sel, element: el.tagName }; } return { isOpen: false }; })()"
},
"modalFocus": {
"description": "모달이 열리면 반드시 모달 내부에서만 동작",
"rules": [
"모달 감지 시 모달 내부 요소만 선택/클릭 가능",
"모달 외부 요소 클릭 시도 전 반드시 모달 닫기 실행",
"모달 내 작업 완료 후 다음 스텝 진행 전 모달 상태 확인"
],
"focusScript": "(function() { const modal = document.querySelector(\"[role='dialog'], [aria-modal='true'], [class*='modal']:not([class*='tooltip'])\"); if (modal && modal.offsetParent !== null) { modal.focus(); return { focused: true, element: modal.tagName }; } return { focused: false }; })()"
},
"modalClose": {
"description": "모달 닫기 절차 (우선순위 순서대로 시도)",
"methods": [
{
"priority": 1,
"name": "X 버튼 클릭",
"description": "모달 우측 상단 X 버튼",
"selectors": [
"button[class*='close']",
"button[class*='Close']",
"[aria-label='닫기']",
"[aria-label='Close']",
"[aria-label='close']",
"svg[class*='close']",
"[class*='modal-close']",
"[class*='dialog-close']",
"button[class*='dismiss']"
]
},
{
"priority": 2,
"name": "닫기/Close 텍스트 버튼",
"description": "텍스트가 '닫기' 또는 'Close'인 버튼",
"textMatches": ["닫기", "Close", "close"]
},
{
"priority": 3,
"name": "취소/Cancel 버튼",
"description": "작업 취소 버튼",
"textMatches": ["취소", "Cancel", "cancel"]
},
{
"priority": 4,
"name": "확인/OK 버튼",
"description": "확인 후 닫기 (알림 다이얼로그)",
"textMatches": ["확인", "OK", "ok", "예", "Yes"]
},
{
"priority": 5,
"name": "ESC 키",
"description": "Escape 키로 닫기",
"action": "pressKey",
"key": "Escape"
},
{
"priority": 6,
"name": "백드롭 클릭",
"description": "모달 외부 영역 클릭 (최후 수단)",
"action": "clickBackdrop"
}
],
"closeScript": "(async function() { const modal = document.querySelector(\"[role='dialog'], [aria-modal='true'], [class*='modal']:not([class*='tooltip']), [class*='Modal'], [class*='Dialog']\"); if (!modal || modal.offsetParent === null) return { status: 'no_modal', closed: true }; const xBtn = modal.querySelector(\"button[class*='close'], button[class*='Close'], [aria-label='닫기'], [aria-label='Close'], svg[class*='close'], [class*='modal-close'], [class*='dialog-close']\"); if (xBtn) { xBtn.click(); await new Promise(r => setTimeout(r, 500)); return { status: 'closed_by_x', closed: true }; } const textBtn = Array.from(modal.querySelectorAll('button, [role=\"button\"]')).find(b => ['닫기', 'Close', '취소', 'Cancel', '확인', 'OK'].some(t => b.innerText?.trim() === t || b.innerText?.trim().includes(t))); if (textBtn) { textBtn.click(); await new Promise(r => setTimeout(r, 500)); return { status: 'closed_by_text_btn', closed: true }; } document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27, bubbles: true })); await new Promise(r => setTimeout(r, 500)); const stillOpen = document.querySelector(\"[role='dialog'], [aria-modal='true'], [class*='modal']:not([class*='tooltip'])\"); return { status: stillOpen?.offsetParent ? 'failed_to_close' : 'closed_by_esc', closed: !stillOpen?.offsetParent }; })()"
},
"actionHooks": {
"description": "액션 전후에 실행할 모달 처리 훅",
"beforeAction": {
"description": "모든 액션 실행 전 모달 상태 확인",
"checkModalOpen": true,
"ifModalOpen": {
"action": "focus_modal",
"description": "모달이 열려있으면 모달에 포커스"
}
},
"afterAction": {
"description": "모달 열기 액션 후 처리",
"triggers": ["click_register", "click_add", "click_detail", "click_edit", "click_view"],
"waitForModal": 1000,
"actions": [
"wait_for_modal_animation",
"focus_modal_content",
"verify_modal_visible"
]
},
"beforeNavigation": {
"description": "페이지 이동 또는 다른 요소 클릭 전 모달 닫기",
"forceCloseModal": true,
"maxCloseAttempts": 3,
"waitAfterClose": 500
},
"onStepComplete": {
"description": "각 스텝 완료 시 모달 상태 정리",
"checkAndCloseModal": true,
"reportIfNotClosed": true
}
},
"specialCases": {
"printDialog": {
"description": "인쇄 다이얼로그 처리",
"detection": "window.print() 호출 또는 인쇄 버튼 클릭",
"handling": "ESC 키로 닫기, 1000ms 대기",
"script": "(async function() { document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27, bubbles: true })); await new Promise(r => setTimeout(r, 1000)); return 'print_dialog_handled'; })()"
},
"pdfPreview": {
"description": "PDF 미리보기 처리",
"detection": "iframe[src*='pdf'], embed[type*='pdf'], [class*='pdf-preview']",
"handling": "부모 모달의 닫기 버튼 클릭",
"script": "(function() { const pdf = document.querySelector(\"iframe[src*='pdf'], embed[type*='pdf'], [class*='pdf-preview']\"); if (pdf) { const parentModal = pdf.closest(\"[role='dialog'], [class*='modal']\"); const closeBtn = parentModal?.querySelector(\"[class*='close']\"); if (closeBtn) { closeBtn.click(); return 'pdf_closed'; } } return 'no_pdf'; })()"
},
"confirmDialog": {
"description": "확인 다이얼로그 (alert, confirm)",
"detection": "[role='alertdialog'], [class*='confirm'], [class*='alert']",
"handling": "확인 또는 취소 버튼 클릭"
},
"toastNotification": {
"description": "토스트 알림 (닫기 불필요)",
"detection": "[class*='toast'], [class*='Toast'], [class*='notification']",
"handling": "자동 닫힘 대기 또는 무시"
}
},
"enforceRules": {
"description": "강제 적용 규칙",
"rule1_modal_focus": {
"name": "모달 포커스 규칙",
"description": "모달이 열린 상태에서는 모달 내부 요소만 조작",
"enforcement": "모달 외부 클릭 시 자동으로 모달 닫기 먼저 실행"
},
"rule2_close_before_navigate": {
"name": "이동 전 닫기 규칙",
"description": "페이지 이동 또는 메뉴 클릭 전 모든 모달 닫기",
"enforcement": "navigate, menuClick 액션 전 forceCloseAllModals 실행"
},
"rule3_step_cleanup": {
"name": "스텝 정리 규칙",
"description": "각 스텝 완료 후 모달 상태 확인 및 정리",
"enforcement": "스텝 종료 시 checkAndCloseModal 자동 실행"
},
"rule4_report_unclosed": {
"name": "미닫힘 보고 규칙",
"description": "모달을 닫지 못한 경우 리포트에 기록",
"enforcement": "BLOCKED: modal_not_closed 상태로 기록"
}
},
"scripts": {
"checkModalState": "(function() { const selectors = [\"[role='dialog']\", \"[role='alertdialog']\", \"[aria-modal='true']\", \"[class*='modal']:not([class*='tooltip'])\", \"[class*='Modal']\", \"[class*='Dialog']\"]; for (const sel of selectors) { const el = document.querySelector(sel); if (el && el.offsetParent !== null) { return { isOpen: true, selector: sel, tagName: el.tagName, className: el.className }; } } return { isOpen: false }; })()",
"forceCloseAllModals": "(async function() { const results = []; let attempts = 0; while (attempts < 3) { const modal = document.querySelector(\"[role='dialog'], [aria-modal='true'], [class*='modal']:not([class*='tooltip']), [class*='Modal']\"); if (!modal || modal.offsetParent === null) break; const closeBtn = modal.querySelector(\"button[class*='close'], [aria-label='닫기'], [aria-label='Close']\") || Array.from(modal.querySelectorAll('button')).find(b => ['닫기', 'Close', '취소', 'Cancel', '확인'].some(t => b.innerText?.includes(t))); if (closeBtn) { closeBtn.click(); results.push({ attempt: attempts + 1, method: 'button' }); } else { document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27, bubbles: true })); results.push({ attempt: attempts + 1, method: 'escape' }); } await new Promise(r => setTimeout(r, 500)); attempts++; } const stillOpen = document.querySelector(\"[role='dialog'], [aria-modal='true'], [class*='modal']:not([class*='tooltip'])\"); return { closed: !stillOpen?.offsetParent, attempts, results }; })()",
"waitForModalAndFocus": "(async function(timeout = 2000) { const start = Date.now(); while (Date.now() - start < timeout) { const modal = document.querySelector(\"[role='dialog'], [aria-modal='true'], [class*='modal']:not([class*='tooltip'])\"); if (modal && modal.offsetParent !== null) { modal.focus(); const firstInput = modal.querySelector('input, textarea, select, button'); if (firstInput) firstInput.focus(); return { found: true, focused: true, waitTime: Date.now() - start }; } await new Promise(r => setTimeout(r, 100)); } return { found: false, focused: false, waitTime: timeout }; })()",
"interactWithinModal": "(function(selector, action = 'click') { const modal = document.querySelector(\"[role='dialog'], [aria-modal='true'], [class*='modal']:not([class*='tooltip'])\"); if (!modal || modal.offsetParent === null) return { error: 'no_modal_open' }; const element = modal.querySelector(selector); if (!element) return { error: 'element_not_found_in_modal', selector }; if (action === 'click') element.click(); return { success: true, action, selector }; })"
},
"testExecutionFlow": {
"description": "테스트 실행 흐름에서 모달 처리 통합",
"stepExecution": [
"1. 스텝 시작 전: checkModalState 실행",
"2. 모달 열림 상태면: 모달 내부에서만 액션 실행",
"3. 모달 열기 액션 후: waitForModalAndFocus 실행",
"4. 모달 내 작업 완료 후: 다음 스텝 전 모달 닫기 여부 확인",
"5. 페이지 이동 전: forceCloseAllModals 실행",
"6. 스텝 완료 시: 모달 상태 확인 및 리포트"
],
"errorHandling": {
"modalNotClosed": {
"action": "retry_close",
"maxRetries": 3,
"onFailure": "report_as_blocked"
},
"elementNotInModal": {
"action": "check_if_modal_closed",
"ifClosed": "proceed_normally",
"ifOpen": "report_error"
}
}
}
}