Yattee v2 rewrite

This commit is contained in:
Arkadiusz Fal
2026-02-08 18:31:16 +01:00
parent 20d0cfc0c7
commit 05f921d605
1043 changed files with 163875 additions and 68430 deletions

View File

@@ -0,0 +1,71 @@
# frozen_string_literal: true
require_relative '../spec_helper'
RSpec.describe 'App Launch', :smoke do
before(:all) do
# Boot simulator
@udid = UITest::Simulator.boot(UITest::Config.device)
# Build app (unless skipped)
UITest::App.build(
device: UITest::Config.device,
skip: UITest::Config.skip_build?
)
# Install and launch
UITest::App.install(udid: @udid)
UITest::App.launch(udid: @udid)
# Wait for app to stabilize
sleep UITest::Config.app_launch_wait
# Initialize AXe
@axe = UITest::Axe.new(@udid)
end
after(:all) do
# Terminate app
UITest::App.terminate(udid: @udid, silent: true) if @udid
# Shutdown simulator unless --keep-simulator
UITest::Simulator.shutdown(@udid) if @udid && !UITest::Config.keep_simulator?
end
describe 'Library tab' do
it 'displays the Library navigation bar' do
# With toolbarTitleDisplayMode(.inlineLarge), the title is an AXHeading with AXLabel "Library"
# but no AXUniqueId, so we check for the text instead
expect(@axe).to have_text('Library')
end
it 'displays the Tab Bar' do
expect(@axe).to have_text('Tab Bar')
end
it 'displays the Playlists card' do
expect(@axe).to have_element('library.card.playlists')
end
it 'displays the Bookmarks card' do
expect(@axe).to have_element('library.card.bookmarks')
end
it 'displays the History card' do
expect(@axe).to have_element('library.card.history')
end
it 'displays the Downloads card' do
expect(@axe).to have_element('library.card.downloads')
end
it 'displays the Channels card' do
expect(@axe).to have_element('library.card.channels')
end
it 'matches the baseline screenshot', :visual do
screenshot = @axe.screenshot('app-launch-library')
expect(screenshot).to match_baseline
end
end
end

View File

@@ -0,0 +1,353 @@
# frozen_string_literal: true
require_relative '../spec_helper'
RSpec.describe 'Import Playlists from Piped', :smoke do
before(:all) do
skip 'Piped credentials not configured' unless UITest::Config.piped_credentials?
# Boot simulator
@udid = UITest::Simulator.boot(UITest::Config.device)
# Build app (unless skipped)
UITest::App.build(
device: UITest::Config.device,
skip: UITest::Config.skip_build?
)
# Install and launch
UITest::App.install(udid: @udid)
UITest::App.launch(udid: @udid)
# Wait for app to stabilize
sleep UITest::Config.app_launch_wait
# Initialize helpers
@axe = UITest::Axe.new(@udid)
@instance_setup = UITest::InstanceSetup.new(@axe)
# Set up prerequisites: Yattee Server + logged-in Piped
@instance_setup.ensure_yattee_server(UITest::Config.yattee_server_url)
@instance_setup.ensure_piped(UITest::Config.piped_url)
@instance_setup.ensure_piped_logged_in(UITest::Config.piped_host)
end
after(:all) do
UITest::App.terminate(udid: @udid, silent: true) if @udid
UITest::Simulator.shutdown(@udid) if @udid && !UITest::Config.keep_simulator?
end
describe 'Import section visibility' do
it 'shows Import section with Playlists link when logged in to Piped' do
# Navigate to Piped instance settings
@instance_setup.send(:navigate_to_sources)
# Tap the Piped row (using helper due to iOS 26 multiple-match issue)
@instance_setup.send(:tap_first_element_with_id, "sources.row.piped.#{UITest::Config.piped_host}")
sleep 0.8
# Wait for EditSourceView
start_time = Time.now
loop do
break if @axe.element_exists?('editSource.view')
raise 'EditSourceView not found' if Time.now - start_time > 10
sleep 0.3
end
# Verify Import section is visible with Playlists link
expect(@axe).to have_text('Import')
expect(@axe).to have_element('sources.import.playlists')
# Close settings
@instance_setup.send(:close_edit_sheet)
@instance_setup.send(:close_settings)
end
end
describe 'Import Playlists view' do
before do
# Navigate to Import Playlists
@instance_setup.send(:navigate_to_sources)
# Tap the Piped row (using helper due to iOS 26 multiple-match issue)
@instance_setup.send(:tap_first_element_with_id, "sources.row.piped.#{UITest::Config.piped_host}")
sleep 0.8
# Wait for EditSourceView
start_time = Time.now
loop do
break if @axe.element_exists?('editSource.view')
raise 'EditSourceView not found' if Time.now - start_time > 10
sleep 0.3
end
# Tap Playlists navigation link
@axe.tap_id('sources.import.playlists')
sleep 1
end
after do
# Navigate back and close settings
# Try back button or swipe
begin
@axe.tap_label('Back')
sleep 0.5
rescue UITest::Axe::AxeError
@axe.swipe(start_x: 0, start_y: 400, end_x: 200, end_y: 400, duration: 0.3)
sleep 0.5
end
@instance_setup.send(:close_edit_sheet)
@instance_setup.send(:close_settings)
end
it 'displays Import Playlists view' do
expect(@axe).to have_element('import.playlists.view')
end
it 'loads playlists from Piped' do
# Wait for loading to complete
start_time = Time.now
loop do
# iOS 26 doesn't expose List's accessibilityIdentifier properly
# Check for list by looking for row elements, or empty/error states
has_list = has_playlist_rows?
has_empty = @axe.element_exists?('import.playlists.empty')
has_error = @axe.element_exists?('import.playlists.error')
break if has_list || has_empty || has_error
raise 'Timeout waiting for playlists' if Time.now - start_time > 15
sleep 0.5
end
# Should show list or empty state (not loading)
has_list = has_playlist_rows?
has_empty = @axe.element_exists?('import.playlists.empty')
has_error = @axe.element_exists?('import.playlists.error')
# Either list or empty is success, error is acceptable but not ideal
expect(has_list || has_empty || has_error).to be true
end
it 'shows Add All button when there are unimported playlists' do
# Wait for list to load
wait_for_playlists_list
# Check if there are any unimported playlists (Add buttons visible)
# iOS 26 doesn't expose button IDs properly, so look for buttons with "Add" label
tree = @axe.describe_ui
add_button = find_add_button_element(tree)
if add_button
# Add All button should be in toolbar - tap by coordinates (top-right)
# Toolbar buttons don't expose accessibility IDs on iOS 26
@axe.tap_coordinates(x: 370, y: 105)
sleep 0.5
# Confirmation dialog should appear
expect(@axe).to have_text('Add All')
# Dismiss dialog by tapping outside or swiping down
@axe.tap_coordinates(x: 200, y: 300)
sleep 0.3
else
skip 'All playlists already imported - Add All button correctly hidden'
end
end
it 'can add individual playlist' do
wait_for_playlists_list
# Skip if no playlists or empty
skip 'No playlists to import' unless has_playlist_rows?
# Find first add button - iOS 26 doesn't expose button IDs, use label + coordinates
tree = @axe.describe_ui
add_button = find_add_button_element(tree)
skip 'No unimported playlists' unless add_button
# Get button coordinates
frame = add_button['frame']
x = frame['x'] + (frame['width'] / 2)
y = frame['y'] + (frame['height'] / 2)
# Tap the add button
@axe.tap_coordinates(x: x, y: y)
sleep 0.5
# Either progress indicator appears or merge warning dialog appears
# Wait a bit for import to start
sleep 1
# Check if merge warning is shown (playlist already exists)
if @axe.text_visible?('Playlist Exists')
# Dismiss the merge warning
@axe.tap_label('Cancel')
sleep 0.5
# Test passes - merge warning shown correctly
else
# Wait for import to complete (progress indicator should disappear)
start_time = Time.now
loop do
# Check if import completed (no more progress indicators)
tree = @axe.describe_ui
has_progress = find_progress_indicator(tree)
break unless has_progress
raise 'Import timeout' if Time.now - start_time > 30
sleep 0.5
end
# The button should change to checkmark
new_tree = @axe.describe_ui
new_add_buttons = count_add_buttons(new_tree)
original_add_buttons = count_add_buttons(tree)
# Either we have fewer add buttons, or the button changed to imported indicator
expect(new_add_buttons).to be <= original_add_buttons
end
end
it 'shows confirmation dialog before Add All' do
wait_for_playlists_list
# Check if there are playlists to add
tree = @axe.describe_ui
add_button = find_add_button_element(tree)
skip 'No unimported playlists - Add All not shown' unless add_button
# Tap Add All button in toolbar by coordinates (top-right area)
@axe.tap_coordinates(x: 370, y: 105)
sleep 0.5
# Confirmation dialog should appear with "Add All" action
expect(@axe).to have_text('Add All')
end
end
private
def wait_for_playlists_list(timeout: 15)
start_time = Time.now
loop do
# iOS 26 doesn't expose List's accessibilityIdentifier properly
# Check for rows or empty/error states instead
return if has_playlist_rows? ||
@axe.element_exists?('import.playlists.empty')
raise 'Timeout waiting for playlists' if Time.now - start_time > timeout
sleep 0.5
end
end
# Check if any playlist row elements are visible in the UI tree
# iOS 26 doesn't expose List container ID, but rows are visible
def has_playlist_rows?
tree = @axe.describe_ui
find_element_with_prefix(tree, 'import.playlists.row.')
end
def find_element_with_prefix(node, prefix)
case node
when Hash
id = node['AXUniqueId']
return true if id&.start_with?(prefix)
node.each_value do |value|
return true if find_element_with_prefix(value, prefix)
end
when Array
node.each do |item|
return true if find_element_with_prefix(item, prefix)
end
end
false
end
# Find an "Add" button element by its AXLabel (iOS 26 doesn't expose button IDs properly)
def find_add_button_element(node)
case node
when Hash
# Look for buttons with "Add" label that are individual add buttons (not Add All)
if node['role'] == 'AXButton' && node['AXLabel'] == 'Add' && node['frame']
return node
end
node.each_value do |value|
result = find_add_button_element(value)
return result if result
end
when Array
node.each do |item|
result = find_add_button_element(item)
return result if result
end
end
nil
end
# Find progress indicator (ProgressView) in the tree
def find_progress_indicator(node)
case node
when Hash
# ProgressView shows as AXProgressIndicator
return true if node['role'] == 'AXProgressIndicator'
node.each_value do |value|
return true if find_progress_indicator(value)
end
when Array
node.each do |item|
return true if find_progress_indicator(item)
end
end
false
end
# Count the number of "Add" buttons in the tree
def count_add_buttons(node, count = 0)
case node
when Hash
count += 1 if node['role'] == 'AXButton' && node['AXLabel'] == 'Add'
node.each_value do |value|
count = count_add_buttons(value, count)
end
when Array
node.each do |item|
count = count_add_buttons(item, count)
end
end
count
end
def find_first_add_button(node)
case node
when Hash
id = node['AXUniqueId']
return id if id&.start_with?('import.playlists.add.')
node.each_value do |value|
result = find_first_add_button(value)
return result if result
end
when Array
node.each do |item|
result = find_first_add_button(item)
return result if result
end
end
nil
end
end

