From bd41ff1c12caa75ecd33601d2bcafb8d4726eedd Mon Sep 17 00:00:00 2001 From: light Date: Sat, 31 Jan 2026 08:51:06 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=A0=84=EC=97=AD=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC=20=EC=84=A4=EC=A0=95=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _global-modal-config.json | 224 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 _global-modal-config.json diff --git a/_global-modal-config.json b/_global-modal-config.json new file mode 100644 index 0000000..a1cc473 --- /dev/null +++ b/_global-modal-config.json @@ -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" + } + } + } +}