Add observability for LLM topic context inclusion (#1038)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com> Co-authored-by: Thomas Nordquist <thomasnordquist@users.noreply.github.com>
This commit is contained in:
@@ -25,7 +25,7 @@ function reportIssue(issue: UXIssue) {
|
||||
|
||||
async function checkAccessibility(page: Page, context: string) {
|
||||
console.log(`\n=== Checking accessibility: ${context} ===\n`)
|
||||
|
||||
|
||||
// Check for proper ARIA labels
|
||||
const buttonsWithoutAriaLabel = await page.locator('button:not([aria-label])').count()
|
||||
if (buttonsWithoutAriaLabel > 0) {
|
||||
@@ -34,7 +34,7 @@ async function checkAccessibility(page: Page, context: string) {
|
||||
severity: 'medium',
|
||||
description: `${buttonsWithoutAriaLabel} buttons without aria-label found`,
|
||||
location: context,
|
||||
recommendation: 'Add aria-label attributes to all interactive buttons for screen reader support'
|
||||
recommendation: 'Add aria-label attributes to all interactive buttons for screen reader support',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ async function checkAccessibility(page: Page, context: string) {
|
||||
severity: 'medium',
|
||||
description: `${inputsWithoutLabel} input fields without proper labels`,
|
||||
location: context,
|
||||
recommendation: 'Add aria-label or associated label elements to all inputs'
|
||||
recommendation: 'Add aria-label or associated label elements to all inputs',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -57,103 +57,107 @@ async function checkAccessibility(page: Page, context: string) {
|
||||
|
||||
async function checkTouchTargets(page: Page, context: string) {
|
||||
console.log(`\n=== Checking touch target sizes: ${context} ===\n`)
|
||||
|
||||
|
||||
// Check button sizes (should be at least 44x44px for touch)
|
||||
const buttons = await page.locator('button, a, [role="button"]').all()
|
||||
let smallButtons = 0
|
||||
|
||||
|
||||
for (const button of buttons) {
|
||||
const box = await button.boundingBox()
|
||||
if (box && (box.width < 44 || box.height < 44)) {
|
||||
smallButtons++
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (smallButtons > 0) {
|
||||
reportIssue({
|
||||
category: 'Touch Usability',
|
||||
severity: 'high',
|
||||
description: `${smallButtons} interactive elements smaller than 44x44px`,
|
||||
location: context,
|
||||
recommendation: 'Increase touch target sizes to at least 44x44px for better mobile usability'
|
||||
recommendation: 'Increase touch target sizes to at least 44x44px for better mobile usability',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function checkFormUsability(page: Page, context: string) {
|
||||
console.log(`\n=== Checking form usability: ${context} ===\n`)
|
||||
|
||||
|
||||
// Check for password visibility toggle
|
||||
const passwordInputs = await page.locator('input[type="password"]').all()
|
||||
const visibilityToggles = await page.locator('[aria-label*="password" i], [title*="password" i]').count()
|
||||
|
||||
|
||||
if (passwordInputs.length > 0 && visibilityToggles === 0) {
|
||||
reportIssue({
|
||||
category: 'Form Usability',
|
||||
severity: 'medium',
|
||||
description: 'Password fields lack visibility toggle',
|
||||
location: context,
|
||||
recommendation: 'Add a toggle button to show/hide password for better UX'
|
||||
recommendation: 'Add a toggle button to show/hide password for better UX',
|
||||
})
|
||||
}
|
||||
|
||||
// Check for required field indicators
|
||||
const requiredInputs = await page.locator('input[required]').count()
|
||||
const requiredIndicators = await page.locator('input[required] + label:has-text("*"), label:has-text("*") + input[required]').count()
|
||||
|
||||
const requiredIndicators = await page
|
||||
.locator('input[required] + label:has-text("*"), label:has-text("*") + input[required]')
|
||||
.count()
|
||||
|
||||
if (requiredInputs > 0 && requiredIndicators === 0) {
|
||||
reportIssue({
|
||||
category: 'Form Usability',
|
||||
severity: 'low',
|
||||
description: 'Required fields may not be visually indicated',
|
||||
location: context,
|
||||
recommendation: 'Add visual indicators (e.g., asterisk) for required fields'
|
||||
recommendation: 'Add visual indicators (e.g., asterisk) for required fields',
|
||||
})
|
||||
}
|
||||
|
||||
// Check for autocomplete attributes
|
||||
const emailInputs = await page.locator('input[type="email"], input[name*="email" i]').count()
|
||||
const emailAutocomplete = await page.locator('input[type="email"][autocomplete], input[name*="email" i][autocomplete]').count()
|
||||
|
||||
const emailAutocomplete = await page
|
||||
.locator('input[type="email"][autocomplete], input[name*="email" i][autocomplete]')
|
||||
.count()
|
||||
|
||||
if (emailInputs > 0 && emailAutocomplete === 0) {
|
||||
reportIssue({
|
||||
category: 'Form Usability',
|
||||
severity: 'low',
|
||||
description: 'Email inputs lack autocomplete attributes',
|
||||
location: context,
|
||||
recommendation: 'Add autocomplete="email" to email inputs for better UX'
|
||||
recommendation: 'Add autocomplete="email" to email inputs for better UX',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function checkLoadingStates(page: Page, context: string) {
|
||||
console.log(`\n=== Checking loading states: ${context} ===\n`)
|
||||
|
||||
|
||||
// Check for loading indicators
|
||||
const loadingIndicators = await page.locator('[role="progressbar"], .loading, [aria-busy="true"]').count()
|
||||
console.log(` Found ${loadingIndicators} loading indicators`)
|
||||
|
||||
|
||||
// Check if buttons show loading state
|
||||
const buttons = await page.locator('button').all()
|
||||
let buttonsWithDisabledState = 0
|
||||
|
||||
|
||||
for (const button of buttons) {
|
||||
const isDisabled = await button.isDisabled()
|
||||
if (isDisabled) {
|
||||
buttonsWithDisabledState++
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.log(` ${buttonsWithDisabledState} buttons in disabled state`)
|
||||
}
|
||||
|
||||
async function checkErrorMessages(page: Page, context: string) {
|
||||
console.log(`\n=== Checking error message visibility: ${context} ===\n`)
|
||||
|
||||
|
||||
// Check if error messages have proper ARIA roles
|
||||
const errorElements = await page.locator('[role="alert"], .error, [aria-live="assertive"]').count()
|
||||
console.log(` Found ${errorElements} elements with error/alert roles`)
|
||||
|
||||
|
||||
// Check for error text visibility
|
||||
const errorTexts = await page.locator('text=/error/i, text=/invalid/i, text=/failed/i').all()
|
||||
for (const errorText of errorTexts) {
|
||||
@@ -164,7 +168,7 @@ async function checkErrorMessages(page: Page, context: string) {
|
||||
severity: 'medium',
|
||||
description: 'Error message exists but may not be visible',
|
||||
location: context,
|
||||
recommendation: 'Ensure error messages are visible and have proper ARIA roles'
|
||||
recommendation: 'Ensure error messages are visible and have proper ARIA roles',
|
||||
})
|
||||
break
|
||||
}
|
||||
@@ -173,11 +177,13 @@ async function checkErrorMessages(page: Page, context: string) {
|
||||
|
||||
async function checkKeyboardNavigation(page: Page, context: string) {
|
||||
console.log(`\n=== Checking keyboard navigation: ${context} ===\n`)
|
||||
|
||||
|
||||
// Check for focusable elements
|
||||
const focusableElements = await page.locator('button, a, input, select, textarea, [tabindex]:not([tabindex="-1"])').count()
|
||||
const focusableElements = await page
|
||||
.locator('button, a, input, select, textarea, [tabindex]:not([tabindex="-1"])')
|
||||
.count()
|
||||
console.log(` Found ${focusableElements} focusable elements`)
|
||||
|
||||
|
||||
// Check for skip links
|
||||
const skipLinks = await page.locator('a[href*="#"]:has-text(/skip/i)').count()
|
||||
if (skipLinks === 0) {
|
||||
@@ -186,29 +192,29 @@ async function checkKeyboardNavigation(page: Page, context: string) {
|
||||
severity: 'low',
|
||||
description: 'No skip navigation links found',
|
||||
location: context,
|
||||
recommendation: 'Add skip to content links for keyboard users'
|
||||
recommendation: 'Add skip to content links for keyboard users',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function checkResponsiveDesign(page: Page, context: string) {
|
||||
console.log(`\n=== Checking responsive design: ${context} ===\n`)
|
||||
|
||||
|
||||
// Check for horizontal scroll
|
||||
const hasHorizontalScroll = await page.evaluate(() => {
|
||||
return document.documentElement.scrollWidth > document.documentElement.clientWidth
|
||||
})
|
||||
|
||||
const hasHorizontalScroll = await page.evaluate(
|
||||
() => document.documentElement.scrollWidth > document.documentElement.clientWidth
|
||||
)
|
||||
|
||||
if (hasHorizontalScroll) {
|
||||
reportIssue({
|
||||
category: 'Responsive Design',
|
||||
severity: 'high',
|
||||
description: 'Page has horizontal scrolling',
|
||||
location: context,
|
||||
recommendation: 'Ensure content fits within viewport width without horizontal scrolling'
|
||||
recommendation: 'Ensure content fits within viewport width without horizontal scrolling',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Check for viewport meta tag
|
||||
const hasViewportMeta = await page.locator('meta[name="viewport"]').count()
|
||||
if (hasViewportMeta === 0) {
|
||||
@@ -217,7 +223,7 @@ async function checkResponsiveDesign(page: Page, context: string) {
|
||||
severity: 'critical',
|
||||
description: 'Missing viewport meta tag',
|
||||
location: context,
|
||||
recommendation: 'Add <meta name="viewport" content="width=device-width, initial-scale=1">'
|
||||
recommendation: 'Add <meta name="viewport" content="width=device-width, initial-scale=1">',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -226,26 +232,26 @@ async function exploreDesktopUX(browser: Browser) {
|
||||
console.log('\n\n========================================')
|
||||
console.log('DESKTOP UX EXPLORATION')
|
||||
console.log('========================================\n')
|
||||
|
||||
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1920, height: 1080 }
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
})
|
||||
const page = await context.newPage()
|
||||
|
||||
|
||||
try {
|
||||
// Start server and wait for it
|
||||
await page.goto('http://localhost:3000', { waitUntil: 'networkidle' })
|
||||
await page.waitForTimeout(2000)
|
||||
|
||||
|
||||
// Check initial page load
|
||||
await checkAccessibility(page, 'Desktop: Initial Load')
|
||||
await checkResponsiveDesign(page, 'Desktop: Initial Load')
|
||||
await checkKeyboardNavigation(page, 'Desktop: Initial Load')
|
||||
|
||||
|
||||
// Take screenshot
|
||||
await page.screenshot({ path: '/tmp/desktop-initial.png', fullPage: true })
|
||||
console.log('Screenshot saved: /tmp/desktop-initial.png')
|
||||
|
||||
|
||||
// Check login dialog if present
|
||||
const loginDialog = page.locator('[role="dialog"]').first()
|
||||
if (await loginDialog.isVisible()) {
|
||||
@@ -255,7 +261,7 @@ async function exploreDesktopUX(browser: Browser) {
|
||||
await page.screenshot({ path: '/tmp/desktop-login.png' })
|
||||
console.log('Screenshot saved: /tmp/desktop-login.png')
|
||||
}
|
||||
|
||||
|
||||
// Check connection dialog
|
||||
const connectionDialog = page.locator('text=MQTT Connection').first()
|
||||
if (await connectionDialog.isVisible()) {
|
||||
@@ -265,7 +271,6 @@ async function exploreDesktopUX(browser: Browser) {
|
||||
await page.screenshot({ path: '/tmp/desktop-connection.png' })
|
||||
console.log('Screenshot saved: /tmp/desktop-connection.png')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during desktop exploration:', error)
|
||||
} finally {
|
||||
@@ -277,26 +282,26 @@ async function exploreMobileUX(browser: Browser) {
|
||||
console.log('\n\n========================================')
|
||||
console.log('MOBILE UX EXPLORATION')
|
||||
console.log('========================================\n')
|
||||
|
||||
|
||||
const context = await browser.newContext({
|
||||
...PIXEL_6,
|
||||
viewport: { width: 412, height: 914 }
|
||||
viewport: { width: 412, height: 914 },
|
||||
})
|
||||
const page = await context.newPage()
|
||||
|
||||
|
||||
try {
|
||||
await page.goto('http://localhost:3000', { waitUntil: 'networkidle' })
|
||||
await page.waitForTimeout(2000)
|
||||
|
||||
|
||||
// Check mobile-specific issues
|
||||
await checkAccessibility(page, 'Mobile: Initial Load')
|
||||
await checkResponsiveDesign(page, 'Mobile: Initial Load')
|
||||
await checkTouchTargets(page, 'Mobile: Initial Load')
|
||||
|
||||
|
||||
// Take screenshot
|
||||
await page.screenshot({ path: '/tmp/mobile-initial.png', fullPage: true })
|
||||
console.log('Screenshot saved: /tmp/mobile-initial.png')
|
||||
|
||||
|
||||
// Check login dialog on mobile
|
||||
const loginDialog = page.locator('[role="dialog"]').first()
|
||||
if (await loginDialog.isVisible()) {
|
||||
@@ -306,14 +311,14 @@ async function exploreMobileUX(browser: Browser) {
|
||||
await page.screenshot({ path: '/tmp/mobile-login.png' })
|
||||
console.log('Screenshot saved: /tmp/mobile-login.png')
|
||||
}
|
||||
|
||||
|
||||
// Check connection dialog on mobile
|
||||
const connectionDialog = page.locator('text=MQTT Connection').first()
|
||||
if (await connectionDialog.isVisible()) {
|
||||
console.log('\nConnection dialog on mobile detected')
|
||||
await checkFormUsability(page, 'Mobile: Connection Dialog')
|
||||
await checkTouchTargets(page, 'Mobile: Connection Dialog')
|
||||
|
||||
|
||||
// Check if dialog fits on screen
|
||||
const dialogBox = await connectionDialog.boundingBox()
|
||||
if (dialogBox && dialogBox.width > 412) {
|
||||
@@ -322,14 +327,13 @@ async function exploreMobileUX(browser: Browser) {
|
||||
severity: 'high',
|
||||
description: `Dialog width (${dialogBox.width}px) exceeds mobile viewport (412px)`,
|
||||
location: 'Mobile: Connection Dialog',
|
||||
recommendation: 'Make dialog responsive to fit within mobile viewports'
|
||||
recommendation: 'Make dialog responsive to fit within mobile viewports',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
await page.screenshot({ path: '/tmp/mobile-connection.png' })
|
||||
console.log('Screenshot saved: /tmp/mobile-connection.png')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during mobile exploration:', error)
|
||||
} finally {
|
||||
@@ -340,33 +344,33 @@ async function exploreMobileUX(browser: Browser) {
|
||||
async function main() {
|
||||
console.log('Starting UX exploration of MQTT Explorer...\n')
|
||||
console.log('This script will identify UX flaws in the application.\n')
|
||||
|
||||
const browser = await chromium.launch({
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
})
|
||||
|
||||
|
||||
try {
|
||||
await exploreDesktopUX(browser)
|
||||
await exploreMobileUX(browser)
|
||||
|
||||
|
||||
// Summary report
|
||||
console.log('\n\n========================================')
|
||||
console.log('UX ISSUES SUMMARY')
|
||||
console.log('========================================\n')
|
||||
|
||||
|
||||
const critical = uxIssues.filter(i => i.severity === 'critical')
|
||||
const high = uxIssues.filter(i => i.severity === 'high')
|
||||
const medium = uxIssues.filter(i => i.severity === 'medium')
|
||||
const low = uxIssues.filter(i => i.severity === 'low')
|
||||
|
||||
|
||||
console.log(`Total Issues Found: ${uxIssues.length}`)
|
||||
console.log(` Critical: ${critical.length}`)
|
||||
console.log(` High: ${high.length}`)
|
||||
console.log(` Medium: ${medium.length}`)
|
||||
console.log(` Low: ${low.length}`)
|
||||
console.log('')
|
||||
|
||||
|
||||
if (critical.length > 0) {
|
||||
console.log('CRITICAL ISSUES:')
|
||||
critical.forEach((issue, i) => {
|
||||
@@ -374,7 +378,7 @@ async function main() {
|
||||
})
|
||||
console.log('')
|
||||
}
|
||||
|
||||
|
||||
if (high.length > 0) {
|
||||
console.log('HIGH PRIORITY ISSUES:')
|
||||
high.forEach((issue, i) => {
|
||||
@@ -382,7 +386,7 @@ async function main() {
|
||||
})
|
||||
console.log('')
|
||||
}
|
||||
|
||||
|
||||
// Save detailed report
|
||||
const report = {
|
||||
timestamp: new Date().toISOString(),
|
||||
@@ -391,15 +395,14 @@ async function main() {
|
||||
critical: critical.length,
|
||||
high: high.length,
|
||||
medium: medium.length,
|
||||
low: low.length
|
||||
low: low.length,
|
||||
},
|
||||
issues: uxIssues
|
||||
issues: uxIssues,
|
||||
}
|
||||
|
||||
|
||||
const fs = require('fs')
|
||||
fs.writeFileSync('/tmp/ux-report.json', JSON.stringify(report, null, 2))
|
||||
console.log('Detailed report saved to: /tmp/ux-report.json\n')
|
||||
|
||||
} finally {
|
||||
await browser.close()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user