View File

@@ -0,0 +1,353 @@
# frozen_string_literal: true
require_relative '../spec_helper'
RSpec.describe 'Import Playlists from Invidious', :smoke do
before(:all) do
skip 'Invidious credentials not configured' unless UITest::Config.invidious_credentials?
# Boot simulator
@udid = UITest::Simulator.boot(UITest::Config.device)
# Build app (unless skipped)
UITest::App.build(
device: UITest::Config.device,
skip: UITest::Config.skip_build?
)
# Install and launch
UITest::App.install(udid: @udid)
UITest::App.launch(udid: @udid)
# Wait for app to stabilize
sleep UITest::Config.app_launch_wait
# Initialize helpers
@axe = UITest::Axe.new(@udid)
@instance_setup = UITest::InstanceSetup.new(@axe)
# Set up prerequisites: Yattee Server + logged-in Invidious
@instance_setup.ensure_yattee_server(UITest::Config.yattee_server_url)
@instance_setup.ensure_invidious(UITest::Config.invidious_url)
@instance_setup.ensure_invidious_logged_in(UITest::Config.invidious_host)
end
after(:all) do
UITest::App.terminate(udid: @udid, silent: true) if @udid
UITest::Simulator.shutdown(@udid) if @udid && !UITest::Config.keep_simulator?
end
describe 'Import section visibility' do
it 'shows Import section with Playlists link when logged in to Invidious' do
# Navigate to Invidious instance settings
@instance_setup.send(:navigate_to_sources)
# Tap the Invidious row (using helper due to iOS 26 multiple-match issue)
@instance_setup.send(:tap_first_element_with_id, "sources.row.invidious.#{UITest::Config.invidious_host}")
sleep 0.8
# Wait for EditSourceView
start_time = Time.now
loop do
break if @axe.element_exists?('editSource.view')
raise 'EditSourceView not found' if Time.now - start_time > 10
sleep 0.3
end
# Verify Import section is visible with Playlists link
expect(@axe).to have_text('Import')
expect(@axe).to have_element('sources.import.playlists')
# Close settings
@instance_setup.send(:close_edit_sheet)
@instance_setup.send(:close_settings)
end
end
describe 'Import Playlists view' do
before do
# Navigate to Import Playlists
@instance_setup.send(:navigate_to_sources)
# Tap the Invidious row (using helper due to iOS 26 multiple-match issue)
@instance_setup.send(:tap_first_element_with_id, "sources.row.invidious.#{UITest::Config.invidious_host}")
sleep 0.8
# Wait for EditSourceView
start_time = Time.now
loop do
break if @axe.element_exists?('editSource.view')
raise 'EditSourceView not found' if Time.now - start_time > 10
sleep 0.3
end
# Tap Playlists navigation link
@axe.tap_id('sources.import.playlists')
sleep 1
end
after do
# Navigate back and close settings
# Try back button or swipe
begin
@axe.tap_label('Back')
sleep 0.5
rescue UITest::Axe::AxeError
@axe.swipe(start_x: 0, start_y: 400, end_x: 200, end_y: 400, duration: 0.3)
sleep 0.5
end
@instance_setup.send(:close_edit_sheet)
@instance_setup.send(:close_settings)
end
it 'displays Import Playlists view' do
expect(@axe).to have_element('import.playlists.view')
end
it 'loads playlists from Invidious' do
# Wait for loading to complete
start_time = Time.now
loop do
# iOS 26 doesn't expose List's accessibilityIdentifier properly
# Check for list by looking for row elements, or empty/error states
has_list = has_playlist_rows?
has_empty = @axe.element_exists?('import.playlists.empty')
has_error = @axe.element_exists?('import.playlists.error')
break if has_list || has_empty || has_error
raise 'Timeout waiting for playlists' if Time.now - start_time > 15
sleep 0.5
end
# Should show list or empty state (not loading)
has_list = has_playlist_rows?
has_empty = @axe.element_exists?('import.playlists.empty')
has_error = @axe.element_exists?('import.playlists.error')
# Either list or empty is success, error is acceptable but not ideal
expect(has_list || has_empty || has_error).to be true
end
it 'shows Add All button when there are unimported playlists' do
# Wait for list to load
wait_for_playlists_list
# Check if there are any unimported playlists (Add buttons visible)
# iOS 26 doesn't expose button IDs properly, so look for buttons with "Add" label
tree = @axe.describe_ui
add_button = find_add_button_element(tree)
if add_button
# Add All button should be in toolbar - tap by coordinates (top-right)
# Toolbar buttons don't expose accessibility IDs on iOS 26
@axe.tap_coordinates(x: 370, y: 105)
sleep 0.5
# Confirmation dialog should appear
expect(@axe).to have_text('Add All')
# Dismiss dialog by tapping outside or swiping down
@axe.tap_coordinates(x: 200, y: 300)
sleep 0.3
else
skip 'All playlists already imported - Add All button correctly hidden'
end
end
it 'can add individual playlist' do
wait_for_playlists_list
# Skip if no playlists or empty
skip 'No playlists to import' unless has_playlist_rows?
# Find first add button - iOS 26 doesn't expose button IDs, use label + coordinates
tree = @axe.describe_ui
add_button = find_add_button_element(tree)
skip 'No unimported playlists' unless add_button
# Get button coordinates
frame = add_button['frame']
x = frame['x'] + (frame['width'] / 2)
y = frame['y'] + (frame['height'] / 2)
# Tap the add button
@axe.tap_coordinates(x: x, y: y)
sleep 0.5
# Either progress indicator appears or merge warning dialog appears
# Wait a bit for import to start
sleep 1
# Check if merge warning is shown (playlist already exists)
if @axe.text_visible?('Playlist Exists')
# Dismiss the merge warning
@axe.tap_label('Cancel')
sleep 0.5
# Test passes - merge warning shown correctly
else
# Wait for import to complete (progress indicator should disappear)
start_time = Time.now
loop do
# Check if import completed (no more progress indicators)
tree = @axe.describe_ui
has_progress = find_progress_indicator(tree)
break unless has_progress
raise 'Import timeout' if Time.now - start_time > 30
sleep 0.5
end
# The button should change to checkmark
new_tree = @axe.describe_ui
new_add_buttons = count_add_buttons(new_tree)
original_add_buttons = count_add_buttons(tree)
# Either we have fewer add buttons, or the button changed to imported indicator
expect(new_add_buttons).to be <= original_add_buttons
end
end
it 'shows confirmation dialog before Add All' do
wait_for_playlists_list
# Check if there are playlists to add
tree = @axe.describe_ui
add_button = find_add_button_element(tree)
skip 'No unimported playlists - Add All not shown' unless add_button
# Tap Add All button in toolbar by coordinates (top-right area)
@axe.tap_coordinates(x: 370, y: 105)
sleep 0.5
# Confirmation dialog should appear with "Add All" action
expect(@axe).to have_text('Add All')
end
end
private
def wait_for_playlists_list(timeout: 15)
start_time = Time.now
loop do
# iOS 26 doesn't expose List's accessibilityIdentifier properly
# Check for rows or empty/error states instead
return if has_playlist_rows? ||
@axe.element_exists?('import.playlists.empty')
raise 'Timeout waiting for playlists' if Time.now - start_time > timeout
sleep 0.5
end
end
# Check if any playlist row elements are visible in the UI tree
# iOS 26 doesn't expose List container ID, but rows are visible
def has_playlist_rows?
tree = @axe.describe_ui
find_element_with_prefix(tree, 'import.playlists.row.')
end
def find_element_with_prefix(node, prefix)
case node
when Hash
id = node['AXUniqueId']
return true if id&.start_with?(prefix)
node.each_value do |value|
return true if find_element_with_prefix(value, prefix)
end
when Array
node.each do |item|
return true if find_element_with_prefix(item, prefix)
end
end
false
end
# Find an "Add" button element by its AXLabel (iOS 26 doesn't expose button IDs properly)
def find_add_button_element(node)
case node
when Hash
# Look for buttons with "Add" label that are individual add buttons (not Add All)
if node['role'] == 'AXButton' && node['AXLabel'] == 'Add' && node['frame']
return node
end
node.each_value do |value|
result = find_add_button_element(value)
return result if result
end
when Array
node.each do |item|
result = find_add_button_element(item)
return result if result
end
end
nil
end
# Find progress indicator (ProgressView) in the tree
def find_progress_indicator(node)
case node
when Hash
# ProgressView shows as AXProgressIndicator
return true if node['role'] == 'AXProgressIndicator'
node.each_value do |value|
return true if find_progress_indicator(value)
end
when Array
node.each do |item|
return true if find_progress_indicator(item)
end
end
false
end
# Count the number of "Add" buttons in the tree
def count_add_buttons(node, count = 0)
case node
when Hash
count += 1 if node['role'] == 'AXButton' && node['AXLabel'] == 'Add'
node.each_value do |value|
count = count_add_buttons(value, count)
end
when Array
node.each do |item|
count = count_add_buttons(item, count)
end
end
count
end
def find_first_add_button(node)
case node
when Hash
id = node['AXUniqueId']
return id if id&.start_with?('import.playlists.add.')
node.each_value do |value|
result = find_first_add_button(value)
return result if result
end
when Array
node.each do |item|
result = find_first_add_button(item)
return result if result
end
end
nil
end
end

