test(e2e): guard the sheet, track, locale, and TV settings behaviour

Four regression flows, each run on a Pixel 7 and, where relevant, a real
Android TV box.

Discriminating — the baseline fails, HEAD passes:

`07_sheet_back_dismiss` pins both halves of the hosted-sheet fix on touch: the
barrier removes the rows behind it from the semantics tree, and one Back closes
only the sheet while Settings stays the current route, including the nested
per-library options page. It fails on c48cbf70, the commit before 8e1904dd.
`03_tv_library_focus` gains the same occlusion assertion for the TV sort sheet
and fails there too.

`08_track_choice_survives_pending_pass` picks a non-default audio and subtitle
track, lets playback outlive the automatic pass's 5s attempt and 25s deadline,
and asserts the choice is still selected. On a56b9a3d the audio reverts to the
container's default. Both new flows onboard from a cleared install: the TV
regressions in the same group leave "Force TV mode" enabled, and a remembered
track selection would pre-select the rows under test.

Coverage without a comparable baseline:

`09_language_picker_locales` asserts the four new endonyms, switches to Turkish,
reads root navigation labels from the generated locale, and restores English.
The locales do not exist before 7677d159/100d7729, so there is nothing to fail
against — this is forward coverage, not a reproduction.

`10_tv_settings_navigation` runs on real Android TV hardware, which no existing
flow covers: the rail layout a device reports on its own, the TV dialog path for
Manage Libraries, the Apple-only Atmos gate staying closed on Android, and the
D-pad-only route to the number spinner's accessibility labels, which a single
tap would hide. It passes on either side of the range and is verified against
both an empty server and one with a resume position, since rail order shifts
with that. It does not assert the 15b54e2e row density: that change is invisible
to a semantics-tree driver, and a pixel `height` assertion would only hold for
one DPR. It registers under a new `android-tv-device` group no workflow
dispatches.

No TV playback flow is included. Entering content on the TV Recommended view has
no stable anchor: the accessibility `focused` flag sits on the hero backdrop
rather than the rail card, and a resume position anywhere in the library pushes
"Recently Added" below the fold and out of the semantics tree entirely. Search
is not a way around it either — inputText does not reach the TV search field.
Covering TV playback needs a testID on the rail card, not a cleverer selector.

Also repairs three assertions that could never fail:

`03_tv_library_focus` gated the sort sheet closing on `notVisible: "Sort by"`,
but the header renders "Sort By" and Maestro selectors are case-sensitive
regexes, so the wait returned immediately and the next D-pad press landed in the
sheet's close animation.

`open_codec_sample` matched a card's watch state as `watched|unwatched` only. A
codec sample keeps a resume position once any earlier flow has played it, so the
row announces "N percent watched" and the subflow stopped finding it on a
fixture container that outlives one suite.

