Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .eas/workflows/e2e-linux.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: E2E (Linux, Electron)

on:
workflow_dispatch: {}
pull_request:
paths:
- apps/menu-bar/src/**
- apps/menu-bar/e2e/**
- apps/menu-bar/electron/**
- .eas/workflows/e2e-linux.yml

concurrency:
group: ${{ workflow.filename }}-${{ github.ref }}
cancel_in_progress: true

defaults:
tools:
node: '22'

jobs:
electron_e2e:
name: Electron E2E (Linux)
runs_on: linux-medium
steps:
- uses: eas/checkout

# The linux-medium GCE image is barebones; Electron needs a virtual
# display (xvfb) and a handful of runtime libs that aren't preinstalled.
# Without these, Chromium exits immediately and Chromedriver reports
# "DevToolsActivePort file doesn't exist".
- name: Install xvfb and Electron runtime libs
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
xvfb \
libgbm1 libgtk-3-0 libnotify4 libnss3 libxss1 libxtst6 \
libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2
# libasound2 on Ubuntu 22.04, libasound2t64 on 24.04; try both.
sudo apt-get install -y --no-install-recommends libasound2 \
|| sudo apt-get install -y --no-install-recommends libasound2t64

- name: Install workspace dependencies
run: yarn install --frozen-lockfile

- name: Build packages
run: yarn build

- name: Install electron dependencies
working_directory: apps/menu-bar/electron
run: yarn install --frozen-lockfile

- name: Package electron app
working_directory: apps/menu-bar/electron
run: yarn package

- name: Install E2E dependencies
working_directory: apps/menu-bar/e2e
run: yarn install

- name: Run E2E tests
working_directory: apps/menu-bar/e2e
run: xvfb-run --auto-servernum yarn test:electron
123 changes: 123 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
name: E2E Tests

# Linux Electron E2E runs on EAS Workflows; see .eas/workflows/e2e-linux.yml.
# Windows and macOS stay on GitHub Actions:
# - EAS has no Windows runners.
# - EAS macOS workers don't expose an active Aqua/Console session, so
# xcodebuild test (which Appium mac2 relies on) can't reach testmanagerd.

on:
workflow_dispatch:
pull_request:
paths:
- apps/menu-bar/src/**
- apps/menu-bar/e2e/**
- apps/menu-bar/electron/**
- apps/menu-bar/macos/**
- .github/workflows/e2e-tests.yml

concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true

jobs:
electron-e2e-windows:
runs-on: windows-latest
name: Electron E2E (Windows)
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22

- name: Install workspace dependencies
run: yarn install --frozen-lockfile

- name: Build packages
run: yarn build

- name: Install electron dependencies
run: yarn install --frozen-lockfile
working-directory: apps/menu-bar/electron

- name: Package electron app
run: yarn package
working-directory: apps/menu-bar/electron

- name: Install E2E dependencies
run: yarn install
working-directory: apps/menu-bar/e2e

- name: Run E2E tests
run: yarn test:electron
working-directory: apps/menu-bar/e2e

macos-e2e:
runs-on: macos-15
name: macOS E2E (Appium mac2)
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22

- name: Switch to Xcode 26.2
run: sudo xcode-select --switch /Applications/Xcode_26.2.app

- name: Install workspace dependencies
run: yarn install --frozen-lockfile

- name: Build packages
run: yarn build

- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.2"
bundler-cache: true
working-directory: apps/menu-bar

- name: Install CocoaPods
working-directory: apps/menu-bar/macos
run: bundle exec pod install

- name: Build macOS app (unsigned — E2E only)
working-directory: apps/menu-bar
# Skip codesigning: the E2E job has no Developer ID Application cert,
# and Appium mac2 launches the .app by path in the same user session,
# so Gatekeeper isn't in the picture. Production builds are still
# signed/notarized via apps/menu-bar/.eas/build/build-release.yml.
run: |
xcodebuild build \
-workspace macos/ExpoMenuBar.xcworkspace \
-scheme ExpoMenuBar-macOS \
-configuration Release \
-destination 'platform=macOS' \
-derivedDataPath macos/build \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO

- name: Install E2E dependencies
run: yarn install
working-directory: apps/menu-bar/e2e

- name: Install Appium mac2 driver
working-directory: apps/menu-bar/e2e
run: npx appium driver install mac2

- name: Run macOS E2E tests
working-directory: apps/menu-bar/e2e
env:
MACOS_APP_PATH: ../macos/build/Build/Products/Release/Expo Orbit.app
run: yarn test:macos
7 changes: 7 additions & 0 deletions .github/workflows/lint-and-tsc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ jobs:
run: yarn tsc --noEmit
working-directory: apps/menu-bar/electron

- name: 🧶 Install E2E node modules
run: yarn install
working-directory: apps/menu-bar/e2e
- name: 🏗️ TSC "Orbit" e2e files
run: yarn tsc --noEmit
working-directory: apps/menu-bar/e2e

- name: 🏗️ TSC "CLI" app files
run: yarn tsc --noEmit
working-directory: apps/cli
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

### 💡 Others

- Add cross-platform E2E testing with WebdriverIO for Electron and Appium mac2 for macOS. ([#334](https://github.com/expo/orbit/pull/334) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- Use EAS custom builds for macOS releases. ([#330](https://github.com/expo/orbit/pull/330) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- Enable react-native new architecture. ([#310](https://github.com/expo/orbit/pull/310) by [@gabrieldonadel](https://github.com/gabrieldonadel))

Expand Down
176 changes: 176 additions & 0 deletions apps/menu-bar/e2e/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# Cross-Platform E2E Testing — Findings & Notes

## Architecture

Both macOS (native via react-native-macos) and Electron share the same React Native codebase in `apps/menu-bar/src/`. Tests use WebdriverIO with platform-specific drivers:

| | Electron | macOS Native |
| ----------------- | ----------------------------------------- | -------------------------------------------- |
| Driver | `wdio-electron-service` | `appium-mac2-driver` |
| Element lookup | CSS `[data-testid="..."]` | Accessibility ID `~...` |
| Window management | `getWindowHandles()` + `switchToWindow()` | Not supported — all windows in one tree |
| App launch | `appBinaryPath` in electron service | `appium:bundleId` + optional `appium:prerun` |

## macOS Accessibility Tree

### Only interactive elements are visible

On macOS, React Native's `testID` sets `accessibilityIdentifier` on the native `NSView`, but **only interactive elements** (Pressable, TouchableOpacity, Button) appear as accessibility elements in XCUITest's tree. Plain `<View testID="...">` elements are **not** queryable.

This means:

- ✅ `get-started-button` (Button/TouchableOpacity) — **works**
- ✅ `settings-button`, `quit-button` (Pressable via Item component) — **works**
- ✅ `select-build-eas`, `select-build-local` (Pressable via Item) — **works**
- ✅ `device-item` (Pressable) — **works**
- ❌ `onboarding-window` (plain View) — **not in tree**
- ❌ `popover-core`, `popover-footer`, `builds-section` (plain View) — **not in tree**
- ❌ `settings-window` (plain View) — **not in tree**

**Do not** add `accessible={true}` to wrapper Views to fix this — it makes the View an accessibility container that **hides all its children** from the tree.

### Dumping the tree for debugging

Run with `E2E_DEBUG=1` to dump the full accessibility tree at test start:

```sh
E2E_DEBUG=1 yarn test:macos
```

This outputs the XML source via `browser.getPageSource()` in the `before` hook (see `wdio.shared.ts`).

## macOS App Launch

### `appium:app` does not work on mac2

Unlike iOS, the `appium:app` capability does **not** launch the specified `.app` bundle on macOS. The mac2 driver only reliably launches apps via `appium:bundleId`.

### Launching a specific build

To test a specific `.app` build (not the installed one in `/Applications/`):

1. Use `appium:prerun` to open the `.app` via `open -a` before the session starts
2. Use `appium:bundleId` to connect to it

```typescript
{
'appium:bundleId': 'dev.expo.orbit',
'appium:prerun': { command: 'open', args: ['-a', '/absolute/path/to/Expo Orbit.app'] },
}
```

### Never set `appium:bundleId` and `appium:app` simultaneously

When both are present, the driver prefers the installed app matching that bundle ID and silently ignores `appium:app`.

### Path must be absolute

The mac2 driver resolves relative paths from WebDriverAgentRunner's sandboxed container (`~/Library/Containers/io.appium.WebDriverAgentRunner.xctrunner/...`), not from the test directory. Always use `path.resolve()`.

## macOS Window Management

### `getWindowHandles()` is not supported

The mac2 driver does not implement the WebDriver `/window/handles` endpoint — it's a browser-specific concept. On macOS, all app windows (popover, onboarding, settings) live in a single accessibility tree and are queryable directly without switching.

### Menu bar app behavior

Orbit is a menu bar app (`NSApp.setActivationPolicy(.accessory)` in Release). There's no dock icon or main window. The UI is:

- A status bar icon (XCUIElementTypeStatusItem)
- A popover attached to the status bar icon
- Secondary windows (Onboarding, Settings) as regular NSWindows

## State Management

### MMKV storage location

On macOS native the storage path is `~/.expo/orbit/` (defined in `packages/common-types/src/storage.ts` via `StorageUtils.getExpoOrbitDirectory()`). The instance ID is `mmkv.default`.

On **Electron**, `Platform.OS === 'web'` in the renderer, so [`apps/menu-bar/src/modules/Storage.ts`](../src/modules/Storage.ts) passes `path: undefined` to `new MMKV()`, and `react-native-mmkv`'s web shim stores everything in `localStorage`. That ends up in the Electron userData directory (`~/Library/Application Support/<app-name>/Local Storage/leveldb/`), **not** `~/.expo/orbit/`.

Key values:

- `has-seen-onboarding` — boolean, controls whether onboarding shows on launch
- `user-preferences` — JSON string with user settings
- `sessionSecret` — auth token

### Pinning the Electron userData dir

Chromedriver defaults to a throwaway `--user-data-dir` per session, so without pinning, every `wdio run ./wdio.electron.ts` would start fresh (onboarding shows again, prefs lost, etc.). We pass an explicit `--user-data-dir=<tmpdir>/orbit-e2e-user-data` via `appArgs` in [`wdio.electron.ts`](wdio.electron.ts) so state persists between spec sessions. `reset-state.sh` wipes that dir before each `yarn test:electron` run.

### Resetting state for tests

Run `yarn reset-state` (or it runs automatically via `pretest:*` hooks) to clear `~/.expo/orbit/` and the pinned Electron userData dir so the app launches in a first-run state with onboarding visible.

## Platform Detection

### Use `automationName` to detect mac2

Neither `browserName` nor `platformName` are reliable for distinguishing Electron from macOS native:

- `browserName: 'electron'` → Chromedriver overrides it to `'chrome'` at runtime
- `platformName: 'mac'` → `wdio-electron-service` also sets this when running on macOS

The only reliable discriminator is `automationName`, which is `'Mac2'` for Appium mac2 and absent for Electron:

```typescript
// ✅ Reliable — only set in wdio.macos.ts, never present for Electron
const isNativeMac =
(browser.capabilities as Record<string, unknown>)['appium:automationName'] === 'Mac2';

// ❌ Broken — Chromedriver reports 'chrome', not 'electron'
const isElectron = browser.capabilities.browserName === 'electron';

// ❌ Broken — wdio-electron-service sets 'mac' on macOS too
const isNativeMac = browser.capabilities.platformName === 'mac';
```

## Electron-Specific Notes

### `browserVersion` is required

When the `e2e/` package doesn't have `electron` as a direct dependency, you must set `browserVersion` in the capability (e.g. `'33.2.0'`) so `wdio-electron-service` can fetch the matching Chromedriver.

### `expect` import

Always import `expect` from `@wdio/globals`, **not** from `expect-webdriverio` directly. Double-importing causes `Cannot redefine property: soft` because the global is registered twice.

```typescript
// ✅ Correct
import { browser, expect } from '@wdio/globals';

// ❌ Causes runtime error
import { expect } from 'expect-webdriverio';
```

### Disable the CDP bridge if you don't use `browser.electron.*`

`wdio-electron-service` opens a Chrome DevTools Protocol bridge to the Electron main process on startup to support `browser.electron.execute`/`mock`. Waiting for `Runtime.executionContextCreated` with `auxData.isDefault` takes ~1–10s, retries up to 3×, and produces a spurious `Timeout exceeded to get the ContextId` error log even on success (the orphan `setTimeout` calls `log.error` regardless of whether the promise already resolved).

If the specs don't use `browser.electron.*` (ours don't), pass `useCdpBridge: false`:

```ts
services: [['electron', { useCdpBridge: false }]],
```

This falls back to the deprecated IPC-bridge. The service logs a deprecation warning, but startup is fast and the spurious `ContextId` log goes away.

### Stale renderer bundle trap

The packaged `.app` bundles the Expo web export from `apps/menu-bar/electron/dist/`, which is only regenerated when `yarn export-web` runs. The `generateAssets` hook in `forge.config.ts` fires this for both `make` and `package` — but historically it only fired for `make`, which meant `yarn package` would silently bundle a stale `dist/` and ship an old renderer into the `.app`.

**Symptom**: the popover's DOM only contains a subset of expected `testID`s even though they exist in the source.

**Diagnose**: grep the packaged bundle for the testid string:

```sh
grep -c "select-build-eas" \
"apps/menu-bar/electron/out/Expo Orbit-darwin-arm64/Expo Orbit.app/Contents/Resources/app/.vite/build/renderer/dist/_expo/static/js/web/"*.js
```

If `0`, the renderer is stale. Re-run `yarn export-web` in `apps/menu-bar` and re-package.

### `--user-data-dir` rejected when launching the Electron binary directly

Running `"Expo Orbit.app/Contents/MacOS/expo-orbit" --user-data-dir=/tmp/x` from a shell prints `bad option: --user-data-dir=...` and exits — the ad-hoc-signed dev build's launcher stub rejects args when invoked directly. This is **not** a problem for the E2E setup: when Chromedriver launches Electron, it injects the flag via `goog:chromeOptions.args` and the Chromium layer accepts it normally. Don't waste time trying to reproduce E2E failures by invoking the binary manually — use `yarn test:electron`.
Loading
Loading