View File

@@ -0,0 +1,313 @@
# frozen_string_literal: true
require_relative '../spec_helper'
RSpec.describe 'Import Subscriptions from Piped', :smoke do
before(:all) do
skip 'Piped credentials not configured' unless UITest::Config.piped_credentials?
# Boot simulator
@udid = UITest::Simulator.boot(UITest::Config.device)
# Build app (unless skipped)
UITest::App.build(
device: UITest::Config.device,
skip: UITest::Config.skip_build?
)
# Install and launch
UITest::App.install(udid: @udid)
UITest::App.launch(udid: @udid)
# Wait for app to stabilize
sleep UITest::Config.app_launch_wait
# Initialize helpers
@axe = UITest::Axe.new(@udid)
@instance_setup = UITest::InstanceSetup.new(@axe)
# Set up prerequisites: Yattee Server + logged-in Piped
@instance_setup.ensure_yattee_server(UITest::Config.yattee_server_url)
@instance_setup.ensure_piped(UITest::Config.piped_url)
@instance_setup.ensure_piped_logged_in(UITest::Config.piped_host)
end
after(:all) do
UITest::App.terminate(udid: @udid, silent: true) if @udid
UITest::Simulator.shutdown(@udid) if @udid && !UITest::Config.keep_simulator?
end
describe 'Import section visibility' do
it 'shows Import section when Yattee Server exists and logged in to Piped' do
# Navigate to Piped instance settings
@instance_setup.send(:navigate_to_sources)
# Tap the Piped row (using helper due to iOS 26 multiple-match issue)
@instance_setup.send(:tap_first_element_with_id, "sources.row.piped.#{UITest::Config.piped_host}")
sleep 0.8
# Wait for EditSourceView
start_time = Time.now
loop do
break if @axe.element_exists?('editSource.view')
raise 'EditSourceView not found' if Time.now - start_time > 10
sleep 0.3
end
# Verify Import section is visible
expect(@axe).to have_text('Import')
expect(@axe).to have_element('sources.import.subscriptions')
# Close settings
@instance_setup.send(:close_edit_sheet)
@instance_setup.send(:close_settings)
end
end
describe 'Import Subscriptions view' do
before do
# Navigate to Import Subscriptions
@instance_setup.send(:navigate_to_sources)
# Tap the Piped row (using helper due to iOS 26 multiple-match issue)
@instance_setup.send(:tap_first_element_with_id, "sources.row.piped.#{UITest::Config.piped_host}")
sleep 0.8
# Wait for EditSourceView
start_time = Time.now
loop do
break if @axe.element_exists?('editSource.view')
raise 'EditSourceView not found' if Time.now - start_time > 10
sleep 0.3
end
# Tap Subscriptions navigation link
@axe.tap_id('sources.import.subscriptions')
sleep 1
end
after do
# Navigate back and close settings
# Try back button or swipe
begin
@axe.tap_label('Back')
sleep 0.5
rescue UITest::Axe::AxeError
@axe.swipe(start_x: 0, start_y: 400, end_x: 200, end_y: 400, duration: 0.3)
sleep 0.5
end
@instance_setup.send(:close_edit_sheet)
@instance_setup.send(:close_settings)
end
it 'displays Import Subscriptions view' do
expect(@axe).to have_element('import.subscriptions.view')
end
it 'loads subscriptions from Piped' do
# Wait for loading to complete
start_time = Time.now
loop do
# iOS 26 doesn't expose List's accessibilityIdentifier properly
# Check for list by looking for row elements, or empty/error states
has_list = has_subscription_rows?
has_empty = @axe.element_exists?('import.subscriptions.empty')
has_error = @axe.element_exists?('import.subscriptions.error')
break if has_list || has_empty || has_error
raise 'Timeout waiting for subscriptions' if Time.now - start_time > 15
sleep 0.5
end
# Should show list or empty state (not loading)
has_list = has_subscription_rows?
has_empty = @axe.element_exists?('import.subscriptions.empty')
has_error = @axe.element_exists?('import.subscriptions.error')
# Either list or empty is success, error is acceptable but not ideal
expect(has_list || has_empty || has_error).to be true
end
it 'shows Add All button when there are unsubscribed channels' do
# Wait for list to load
wait_for_subscriptions_list
# Check if there are any unsubscribed channels (Add buttons visible)
# iOS 26 doesn't expose button IDs properly, so look for buttons with "Add" label
tree = @axe.describe_ui
add_button = find_add_button_element(tree)
if add_button
# Add All button should be in toolbar - tap by coordinates (top-right)
# Toolbar buttons don't expose accessibility IDs on iOS 26
@axe.tap_coordinates(x: 370, y: 105)
sleep 0.5
# Confirmation dialog should appear
expect(@axe).to have_text('Add All')
# Dismiss dialog by tapping outside or swiping down
@axe.tap_coordinates(x: 200, y: 300)
sleep 0.3
else
skip 'All channels already subscribed - Add All button correctly hidden'
end
end
it 'can add individual subscription' do
wait_for_subscriptions_list
# Skip if no subscriptions or empty
skip 'No subscriptions to import' unless has_subscription_rows?
# Find first add button - iOS 26 doesn't expose button IDs, use label + coordinates
tree = @axe.describe_ui
add_button = find_add_button_element(tree)
skip 'No unsubscribed channels' unless add_button
# Get button coordinates
frame = add_button['frame']
x = frame['x'] + (frame['width'] / 2)
y = frame['y'] + (frame['height'] / 2)
# Tap the add button
@axe.tap_coordinates(x: x, y: y)
sleep 1
# The button should change - verify by checking that same position now shows checkmark
# or that there's one fewer Add button
new_tree = @axe.describe_ui
new_add_buttons = count_add_buttons(new_tree)
original_add_buttons = count_add_buttons(tree)
# Either we have fewer add buttons, or the button changed to subscribed indicator
expect(new_add_buttons).to be < original_add_buttons
end
it 'shows confirmation dialog before Add All' do
wait_for_subscriptions_list
# Check if there are channels to add
tree = @axe.describe_ui
add_button = find_add_button_element(tree)
skip 'No unsubscribed channels - Add All not shown' unless add_button
# Tap Add All button in toolbar by coordinates (top-right area)
@axe.tap_coordinates(x: 370, y: 105)
sleep 0.5
# Confirmation dialog should appear with "Add All" action
expect(@axe).to have_text('Add All')
end
end
private
def wait_for_subscriptions_list(timeout: 15)
start_time = Time.now
loop do
# iOS 26 doesn't expose List's accessibilityIdentifier properly
# Check for rows or empty/error states instead
return if has_subscription_rows? ||
@axe.element_exists?('import.subscriptions.empty')
raise 'Timeout waiting for subscriptions' if Time.now - start_time > timeout
sleep 0.5
end
end
# Check if any subscription row elements are visible in the UI tree
# iOS 26 doesn't expose List container ID, but rows are visible
def has_subscription_rows?
tree = @axe.describe_ui
find_element_with_prefix(tree, 'import.subscriptions.row.')
end
def find_element_with_prefix(node, prefix)
case node
when Hash
id = node['AXUniqueId']
return true if id&.start_with?(prefix)
node.each_value do |value|
return true if find_element_with_prefix(value, prefix)
end
when Array
node.each do |item|
return true if find_element_with_prefix(item, prefix)
end
end
false
end
# Find an "Add" button element by its AXLabel (iOS 26 doesn't expose button IDs properly)
def find_add_button_element(node)
case node
when Hash
# Look for buttons with "Add" label that are individual add buttons (not Add All)
if node['role'] == 'AXButton' && node['AXLabel'] == 'Add' && node['frame']
return node
end
node.each_value do |value|
result = find_add_button_element(value)
return result if result
end
when Array
node.each do |item|
result = find_add_button_element(item)
return result if result
end
end
nil
end
# Count the number of "Add" buttons in the tree
def count_add_buttons(node, count = 0)
case node
when Hash
if node['role'] == 'AXButton' && node['AXLabel'] == 'Add'
count += 1
end
node.each_value do |value|
count = count_add_buttons(value, count)
end
when Array
node.each do |item|
count = count_add_buttons(item, count)
end
end
count
end
def find_first_add_button(node)
case node
when Hash
id = node['AXUniqueId']
return id if id&.start_with?('import.subscriptions.add.')
node.each_value do |value|
result = find_first_add_button(value)
return result if result
end
when Array
node.each do |item|
result = find_first_add_button(item)
return result if result
end
end
nil
end
end

