Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

browser.keys() on Firefox usually works once the command, target and WebDriver session line up. The reliable fix is to use WebdriverIO’s current Key constants, make sure the intended element is the active and keyboard-interactable target, and only then investigate Firefox, geckodriver and WebdriverIO versions. There is no single Firefox-specific workaround that can be prescribed without the failing code, error text and exact versions.

Start by identifying what should receive the keystrokes

WebdriverIO has two different ways to send keyboard input:

  • browser.keys() sends keys to the element that currently owns focus in the active browser window. It is appropriate for Enter, modifier chords, tabbing and other keyboard-navigation actions.
  • An element’s text-entry methods target a specific form control. WebdriverIO recommends the higher-level setValue() and addValue() methods for ordinary text entry.

Firefox’s geckodriver checks whether an element is focusable when it receives keys. A message such as “element not interactable” can therefore describe the page state, not a broken key mapping. Confirm the target, focus and frame before changing driver settings. See the WebdriverIO key constants and browser.keys API, the WebDriver element-send-keys documentation and Mozilla’s Firefox capabilities guidance.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use the current WebdriverIO key constants

Import Key from webdriverio for non-printable keys. This avoids relying on escape characters or browser-specific assumptions.

import { Key } from 'webdriverio'

await browser.keys(Key.Enter)
await browser.keys([Key.Ctrl, 'a'])

Key.Ctrl is documented as cross-platform: it represents Command on macOS and Control on Windows and Linux. The same API supports navigation sequences:

import { Key } from 'webdriverio'

await browser.keys([Key.ArrowDown, Key.ArrowDown, Key.Enter])

For a printable string, pass the string directly:

await browser.keys('hello')
await browser.keys('[email protected]')

These calls act on the currently focused element. If focus is on the page body, a shortcut may be handled by the document; if focus is on a text field, the characters should be inserted there; and if focus has been lost, the command can appear to do nothing.

Choose the right command for text fields

Replace the existing value with setValue()

When the test knows which input should contain a value, target it directly and replace its contents:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const email = await $('#email')
await email.waitForDisplayed()
await email.setValue('[email protected]')

This avoids depending on whichever element happened to retain focus after the previous action.

Append text with addValue()

Use addValue() when the existing value must remain and new text should be appended:

const search = await $('input[name="q"]')
await search.waitForDisplayed()
await search.addValue(' firefox')

Use browser.keys() for a focused-field action

After explicitly focusing a known control, browser-level keys are useful for submission and navigation:

const search = await $('input[name="q"]')
await search.waitForDisplayed()
await search.click()
await browser.keys(Key.Enter)

If the purpose is simply to enter text, prefer setValue() or addValue(). If the purpose is a keyboard gesture—such as selecting all, moving between controls or activating a focused button—use browser.keys().

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Verify Firefox focus and interactability before changing configuration

Run this checklist at the point of failure:

  • The intended browser window or tab is active.
  • The test is in the correct frame. An element located in an iframe cannot be controlled while the top-level document is selected.
  • The selector resolves to the expected element, not a hidden duplicate or an off-screen template.
  • The control is visible, enabled and editable. A disabled input, read-only field, hidden element or non-editable container cannot accept normal text input.
  • A cookie banner, modal, loading layer or chat widget is not covering the control or stealing focus.
  • The page has finished the transition that creates or enables the control.

Check the active window and frame

When a test opens a new tab or uses an iframe, switch explicitly before locating and focusing the target. The exact window handles and frame element depend on the application, but the sequence should be deliberate:

const handles = await browser.getWindowHandles()
await browser.switchToWindow(handles[handles.length - 1])

const frame = await $('iframe[title="Checkout"]')
await frame.waitForDisplayed()
await browser.switchToFrame(frame)

const cardNumber = await $('input[name="cardnumber"]')
await cardNumber.waitForDisplayed()
await cardNumber.click()
await browser.keys('4111')

After the frame work, return to the parent document when the next assertion is outside the iframe:

await browser.switchToParentFrame()

If the application changes windows asynchronously, wait for the expected handle or title rather than assuming the newest handle is always correct.

Prove that the element can receive keys

