mirror of
https://github.com/yattee/yattee.git
synced 2026-02-19 17:29:45 +00:00
Add URL scheme UI tests for deep link navigation
Test yattee:// custom scheme URLs navigate to correct screens: playlists, bookmarks, history, downloads, channels, subscriptions, continue-watching, and search. Handles iOS system confirmation dialog via coordinate taps since it's invisible to AXe. Settings deep link is excluded (known app bug - doesn't render when pushed to nav stack).
This commit is contained in:
392
spec/ui/smoke/url_schemes_spec.rb
Normal file
392
spec/ui/smoke/url_schemes_spec.rb
Normal file
@@ -0,0 +1,392 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../spec_helper'
|
||||
|
||||
RSpec.describe 'URL Schemes', :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
|
||||
|
||||
# Open a URL and dismiss the iOS "Open in Yattee?" system confirmation dialog.
|
||||
# The dialog is a system banner invisible to AXe (like the password save dialog),
|
||||
# so we must dismiss it with coordinate taps.
|
||||
# On iPhone 17 Pro (393x852pt), the "Open" button appears on the right side
|
||||
# of a centered banner. The vertical position varies depending on content behind it.
|
||||
# IMPORTANT: Tapping outside the dialog dismisses it as "Cancel", so we check
|
||||
# whether the dialog is still showing (empty AXe tree) before each tap.
|
||||
def open_url(url)
|
||||
UITest::Simulator.open_url(@udid, url)
|
||||
sleep 0.8
|
||||
|
||||
# "Open" button positions from highest to lowest - dialog position varies
|
||||
# based on the content behind it (higher on Home, lower on pushed views)
|
||||
open_button_positions = [
|
||||
{ x: 280, y: 350 },
|
||||
{ x: 290, y: 400 },
|
||||
{ x: 290, y: 460 },
|
||||
{ x: 280, y: 480 }
|
||||
]
|
||||
|
||||
open_button_positions.each do |pos|
|
||||
# Check if dialog is still blocking (AXe tree empty when system dialog is up)
|
||||
tree = @axe.describe_ui
|
||||
app_children = tree.is_a?(Array) ? tree.first&.dig('children') : nil
|
||||
break if app_children && !app_children.empty?
|
||||
|
||||
@axe.tap_coordinates(x: pos[:x], y: pos[:y])
|
||||
sleep 0.5
|
||||
end
|
||||
end
|
||||
|
||||
# Navigate back to Home tab between tests
|
||||
def navigate_to_home
|
||||
# Try edge swipes to pop any pushed views (up to 5 times)
|
||||
5.times do
|
||||
break if @axe.element_exists?('home.settingsButton')
|
||||
|
||||
@axe.swipe(start_x: 0, start_y: 400, end_x: 200, end_y: 400, duration: 0.3)
|
||||
sleep 0.5
|
||||
end
|
||||
|
||||
# Fallback: tap Home tab bar item
|
||||
unless @axe.element_exists?('home.settingsButton')
|
||||
begin
|
||||
@axe.tap_label('Home')
|
||||
sleep 0.5
|
||||
rescue UITest::Axe::AxeError
|
||||
# Already on Home or tab not found
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Wait for text to become visible with polling
|
||||
def wait_for_text(text, timeout: UITest::Config.element_timeout)
|
||||
start_time = Time.now
|
||||
|
||||
loop do
|
||||
return true if @axe.text_visible?(text)
|
||||
|
||||
if Time.now - start_time > timeout
|
||||
@axe.screenshot("debug-wait-for-#{text.downcase.gsub(/\s+/, '-')}")
|
||||
raise "Text '#{text}' not visible after #{timeout} seconds"
|
||||
end
|
||||
|
||||
sleep 0.5
|
||||
end
|
||||
end
|
||||
|
||||
# Wait for element to become visible with polling
|
||||
def wait_for_element(identifier, timeout: UITest::Config.element_timeout)
|
||||
start_time = Time.now
|
||||
|
||||
loop do
|
||||
return true if @axe.element_exists?(identifier)
|
||||
|
||||
if Time.now - start_time > timeout
|
||||
@axe.screenshot("debug-wait-for-#{identifier.gsub('.', '-')}")
|
||||
raise "Element '#{identifier}' not found after #{timeout} seconds"
|
||||
end
|
||||
|
||||
sleep 0.5
|
||||
end
|
||||
end
|
||||
|
||||
describe 'yattee:// navigation URLs' do
|
||||
after(:each) do
|
||||
navigate_to_home
|
||||
end
|
||||
|
||||
it 'opens Playlists via yattee://playlists' do
|
||||
open_url('yattee://playlists')
|
||||
sleep 1
|
||||
wait_for_text('Playlists')
|
||||
expect(@axe).to have_text('Playlists')
|
||||
@axe.screenshot('url-scheme-playlists')
|
||||
end
|
||||
|
||||
it 'opens Bookmarks via yattee://bookmarks' do
|
||||
open_url('yattee://bookmarks')
|
||||
sleep 1
|
||||
wait_for_text('Bookmarks')
|
||||
expect(@axe).to have_text('Bookmarks')
|
||||
@axe.screenshot('url-scheme-bookmarks')
|
||||
end
|
||||
|
||||
it 'opens History via yattee://history' do
|
||||
open_url('yattee://history')
|
||||
sleep 1
|
||||
wait_for_text('History')
|
||||
expect(@axe).to have_text('History')
|
||||
@axe.screenshot('url-scheme-history')
|
||||
end
|
||||
|
||||
it 'opens Downloads via yattee://downloads' do
|
||||
open_url('yattee://downloads')
|
||||
sleep 1
|
||||
wait_for_text('Downloads')
|
||||
expect(@axe).to have_text('Downloads')
|
||||
@axe.screenshot('url-scheme-downloads')
|
||||
end
|
||||
|
||||
it 'opens Channels via yattee://channels' do
|
||||
open_url('yattee://channels')
|
||||
sleep 1
|
||||
wait_for_text('Channels')
|
||||
expect(@axe).to have_text('Channels')
|
||||
@axe.screenshot('url-scheme-channels')
|
||||
end
|
||||
|
||||
it 'opens Subscriptions via yattee://subscriptions' do
|
||||
open_url('yattee://subscriptions')
|
||||
sleep 1
|
||||
wait_for_text('Subscriptions')
|
||||
expect(@axe).to have_text('Subscriptions')
|
||||
@axe.screenshot('url-scheme-subscriptions')
|
||||
end
|
||||
|
||||
it 'opens Continue Watching via yattee://continue-watching' do
|
||||
open_url('yattee://continue-watching')
|
||||
sleep 1
|
||||
wait_for_text('Continue Watching')
|
||||
expect(@axe).to have_text('Continue Watching')
|
||||
@axe.screenshot('url-scheme-continue-watching')
|
||||
end
|
||||
|
||||
# NOTE: yattee://settings deep link is broken in the app -
|
||||
# handlePendingNavigation pushes .settings onto homePath but it doesn't render.
|
||||
# Skipping until the app's deep link handler is fixed to present Settings properly.
|
||||
|
||||
it 'opens Search via yattee://search?q=test' do
|
||||
open_url('yattee://search?q=test')
|
||||
sleep 1
|
||||
# SearchView pushed via deep link has a sparse accessibility tree -
|
||||
# only the nav bar (AXUniqueId "Search") is exposed, not its children.
|
||||
# Verify navigation by checking the nav bar identifier.
|
||||
wait_for_element('Search')
|
||||
expect(@axe).to have_element('Search')
|
||||
@axe.screenshot('url-scheme-search')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe 'URL Schemes with Backend', :url_backend do
|
||||
before(:all) do
|
||||
skip 'Backend URL not configured (set YATTEE_SERVER_URL or INVIDIOUS_URL)' unless backend_available?
|
||||
|
||||
@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)
|
||||
@player = UITest::PlayerHelper.new(@axe)
|
||||
@instance_setup = UITest::InstanceSetup.new(@axe)
|
||||
|
||||
# Set up backend instance
|
||||
setup_backend_instance
|
||||
end
|
||||
|
||||
after(:all) do
|
||||
UITest::App.terminate(udid: @udid, silent: true) if @udid
|
||||
UITest::Simulator.shutdown(@udid) if @udid && !UITest::Config.keep_simulator?
|
||||
end
|
||||
|
||||
def self.backend_available?
|
||||
ENV['YATTEE_SERVER_URL'] || ENV['INVIDIOUS_URL']
|
||||
end
|
||||
|
||||
def backend_available?
|
||||
self.class.backend_available?
|
||||
end
|
||||
|
||||
def setup_backend_instance
|
||||
if ENV['YATTEE_SERVER_URL']
|
||||
@instance_setup.ensure_yattee_server(ENV['YATTEE_SERVER_URL'])
|
||||
elsif ENV['INVIDIOUS_URL']
|
||||
@instance_setup.ensure_invidious(ENV['INVIDIOUS_URL'])
|
||||
end
|
||||
end
|
||||
|
||||
# Open a URL and dismiss the iOS "Open in Yattee?" system confirmation dialog
|
||||
def open_url(url)
|
||||
UITest::Simulator.open_url(@udid, url)
|
||||
sleep 0.8
|
||||
|
||||
open_button_positions = [
|
||||
{ x: 280, y: 350 },
|
||||
{ x: 290, y: 400 },
|
||||
{ x: 290, y: 460 },
|
||||
{ x: 280, y: 480 }
|
||||
]
|
||||
|
||||
open_button_positions.each do |pos|
|
||||
tree = @axe.describe_ui
|
||||
app_children = tree.is_a?(Array) ? tree.first&.dig('children') : nil
|
||||
break if app_children && !app_children.empty?
|
||||
|
||||
@axe.tap_coordinates(x: pos[:x], y: pos[:y])
|
||||
sleep 0.5
|
||||
end
|
||||
end
|
||||
|
||||
# Navigate back to Home tab
|
||||
def navigate_to_home
|
||||
5.times do
|
||||
break if @axe.element_exists?('home.settingsButton')
|
||||
|
||||
@axe.swipe(start_x: 0, start_y: 400, end_x: 200, end_y: 400, duration: 0.3)
|
||||
sleep 0.5
|
||||
end
|
||||
|
||||
unless @axe.element_exists?('home.settingsButton')
|
||||
begin
|
||||
@axe.tap_label('Home')
|
||||
sleep 0.5
|
||||
rescue UITest::Axe::AxeError
|
||||
# Already on Home
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Wait for text with timeout
|
||||
def wait_for_text(text, timeout: 15)
|
||||
start_time = Time.now
|
||||
|
||||
loop do
|
||||
return true if @axe.text_visible?(text)
|
||||
|
||||
if Time.now - start_time > timeout
|
||||
@axe.screenshot("debug-backend-wait-#{text.downcase.gsub(/\s+/, '-')}")
|
||||
raise "Text '#{text}' not visible after #{timeout} seconds"
|
||||
end
|
||||
|
||||
sleep 0.5
|
||||
end
|
||||
end
|
||||
|
||||
# Wait for any content to load (not an empty state)
|
||||
def wait_for_content_loaded(timeout: 20)
|
||||
start_time = Time.now
|
||||
|
||||
loop do
|
||||
# Content loaded if we see video titles, channel names, etc.
|
||||
# Check that we're not just on an empty/loading screen
|
||||
tree = @axe.describe_ui
|
||||
tree_str = tree.to_s
|
||||
|
||||
# Look for signs of loaded content (not just nav titles)
|
||||
has_content = tree_str.include?('AXImage') ||
|
||||
tree_str.include?('AXStaticText') && tree_str.length > 2000
|
||||
|
||||
return true if has_content
|
||||
|
||||
if Time.now - start_time > timeout
|
||||
@axe.screenshot('debug-content-load-timeout')
|
||||
raise "Content did not load after #{timeout} seconds"
|
||||
end
|
||||
|
||||
sleep 1
|
||||
end
|
||||
end
|
||||
|
||||
# Close player if open, then navigate home
|
||||
def cleanup_after_video
|
||||
if @player.player_expanded?
|
||||
@player.close_player
|
||||
sleep 0.5
|
||||
end
|
||||
navigate_to_home
|
||||
end
|
||||
|
||||
describe 'yattee:// content URLs' do
|
||||
after(:each) do
|
||||
cleanup_after_video
|
||||
end
|
||||
|
||||
it 'opens video via yattee://video/{id}' do
|
||||
open_url('yattee://video/dQw4w9WgXcQ')
|
||||
sleep 3
|
||||
wait_for_content_loaded
|
||||
@axe.screenshot('url-scheme-video')
|
||||
end
|
||||
|
||||
it 'opens channel via yattee://channel/{id}' do
|
||||
open_url('yattee://channel/UC_x5XG1OV2P6uZZ5FSM9Ttw')
|
||||
sleep 3
|
||||
wait_for_content_loaded
|
||||
@axe.screenshot('url-scheme-channel')
|
||||
end
|
||||
|
||||
it 'opens playlist via yattee://playlist/{id}' do
|
||||
open_url('yattee://playlist/PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf')
|
||||
sleep 3
|
||||
wait_for_content_loaded
|
||||
@axe.screenshot('url-scheme-playlist')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'YouTube URL parsing' do
|
||||
after(:each) do
|
||||
cleanup_after_video
|
||||
end
|
||||
|
||||
it 'opens video via youtube.com/watch URL' do
|
||||
open_url('https://www.youtube.com/watch?v=dQw4w9WgXcQ')
|
||||
sleep 3
|
||||
wait_for_content_loaded
|
||||
@axe.screenshot('url-scheme-youtube-watch')
|
||||
end
|
||||
|
||||
it 'opens video via youtu.be short URL' do
|
||||
open_url('https://youtu.be/dQw4w9WgXcQ')
|
||||
sleep 3
|
||||
wait_for_content_loaded
|
||||
@axe.screenshot('url-scheme-youtube-short')
|
||||
end
|
||||
|
||||
it 'opens video via youtube.com/shorts URL' do
|
||||
open_url('https://www.youtube.com/shorts/dQw4w9WgXcQ')
|
||||
sleep 3
|
||||
wait_for_content_loaded
|
||||
@axe.screenshot('url-scheme-youtube-shorts')
|
||||
end
|
||||
|
||||
it 'opens playlist via youtube.com/playlist URL' do
|
||||
open_url('https://www.youtube.com/playlist?list=PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf')
|
||||
sleep 3
|
||||
wait_for_content_loaded
|
||||
@axe.screenshot('url-scheme-youtube-playlist')
|
||||
end
|
||||
|
||||
it 'opens channel via youtube.com/@handle URL' do
|
||||
open_url('https://www.youtube.com/@Google')
|
||||
sleep 3
|
||||
wait_for_content_loaded
|
||||
@axe.screenshot('url-scheme-youtube-handle')
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user