View File

@@ -0,0 +1,315 @@
# frozen_string_literal: true
require_relative '../spec_helper'
RSpec.describe 'Import Subscriptions from Invidious', :smoke do
before(:all) do
skip 'Invidious credentials not configured' unless UITest::Config.invidious_credentials?
# Boot simulator
@udid = UITest::Simulator.boot(UITest::Config.device)
# Build app (unless skipped)
UITest::App.build(
device: UITest::Config.device,
skip: UITest::Config.skip_build?
)
# Install and launch
UITest::App.install(udid: @udid)
UITest::App.launch(udid: @udid)
# Wait for app to stabilize
sleep UITest::Config.app_launch_wait
# Initialize helpers
@axe = UITest::Axe.new(@udid)
@instance_setup = UITest::InstanceSetup.new(@axe)
# Set up prerequisites: Yattee Server + logged-in Invidious
@instance_setup.ensure_yattee_server(UITest::Config.yattee_server_url)
@instance_setup.ensure_invidious(UITest::Config.invidious_url)
@instance_setup.ensure_invidious_logged_in(UITest::Config.invidious_host)
end
after(:all) do
UITest::App.terminate(udid: @udid, silent: true) if @udid
UITest::Simulator.shutdown(@udid) if @udid && !UITest::Config.keep_simulator?
end
describe 'Import section visibility' do
it 'shows Import section when Yattee Server exists and logged in to Invidious' do
# Navigate to Invidious instance settings
@instance_setup.send(:navigate_to_sources)
# Tap the Invidious row (using helper due to iOS 26 multiple-match issue)
@instance_setup.send(:tap_first_element_with_id, "sources.row.invidious.#{UITest::Config.invidious_host}")
sleep 0.8
# Wait for EditSourceView
start_time = Time.now
loop do
break if @axe.element_exists?('editSource.view')
raise 'EditSourceView not found' if Time.now - start_time > 10
sleep 0.3
end
# Verify Import section is visible
expect(@axe).to have_text('Import')
expect(@axe).to have_element('sources.import.subscriptions')
# Close settings
@instance_setup.send(:close_edit_sheet)
@instance_setup.send(:close_settings)
end
end
describe 'Import Subscriptions view' do
before do
# Navigate to Import Subscriptions
@instance_setup.send(:navigate_to_sources)
# Tap the Invidious row (using helper due to iOS 26 multiple-match issue)
@instance_setup.send(:tap_first_element_with_id, "sources.row.invidious.#{UITest::Config.invidious_host}")
sleep 0.8
# Wait for EditSourceView
start_time = Time.now
loop do
break if @axe.element_exists?('editSource.view')
raise 'EditSourceView not found' if Time.now - start_time > 10
sleep 0.3
end
# Tap Subscriptions navigation link
@axe.tap_id('sources.import.subscriptions')
sleep 1
end
after do
# Navigate back and close settings
# Try back button or swipe
begin
@axe.tap_label('Back')
sleep 0.5
rescue UITest::Axe::AxeError
@axe.swipe(start_x: 0, start_y: 400, end_x: 200, end_y: 400, duration: 0.3)
sleep 0.5
end
@instance_setup.send(:close_edit_sheet)
@instance_setup.send(:close_settings)
end
it 'displays Import Subscriptions view' do
expect(@axe).to have_element('import.subscriptions.view')
end
it 'loads subscriptions from Invidious' do
# Wait for loading to complete
start_time = Time.now
loop do
# iOS 26 doesn't expose List's accessibilityIdentifier properly
# Check for list by looking for row elements, or empty/error states
has_list = has_subscription_rows?
has_empty = @axe.element_exists?('import.subscriptions.empty')
has_error = @axe.element_exists?('import.subscriptions.error')
break if has_list || has_empty || has_error
if Time.now - start_time > 15
raise 'Timeout waiting for subscriptions'
end
sleep 0.5
end
# Should show list or empty state (not loading)
has_list = has_subscription_rows?
has_empty = @axe.element_exists?('import.subscriptions.empty')
has_error = @axe.element_exists?('import.subscriptions.error')
# Either list or empty is success, error is acceptable but not ideal
expect(has_list || has_empty || has_error).to be true
end
it 'shows Add All button when there are unsubscribed channels' do
# Wait for list to load
wait_for_subscriptions_list
# Check if there are any unsubscribed channels (Add buttons visible)
# iOS 26 doesn't expose button IDs properly, so look for buttons with "Add" label
tree = @axe.describe_ui
add_button = find_add_button_element(tree)
if add_button
# Add All button should be in toolbar - tap by coordinates (top-right)
# Toolbar buttons don't expose accessibility IDs on iOS 26
@axe.tap_coordinates(x: 370, y: 105)
sleep 0.5
# Confirmation dialog should appear
expect(@axe).to have_text('Add All')
# Dismiss dialog by tapping outside or swiping down
@axe.tap_coordinates(x: 200, y: 300)
sleep 0.3
else
skip 'All channels already subscribed - Add All button correctly hidden'
end
end
it 'can add individual subscription' do
wait_for_subscriptions_list
# Skip if no subscriptions or empty
skip 'No subscriptions to import' unless has_subscription_rows?
# Find first add button - iOS 26 doesn't expose button IDs, use label + coordinates
tree = @axe.describe_ui
add_button = find_add_button_element(tree)
skip 'No unsubscribed channels' unless add_button
# Get button coordinates
frame = add_button['frame']
x = frame['x'] + (frame['width'] / 2)
y = frame['y'] + (frame['height'] / 2)
# Tap the add button
@axe.tap_coordinates(x: x, y: y)
sleep 1
# The button should change - verify by checking that same position now shows checkmark
# or that there's one fewer Add button
new_tree = @axe.describe_ui
new_add_buttons = count_add_buttons(new_tree)
original_add_buttons = count_add_buttons(tree)
# Either we have fewer add buttons, or the button changed to subscribed indicator
expect(new_add_buttons).to be < original_add_buttons
end
it 'shows confirmation dialog before Add All' do
wait_for_subscriptions_list
# Check if there are channels to add
tree = @axe.describe_ui
add_button = find_add_button_element(tree)
skip 'No unsubscribed channels - Add All not shown' unless add_button
# Tap Add All button in toolbar by coordinates (top-right area)
@axe.tap_coordinates(x: 370, y: 105)
sleep 0.5
# Confirmation dialog should appear with "Add All" action
expect(@axe).to have_text('Add All')
end
end
private
def wait_for_subscriptions_list(timeout: 15)
start_time = Time.now
loop do
# iOS 26 doesn't expose List's accessibilityIdentifier properly
# Check for rows or empty/error states instead
return if has_subscription_rows? ||
@axe.element_exists?('import.subscriptions.empty')
raise 'Timeout waiting for subscriptions' if Time.now - start_time > timeout
sleep 0.5
end
end
# Check if any subscription row elements are visible in the UI tree
# iOS 26 doesn't expose List container ID, but rows are visible
def has_subscription_rows?
tree = @axe.describe_ui
find_element_with_prefix(tree, 'import.subscriptions.row.')
end
def find_element_with_prefix(node, prefix)
case node
when Hash
id = node['AXUniqueId']
return true if id&.start_with?(prefix)
node.each_value do |value|
return true if find_element_with_prefix(value, prefix)
end
when Array
node.each do |item|
return true if find_element_with_prefix(item, prefix)
end
end
false
end
# Find an "Add" button element by its AXLabel (iOS 26 doesn't expose button IDs properly)
def find_add_button_element(node)
case node
when Hash
# Look for buttons with "Add" label that are individual add buttons (not Add All)
if node['role'] == 'AXButton' && node['AXLabel'] == 'Add' && node['frame']
return node
end
node.each_value do |value|
result = find_add_button_element(value)
return result if result
end
when Array
node.each do |item|
result = find_add_button_element(item)
return result if result
end
end
nil
end
# Count the number of "Add" buttons in the tree
def count_add_buttons(node, count = 0)
case node
when Hash
if node['role'] == 'AXButton' && node['AXLabel'] == 'Add'
count += 1
end
node.each_value do |value|
count = count_add_buttons(value, count)
end
when Array
node.each do |item|
count = count_add_buttons(item, count)
end
end
count
end
def find_first_add_button(node)
case node
when Hash
id = node['AXUniqueId']
return id if id&.start_with?('import.subscriptions.add.')
node.each_value do |value|
result = find_first_add_button(value)
return result if result
end
when Array
node.each do |item|
result = find_first_add_button(item)
return result if result
end
end
nil
end
end

