> ## Documentation Index
> Fetch the complete documentation index at: https://extension.js.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Trigger toolbar actions and commands

> Fire chrome.action.onClicked and chrome.commands.onCommand from the terminal or CI headlessly on Chrome and Firefox, with real-gesture clicks for activeTab.

You can't click a toolbar icon in CI, and a popup-less action only does something when its `onClicked` fires. Extension.js lets you trigger those handlers directly (the toolbar action and keyboard-shortcut commands), so you can exercise them headlessly and assert on what they did.

This works because Extension.js captures your `chrome.action.onClicked` and `chrome.commands.onCommand` listeners at build time (the dev runtime loads before your background code) and replays them on demand. The replay runs in the background context, the same service worker or background page where your listeners live. No window, no mouse, no flake.

<Frame>
  <iframe className="w-full aspect-video rounded-xl" src="https://www.youtube-nocookie.com/embed/vjkEzYMlh5g?rel=0" title="Extension.js: extension open action" loading="lazy" allow="encrypted-media; picture-in-picture; fullscreen" allowFullScreen />
</Frame>

<Note>
  Requires a dev session started with `--allow-control`. See [Debugging
  overview](/docs/debugging).
</Note>

## Trigger the toolbar action

```bash theme={null}
pnpm extension open action --output json
```

If the action has a `default_popup`, this opens it. If it doesn't, it replays the `onClicked` listeners:

```json theme={null}
{
  "schema": 1,
  "ok": true,
  "command": "open",
  "status": "ok",
  "value": { "triggered": "onClicked", "listeners": 1, "gesture": false },
  "error": null,
  "warnings": []
}
```

## Trigger a keyboard command

Replay any `chrome.commands.onCommand` handler by name. The shortcut itself is never pressed:

```bash theme={null}
pnpm extension open command --name toggle-theme --output json
```

```json theme={null}
{
  "schema": 1,
  "ok": true,
  "command": "open",
  "status": "ok",
  "value": {
    "triggered": "command",
    "command": "toggle-theme",
    "listeners": 1,
    "gesture": false
  },
  "error": null,
  "warnings": []
}
```

## The one caveat: no user gesture

Replay invokes your handler, but it is **not** a real user click, so the browser does not attach a user gesture. The practical consequence:

<Warning>
  A real toolbar click grants temporary **`activeTab`** and satisfies
  gesture-gated APIs (`chrome.permissions.request`, interactive
  `chrome.identity.getAuthToken`). A replay does not. So a handler that relies
  on `activeTab` (`chrome.scripting.executeScript` on the active tab,
  `chrome.tabs.captureVisibleTab`) will behave differently than a real click.
</Warning>

Extension.js tells you when this matters: the result reports `gesture: false`, and adds a `warning` when your manifest declares `activeTab`:

```json theme={null}
{
  "schema": 1,
  "ok": true,
  "command": "open",
  "status": "ok",
  "value": {
    "triggered": "onClicked",
    "listeners": 1,
    "gesture": false,
    "warning": "replayed without a user gesture: activeTab is NOT granted, so APIs that depend on it (scripting on the active tab, captureVisibleTab) behave differently than a real click"
  },
  "error": null,
  "warnings": []
}
```

For most handler logic (toggling state, writing storage, opening a tab, messaging), replay is exactly what you want: deterministic and scriptable. Reach for the real click below only when you specifically need gesture semantics.

## Need a genuine-gesture click?

A real action click grants `activeTab`, confirmed on Chrome 149. The replay deliberately does **not** carry that gesture (so `activeTab`-dependent handlers behave differently). For the rare case where you need true gesture semantics, use [chrome-devtools-mcp](https://github.com/ChromeDevTools/chrome-devtools-mcp)'s `trigger_extension_action` (Chromium only), which drives a real invocation through Puppeteer over the supported pipe transport.

Consider [running it alongside Extension.js](/docs/integrations/chrome-devtools-mcp) anyway: let it own genuine-gesture browser driving, while Extension.js owns the build-aware, cross-browser, headless replay you use for everything else.

## In CI: assert your action handler ran

Boot a session, fire the action, and assert on the side effect, with no browser UI and no Playwright. This is the whole loop in a GitHub Actions job:

```yaml theme={null}
- run: |
    pnpm extension dev --browser=chromium --allow-control &
    pnpm extension dev --browser=chromium --wait --output json
    pnpm extension open action --browser=chromium --output json
    pnpm extension storage get --key lastClickedAt --browser=chromium --output json \
      | node -e 'process.exit(JSON.parse(require("fs").readFileSync(0)).value?.lastClickedAt ? 0 : 1)'
```

The first line boots the session in the background. The second is the readiness gate: `--wait`
waits for that session's [ready contract](/docs/contracts/ready-json) and exits, it never
launches anything itself. Keep both in one `run:` step so the backgrounded server and the gate
share a shell.

Because the trigger is deterministic and headless, it's a reliable gate. The same path is exercised by Extension.js's own `smoke:open-action` check, which fires the action *and* a command, then reads `chrome.storage` back to prove each handler actually executed.

See [CI templates](/docs/workflows/ci-templates#test-handlers-without-a-browser-ui) for the full pipeline.

## Cross-browser

`open action` and `open command` are built on the in-browser companion, so they run on **both Chrome and Firefox**. They touch only `addListener`, nothing engine-specific, and are verified on both. A single `background.service_worker` source works on Firefox too: Extension.js translates it to a `background.scripts` event page for the Firefox build. (A genuine-gesture click is Chromium-only and lives in chrome-devtools-mcp. See above.)

## Next steps

* [Debugging overview](/docs/debugging): the full control surface.
* [CI templates](/docs/workflows/ci-templates): wire this into a pull-request gate.
* [Chrome DevTools MCP](/docs/integrations/chrome-devtools-mcp): fidelity vs. portability, side by side.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.