Files
orca/tests/e2e/computer-mac-safari.e2e.ts
Brennan Benson 66dfdc456f feat(computer-use): support macOS middle click and stop the silent left-click fallback (#14721)
* feat(computer-use): support macOS middle click and gate the AX click path

`--mouse-button middle` already validated end-to-end through the CLI, the
zod schema, and the provider validator, and both the Windows and Linux
providers honored it. Only the macOS provider rejected it outright with
"middle-click is not yet supported", so the flag was a dead end on the one
platform that has no fallback.

Two changes:

- Add `.middle` to the macOS button mapping. macOS has no dedicated middle
  event family, so it rides `otherMouseDown`/`otherMouseUp` with the button
  number carried by `mouseButton: .center`; that constructor argument is
  honored for exactly the `otherMouse*` types, so no extra field write is
  needed.
- Validate the requested button before the accessibility fast path, and skip
  that path for buttons it cannot express. Previously the raw string was read
  unvalidated, and `performClickAction` only special-cased `right`, so
  `click --mouse-button middle --element-index N` (no modifiers, count 1) fell
  through to `AXPress` — a left click — and reported success with
  `path: "accessibility"`. Any unrecognized button string did the same. This
  matches guards the Windows and Linux providers already had.

The button enum moves into `OrcaComputerUseMacOSCore` so it is unit-testable;
`main.swift` keeps only the CoreGraphics mapping.

Also documents `--mouse-button` in the computer-use skill guide, which never
mentioned the flag, so agents on Windows and Linux had no way to discover it.

* test(computer-use): cover macOS middle click in the real-desktop e2e suite

* test(computer-use): prove macOS middle-click delivery
2026-08-15 00:41:45 -07:00

232 lines
6.4 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, test } from 'vitest'
import type {
ComputerActionResult,
ComputerListWindowsResult,
ComputerSnapshotResult
} from '../../src/shared/runtime-types'
import {
closeSafariDraftFixture,
ensureOrcaRuntimeLaunched,
ensureSafariDraftFixtureLaunched,
findRoleIndex,
parseJsonOutput,
runOrcaCli,
type SafariDraftFixture
} from './helpers/computer-driver'
const isMac = process.platform === 'darwin'
const e2eOptIn = process.env.ORCA_COMPUTER_E2E === '1'
describe.skipIf(!isMac || !e2eOptIn)('computer-use macOS e2e (Safari web app)', () => {
let fixture: SafariDraftFixture
beforeAll(async () => {
await ensureOrcaRuntimeLaunched()
fixture = await ensureSafariDraftFixtureLaunched()
})
afterAll(async () => {
await closeSafariDraftFixture()
})
test('fills a browser-hosted draft using fresh state between actions', async () => {
const targetArgs = await safariFixtureWindowTargetArgs(fixture.title)
let state = parseJsonOutput<{ result: ComputerSnapshotResult }>(
(
await runOrcaCli([
'computer',
'get-app-state',
'--app',
'com.apple.Safari',
...targetArgs,
'--restore-window',
'--no-screenshot',
'--json'
])
).stdout
)
expect(state.result.snapshot.window.title).toContain(fixture.title)
expect(state.result.snapshot.treeText).toContain('Draft empty')
const recipientIndex = findRoleIndex(
state.result.snapshot.treeText,
/^\s*(\d+)\s+text field \(settable\) Recipient/m
)
expect(recipientIndex).toBeGreaterThanOrEqual(0)
await runOrcaCli([
'computer',
'click',
'--app',
'com.apple.Safari',
...targetArgs,
'--element-index',
String(recipientIndex),
'--restore-window',
'--no-screenshot',
'--json'
])
const recipient = `agent-${Date.now()}@example.com`
const recipientPaste = parseJsonOutput<{ result: ComputerActionResult }>(
(
await runOrcaCli([
'computer',
'paste-text',
'--app',
'com.apple.Safari',
...targetArgs,
'--text',
recipient,
'--restore-window',
'--no-screenshot',
'--json'
])
).stdout
)
expect(recipientPaste.result.action?.verification).toMatchObject({
state: 'verified',
property: 'focusedText',
expected: recipient
})
state = parseJsonOutput<{ result: ComputerSnapshotResult }>(
(
await runOrcaCli([
'computer',
'press-key',
'--app',
'com.apple.Safari',
...targetArgs,
'--key',
'Tab',
'--restore-window',
'--no-screenshot',
'--json'
])
).stdout
)
expect(state.result.snapshot.treeText).toContain('focused UI element is')
expect(state.result.snapshot.treeText).toContain('Body')
const body = `hello browser draft ${Date.now()}`
const bodyPaste = parseJsonOutput<{ result: ComputerActionResult }>(
(
await runOrcaCli([
'computer',
'paste-text',
'--app',
'com.apple.Safari',
...targetArgs,
'--text',
body,
'--restore-window',
'--no-screenshot',
'--json'
])
).stdout
)
expect(bodyPaste.result.action?.verification).toMatchObject({
state: 'verified',
property: 'focusedText',
expected: body
})
state = parseJsonOutput<{ result: ComputerSnapshotResult }>(
(
await runOrcaCli([
'computer',
'get-app-state',
'--app',
'com.apple.Safari',
...targetArgs,
'--restore-window',
'--no-screenshot',
'--json'
])
).stdout
)
const saveIndex = findRoleIndex(state.result.snapshot.treeText, 'button Save draft')
expect(saveIndex).toBeGreaterThanOrEqual(0)
const saved = parseJsonOutput<{ result: ComputerActionResult }>(
(
await runOrcaCli([
'computer',
'click',
'--app',
'com.apple.Safari',
...targetArgs,
'--element-index',
String(saveIndex),
'--restore-window',
'--no-screenshot',
'--json'
])
).stdout
)
expect(saved.result.action?.actionName).toBe('AXPress')
expect(saved.result.snapshot.treeText).toContain(`Draft ready: ${recipient} / ${body}`)
})
test('delivers an element-index middle click to the browser receiver', async () => {
const targetArgs = await safariFixtureWindowTargetArgs(fixture.title)
const before = parseJsonOutput<{ result: ComputerSnapshotResult }>(
(
await runOrcaCli([
'computer',
'get-app-state',
'--app',
'com.apple.Safari',
...targetArgs,
'--restore-window',
'--no-screenshot',
'--json'
])
).stdout
)
expect(before.result.snapshot.treeText).toContain('Middle click waiting')
const receiverIndex = findRoleIndex(
before.result.snapshot.treeText,
'button Middle click receiver'
)
expect(receiverIndex).toBeGreaterThanOrEqual(0)
const middle = parseJsonOutput<{ result: ComputerActionResult }>(
(
await runOrcaCli([
'computer',
'click',
'--app',
'com.apple.Safari',
...targetArgs,
'--element-index',
String(receiverIndex),
'--mouse-button',
'middle',
'--restore-window',
'--no-screenshot',
'--json'
])
).stdout
)
expect(middle.result.action?.path).toBe('synthetic')
expect(middle.result.action?.fallbackReason).toBe('actionUnsupported')
expect(middle.result.snapshot.treeText).toContain('Middle click received')
})
})
async function safariFixtureWindowTargetArgs(title: string): Promise<string[]> {
const windows = parseJsonOutput<{ result: ComputerListWindowsResult }>(
(await runOrcaCli(['computer', 'list-windows', '--app', 'com.apple.Safari', '--json'])).stdout
).result.windows
const target = windows.find((window) => window.title.includes(title))
expect(target).toBeTruthy()
if (target?.id !== undefined && target.id !== null) {
return ['--window-id', String(target.id)]
}
return ['--window-index', String(target?.index ?? 0)]
}