View File

@@ -0,0 +1,73 @@
# frozen_string_literal: true
require_relative '../spec_helper'
RSpec.describe 'Invidious Instance', :smoke do
before(:all) do
# Boot simulator
@udid = UITest::Simulator.boot(UITest::Config.device)
# Build app (unless skipped)
UITest::App.build(
device: UITest::Config.device,
skip: UITest::Config.skip_build?
)
# Install and launch
UITest::App.install(udid: @udid)
UITest::App.launch(udid: @udid)
# Wait for app to stabilize
sleep UITest::Config.app_launch_wait
# Initialize AXe and InstanceSetup helper
@axe = UITest::Axe.new(@udid)
@instance_setup = UITest::InstanceSetup.new(@axe)
end
after(:all) do
# Terminate app
UITest::App.terminate(udid: @udid, silent: true) if @udid
# Shutdown simulator unless --keep-simulator
UITest::Simulator.shutdown(@udid) if @udid && !UITest::Config.keep_simulator?
end
describe 'adding via Detect & Add' do
it 'adds Invidious instance and verifies it appears in Sources' do
invidious_url = UITest::Config.invidious_url
invidious_host = UITest::Config.invidious_host
# Ensure we start from Library (check for text since inlineLarge title has no AXUniqueId)
expect(@axe).to have_text('Library')
# Remove existing instance (if any) and add fresh - always tests the add flow
@instance_setup.remove_and_add_invidious(invidious_url)
# Navigate to Sources to verify the instance was added
# Open Settings using accessibility identifier
@axe.tap_id('library.settingsButton')
sleep 1
expect(@axe).to have_element('settings.view')
# Navigate to Sources
@axe.tap_id('settings.row.sources')
sleep 0.5
# Verify we're on Sources list
expect(@axe).to have_element('sources.view')
# Verify instance appears in Sources list
expect(@axe).to have_element("sources.row.invidious.#{invidious_host}")
# Close settings
begin
@axe.tap_label('Done')
rescue StandardError
nil
end
sleep 0.5
end
end
end

