Skip to content
Merged
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
9 changes: 1 addition & 8 deletions .github/actions/build-android/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,7 @@ runs:

- name: Install example dependencies
shell: bash
run: |
npm pkg delete scripts.prepare
if [ -n "${{ inputs.rn-version }}" ]; then
npm --prefix example pkg set "dependencies.react-native=${{ inputs.rn-version }}" "overrides.react-native=${{ inputs.rn-version }}"
npm --prefix example install --no-save
else
npm --prefix example ci
fi
run: .github/scripts/install-example-deps.sh "${{ inputs.rn-version }}"

- name: Prebuild native project (android)
shell: bash
Expand Down
9 changes: 1 addition & 8 deletions .github/actions/build-ios/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,7 @@ runs:

- name: Install example dependencies
shell: bash
run: |
npm pkg delete scripts.prepare
if [ -n "${{ inputs.rn-version }}" ]; then
npm --prefix example pkg set "dependencies.react-native=${{ inputs.rn-version }}" "overrides.react-native=${{ inputs.rn-version }}"
npm --prefix example install --no-save
else
npm --prefix example ci
fi
run: .github/scripts/install-example-deps.sh "${{ inputs.rn-version }}"

- name: Prebuild native project (ios)
shell: bash
Expand Down
28 changes: 28 additions & 0 deletions .github/actions/build-web/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Build Web
description: >-
Export the example app for web as a static bundle

inputs:
rn-version:
description: 'react-native version to force in the example (e.g. "nightly", "0.84")'
required: false
default: ''

runs:
using: composite
steps:
- name: Setup Node
uses: actions/setup-node@v5
with:
node-version: 24
cache: 'npm'
cache-dependency-path: example/package-lock.json

- name: Install example dependencies
shell: bash
run: .github/scripts/install-example-deps.sh "${{ inputs.rn-version }}"

- name: Export web
shell: bash
run: npx expo export --platform web
working-directory: example
6 changes: 2 additions & 4 deletions .github/actions/e2e-android/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ runs:

- name: Install example dependencies
shell: bash
run: |
npm pkg delete scripts.prepare
npm --prefix example ci
run: .github/scripts/install-example-deps.sh

- name: Install Maestro
shell: bash
Expand All @@ -56,7 +54,7 @@ runs:

- name: Upload failure screenshot
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: failure-screenshot-android
path: maestro-debug/
Expand Down
6 changes: 2 additions & 4 deletions .github/actions/e2e-ios/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ runs:

- name: Install example dependencies
shell: bash
run: |
npm pkg delete scripts.prepare
npm --prefix example ci
run: .github/scripts/install-example-deps.sh

- name: Install Maestro
shell: bash
Expand All @@ -41,7 +39,7 @@ runs:

- name: Upload failure screenshot
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: failure-screenshot-ios
path: maestro-debug/
Expand Down
43 changes: 43 additions & 0 deletions .github/actions/e2e-web/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: E2E Web
description: >-
run the e2e Maestro tests on web (behavior only, no screenshot comparison)

runs:
using: composite
steps:
- name: Setup Node
uses: actions/setup-node@v5
with:
node-version: 24
cache: 'npm'
cache-dependency-path: example/package-lock.json

- name: Install example dependencies
shell: bash
run: .github/scripts/install-example-deps.sh

- name: Install Maestro
shell: bash
run: |
curl -Ls "https://get.maestro.mobile.dev" | MAESTRO_VERSION=2.10.0 bash
echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"

- name: Run E2E (web)
shell: bash
run: .github/scripts/e2e-web.sh

- name: Collect web e2e debug output
if: failure()
shell: bash
run: |
mkdir -p maestro-debug
cp -R "$HOME/.maestro/tests" maestro-debug/ 2>/dev/null || true
cp "$RUNNER_TEMP/web-server.log" maestro-debug/ 2>/dev/null || true

- name: Upload web e2e debug output
if: failure()
uses: actions/upload-artifact@v5
with:
name: e2e-web-debug
path: maestro-debug/
if-no-files-found: ignore
2 changes: 2 additions & 0 deletions .github/scripts/collect-failure-screenshots.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#!/usr/bin/env bash
mkdir -p maestro-debug

