Always use TAB key for keyboard navigation instead of javascript focus()
This commit is contained in:
256
daemon/daemon.go
256
daemon/daemon.go
@@ -2062,7 +2062,6 @@ func (d *Daemon) handleCommand(w http.ResponseWriter, r *http.Request) {
|
||||
case "test-keyboard":
|
||||
tabID := cmd.Params["tab"]
|
||||
timeoutStr := cmd.Params["timeout"]
|
||||
useRealKeys := cmd.Params["use_real_keys"] == "true" // Default to false for backward compatibility
|
||||
|
||||
// Parse timeout (default to 15 seconds for comprehensive testing)
|
||||
timeout := 15
|
||||
@@ -2072,15 +2071,8 @@ func (d *Daemon) handleCommand(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
var result *KeyboardTestResult
|
||||
var err error
|
||||
|
||||
// Use real keyboard simulation if requested, otherwise use legacy method
|
||||
if useRealKeys {
|
||||
result, err = d.testKeyboardNavigationWithRealKeys(tabID, timeout)
|
||||
} else {
|
||||
result, err = d.testKeyboardNavigation(tabID, timeout)
|
||||
}
|
||||
// Always use real keyboard simulation for accurate :focus-visible detection
|
||||
result, err := d.testKeyboardNavigationWithRealKeys(tabID, timeout)
|
||||
|
||||
if err != nil {
|
||||
response = Response{Success: false, Error: err.Error()}
|
||||
@@ -2210,7 +2202,6 @@ func (d *Daemon) handleCommand(w http.ResponseWriter, r *http.Request) {
|
||||
checkFocusIndicators := cmd.Params["check_focus_indicators"] == "true"
|
||||
checkTabOrder := cmd.Params["check_tab_order"] == "true"
|
||||
checkKeyboardTraps := cmd.Params["check_keyboard_traps"] == "true"
|
||||
useRealKeys := cmd.Params["use_real_keys"] != "false" // Default to true for better accuracy
|
||||
timeoutStr := cmd.Params["timeout"]
|
||||
|
||||
// Parse timeout (default to 15 seconds)
|
||||
@@ -2221,7 +2212,8 @@ func (d *Daemon) handleCommand(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
result, err := d.getKeyboardAudit(tabID, checkFocusIndicators, checkTabOrder, checkKeyboardTraps, useRealKeys, timeout)
|
||||
// Always use real keyboard simulation for accurate :focus-visible detection
|
||||
result, err := d.getKeyboardAudit(tabID, checkFocusIndicators, checkTabOrder, checkKeyboardTraps, true, timeout)
|
||||
if err != nil {
|
||||
response = Response{Success: false, Error: err.Error()}
|
||||
} else {
|
||||
@@ -11484,232 +11476,6 @@ func (d *Daemon) testKeyboardNavigationWithRealKeys(tabID string, timeout int) (
|
||||
return &initialResult, nil
|
||||
}
|
||||
|
||||
// testKeyboardNavigation tests keyboard navigation and accessibility
|
||||
// This is the legacy version using programmatic .focus()
|
||||
func (d *Daemon) testKeyboardNavigation(tabID string, timeout int) (*KeyboardTestResult, error) {
|
||||
d.debugLog("Testing keyboard navigation for tab: %s", tabID)
|
||||
|
||||
page, err := d.getTab(tabID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get page: %v", err)
|
||||
}
|
||||
|
||||
// JavaScript code to test keyboard navigation
|
||||
jsCode := `() => {
|
||||
const results = {
|
||||
total_interactive: 0,
|
||||
focusable: 0,
|
||||
not_focusable: 0,
|
||||
no_focus_indicator: 0,
|
||||
keyboard_traps: 0,
|
||||
tab_order: [],
|
||||
issues: []
|
||||
};
|
||||
|
||||
// Helper to check if element is visible
|
||||
function isVisible(element) {
|
||||
const style = window.getComputedStyle(element);
|
||||
return style.display !== 'none' &&
|
||||
style.visibility !== 'hidden' &&
|
||||
style.opacity !== '0' &&
|
||||
element.offsetWidth > 0 &&
|
||||
element.offsetHeight > 0;
|
||||
}
|
||||
|
||||
// Helper to get element selector
|
||||
function getSelector(element) {
|
||||
if (element.id) return '#' + element.id;
|
||||
if (element.className && typeof element.className === 'string') {
|
||||
const classes = element.className.trim().split(/\s+/).slice(0, 2).join('.');
|
||||
if (classes) return element.tagName.toLowerCase() + '.' + classes;
|
||||
}
|
||||
return element.tagName.toLowerCase();
|
||||
}
|
||||
|
||||
// Helper to check focus indicator
|
||||
function hasFocusIndicator(element) {
|
||||
element.focus();
|
||||
const focusedStyle = window.getComputedStyle(element);
|
||||
element.blur();
|
||||
const blurredStyle = window.getComputedStyle(element);
|
||||
|
||||
// Check for outline changes
|
||||
if (focusedStyle.outline !== blurredStyle.outline &&
|
||||
focusedStyle.outline !== 'none' &&
|
||||
focusedStyle.outlineWidth !== '0px') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for border changes
|
||||
if (focusedStyle.border !== blurredStyle.border) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for background changes
|
||||
if (focusedStyle.backgroundColor !== blurredStyle.backgroundColor) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for box-shadow changes
|
||||
if (focusedStyle.boxShadow !== blurredStyle.boxShadow &&
|
||||
focusedStyle.boxShadow !== 'none') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get all interactive elements
|
||||
const interactiveSelectors = [
|
||||
'a[href]',
|
||||
'button',
|
||||
'input:not([type="hidden"])',
|
||||
'select',
|
||||
'textarea',
|
||||
'[tabindex]:not([tabindex="-1"])',
|
||||
'[role="button"]',
|
||||
'[role="link"]',
|
||||
'[role="checkbox"]',
|
||||
'[role="radio"]',
|
||||
'[role="tab"]',
|
||||
'[role="menuitem"]'
|
||||
];
|
||||
|
||||
const allInteractive = document.querySelectorAll(interactiveSelectors.join(','));
|
||||
results.total_interactive = allInteractive.length;
|
||||
|
||||
// Test each interactive element
|
||||
allInteractive.forEach((element, index) => {
|
||||
const visible = isVisible(element);
|
||||
const selector = getSelector(element);
|
||||
const tagName = element.tagName.toLowerCase();
|
||||
const role = element.getAttribute('role') || '';
|
||||
const text = element.textContent.trim().substring(0, 50);
|
||||
const tabIndex = element.tabIndex;
|
||||
|
||||
// Check if element is focusable
|
||||
let isFocusable = false;
|
||||
try {
|
||||
element.focus();
|
||||
isFocusable = document.activeElement === element;
|
||||
element.blur();
|
||||
} catch (e) {
|
||||
// Element not focusable
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
if (isFocusable) {
|
||||
results.focusable++;
|
||||
|
||||
// Check for focus indicator
|
||||
const hasFocus = hasFocusIndicator(element);
|
||||
if (!hasFocus) {
|
||||
results.no_focus_indicator++;
|
||||
results.issues.push({
|
||||
type: 'no_focus_indicator',
|
||||
severity: 'high',
|
||||
element: selector,
|
||||
description: 'Interactive element lacks visible focus indicator'
|
||||
});
|
||||
}
|
||||
|
||||
// Add to tab order
|
||||
results.tab_order.push({
|
||||
index: results.tab_order.length,
|
||||
selector: selector,
|
||||
tag_name: tagName,
|
||||
role: role,
|
||||
text: text,
|
||||
tab_index: tabIndex,
|
||||
has_focus_style: hasFocus,
|
||||
is_visible: visible
|
||||
});
|
||||
} else {
|
||||
results.not_focusable++;
|
||||
results.issues.push({
|
||||
type: 'not_focusable',
|
||||
severity: 'high',
|
||||
element: selector,
|
||||
description: 'Interactive element is not keyboard focusable'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Test for keyboard traps by simulating tab navigation
|
||||
const focusableElements = Array.from(allInteractive).filter(el => {
|
||||
try {
|
||||
el.focus();
|
||||
const focused = document.activeElement === el;
|
||||
el.blur();
|
||||
return focused && isVisible(el);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Simple keyboard trap detection
|
||||
if (focusableElements.length > 0) {
|
||||
const firstElement = focusableElements[0];
|
||||
const lastElement = focusableElements[focusableElements.length - 1];
|
||||
|
||||
// Focus first element
|
||||
firstElement.focus();
|
||||
|
||||
// Simulate Shift+Tab from first element
|
||||
// If focus doesn't move to last element or body, might be a trap
|
||||
// Note: This is a simplified check, real trap detection is complex
|
||||
}
|
||||
|
||||
return JSON.stringify(results);
|
||||
}`
|
||||
|
||||
var jsResult *proto.RuntimeRemoteObject
|
||||
if timeout > 0 {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
done := make(chan struct {
|
||||
result *proto.RuntimeRemoteObject
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
go func() {
|
||||
result, err := page.Eval(jsCode)
|
||||
done <- struct {
|
||||
result *proto.RuntimeRemoteObject
|
||||
err error
|
||||
}{result, err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case res := <-done:
|
||||
if res.err != nil {
|
||||
return nil, fmt.Errorf("failed to test keyboard navigation: %w", res.err)
|
||||
}
|
||||
jsResult = res.result
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("keyboard navigation test timed out after %d seconds", timeout)
|
||||
}
|
||||
} else {
|
||||
jsResult, err = page.Eval(jsCode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to test keyboard navigation: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the results
|
||||
resultsJSON := jsResult.Value.Str()
|
||||
var result KeyboardTestResult
|
||||
err = json.Unmarshal([]byte(resultsJSON), &result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse keyboard test results: %w", err)
|
||||
}
|
||||
|
||||
d.debugLog("Successfully tested keyboard navigation for tab: %s (found %d issues)", tabID, len(result.Issues))
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// ZoomTestResult represents the result of zoom level testing
|
||||
type ZoomTestResult struct {
|
||||
ZoomLevels []ZoomLevelTest `json:"zoom_levels"`
|
||||
@@ -12678,18 +12444,10 @@ type KeyboardAuditResult struct {
|
||||
|
||||
// getKeyboardAudit performs a keyboard navigation assessment
|
||||
func (d *Daemon) getKeyboardAudit(tabID string, checkFocusIndicators, checkTabOrder, checkKeyboardTraps, useRealKeys bool, timeout int) (*KeyboardAuditResult, error) {
|
||||
d.debugLog("Getting keyboard audit for tab: %s (useRealKeys: %v)", tabID, useRealKeys)
|
||||
|
||||
// Run keyboard navigation test with real keys or legacy method
|
||||
var keyboardResult *KeyboardTestResult
|
||||
var err error
|
||||
|
||||
if useRealKeys {
|
||||
keyboardResult, err = d.testKeyboardNavigationWithRealKeys(tabID, timeout)
|
||||
} else {
|
||||
keyboardResult, err = d.testKeyboardNavigation(tabID, timeout)
|
||||
}
|
||||
d.debugLog("Getting keyboard audit for tab: %s", tabID)
|
||||
|
||||
// Always use real keyboard simulation for accurate :focus-visible detection
|
||||
keyboardResult, err := d.testKeyboardNavigationWithRealKeys(tabID, timeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to test keyboard navigation: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user