Log the element state immediately before the key call. This turns a vague failure into a page-state diagnosis:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const field = await $('#username')
await field.waitForDisplayed()

console.log({
  displayed: await field.isDisplayed(),
  enabled: await field.isEnabled(),
  tag: await field.getTagName(),
  type: await field.getAttribute('type'),
  readonly: await field.getAttribute('readonly'),
  disabled: await field.getAttribute('disabled')
})

await field.click()
await browser.keys([Key.Ctrl, 'a'])
await browser.keys('alice')

A click can still fail to establish useful focus when an overlay intercepts it or when the element is not genuinely keyboard-interactable. Inspect the rendered page and remove the blocking condition in the test setup instead of forcing keys into a hidden node.

Handle custom editors carefully

A contenteditable region, code editor or widget may keep focus on an internal element rather than the outer selector. Locate the actual editable node, click it, and use the widget’s supported interaction model. For a normal input or textarea, element-level value methods are usually more deterministic than sending a long string through the browser-level command.

Capture the exact failure and versions

Only investigate the driver after the target and focus checks pass. Save the complete error, the command that failed, the browser window/frame state and these version values:

  • WebdriverIO package versions
  • Firefox version
  • geckodriver version
  • Node.js version and operating system

geckodriver is a separate WebDriver proxy between WebdriverIO and Firefox; its version scheme is not the same as Firefox’s. WebdriverIO documents Firefox driver binaries and allows a geckodriver version to be pinned separately with wdio:geckodriverOptions.geckoDriverVersion. Consult Mozilla’s geckodriver overview and WebdriverIO’s Firefox and Geckodriver driver-binaries documentation.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Pin the driver when reproducibility requires it

If a known-good CI image uses a particular geckodriver, make that choice explicit in the WebdriverIO capability rather than relying on an unrecorded auto-selection:

export const config = {
  capabilities: [{
    browserName: 'firefox',
    'wdio:geckodriverOptions': {
      geckoDriverVersion: 'SET_THE_VERSION_USED_BY_YOUR_CI_IMAGE'
    }
  }]
}

Replace the example value with the version you have selected and recorded for your environment. Do not infer compatibility from the Firefox major number alone; test the complete browser–driver pair. If changing the pair fixes the failure, retain the version evidence in the project’s CI configuration and bug report.

Do not make moz:webdriverClick the default fix

Mozilla documents the moz:webdriverClick capability as changing interactability checks for clicks and sending keys. Setting it to false temporarily disables conformant checks, but Mozilla also describes the capability as temporary and intended for removal after the implementation stabilizes.

That makes it a narrow diagnostic for a legacy or version-sensitive situation, not a durable solution to a hidden, disabled or incorrectly focused element. If you use it to isolate a suspected driver defect, run the same test with the default behavior, record the browser and geckodriver versions, and report a minimal reproducible case. Prefer fixing focusability and page state.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A practical decision guide

Symptom or goal First action Why
Enter, Tab, arrows or a modifier chord do nothing Focus the intended element, then call browser.keys(Key...) Browser-level keys use the active element and active window
Replacing text in a known input Use setValue() Targets the element directly and replaces its value
Appending to an existing value Use addValue() Preserves existing text
“Element not interactable” Check visibility, enabled state, editability, overlays, frame and focus Firefox checks focusability when sending keys
Works locally but not in CI Record and compare WebdriverIO, Firefox, geckodriver, Node.js and OS The proxy and browser are separate versioned components
Suspected driver regression Reproduce with a pinned, recorded geckodriver and default capabilities Separates a driver defect from page-state errors

Troubleshooting common failures

“browser.keys is not a function”

Check that the test is running against a WebdriverIO browser session and that the call is made on the browser instance supplied by the runner. A plain unit-test object, a mock with no command implementation or a detached context will not have the WebDriver command.

The call resolves but characters are missing

Confirm that focus is on an editable control and that the value is not being replaced by a framework re-render. Try the element’s setValue() method for text entry, then reserve browser.keys() for the keyboard action itself. Also check that the page has not moved focus to a validation message or another window.

Enter does not submit the form

