Visual QA Testing Skill
Use this skill to perform visual quality assurance by capturing screenshots of UI states, comparing layouts, and verifying visual correctness across different scenarios.
When to Use
- •After UI implementation
- •After CSS/styling changes
- •Before deployments
- •For regression testing
- •When user reports visual bugs
- •During comprehensive QA validation
What This Skill Does
1. State-Based Screenshot Capture
Captures screenshots for ALL UI states:
- •Initial/Empty State - No data loaded
- •Loading State - Spinners, skeletons active
- •Data Loaded State - Content displayed
- •Error State - Error messages shown
- •Validation State - Form validation errors
- •Success State - Success messages
- •Hover State - Interactive element hover
- •Focus State - Keyboard focus indicators
- •Active/Selected State - Selected items
- •Disabled State - Disabled elements
2. Multi-Breakpoint Screenshot Matrix
For EACH state, capture at ALL breakpoints:
- •Mobile Portrait (375px)
- •Mobile Large (414px)
- •Tablet (768px)
- •Desktop (1280px)
- •Large Desktop (1920px)
Result: Comprehensive screenshot matrix (10 states × 5 breakpoints = 50 screenshots per page)
3. Component-Level Capture
Screenshot specific components:
- •Forms (empty, filled, with errors)
- •Modals/Dialogs
- •Navigation menus
- •Data tables/grids
- •Cards and lists
- •Buttons (all variants)
- •Input fields (all states)
4. User Workflow Capture
Screenshot each step of user journeys:
- •Login flow
- •Create/Edit/Delete workflows
- •Multi-step forms
- •Checkout processes
- •Onboarding flows
5. Visual Regression Detection
Compare screenshots to identify:
- •Layout shifts
- •Broken layouts
- •Missing styles
- •Color inconsistencies
- •Font rendering issues
- •Icon/image problems
Available Playwright MCP Tools
- •
mcp__playwright__browser_navigate- Navigate to pages - •
mcp__playwright__browser_resize- Change viewport CRITICAL - •
mcp__playwright__browser_take_screenshot- Capture screenshots PRIMARY TOOL - •
mcp__playwright__browser_click- Trigger interactions - •
mcp__playwright__browser_type- Fill inputs - •
mcp__playwright__browser_fill_form- Fill forms - •
mcp__playwright__browser_evaluate- Manipulate DOM/trigger states - •
mcp__playwright__browser_wait_for- Wait for state changes - •
mcp__playwright__browser_snapshot- Get accessibility tree
Testing Workflow
Step 1: Initialize Test Session
const testSession = {
feature: 'Schedule Calendar',
url: 'http://172.18.0.5:80/schedules',
breakpoints: [375, 414, 768, 1280, 1920],
states: ['empty', 'loading', 'loaded', 'error'],
screenshots: []
}
// Navigate
mcp__playwright__browser_navigate(testSession.url)
Step 2: Capture Empty State
// For each breakpoint
for (const width of testSession.breakpoints) {
// Resize
mcp__playwright__browser_resize(width, 800)
await mcp__playwright__browser_wait_for(timeout: 300)
// Screenshot
await mcp__playwright__browser_take_screenshot(
filename: `empty_state_${width}px.png`,
fullPage: true
)
testSession.screenshots.push({
state: 'empty',
breakpoint: width,
filename: `empty_state_${width}px.png`
})
}
Step 3: Capture Loading State
// Trigger loading state (if possible)
await mcp__playwright__browser_evaluate(`
// Show loading skeletons or spinners
document.querySelector('.loading-indicator')?.classList.add('visible')
// Or trigger a slow API call
`)
// Capture at all breakpoints
for (const width of testSession.breakpoints) {
mcp__playwright__browser_resize(width, 800)
await mcp__playwright__browser_wait_for(timeout: 300)
await mcp__playwright__browser_take_screenshot(
filename: `loading_state_${width}px.png`,
fullPage: true
)
}
Step 4: Capture Data Loaded State
// Wait for data to load
await mcp__playwright__browser_wait_for(
selector: '.schedule-calendar',
state: 'visible'
)
// Capture at all breakpoints
for (const width of testSession.breakpoints) {
mcp__playwright__browser_resize(width, 800)
await mcp__playwright__browser_wait_for(timeout: 300)
await mcp__playwright__browser_take_screenshot(
filename: `loaded_state_${width}px.png`,
fullPage: true
)
console.log(`✅ Captured loaded state at ${width}px`)
}
Step 5: Capture Error State
// Trigger error state
// Option 1: Fill form with invalid data
await mcp__playwright__browser_fill_form({
'employee': '', // Required field left empty
'date': 'invalid-date'
})
await mcp__playwright__browser_click(selector: 'button[type="submit"]')
// Wait for error to appear
await mcp__playwright__browser_wait_for(
selector: '.error-message',
state: 'visible'
)
// Capture at all breakpoints
for (const width of testSession.breakpoints) {
mcp__playwright__browser_resize(width, 800)
await mcp__playwright__browser_wait_for(timeout: 300)
await mcp__playwright__browser_take_screenshot(
filename: `error_state_${width}px.png`,
fullPage: true
)
console.log(`✅ Captured error state at ${width}px`)
}
Step 6: Capture Form Validation States
// Click into each input field to trigger validation
const formFields = ['employee', 'date', 'startTime', 'endTime']
for (const field of formFields) {
// Click field
await mcp__playwright__browser_click(selector: `[name="${field}"]`)
// Click outside to blur (trigger validation)
await mcp__playwright__browser_click(selector: 'body')
// Wait for validation message
await mcp__playwright__browser_wait_for(timeout: 200)
// Screenshot at mobile only (375px)
mcp__playwright__browser_resize(375, 667)
await mcp__playwright__browser_take_screenshot(
filename: `validation_${field}_375px.png`
)
// Screenshot at desktop (1280px)
mcp__playwright__browser_resize(1280, 720)
await mcp__playwright__browser_take_screenshot(
filename: `validation_${field}_1280px.png`
)
}
Step 7: Capture Interactive States
// Hover state (desktop only)
mcp__playwright__browser_resize(1280, 720)
// Find interactive elements
const interactiveElements = await mcp__playwright__browser_evaluate(`
Array.from(document.querySelectorAll('button, a, .clickable')).map(el => ({
selector: el.className || el.tagName,
text: el.textContent?.substring(0, 20)
}))
`)
for (const element of interactiveElements.slice(0, 5)) { // First 5 elements
// Hover
await mcp__playwright__browser_evaluate(`
const el = document.querySelector('${element.selector}')
el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }))
`)
await mcp__playwright__browser_take_screenshot(
filename: `hover_${element.selector}_1280px.png`
)
console.log(`✅ Captured hover state: ${element.text}`)
}
// Focus state (keyboard navigation)
for (const element of interactiveElements.slice(0, 5)) {
// Focus
await mcp__playwright__browser_evaluate(`
document.querySelector('${element.selector}')?.focus()
`)
await mcp__playwright__browser_take_screenshot(
filename: `focus_${element.selector}_1280px.png`
)
console.log(`✅ Captured focus state: ${element.text}`)
}
Step 8: Capture Modal/Dialog States
// Open modal
await mcp__playwright__browser_click(selector: 'button[data-testid="create-schedule"]')
// Wait for modal to appear
await mcp__playwright__browser_wait_for(
selector: '[role="dialog"]',
state: 'visible'
)
// Capture modal at all breakpoints
for (const width of [375, 768, 1280]) {
mcp__playwright__browser_resize(width, 800)
await mcp__playwright__browser_wait_for(timeout: 300)
await mcp__playwright__browser_take_screenshot(
filename: `modal_create_${width}px.png`,
fullPage: true
)
console.log(`✅ Captured modal at ${width}px`)
}
// Close modal
await mcp__playwright__browser_click(selector: '[aria-label="Close"]')
Step 9: Capture Scroll States
// Long page content - capture top, middle, bottom
const positions = ['top', 'middle', 'bottom']
for (const width of [375, 1280]) {
mcp__playwright__browser_resize(width, 800)
// Top
await mcp__playwright__browser_evaluate(`window.scrollTo(0, 0)`)
await mcp__playwright__browser_wait_for(timeout: 300)
await mcp__playwright__browser_take_screenshot(
filename: `scroll_top_${width}px.png`,
fullPage: false // Viewport only
)
// Middle
await mcp__playwright__browser_evaluate(`
window.scrollTo(0, document.documentElement.scrollHeight / 2)
`)
await mcp__playwright__browser_wait_for(timeout: 300)
await mcp__playwright__browser_take_screenshot(
filename: `scroll_middle_${width}px.png`,
fullPage: false
)
// Bottom
await mcp__playwright__browser_evaluate(`
window.scrollTo(0, document.documentElement.scrollHeight)
`)
await mcp__playwright__browser_wait_for(timeout: 300)
await mcp__playwright__browser_take_screenshot(
filename: `scroll_bottom_${width}px.png`,
fullPage: false
)
console.log(`✅ Captured scroll positions at ${width}px`)
}
Step 10: Generate Visual QA Report
const report = {
feature: testSession.feature,
test_date: new Date().toISOString(),
total_screenshots: testSession.screenshots.length,
breakpoints_tested: testSession.breakpoints,
states_captured: testSession.states,
screenshots: testSession.screenshots,
visual_issues: [],
status: 'PASS'
}
console.log('=== Visual QA Test Report ===')
console.log(`Feature: ${report.feature}`)
console.log(`Screenshots: ${report.total_screenshots}`)
console.log(`Breakpoints: ${report.breakpoints_tested.join(', ')}`)
console.log(`States: ${report.states_captured.join(', ')}`)
Docker IP Configuration
CRITICAL: Always use Docker container IP, not localhost.
Get Container IP:
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' {container}_ui
Use in Tests:
# Wrong: http://localhost:3003 # Correct: http://172.18.0.5:80
Screenshot Naming Convention
Use consistent naming for easy organization:
{state}_{component}_{breakpoint}px_{variant}.png
Examples:
- empty_state_calendar_375px.png
- loading_state_table_1280px.png
- error_state_form_768px_validation.png
- hover_state_button_1280px_primary.png
- focus_state_input_375px_email.png
Expected Inputs
- •URL to test (Docker IP)
- •Feature/component name
- •Specific states to capture
- •Breakpoints to test
- •User workflows to capture
Deliverables
- •Complete screenshot matrix (all states × all breakpoints)
- •Organized screenshot files with descriptive names
- •Visual QA report listing all captures
- •List of visual issues detected
- •Comparison notes (if baseline exists)
- •Recommendations for fixes
Validation Checklist
Layout
- • No overlapping elements
- • Proper spacing and alignment
- • Grid/flexbox layouts correct
- • Responsive breakpoints work
- • No cut-off content
Typography
- • Font sizes appropriate (min 16px mobile)
- • Line height readable
- • Text colors have sufficient contrast (WCAG AA)
- • No text overflow
- • Headings hierarchy correct
Colors & Theming
- • Colors match design system
- • Theme consistency throughout
- • Proper contrast ratios
- • Brand colors used correctly
- • Error/warning/success colors distinct
Interactive Elements
- • Hover states visible
- • Focus indicators present
- • Active states clear
- • Disabled states distinguishable
- • Click feedback appropriate
Forms
- • Input fields properly sized
- • Labels aligned correctly
- • Validation messages clear
- • Error states highlighted
- • Success feedback shown
Images & Icons
- • Images scale correctly
- • No broken images
- • Icons sized appropriately
- • Alt text present (check snapshot)
- • Aspect ratios maintained
Modals & Overlays
- • Centered on screen
- • Backdrop visible
- • Close button accessible
- • Scrollable if content long
- • Mobile-friendly size
Example: Complete Visual QA Flow
// Test configuration
const visualQATest = {
feature: 'Schedule Calendar Page',
url: 'http://172.18.0.5:80/schedules',
container: 'scheduling_ui',
breakpoints: [375, 768, 1280],
states: [
'empty',
'loading',
'loaded_with_data',
'create_modal',
'edit_modal',
'delete_confirmation',
'validation_errors',
'success_message',
'error_message'
]
}
// Step 1: Navigate
mcp__playwright__browser_navigate(visualQATest.url)
// Step 2: Empty state
console.log('Capturing empty state...')
for (const bp of visualQATest.breakpoints) {
mcp__playwright__browser_resize(bp, 800)
await mcp__playwright__browser_wait_for(timeout: 300)
await mcp__playwright__browser_take_screenshot(
filename: `empty_${bp}px.png`,
fullPage: true
)
}
// Step 3: Loading state (if available)
console.log('Capturing loading state...')
// Trigger loading...
for (const bp of visualQATest.breakpoints) {
mcp__playwright__browser_resize(bp, 800)
await mcp__playwright__browser_take_screenshot(
filename: `loading_${bp}px.png`,
fullPage: true
)
}
// Step 4: Loaded with data
console.log('Capturing loaded state...')
await mcp__playwright__browser_wait_for(selector: '.schedule-item', state: 'visible')
for (const bp of visualQATest.breakpoints) {
mcp__playwright__browser_resize(bp, 800)
await mcp__playwright__browser_take_screenshot(
filename: `loaded_${bp}px.png`,
fullPage: true
)
}
// Step 5: Create modal
console.log('Capturing create modal...')
await mcp__playwright__browser_click(selector: 'button:has-text("Create Schedule")')
await mcp__playwright__browser_wait_for(selector: '[role="dialog"]', state: 'visible')
for (const bp of visualQATest.breakpoints) {
mcp__playwright__browser_resize(bp, 800)
await mcp__playwright__browser_take_screenshot(
filename: `create_modal_${bp}px.png`,
fullPage: true
)
}
// Step 6: Validation errors
console.log('Capturing validation errors...')
await mcp__playwright__browser_click(selector: 'button[type="submit"]')
await mcp__playwright__browser_wait_for(selector: '.error-message', state: 'visible')
for (const bp of visualQATest.breakpoints) {
mcp__playwright__browser_resize(bp, 800)
await mcp__playwright__browser_take_screenshot(
filename: `validation_errors_${bp}px.png`,
fullPage: true
)
}
// Step 7: Success state (fill form correctly)
console.log('Capturing success state...')
await mcp__playwright__browser_fill_form({
'employee': 'John Doe',
'date': '2025-01-20',
'startTime': '09:00',
'endTime': '17:00'
})
await mcp__playwright__browser_click(selector: 'button[type="submit"]')
await mcp__playwright__browser_wait_for(selector: '.success-message', state: 'visible')
for (const bp of visualQATest.breakpoints) {
mcp__playwright__browser_resize(bp, 800)
await mcp__playwright__browser_take_screenshot(
filename: `success_message_${bp}px.png`,
fullPage: true
)
}
// Close browser
mcp__playwright__browser_close()
console.log('✅ Visual QA testing complete!')
console.log(`Total screenshots captured: ${visualQATest.states.length * visualQATest.breakpoints.length}`)
Integration with QA Agents
The QA Frontend Engineer agent should use this skill to:
- •Capture screenshots of ALL UI states
- •Test at multiple breakpoints (not just one)
- •Verify visual consistency
- •Check interactive element states
- •Document visual issues with screenshots
- •Provide evidence of testing
Common Visual Issues Detected
Layout Breaks
- •Overlapping elements on mobile
- •Text overflow outside containers
- •Fixed widths breaking responsive design
- •Grid/flexbox misconfiguration
Color Issues
- •Insufficient contrast (< 4.5:1)
- •Inconsistent brand colors
- •Wrong theme applied
- •Missing hover/focus colors
Typography Problems
- •Font too small on mobile (< 16px)
- •Line height too tight
- •Text not wrapping
- •Font weight inconsistent
Interactive State Issues
- •No hover indication
- •Missing focus outline
- •Disabled state not clear
- •Active state same as inactive
Responsive Issues
- •Desktop layout on mobile
- •Mobile menu not appearing
- •Touch targets too small
- •Horizontal scrolling
This skill provides comprehensive visual documentation and verification that catches visual bugs, layout issues, and responsive design problems through systematic screenshot capture and analysis.