View File

@@ -0,0 +1,73 @@
# frozen_string_literal: true
require_relative '../spec_helper'
RSpec.describe 'Piped Instance', :smoke do
before(:all) do
# Boot simulator
@udid = UITest::Simulator.boot(UITest::Config.device)
# Build app (unless skipped)
UITest::App.build(
device: UITest::Config.device,
skip: UITest::Config.skip_build?
)
# Install and launch
UITest::App.install(udid: @udid)
UITest::App.launch(udid: @udid)
# Wait for app to stabilize
sleep UITest::Config.app_launch_wait
# Initialize AXe and InstanceSetup helper
@axe = UITest::Axe.new(@udid)
@instance_setup = UITest::InstanceSetup.new(@axe)
end
after(:all) do
# Terminate app
UITest::App.terminate(udid: @udid, silent: true) if @udid
# Shutdown simulator unless --keep-simulator
UITest::Simulator.shutdown(@udid) if @udid && !UITest::Config.keep_simulator?
end
describe 'adding via Detect & Add' do
it 'adds Piped instance and verifies it appears in Sources' do
piped_url = UITest::Config.piped_url
piped_host = UITest::Config.piped_host
# Ensure we start from Library (check for text since inlineLarge title has no AXUniqueId)
expect(@axe).to have_text('Library')
# Remove existing instance (if any) and add fresh - always tests the add flow
@instance_setup.remove_and_add_piped(piped_url)
# Navigate to Sources to verify the instance was added
# Open Settings using accessibility identifier
@axe.tap_id('library.settingsButton')
sleep 1
expect(@axe).to have_element('settings.view')
# Navigate to Sources
@axe.tap_id('settings.row.sources')
sleep 0.5
# Verify we're on Sources list
expect(@axe).to have_element('sources.view')
# Verify instance appears in Sources list
expect(@axe).to have_element("sources.row.piped.#{piped_host}")
# Close settings
begin
@axe.tap_label('Done')
rescue StandardError
nil
end
sleep 0.5
end
end
end

View File