latest=$(find "$HOME/.maestro/tests" -type f -name '*.png' -print0 2>/dev/null \
| xargs -0 ls -t 2>/dev/null | head -1)

if [ -n "$latest" ] && [ -e "$latest" ]; then
cp "$latest" maestro-debug/failure.png
fi
10 changes: 10 additions & 0 deletions .github/scripts/e2e-web.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail

log_path="${RUNNER_TEMP:-/tmp}/web-server.log"

cd example

npx expo export --platform web
npx expo serve --port 8081 > "$log_path" 2>&1 &
npm run e2e:run -- --platform web --run-steps generation tests
13 changes: 13 additions & 0 deletions .github/scripts/install-example-deps.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail

rn_version="${1:-}"

npm pkg delete scripts.prepare

if [ -n "$rn_version" ]; then
npm --prefix example pkg set "dependencies.react-native=$rn_version" "overrides.react-native=$rn_version"
npm --prefix example install --no-save
else
npm --prefix example ci
fi
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ jobs:
- name: Build iOS
uses: ./.github/actions/build-ios

web:
name: Build Web
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Build Web
uses: ./.github/actions/build-web

e2e-android:
name: E2E Android (new arch)
runs-on: ubuntu-latest
Expand All @@ -94,3 +104,13 @@ jobs:

- name: Run E2E iOS
uses: ./.github/actions/e2e-ios

e2e-web:
name: E2E Web
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Run E2E Web
uses: ./.github/actions/e2e-web
12 changes: 12 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,15 @@ jobs:
uses: ./.github/actions/build-ios
with:
rn-version: nightly

nightly-web:
name: Build Web against react-native@nightly
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Build Web
uses: ./.github/actions/build-web
with:
rn-version: nightly
66 changes: 44 additions & 22 deletions example/e2e-run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ const SCREENSHOTS_PATH = './e2e/screenshots/';
const EXPECTED_SCREENSHOTS_PATH = './e2e/expected-screenshots/';
const GENERATED_FLOW_NAME = "generated_flow.yaml";

// Web runs headless in a browser tall enough to fit
// every demo card without scrolling:
// Maestro cannot scroll react-native-web's inner ScrollView,
// so off-screen cards would be unreachable
const WEB_SCREEN_SIZE = '1280x2200';

