mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 21:22:53 +00:00
Pages list - Info is pinned first and always enabled in every layout: normalized pages force it to the front and enabled (older presets self-correct), disabling it is a no-op, and any move that would displace it snaps back. The iOS/macOS list renders it outside the reorderable ForEach (no drag handle, nothing drops above it) and the editors hide its enabled toggle; tvOS also hides its move buttons. The "at least one page must stay enabled" guard and its strings are gone since Info always counts. - iPhone never renders queue or comments as pages, so its Sections list hides them (PanelSettings.hiddenSettingsPageTypes). Reordering goes through moveVisiblePages, which keeps hidden entries in their stored slots. Comments get their own group on the Details Panel screen (pill mode + avatars) and the overlay hosts pass the avatars flag. Default visibility - "When Player Opens" is now configured per orientation (portraitDefaultVisibility / landscapeDefaultVisibility). The legacy single value is still decoded and seeds both until set individually; presets that never set it default to portrait Visible, landscape Hidden. macOS shows one picker (no portrait layout). - "Remember Last" no longer leaks across orientations: separate remembered flags, written by the portrait toggles and by the wide and corner layouts respectively, with the old shared flag read once as a fallback. The corner layout seeded from the portrait resolution before; it now uses landscape. Section options (iOS inline-sections hosts) - Chapters and Related editors gain "Expanded by Default" (on by default). The Info editor gains a Description group with "Expanded by Default" (off) and a "Collapsed Lines" stepper (1–10, default 3). Applied when the panel appears and on each video change. - Expanding a collapsed section scrolls its header to the top of the panel: PanelPageScrollView can hand its scroll proxy to page content and each inline section is wrapped in one anchored container. - The portrait panel's bottom fade sized its touch blocker from a stale "expanded comments pill" answer that ignored collapse on scroll, so a collapsed section header (the last item) sat under it and could not be tapped. The predicate now tracks the pill's actual collapse state.
296 lines
13 KiB
Swift
296 lines
13 KiB
Swift
//
|
|
// PanelSettingsTests.swift
|
|
// YatteeTests
|
|
//
|
|
// Tests for details panel settings model (Codable compatibility).
|
|
//
|
|
|
|
import Foundation
|
|
import Testing
|
|
@testable import Yattee
|
|
|
|
@Suite("Panel Settings Tests")
|
|
struct PanelSettingsTests {
|
|
private func roundTrip(_ settings: PanelSettings) throws -> PanelSettings {
|
|
let encoded = try JSONEncoder().encode(settings)
|
|
return try JSONDecoder().decode(PanelSettings.self, from: encoded)
|
|
}
|
|
|
|
// MARK: - Round Trip
|
|
|
|
@Test("Default settings round-trip")
|
|
func defaultRoundTrip() throws {
|
|
let decoded = try roundTrip(.default)
|
|
#expect(decoded == PanelSettings.default)
|
|
}
|
|
|
|
@Test("Customized settings round-trip")
|
|
func customizedRoundTrip() throws {
|
|
var settings = PanelSettings.default
|
|
settings.layoutMode = .corner
|
|
settings.setDefaultVisibility(.remember, for: .landscape)
|
|
settings.showPageBarLabels = false
|
|
settings.setEnabled(false, for: .related)
|
|
settings.movePages(fromOffsets: IndexSet(integer: 1), toOffset: 0)
|
|
settings.updateSettings(
|
|
.info(InfoPageSettings(
|
|
showStatsRow: false,
|
|
showDescription: false,
|
|
descriptionCollapsedLines: 5,
|
|
descriptionExpandedByDefault: true
|
|
)),
|
|
for: .info
|
|
)
|
|
|
|
let decoded = try roundTrip(settings)
|
|
#expect(decoded == settings)
|
|
#expect(decoded.effectiveLayoutMode == .corner)
|
|
#expect(decoded.effectiveDefaultVisibility(for: .landscape) == .remember)
|
|
#expect(decoded.effectiveDefaultVisibility(for: .portrait) == .visible)
|
|
#expect(decoded.effectiveShowPageBarLabels == false)
|
|
#expect(!decoded.isPageEnabled(.related))
|
|
|
|
let info = decoded.configuration(for: .info)?.infoSettings
|
|
#expect(info?.effectiveShowStatsRow == false)
|
|
#expect(info?.effectiveShowDescription == false)
|
|
#expect(info?.effectiveShowChannelRow == true)
|
|
#expect(info?.effectiveDescriptionCollapsedLines == 5)
|
|
#expect(info?.effectiveDescriptionExpandedByDefault == true)
|
|
#expect(InfoPageSettings(descriptionCollapsedLines: 0).effectiveDescriptionCollapsedLines == 1)
|
|
#expect(InfoPageSettings().effectiveDescriptionCollapsedLines == 3)
|
|
}
|
|
|
|
@Test("Inline sections follow the configured page order and enablement")
|
|
func inlineSectionsFollowOrder() {
|
|
var settings = PanelSettings.default
|
|
// Default order: info, comments, related, chapters, queue
|
|
#expect(settings.inlineSections().map(\.pageType) == [.related, .chapters])
|
|
|
|
// Move chapters (index 3) above related (index 2)
|
|
settings.movePages(fromOffsets: IndexSet(integer: 3), toOffset: 2)
|
|
#expect(settings.inlineSections().map(\.pageType) == [.chapters, .related])
|
|
|
|
settings.setEnabled(false, for: .related)
|
|
#expect(settings.inlineSections().map(\.pageType) == [.chapters])
|
|
|
|
settings.updateSettings(.chapters(ChaptersPageSettings(showThumbnails: false)), for: .chapters)
|
|
#expect(settings.inlineSections().first?.chaptersSettings.effectiveShowThumbnails == false)
|
|
}
|
|
|
|
@Test("Per-page element settings round-trip for every page type")
|
|
func perPageSettingsRoundTrip() throws {
|
|
var settings = PanelSettings.default
|
|
settings.updateSettings(.comments(CommentsPageSettings(showAvatars: false)), for: .comments)
|
|
settings.updateSettings(.related(RelatedPageSettings(showThumbnails: false)), for: .related)
|
|
settings.updateSettings(.chapters(ChaptersPageSettings(showThumbnails: false, expandedByDefault: false)), for: .chapters)
|
|
settings.updateSettings(.queue(QueuePageSettings(showHistory: false)), for: .queue)
|
|
|
|
let decoded = try roundTrip(settings)
|
|
#expect(decoded == settings)
|
|
#expect(decoded.configuration(for: .comments)?.commentsSettings.effectiveShowAvatars == false)
|
|
#expect(decoded.configuration(for: .related)?.relatedSettings.effectiveShowThumbnails == false)
|
|
#expect(decoded.configuration(for: .chapters)?.chaptersSettings.effectiveShowThumbnails == false)
|
|
#expect(decoded.configuration(for: .chapters)?.chaptersSettings.effectiveExpandedByDefault == false)
|
|
#expect(decoded.configuration(for: .related)?.relatedSettings.effectiveExpandedByDefault == true)
|
|
#expect(decoded.configuration(for: .queue)?.queueSettings.effectiveShowHistory == false)
|
|
}
|
|
|
|
@Test("Typed accessors fall back to defaults for mismatched payloads")
|
|
func typedAccessorFallbacks() {
|
|
// An info payload read through the comments accessor yields defaults.
|
|
let page = PanelPageConfiguration(
|
|
pageType: .comments,
|
|
settings: .info(InfoPageSettings(showStatsRow: false))
|
|
)
|
|
#expect(page.commentsSettings.effectiveShowAvatars == true)
|
|
#expect(page.queueSettings.effectiveShowHistory == true)
|
|
}
|
|
|
|
// MARK: - Backward Compatibility
|
|
|
|
@Test("Layout without panelSettings decodes and falls back to default")
|
|
func layoutWithoutPanelSettings() throws {
|
|
let layout = PlayerControlsLayout.default
|
|
var json = try #require(
|
|
try JSONSerialization.jsonObject(with: JSONEncoder().encode(layout)) as? [String: Any]
|
|
)
|
|
json.removeValue(forKey: "panelSettings")
|
|
let data = try JSONSerialization.data(withJSONObject: json)
|
|
|
|
let decoded = try JSONDecoder().decode(PlayerControlsLayout.self, from: data)
|
|
#expect(decoded.panelSettings == nil)
|
|
#expect(decoded.effectivePanelSettings == .default)
|
|
}
|
|
|
|
@Test("Minimal page entries decode with defaults")
|
|
func minimalPageEntries() throws {
|
|
let json = """
|
|
{ "pages": [ { "pageType": "info" }, { "pageType": "comments" } ] }
|
|
"""
|
|
let decoded = try JSONDecoder().decode(PanelSettings.self, from: Data(json.utf8))
|
|
#expect(decoded.pages.count == 2)
|
|
#expect(decoded.pages.allSatisfy { $0.isEnabled })
|
|
#expect(decoded.pages.allSatisfy { $0.region == .primary })
|
|
#expect(decoded.effectiveLayoutMode == .stacked)
|
|
#expect(decoded.effectiveDefaultVisibility(for: .portrait) == .visible)
|
|
#expect(decoded.effectiveDefaultVisibility(for: .landscape) == .hidden)
|
|
}
|
|
|
|
// MARK: - Forward Compatibility
|
|
|
|
@Test("Unknown page type from a future version is dropped")
|
|
func unknownPageTypeDropped() throws {
|
|
let json = """
|
|
{ "pages": [
|
|
{ "pageType": "info" },
|
|
{ "pageType": "futureHologram", "isEnabled": true },
|
|
{ "pageType": "queue" }
|
|
] }
|
|
"""
|
|
let decoded = try JSONDecoder().decode(PanelSettings.self, from: Data(json.utf8))
|
|
#expect(decoded.pages.map(\.pageType) == [.info, .queue])
|
|
}
|
|
|
|
@Test("Unknown settings discriminator decodes to nil settings")
|
|
func unknownSettingsDiscriminator() throws {
|
|
let json = """
|
|
{ "pages": [
|
|
{ "pageType": "info", "settings": { "type": "futureKind", "futureKind": { "x": 1 } } }
|
|
] }
|
|
"""
|
|
let decoded = try JSONDecoder().decode(PanelSettings.self, from: Data(json.utf8))
|
|
#expect(decoded.pages.count == 1)
|
|
#expect(decoded.pages.first?.settings == nil)
|
|
// nil settings fall back to defaults
|
|
#expect(decoded.pages.first?.infoSettings.effectiveShowDescription == true)
|
|
}
|
|
|
|
@Test("Legacy single default visibility seeds both orientations until set individually")
|
|
func legacyDefaultVisibilitySeedsBothOrientations() throws {
|
|
let json = """
|
|
{ "pages": [ { "pageType": "info" } ], "defaultVisibility": "remember" }
|
|
"""
|
|
var settings = try JSONDecoder().decode(PanelSettings.self, from: Data(json.utf8))
|
|
#expect(settings.effectiveDefaultVisibility(for: .portrait) == .remember)
|
|
#expect(settings.effectiveDefaultVisibility(for: .landscape) == .remember)
|
|
|
|
settings.setDefaultVisibility(.visible, for: .portrait)
|
|
#expect(settings.effectiveDefaultVisibility(for: .portrait) == .visible)
|
|
#expect(settings.effectiveDefaultVisibility(for: .landscape) == .remember)
|
|
|
|
let decoded = try roundTrip(settings)
|
|
#expect(decoded == settings)
|
|
|
|
#expect(PanelDefaultVisibility.remember.resolve(lastVisible: true))
|
|
#expect(!PanelDefaultVisibility.hidden.resolve(lastVisible: true))
|
|
#expect(PanelDefaultVisibility.visible.resolve(lastVisible: false))
|
|
}
|
|
|
|
@Test("Unknown layoutMode and defaultVisibility decode to nil")
|
|
func unknownEnumValues() throws {
|
|
let json = """
|
|
{ "pages": [ { "pageType": "info" } ],
|
|
"layoutMode": "floating",
|
|
"defaultVisibility": "sometimes" }
|
|
"""
|
|
let decoded = try JSONDecoder().decode(PanelSettings.self, from: Data(json.utf8))
|
|
#expect(decoded.layoutMode == nil)
|
|
#expect(decoded.defaultVisibility == nil)
|
|
#expect(decoded.effectiveLayoutMode == .stacked)
|
|
#expect(decoded.effectiveDefaultVisibility(for: .landscape) == .hidden)
|
|
}
|
|
|
|
// MARK: - Normalization
|
|
|
|
@Test("Normalized pages append missing types as disabled")
|
|
func normalizedPagesAppendMissing() throws {
|
|
let json = """
|
|
{ "pages": [ { "pageType": "info" } ] }
|
|
"""
|
|
let decoded = try JSONDecoder().decode(PanelSettings.self, from: Data(json.utf8))
|
|
let normalized = decoded.normalizedPages
|
|
#expect(normalized.count == PanelPageType.allCases.count)
|
|
#expect(normalized.first?.pageType == .info)
|
|
#expect(normalized.dropFirst().allSatisfy { !$0.isEnabled })
|
|
}
|
|
|
|
@Test("Info is pinned first and always enabled")
|
|
func infoPinnedFirstAndEnabled() throws {
|
|
let json = """
|
|
{ "pages": [
|
|
{ "pageType": "related" },
|
|
{ "pageType": "info", "isEnabled": false },
|
|
{ "pageType": "chapters" }
|
|
] }
|
|
"""
|
|
var settings = try JSONDecoder().decode(PanelSettings.self, from: Data(json.utf8))
|
|
#expect(settings.normalizedPages.first?.pageType == .info)
|
|
#expect(settings.isPageEnabled(.info))
|
|
#expect(settings.enabledPages().map(\.pageType) == [.info, .related, .chapters])
|
|
|
|
settings.setEnabled(false, for: .info)
|
|
#expect(settings.isPageEnabled(.info))
|
|
|
|
// Moving info down, or another page above it, leaves info first
|
|
settings.movePages(fromOffsets: IndexSet(integer: 0), toOffset: 3)
|
|
#expect(settings.pages.first?.pageType == .info)
|
|
settings.movePages(fromOffsets: IndexSet(integer: 2), toOffset: 0)
|
|
#expect(settings.pages.map(\.pageType).prefix(3) == [.info, .chapters, .related])
|
|
}
|
|
|
|
@Test("Moving displayed pages keeps hidden pages in their slots")
|
|
func moveVisiblePagesKeepsHiddenSlots() {
|
|
var settings = PanelSettings.default
|
|
// Stored: info, comments, related, chapters, queue. A list that hides
|
|
// info (pinned) and queue displays comments, related, chapters.
|
|
let visible: [PanelPageType] = [.comments, .related, .chapters]
|
|
|
|
// Move chapters (displayed index 2) to the top of the displayed list
|
|
settings.moveVisiblePages(visible, fromOffsets: IndexSet(integer: 2), toOffset: 0)
|
|
#expect(settings.pages.map(\.pageType) == [.info, .chapters, .comments, .related, .queue])
|
|
|
|
// Hidden page in the middle keeps its slot
|
|
settings.movePages(fromOffsets: IndexSet(integer: 4), toOffset: 2)
|
|
#expect(settings.pages.map(\.pageType) == [.info, .chapters, .queue, .comments, .related])
|
|
settings.moveVisiblePages([.chapters, .comments, .related], fromOffsets: IndexSet(integer: 0), toOffset: 3)
|
|
#expect(settings.pages.map(\.pageType) == [.info, .comments, .queue, .related, .chapters])
|
|
}
|
|
|
|
@Test("Enabled pages filter by region")
|
|
func enabledPagesRegionFilter() {
|
|
let settings = PanelSettings.default
|
|
let primary = settings.enabledPages(in: .primary).map(\.pageType)
|
|
let belowVideo = settings.enabledPages(in: .belowVideo).map(\.pageType)
|
|
let all = settings.enabledPages().map(\.pageType)
|
|
|
|
#expect(primary == [.related, .chapters, .queue])
|
|
#expect(belowVideo == [.info, .comments])
|
|
#expect(all == [.info, .comments, .related, .chapters, .queue])
|
|
}
|
|
|
|
// MARK: - Mutations
|
|
|
|
@Test("Disabling and re-enabling a page preserves order")
|
|
func toggleKeepsOrder() {
|
|
var settings = PanelSettings.default
|
|
settings.setEnabled(false, for: .comments)
|
|
#expect(!settings.isPageEnabled(.comments))
|
|
settings.setEnabled(true, for: .comments)
|
|
#expect(settings.pages.map(\.pageType) == PanelSettings.default.pages.map(\.pageType))
|
|
}
|
|
|
|
@Test("Region assignment and layout mode round-trip through Codable")
|
|
func regionAndLayoutModeRoundTrip() throws {
|
|
var settings = PanelSettings.default
|
|
settings.layoutMode = .corner
|
|
settings.setRegion(.belowVideo, for: .related)
|
|
#expect(settings.configuration(for: .related)?.region == .belowVideo)
|
|
#expect(settings.enabledPages(in: .primary).map(\.pageType) == [.chapters, .queue])
|
|
|
|
let data = try JSONEncoder().encode(settings)
|
|
let decoded = try JSONDecoder().decode(PanelSettings.self, from: data)
|
|
#expect(decoded.effectiveLayoutMode == .corner)
|
|
#expect(decoded.configuration(for: .related)?.region == .belowVideo)
|
|
}
|
|
}
|