Enter may be reaching a different focused element, or the page may handle submission only from a particular control. Focus the submit-associated input, send Key.Enter, and inspect the form’s event behavior. If the product requires clicking a visible submit button, test that path separately rather than assuming Enter must trigger it.

Ctrl+A selects the page instead of the field

The field did not own focus when the chord was sent. Click or otherwise focus the input, then send [Key.Ctrl, 'a']. On macOS, WebdriverIO maps Key.Ctrl to Command; do not hard-code a platform-specific modifier unless the test genuinely requires one.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The target is inside an iframe

Switch into the frame before locating and focusing the element. If the frame reloads, its element reference can become stale; locate it again after the reload and wait for the new control.

A workaround works only with moz:webdriverClick: false

Treat that result as evidence of an interactability or driver-implementation discrepancy, not proof that the capability should remain in production. Remove the override, capture a minimal reproduction and compare a pinned browser–geckodriver pair.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Make the test reliable and economical

  • Use stable selectors tied to the control’s role, label or name instead of coordinates.
  • Wait for the specific state you need—displayed, enabled and ready for input—rather than inserting a long fixed sleep.
  • Focus once immediately before a keyboard sequence; asynchronous UI updates can steal focus between commands.
  • Keep text entry and keyboard navigation separate so a failure identifies whether the value or the gesture is at fault.
  • Record the command, selector, active window, frame, focus target and component versions in CI logs.
  • When debugging, preserve a screenshot, page source and browser console output at the failure point. These artifacts show overlays and unexpected navigation that a stack trace cannot.

Keyboard calls are small, but they still incur a browser round trip. A short sequence of explicit actions is generally easier to diagnose than a long opaque string. Do not optimize by removing the waits that establish the required state; optimize by waiting on the correct condition.

Or skip the browser setup

If your actual goal is a rendered image or PDF of a page rather than keyboard interaction, ScreenshotNeo provides a direct website screenshot API and MCP server. It accepts a URL and returns PNG, JPEG, WebP or PDF. Before capture it accepts the cookie or consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups and chat widgets; each cleanup step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads and cache hits cost nothing, and each response reports the result in X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info and capture_pdf for Claude, Cursor and other MCP clients.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

One GET request is enough:

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 for parameters and response details. The same request in Python is:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

And in Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' }); const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo includes full-page capture with lazy images loaded, CSS-selector element capture, dark mode, 12 device presets and custom viewports, retina scale, PDF paper and page options, custom CSS and JavaScript, pre-capture clicks, selector hiding, waits, request and resource blocking, headers, cookies, user agents, Authorization, timezone and geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed image links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, a usage API and an OpenAPI specification. Parameter names used by other screenshot APIs also work to ease migration.

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; higher plans are Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000 and Business $249 for 1,000,000. Yearly billing provides two months free, and every feature is available on every plan. Sign up free for ScreenshotNeo with no card required.

When to report a persistent Firefox failure

Open an issue only after you can reproduce the behavior with a minimal page and a recorded environment. Include the exact WebdriverIO command, the complete error text, whether the target was a window or iframe, the element’s visible/enabled/editable state, Firefox and geckodriver versions, WebdriverIO version, operating system and whether the behavior changes with a pinned driver. That information lets maintainers distinguish a page interactability problem from a genuine geckodriver defect.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently Asked Questions

Does Firefox require a different key syntax from Chrome in WebdriverIO?

Use WebdriverIO’s shared Key constants and the same browser.keys() API. Firefox-specific investigation is usually about focusability, frame/window state or the geckodriver proxy rather than a separate key syntax.

Should I add a fixed delay before every keyboard command?

No. Wait for the specific element state or page condition that makes the action valid. Fixed sleeps can hide races while making the suite slower and still do not guarantee that focus is correct.

What information is most useful in a bug report?

Provide the complete error, minimal test, target HTML or URL, active window and frame details, element state, WebdriverIO/Firefox/geckodriver/Node.js versions and operating system.

Can I use browser.keys() to type into a hidden input?

No. Firefox’s interactability checks require a keyboard-focusable target. Use the visible editable control or the application’s supported API instead of forcing keys into a hidden field.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.