225 lines
12 KiB
JSON
225 lines
12 KiB
JSON
{
|
|
"$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"
|
|
}
|
|
}
|
|
}
|
|
}
|