import at_record_web/model.{ type Entry, Entry, Failure, Grid, LoggedIn, LoggedOut, Model, Notice, Rows, ShelfFailed, ShelfLoaded, ShelfLoading, Warning, blank_form, } import at_record_web/msg.{ ClearNotice, FormTitle, GotBrowseAdd, GotShelf, GotShelfMore, HandleChanged, RetryShelf, SetDisplay, SetView, ShelfData, ShowMoreCrate, } import at_record_web/pages/crate import at_record_web/update.{update} import at_record_web/view import gleam/dict import gleam/http/response.{Response} import gleam/int import gleam/list import gleam/option.{None, Some} import gleam/string import lustre/element import rsvp import support.{ an_entry, base, empty_effect, logged_in, rendered_empty_crate, rendered_loaded_crate, unauthorized, } pub fn blank_form_defaults_to_owned_test() { assert blank_form().status == "owned" } pub fn handle_changed_updates_login_handle_test() { let #(model, _effect) = update(base(), HandleChanged("alice.test")) assert model.login_handle == "alice.test" } pub fn form_title_updates_form_test() { let #(model, _effect) = update(base(), FormTitle("Spiderland")) assert model.form.title == "Spiderland" } pub fn shelf_401_logs_out_test() { let #(model, _) = update(logged_in(), GotShelf(Error(rsvp.HttpError(Response(401, [], ""))))) assert model.auth == LoggedOut } pub fn shelf_500_keeps_auth_and_marks_failed_test() { let #(model, _) = update(logged_in(), GotShelf(Error(rsvp.HttpError(Response(500, [], ""))))) assert model.auth == LoggedIn("alice.test") assert model.shelf == ShelfFailed assert case model.notice { Some(Notice(Failure, _)) -> True _ -> False } } pub fn shelf_network_error_keeps_auth_and_marks_failed_test() { let #(model, _) = update(logged_in(), GotShelf(Error(rsvp.NetworkError))) assert model.auth == LoggedIn("alice.test") assert model.shelf == ShelfFailed assert case model.notice { Some(Notice(Failure, _)) -> True _ -> False } } pub fn shelf_401_clears_to_empty_loaded_test() { let #(model, _) = update(logged_in(), GotShelf(Error(rsvp.HttpError(Response(401, [], ""))))) assert model.auth == LoggedOut assert model.shelf == ShelfLoaded([]) } pub fn shelf_ok_marks_loaded_test() { let #(model, _) = update( logged_in(), GotShelf(Ok(ShelfData("alice.test", [], dict.new(), None))), ) assert model.shelf == ShelfLoaded([]) assert model.busy == False } pub fn retry_shelf_resets_to_loading_and_reloads_test() { let failed = Model(..logged_in(), shelf: ShelfFailed, busy: False) let #(model, effect) = update(failed, RetryShelf) assert model.shelf == ShelfLoading assert model.busy == True assert model.notice == None // A real load effect is dispatched (not effect.none()). assert effect != empty_effect() } pub fn set_view_resets_to_loading_test() { let seeded = Model(..logged_in(), crate_window: 120) let #(updated, _) = update(seeded, SetView("wanted")) assert updated.shelf == ShelfLoading assert updated.view == "wanted" // Switching filter rewinds the window so the new view starts from the top. assert updated.crate_window == model.crate_window_size } // A cursor is scoped to the view it was minted under, so both reloads drop // it (and any fetch in flight) rather than resume into the wrong pages. pub fn set_view_and_retry_shelf_reset_the_cursor_and_loading_state_test() { let seeded = Model( ..logged_in(), crate_cursor: Some("owned:e1"), shelf_loading_more: True, ) [update(seeded, SetView("wanted")), update(seeded, RetryShelf)] |> list.each(fn(result) { let #(updated, _) = result assert updated.crate_cursor == None assert updated.shelf_loading_more == False }) } pub fn set_display_switches_to_rows_test() { let #(model, _) = update(logged_in(), SetDisplay(Rows)) assert model.display == Rows } pub fn set_display_back_to_grid_persists_without_a_shelf_reload_test() { let rows_view = Model(..logged_in(), display: Rows) let #(model, effect) = update(rows_view, SetDisplay(Grid)) assert model.display == Grid assert effect != empty_effect() } pub fn session_expired_is_a_warning_notice_test() { let #(model, _) = update(logged_in(), GotBrowseAdd("at://x", Error(unauthorized()))) assert case model.notice { Some(Notice(Warning, _)) -> True _ -> False } } pub fn clear_notice_drops_it_test() { let noticed = Model(..logged_in(), notice: Some(Notice(Failure, "boom"))) let #(model, _) = update(noticed, ClearNotice) assert model.notice == None } pub fn empty_owned_view_shows_first_run_panel_with_toolbar_test() { let html = rendered_empty_crate("owned") assert string.contains(html, "NOTHING IN THE CRATE YET") assert string.contains(html, "WANTED") assert string.contains(html, "HISTORY") // The first-run state is the only crate state without the FAB. assert !string.contains(html, "class=\"fab\"") } pub fn empty_wanted_view_keeps_toolbar_and_fab_test() { let html = rendered_empty_crate("wanted") assert string.contains(html, "OWNED") assert string.contains(html, "HISTORY") assert string.contains(html, "0 on your wishlist") assert string.contains(html, "Nothing on your wishlist yet") assert string.contains(html, "class=\"fab\"") assert !string.contains(html, "NOTHING IN THE CRATE YET") } pub fn empty_history_view_keeps_toolbar_and_fab_test() { let html = rendered_empty_crate("history") assert string.contains(html, "OWNED") assert string.contains(html, "WANTED") assert string.contains(html, "0 in your history") assert string.contains(html, "No history yet") assert string.contains(html, "class=\"fab\"") assert !string.contains(html, "NOTHING IN THE CRATE YET") } pub fn empty_owned_view_omits_the_display_toggle_test() { let html = rendered_empty_crate("owned") assert !string.contains(html, "class=\"display-toggle\"") } pub fn loaded_grid_shows_the_display_toggle_and_cover_cards_test() { let html = rendered_loaded_crate(Grid) assert string.contains(html, "class=\"display-toggle\"") assert string.contains(html, "▦") assert string.contains(html, "☰") assert string.contains(html, "class=\"cover-card\"") assert !string.contains(html, "class=\"row\"") } pub fn display_toggle_buttons_carry_labels_and_pressed_state_test() { let html = rendered_loaded_crate(Grid) assert string.contains(html, "aria-label=\"Grid view\"") assert string.contains(html, "aria-label=\"List view\"") assert string.contains(html, "aria-pressed=\"true\"") assert string.contains(html, "aria-pressed=\"false\"") } pub fn loaded_rows_render_title_and_muted_artist_format_meta_test() { let html = rendered_loaded_crate(Rows) assert string.contains(html, "class=\"row\"") assert string.contains(html, "class=\"row__title\"") assert string.contains(html, "Spiderland") assert string.contains(html, "Slint · LP / 1991") assert !string.contains(html, "class=\"cover-card\"") } pub fn crate_app_bar_avatar_links_to_settings_test() { let html = view.view(logged_in()) |> element.to_string assert string.contains(html, "href=\"/settings\"") // The account popout is gone: its actions now live on the Settings page, // never in the crate app bar itself. assert !string.contains(html, "EDIT INBOX") assert !string.contains(html, "LOG OUT") } pub fn crate_app_bar_icon_links_carry_aria_labels_test() { let html = view.view(logged_in()) |> element.to_string assert string.contains(html, "aria-label=\"Browse the network\"") assert string.contains(html, "aria-label=\"Scan barcodes\"") } fn many_entries(n: Int) -> List(Entry) { list.repeat(an_entry(), n) |> list.index_map(fn(e, i) { Entry(..e, entry_id: "e" <> int.to_string(i)) }) } pub fn crate_windows_items_and_shows_load_more_when_more_remain_test() { let seeded = Model(..logged_in(), shelf: ShelfLoaded(many_entries(31)), crate_window: 30) let html = crate.view(seeded) |> element.to_string assert string.contains(html, "class=\"load-more-count\"") assert string.contains(html, "showing 30 of 31") assert !string.contains(html, "LOADING MORE") } pub fn crate_without_overflow_omits_load_more_test() { let seeded = Model(..logged_in(), shelf: ShelfLoaded(many_entries(5)), crate_window: 30) let html = crate.view(seeded) |> element.to_string assert !string.contains(html, "load-more-block") } pub fn show_more_crate_grows_the_window_by_a_page_test() { let seeded = Model(..logged_in(), crate_window: 30) let #(updated, effect) = update(seeded, ShowMoreCrate) assert updated.crate_window == 30 + model.crate_window_size // Windowing is pure local render state: no shelf reload. assert effect == empty_effect() } pub fn got_shelf_resets_the_window_test() { let seeded = Model(..logged_in(), crate_window: 120) let #(updated, _) = update(seeded, GotShelf(Ok(ShelfData("alice.test", [], dict.new(), None)))) assert updated.crate_window == model.crate_window_size } pub fn got_shelf_stores_the_next_cursor_test() { let #(updated, _) = update( logged_in(), GotShelf(Ok(ShelfData("alice.test", [], dict.new(), Some("owned:e30")))), ) assert updated.crate_cursor == Some("owned:e30") } pub fn show_more_crate_only_fetches_once_the_window_catches_up_to_the_cursor_test() { [ #(60, Some("owned:e59"), False), #(30, Some("owned:e29"), True), #(30, None, False), ] |> list.each(fn(row) { let #(fetched, cursor, expect_fetch) = row let seeded = Model( ..logged_in(), shelf: ShelfLoaded(many_entries(fetched)), crate_window: 30, crate_cursor: cursor, ) let #(updated, effect) = update(seeded, ShowMoreCrate) assert updated.crate_window == 30 + model.crate_window_size assert updated.shelf_loading_more == expect_fetch assert { effect == empty_effect() } != expect_fetch }) } pub fn got_shelf_more_appends_and_advances_the_cursor_test() { let seeded = Model( ..logged_in(), shelf: ShelfLoaded(many_entries(2)), crate_cursor: Some("owned:e1"), shelf_loading_more: True, ) let next_page = ShelfData( "alice.test", many_entries(4) |> list.drop(2), dict.new(), Some("owned:e3"), ) let #(updated, _) = update(seeded, GotShelfMore(Ok(next_page))) let assert ShelfLoaded(items) = updated.shelf assert list.map(items, fn(e) { e.entry_id }) == ["e0", "e1", "e2", "e3"] assert updated.crate_cursor == Some("owned:e3") assert updated.shelf_loading_more == False } pub fn got_shelf_more_dedupes_by_entry_id_test() { let seeded = Model( ..logged_in(), shelf: ShelfLoaded(many_entries(2)), crate_cursor: Some("owned:e1"), ) // A write shifting the fold between pages must not duplicate the overlap. let overlapping_page = ShelfData("alice.test", many_entries(3) |> list.drop(1), dict.new(), None) let #(updated, _) = update(seeded, GotShelfMore(Ok(overlapping_page))) let assert ShelfLoaded(items) = updated.shelf assert list.map(items, fn(e) { e.entry_id }) == ["e0", "e1", "e2"] } pub fn got_shelf_more_merges_via_handles_test() { let seeded = Model( ..logged_in(), shelf: ShelfLoaded(many_entries(1)), via_handles: dict.from_list([#("e0", "a.test")]), ) let next_page = ShelfData( "alice.test", many_entries(2) |> list.drop(1), dict.from_list([#("e1", "b.test")]), None, ) let #(updated, _) = update(seeded, GotShelfMore(Ok(next_page))) assert dict.get(updated.via_handles, "e0") == Ok("a.test") assert dict.get(updated.via_handles, "e1") == Ok("b.test") } pub fn got_shelf_more_error_clears_loading_and_notices_the_user_test() { let seeded = Model(..logged_in(), shelf_loading_more: True) let #(updated, _) = update(seeded, GotShelfMore(Error(rsvp.NetworkError))) assert updated.shelf_loading_more == False assert case updated.notice { Some(Notice(Failure, _)) -> True _ -> False } } pub fn load_more_shows_when_a_cursor_remains_even_without_local_overflow_test() { let seeded = Model( ..logged_in(), shelf: ShelfLoaded(many_entries(5)), crate_window: 30, crate_cursor: Some("owned:e4"), ) let html = crate.view(seeded) |> element.to_string assert string.contains(html, "class=\"load-more-count\"") } pub fn load_more_renders_a_disabled_loading_state_while_fetching_test() { let seeded = Model( ..logged_in(), shelf: ShelfLoaded(many_entries(31)), crate_window: 30, shelf_loading_more: True, ) let html = crate.view(seeded) |> element.to_string assert string.contains(html, "LOADING MORE") assert string.contains(html, "disabled") assert string.contains(html, "class=\"load-more-count\"") }