mirror of
https://github.com/yattee/yattee.git
synced 2026-04-11 10:06:58 +00:00
Yattee v2 rewrite
This commit is contained in:
71
spec/ui/smoke/app_launch_spec.rb
Normal file
71
spec/ui/smoke/app_launch_spec.rb
Normal 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
|
||||
353
spec/ui/smoke/import_playlists_piped_spec.rb
Normal file
353
spec/ui/smoke/import_playlists_piped_spec.rb
Normal 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
|
||||
353
spec/ui/smoke/import_playlists_spec.rb
Normal file
353
spec/ui/smoke/import_playlists_spec.rb
Normal 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
|
||||
313
spec/ui/smoke/import_subscriptions_piped_spec.rb
Normal file
313
spec/ui/smoke/import_subscriptions_piped_spec.rb
Normal 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
|
||||
315
spec/ui/smoke/import_subscriptions_spec.rb
Normal file
315
spec/ui/smoke/import_subscriptions_spec.rb
Normal 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
|
||||
73
spec/ui/smoke/invidious_spec.rb
Normal file
73
spec/ui/smoke/invidious_spec.rb
Normal 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
|
||||
73
spec/ui/smoke/piped_spec.rb
Normal file
73
spec/ui/smoke/piped_spec.rb
Normal 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
|
||||
47
spec/ui/smoke/player_controls_preview_spec.rb
Normal file
47
spec/ui/smoke/player_controls_preview_spec.rb
Normal 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
|
||||
63
spec/ui/smoke/search_spec.rb
Normal file
63
spec/ui/smoke/search_spec.rb
Normal 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
|
||||
107
spec/ui/smoke/settings_spec.rb
Normal file
107
spec/ui/smoke/settings_spec.rb
Normal 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
|
||||
76
spec/ui/smoke/video_playback_invidious_spec.rb
Normal file
76
spec/ui/smoke/video_playback_invidious_spec.rb
Normal 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
|
||||
79
spec/ui/smoke/video_playback_piped_spec.rb
Normal file
79
spec/ui/smoke/video_playback_piped_spec.rb
Normal 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
|
||||
71
spec/ui/smoke/video_playback_spec.rb
Normal file
71
spec/ui/smoke/video_playback_spec.rb
Normal 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
|
||||
73
spec/ui/smoke/yattee_server_spec.rb
Normal file
73
spec/ui/smoke/yattee_server_spec.rb
Normal 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
|
||||
Reference in New Issue
Block a user