@@ -0,0 +1,47 @@
# frozen_string_literal: true
require_relative '../spec_helper'
RSpec.describe 'Player Controls Preview', :smoke do
before(:all) do
@udid = UITest::Simulator.boot(UITest::Config.device)
UITest::App.build(device: UITest::Config.device, skip: UITest::Config.skip_build?)
UITest::App.install(udid: @udid)
UITest::App.launch(udid: @udid)
sleep UITest::Config.app_launch_wait
@axe = UITest::Axe.new(@udid)
end
after(:all) do
UITest::App.terminate(udid: @udid, silent: true) if @udid
UITest::Simulator.shutdown(@udid) if @udid && !UITest::Config.keep_simulator?
end
describe 'preview padding comparison' do
it 'captures Portrait and Landscape screenshots for comparison' do
# Navigate to Settings tab
@axe.tap_label('Settings')
sleep 1
# Navigate to Player Controls
@axe.tap_label('Player Controls')
sleep 1
# Capture Portrait screenshot (default)
portrait_path = @axe.screenshot('player_controls_portrait')
puts "Portrait screenshot: #{portrait_path}"
# Switch to Landscape preview by tapping the right side of the segmented control
# The picker is at x=48, width=306, so Landscape segment is around x=280, y=398
@axe.tap_coordinates(x: 280, y: 398)
sleep 0.5
# Capture Landscape screenshot
landscape_path = @axe.screenshot('player_controls_landscape')
puts "Landscape screenshot: #{landscape_path}"
puts "\nScreenshots saved to: #{UITest::Config.current_dir}"
puts 'Compare these screenshots to verify padding consistency.'
end
end
end

View File

@@ -0,0 +1,63 @@
# frozen_string_literal: true
require_relative '../spec_helper'
RSpec.describe 'Search', :smoke do
before(:all) do
# Boot simulator
@udid = UITest::Simulator.boot(UITest::Config.device)
# Build app (unless skipped)
UITest::App.build(
device: UITest::Config.device,
skip: UITest::Config.skip_build?
)
# Install and launch
UITest::App.install(udid: @udid)
UITest::App.launch(udid: @udid)
# Wait for app to stabilize
sleep UITest::Config.app_launch_wait
# Initialize helpers
@axe = UITest::Axe.new(@udid)
@instance_setup = UITest::InstanceSetup.new(@axe)
@search_helper = UITest::SearchHelper.new(@axe)
# Ensure Yattee Server instance is configured
@instance_setup.ensure_yattee_server(UITest::Config.yattee_server_url)
end
after(:all) do
# Terminate app
UITest::App.terminate(udid: @udid, silent: true) if @udid
# Shutdown simulator unless --keep-simulator
UITest::Simulator.shutdown(@udid) if @udid && !UITest::Config.keep_simulator?
end
describe 'searching for videos' do
it 'navigates to search, enters query, and displays results' do
video_id = 'XfELJU1mRMg'
# Navigate to Search tab
@search_helper.navigate_to_search
expect(@search_helper.search_visible?).to be true
# Search for the known video ID
@search_helper.search(video_id)
# Wait for results view to appear (filter strip + results container)
@search_helper.wait_for_results
# Verify results are displayed
expect(@search_helper.results_visible?).to be true
# Take a screenshot for visual verification
# Note: Individual video rows aren't exposed in iOS accessibility tree
# due to ScrollView/LazyVStack limitations
@axe.screenshot('search_results_final')
end
end
end

View File

@@ -0,0 +1,107 @@
# frozen_string_literal: true
require_relative '../spec_helper'
RSpec.describe 'Settings', :smoke do
before(:all) do
# Boot simulator
@udid = UITest::Simulator.boot(UITest::Config.device)
# Build app (unless skipped)
UITest::App.build(
device: UITest::Config.device,
skip: UITest::Config.skip_build?
)
# Install and launch
UITest::App.install(udid: @udid)
UITest::App.launch(udid: @udid)
# Wait for app to stabilize
sleep UITest::Config.app_launch_wait
# Initialize AXe
@axe = UITest::Axe.new(@udid)
end
after(:all) do
# Terminate app
UITest::App.terminate(udid: @udid, silent: true) if @udid
# Shutdown simulator unless --keep-simulator
UITest::Simulator.shutdown(@udid) if @udid && !UITest::Config.keep_simulator?
end
describe 'opening Settings from Library' do
before(:all) do
# Ensure we're on Library tab first (check for text since inlineLarge title has no AXUniqueId)
expect(@axe).to have_text('Library')
# Tap Settings button using accessibility identifier
@axe.tap_id('library.settingsButton')
sleep 1
end
after(:all) do
# Close Settings to return to Library
begin
@axe.tap_label('Done')
rescue StandardError
nil
end
sleep 0.5
end
it 'opens the Settings view' do
expect(@axe).to have_element('settings.view')
end
it 'displays the Settings title' do
expect(@axe).to have_text('Settings')
end
it 'displays the Done button' do
expect(@axe).to have_element('settings.doneButton')
end
it 'displays the Sources section' do
expect(@axe).to have_text('Sources')
end
it 'displays the Playback section' do
expect(@axe).to have_text('Playback')
end
it 'displays the Appearance section' do
expect(@axe).to have_text('Appearance')
end
it 'matches the baseline screenshot', :visual do
screenshot = @axe.screenshot('settings-main')
expect(screenshot).to match_baseline
end
end
describe 'closing Settings' do
before(:all) do
# Open settings if not already open
unless @axe.element_exists?('settings.view')
@axe.tap_id('library.settingsButton')
sleep 1
end
end
it 'closes Settings when tapping Done' do
# Verify we're in Settings
expect(@axe).to have_element('settings.view')
# Tap Done
@axe.tap_label('Done')
sleep 0.5
# Verify we're back on Library (check for text since inlineLarge title has no AXUniqueId)
expect(@axe).to have_text('Library')
expect(@axe).not_to have_element('settings.view')
end
end
end

View File

@@ -0,0 +1,76 @@
# frozen_string_literal: true
require_relative '../spec_helper'
RSpec.describe 'Video Playback with Invidious', :smoke do
before(:all) do
# Skip if Invidious URL not explicitly configured
skip 'Invidious URL not configured (set INVIDIOUS_URL env var)' unless ENV['INVIDIOUS_URL']
# Boot simulator
@udid = UITest::Simulator.boot(UITest::Config.device)
# Build app (unless skipped)
UITest::App.build(
device: UITest::Config.device,
skip: UITest::Config.skip_build?
)
# Install and launch
UITest::App.install(udid: @udid)
UITest::App.launch(udid: @udid)
# Wait for app to stabilize
sleep UITest::Config.app_launch_wait
# Initialize helpers
@axe = UITest::Axe.new(@udid)
@instance_setup = UITest::InstanceSetup.new(@axe)
@search_helper = UITest::SearchHelper.new(@axe)
@player_helper = UITest::PlayerHelper.new(@axe)
# Remove Yattee Server if exists (to ensure searches use Invidious)
@instance_setup.remove_yattee_server(UITest::Config.yattee_server_host)
# Ensure Invidious instance is configured
@instance_setup.ensure_invidious(UITest::Config.invidious_url)
end
after(:all) do
# Terminate app
UITest::App.terminate(udid: @udid, silent: true) if @udid
# Shutdown simulator unless --keep-simulator
UITest::Simulator.shutdown(@udid) if @udid && !UITest::Config.keep_simulator?
end
describe 'playing video from search' do
it 'searches, taps video, plays, and closes player' do
video_id = 'XfELJU1mRMg'
# Navigate to Search and find video
@search_helper.navigate_to_search
@search_helper.search(video_id)
@search_helper.wait_for_results
# Verify results view is displayed
expect(@search_helper.results_visible?).to be true
# Tap first video result thumbnail to start playback directly
@search_helper.tap_first_result_thumbnail
# Wait for player to expand and start playing
@player_helper.wait_for_player_expanded
@player_helper.wait_for_playback_started
# Take screenshot of playback for visual verification
@axe.screenshot('video_playback_invidious_playing')
# Close the player
@player_helper.close_player
# Take screenshot after close attempt
@axe.screenshot('video_playback_invidious_after_close')
end
end
end

