305 lines
15 KiB
JSON
305 lines
15 KiB
JSON
|
|
{
|
||
|
|
"$schema": "E2E Accessibility Testing Configuration",
|
||
|
|
"version": "1.0.0",
|
||
|
|
"description": "WCAG 기반 접근성 테스트 설정",
|
||
|
|
"lastUpdated": "2026-01-31",
|
||
|
|
|
||
|
|
"accessibility": {
|
||
|
|
"enabled": true,
|
||
|
|
"standard": "WCAG 2.1",
|
||
|
|
"level": "AA",
|
||
|
|
"runOnEveryPage": true,
|
||
|
|
"failOnViolation": false
|
||
|
|
},
|
||
|
|
|
||
|
|
"wcagLevels": {
|
||
|
|
"A": {
|
||
|
|
"description": "최소 접근성 수준",
|
||
|
|
"required": true,
|
||
|
|
"criteria": 30
|
||
|
|
},
|
||
|
|
"AA": {
|
||
|
|
"description": "권장 접근성 수준 (법적 기준)",
|
||
|
|
"required": true,
|
||
|
|
"criteria": 20
|
||
|
|
},
|
||
|
|
"AAA": {
|
||
|
|
"description": "최고 접근성 수준",
|
||
|
|
"required": false,
|
||
|
|
"criteria": 28
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
"categories": {
|
||
|
|
"perceivable": {
|
||
|
|
"description": "인식의 용이성",
|
||
|
|
"checks": [
|
||
|
|
"imageAltText",
|
||
|
|
"colorContrast",
|
||
|
|
"textResize",
|
||
|
|
"audioVideoAlternatives",
|
||
|
|
"contentStructure"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"operable": {
|
||
|
|
"description": "운용의 용이성",
|
||
|
|
"checks": [
|
||
|
|
"keyboardAccessible",
|
||
|
|
"focusVisible",
|
||
|
|
"skipLinks",
|
||
|
|
"pageTitle",
|
||
|
|
"linkPurpose",
|
||
|
|
"noKeyboardTrap"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"understandable": {
|
||
|
|
"description": "이해의 용이성",
|
||
|
|
"checks": [
|
||
|
|
"languageOfPage",
|
||
|
|
"consistentNavigation",
|
||
|
|
"errorIdentification",
|
||
|
|
"labels",
|
||
|
|
"errorSuggestion"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"robust": {
|
||
|
|
"description": "견고성",
|
||
|
|
"checks": [
|
||
|
|
"validHTML",
|
||
|
|
"ariaUsage",
|
||
|
|
"nameRoleValue",
|
||
|
|
"statusMessages"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
"rules": {
|
||
|
|
"imageAltText": {
|
||
|
|
"id": "image-alt",
|
||
|
|
"description": "이미지에 대체 텍스트 필수",
|
||
|
|
"wcag": "1.1.1",
|
||
|
|
"level": "A",
|
||
|
|
"impact": "critical",
|
||
|
|
"selector": "img",
|
||
|
|
"check": "hasAltAttribute"
|
||
|
|
},
|
||
|
|
"colorContrast": {
|
||
|
|
"id": "color-contrast",
|
||
|
|
"description": "텍스트와 배경의 명도 대비",
|
||
|
|
"wcag": "1.4.3",
|
||
|
|
"level": "AA",
|
||
|
|
"impact": "serious",
|
||
|
|
"ratios": {
|
||
|
|
"normalText": 4.5,
|
||
|
|
"largeText": 3,
|
||
|
|
"uiComponents": 3
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"keyboardAccessible": {
|
||
|
|
"id": "keyboard",
|
||
|
|
"description": "키보드로 모든 기능 사용 가능",
|
||
|
|
"wcag": "2.1.1",
|
||
|
|
"level": "A",
|
||
|
|
"impact": "critical",
|
||
|
|
"check": "focusableElements"
|
||
|
|
},
|
||
|
|
"focusVisible": {
|
||
|
|
"id": "focus-visible",
|
||
|
|
"description": "포커스 표시 명확",
|
||
|
|
"wcag": "2.4.7",
|
||
|
|
"level": "AA",
|
||
|
|
"impact": "serious",
|
||
|
|
"check": "hasFocusIndicator"
|
||
|
|
},
|
||
|
|
"formLabels": {
|
||
|
|
"id": "label",
|
||
|
|
"description": "폼 요소에 레이블 연결",
|
||
|
|
"wcag": "1.3.1",
|
||
|
|
"level": "A",
|
||
|
|
"impact": "critical",
|
||
|
|
"selector": "input, select, textarea",
|
||
|
|
"check": "hasAssociatedLabel"
|
||
|
|
},
|
||
|
|
"linkPurpose": {
|
||
|
|
"id": "link-name",
|
||
|
|
"description": "링크 목적 명확",
|
||
|
|
"wcag": "2.4.4",
|
||
|
|
"level": "A",
|
||
|
|
"impact": "serious",
|
||
|
|
"selector": "a[href]",
|
||
|
|
"check": "hasAccessibleName"
|
||
|
|
},
|
||
|
|
"headingOrder": {
|
||
|
|
"id": "heading-order",
|
||
|
|
"description": "제목 순서 논리적",
|
||
|
|
"wcag": "1.3.1",
|
||
|
|
"level": "A",
|
||
|
|
"impact": "moderate",
|
||
|
|
"check": "sequentialHeadings"
|
||
|
|
},
|
||
|
|
"ariaValid": {
|
||
|
|
"id": "aria-valid",
|
||
|
|
"description": "ARIA 속성 올바른 사용",
|
||
|
|
"wcag": "4.1.2",
|
||
|
|
"level": "A",
|
||
|
|
"impact": "critical",
|
||
|
|
"check": "validAriaAttributes"
|
||
|
|
},
|
||
|
|
"buttonName": {
|
||
|
|
"id": "button-name",
|
||
|
|
"description": "버튼에 접근 가능한 이름",
|
||
|
|
"wcag": "4.1.2",
|
||
|
|
"level": "A",
|
||
|
|
"impact": "critical",
|
||
|
|
"selector": "button, [role='button']",
|
||
|
|
"check": "hasAccessibleName"
|
||
|
|
},
|
||
|
|
"tabIndex": {
|
||
|
|
"id": "tabindex",
|
||
|
|
"description": "tabindex 올바른 사용",
|
||
|
|
"wcag": "2.4.3",
|
||
|
|
"level": "A",
|
||
|
|
"impact": "serious",
|
||
|
|
"check": "validTabIndex"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
"scripts": {
|
||
|
|
"runAccessibilityAudit": "(async function() { const issues = []; const checks = { images: [], forms: [], buttons: [], links: [], headings: [], aria: [], focus: [], contrast: [] }; document.querySelectorAll('img').forEach(img => { if (!img.alt && !img.getAttribute('aria-label') && !img.getAttribute('aria-labelledby')) { checks.images.push({ element: 'img', src: img.src?.substring(0, 50), issue: 'Missing alt text', wcag: '1.1.1', impact: 'critical' }); } }); document.querySelectorAll('input, select, textarea').forEach(input => { const id = input.id; const hasLabel = id && document.querySelector(`label[for='${id}']`); const hasAriaLabel = input.getAttribute('aria-label') || input.getAttribute('aria-labelledby'); if (!hasLabel && !hasAriaLabel && input.type !== 'hidden' && input.type !== 'submit' && input.type !== 'button') { checks.forms.push({ element: input.tagName.toLowerCase(), type: input.type, issue: 'Missing label', wcag: '1.3.1', impact: 'critical' }); } }); document.querySelectorAll('button, [role=\"button\"]').forEach(btn => { const hasName = btn.innerText?.trim() || btn.getAttribute('aria-label') || btn.getAttribute('title'); if (!hasName) { checks.buttons.push({ element: 'button', issue: 'Missing accessible name', wcag: '4.1.2', impact: 'critical' }); } }); document.querySelectorAll('a[href]').forEach(link => { const hasName = link.innerText?.trim() || link.getAttribute('aria-label'); if (!hasName) { checks.links.push({ element: 'a', href: link.href?.substring(0, 30), issue: 'Missing link text', wcag: '2.4.4', impact: 'serious' }); } }); const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); let lastLevel = 0; headings.forEach(h => { const level = parseInt(h.tagName[1]); if (level > lastLevel + 1 && lastLevel !== 0) { checks.headings.push({ element: h.tagName, issue: `Skipped heading level (h${lastLevel} to h${level})`, wcag: '1.3.1', impact: 'moderate' }); } lastLevel = level; }); const summary = { total: 0, critical: 0, serious: 0, moderate: 0, minor: 0 }; Object.values(checks).forEach(arr => { arr.forEach(issue => { summary.total++; summary[issue.impact] = (summary[issue.impact] || 0) + 1; }); }); return { url: window.location.href, timestamp: new Date().toISOString(), summary, checks, passed: summary.critical === 0 }; })()",
|
||
|
|
|
||
|
|
"checkColorContrast": "(function() { const results = []; const textElements = document.querySelectorAll('p, span, a, button, label, h1, h2, h3, h4, h5, h6, li, td, th'); const sampleSize = Math.min(textElements.length, 50); for (let i = 0; i < sampleSize; i++) { const el = textElements[i]; const style = window.getComputedStyle(el); const color = style.color; const bgColor = style.backgroundColor; if (color && bgColor && bgColor !== 'rgba(0, 0, 0, 0)') { results.push({ element: el.tagName, text: el.innerText?.substring(0, 20), color, bgColor }); } } return { checked: sampleSize, results: results.slice(0, 10) }; })()",
|
||
|
|
|
||
|
|
"checkKeyboardNavigation": "(async function() { const focusable = document.querySelectorAll('a[href], button, input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'); const results = { total: focusable.length, withFocusStyle: 0, tabbable: 0, issues: [] }; focusable.forEach(el => { const tabIndex = el.getAttribute('tabindex'); if (tabIndex === null || parseInt(tabIndex) >= 0) results.tabbable++; el.focus(); const style = window.getComputedStyle(el); if (style.outlineWidth !== '0px' || style.boxShadow !== 'none') { results.withFocusStyle++; } else { results.issues.push({ element: el.tagName, text: el.innerText?.substring(0, 20) || el.placeholder, issue: 'No visible focus indicator' }); } }); return results; })()",
|
||
|
|
|
||
|
|
"checkAriaUsage": "(function() { const ariaElements = document.querySelectorAll('[aria-label], [aria-labelledby], [aria-describedby], [role]'); const results = { total: ariaElements.length, valid: 0, issues: [] }; ariaElements.forEach(el => { const role = el.getAttribute('role'); const ariaLabelledby = el.getAttribute('aria-labelledby'); const ariaDescribedby = el.getAttribute('aria-describedby'); if (ariaLabelledby) { const labelEl = document.getElementById(ariaLabelledby); if (!labelEl) { results.issues.push({ element: el.tagName, issue: `aria-labelledby references non-existent id: ${ariaLabelledby}` }); } } if (ariaDescribedby) { const descEl = document.getElementById(ariaDescribedby); if (!descEl) { results.issues.push({ element: el.tagName, issue: `aria-describedby references non-existent id: ${ariaDescribedby}` }); } } if (!results.issues.find(i => i.element === el.tagName)) results.valid++; }); return results; })()",
|
||
|
|
|
||
|
|
"checkFormAccessibility": "(function() { const forms = document.querySelectorAll('form'); const results = { forms: forms.length, issues: [] }; forms.forEach((form, formIndex) => { const inputs = form.querySelectorAll('input, select, textarea'); inputs.forEach(input => { if (input.type === 'hidden' || input.type === 'submit' || input.type === 'button') return; const id = input.id; const name = input.name; const hasLabel = id && document.querySelector(`label[for='${id}']`); const hasAriaLabel = input.getAttribute('aria-label'); const hasPlaceholder = input.placeholder; if (!hasLabel && !hasAriaLabel) { results.issues.push({ form: formIndex + 1, element: input.tagName, type: input.type, name: name || id, issue: 'No associated label', suggestion: hasPlaceholder ? 'Has placeholder but no label' : 'Add label element or aria-label' }); } }); const submitBtn = form.querySelector('button[type=\"submit\"], input[type=\"submit\"]'); if (!submitBtn) { results.issues.push({ form: formIndex + 1, issue: 'No submit button found' }); } }); return results; })()",
|
||
|
|
|
||
|
|
"generateAccessibilityReport": "(function() { const audit = { url: window.location.href, timestamp: new Date().toISOString(), images: { total: 0, withAlt: 0, issues: [] }, forms: { total: 0, labeled: 0, issues: [] }, buttons: { total: 0, named: 0, issues: [] }, links: { total: 0, named: 0, issues: [] }, headings: { structure: [], issues: [] }, landmarks: [], focusable: 0 }; document.querySelectorAll('img').forEach(img => { audit.images.total++; if (img.alt || img.getAttribute('aria-label')) audit.images.withAlt++; else audit.images.issues.push({ src: img.src?.split('/').pop() }); }); document.querySelectorAll('input:not([type=hidden]), select, textarea').forEach(el => { audit.forms.total++; const hasLabel = (el.id && document.querySelector(`label[for='${el.id}']`)) || el.getAttribute('aria-label'); if (hasLabel) audit.forms.labeled++; else audit.forms.issues.push({ type: el.type, name: el.name }); }); document.querySelectorAll('button, [role=button]').forEach(btn => { audit.buttons.total++; if (btn.innerText?.trim() || btn.getAttribute('aria-label')) audit.buttons.named++; else audit.buttons.issues.push({ class: btn.className?.substring(0, 30) }); }); document.querySelectorAll('h1,h2,h3,h4,h5,h6').forEach(h => { audit.headings.structure.push({ level: h.tagName, text: h.innerText?.substring(0, 30) }); }); audit.landmarks = Array.from(document.querySelectorAll('header, nav, main, aside, footer, [role=banner], [role=navigation], [role=main], [role=complementary], [role=contentinfo]')).map(el => el.tagName.toLowerCase() + (el.getAttribute('role') ? `[role=${el.getAttribute('role')}]` : '')); audit.focusable = document.querySelectorAll('a[href], button, input, select, textarea, [tabindex]:not([tabindex=\"-1\"])').length; const score = Math.round(((audit.images.withAlt / Math.max(audit.images.total, 1)) + (audit.forms.labeled / Math.max(audit.forms.total, 1)) + (audit.buttons.named / Math.max(audit.buttons.total, 1))) / 3 * 100); return { ...audit, score, grade: score >= 90 ? 'A' : score >= 70 ? 'B' : score >= 50 ? 'C' : 'F' }; })()"
|
||
|
|
},
|
||
|
|
|
||
|
|
"impactLevels": {
|
||
|
|
"critical": {
|
||
|
|
"description": "스크린 리더 사용자가 콘텐츠 접근 불가",
|
||
|
|
"action": "즉시 수정",
|
||
|
|
"examples": ["이미지 alt 누락", "폼 레이블 누락", "키보드 접근 불가"]
|
||
|
|
},
|
||
|
|
"serious": {
|
||
|
|
"description": "접근성 심각한 장애",
|
||
|
|
"action": "우선 수정",
|
||
|
|
"examples": ["색상 대비 부족", "포커스 표시 없음", "링크 텍스트 없음"]
|
||
|
|
},
|
||
|
|
"moderate": {
|
||
|
|
"description": "사용성 저하",
|
||
|
|
"action": "개선 권장",
|
||
|
|
"examples": ["제목 순서 불규칙", "랜드마크 누락"]
|
||
|
|
},
|
||
|
|
"minor": {
|
||
|
|
"description": "모범 사례 미준수",
|
||
|
|
"action": "선택적 개선",
|
||
|
|
"examples": ["중복 ID", "빈 링크"]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
"reporting": {
|
||
|
|
"includeInTestReport": true,
|
||
|
|
"separateReport": true,
|
||
|
|
"format": {
|
||
|
|
"summary": {
|
||
|
|
"score": "접근성 점수 (0-100)",
|
||
|
|
"grade": "등급 (A/B/C/F)",
|
||
|
|
"violations": "위반 사항 수",
|
||
|
|
"passed": "통과 항목 수"
|
||
|
|
},
|
||
|
|
"detail": {
|
||
|
|
"rule": "위반 규칙",
|
||
|
|
"impact": "영향도",
|
||
|
|
"wcag": "WCAG 기준",
|
||
|
|
"element": "해당 요소",
|
||
|
|
"suggestion": "수정 제안"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"template": {
|
||
|
|
"header": "## ♿ 접근성 테스트 결과",
|
||
|
|
"sections": {
|
||
|
|
"summary": "### 요약",
|
||
|
|
"violations": "### 위반 사항",
|
||
|
|
"passed": "### 통과 항목",
|
||
|
|
"recommendations": "### 개선 권장"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
"thresholds": {
|
||
|
|
"pass": {
|
||
|
|
"score": 70,
|
||
|
|
"critical": 0,
|
||
|
|
"serious": 5
|
||
|
|
},
|
||
|
|
"warn": {
|
||
|
|
"score": 50,
|
||
|
|
"critical": 3,
|
||
|
|
"serious": 10
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
"pageSpecific": {
|
||
|
|
"login": {
|
||
|
|
"critical": ["formLabels", "buttonName", "colorContrast"],
|
||
|
|
"focus": ["keyboardAccessible", "focusVisible"]
|
||
|
|
},
|
||
|
|
"dashboard": {
|
||
|
|
"critical": ["imageAltText", "linkPurpose", "headingOrder"],
|
||
|
|
"focus": ["ariaValid", "landmarks"]
|
||
|
|
},
|
||
|
|
"form": {
|
||
|
|
"critical": ["formLabels", "errorIdentification", "inputInstructions"],
|
||
|
|
"focus": ["focusOrder", "errorSuggestion"]
|
||
|
|
},
|
||
|
|
"table": {
|
||
|
|
"critical": ["tableHeaders", "tableCaption"],
|
||
|
|
"focus": ["tableSummary"]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
"automation": {
|
||
|
|
"runOn": {
|
||
|
|
"pageLoad": true,
|
||
|
|
"formSubmit": false,
|
||
|
|
"modalOpen": true
|
||
|
|
},
|
||
|
|
"skipElements": [
|
||
|
|
"[aria-hidden='true']",
|
||
|
|
"[hidden]",
|
||
|
|
".sr-only",
|
||
|
|
".visually-hidden"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
|
||
|
|
"recommendations": {
|
||
|
|
"imageAltText": "모든 이미지에 alt 속성 추가. 장식용 이미지는 alt='' 사용",
|
||
|
|
"formLabels": "<label for='id'>를 사용하거나 aria-label 속성 추가",
|
||
|
|
"colorContrast": "텍스트와 배경의 명도 대비 4.5:1 이상 유지",
|
||
|
|
"keyboardNav": "모든 인터랙티브 요소에 키보드로 접근 가능하게",
|
||
|
|
"focusVisible": "포커스 시 outline 또는 다른 시각적 표시 제공",
|
||
|
|
"headingOrder": "h1 → h2 → h3 순서로 사용, 레벨 건너뛰기 금지",
|
||
|
|
"linkPurpose": "링크 텍스트만으로 목적지 이해 가능하게",
|
||
|
|
"ariaUsage": "ARIA는 네이티브 HTML이 불가능할 때만 사용"
|
||
|
|
},
|
||
|
|
|
||
|
|
"integration": {
|
||
|
|
"withVisual": {
|
||
|
|
"captureOnViolation": true,
|
||
|
|
"highlightIssues": true
|
||
|
|
},
|
||
|
|
"withPerformance": {
|
||
|
|
"trackLoadTime": true,
|
||
|
|
"correlateWithA11y": true
|
||
|
|
},
|
||
|
|
"withCRUD": {
|
||
|
|
"checkForms": true,
|
||
|
|
"checkMessages": true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|