mirror of
https://github.com/Cuteminded/jellyframe.git
synced 2026-10-25 08:00:33 +01:00
Add season browser for series details
Replaces the flat series episode list with a dedicated season/episode browser in the details view. Added Jellyfin client helpers for season and paginated episode endpoints, introduced reusable `SeriesBrowser` and `EpisodeCard` UI components, and wired section scrolling/playback events through the details page. Updated HTTP/details tests and added comprehensive series browser coverage for pagination, retries, empty states, and stale-response handling.
This commit is contained in:
1 parent
ed9774f151
commit
ebe8c141af
11 files changed
+328
-32
No files matched your search
@@ -94,6 +94,12 @@ func resume_items() -> Dictionary:
|
||||
func details(id: String) -> Dictionary:
|
||||
return await request("/Users/%s/Items/%s" % [user_id, id.uri_encode()])
|
||||
|
||||
func seasons(series_id: String) -> Dictionary:
|
||||
return await request("/Shows/%s/Seasons" % series_id.uri_encode() + query({"UserId": user_id, "Fields": "Overview,UserData", "EnableUserData": "true"}))
|
||||
|
||||
func episodes(series_id: String, season_id: String, offset: int = 0) -> Dictionary:
|
||||
return await request("/Shows/%s/Episodes" % series_id.uri_encode() + query({"UserId": user_id, "SeasonId": season_id, "StartIndex": offset, "Limit": 100, "Fields": "Overview,UserData", "EnableUserData": "true", "IsMissing": "false"}))
|
||||
|
||||
func image_url(item: Dictionary, backdrop: bool = false) -> String:
|
||||
var kind := "Backdrop/0" if backdrop else "Primary"
|
||||
var tags: Array = item.get("BackdropImageTags", []) if item.get("BackdropImageTags") else []
|
||||
|
||||
@@ -240,11 +240,6 @@ func _details(item: Dictionary) -> void:
|
||||
view.back_requested.connect(_show_library)
|
||||
page.add_child(view)
|
||||
status.text = "Ready."
|
||||
if detail.get("Type", "") == "Series":
|
||||
var episodes := await api.request("/Shows/%s/Episodes" % detail.Id + api.query({"UserId": api.user_id, "Limit": 100, "Fields": "UserData"}))
|
||||
if not is_instance_valid(view) or not view.is_inside_tree(): return
|
||||
view.set_episodes(episodes.data.get("Items", []) if episodes.ok else [], "Could not load episodes. Return to the library and try again." if not episodes.ok else "")
|
||||
if not episodes.ok: status.text = episodes.error
|
||||
|
||||
func _start(item: Dictionary) -> void:
|
||||
if not video.available():
|
||||
|
||||
+13
-20
@@ -9,7 +9,7 @@ var api: JellyfinClient
|
||||
var artwork: ArtworkLoader
|
||||
var scroll: ScrollContainer
|
||||
var body: VBoxContainer
|
||||
var episode_list: VBoxContainer
|
||||
var series_browser: SeriesBrowser
|
||||
var play_button: Button
|
||||
var back_button: Button
|
||||
var title_label: Label
|
||||
@@ -38,11 +38,13 @@ func _ready() -> void:
|
||||
_build_header()
|
||||
_build_information()
|
||||
if item.get("Type", "") == "Series":
|
||||
Widgets.label("Episodes", body, 28)
|
||||
episode_list = VBoxContainer.new()
|
||||
episode_list.add_theme_constant_override("separation", 12)
|
||||
body.add_child(episode_list)
|
||||
Widgets.label("Loading episodes...", episode_list, 22)
|
||||
series_browser = SeriesBrowser.new()
|
||||
series_browser.api = api
|
||||
series_browser.artwork = artwork
|
||||
series_browser.series_id = str(item.Id)
|
||||
series_browser.play_requested.connect(func(episode: Dictionary): play_requested.emit(episode))
|
||||
series_browser.section_changed.connect(_scroll_to_series)
|
||||
body.add_child(series_browser)
|
||||
_build_people()
|
||||
|
||||
func _build_header() -> void:
|
||||
@@ -103,7 +105,6 @@ func _build_header() -> void:
|
||||
|
||||
func _build_playback(parent: VBoxContainer) -> void:
|
||||
if item.get("Type", "") == "Series":
|
||||
_muted("Choose an episode below to start watching.", parent)
|
||||
return
|
||||
var user: Dictionary = item.get("UserData", {}) if item.get("UserData") else {}
|
||||
var position := int(user.get("PlaybackPositionTicks", 0))
|
||||
@@ -162,19 +163,6 @@ func _build_people() -> void:
|
||||
var role := str(person.get("Role") if person.get("Role") else person.get("Type", ""))
|
||||
_wrapped(str(person.Name) + (" / " + role if not role.is_empty() else ""), names, 22)
|
||||
|
||||
func set_episodes(items: Array, error: String = "") -> void:
|
||||
for child in episode_list.get_children():
|
||||
episode_list.remove_child(child)
|
||||
child.queue_free()
|
||||
if not error.is_empty():
|
||||
_wrapped(error, episode_list)
|
||||
return
|
||||
if items.is_empty():
|
||||
_muted("No episodes available.", episode_list)
|
||||
for episode in items:
|
||||
var button := Widgets.button("S%s E%s %s" % [episode.get("ParentIndexNumber", "?"), episode.get("IndexNumber", "?"), episode.get("Name", "Episode")], func(): play_requested.emit(episode), episode_list)
|
||||
button.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
|
||||
func _image(parent: Node) -> TextureRect:
|
||||
var image := TextureRect.new()
|
||||
image.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
@@ -183,6 +171,11 @@ func _image(parent: Node) -> TextureRect:
|
||||
parent.add_child(image)
|
||||
return image
|
||||
|
||||
func _scroll_to_series() -> void:
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
if is_inside_tree(): scroll.scroll_vertical = int(series_browser.position.y)
|
||||
|
||||
func _wrapped(text: String, parent: Node, font_size: int = 22) -> Label:
|
||||
var label := Widgets.label(text, parent, font_size)
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
class_name EpisodeCard
|
||||
extends PanelContainer
|
||||
|
||||
signal chosen(item: Dictionary)
|
||||
|
||||
var item: Dictionary
|
||||
var artwork: ArtworkLoader
|
||||
var play_button: Button
|
||||
var thumbnail: TextureRect
|
||||
var title_label: Label
|
||||
|
||||
func _ready() -> void:
|
||||
add_theme_stylebox_override("panel", Widgets.surface(Color("162133"), 14, 18))
|
||||
var row := BoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 22)
|
||||
add_child(row)
|
||||
row.vertical = size.x < 740
|
||||
resized.connect(func(): row.vertical = size.x < 740)
|
||||
var image_column := VBoxContainer.new()
|
||||
image_column.add_theme_constant_override("separation", 12)
|
||||
image_column.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
image_column.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||
row.add_child(image_column)
|
||||
var frame := PanelContainer.new()
|
||||
frame.custom_minimum_size = Vector2(240, 135)
|
||||
frame.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||
frame.add_theme_stylebox_override("panel", Widgets.surface(Color("25334a"), 10))
|
||||
image_column.add_child(frame)
|
||||
var fallback := Widgets.label("No preview available", frame, 20)
|
||||
fallback.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
fallback.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
thumbnail = TextureRect.new()
|
||||
thumbnail.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
thumbnail.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
|
||||
thumbnail.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
frame.add_child(thumbnail)
|
||||
artwork.fetch(thumbnail, item)
|
||||
var text := VBoxContainer.new()
|
||||
text.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
text.add_theme_constant_override("separation", 12)
|
||||
row.add_child(text)
|
||||
title_label = _label("%s. %s" % [item.get("IndexNumber", "?"), item.get("Name", "Episode")], text, 26)
|
||||
var facts := DetailsMetadata.facts(item)
|
||||
var user: Dictionary = item.get("UserData", {}) if item.get("UserData") else {}
|
||||
var position := int(user.get("PlaybackPositionTicks", 0))
|
||||
if position > 0: facts.append("Continue from " + DetailsMetadata.duration(position))
|
||||
if not facts.is_empty():
|
||||
_label(" / ".join(facts), text, 20).add_theme_color_override("font_color", Color("b0bbce"))
|
||||
_label(str(item.get("Overview") if item.get("Overview") else "No description available."), text, 22)
|
||||
play_button = Widgets.button("Resume episode" if position > 0 else "Play episode", func(): chosen.emit(item), image_column)
|
||||
play_button.custom_minimum_size = Vector2(190, 56)
|
||||
play_button.accessibility_name = play_button.text + " " + str(item.get("Name", "Episode"))
|
||||
|
||||
func _label(value: String, parent: Node, font_size: int) -> Label:
|
||||
var label := Widgets.label(value, parent, font_size)
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
label.add_theme_constant_override("line_spacing", 5)
|
||||
return label
|
||||
@@ -0,0 +1 @@
|
||||
uid://oxidgmg6k3fd
|
||||
@@ -0,0 +1,121 @@
|
||||
class_name SeriesBrowser
|
||||
extends VBoxContainer
|
||||
|
||||
signal play_requested(item: Dictionary)
|
||||
signal section_changed
|
||||
|
||||
var api: JellyfinClient
|
||||
var artwork: ArtworkLoader
|
||||
var series_id := ""
|
||||
var season_grid: HFlowContainer
|
||||
var season_section: VBoxContainer
|
||||
var episode_section: VBoxContainer
|
||||
var episode_list: VBoxContainer
|
||||
var episode_heading: Label
|
||||
var season_description: Label
|
||||
var all_seasons_button: Button
|
||||
var generation := 0
|
||||
|
||||
func _ready() -> void:
|
||||
add_theme_constant_override("separation", 20)
|
||||
season_section = VBoxContainer.new()
|
||||
season_section.add_theme_constant_override("separation", 18)
|
||||
add_child(season_section)
|
||||
Widgets.label("Seasons", season_section, 28)
|
||||
season_grid = HFlowContainer.new()
|
||||
season_grid.add_theme_constant_override("h_separation", 18)
|
||||
season_grid.add_theme_constant_override("v_separation", 18)
|
||||
season_section.add_child(season_grid)
|
||||
episode_section = VBoxContainer.new()
|
||||
episode_section.add_theme_constant_override("separation", 18)
|
||||
add_child(episode_section)
|
||||
var navigation := HBoxContainer.new()
|
||||
episode_section.add_child(navigation)
|
||||
all_seasons_button = Widgets.button("All seasons", _show_seasons, navigation)
|
||||
episode_heading = _label("", episode_section, 28)
|
||||
season_description = _label("", episode_section, 22)
|
||||
episode_list = VBoxContainer.new()
|
||||
episode_list.add_theme_constant_override("separation", 16)
|
||||
episode_section.add_child(episode_list)
|
||||
episode_section.hide()
|
||||
load_seasons()
|
||||
|
||||
func load_seasons() -> void:
|
||||
generation += 1
|
||||
var current := generation
|
||||
_clear(season_grid)
|
||||
_label("Loading seasons...", season_grid, 22)
|
||||
var result := await api.seasons(series_id)
|
||||
if not _current(current): return
|
||||
_clear(season_grid)
|
||||
if not result.ok:
|
||||
_label("Could not load seasons.", season_grid, 22)
|
||||
Widgets.button("Retry seasons", load_seasons, season_grid)
|
||||
return
|
||||
var seasons: Array = result.data.get("Items", [])
|
||||
if seasons.is_empty():
|
||||
_label("No seasons available.", season_grid, 22)
|
||||
for season in seasons:
|
||||
var card := MediaCard.new()
|
||||
card.item = season
|
||||
card.artwork = artwork
|
||||
card.chosen.connect(open_season)
|
||||
season_grid.add_child(card)
|
||||
|
||||
func open_season(season: Dictionary) -> void:
|
||||
generation += 1
|
||||
var current := generation
|
||||
season_section.hide()
|
||||
episode_section.show()
|
||||
all_seasons_button.grab_focus()
|
||||
episode_heading.text = str(season.get("Name", "Season")) + " episodes"
|
||||
season_description.text = str(season.get("Overview") if season.get("Overview") else "")
|
||||
season_description.visible = not season_description.text.is_empty()
|
||||
_clear(episode_list)
|
||||
_label("Loading episodes...", episode_list, 22)
|
||||
section_changed.emit()
|
||||
var items: Array = []
|
||||
while true:
|
||||
var result := await api.episodes(series_id, str(season.Id), items.size())
|
||||
if not _current(current): return
|
||||
if not result.ok:
|
||||
_clear(episode_list)
|
||||
_label("Could not load episodes.", episode_list, 22)
|
||||
Widgets.button("Retry episodes", open_season.bind(season), episode_list)
|
||||
section_changed.emit()
|
||||
return
|
||||
var batch: Array = result.data.get("Items", [])
|
||||
items.append_array(batch)
|
||||
if batch.is_empty() or items.size() >= int(result.data.get("TotalRecordCount", items.size())): break
|
||||
_clear(episode_list)
|
||||
if items.is_empty(): _label("No episodes available in this season.", episode_list, 22)
|
||||
for episode in items:
|
||||
var card := EpisodeCard.new()
|
||||
card.item = episode
|
||||
card.artwork = artwork
|
||||
card.chosen.connect(func(chosen: Dictionary): play_requested.emit(chosen))
|
||||
episode_list.add_child(card)
|
||||
section_changed.emit()
|
||||
|
||||
func _show_seasons() -> void:
|
||||
generation += 1
|
||||
episode_section.hide()
|
||||
season_section.show()
|
||||
section_changed.emit()
|
||||
if season_grid.get_child_count() > 0:
|
||||
var first := season_grid.get_child(0) as Control
|
||||
if first and first.focus_mode != Control.FOCUS_NONE: first.grab_focus()
|
||||
|
||||
func _current(current: int) -> bool:
|
||||
return current == generation and is_inside_tree()
|
||||
|
||||
func _clear(parent: Node) -> void:
|
||||
for child in parent.get_children():
|
||||
parent.remove_child(child)
|
||||
child.queue_free()
|
||||
|
||||
func _label(value: String, parent: Node, font_size: int) -> Label:
|
||||
var label := Widgets.label(value, parent, font_size)
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
return label
|
||||
@@ -0,0 +1 @@
|
||||
uid://cct22i6ylma4e
|
||||
@@ -1,5 +1,9 @@
|
||||
extends SceneTree
|
||||
|
||||
class FixtureApi extends JellyfinClient:
|
||||
func seasons(_series_id: String) -> Dictionary:
|
||||
return {"ok": true, "data": {"Items": []}}
|
||||
|
||||
var failures := 0
|
||||
var selected: Array = []
|
||||
var back_count := 0
|
||||
@@ -33,7 +37,7 @@ func show_details(shell: UiShell, item: Dictionary) -> DetailsView:
|
||||
shell.page.remove_child(child)
|
||||
child.queue_free()
|
||||
var view := DetailsView.new()
|
||||
view.api = JellyfinClient.new()
|
||||
view.api = FixtureApi.new()
|
||||
view.add_child(view.api)
|
||||
view.item = item
|
||||
view.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
@@ -81,11 +85,7 @@ func run_checks() -> void:
|
||||
view = show_details(shell, {"Id": "series", "Name": "Example series", "Type": "Series"})
|
||||
await settle()
|
||||
check(view.play_button == null, "Series must select episodes instead of playing the series itself.")
|
||||
var episode := {"Id": "episode", "Name": "Pilot", "ParentIndexNumber": 1, "IndexNumber": 1}
|
||||
view.set_episodes([episode])
|
||||
view.episode_list.get_child(0).pressed.emit()
|
||||
check(selected.back() == episode, "Episode selection must open the chosen episode.")
|
||||
view.set_episodes([], "Could not load episodes.")
|
||||
check(view.episode_list.get_child(0).text == "Could not load episodes.", "Episode failures must remain visible.")
|
||||
check(view.series_browser != null, "Series must show a season browser.")
|
||||
check(view.series_browser.season_grid.get_child(0).text == "No seasons available.", "An empty series must have a readable fallback.")
|
||||
print("Details checks: ", "PASS" if failures == 0 else str(failures) + " failures")
|
||||
quit(1 if failures else 0)
|
||||
@@ -58,6 +58,11 @@ func run_checks() -> void:
|
||||
check("ParentId=movies-id" in last_request and "StartIndex=48" in last_request and "SearchTerm=space" in last_request, "Search and pagination stay inside the selected library")
|
||||
await api.views()
|
||||
check(last_request.begins_with("GET /jellyfin/UserViews?") and "UserId=fixture-user" in last_request, "Sidebar loads the signed-in user's libraries")
|
||||
await api.seasons("show id")
|
||||
check(last_request.begins_with("GET /jellyfin/Shows/show%20id/Seasons?"), "Seasons load from the selected series with an escaped ID")
|
||||
await api.episodes("show id", "season & id", 100)
|
||||
check("SeasonId=season%20%26%20id" in last_request and "StartIndex=100" in last_request, "Episode pagination stays inside the selected season")
|
||||
check("Fields=Overview%2CUserData" in last_request and "IsMissing=false" in last_request, "Episode cards request summaries and exclude missing episodes")
|
||||
code = 401
|
||||
payload = ""
|
||||
result = await api.request("/Users/Me")
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
extends SceneTree
|
||||
|
||||
class FixtureApi extends JellyfinClient:
|
||||
var calls: Array = []
|
||||
var fail_seasons := false
|
||||
var fail_episodes := false
|
||||
var empty_seasons := false
|
||||
var empty_episodes := false
|
||||
var season_items := [{"Id": "one", "Name": "Season 1", "IndexNumber": 1}, {"Id": "two", "Name": "Season 2", "IndexNumber": 2}]
|
||||
|
||||
func seasons(_series_id: String) -> Dictionary:
|
||||
await get_tree().process_frame
|
||||
if fail_seasons: return {"ok": false, "error": "Offline"}
|
||||
return {"ok": true, "data": {"Items": [] if empty_seasons else season_items}}
|
||||
|
||||
func episodes(series_id: String, season_id: String, offset: int = 0) -> Dictionary:
|
||||
calls.append([series_id, season_id, offset])
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
if fail_episodes: return {"ok": false, "error": "Offline"}
|
||||
var items := []
|
||||
if not empty_episodes:
|
||||
for index in range(offset, mini(offset + 2, 4)):
|
||||
items.append({"Id": season_id + str(index), "Name": ["Seekest Thou the Road", "Circle Sewn with Fate", "Through the trials", "The final chapter"][index], "IndexNumber": index + 1, "RunTimeTicks": 24000000000, "CommunityRating": 7.0, "Overview": "Agatha and her companions face another trial on the Witches' Road.", "UserData": {"PlaybackPositionTicks": 3000000000 if index == 0 else 0, "Played": index == 1}})
|
||||
return {"ok": true, "data": {"Items": items, "TotalRecordCount": 0 if empty_episodes else 4}}
|
||||
|
||||
var failures := 0
|
||||
var selected: Array = []
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run_checks")
|
||||
|
||||
func settle() -> void:
|
||||
for frame in range(12): await process_frame
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
failures += 1
|
||||
push_error(message)
|
||||
|
||||
func capture(path: String) -> void:
|
||||
if "--capture" not in OS.get_cmdline_user_args(): return
|
||||
await RenderingServer.frame_post_draw
|
||||
root.get_texture().get_image().save_png(path)
|
||||
|
||||
func run_checks() -> void:
|
||||
root.size = Vector2i(1536, 960)
|
||||
var shell := UiShell.new()
|
||||
root.add_child(shell)
|
||||
shell.set_signed_in(true)
|
||||
shell.select("tv", "TV shows")
|
||||
shell.set_libraries([{"Id": "movies", "Name": "Movies", "CollectionType": "movies"}, {"Id": "tv", "Name": "TV shows", "CollectionType": "tvshows"}])
|
||||
var api := FixtureApi.new()
|
||||
root.add_child(api)
|
||||
var view := DetailsView.new()
|
||||
view.api = api
|
||||
view.item = {"Id": "agatha", "Name": "Agatha All Along", "Type": "Series", "ProductionYear": 2024, "OfficialRating": "NL-12", "CommunityRating": 7.2, "Genres": ["Action", "Adventure", "Comedy"], "Overview": "Agatha sets out on the Witches' Road with a new coven to reclaim her powers.", "Studios": [{"Name": "Disney+"}]}
|
||||
view.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
view.play_requested.connect(func(item: Dictionary): selected.append(item))
|
||||
shell.page.add_child(view)
|
||||
await settle()
|
||||
var browser := view.series_browser
|
||||
check(browser.season_grid.get_child_count() == 2, "Series must show each season as a poster card.")
|
||||
view.scroll.ensure_control_visible(browser.season_section)
|
||||
await settle()
|
||||
await capture("res://dist/series-preview.png")
|
||||
browser.season_grid.get_child(0).pressed.emit()
|
||||
await settle()
|
||||
check(api.calls == [["agatha", "one", 0], ["agatha", "one", 2]], "All episode pages must stay in the chosen season.")
|
||||
check(browser.episode_list.get_child_count() == 4, "Every episode page must appear in the list.")
|
||||
var first: EpisodeCard = browser.episode_list.get_child(0)
|
||||
check(first.play_button.text == "Resume episode", "Started episodes must offer Resume.")
|
||||
first.play_button.pressed.emit()
|
||||
check(selected.back().Id == "one0", "Playback must receive the selected episode, not the series or season.")
|
||||
check(browser.episode_list.get_child(1).play_button.text == "Play episode", "Unstarted episodes must offer Play.")
|
||||
await capture("res://dist/episodes-preview.png")
|
||||
for width in [1280, 960]:
|
||||
root.size.x = width
|
||||
await settle()
|
||||
check(first.size.x <= view.scroll.size.x, "Episode cards must fit the available panel width.")
|
||||
check(first.title_label.get_global_rect().end.x <= view.scroll.get_global_rect().end.x, "Episode text must remain inside the panel.")
|
||||
root.size.x = 1536
|
||||
browser.open_season(api.season_items[0])
|
||||
browser.all_seasons_button.pressed.emit()
|
||||
browser.open_season(api.season_items[1])
|
||||
await settle()
|
||||
check(browser.episode_list.get_child(0).item.Id == "two0", "Changing season must ignore an older episode response.")
|
||||
api.fail_episodes = true
|
||||
browser.open_season(api.season_items[0])
|
||||
await settle()
|
||||
check(browser.episode_list.get_child(0).text == "Could not load episodes.", "Episode errors must be visible.")
|
||||
api.fail_episodes = false
|
||||
browser.episode_list.get_child(1).pressed.emit()
|
||||
await settle()
|
||||
check(browser.episode_list.get_child_count() == 4, "Retry must restore the selected season's episodes.")
|
||||
api.empty_episodes = true
|
||||
browser.open_season(api.season_items[1])
|
||||
await settle()
|
||||
check(browser.episode_list.get_child(0).text == "No episodes available in this season.", "Empty seasons must have a readable message.")
|
||||
browser.all_seasons_button.pressed.emit()
|
||||
api.fail_seasons = true
|
||||
browser.load_seasons()
|
||||
await settle()
|
||||
check(browser.season_grid.get_child(0).text == "Could not load seasons.", "Season errors must offer recovery.")
|
||||
api.fail_seasons = false
|
||||
browser.season_grid.get_child(1).pressed.emit()
|
||||
await settle()
|
||||
check(browser.season_grid.get_child_count() == 2, "Retry must restore season selection.")
|
||||
api.empty_seasons = true
|
||||
browser.load_seasons()
|
||||
await settle()
|
||||
check(browser.season_grid.get_child(0).text == "No seasons available.", "Empty series must have a readable message.")
|
||||
print("Series checks: ", "PASS" if failures == 0 else str(failures) + " failures")
|
||||
quit(1 if failures else 0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://brq5jgygov6ld
|
||||
Reference in new issue
Block a user