// Initialize logger
const logger = new Logger();
logger.isVerbose = true;
Expand All @@ -37,7 +43,12 @@ const argv = yargs(hideBin(process.argv))
demandOption: true,
describe: 'Specify the platform for the build',
type: 'string',
choices: ['ios', 'android'],
choices: ['ios', 'android', 'web'],
})
.option('url', {
describe: 'Base URL of the running web app (web platform only)',
type: 'string',
default: 'http://localhost:8081',
})
.option('architecture', {
alias: 'arch',
Expand Down Expand Up @@ -77,20 +88,23 @@ const argv = yargs(hideBin(process.argv))
.parse();

// Setup variables
const { platform, architecture, runSteps, silent, updateScreenshots, device: specificDevice, deviceLabel } = argv;
const { platform, architecture, runSteps, silent, updateScreenshots, device: specificDevice, deviceLabel, url } = argv;
const isWeb = platform === 'web';

logger.isVerbose = !silent;

// App id comes from the Expo config (single example app), not a per-project derivation.
const appConfig = JSON.parse(fs.readFileSync('./app.json', 'utf8'));
const appId = platform === 'ios'
? appConfig.expo.ios.bundleIdentifier
: appConfig.expo.android.package;
const appId = isWeb
? 'web'
: platform === 'ios'
? appConfig.expo.ios.bundleIdentifier
: appConfig.expo.android.package;

const modalsConfig = JSON.parse(fs.readFileSync('./demo-components/src/modals.config.json', 'utf8'));

const generatedFlowPath = path.join(process.cwd(), 'e2e', 'flows', GENERATED_FLOW_NAME);
const baseFlowPath = path.join(process.cwd(), 'e2e', 'flows', 'base.yaml');
const baseFlowPath = path.join(process.cwd(), 'e2e', 'flows', isWeb ? 'base.web.yaml' : 'base.yaml');

const getBootedDevice = () => {
if (specificDevice) {
Expand All @@ -108,7 +122,7 @@ const getBootedDevice = () => {
}
}

const device = getBootedDevice();
const device = isWeb ? null : getBootedDevice();
// Screenshots are keyed by a stable label, not the device UDID, so baselines stay reproducible
// across machines. Pin the same simulator/emulator model when (re)generating expected screenshots.
const screenshotDevice = deviceLabel || platform;
Expand Down Expand Up @@ -152,28 +166,36 @@ const generateFlow = () => {

// Run E2E tests using Maestro
const runE2ETests = () => {
if (!device) {
if (!isWeb && !device) {
logger.raiseException(`No booted ${platform} device found. Boot a simulator/emulator (or pass --device) before running e2e.`);
}

logger.log(`Running E2E tests for app ${appId} on device: ${device}`);
let maestroCmd;
if (isWeb) {
logger.log(`Running web E2E tests against ${url}`);
maestroCmd = `maestro test --headless --screen-size ${WEB_SCREEN_SIZE} ${generatedFlowPath} -e URL=${url} -e APP_ID=${appId}`;
} else {
logger.log(`Running E2E tests for app ${appId} on device: ${device}`);

// Freeze the status bar so screenshots are deterministic across runs (clock, battery, signal).
if (platform === 'ios') {
execSync(`xcrun simctl status_bar ${device} override --time "9:41" --batteryState charged --batteryLevel 100 --cellularMode active --cellularBars 4 --dataNetwork wifi --wifiMode active --wifiBars 3`, { stdio: 'ignore' });
} else if (platform === 'android') {
// Android SystemUI demo mode: fixed clock/battery/signal.
const demo = (args) => execSync(`adb -s ${device} shell am broadcast -a com.android.systemui.demo ${args}`, { stdio: 'ignore' });
execSync(`adb -s ${device} shell settings put global sysui_demo_allowed 1`, { stdio: 'ignore' });
demo('-e command clock -e hhmm 0941');
demo('-e command battery -e level 100 -e plugged false');
demo('-e command network -e wifi show -e level 4');
demo('-e command network -e mobile show -e datatype none -e level 4');
demo('-e command notifications -e visible false');
}

// Freeze the status bar so screenshots are deterministic across runs (clock, battery, signal).
if (platform === 'ios') {
execSync(`xcrun simctl status_bar ${device} override --time "9:41" --batteryState charged --batteryLevel 100 --cellularMode active --cellularBars 4 --dataNetwork wifi --wifiMode active --wifiBars 3`, { stdio: 'ignore' });
} else if (platform === 'android') {
// Android SystemUI demo mode: fixed clock/battery/signal.
const demo = (args) => execSync(`adb -s ${device} shell am broadcast -a com.android.systemui.demo ${args}`, { stdio: 'ignore' });
execSync(`adb -s ${device} shell settings put global sysui_demo_allowed 1`, { stdio: 'ignore' });
demo('-e command clock -e hhmm 0941');
demo('-e command battery -e level 100 -e plugged false');
demo('-e command network -e wifi show -e level 4');
demo('-e command network -e mobile show -e datatype none -e level 4');
demo('-e command notifications -e visible false');
maestroCmd = `maestro --device ${device} test ${generatedFlowPath} -e APP_ID=${appId}`;
}

try {
execSync(`maestro --device ${device} test ${generatedFlowPath} -e APP_ID=${appId}`, { stdio: 'inherit' });
execSync(maestroCmd, { stdio: 'inherit' });
logger.log('E2E tests completed successfully.');
} catch (error) {
logger.raiseException('E2E tests failed:', error.message);
Expand Down
14 changes: 14 additions & 0 deletions example/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,17 @@ Useful flags:
- `--device-label <name>` (stable label in screenshot file names)
- `--run-steps generation tests images`,
- `--silent`

## Web behavior only

Prerequisites: Maestro CLI 2.10.0+

```sh
npx expo start --web
```

Then in another terminal:

```sh
npm run e2e:run -- --platform web --run-steps generation tests
```
3 changes: 3 additions & 0 deletions example/e2e/flows/base.web.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
url: ${URL}
---
- launchApp