View File

@@ -0,0 +1,79 @@
# frozen_string_literal: true
require_relative '../spec_helper'
RSpec.describe 'Video Playback with Piped', :smoke do
before(:all) do
# Skip if Piped URL not explicitly configured
skip 'Piped URL not configured (set PIPED_URL env var)' unless ENV['PIPED_URL']
# Boot simulator
@udid = UITest::Simulator.boot(UITest::Config.device)
# Build app (unless skipped)
UITest::App.build(
device: UITest::Config.device,
skip: UITest::Config.skip_build?
)
# Install and launch
UITest::App.install(udid: @udid)
UITest::App.launch(udid: @udid)
# Wait for app to stabilize
sleep UITest::Config.app_launch_wait
# Initialize helpers
@axe = UITest::Axe.new(@udid)
@instance_setup = UITest::InstanceSetup.new(@axe)
@search_helper = UITest::SearchHelper.new(@axe)
@player_helper = UITest::PlayerHelper.new(@axe)
# Remove Yattee Server if exists (to ensure searches use Piped)
@instance_setup.remove_yattee_server(UITest::Config.yattee_server_host)
# Remove Invidious if exists (to ensure Piped is the only backend)
@instance_setup.remove_invidious(UITest::Config.invidious_host)
# Ensure Piped instance is configured
@instance_setup.ensure_piped(UITest::Config.piped_url)
end
after(:all) do
# Terminate app
UITest::App.terminate(udid: @udid, silent: true) if @udid
# Shutdown simulator unless --keep-simulator
UITest::Simulator.shutdown(@udid) if @udid && !UITest::Config.keep_simulator?
end
describe 'playing video from search' do
it 'searches, taps video, plays, and closes player' do
video_id = 'XfELJU1mRMg'
# Navigate to Search and find video
@search_helper.navigate_to_search
@search_helper.search(video_id)
@search_helper.wait_for_results
# Verify results view is displayed
expect(@search_helper.results_visible?).to be true
# Tap first video result thumbnail to start playback directly
@search_helper.tap_first_result_thumbnail
# Wait for player to expand and start playing
@player_helper.wait_for_player_expanded
@player_helper.wait_for_playback_started
# Take screenshot of playback for visual verification
@axe.screenshot('video_playback_piped_playing')
# Close the player
@player_helper.close_player
# Take screenshot after close attempt
@axe.screenshot('video_playback_piped_after_close')
end
end
end

View File

@@ -0,0 +1,71 @@
# frozen_string_literal: true
require_relative '../spec_helper'
RSpec.describe 'Video Playback', :smoke do
before(:all) do
# Boot simulator
@udid = UITest::Simulator.boot(UITest::Config.device)
# Build app (unless skipped)
UITest::App.build(
device: UITest::Config.device,
skip: UITest::Config.skip_build?
)
# Install and launch
UITest::App.install(udid: @udid)
UITest::App.launch(udid: @udid)
# Wait for app to stabilize
sleep UITest::Config.app_launch_wait
# Initialize helpers
@axe = UITest::Axe.new(@udid)
@instance_setup = UITest::InstanceSetup.new(@axe)
@search_helper = UITest::SearchHelper.new(@axe)
@player_helper = UITest::PlayerHelper.new(@axe)
# Ensure Yattee Server instance is configured
@instance_setup.ensure_yattee_server(UITest::Config.yattee_server_url)
end
after(:all) do
# Terminate app
UITest::App.terminate(udid: @udid, silent: true) if @udid
# Shutdown simulator unless --keep-simulator
UITest::Simulator.shutdown(@udid) if @udid && !UITest::Config.keep_simulator?
end
describe 'playing video from search' do
it 'searches, taps video, plays, and closes player' do
video_id = 'XfELJU1mRMg'
# Navigate to Search and find video
@search_helper.navigate_to_search
@search_helper.search(video_id)
@search_helper.wait_for_results
# Verify results view is displayed
expect(@search_helper.results_visible?).to be true
# Tap first video result thumbnail to start playback directly
# Note: Tapping thumbnail plays video, tapping text area opens info
@search_helper.tap_first_result_thumbnail
# Wait for player to expand and start playing
@player_helper.wait_for_player_expanded
@player_helper.wait_for_playback_started
# Take screenshot of playback for visual verification
@axe.screenshot('video_playback_playing')
# Close the player
@player_helper.close_player
# Take screenshot after close attempt
@axe.screenshot('video_playback_after_close')
end
end
end

View File

@@ -0,0 +1,73 @@
# frozen_string_literal: true
require_relative '../spec_helper'
RSpec.describe 'Yattee Server Instance', :smoke do
before(:all) do
# Boot simulator
@udid = UITest::Simulator.boot(UITest::Config.device)
# Build app (unless skipped)
UITest::App.build(
device: UITest::Config.device,
skip: UITest::Config.skip_build?
)
# Install and launch
UITest::App.install(udid: @udid)
UITest::App.launch(udid: @udid)
# Wait for app to stabilize
sleep UITest::Config.app_launch_wait
# Initialize AXe and InstanceSetup helper
@axe = UITest::Axe.new(@udid)
@instance_setup = UITest::InstanceSetup.new(@axe)
end
after(:all) do
# Terminate app
UITest::App.terminate(udid: @udid, silent: true) if @udid
# Shutdown simulator unless --keep-simulator
UITest::Simulator.shutdown(@udid) if @udid && !UITest::Config.keep_simulator?
end
describe 'adding via Detect & Add' do
it 'adds Yattee Server instance and verifies it appears in Sources' do
server_url = UITest::Config.yattee_server_url
server_host = UITest::Config.yattee_server_host
# Ensure we start from Library (check for text since inlineLarge title has no AXUniqueId)
expect(@axe).to have_text('Library')
# Remove existing instance (if any) and add fresh - always tests the add flow
@instance_setup.remove_and_add_yattee_server(server_url)
# Navigate to Sources to verify the instance was added
# Open Settings using accessibility identifier
@axe.tap_id('library.settingsButton')
sleep 1
expect(@axe).to have_element('settings.view')
# Navigate to Sources
@axe.tap_id('settings.row.sources')
sleep 0.5
# Verify we're on Sources list
expect(@axe).to have_element('sources.view')
# Verify instance appears in Sources list
expect(@axe).to have_element("sources.row.yatteeServer.#{server_host}")
# Close settings
begin
@axe.tap_label('Done')
rescue StandardError
nil
end
sleep 0.5
end
end
end