How to Test Autofocus Behavior in UI Tests
Test when and where focus moves on load and after interactions. Cover keyboard behavior, dialogs, and platform-specific UI test approaches.
Test autofocus by checking the focused element at the moment it should change: after a page or screen loads, a dialog opens, or a user action changes the interface. Then verify that keyboard navigation still works as intended. On the web, focus may come from the HTML autofocus attribute or JavaScript such as element.focus(); mobile frameworks expose their own focus state and input APIs.
A good test covers both the target and the resulting keyboard experience. A control being focusable is not enough: focus should move at the right time, remain visible, follow a meaningful order, and avoid unexpected context changes.
1. Define the expected focus behavior
Before writing an assertion, specify the starting state, trigger, expected focused control, and expected next keyboard action. This keeps the test from confusing “the control can receive focus” with “focus moved correctly.”
| Scenario | What to assert |
|---|---|
| Initial load, no intentional autofocus | Focus was not unexpectedly moved to a control. The normal keyboard starting point and visible focus remain predictable. |
| Initial load with intentional autofocus | The intended control is active after rendering, and keyboard navigation proceeds sensibly from there. |
| Dialog opens | Focus moves to the designed target within the dialog. Closing the dialog returns navigation to the invoking control or the interface’s documented return point. |
| User-triggered state change | Focus moves only when the interaction design calls for it, and does not jump to an unrelated control. |
| Keyboard navigation | The focus indicator is visible, order is meaningful, and focusing a control does not unexpectedly submit a form, open a window, or change context. |
Autofocus is not automatically wrong. Moving focus into a newly opened dialog can be appropriate. The test should encode the intended behavior for that state, including what happens when the dialog closes.
2. Test autofocus in browser UI tests
For web UI tests, query the active element after the relevant event. Test the initial page state and any transition that is supposed to set focus. Use the testing library and browser driver already used by your project; the shared assertion is that the expected element is the document’s active element at the right time.
Runnable browser example with Playwright
The following example assumes an application page at /settings with a button named “Add email” that opens a dialog containing an input labeled “Email address.” Adapt the route and accessible names to the app. Install Playwright in the project and run the test with its test runner.
import { test, expect } from '@playwright/test';
test('dialog autofocuses its email field and returns focus on close', async ({ page }) => {
await page.goto('/settings');
const opener = page.getByRole('button', { name: 'Add email' });
await opener.focus();
await expect(opener).toBeFocused();
await page.keyboard.press('Enter');
const dialog = page.getByRole('dialog');
const email = dialog.getByRole('textbox', { name: 'Email address' });
await expect(dialog).toBeVisible();
await expect(email).toBeFocused();
// Confirm keyboard navigation inside the dialog is usable.
await page.keyboard.press('Tab');
await expect(dialog.getByRole('button', { name: 'Cancel' })).toBeFocused();
await page.keyboard.press('Escape');
await expect(dialog).not.toBeVisible();
await expect(opener).toBeFocused();
});
This test exercises a keyboard-triggered opening, checks focus after the transition, checks a Tab destination, and checks return focus after closing. If the dialog’s design uses a different initial target or close behavior, assert that documented behavior instead.
Check absence of unwanted autofocus
When no control should take focus on load, assert the intended baseline rather than asserting that every possible element is unfocused. For an ordinary document, the active element is commonly the body before a user interaction:
test('does not steal focus on initial load', async ({ page }) => {
await page.goto('/article');
await expect(page.locator('body')).toBeFocused();
});
Use this only when that baseline matches the page and browser behavior under test. If the app deliberately focuses a landmark or another element, assert that element instead. A page with an autofocus attribute and a page whose application code calls focus() can produce the same active-element result, so test behavior rather than relying only on inspecting implementation details.
Web edge cases
- Asynchronous rendering: if the target appears after data loading or an animation, wait for the element and then assert focus. Avoid a fixed delay unless a real timing contract requires one.
- Conditional dialogs: test both opening and dismissal paths, including validation or error states that may change the intended target.
- Multiple matching controls: scope queries to the dialog or component so the assertion checks the correct instance.
- Pointer-only setup: a mouse click can hide keyboard problems. Open the control using keyboard input in at least one test.
- Focus traps: for modal dialogs, verify Tab and Shift+Tab stay within the modal when that is the design, then verify closing restores the expected focus.
- Browser defaults: autofocus timing can interact with rendering and browser behavior. Assert after the relevant rendered state, and run the test in the supported browser engines when cross-browser behavior matters.
3. Test keyboard focus in Android Compose
Compose UI tests can assert focused and not-focused states and simulate key input, including Tab and Shift+Tab. The test environment starts in touch mode by default, so account for input mode when the behavior being tested depends on a hardware keyboard or keyboard navigation.
The exact test APIs and semantics depend on the component and Compose version. Structure the test around these steps:
- Render the screen in the state where focus should or should not move.
- Set or account for the appropriate input mode if keyboard behavior is part of the assertion.
- Assert the intended node is focused (or is not focused) after initial rendering or the triggering action.
- Send Tab or Shift+Tab and assert the next expected focus state.
Keep the focus assertion tied to an accessible, uniquely identified node where possible. If the UI changes focus after an event, perform that event in the test before inspecting focus. Consult the official Compose testing documentation for the current APIs for focus assertions and simulated input.
4. Test focus with Apple UI automation
XCTest with XCUIAutomation can interact with the app interface and inspect UI state. The exact autofocus assertion depends on which element is focusable and how the app exposes it through accessibility. Query that element, perform the action that should move focus, and assert the resulting state using the app’s accessibility setup.
- Launch the app into the relevant screen or state.
- Trigger the event, such as opening a dialog or activating a control.
- Query the intended focusable element through the accessibility interface and inspect the resulting UI state.
- Exercise keyboard navigation where the target platform and test setup support it; supplement automation with manual keyboard and assistive-technology checks when judgment is needed.
Do not assume a web document.activeElement assertion maps directly to Apple UI automation. Use the XCUIAutomation query and state that accurately represents the app’s focus and accessibility behavior. See Apple’s UI testing documentation.
5. Add manual keyboard and assistive-technology checks
Automation is useful for repeatable focus assertions, but some accessibility outcomes need human judgment. Run through the relevant screen with a keyboard and, where appropriate, a screen reader. Check that focus is visible, order makes sense, and opening or closing a component does not leave the user somewhere confusing.
- Navigate without a pointer and confirm the focus indicator is visible at each step.
- Check that Tab order follows the interface’s meaning rather than its visual position alone.
- Confirm focus itself does not trigger a new window, submit a form, or make an unexpected context change.
- For dialogs, inspect the opening target, navigation within the dialog, and the return point after dismissal.
web.dev advises testing focus for keyboard-navigable components. The WP Accessibility Knowledge Base also recommends checking that keyboard and visible focus is not automatically set on page load, whether through the autofocus attribute or JavaScript assignment. See web.dev’s focus guidance and the WP Accessibility Knowledge Base.
6. Troubleshoot common autofocus test failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Focus assertion fails immediately after navigation | The target is rendered asynchronously, or the assertion runs before the focus-changing effect. | Wait for the relevant rendered state or triggering event, then assert focus. Prefer condition-based waiting to arbitrary sleeps. |
| Dialog is visible but its field is not focused | The dialog did not implement its focus behavior, the wrong target was specified, or another focus call ran afterward. | Confirm the intended initial target and inspect the order of dialog and focus updates. Assert the behavior after the dialog is open. |
| Test passes with a mouse but fails for keyboard users | The test uses pointer interaction only or does not verify keyboard order. | Open the component with keyboard input and assert the focus destination and subsequent Tab behavior. |
| Compose key simulation does not move focus | The test remains in the default touch input mode or the UI has no eligible next focus target. | Configure or account for keyboard input mode and confirm the intended nodes are focusable. |
| Focus returns to the wrong place after closing a dialog | The invoking control was replaced, removed, or not recorded as the return target. | Define the intended return point for that state and assert it. If the opener no longer exists, specify a sensible fallback in the product behavior. |
| Focus assertion matches the wrong element | Queries are ambiguous or global when several similar controls exist. | Use accessible names and scope the query to the active dialog or component. |
| Automated test passes but focus is hard to see | The assertion checked focus state but not the rendered focus indicator. | Inspect keyboard navigation manually and verify the visible indicator in the relevant visual states. |
7. Reliability, performance, and maintenance
- Prefer event-based assertions: wait for the dialog or state transition, then check focus. Fixed sleeps add runtime and can still be flaky on slower machines.
- Keep tests isolated: each test should establish its own starting state and perform the interaction that causes focus to move.
- Test both positive and negative behavior: cover a deliberate focus move and a representative state where focus should stay in normal order.
- Choose coverage by risk: focus transitions in dialogs and keyboard-critical flows deserve explicit regression coverage; avoid duplicating identical assertions across every page.
- Include manual review: focus state can be automated, while whether the sequence is understandable and visible may need human assessment.
Browser screenshot comparison can help review a visible focus indicator, but an image does not prove which element owns keyboard focus. Pair visual review with an active-element or framework focus assertion.
Or skip the browser setup
If you need a rendered page image while documenting or reviewing a focus state, ScreenshotNeo can capture a URL with one request. ScreenshotNeo is a website screenshot API and MCP server for developers. Its clean-shot flow accepts consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. AI agents can use its MCP server tools to take screenshots, get page information, and capture PDFs.
Example using the documented API pattern (replace the URL and key):
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation. ScreenshotNeo has 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000. Visit ScreenshotNeo to learn about the service, or sign up for 1,000 free screenshots a month with no card.
FAQ
Should every page have an autofocus test?
No. Test the states where focus management is intentional or where an unexpected focus move would disrupt keyboard use. Include a representative check that ordinary pages do not steal focus when that is the expected behavior.
Is checking the active element enough?
It verifies which element owns focus at that point in a web test. Also test keyboard order and visible focus, and manually assess assistive-technology behavior where needed.
Should a dialog always focus its first input?
Not necessarily. Choose the initial target that fits the dialog’s purpose and content, then test that choice and the focus return path.
Can a screenshot prove autofocus works?
No. A screenshot may show a focus indicator, but it cannot reliably establish the active element or keyboard sequence. Use UI assertions for focus state.


