V13-FE-005: Complete KBX v60 frontend components, T01-T12 screen recipes, and WBS progress tracker
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('HomePage - Full DOM & Visual Analysis', async ({ page }) => {
|
||||
// Navigate to home
|
||||
await page.goto('/', { waitUntil: 'networkidle' })
|
||||
|
||||
// Take screenshot
|
||||
await page.screenshot({ path: 'D:\\Temp\\homepage-full.png', fullPage: true })
|
||||
console.log('✅ Screenshot saved: D:\\Temp\\homepage-full.png')
|
||||
|
||||
// DOM Analysis
|
||||
const domData = await page.evaluate(() => {
|
||||
const totalElements = document.querySelectorAll('*').length
|
||||
const visibleElements = Array.from(document.querySelectorAll('*')).filter(el => {
|
||||
const style = window.getComputedStyle(el)
|
||||
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'
|
||||
}).length
|
||||
|
||||
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6')
|
||||
const buttons = document.querySelectorAll('button')
|
||||
const links = document.querySelectorAll('a')
|
||||
const images = document.querySelectorAll('img')
|
||||
const cards = document.querySelectorAll('[class*="card"], [class*="Card"]')
|
||||
|
||||
// Text content
|
||||
const textContent = document.body.innerText
|
||||
const wordCount = textContent.split(/\s+/).filter(w => w.length > 0).length
|
||||
|
||||
// Extract heading texts
|
||||
const headingTexts = Array.from(headings).map(h => ({
|
||||
tag: h.tagName,
|
||||
text: h.textContent?.trim().substring(0, 50),
|
||||
}))
|
||||
|
||||
// Extract button texts
|
||||
const buttonTexts = Array.from(buttons).map(b => ({
|
||||
text: b.textContent?.trim(),
|
||||
type: b.getAttribute('type') || 'button',
|
||||
}))
|
||||
|
||||
// Extract link texts
|
||||
const linkTexts = Array.from(links).map(l => ({
|
||||
text: l.textContent?.trim().substring(0, 30),
|
||||
href: l.getAttribute('href'),
|
||||
}))
|
||||
|
||||
return {
|
||||
totalElements,
|
||||
visibleElements,
|
||||
headingCount: headings.length,
|
||||
headingTexts,
|
||||
buttonCount: buttons.length,
|
||||
buttonTexts,
|
||||
linkCount: links.length,
|
||||
linkTexts: linkTexts.slice(0, 10),
|
||||
imageCount: images.length,
|
||||
cardCount: cards.length,
|
||||
wordCount,
|
||||
}
|
||||
})
|
||||
|
||||
console.log('\n=== HomePage DOM Analysis ===')
|
||||
console.log(`Total Elements: ${domData.totalElements}`)
|
||||
console.log(`Visible Elements: ${domData.visibleElements}`)
|
||||
console.log(`Headings: ${domData.headingCount}`)
|
||||
console.log(
|
||||
'Heading Texts:',
|
||||
domData.headingTexts.map(h => `${h.tag}: "${h.text}"`).join(' | ')
|
||||
)
|
||||
console.log(`\nButtons: ${domData.buttonCount}`)
|
||||
domData.buttonTexts.forEach((b, i) => {
|
||||
console.log(` ${i + 1}. "${b.text}" (type: ${b.type})`)
|
||||
})
|
||||
console.log(`\nLinks: ${domData.linkCount}`)
|
||||
domData.linkTexts.forEach((l, i) => {
|
||||
console.log(` ${i + 1}. "${l.text}" → ${l.href}`)
|
||||
})
|
||||
console.log(`\nImages: ${domData.imageCount}`)
|
||||
console.log(`Cards/Components: ${domData.cardCount}`)
|
||||
console.log(`Word Count: ${domData.wordCount}`)
|
||||
|
||||
// Accessibility Check
|
||||
const a11yData = await page.evaluate(() => {
|
||||
const elementsWithAria = document.querySelectorAll('[aria-label], [aria-describedby], [role]')
|
||||
const imagesWithAlt = document.querySelectorAll('img[alt]')
|
||||
const buttonsWithAriaOrText = Array.from(document.querySelectorAll('button')).filter(
|
||||
b => b.getAttribute('aria-label') || b.textContent?.trim().length > 0
|
||||
)
|
||||
const linksWithAriaOrText = Array.from(document.querySelectorAll('a')).filter(
|
||||
l => l.getAttribute('aria-label') || l.textContent?.trim().length > 0
|
||||
)
|
||||
const hasLandmarks = !!document.querySelector('main, nav[aria-label], aside[aria-label]')
|
||||
const skipLink = document.querySelector('a.ks-skip')
|
||||
|
||||
return {
|
||||
ariaElements: elementsWithAria.length,
|
||||
imagesWithAlt: imagesWithAlt.length,
|
||||
totalImages: document.querySelectorAll('img').length,
|
||||
accessibleButtons: buttonsWithAriaOrText.length,
|
||||
totalButtons: document.querySelectorAll('button').length,
|
||||
accessibleLinks: linksWithAriaOrText.length,
|
||||
totalLinks: document.querySelectorAll('a').length,
|
||||
hasLandmarks,
|
||||
hasSkipLink: !!skipLink,
|
||||
}
|
||||
})
|
||||
|
||||
console.log('\n=== Accessibility Assessment ===')
|
||||
console.log(`ARIA/Role Elements: ${a11yData.ariaElements}`)
|
||||
console.log(`Images with Alt Text: ${a11yData.imagesWithAlt}/${a11yData.totalImages}`)
|
||||
console.log(`Accessible Buttons: ${a11yData.accessibleButtons}/${a11yData.totalButtons}`)
|
||||
console.log(`Accessible Links: ${a11yData.accessibleLinks}/${a11yData.totalLinks}`)
|
||||
console.log(`Has Landmarks: ${a11yData.hasLandmarks}`)
|
||||
console.log(`Has Skip Link: ${a11yData.hasSkipLink}`)
|
||||
|
||||
// Color & Typography Check
|
||||
const styleData = await page.evaluate(() => {
|
||||
const h1 = document.querySelector('h1')
|
||||
const buttons = document.querySelectorAll('button')
|
||||
const cards = document.querySelectorAll('[class*="card"], [class*="Card"]')
|
||||
|
||||
const h1Style = h1
|
||||
? {
|
||||
fontSize: window.getComputedStyle(h1).fontSize,
|
||||
fontWeight: window.getComputedStyle(h1).fontWeight,
|
||||
color: window.getComputedStyle(h1).color,
|
||||
}
|
||||
: null
|
||||
|
||||
const buttonStyles = Array.from(buttons)
|
||||
.slice(0, 3)
|
||||
.map(b => ({
|
||||
text: b.textContent?.trim().substring(0, 20),
|
||||
backgroundColor: window.getComputedStyle(b).backgroundColor,
|
||||
color: window.getComputedStyle(b).color,
|
||||
padding: window.getComputedStyle(b).padding,
|
||||
minHeight: window.getComputedStyle(b).minHeight,
|
||||
}))
|
||||
|
||||
return {
|
||||
h1Style,
|
||||
buttonStyles,
|
||||
cardCount: cards.length,
|
||||
}
|
||||
})
|
||||
|
||||
console.log('\n=== Typography & Styling ===')
|
||||
console.log('H1:', styleData.h1Style)
|
||||
console.log('Button Samples:')
|
||||
styleData.buttonStyles.forEach((b, i) => {
|
||||
console.log(` ${i + 1}. "${b.text}" | bg: ${b.backgroundColor} | color: ${b.color} | padding: ${b.padding}`)
|
||||
})
|
||||
|
||||
// Completeness Score Calculation
|
||||
const completenessScore = (() => {
|
||||
let score = 0
|
||||
|
||||
// DOM structure (max 25)
|
||||
score += Math.min((domData.visibleElements / 150) * 25, 25)
|
||||
|
||||
// Content (max 20)
|
||||
score += Math.min((domData.wordCount / 300) * 20, 20)
|
||||
|
||||
// Interactivity (max 20)
|
||||
score += Math.min(((domData.buttonCount + domData.linkCount) / 15) * 20, 20)
|
||||
|
||||
// Accessibility (max 20)
|
||||
const a11yScore =
|
||||
(a11yData.imagesWithAlt / Math.max(a11yData.totalImages, 1)) * 5 +
|
||||
(a11yData.accessibleButtons / Math.max(a11yData.totalButtons, 1)) * 5 +
|
||||
(a11yData.accessibleLinks / Math.max(a11yData.totalLinks, 1)) * 5 +
|
||||
(a11yData.hasLandmarks ? 3 : 0) +
|
||||
(a11yData.hasSkipLink ? 2 : 0)
|
||||
score += Math.min(a11yScore, 20)
|
||||
|
||||
// Visual hierarchy (max 15)
|
||||
score += styleData.h1Style ? 8 : 0
|
||||
score += styleData.cardCount > 0 ? 7 : 0
|
||||
|
||||
return Math.min(score, 100)
|
||||
})()
|
||||
|
||||
console.log(`\n=== Completeness Score ===`)
|
||||
console.log(`Overall: ${completenessScore.toFixed(1)}%`)
|
||||
console.log(
|
||||
`Status: ${completenessScore >= 90 ? '✅ COMMERCIAL-GRADE' : completenessScore >= 80 ? '⚠️ GOOD' : '❌ NEEDS WORK'}`
|
||||
)
|
||||
|
||||
// Expectations
|
||||
expect(domData.visibleElements).toBeGreaterThan(80)
|
||||
expect(domData.headingCount).toBeGreaterThan(0)
|
||||
expect(domData.buttonCount).toBeGreaterThan(0)
|
||||
expect(a11yData.hasLandmarks).toBe(true)
|
||||
})
|
||||
@@ -0,0 +1,200 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('모든 페이지 종합 검증 (AGENTS.md v16.0)', () => {
|
||||
// 1. HomePage 검증
|
||||
test('1. HomePage - 국내 기준 상용 수준 검증', async ({ page }) => {
|
||||
await page.goto('/', { waitUntil: 'networkidle' })
|
||||
await page.screenshot({ path: 'D:\\Temp\\home-validation.png', fullPage: true })
|
||||
|
||||
const analysis = await page.evaluate(() => {
|
||||
const h1 = document.querySelector('h1')?.textContent || ''
|
||||
const sections = document.querySelectorAll('section').length
|
||||
const ctas = document.querySelectorAll('a[href*="/model-ops"], a[href*="/governance"]').length
|
||||
const trustedElements = document.querySelectorAll('[class*="trust"], [class*="stat"]').length
|
||||
|
||||
return {
|
||||
heroTitle: h1.substring(0, 50),
|
||||
sections,
|
||||
ctas,
|
||||
trustElements: trustedElements,
|
||||
visibleText: document.body.innerText.length,
|
||||
}
|
||||
})
|
||||
|
||||
console.log('\n✅ HomePage 분석:')
|
||||
console.log(` Hero Title: "${analysis.heroTitle}"`)
|
||||
console.log(` Sections: ${analysis.sections}`)
|
||||
console.log(` CTAs: ${analysis.ctas}`)
|
||||
console.log(` Trust Elements: ${analysis.trustElements}`)
|
||||
console.log(` Text Length: ${analysis.visibleText}`)
|
||||
|
||||
expect(analysis.sections).toBeGreaterThan(5)
|
||||
expect(analysis.ctas).toBeGreaterThan(2)
|
||||
})
|
||||
|
||||
// 2. ModelList 검증
|
||||
test('2. ModelList - 마스터 디테일 레이아웃 검증', async ({ page }) => {
|
||||
await page.goto('/model-ops/models-master', { waitUntil: 'networkidle' })
|
||||
await page.screenshot({ path: 'D:\\Temp\\models-validation.png', fullPage: true })
|
||||
|
||||
const analysis = await page.evaluate(() => {
|
||||
const title = document.querySelector('h1')?.textContent || ''
|
||||
const masterItems = document.querySelectorAll('[class*="list"], [class*="item"]').length
|
||||
const detailPanel = document.querySelector('[class*="detail"]')?.textContent?.length || 0
|
||||
const metrics = document.querySelectorAll('[class*="metric"], [class*="stat"]').length
|
||||
const actions = document.querySelectorAll('button, a[class*="btn"], a[class*="action"]').length
|
||||
|
||||
return {
|
||||
title: title.substring(0, 40),
|
||||
masterItems,
|
||||
detailPanelTextLength: detailPanel,
|
||||
metrics,
|
||||
actions,
|
||||
}
|
||||
})
|
||||
|
||||
console.log('\n✅ ModelList 분석:')
|
||||
console.log(` Title: "${analysis.title}"`)
|
||||
console.log(` Master Items: ${analysis.masterItems}`)
|
||||
console.log(` Detail Panel Content: ${analysis.detailPanelTextLength} chars`)
|
||||
console.log(` Metrics Displayed: ${analysis.metrics}`)
|
||||
console.log(` Action Buttons: ${analysis.actions}`)
|
||||
|
||||
expect(analysis.masterItems).toBeGreaterThan(0)
|
||||
expect(analysis.actions).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
// 3. ShadowRunQueue 검증
|
||||
test('3. ShadowRunQueue - 작업 모니터링 검증', async ({ page }) => {
|
||||
await page.goto('/model-ops/shadow-run-jobs', { waitUntil: 'networkidle' })
|
||||
await page.screenshot({ path: 'D:\\Temp\\shadowrun-validation.png', fullPage: true })
|
||||
|
||||
const analysis = await page.evaluate(() => {
|
||||
const title = document.querySelector('h1')?.textContent || ''
|
||||
const statCards = document.querySelectorAll('[class*="stat"]').length
|
||||
const jobCards = document.querySelectorAll('[class*="card"], [class*="job"], article').length
|
||||
const progressBars = document.querySelectorAll('progress, [class*="progress"], [class*="bar"]').length
|
||||
const statusBadges = document.querySelectorAll('[class*="badge"], [class*="status"]').length
|
||||
const filters = document.querySelectorAll('input, select, [class*="filter"]').length
|
||||
|
||||
return {
|
||||
title: title.substring(0, 40),
|
||||
stats: statCards,
|
||||
jobs: jobCards,
|
||||
progress: progressBars,
|
||||
statusBadges,
|
||||
filters,
|
||||
}
|
||||
})
|
||||
|
||||
console.log('\n✅ ShadowRunQueue 분석:')
|
||||
console.log(` Title: "${analysis.title}"`)
|
||||
console.log(` Status Cards: ${analysis.stats}`)
|
||||
console.log(` Job Cards: ${analysis.jobs}`)
|
||||
console.log(` Progress Bars: ${analysis.progress}`)
|
||||
console.log(` Status Badges: ${analysis.statusBadges}`)
|
||||
console.log(` Filters: ${analysis.filters}`)
|
||||
|
||||
expect(analysis.jobs).toBeGreaterThan(0)
|
||||
expect(analysis.statusBadges).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
// 4. ApprovalQueue 검증
|
||||
test('4. ApprovalQueue - 승인 워크플로우 검증', async ({ page }) => {
|
||||
await page.goto('/governance/approvals', { waitUntil: 'networkidle' })
|
||||
await page.screenshot({ path: 'D:\\Temp\\approval-validation.png', fullPage: true })
|
||||
|
||||
const analysis = await page.evaluate(() => {
|
||||
const title = document.querySelector('h1')?.textContent || ''
|
||||
const statCards = document.querySelectorAll('[class*="stat"]').length
|
||||
const requestItems = document.querySelectorAll('[class*="request"], [class*="item"], article').length
|
||||
const approveButtons = Array.from(document.querySelectorAll('button, a')).filter(el =>
|
||||
el.textContent?.includes('승인') || el.textContent?.includes('Approve')
|
||||
).length
|
||||
const rejectButtons = Array.from(document.querySelectorAll('button, a')).filter(el =>
|
||||
el.textContent?.includes('거부') || el.textContent?.includes('Reject')
|
||||
).length
|
||||
const metrics = document.querySelectorAll('[class*="metric"], [class*="validation"]').length
|
||||
|
||||
return {
|
||||
title: title.substring(0, 40),
|
||||
stats: statCards,
|
||||
requests: requestItems,
|
||||
approveActions: approveButtons,
|
||||
rejectActions: rejectButtons,
|
||||
metrics,
|
||||
}
|
||||
})
|
||||
|
||||
console.log('\n✅ ApprovalQueue 분석:')
|
||||
console.log(` Title: "${analysis.title}"`)
|
||||
console.log(` Status Cards: ${analysis.stats}`)
|
||||
console.log(` Request Items: ${analysis.requests}`)
|
||||
console.log(` Approve Actions: ${analysis.approveActions}`)
|
||||
console.log(` Reject Actions: ${analysis.rejectActions}`)
|
||||
console.log(` Validation Metrics: ${analysis.metrics}`)
|
||||
|
||||
expect(analysis.requests).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
// 5. 종합 평가
|
||||
test('5. 종합 평가 - AGENTS.md v16.0 준수도', async ({ page }) => {
|
||||
const pages = [
|
||||
{ name: 'HomePage', url: '/' },
|
||||
{ name: 'ModelList', url: '/model-ops/models-master' },
|
||||
{ name: 'ShadowRunQueue', url: '/model-ops/shadow-run-jobs' },
|
||||
{ name: 'ApprovalQueue', url: '/governance/approvals' },
|
||||
]
|
||||
|
||||
const results = []
|
||||
|
||||
for (const pageInfo of pages) {
|
||||
await page.goto(pageInfo.url, { waitUntil: 'networkidle' })
|
||||
|
||||
const metrics = await page.evaluate(() => {
|
||||
// SOLID: 컴포넌트 책임 분리 확인
|
||||
const uniqueClasses = new Set(
|
||||
Array.from(document.querySelectorAll('*')).map(el => el.className)
|
||||
)
|
||||
|
||||
// 필요성 주도: 불필요한 요소 확인
|
||||
const allElements = document.querySelectorAll('*').length
|
||||
const visibleElements = Array.from(document.querySelectorAll('*')).filter(el => {
|
||||
const style = window.getComputedStyle(el)
|
||||
return style.display !== 'none' && style.visibility !== 'hidden'
|
||||
}).length
|
||||
|
||||
// 접근성: ARIA 속성
|
||||
const ariaElements = document.querySelectorAll('[aria-label], [role], [aria-describedby]').length
|
||||
|
||||
// 반응형: 미디어 쿼리 감지
|
||||
const hasResponsiveDesign = window.innerWidth < 1024
|
||||
? document.querySelectorAll('[class*="mobile"], [class*="responsive"]').length > 0
|
||||
: true
|
||||
|
||||
return {
|
||||
solidScore: Math.min(uniqueClasses.size / 20, 1) * 100,
|
||||
necessityScore: Math.min(visibleElements / allElements, 1) * 100,
|
||||
accessibilityScore: Math.min(ariaElements / 20, 1) * 100,
|
||||
responsiveReady: hasResponsiveDesign,
|
||||
}
|
||||
})
|
||||
|
||||
results.push({
|
||||
page: pageInfo.name,
|
||||
...metrics,
|
||||
})
|
||||
}
|
||||
|
||||
console.log('\n📊 종합 평가:')
|
||||
results.forEach(r => {
|
||||
console.log(`\n${r.page}:`)
|
||||
console.log(` SOLID: ${r.solidScore.toFixed(1)}%`)
|
||||
console.log(` 필요성 주도: ${r.necessityScore.toFixed(1)}%`)
|
||||
console.log(` 접근성: ${r.accessibilityScore.toFixed(1)}%`)
|
||||
console.log(` 반응형: ${r.responsiveReady ? '✅' : '⚠️'}`)
|
||||
})
|
||||
|
||||
expect(results.length).toBe(4)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user