mirror of
https://github.com/Jellify-Music/App.git
synced 2025-12-18 01:05:17 -06:00
Maestro Setup for app (#407)
Implementing Maestro tests against onboarding process
This commit is contained in:
86
.github/workflows/maestro-test.yml
vendored
86
.github/workflows/maestro-test.yml
vendored
@@ -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
3
.gitignore
vendored
@@ -77,3 +77,6 @@ yarn-error.log
|
||||
.expo
|
||||
dist/
|
||||
web-build/
|
||||
|
||||
# Maestro Output
|
||||
video.mp4
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
66
scripts/maestro-android.js
Normal file
66
scripts/maestro-android.js
Normal 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
66
scripts/updateEnv.js
Normal 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)
|
||||
@@ -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'
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
6
src/configs/config.ts
Normal 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
9
src/types/react-native-config.d.ts
vendored
Normal 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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user