`06_playback_recovery` tapped "Zulu Zone" out of the Recently Added rail, but
every seeded alphabet title shares one dateadded, so which of them the rail
returns is a tie-break. The flow only needs some playable movie.
This commit is contained in:
edde746
2026-07-27 13:31:51 +02:00
parent bae6055123
commit 41c6389cd4
10 changed files with 590 additions and 5 deletions
@@ -22,14 +22,23 @@ tags:
- pressKey: "Remote Dpad Up"
- pressKey: "Remote Dpad Right"
- pressKey: "Remote Dpad Right"
# The grid row must be matchable before the sort sheet opens, otherwise the
# occlusion assertion below would pass for the wrong reason.
- extendedWaitUntil:
visible: "(?s).*Alpha Archive.*"
timeout: 15000
- pressKey: "Remote Dpad Center"
- extendedWaitUntil:
visible: "Title"
timeout: 10000
# The host renders the grid, the barrier, and the sheet in one Stack and never
# unmounts the grid, so before 8e1904dd wrapped the barrier in BlockSemantics
# an occluded row still read as visible to accessibility and to Maestro.
- assertNotVisible: "(?s).*Alpha Archive.*"
- pressKey: "Remote Dpad Center"
- pressKey: "Remote Dpad Left"
- extendedWaitUntil:
notVisible: "Sort by"
notVisible: "Sort By"
timeout: 10000
- pressKey: "Remote Dpad Down"
- extendedWaitUntil:
@@ -8,10 +8,14 @@ tags:
---
- runFlow: ../subflows/onboard_jellyfin.yaml
- tapOn: "(?s)^Libraries.*"
# Every seeded alphabet title carries the same dateadded, so which of them the
# "Recently Added" rail returns is not stable across scans. This flow only
# needs some playable movie, so accept any of them rather than pinning one and
# failing whenever the tie-break lands elsewhere.
- extendedWaitUntil:
visible: "(?s).*Zulu Zone.*"
visible: "(?s).*(?:Alpha Archive|Bravo Beacon|Charlie Circuit|Delta Drive|Echo Engine|Foxtrot Frame|Gamma Garden|Hotel Horizon|India Index|Juliet Junction|Kilo Key|Lima Loop|Mike Matrix|November Node|Oscar Orbit|Papa Pipeline|Quebec Queue|Romeo Relay|Sierra Signal|Tango Track|Uniform Update|Victor View|Whiskey Widget|Xray XML|Yankee Yield|Zulu Zone).*, movie, .*"
timeout: 15000
- tapOn: "(?s).*Zulu Zone.*"
- tapOn: "(?s).*(?:Alpha Archive|Bravo Beacon|Charlie Circuit|Delta Drive|Echo Engine|Foxtrot Frame|Gamma Garden|Hotel Horizon|India Index|Juliet Junction|Kilo Key|Lima Loop|Mike Matrix|November Node|Oscar Orbit|Papa Pipeline|Quebec Queue|Romeo Relay|Sierra Signal|Tango Track|Uniform Update|Victor View|Whiskey Widget|Xray XML|Yankee Yield|Zulu Zone).*, movie, .*"
- extendedWaitUntil:
visible: "Play"
timeout: 15000
@@ -0,0 +1,94 @@
appId: com.edde746.plezy
name: System back dismisses a hosted sheet without leaving Settings
tags:
- e2e
- regression
- settings
- sheets
---
# Guards 8e1904dd and c48cbf70 together.
#
# On a phone, Settings is a pushed route. Before c48cbf70 it carried no
# OverlaySheetHost, so Manage Libraries fell back to showModalBottomSheet and a
# single Back tore down the sheet *and* Settings. Before 8e1904dd the host
# answered the platform pop with a TV-only dedup marker that could already be
# spent, stranding the sheet open with no way out. Either defect fails the
# "Manage Libraries is still there after one Back" assertion below.
#
# The same commit blocks semantics behind the barrier, so the occlusion
# assertions pin that an open sheet actually hides the rows underneath.
# Pristine state on purpose. The TV regressions in the same CI group leave
# "Force TV mode" enabled, and this flow asserts the handheld Settings route,
# so it must not inherit whichever layout the previous flow left behind.
- runFlow: ../subflows/onboard_jellyfin.yaml
- runFlow: ../subflows/open_settings.yaml
- assertVisible: "(?s)^Services.*"
- assertVisible: "(?s)^Video Playback.*"
- tapOn: "(?s)^Manage Libraries.*"
- extendedWaitUntil:
visible: "Maestro Movies"
timeout: 15000
# The barrier swallows every pointer event, so the rows behind it must leave
# the semantics tree too. Without BlockSemantics an occluded row still reads as
# visible and a tap on it lands on the barrier instead. "Manage Libraries" is
# excluded on purpose: it is also the sheet's own title.
- assertNotVisible: "(?s)^Services.*"
- assertNotVisible: "(?s)^Video Playback.*"
- assertNotVisible: "(?s)^Appearance.*"
- assertNotVisible: "(?s)^Connections.*"
- back
- waitForAnimationToEnd:
timeout: 3000
- assertNotVisible: "Maestro Movies"
# One Back closed only the sheet: Settings is still the current route.
- extendedWaitUntil:
visible: "(?s)^Manage Libraries.*"
timeout: 10000
- assertVisible: "(?s)^Services.*"
- assertVisible: "(?s)^Video Playback.*"
- assertNotVisible: "Discover"
# Reopening proves the host did not strand the one-shot dedup marker: a second
# dismissal has to work exactly like the first.
- tapOn: "(?s)^Manage Libraries.*"
- extendedWaitUntil:
visible: "Maestro Movies"
timeout: 15000
# The per-library menu is a nested page of the same sheet now that the host
# exists, so Back returns to the library list instead of tearing the sheet down.
- tapOn: "Library options"
- extendedWaitUntil:
visible: "(?s).*Refresh Metadata.*"
timeout: 15000
- back
- waitForAnimationToEnd:
timeout: 3000
- extendedWaitUntil:
visible: "Maestro Movies"
timeout: 10000
- assertNotVisible: "(?s).*Refresh Metadata.*"
# Back at the library list, one more Back leaves the sheet entirely.
- back
- waitForAnimationToEnd:
timeout: 3000
- assertNotVisible: "Maestro Movies"
- extendedWaitUntil:
visible: "(?s)^Manage Libraries.*"
timeout: 10000
- tapOn: "(?s)^Manage Libraries.*"
- extendedWaitUntil:
visible: "Maestro Movies"
timeout: 15000
- back
- waitForAnimationToEnd:
timeout: 3000
- assertNotVisible: "Maestro Movies"
- extendedWaitUntil:
visible: "(?s)^Manage Libraries.*"
timeout: 10000
# With no sheet open the host must let the route pop normally.
- back
- extendedWaitUntil:
visible: "Discover"
timeout: 15000
- assertNotVisible: "(?s)^Manage Libraries.*"
@@ -0,0 +1,112 @@
appId: com.edde746.plezy
name: An explicit track choice survives the pending automatic pass
tags:
- e2e
- regression
- playback
- subtitles
---
# Guards 468d6804. When a source advertises tracks the native list has not
# produced yet, TrackManager arms an automatic selection pass with a 5s first
# attempt and a 25s deadline. A user pick made inside that window used to only
# persist the preference, leaving the pass armed, so the choice snapped back to
# the server/profile preferred track up to ~30s later. The pick now retires the
# pending pass, and the wait below is long enough to outlive both timers.
#
# Fixture: Codec H264 EAC3 Multisub advertises three E-AC-3 audio streams
# (Hindi and Japanese flagged default, English not) and 36 SRT subtitle
# streams, so both picks below are genuine changes away from the automatic one.
# Pristine state on purpose, for two reasons: the TV regressions in the same
# CI group leave "Force TV mode" enabled, and a remembered track selection
# from an earlier run would pre-select the very rows this flow picks, making
# every assertion below vacuous.
- runFlow: ../subflows/onboard_jellyfin.yaml
- runFlow:
file: ../subflows/open_codec_sample.yaml
env:
SAMPLE_TITLE: "Codec H264 EAC3 Multisub"
SAMPLE_OVERVIEW: "H.264 video with three E-AC-3 audio tracks and a broad multilingual SRT subtitle set."
- assertVisible: "(?is)^English\\s+[^,]*(?:Dolby Digital Plus|E-AC3|Surround|5\\.1)[^,]*$"
- tapOn:
text: "(?is)^English\\s+[^,]*(?:Dolby Digital Plus|E-AC3|Surround|5\\.1)[^,]*$"
- extendedWaitUntil:
visible: "(?s)^.*Audio & Subtitles$"
timeout: 30000
- tapOn: "(?s)^.*Audio & Subtitles$"
# German is the only German subtitle in the set and is never the automatic
# pick, so a revert to the preferred language or to Off is unambiguous.
- repeat:
times: 12
while:
notVisible: "(?is)^German\\s+[^,]*(?:SRT|APPLICATION/X-SUBRIP)[^,]*$"
commands:
- swipe:
start: 70%, 82%
end: 70%, 68%
duration: 300
- tapOn:
text: "(?is)^German\\s+[^,]*(?:SRT|APPLICATION/X-SUBRIP)[^,]*$"
- extendedWaitUntil:
visible: "(?s)^.*Audio & Subtitles$"
timeout: 30000
# The pending pass is wall-clock driven and a paused frame settles instantly,
# so playback has to run for the wait below to consume real time.
- runFlow:
when:
visible: "Play"
commands:
- tapOn: "Play"
- extendedWaitUntil:
visible: "Pause"
timeout: 20000
- repeat:
times: 14
commands:
- waitForAnimationToEnd:
timeout: 3000
# Chrome auto-hides after 30s even in Maestro builds, so bring it back.
- tapOn:
point: 50%, 50%
- extendedWaitUntil:
visible: "(?s)^.*Audio & Subtitles$"
timeout: 30000
# The tracks button announces the live selection, so this catches a revert
# without reopening the sheet.
- assertVisible: "(?is)^English[^,]*(?:Dolby Digital Plus|E-AC3|Surround|5\\.1)[^,]*,.*Audio & Subtitles$"
- tapOn: "(?s)^.*Audio & Subtitles$"
- assertVisible:
text: "(?is)^English\\s+[^,]*(?:Dolby Digital Plus|E-AC3|Surround|5\\.1)[^,]*$"
selected: true
- assertNotVisible:
text: "(?is)^Japanese\\s+[^,]*(?:Dolby Digital Plus|E-AC3|Surround|5\\.1)[^,]*$"
selected: true
- repeat:
times: 12
while:
notVisible: "(?is)^German\\s+[^,]*(?:SRT|APPLICATION/X-SUBRIP)[^,]*$"
commands:
- swipe:
start: 70%, 82%
end: 70%, 68%
duration: 300
- assertVisible:
text: "(?is)^German\\s+[^,]*(?:SRT|APPLICATION/X-SUBRIP)[^,]*$"
selected: true
- assertNotVisible:
text: "Off"
selected: true
- back
- waitForAnimationToEnd:
timeout: 3000
- back
- waitForAnimationToEnd:
timeout: 3000
- runFlow:
when:
notVisible: "Overview"
commands:
- back
- extendedWaitUntil:
visible: "Overview"
timeout: 15000
@@ -0,0 +1,90 @@
appId: com.edde746.plezy
name: Language picker offers the new locales and switching one relocalizes the app
tags:
- e2e
- regression
- settings
- i18n
---
# Guards 7677d159 and 100d7729. AppLocale is switched over exhaustively in
# appearance_settings_screen.dart, so a missing arm is a compile error, but a
# locale whose JSON never reached the generator ships a picker entry that falls
# back to English. Selecting Turkish and reading root-level navigation labels
# proves the generated locale is wired end to end.
#
# This flow mutates a persisted pref, and the app language decides every
# selector after the tap, so it starts from a cleared install rather than
# ensure_onboarded: a run that dies mid-flow must not leave the next flow
# hunting for English strings in a Turkish tree.
- runFlow: ../subflows/onboard_jellyfin.yaml
- runFlow: ../subflows/open_settings.yaml
- tapOn: "(?s)^Appearance.*"
- extendedWaitUntil:
visible: "(?s).*Theme.*"
timeout: 15000
- tapOn: "(?s)^Language.*"
- extendedWaitUntil:
visible: "Azərbaycanca"
timeout: 10000
- assertVisible: "English"
- assertVisible: "Қазақша"
# The dialog holds 22 endonyms and only the first 16 fit on a phone, so the two
# locales that sort last need scrolling into view.
- repeat:
times: 8
while:
notVisible: "Türkçe"
commands:
- swipe:
start: 50%, 80%
end: 50%, 45%
duration: 300
- assertVisible: "Türkçe"
- assertVisible: "Oʻzbekcha"
- tapOn: "Türkçe"
# Selecting a language rebuilds the app from the root route, so everything
# below runs against the Turkish tree.
- extendedWaitUntil:
visible: "(?s)^Kitaplıklar.*"
timeout: 30000
- assertVisible: "(?s)^Ana Sayfa.*"
- assertVisible: "(?s)^Ara.*"
- assertVisible: "(?s)^İndirmeler.*"
- assertVisible: "Keşfet"
- assertNotVisible: "(?s)^Libraries.*"
- assertNotVisible: "Discover"
# Walk back through the Turkish tree and restore English. The pref outlives a
# plain launchApp, so leaving the device in Turkish would break every later
# flow in the suite.
- repeat:
times: 3
while:
notVisible: "Ayarlar"
commands:
- tapOn: "(?s)^M(?:.*Profiller.*)?$"
- waitForAnimationToEnd:
timeout: 3000
- tapOn:
text: "Ayarlar"
below:
text: "Profiller"
- extendedWaitUntil:
visible: "(?s)^Görünüm.*"
timeout: 15000
- assertVisible: "(?s)^Servisler.*"
- assertVisible: "(?s)^Video Oynatma.*"
- tapOn: "(?s)^Görünüm.*"
- extendedWaitUntil:
visible: "(?s).*Tema.*"
timeout: 15000
- tapOn: "(?s)^Dil.*"
- extendedWaitUntil:
visible: "English"
timeout: 10000
- tapOn: "English"
- extendedWaitUntil:
visible: "(?s)^Libraries.*"
timeout: 30000
- assertVisible: "Discover"
- assertNotVisible: "(?s)^Kitaplıklar.*"
@@ -0,0 +1,142 @@
appId: com.edde746.plezy
name: TV settings stay navigable and dismiss their overlays
tags:
- e2e
- regression
- tv
- settings
---
# Guards ef310459 and the Apple-only gates touched by 6c14049e, and pins TV
# settings navigation generally.
#
# It deliberately does NOT guard the 15b54e2e density change. That commit took
# every settings row from ~80dp to ~61dp, but the rows below the fold stay in
# the semantics tree either way, so this flow passes identically on both sides
# of it — measured, not assumed. A real guard would need a pixel `height`
# assertion, and Maestro has no DPR-relative form, so pinning one here would
# only hold for the box it was written on. Row geometry belongs to
# test/widgets/setting_tile_test.dart.
#
# Every step uses D-pad keys, never taps. InputModeTracker flips to pointer
# mode on the first touch event and the TV number spinner only builds in
# keyboard mode, so a single tapOn anywhere above would silently swap the
# spinner for a plain text field.
- runFlow: ../subflows/onboard_jellyfin_tv_device.yaml
- pressKey: "Remote Dpad Left"
- extendedWaitUntil:
visible: "Settings"
timeout: 15000
- repeat:
times: 12
while:
notVisible:
text: "Settings"
focused: true
commands:
- pressKey: "Remote Dpad Down"
- pressKey: "Remote Dpad Center"
- extendedWaitUntil:
visible: "(?s)^Appearance.*"
timeout: 15000
- assertVisible: "(?s)^Video Playback.*"
- assertVisible: "(?s)^Manage Libraries.*"
- assertVisible: "(?s)^Services.*"
# The Connections card sits below the first card, so reaching it also proves
# the list scrolled or fits — not that any particular row height applies.
- assertVisible: "(?s)^Add connection.*"
- assertVisible: "(?s)^Profiles.*"
# Manage Libraries takes the TV dialog path, not the overlay-sheet path, and
# one Back has to return to Settings rather than leave it.
- repeat:
times: 6
while:
notVisible:
text: "(?s)^Manage Libraries.*"
focused: true
commands:
- pressKey: "Remote Dpad Down"
- pressKey: "Remote Dpad Center"
- extendedWaitUntil:
visible: "Maestro Movies"
timeout: 15000
- assertVisible: "Maestro Shows"
- assertNotVisible: "(?s)^Services.*"
- assertNotVisible: "(?s)^Video Playback.*"
- pressKey: "back"
- waitForAnimationToEnd:
timeout: 3000
- assertNotVisible: "Maestro Movies"
- extendedWaitUntil:
visible: "(?s)^Services.*"
timeout: 15000
- assertVisible: "(?s)^Manage Libraries.*"
# Services keeps every hub row after losing its own density overrides.
- repeat:
times: 6
while:
notVisible:
text: "(?s)^Services.*"
focused: true
commands:
- pressKey: "Remote Dpad Down"
- pressKey: "Remote Dpad Center"
- extendedWaitUntil:
visible: "(?s)^Trakt.*"
timeout: 15000
- assertVisible: "(?s)^MyAnimeList.*"
- assertVisible: "(?s)^AniList.*"
- assertVisible: "(?s)^Simkl.*"
- assertVisible: "(?s)^Seerr.*"
- pressKey: "back"
- extendedWaitUntil:
visible: "(?s)^Manage Libraries.*"
timeout: 15000
# Video Playback: the Atmos test screen is gated to Apple TV, and the D-pad
# numeric dialog must expose the spinner's restored accessibility labels.
- repeat:
times: 8
while:
notVisible:
text: "(?s)^Video Playback.*"
focused: true
commands:
- pressKey: "Remote Dpad Up"
- pressKey: "Remote Dpad Center"
- extendedWaitUntil:
visible: "(?s)^Player.*"
timeout: 15000
- assertNotVisible: "(?s)^Atmos Output Test.*"
- repeat:
times: 30
while:
notVisible:
text: "(?s)^Small Skip Duration.*"
focused: true
commands:
- pressKey: "Remote Dpad Down"
- pressKey: "Remote Dpad Center"
- extendedWaitUntil:
visible: "Decrease"
timeout: 15000
- assertVisible: "Increase"
- assertVisible: "(?s)^Enter duration.*"
- pressKey: "back"
- extendedWaitUntil:
visible: "(?s)^Small Skip Duration.*"
timeout: 15000
- pressKey: "back"
- extendedWaitUntil:
visible: "(?s)^Manage Libraries.*"
timeout: 15000
# Settings is a rail tab on TV rather than a pushed route, so Back hands focus
# back to the rail and expands it instead of leaving the screen.
- pressKey: "back"
- extendedWaitUntil:
visible:
text: "Settings"
focused: true
timeout: 20000
- assertVisible: "Home"
- assertVisible: "Libraries"
- assertVisible: "Downloads"
@@ -0,0 +1,57 @@
appId: com.edde746.plezy
---
# Onboarding for a device that already reports itself as a TV.
#
# `onboard_jellyfin_tv.yaml` exists for the phone emulator CI runs on: it
# onboards through the handheld layout and then walks Settings to flip
# "Force TV mode". A real Android TV box takes the TV layout from the first
# frame, so that walk has no handheld chrome to tap and the toggle would only
# re-assert what PlatformDetector already reports.
- retry:
maxRetries: 1
commands:
- launchApp:
clearState: true
permissions:
all: allow
- extendedWaitUntil:
visible: "(?s)^(?:Connect to Jellyfin|Wait).*"
timeout: 30000
- runFlow:
when:
visible: "Wait"
commands:
- tapOn: "Wait"
- extendedWaitUntil:
visible: "Connect to Jellyfin"
timeout: 30000
- tapOn: "Connect to Jellyfin"
- extendedWaitUntil:
visible: "Add Jellyfin server"
timeout: 15000
- tapOn: "(?s)Server URLs.*"
- inputText: ${JELLYFIN_URL}
- tapOn: "Find server"
# A TV-sized layout offers Quick Connect first because typing on a remote is
# slow. The code path needs a second device to approve, so fall back to the
# username form the phone flow already uses.
- extendedWaitUntil:
visible: "(?s)^(?:Username|Cancel)$"
timeout: 30000
- runFlow:
when:
notVisible: "Username"
commands:
- tapOn: "Cancel"
- extendedWaitUntil:
visible: "Username"
timeout: 20000
- tapOn: "Username"
- inputText: "maestro"
- tapOn: "Password"
- inputText: "maestro"
- tapOn: "Sign in"
- extendedWaitUntil:
visible:
id: "tv_browse_rail_selection"
timeout: 45000
+6 -2
View File
@@ -17,10 +17,14 @@ appId: com.edde746.plezy
platform: Android
commands:
- hideKeyboard
# A card announces its watch state three ways, and the codec samples keep a
# resume position on the server once any earlier flow has played them, so the
# partial form has to be accepted for this subflow to be re-runnable against a
# fixture container that is not thrown away between suites.
- extendedWaitUntil:
visible: "(?s)^${SAMPLE_TITLE}, movie, (?:watched|unwatched)$"
visible: "(?s)^${SAMPLE_TITLE}, movie, (?:watched|unwatched|[0-9]+ percent watched)$"
timeout: 15000
- tapOn: "(?s)^${SAMPLE_TITLE}, movie, (?:watched|unwatched)$"
- tapOn: "(?s)^${SAMPLE_TITLE}, movie, (?:watched|unwatched|[0-9]+ percent watched)$"
- extendedWaitUntil:
visible: "Overview"
timeout: 15000
+27
View File
@@ -0,0 +1,27 @@
appId: com.edde746.plezy
---
# Reach Settings from a signed-in Home on a phone layout.
#
# The profile menu is itself an overlay sheet on the MainScreen host, so a tap
# issued while the previous route is still settling opens nothing. The guarded
# repeat re-opens the menu instead of failing the flow, matching the pattern
# already used by flows/07_profiles_settings.yaml.
- extendedWaitUntil:
visible: "Discover"
timeout: 30000
- repeat:
times: 3
while:
notVisible: "Settings"
commands:
- tapOn: "(?s)^M(?:.*Profiles.*)?$"
- waitForAnimationToEnd:
timeout: 3000
- tapOn:
text: "Settings"
below:
text: "Profiles"
- extendedWaitUntil:
visible: "(?s)^Appearance.*"
timeout: 15000
- assertVisible: "(?s)^Manage Libraries.*"