{ "$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" } } } }