Maestro Setup for app (#407)

Implementing Maestro tests against onboarding process
This commit is contained in:
Ritesh Shukla
2025-06-05 02:14:52 +05:30
committed by GitHub
parent 1c069fe0bd
commit ecbbabcf4d
15 changed files with 273 additions and 8 deletions

2
.env Normal file
View File

@@ -0,0 +1,2 @@
OTA_UPDATE_ENABLED=true
IS_MAESTRO_BUILD = false

View File

@@ -4,11 +4,69 @@ on:
workflow_dispatch:
jobs:
build-android:
runs-on: macos-15
outputs:
version: ${{ steps.setver.outputs.version }}
steps:
- name: 🛒 Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.SIGNING_REPO_PAT }}
- name: 🖥 Setup Node 20
uses: actions/setup-node@v4
with:
node-version: 20
- name: 💎 Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
bundler-cache: true
- name: 🍎 Run yarn init-android
run: yarn install --network-concurrency 1
- name: 💬 Disable OTA Updates and Enable Maestro Build
run: node scripts/updateEnv.js OTA_UPDATE_ENABLED=false IS_MAESTRO_BUILD=true
- name: 🤫 Output TelemetryDeck Secrets to TelemetryDeck.json
run: |
echo "{" > telemetrydeck.json
echo "\"appID\": \"${{ secrets.TELEMETRYDECK_APPID }}\"," >> telemetrydeck.json
echo "\"clientUser\": \"anonymous\"," >> telemetrydeck.json
echo "\"app\": \"Jellify\"" >> telemetrydeck.json
echo "}" >> telemetrydeck.json
- name: 🤫 Output Glitchtip Secrets to Glitchtip.json
run: |
echo "{" > glitchtip.json
echo "\"dsn\": \"${{ secrets.GLITCHTIP_DSN }}\"" >> glitchtip.json
echo "}" >> glitchtip.json
- name: ✅ Validate Config Files
run: |
node -e "JSON.parse(require('fs').readFileSync('telemetrydeck.json'))"
node -e "JSON.parse(require('fs').readFileSync('glitchtip.json'))"
- name: 🚀 Run Android fastlane build
run: yarn fastlane:android:build
- name: 📤 Upload Android Artifacts
uses: actions/upload-artifact@v4
with:
name: android-artifacts
path: ./android/app/build/outputs/apk/release/*.apk
run-maestro-tests:
runs-on: ubuntu-latest
needs: build-android
env:
JELLYFIN_TEST_ADDRESS: ${{ secrets.JELLYFIN_TEST_ADDRESS }}
JELLYFIN_TEST_ADDRESS: ${{ secrets.JELLYFIN_TEST_URL }}
JELLYFIN_TEST_USERNAME: ${{ secrets.JELLYFIN_TEST_USERNAME }}
JELLYFIN_TEST_PASSWORD: ${{ secrets.JELLYFIN_TEST_PASSWORD }}
steps:
- name: 🛒 Checkout
@@ -24,12 +82,23 @@ jobs:
run: export MAESTRO_VERSION=1.40.0; curl -Ls "https://get.maestro.mobile.dev" | bash
- name: Set up JDK 17
if: ${{ inputs.install-java == 'true' }}
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'zulu'
- name: 💎 Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
bundler-cache: true
- name: ⬇️ Download Android Artifacts
uses: actions/download-artifact@v4
with:
name: android-artifacts
path: artifacts/
- name: Enable KVM group perms
shell: bash
run: |
@@ -41,7 +110,7 @@ jobs:
id: run-tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 24
api-level: '24'
arch: x86
ram-size: '8192M'
heap-size: '4096M'
@@ -50,6 +119,11 @@ jobs:
disable-animations: false
avd-name: e2e_emulator
script: |
maestro test maestro-tests/flow.yaml \
--env server_address=${{ env.JELLYFIN_TEST_ADDRESS }} \
--env username=${{ env.JELLYFIN_TEST_USERNAME }}
node scripts/maestro-android.js ${{ env.JELLYFIN_TEST_ADDRESS }} ${{ env.JELLYFIN_TEST_USERNAME }} ${{ env.JELLYFIN_TEST_PASSWORD }}
- name: Store tests result
uses: actions/upload-artifact@v4.3.4
if: always()
with:
name: TestResult
path: |
video.mp4

3
.gitignore vendored
View File

@@ -77,3 +77,6 @@ yarn-error.log
.expo
dist/
web-build/
# Maestro Output
video.mp4

View File

@@ -2,6 +2,9 @@ apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"
apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"
/**
* This is the configuration block to customize your React Native Android app.
* By default you don't need to apply any configuration, just uncomment the lines you need.

View File

@@ -1396,6 +1396,10 @@ PODS:
- Yoga
- react-native-carplay (2.4.1-beta.0):
- React
- react-native-config (1.5.5):
- react-native-config/App (= 1.5.5)
- react-native-config/App (1.5.5):
- React-Core
- react-native-mmkv (3.2.0):
- DoubleConversion
- glog
@@ -2258,6 +2262,7 @@ DEPENDENCIES:
- react-native-blob-util (from `../node_modules/react-native-blob-util`)
- "react-native-blur (from `../node_modules/@react-native-community/blur`)"
- react-native-carplay (from `../node_modules/react-native-carplay`)
- react-native-config (from `../node_modules/react-native-config`)
- react-native-mmkv (from `../node_modules/react-native-mmkv`)
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- react-native-ota-hot-update (from `../node_modules/react-native-ota-hot-update`)
@@ -2405,6 +2410,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-community/blur"
react-native-carplay:
:path: "../node_modules/react-native-carplay"
react-native-config:
:path: "../node_modules/react-native-config"
react-native-mmkv:
:path: "../node_modules/react-native-mmkv"
react-native-netinfo:
@@ -2550,6 +2557,7 @@ SPEC CHECKSUMS:
react-native-blob-util: f6fbaf3935531be0b3c4bcaf46f7bff2802fd93b
react-native-blur: 06d0f9906ecd6cde3a42de16c6cd829a2bf0710c
react-native-carplay: 8f388f6f73e5e0f73ed154ad8794371343ee20c0
react-native-config: 644074ab88db883fcfaa584f03520ec29589d7df
react-native-mmkv: d3cc73d2554fafa20dc5b86386359034d1faf8ff
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
react-native-ota-hot-update: e3c981113fc84b550db8a23d2a2d440bd91d9f4b

View File

@@ -1,6 +1,12 @@
appId: com.jellify
---
- launchApp
- tapOn:
text: "Allow"
optional: true
- tapOn:
text: "Allow"
optional: true
- tapOn:
id: "server_address_input"
- inputText: "${server_address}"
@@ -9,6 +15,9 @@ appId: com.jellify
- assertVisible: "Sign in to Cosmonautical"
- tapOn:
id: "username_input"
- inputText: "${username}"
- tapOn:
id: "password_input"
- inputText: "${password}"
- tapOn:
id: "sign_in_button"

View File

@@ -64,6 +64,7 @@
"react-native-background-actions": "^4.0.1",
"react-native-blob-util": "^0.21.2",
"react-native-carplay": "^2.4.1-beta.0",
"react-native-config": "^1.5.5",
"react-native-device-info": "^14.0.4",
"react-native-dns-lookup": "^1.0.6",
"react-native-draggable-flatlist": "^4.0.3",

View File

@@ -0,0 +1,66 @@
const { execSync, exec, spawn } = require('child_process')
const path = require('path')
// Read arguments from CLI
const [, , serverAddress, username, password] = process.argv
if (!serverAddress || !username || !password) {
console.error('Usage: node runMaestro.js <server_address> <username> <password>')
process.exit(1)
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
async function stopRecording(pid) {
try {
// Kill the adb screenrecord process
process.kill(pid, 'SIGINT')
// Wait 3 seconds for file to finalize
await sleep(3000)
// Pull the recorded file
execSync('adb pull /sdcard/screen.mp4 video.mp4', { stdio: 'inherit' })
// Optionally delete the file on device
execSync('adb shell rm /sdcard/screen.mp4')
console.log('✅ Recording pulled and cleaned up')
} catch (err) {
console.error('❌ Failed to stop or pull recording:', err.message)
}
}
;(async () => {
execSync('adb install ./artifacts/app-x86-release.apk', { stdio: 'inherit', env: process.env })
execSync(`adb shell monkey -p com.jellify 1`, { stdio: 'inherit' })
const recording = spawn('adb', ['shell', 'screenrecord', '/sdcard/screen.mp4'], {
stdio: 'ignore',
detached: true,
})
const pid = recording.pid
try {
const MAESTRO_PATH = path.join(process.env.HOME, '.maestro', 'bin', 'maestro')
const FLOW_PATH = './maestro-tests/flow.yaml'
const command = `${MAESTRO_PATH} test ${FLOW_PATH} \
--env server_address=${serverAddress} \
--env username=${username} \
--env password=${password}`
const output = execSync(command, { stdio: 'inherit', env: process.env })
console.log('✅ Maestro test completed')
console.log(output)
await stopRecording(pid)
process.exit(0)
} catch (error) {
await stopRecording(pid)
execSync('pwd', { stdio: 'inherit' })
console.error(`❌ Error: ${error.message}`)
process.exit(1)
}
})()

66
scripts/updateEnv.js Normal file
View File

@@ -0,0 +1,66 @@
const fs = require('fs')
const path = require('path')
function parseArgs(args) {
const updates = {}
args.forEach((arg) => {
const [key, value] = arg.split('=')
if (key && value !== undefined) {
updates[key.trim()] = value.trim()
}
})
return updates
}
function updateEnvFile(filePath, updates) {
let envContent = ''
try {
envContent = fs.readFileSync(filePath, 'utf8')
} catch (err) {
console.error(`❌ Failed to read .env file at ${filePath}`, err.message)
return
}
const lines = envContent.split(/\r?\n/)
const seenKeys = new Set()
const updatedLines = lines.map((line) => {
if (!line.trim() || line.trim().startsWith('#')) return line
const [key, ...rest] = line.split('=')
const trimmedKey = key.trim()
if (updates.hasOwnProperty(trimmedKey)) {
seenKeys.add(trimmedKey)
return `${trimmedKey}=${updates[trimmedKey]}`
}
return line
})
// Add any new keys not already in the file
Object.entries(updates).forEach(([key, value]) => {
if (!seenKeys.has(key)) {
updatedLines.push(`${key}=${value}`)
}
})
try {
fs.writeFileSync(filePath, updatedLines.join('\n'), 'utf8')
console.log('✅ .env file updated successfully')
} catch (err) {
console.error(`❌ Failed to write .env file:`, err.message)
}
}
// Get CLI args (excluding node and script path)
const args = process.argv.slice(2)
if (args.length === 0) {
console.error('❗ Usage: node updateEnv.js KEY1=value1 KEY2=value2')
process.exit(1)
}
const updates = parseArgs(args)
const envPath = path.resolve(__dirname, '../.env')
updateEnvFile(envPath, updates)

View File

@@ -16,6 +16,7 @@ import { useSettingsContext } from '../../../providers/Settings'
import Icon from '../../Global/components/icon'
import { PublicSystemInfo } from '@jellyfin/sdk/lib/generated-client/models'
import { connectToServer } from '../../../api/mutations/login'
import { IS_MAESTRO_BUILD } from '../../../configs/config'
export default function ServerAddress({
navigation,
@@ -126,6 +127,7 @@ export default function ServerAddress({
onChangeText={setServerAddress}
autoCapitalize='none'
autoCorrect={false}
secureTextEntry={IS_MAESTRO_BUILD} // If Maestro build, don't show the server address as screen Records
flex={1}
placeholder='jellyfin.org'
testID='server_address_input'

View File

@@ -13,6 +13,7 @@ import Icon from '../../Global/components/icon'
import { useJellifyContext } from '../../../providers'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import Toast from 'react-native-toast-message'
import { IS_MAESTRO_BUILD } from '../../../configs/config'
export default function ServerAuthentication({
navigation,
@@ -78,7 +79,11 @@ export default function ServerAuthentication({
prependElement={<Icon name='human-greeting-variant' color={'$borderColor'} />}
placeholder='Username'
value={username}
style={
IS_MAESTRO_BUILD ? { backgroundColor: '#000', color: '#000' } : undefined
}
testID='username_input'
secureTextEntry={IS_MAESTRO_BUILD} // If Maestro build, don't show the username as screen Records
onChangeText={(value: string | undefined) => setUsername(value)}
autoCapitalize='none'
autoCorrect={false}
@@ -90,10 +95,14 @@ export default function ServerAuthentication({
prependElement={<Icon name='lock-outline' color={'$borderColor'} />}
placeholder='Password'
value={password}
testID='password_input'
style={
IS_MAESTRO_BUILD ? { backgroundColor: '#000', color: '#000' } : undefined
}
onChangeText={(value: string | undefined) => setPassword(value)}
autoCapitalize='none'
autoCorrect={false}
secureTextEntry
secureTextEntry={IS_MAESTRO_BUILD} // If Maestro build, don't show the password as screen Records
/>
<Spacer />

View File

@@ -12,6 +12,7 @@ import {
import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated'
import hotUpdate from 'react-native-ota-hot-update'
import DeviceInfo from 'react-native-device-info'
import { OTA_UPDATE_ENABLED } from '../../configs/config'
const version = DeviceInfo.getVersion()
@@ -72,7 +73,8 @@ const GitUpdateModal = () => {
}
useEffect(() => {
if (__DEV__) {
console.log('OTA_UPDATE_ENABLED', OTA_UPDATE_ENABLED)
if (__DEV__ || !OTA_UPDATE_ENABLED) {
return
}
onCheckGitVersion()

6
src/configs/config.ts Normal file
View File

@@ -0,0 +1,6 @@
import Config from 'react-native-config'
const OTA_UPDATE_ENABLED = Config.OTA_UPDATE_ENABLED === 'true'
const IS_MAESTRO_BUILD = Config.IS_MAESTRO_BUILD === 'true'
export { OTA_UPDATE_ENABLED, IS_MAESTRO_BUILD }

9
src/types/react-native-config.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
declare module 'react-native-config' {
export interface NativeConfig {
OTA_UPDATE_ENABLED?: string
IS_MAESTRO_BUILD?: string
}
export const Config: NativeConfig
export default Config
}

View File

@@ -8147,6 +8147,11 @@ react-native-cli-bump-version@^1.5.1:
resolved "https://registry.yarnpkg.com/react-native-cli-bump-version/-/react-native-cli-bump-version-1.5.1.tgz#12bc0aa6328413ed20b7a2599a4a908e0fff9945"
integrity sha512-C7Vss+BBD4iNMnn2YR00cU+GDDPZ+LDmIqWoh3FPwI/LBsJ/Vp5qanwtyVYRPcIe7Cg1PPB8WdeZ8XcnqF5Klw==
react-native-config@^1.5.5:
version "1.5.5"
resolved "https://registry.yarnpkg.com/react-native-config/-/react-native-config-1.5.5.tgz#5977db58ff20b6fe9dd6fb28c815a9ba43d018d2"
integrity sha512-dGdLnBU0cd5xL5bF0ROTmHYbsstZnQKOEPfglvZi1vStvAjpld14X25K6mY3KGPTMWAzx6TbjKeq5dR+ILuMMA==
react-native-device-info@^14.0.4:
version "14.0.4"
resolved "https://registry.yarnpkg.com/react-native-device-info/-/react-native-device-info-14.0.4.tgz#56b24ace9ff29a66bdfc667209086421ed6cfdce"