mirror of
https://github.com/yattee/yattee.git
synced 2026-02-19 17:29:45 +00:00
627 lines
21 KiB
Swift
627 lines
21 KiB
Swift
//
|
|
// NavigationTests.swift
|
|
// YatteeTests
|
|
//
|
|
// Tests for navigation components.
|
|
//
|
|
|
|
import Testing
|
|
import Foundation
|
|
import SwiftUI
|
|
@testable import Yattee
|
|
|
|
// MARK: - URLRouter Tests
|
|
|
|
@Suite("URLRouter Tests")
|
|
struct URLRouterTests {
|
|
let router = URLRouter()
|
|
|
|
// MARK: - YouTube URL Tests
|
|
|
|
@Test("Parse standard YouTube watch URL")
|
|
func standardWatchURL() {
|
|
let url = URL(string: "https://www.youtube.com/watch?v=dQw4w9WgXcQ")!
|
|
let destination = router.route(url)
|
|
|
|
if case .video(let source, _) = destination, case .id(let videoID) = source {
|
|
#expect(videoID.videoID == "dQw4w9WgXcQ")
|
|
if case .global = videoID.source {
|
|
// Expected
|
|
} else {
|
|
Issue.record("Expected global source")
|
|
}
|
|
} else {
|
|
Issue.record("Expected video destination")
|
|
}
|
|
}
|
|
|
|
@Test("Parse YouTube short URL")
|
|
func shortURL() {
|
|
let url = URL(string: "https://youtu.be/dQw4w9WgXcQ")!
|
|
let destination = router.route(url)
|
|
|
|
if case .video(let source, _) = destination, case .id(let videoID) = source {
|
|
#expect(videoID.videoID == "dQw4w9WgXcQ")
|
|
} else {
|
|
Issue.record("Expected video destination")
|
|
}
|
|
}
|
|
|
|
@Test("Parse YouTube embed URL")
|
|
func embedURL() {
|
|
let url = URL(string: "https://www.youtube.com/embed/dQw4w9WgXcQ")!
|
|
let destination = router.route(url)
|
|
|
|
if case .video(let source, _) = destination, case .id(let videoID) = source {
|
|
#expect(videoID.videoID == "dQw4w9WgXcQ")
|
|
} else {
|
|
Issue.record("Expected video destination")
|
|
}
|
|
}
|
|
|
|
@Test("Parse YouTube shorts URL")
|
|
func shortsURL() {
|
|
let url = URL(string: "https://www.youtube.com/shorts/abc123def")!
|
|
let destination = router.route(url)
|
|
|
|
if case .video(let source, _) = destination, case .id(let videoID) = source {
|
|
#expect(videoID.videoID == "abc123def")
|
|
} else {
|
|
Issue.record("Expected video destination")
|
|
}
|
|
}
|
|
|
|
@Test("Parse YouTube watch URL with timestamp")
|
|
func watchURLWithTimestamp() {
|
|
let url = URL(string: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=120")!
|
|
let destination = router.route(url)
|
|
|
|
if case .video(let source, _) = destination, case .id(let videoID) = source {
|
|
#expect(videoID.videoID == "dQw4w9WgXcQ")
|
|
} else {
|
|
Issue.record("Expected video destination")
|
|
}
|
|
}
|
|
|
|
@Test("Parse YouTube live URL")
|
|
func liveURL() {
|
|
let url = URL(string: "https://www.youtube.com/live/abc123def")!
|
|
let destination = router.route(url)
|
|
|
|
if case .video(let source, _) = destination, case .id(let videoID) = source {
|
|
#expect(videoID.videoID == "abc123def")
|
|
} else {
|
|
Issue.record("Expected video destination")
|
|
}
|
|
}
|
|
|
|
@Test("Parse mobile YouTube URL")
|
|
func mobileURL() {
|
|
let url = URL(string: "https://m.youtube.com/watch?v=dQw4w9WgXcQ")!
|
|
let destination = router.route(url)
|
|
|
|
if case .video(let source, _) = destination, case .id(let videoID) = source {
|
|
#expect(videoID.videoID == "dQw4w9WgXcQ")
|
|
} else {
|
|
Issue.record("Expected video destination")
|
|
}
|
|
}
|
|
|
|
// MARK: - PeerTube URL Tests
|
|
|
|
@Test("Parse PeerTube /w/ video URL")
|
|
func peertubeWURL() {
|
|
let url = URL(string: "https://framatube.org/w/abc123-def456-ghi789")!
|
|
let destination = router.route(url)
|
|
|
|
if case .video(let source, _) = destination, case .id(let videoID) = source {
|
|
#expect(videoID.videoID == "abc123-def456-ghi789")
|
|
if case .federated(_, let instance) = videoID.source {
|
|
#expect(instance.host == "framatube.org")
|
|
} else {
|
|
Issue.record("Expected federated source")
|
|
}
|
|
} else {
|
|
Issue.record("Expected video destination")
|
|
}
|
|
}
|
|
|
|
@Test("Parse PeerTube /videos/watch/ URL")
|
|
func peertubeVideosWatchURL() {
|
|
let url = URL(string: "https://peertube.social/videos/watch/abc123")!
|
|
let destination = router.route(url)
|
|
|
|
if case .video(let source, _) = destination, case .id(let videoID) = source {
|
|
#expect(videoID.videoID == "abc123")
|
|
if case .federated(_, let instance) = videoID.source {
|
|
#expect(instance.host == "peertube.social")
|
|
} else {
|
|
Issue.record("Expected federated source")
|
|
}
|
|
} else {
|
|
Issue.record("Expected video destination")
|
|
}
|
|
}
|
|
|
|
// MARK: - Custom Scheme Tests
|
|
|
|
@Test("Parse yattee:// video URL")
|
|
func customSchemeVideoURL() {
|
|
let url = URL(string: "yattee://video/dQw4w9WgXcQ")!
|
|
let destination = router.route(url)
|
|
|
|
if case .video(let source, _) = destination, case .id(let videoID) = source {
|
|
#expect(videoID.videoID == "dQw4w9WgXcQ")
|
|
} else {
|
|
Issue.record("Expected video destination")
|
|
}
|
|
}
|
|
|
|
@Test("Parse yattee:// channel URL")
|
|
func customSchemeChannelURL() {
|
|
let url = URL(string: "yattee://channel/UCtest123")!
|
|
let destination = router.route(url)
|
|
|
|
if case .channel(let channelID, _) = destination {
|
|
#expect(channelID == "UCtest123")
|
|
} else {
|
|
Issue.record("Expected channel destination")
|
|
}
|
|
}
|
|
|
|
// MARK: - Edge Cases
|
|
|
|
@Test("Unknown URL routes to external video for yt-dlp extraction")
|
|
func unknownURL() {
|
|
let url = URL(string: "https://example.com/something")!
|
|
let destination = router.route(url)
|
|
// Unknown URLs are now routed to externalVideo for potential yt-dlp extraction
|
|
if case .externalVideo(let extractedURL) = destination {
|
|
#expect(extractedURL == url)
|
|
} else {
|
|
Issue.record("Expected externalVideo destination for unknown URLs")
|
|
}
|
|
}
|
|
|
|
@Test("YouTube URL without video ID routes to external video")
|
|
func urlWithoutVideoID() {
|
|
let url = URL(string: "https://www.youtube.com/watch")!
|
|
let destination = router.route(url)
|
|
// YouTube URLs without video ID are now treated as potential external videos
|
|
if case .externalVideo(let extractedURL) = destination {
|
|
#expect(extractedURL == url)
|
|
} else {
|
|
Issue.record("Expected externalVideo destination")
|
|
}
|
|
}
|
|
|
|
@Test("Known non-PeerTube hosts route to external video")
|
|
func nonPeerTubeHosts() {
|
|
// Vimeo should not be parsed as PeerTube but routed to external video
|
|
let vimeoURL = URL(string: "https://vimeo.com/w/123456")!
|
|
let vimeoDestination = router.route(vimeoURL)
|
|
if case .externalVideo(let url) = vimeoDestination {
|
|
#expect(url == vimeoURL)
|
|
} else {
|
|
Issue.record("Expected externalVideo destination for Vimeo")
|
|
}
|
|
|
|
// Dailymotion should not be parsed as PeerTube but routed to external video
|
|
let dailymotionURL = URL(string: "https://dailymotion.com/w/123456")!
|
|
let dailymotionDestination = router.route(dailymotionURL)
|
|
if case .externalVideo(let url) = dailymotionDestination {
|
|
#expect(url == dailymotionURL)
|
|
} else {
|
|
Issue.record("Expected externalVideo destination for Dailymotion")
|
|
}
|
|
}
|
|
|
|
// MARK: - YouTube Channel URL Tests
|
|
|
|
@Test("Parse YouTube channel URL")
|
|
func parseChannelURL() {
|
|
let url = URL(string: "https://www.youtube.com/channel/UCxyz123")!
|
|
let channelID = router.parseYouTubeChannelURL(url)
|
|
#expect(channelID == "UCxyz123")
|
|
}
|
|
|
|
@Test("Parse YouTube handle URL")
|
|
func parseHandleURL() {
|
|
let url = URL(string: "https://www.youtube.com/@channelhandle")!
|
|
let channelID = router.parseYouTubeChannelURL(url)
|
|
#expect(channelID == "@channelhandle")
|
|
}
|
|
|
|
@Test("Parse YouTube /c/ custom URL")
|
|
func parseCustomURL() {
|
|
let url = URL(string: "https://www.youtube.com/c/CustomName")!
|
|
let channelID = router.parseYouTubeChannelURL(url)
|
|
#expect(channelID == "CustomName")
|
|
}
|
|
|
|
@Test("Parse YouTube /user/ URL")
|
|
func parseUserURL() {
|
|
let url = URL(string: "https://www.youtube.com/user/Username")!
|
|
let channelID = router.parseYouTubeChannelURL(url)
|
|
#expect(channelID == "Username")
|
|
}
|
|
|
|
@Test("Non-YouTube channel URL returns nil")
|
|
func nonYouTubeChannelURL() {
|
|
let url = URL(string: "https://example.com/channel/test")!
|
|
let channelID = router.parseYouTubeChannelURL(url)
|
|
#expect(channelID == nil)
|
|
}
|
|
|
|
// MARK: - YouTube Playlist URL Tests
|
|
|
|
@Test("Parse YouTube playlist URL")
|
|
func parsePlaylistURL() {
|
|
let url = URL(string: "https://www.youtube.com/playlist?list=PLtest123")!
|
|
let destination = router.route(url)
|
|
|
|
if case .playlist(.remote(let playlistID, _, _)) = destination {
|
|
#expect(playlistID.playlistID == "PLtest123")
|
|
if case .global = playlistID.source {
|
|
// Expected
|
|
} else {
|
|
Issue.record("Expected global source")
|
|
}
|
|
} else {
|
|
Issue.record("Expected playlist destination")
|
|
}
|
|
}
|
|
|
|
@Test("YouTube watch URL with list parameter routes to video not playlist")
|
|
func watchURLWithListParameter() {
|
|
// When a URL has both v= and list=, it's a video playing within a playlist
|
|
// We should route to the video, not the playlist
|
|
let url = URL(string: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=PLtest123")!
|
|
let destination = router.route(url)
|
|
|
|
if case .video(let source, _) = destination, case .id(let videoID) = source {
|
|
#expect(videoID.videoID == "dQw4w9WgXcQ")
|
|
} else {
|
|
Issue.record("Expected video destination for watch URL with list parameter")
|
|
}
|
|
}
|
|
|
|
// MARK: - YouTube Channel URL Routing Tests
|
|
|
|
@Test("Parse and route YouTube channel URL")
|
|
func routeChannelURL() {
|
|
let url = URL(string: "https://www.youtube.com/channel/UCxyz123")!
|
|
let destination = router.route(url)
|
|
|
|
if case .channel(let channelID, let source) = destination {
|
|
#expect(channelID == "UCxyz123")
|
|
if case .global = source {
|
|
// Expected
|
|
} else {
|
|
Issue.record("Expected global source")
|
|
}
|
|
} else {
|
|
Issue.record("Expected channel destination")
|
|
}
|
|
}
|
|
|
|
@Test("Parse and route YouTube handle URL")
|
|
func routeHandleURL() {
|
|
let url = URL(string: "https://www.youtube.com/@channelhandle")!
|
|
let destination = router.route(url)
|
|
|
|
if case .channel(let channelID, _) = destination {
|
|
#expect(channelID == "@channelhandle")
|
|
} else {
|
|
Issue.record("Expected channel destination for handle URL")
|
|
}
|
|
}
|
|
|
|
// MARK: - Custom Scheme Deep Link Tests
|
|
|
|
@Test("Parse yattee:// search URL")
|
|
func customSchemeSearchURL() {
|
|
let url = URL(string: "yattee://search?q=hello%20world")!
|
|
let destination = router.route(url)
|
|
|
|
if case .search(let query) = destination {
|
|
#expect(query == "hello world")
|
|
} else {
|
|
Issue.record("Expected search destination")
|
|
}
|
|
}
|
|
|
|
@Test("Parse yattee:// search URL without query returns nil")
|
|
func customSchemeSearchURLNoQuery() {
|
|
let url = URL(string: "yattee://search")!
|
|
let destination = router.route(url)
|
|
#expect(destination == nil)
|
|
}
|
|
|
|
@Test("Parse yattee:// playlist URL")
|
|
func customSchemePlaylistURL() {
|
|
let url = URL(string: "yattee://playlist/PLtest123")!
|
|
let destination = router.route(url)
|
|
|
|
if case .playlist(.remote(let playlistID, _, _)) = destination {
|
|
#expect(playlistID.playlistID == "PLtest123")
|
|
} else {
|
|
Issue.record("Expected playlist destination")
|
|
}
|
|
}
|
|
|
|
@Test("Parse yattee:// playlists URL")
|
|
func customSchemePlaylistsURL() {
|
|
let url = URL(string: "yattee://playlists")!
|
|
let destination = router.route(url)
|
|
#expect(destination == .playlists)
|
|
}
|
|
|
|
@Test("Parse yattee:// bookmarks URL")
|
|
func customSchemeBookmarksURL() {
|
|
let url = URL(string: "yattee://bookmarks")!
|
|
let destination = router.route(url)
|
|
#expect(destination == .bookmarks)
|
|
}
|
|
|
|
@Test("Parse yattee:// history URL")
|
|
func customSchemeHistoryURL() {
|
|
let url = URL(string: "yattee://history")!
|
|
let destination = router.route(url)
|
|
#expect(destination == .history)
|
|
}
|
|
|
|
@Test("Parse yattee:// downloads URL")
|
|
func customSchemeDownloadsURL() {
|
|
let url = URL(string: "yattee://downloads")!
|
|
let destination = router.route(url)
|
|
#expect(destination == .downloads)
|
|
}
|
|
|
|
@Test("Parse yattee:// channels URL")
|
|
func customSchemeChannelsURL() {
|
|
let url = URL(string: "yattee://channels")!
|
|
let destination = router.route(url)
|
|
#expect(destination == .manageChannels)
|
|
}
|
|
|
|
@Test("Parse yattee:// subscriptions URL")
|
|
func customSchemeSubscriptionsURL() {
|
|
let url = URL(string: "yattee://subscriptions")!
|
|
let destination = router.route(url)
|
|
#expect(destination == .subscriptionsFeed)
|
|
}
|
|
|
|
@Test("Parse yattee:// continue-watching URL")
|
|
func customSchemeContinueWatchingURL() {
|
|
let url = URL(string: "yattee://continue-watching")!
|
|
let destination = router.route(url)
|
|
#expect(destination == .continueWatching)
|
|
}
|
|
|
|
@Test("Parse yattee:// settings URL")
|
|
func customSchemeSettingsURL() {
|
|
let url = URL(string: "yattee://settings")!
|
|
let destination = router.route(url)
|
|
#expect(destination == .settings)
|
|
}
|
|
|
|
@Test("Parse yattee:// channel URL with PeerTube source")
|
|
func customSchemeChannelURLWithPeerTubeSource() {
|
|
let url = URL(string: "yattee://channel/channelid123?source=peertube&instance=https://peertube.social")!
|
|
let destination = router.route(url)
|
|
|
|
if case .channel(let channelID, let source) = destination {
|
|
#expect(channelID == "channelid123")
|
|
if case .federated(_, let instance) = source {
|
|
#expect(instance.host == "peertube.social")
|
|
} else {
|
|
Issue.record("Expected federated source")
|
|
}
|
|
} else {
|
|
Issue.record("Expected channel destination with federated source")
|
|
}
|
|
}
|
|
|
|
@Test("Parse yattee:// channel URL with PeerTube source but no instance falls back to Global")
|
|
func customSchemeChannelURLPeerTubeNoInstance() {
|
|
// If source=peertube but no instance is provided, should fall back to Global
|
|
let url = URL(string: "yattee://channel/channelid123?source=peertube")!
|
|
let destination = router.route(url)
|
|
|
|
if case .channel(let channelID, let source) = destination {
|
|
#expect(channelID == "channelid123")
|
|
if case .global = source {
|
|
// Expected - falls back to global when instance missing
|
|
} else {
|
|
Issue.record("Expected global source fallback")
|
|
}
|
|
} else {
|
|
Issue.record("Expected channel destination")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - NavigationDestination Tests
|
|
|
|
@Suite("NavigationDestination Tests")
|
|
struct NavigationDestinationTests {
|
|
|
|
@Test("Video destinations are hashable")
|
|
func videoHashable() {
|
|
let video1 = NavigationDestination.video(.id(.global("abc")))
|
|
let video2 = NavigationDestination.video(.id(.global("abc")))
|
|
let video3 = NavigationDestination.video(.id(.global("def")))
|
|
|
|
#expect(video1 == video2)
|
|
#expect(video1 != video3)
|
|
}
|
|
|
|
@Test("Different destination types are not equal")
|
|
func differentTypes() {
|
|
let video = NavigationDestination.video(.id(.global("abc")))
|
|
let channel = NavigationDestination.channel("abc", .global(provider: ContentSource.youtubeProvider))
|
|
|
|
#expect(video != channel)
|
|
}
|
|
|
|
@Test("Settings destination")
|
|
func settingsDestination() {
|
|
let settings1 = NavigationDestination.settings
|
|
let settings2 = NavigationDestination.settings
|
|
|
|
#expect(settings1 == settings2)
|
|
}
|
|
|
|
@Test("Downloads destination")
|
|
func downloadsDestination() {
|
|
let downloads1 = NavigationDestination.downloads
|
|
let downloads2 = NavigationDestination.downloads
|
|
|
|
#expect(downloads1 == downloads2)
|
|
}
|
|
|
|
@Test("Search destination with query")
|
|
func searchDestination() {
|
|
let search1 = NavigationDestination.search("hello world")
|
|
let search2 = NavigationDestination.search("hello world")
|
|
let search3 = NavigationDestination.search("different query")
|
|
|
|
#expect(search1 == search2)
|
|
#expect(search1 != search3)
|
|
}
|
|
|
|
@Test("Playlist destination")
|
|
func playlistDestination() {
|
|
let playlist1 = NavigationDestination.playlist(.remote(PlaylistID(source: .global(provider: ContentSource.youtubeProvider), playlistID: "PLtest"), instance: nil))
|
|
let playlist2 = NavigationDestination.playlist(.remote(PlaylistID(source: .global(provider: ContentSource.youtubeProvider), playlistID: "PLtest"), instance: nil))
|
|
let playlist3 = NavigationDestination.playlist(.remote(PlaylistID(source: .global(provider: ContentSource.youtubeProvider), playlistID: "PLother"), instance: nil))
|
|
|
|
#expect(playlist1 == playlist2)
|
|
#expect(playlist1 != playlist3)
|
|
}
|
|
}
|
|
|
|
// MARK: - NavigationCoordinator Tests
|
|
|
|
@Suite("NavigationCoordinator Tests")
|
|
@MainActor
|
|
struct NavigationCoordinatorTests {
|
|
|
|
@Test("Initial state")
|
|
func initialState() {
|
|
let coordinator = NavigationCoordinator()
|
|
|
|
#expect(coordinator.selectedTab == .home)
|
|
#expect(coordinator.path.isEmpty)
|
|
#expect(coordinator.presentedSheet == nil)
|
|
}
|
|
|
|
@Test("Navigate to destination sets pending navigation")
|
|
func navigateToDestination() {
|
|
let coordinator = NavigationCoordinator()
|
|
let destination = NavigationDestination.video(.id(.global("test123")))
|
|
|
|
coordinator.navigate(to: destination)
|
|
|
|
#expect(coordinator.pendingNavigation == destination)
|
|
}
|
|
|
|
@Test("Multiple navigations update pending navigation")
|
|
func multipleNavigations() {
|
|
let coordinator = NavigationCoordinator()
|
|
|
|
coordinator.navigate(to: .video(.id(.global("1"))))
|
|
coordinator.navigate(to: .video(.id(.global("2"))))
|
|
coordinator.navigate(to: .video(.id(.global("3"))))
|
|
|
|
// Only the last navigation is pending
|
|
#expect(coordinator.pendingNavigation == .video(.id(.global("3"))))
|
|
}
|
|
|
|
@Test("Pop to root clears path")
|
|
func popToRoot() {
|
|
let coordinator = NavigationCoordinator()
|
|
|
|
// Manually add to path to test popToRoot
|
|
coordinator.path.append(NavigationDestination.video(.id(.global("1"))))
|
|
coordinator.path.append(NavigationDestination.video(.id(.global("2"))))
|
|
coordinator.path.append(NavigationDestination.video(.id(.global("3"))))
|
|
|
|
#expect(coordinator.path.count == 3)
|
|
|
|
coordinator.popToRoot()
|
|
|
|
#expect(coordinator.path.isEmpty)
|
|
}
|
|
|
|
@Test("Pop removes one level")
|
|
func pop() {
|
|
let coordinator = NavigationCoordinator()
|
|
|
|
// Manually add to path to test pop
|
|
coordinator.path.append(NavigationDestination.video(.id(.global("1"))))
|
|
coordinator.path.append(NavigationDestination.video(.id(.global("2"))))
|
|
|
|
#expect(coordinator.path.count == 2)
|
|
|
|
coordinator.pop()
|
|
|
|
#expect(coordinator.path.count == 1)
|
|
}
|
|
|
|
@Test("Pop on empty path is safe")
|
|
func popOnEmptyPath() {
|
|
let coordinator = NavigationCoordinator()
|
|
|
|
// Should not crash
|
|
coordinator.pop()
|
|
|
|
#expect(coordinator.path.isEmpty)
|
|
}
|
|
|
|
@Test("Switch tab")
|
|
func switchTab() {
|
|
let coordinator = NavigationCoordinator()
|
|
|
|
coordinator.selectedTab = .search
|
|
|
|
#expect(coordinator.selectedTab == .search)
|
|
}
|
|
|
|
@Test("Handle URL sets pending navigation")
|
|
func handleURL() {
|
|
let coordinator = NavigationCoordinator()
|
|
let url = URL(string: "https://youtube.com/watch?v=test123")!
|
|
|
|
coordinator.handle(url: url)
|
|
|
|
#expect(coordinator.pendingNavigation != nil)
|
|
}
|
|
|
|
@Test("Handle unknown URL does nothing")
|
|
func handleUnknownURL() {
|
|
let coordinator = NavigationCoordinator()
|
|
let url = URL(string: "https://example.com/unknown")!
|
|
|
|
coordinator.handle(url: url)
|
|
|
|
#expect(coordinator.path.isEmpty)
|
|
}
|
|
}
|
|
|
|
// MARK: - ConnectivityMonitor Tests
|
|
|
|
@Suite("ConnectivityMonitor Tests")
|
|
@MainActor
|
|
struct ConnectivityMonitorTests {
|
|
|
|
@Test("Initial state assumes online")
|
|
func initialState() {
|
|
let monitor = ConnectivityMonitor()
|
|
|
|
// By default, assume online until NWPathMonitor reports otherwise
|
|
#expect(monitor.isOnline == true)
|
|
}
|
|
}
|