diff --git a/bun.lock b/bun.lock index efb808490..57ed49719 100644 --- a/bun.lock +++ b/bun.lock @@ -96,7 +96,7 @@ "jscodeshift": "17.3.0", "lint-staged": "17.0.8", "patch-package": "8.0.1", - "prettier": "3.8.4", + "prettier": "3.9.3", "react-dom": "19.2.3", "react-native-cli-bump-version": "1.5.1", "react-test-renderer": "19.2.3", @@ -2014,7 +2014,7 @@ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - "prettier": ["prettier@3.8.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q=="], + "prettier": ["prettier@3.9.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-HWmu+K+zvHNpaMfSnYeqdqrDbR16cuIXaPx8WoHaviQkDJh1/0BNtOZmHVQI5jc3wXv0H1yXc9wjvFdXh+n3hQ=="], "prettier-linter-helpers": ["prettier-linter-helpers@1.0.1", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg=="], diff --git a/jest/functional/maestro-flow-integrity.test.ts b/jest/functional/maestro-flow-integrity.test.ts new file mode 100644 index 000000000..dbdacec74 --- /dev/null +++ b/jest/functional/maestro-flow-integrity.test.ts @@ -0,0 +1,63 @@ +import * as fs from 'fs' +import * as path from 'path' + +/** + * Guards the Maestro suite against broken flow references. + * + * flow-smoke.yaml once referenced maestro/tests/*.yaml files that had been + * deleted in a refactor, which made the smoke suite fail instantly in CI. + * Maestro only resolves runFlow targets at runtime, so without this check a + * dangling reference is invisible until an emulator run. + */ + +const MAESTRO_ROOT = path.join(__dirname, '..', '..', 'maestro') + +const collectYamlFiles = (dir: string): string[] => + fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) return collectYamlFiles(fullPath) + return /\.ya?ml$/.test(entry.name) ? [fullPath] : [] + }) + +/** + * Extracts flow file references from a Maestro YAML file. Handles both the + * inline form (`- runFlow: some/flow.yaml`) and the expanded form where the + * target sits under a `file:` key (`- runFlow:` / ` file: some/flow.yaml`). + */ +const extractFlowRefs = (yamlPath: string): { ref: string; line: number }[] => { + const refPattern = /^\s*(?:-\s*)?(?:runFlow|file):\s*(['"]?)([^\s'"]+\.ya?ml)\1\s*$/ + return fs + .readFileSync(yamlPath, 'utf-8') + .split('\n') + .map((text, index) => { + const match = text.match(refPattern) + return match ? { ref: match[2], line: index + 1 } : undefined + }) + .filter((entry): entry is { ref: string; line: number } => entry !== undefined) +} + +describe('maestro flow integrity', () => { + const yamlFiles = collectYamlFiles(MAESTRO_ROOT) + + it('finds maestro flow files', () => { + expect(yamlFiles.length).toBeGreaterThan(0) + }) + + it('every runFlow reference points to a file that exists', () => { + const brokenRefs = yamlFiles.flatMap((yamlFile) => + extractFlowRefs(yamlFile) + .filter(({ ref }) => !fs.existsSync(path.resolve(path.dirname(yamlFile), ref))) + .map( + ({ ref, line }) => + `${path.relative(MAESTRO_ROOT, yamlFile)}:${line} → ${ref} (missing)`, + ), + ) + + expect(brokenRefs).toEqual([]) + }) + + it('the CI entry point flows exist', () => { + expect(fs.existsSync(path.join(MAESTRO_ROOT, 'flow-full.yaml'))).toBe(true) + expect(fs.existsSync(path.join(MAESTRO_ROOT, 'flow-smoke.yaml'))).toBe(true) + }) +}) diff --git a/maestro/README.md b/maestro/README.md index 34dad6873..95630d3eb 100644 --- a/maestro/README.md +++ b/maestro/README.md @@ -25,7 +25,7 @@ maestro/ ## How it works -The top-level flow files (`flow-full.yaml`, `flow-smoke.yaml`) are the entry points. They call `runFlow` on each `tests/*/flow.yaml` in order. Each `flow.yaml` is responsible for a logical area of the app (setup, home, library, etc.) and in turn calls out to the individual test files within its directory to keep logical groups small and focused. +The top-level flow files (`flow-full.yaml`, `flow-smoke.yaml`) are the entry points. They call `runFlow` on each `flows/*/flow.yaml` in order. Each `flow.yaml` is responsible for a logical area of the app (setup, home, library, etc.) and in turn calls out to the individual test files within its directory to keep logical groups small and focused. For example, `flows/home/flow.yaml` navigates to the home screen and then delegates to `recently-played.yaml` and any other home-specific tests. This keeps individual test files small while the `flow.yaml` files act as coordinators for their feature area. @@ -44,7 +44,17 @@ Runs the complete test suite in sequence: ### `flow-smoke.yaml` -A fast subset intended as a CI gate. Covers login → library browse → album detail → search → settings. Designed to run quickly to catch obvious regressions before a full suite run. +A fast subset intended as a CI gate. Reuses the same `flows/` building blocks as the full suite and covers login → home (recently played → album detail → playback) → search → artist detail. Designed to run quickly to catch obvious regressions before a full suite run. + +### Flow hygiene + +Because tabs in Jellify keep their own navigation stacks, a flow that pushes a +detail screen inside a tab **must pop back to that tab's root before moving +on** — otherwise the next flow that selects the tab lands on the leftover +detail screen instead of the tab root, and selectors like `search-input` +simply do not exist in the hierarchy. Flows that enter the Search tab should +also start with the defensive reset used in `flows/search/gd-search.yaml` +(scroll up, then bounded back-presses re-selecting the tab between presses). ## Running locally diff --git a/maestro/flow-smoke.yaml b/maestro/flow-smoke.yaml index 5ee8f0b09..f7eb0a52f 100644 --- a/maestro/flow-smoke.yaml +++ b/maestro/flow-smoke.yaml @@ -1,22 +1,15 @@ appId: com.cosmonautical.jellify --- -# Smoke test flow — fast CI gate (~2 min) -# Covers: login → library browse → album detail → search → settings -# Full regression (flow-0.yaml) runs on nightly schedule +# Smoke test flow — fast CI gate +# Covers: login → home (recently played → album → playback) → search → artist detail +# Full regression (flow-full.yaml) runs on the nightly schedule and via workflow_dispatch. -- clearState +# Setup clears state, launches the app, handles permission dialogs, +# logs in, and selects a server and library +- runFlow: flows/setup/flow.yaml -# Login flow -- runFlow: tests/1-login.yaml +# Home: recently played → album detail → start playback → miniplayer visible +- runFlow: flows/home/flow.yaml -# Quick library check -- runFlow: tests/2-library.yaml - -# Album detail (verifies indexed testIDs work) -- runFlow: tests/8-album.yaml - -# Search (verifies API connectivity) -- runFlow: tests/3-search.yaml - -# Settings and sign out -- runFlow: tests/7-settings.yaml +# Search: Grateful Dead lookup → artist detail (verifies API connectivity + navigation) +- runFlow: flows/search/flow.yaml diff --git a/maestro/flows/discover/flow.yaml b/maestro/flows/discover/flow.yaml index ab0ce4c67..526847206 100644 --- a/maestro/flows/discover/flow.yaml +++ b/maestro/flows/discover/flow.yaml @@ -11,6 +11,12 @@ appId: com.cosmonautical.jellify # Take screenshot of initial discover screen state - takeScreenshot: screenshots/discover/discover_initial_state_screen +# Give the lazily-mounted tab time to fetch its content on slow CI runners +- extendedWaitUntil: + visible: + id: "discover-recently-added" + timeout: 20000 + # Verify we're on the Discover page - assertVisible: id: "discover-recently-added" diff --git a/maestro/flows/flow-settings.yaml b/maestro/flows/flow-settings.yaml deleted file mode 100644 index 209487934..000000000 --- a/maestro/flows/flow-settings.yaml +++ /dev/null @@ -1,7 +0,0 @@ -appId: com.cosmonautical.jellify ---- -# Focused flow for iterating on the new vertical Settings UX. -# Clears state, logs in, then runs the Settings test suite. -- clearState -- runFlow: ../tests/1-login.yaml -- runFlow: ../tests/7-settings.yaml diff --git a/maestro/flows/quick-actions/flow.yaml b/maestro/flows/quick-actions/flow.yaml index de44036b9..9902532f6 100644 --- a/maestro/flows/quick-actions/flow.yaml +++ b/maestro/flows/quick-actions/flow.yaml @@ -185,9 +185,12 @@ appId: com.cosmonautical.jellify # Wait for search screen - waitForAnimationToEnd -# Type a search query +# Focus the search input by testID — its placeholder text is randomized, so a +# text match on "Search" would hit the tab bar label instead of the input. - tapOn: - text: "Search" + id: "search-input" + +- eraseText - inputText: "music" @@ -216,11 +219,41 @@ appId: com.cosmonautical.jellify id: "quick-action-right-0" optional: true -# If actions appeared, close them -- tapOn: - point: 50%, 40% +# Close any open swipe menu by scrolling — the app closes swipeable rows when +# a scroll begins. A blind coordinate tap here can land on a result row and +# push a detail screen onto the Search stack, which breaks later search flows. +- scroll + +- waitForAnimationToEnd + +# Restore the Search tab to a clean state for later flows: scroll back up to +# the input first, then pop any detail screen that may have been pushed, +# re-selecting the tab after each back-press so this loop can never back out +# of the app. +- scrollUntilVisible: + element: + id: "search-input" + direction: UP + timeout: 20000 optional: true +- repeat: + times: 3 + while: + notVisible: + id: "search-input" + commands: + - pressKey: BACK + - waitForAnimationToEnd + - tapOn: + id: "search-tab-button" + - waitForAnimationToEnd + +# Fail loudly here if the Search tab could not be restored — otherwise the +# poisoned stack would surface later as a confusing failure in the search flow. +- assertVisible: + id: "search-input" + # Return to home - tapOn: id: "home-tab-button" diff --git a/maestro/flows/search/gd-search.yaml b/maestro/flows/search/gd-search.yaml index de33ab077..64b122f40 100644 --- a/maestro/flows/search/gd-search.yaml +++ b/maestro/flows/search/gd-search.yaml @@ -1,12 +1,45 @@ appId: com.cosmonautical.jellify --- -# Quick actions may have brought up some search results already, so scroll to the top of the search tab to ensure we're in a consistent state +# The Search tab may not be showing its root when we arrive: earlier flows can +# leave detail screens (album/artist) pushed on this tab's stack, or leave the +# root list scrolled away from the search input. A pushed screen removes +# search-input from the hierarchy entirely, so scrolling alone can never +# recover it. Recover deterministically instead: + +# 1) If the root is just scrolled down, bring the input back into view. - scrollUntilVisible: element: id: "search-input" direction: UP - timeout: 180000 + timeout: 20000 + optional: true + +# 2) If it's still missing, a detail screen is covering the root — pop it. +# Re-select the tab after every back-press so this loop can never back out +# of the app, whichever tab back navigation lands on. +- repeat: + times: 3 + while: + notVisible: + id: "search-input" + commands: + - pressKey: BACK + - waitForAnimationToEnd + - tapOn: + id: "search-tab-button" + - waitForAnimationToEnd + +# 3) The restored root may itself still be scrolled — final tidy-up. +- scrollUntilVisible: + element: + id: "search-input" + direction: UP + timeout: 20000 + optional: true + +- assertVisible: + id: "search-input" # Tap on the search input to focus it and bring up the keyboard - tapOn: diff --git a/maestro/tests/7-settings.yaml b/maestro/tests/7-settings.yaml deleted file mode 100644 index 01431d5f4..000000000 --- a/maestro/tests/7-settings.yaml +++ /dev/null @@ -1,473 +0,0 @@ -appId: com.cosmonautical.jellify ---- -# Wait for app to be ready, then navigate to Settings tab -- assertVisible: - id: 'settings-tab-button' -- tapOn: - id: 'settings-tab-button' - -# ============================================================ -# HUB — verify all navigation elements by testID, not just text -# ============================================================ -- extendedWaitUntil: - visible: - id: 'settings-screen-root' - timeout: 10000 -- waitForAnimationToEnd - -- assertVisible: - id: 'settings-profile-card' -- assertVisible: - id: 'settings-nav-appearance' -- assertVisible: - id: 'settings-nav-gestures' -- assertVisible: - id: 'settings-nav-playback' -- assertVisible: - id: 'settings-nav-storage' -- assertVisible: - id: 'settings-nav-privacy-developer' -- assertVisible: - id: 'settings-nav-about' -- assertVisible: - id: 'settings-signout-button' -# Also verify the text labels render inside the rows -- assertVisible: - text: 'Appearance' -- assertVisible: - text: 'Gestures' -- assertVisible: - text: 'Playback' -- assertVisible: - text: 'Storage' -- assertVisible: - text: 'Privacy & Developer' -- assertVisible: - text: 'About' -- takeScreenshot: screenshots/19_settings_hub - -# ============================================================ -# APPEARANCE — exercise color scheme toggle, verify with screenshots -# ============================================================ -- tapOn: - id: 'settings-nav-appearance' -- extendedWaitUntil: - visible: - id: 'settings-screen-appearance' - timeout: 10000 -- waitForAnimationToEnd - -- assertVisible: - text: 'Theme' -- assertVisible: - text: 'Match Device' -- assertVisible: - text: 'Light' -- assertVisible: - text: 'Dark' -- assertVisible: - text: 'OLED Black' -- assertVisible: - text: 'Color Scheme' -- assertVisible: - text: 'Purple' -- assertVisible: - text: 'Ocean' -- assertVisible: - text: 'Forest' -- assertVisible: - text: 'Sunset' -- assertVisible: - text: 'Peanut' -- takeScreenshot: screenshots/20_settings_appearance - -# Toggle to Ocean, screenshot the state change, then restore -- tapOn: - text: 'Ocean' -- takeScreenshot: screenshots/20a_appearance_ocean_selected -- tapOn: - text: 'Purple' -- takeScreenshot: screenshots/20b_appearance_purple_restored - -# Scroll down to verify Hide Runtimes switch is reachable -- scroll: - direction: DOWN -- assertVisible: - text: 'Hide Runtimes' - optional: true -- takeScreenshot: screenshots/20c_appearance_scrolled - -# Return to hub via back button (tests Android back navigation) -- pressKey: back -- waitForAnimationToEnd -- extendedWaitUntil: - visible: - id: 'settings-screen-root' - timeout: 5000 - -# ============================================================ -# GESTURES — verify chip state changes with screenshots -# ============================================================ -- assertVisible: - id: 'settings-screen-root' -- tapOn: - id: 'settings-nav-gestures' -- extendedWaitUntil: - visible: - id: 'settings-screen-gestures' - timeout: 10000 -- waitForAnimationToEnd - -- assertVisible: - text: 'Swipe Left' -- assertVisible: - text: 'Swipe Right' -# All eight action chips present (use testIDs — Tamagui renders icons -# inside the button, making iOS accessibility labels unreliable for text match) -- assertVisible: - id: 'swipe-left-chip-favorite' -- assertVisible: - id: 'swipe-left-chip-add-to-playlist' -- assertVisible: - id: 'swipe-left-chip-add-to-queue' -- assertVisible: - id: 'swipe-left-chip-play-next' -- assertVisible: - id: 'swipe-right-chip-favorite' -- assertVisible: - id: 'swipe-right-chip-add-to-playlist' -- assertVisible: - id: 'swipe-right-chip-add-to-queue' -- assertVisible: - id: 'swipe-right-chip-play-next' -- takeScreenshot: screenshots/21_settings_gestures - -# Toggle left Play Next: enable → screenshot → disable → screenshot -- tapOn: - id: 'swipe-left-chip-play-next' -- takeScreenshot: screenshots/21a_gestures_play_next_on -- tapOn: - id: 'swipe-left-chip-play-next' -- takeScreenshot: screenshots/21b_gestures_play_next_off - -# Also exercise a right-side chip to test the other store branch -- tapOn: - id: 'swipe-right-chip-play-next' -- tapOn: - id: 'swipe-right-chip-play-next' - -# Return to hub via back -- pressKey: back -- waitForAnimationToEnd -- extendedWaitUntil: - visible: - id: 'settings-screen-root' - timeout: 5000 - -# ============================================================ -# PLAYBACK — exercise radio buttons and verify labels -# ============================================================ -- assertVisible: - id: 'settings-screen-root' -- tapOn: - id: 'settings-nav-playback' -- extendedWaitUntil: - visible: - id: 'settings-screen-playback' - timeout: 10000 -- waitForAnimationToEnd - -- assertVisible: - text: 'Streaming Quality' -- assertVisible: - text: 'Original Quality' -- assertVisible: - text: 'High (320kbps)' -- assertVisible: - text: 'Medium (192kbps)' -- assertVisible: - text: 'Low (128kbps)' -- takeScreenshot: screenshots/22_settings_playback - -# Exercise quality radio: tap High, screenshot, restore to Original -- tapOn: - text: 'High (320kbps)' -- takeScreenshot: screenshots/22a_playback_high_selected -- tapOn: - text: 'Original Quality' - -# Scroll to verify switches are reachable below the radio group -- scroll: - direction: DOWN -- assertVisible: - text: 'Audio Normalization' -- assertVisible: - text: 'Quality Badge' -- takeScreenshot: screenshots/22b_playback_scrolled - -# Return to hub via back -- pressKey: back -- waitForAnimationToEnd -- extendedWaitUntil: - visible: - id: 'settings-screen-root' - timeout: 5000 - -# ============================================================ -# STORAGE — uses dedicated testID, scroll to download settings -# ============================================================ -- assertVisible: - id: 'settings-screen-root' -- tapOn: - id: 'settings-nav-storage' -- extendedWaitUntil: - visible: - id: 'settings-screen-storage' - timeout: 10000 -- waitForAnimationToEnd - -- assertVisible: - text: 'Storage overview' -- takeScreenshot: screenshots/23_settings_storage - -# Download Settings lives below the StorageSummaryCard — scroll to it -- scroll: - direction: DOWN -- scroll: - direction: DOWN -- assertVisible: - text: 'Download Settings' -- assertVisible: - text: 'Auto-Download Tracks' -- assertVisible: - text: 'Download Quality' -- takeScreenshot: screenshots/23a_storage_download_settings - -# Return to hub via tab bar (keep one instance to test both nav paths) -- tapOn: - id: 'settings-tab-button' -- waitForAnimationToEnd -- extendedWaitUntil: - visible: - id: 'settings-screen-root' - timeout: 5000 - -# ============================================================ -# PRIVACY & DEVELOPER — toggle Developer Options to reveal PR input -# ============================================================ -- assertVisible: - id: 'settings-screen-root' -- tapOn: - id: 'settings-nav-privacy-developer' -- extendedWaitUntil: - visible: - id: 'settings-screen-privacy-developer' - timeout: 10000 -- waitForAnimationToEnd - -- assertVisible: - text: 'Send Analytics' -- assertVisible: - text: 'Reduce Haptics' -- assertVisible: - text: 'Developer Options' -- takeScreenshot: screenshots/24_settings_privacy_developer - -# Toggle Developer Options on — should reveal the PR ID input -- tapOn: - id: 'developer-options-switch' -- scroll: - direction: DOWN -- assertVisible: - text: 'Enter PR ID' -- takeScreenshot: screenshots/24a_developer_options_enabled - -# Toggle Developer Options off — PR input should disappear -- scroll: - direction: UP -- tapOn: - id: 'developer-options-switch' -- assertNotVisible: - text: 'Enter PR ID' -- takeScreenshot: screenshots/24b_developer_options_disabled - -# Return to hub via back -- pressKey: back -- waitForAnimationToEnd -- extendedWaitUntil: - visible: - id: 'settings-screen-root' - timeout: 5000 - -# ============================================================ -# ABOUT — verify app name, links, scroll to patrons -# ============================================================ -- assertVisible: - id: 'settings-screen-root' -- tapOn: - id: 'settings-nav-about' -- extendedWaitUntil: - visible: - id: 'settings-screen-about' - timeout: 10000 -- waitForAnimationToEnd - -- assertVisible: - text: 'Jellify' -- assertVisible: - text: 'View Source' -- assertVisible: - text: 'Caught a bug?' -- assertVisible: - text: 'Report Issue' -- assertVisible: - text: 'Join Discord' -- takeScreenshot: screenshots/25_settings_about - -# Scroll to Wall of Fame / patrons section -- scroll: - direction: DOWN -- assertVisible: - text: 'Wall of Fame' -- takeScreenshot: screenshots/25a_about_scrolled - -# Return to hub via back -- pressKey: back -- waitForAnimationToEnd -- extendedWaitUntil: - visible: - id: 'settings-screen-root' - timeout: 5000 - -# ============================================================ -# ACCOUNT — via profile card, verify server info chips -# ============================================================ -- assertVisible: - id: 'settings-screen-root' -- tapOn: - id: 'settings-profile-card' -- extendedWaitUntil: - visible: - id: 'settings-screen-account' - timeout: 10000 -- waitForAnimationToEnd - -- assertVisible: - text: 'Music Library' -- assertVisible: - text: 'Server' -- assertVisible: - text: 'Jellyfin User' -- assertVisible: - id: 'account-change-library-button' - -# Scroll to server info chips -- scroll: - direction: DOWN -- assertVisible: - text: 'Version' -- assertVisible: - text: 'Connection' -- takeScreenshot: screenshots/26_settings_account - -# Exercise library selection navigation and return -- scroll: - direction: UP -- tapOn: - id: 'account-change-library-button' -- extendedWaitUntil: - visible: - id: 'library_selection_screen' - timeout: 10000 -- waitForAnimationToEnd -- takeScreenshot: screenshots/27_library_selection - -# Back from library selection → account → hub -- pressKey: back -- waitForAnimationToEnd -- pressKey: back -- waitForAnimationToEnd -- extendedWaitUntil: - visible: - id: 'settings-screen-root' - timeout: 5000 - -# ============================================================ -# RAPID PUSH/POP STRESS TEST — catch race conditions -# ============================================================ -- assertVisible: - id: 'settings-screen-root' -# Navigate to each screen and immediately back — no waits in between -- tapOn: - id: 'settings-nav-appearance' -- pressKey: back -- tapOn: - id: 'settings-nav-gestures' -- pressKey: back -- tapOn: - id: 'settings-nav-playback' -- pressKey: back -- tapOn: - id: 'settings-nav-storage' -- pressKey: back -- tapOn: - id: 'settings-nav-privacy-developer' -- pressKey: back -- tapOn: - id: 'settings-nav-about' -- pressKey: back -# Let all animations settle and verify the hub is stable -- waitForAnimationToEnd -- extendedWaitUntil: - visible: - id: 'settings-screen-root' - timeout: 10000 -- assertVisible: - id: 'settings-profile-card' -- takeScreenshot: screenshots/30_stress_test_hub_stable - -# ============================================================ -# PERSISTENCE VERIFICATION — re-enter Appearance, confirm state held -# ============================================================ -- tapOn: - id: 'settings-nav-appearance' -- extendedWaitUntil: - visible: - id: 'settings-screen-appearance' - timeout: 10000 -- waitForAnimationToEnd -- assertVisible: - text: 'Purple' -- takeScreenshot: screenshots/31_persistence_check -- pressKey: back -- waitForAnimationToEnd -- extendedWaitUntil: - visible: - id: 'settings-screen-root' - timeout: 5000 - -# ============================================================ -# SIGN OUT (cancel) — use Cancel button for deterministic dismissal -# ============================================================ -- assertVisible: - id: 'settings-screen-root' -- tapOn: - id: 'settings-signout-button' -# Wait for the formSheet modal to appear -- extendedWaitUntil: - visible: - text: 'Cancel' - timeout: 5000 -- takeScreenshot: screenshots/28_signout_confirm - -# Tap Cancel — calls navigation.goBack(), deterministic across platforms -- tapOn: - text: 'Cancel' -- waitForAnimationToEnd -- extendedWaitUntil: - visible: - id: 'settings-screen-root' - timeout: 5000 -- assertVisible: - id: 'settings-screen-root' -- takeScreenshot: screenshots/29_settings_after_cancel_signout diff --git a/scripts/maestro-android.js b/scripts/maestro-android.js index 67dffbd5a..0a291f775 100644 --- a/scripts/maestro-android.js +++ b/scripts/maestro-android.js @@ -11,8 +11,8 @@ if (!serverAddress || !username) { } // Use the orchestrated flow file instead of individual tests -// flow-0.yaml clears state and runs all tests in the correct order -const FLOW_FILE = './maestro/flows/flow-0.yaml' +// flow-full.yaml clears state and runs all tests in the correct order +const FLOW_FILE = './maestro/flow-full.yaml' function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)) diff --git a/src/api/queries/track/index.ts b/src/api/queries/track/index.ts index b304e1031..091796b8e 100644 --- a/src/api/queries/track/index.ts +++ b/src/api/queries/track/index.ts @@ -199,8 +199,7 @@ function isDownloadedTrackAlsoFavorite( if (!user) return false const userData = queryClient.getQueryData(UserDataQueryKey(user!, trackId!)) as - | UserItemDataDto - | undefined + UserItemDataDto | undefined return userData?.IsFavorite ?? false } diff --git a/src/components/Global/helpers/animated-row.tsx b/src/components/Global/helpers/animated-row.tsx index 93a3efcd6..c31eb4b82 100644 --- a/src/components/Global/helpers/animated-row.tsx +++ b/src/components/Global/helpers/animated-row.tsx @@ -1,4 +1,4 @@ -import { StyleSheet } from 'react-native' +import { StyleSheet, View } from 'react-native' import Animated, { FadeIn, FadeOut, @@ -26,7 +26,12 @@ export default function AnimatedRow({ children, testID }: AnimatedRowProps) { {children} ) : ( - children + // Keep the testID and layout when animations are off — reduced motion + // is reported as true on emulators with animations disabled (CI), and + // dropping the testID here makes these rows invisible to Maestro. + + {children} + ) } diff --git a/src/components/Library/tab-bar.tsx b/src/components/Library/tab-bar.tsx index 7552d4a23..245ec01e5 100644 --- a/src/components/Library/tab-bar.tsx +++ b/src/components/Library/tab-bar.tsx @@ -18,10 +18,7 @@ function LibraryTabBar(props: MaterialTopTabBarProps) { const insets = useSafeAreaInsets() const currentTab = props.state.routes[props.state.index].name as - | 'Tracks' - | 'Albums' - | 'Artists' - | 'Playlists' + 'Tracks' | 'Albums' | 'Artists' | 'Playlists' // Subscribe directly to the current tab's filter state for reactivity const currentFilters = useLibraryStore((state) => { diff --git a/src/components/theme.ts b/src/components/theme.ts index cc9ed393a..baa442864 100644 --- a/src/components/theme.ts +++ b/src/components/theme.ts @@ -13,17 +13,7 @@ interface Fonts { interface FontStyle { fontFamily: string fontWeight: - | 'normal' - | 'bold' - | '200' - | '900' - | '100' - | '500' - | '300' - | '400' - | '600' - | '700' - | '800' + 'normal' | 'bold' | '200' | '900' | '100' | '500' | '300' | '400' | '600' | '700' | '800' } const JellifyFonts: Fonts = {