import crate/gen/defs import crate/gen/shelf/list_entries import crate_web/model.{ type Entry, type EntryDetail, type ReleaseInfo, EntryDetail, EntryDetailLoaded, EntryDetailLoading, Model, Record, ReleaseInfo, } import crate_web/msg.{ type EntryDetailData, ArmRemove, DisarmRemove, EntryAction, EntryDetailData, GotEntry, OnRouteChange, } import crate_web/pages/record import crate_web/update.{update} import gleam/dict import gleam/option.{type Option, None, Some} import gleam/string import lustre/element import support.{a_detail, an_entry, logged_in} /// `a_detail()` with its folded entry swapped for `entry`. fn detail(entry: Entry) -> EntryDetail { EntryDetail(..a_detail(), entry:) } /// A `defs.source` carrying just the capture origin and (via an external /// match) the provider slug the provenance line reads. fn src(origin: Option(String), provider: Option(String)) -> defs.Source { defs.Source( client_agent: None, external: option.map(provider, fn(p) { defs.ExternalId(id: "", provider: p, url: None) }), origin:, origin_url: None, record: None, ) } pub fn record_view_shows_the_sleeve_grade_card_when_present_test() { let html = record.view(logged_in(), a_detail()) |> element.to_string assert string.contains(html, "SLEEVE") assert string.contains(html, "VG+") } pub fn record_view_omits_the_sleeve_grade_card_when_absent_test() { let html = record.view( logged_in(), detail(list_entries.Entry(..an_entry(), sleeve_grade: None)), ) |> element.to_string assert !string.contains(html, "SLEEVE") } pub fn record_view_hides_paid_and_from_rows_when_present_test() { let html = record.view(logged_in(), a_detail()) |> element.to_string assert !string.contains(html, "PAID") assert !string.contains(html, "EUR 40.00") assert !string.contains(html, "FROM") assert !string.contains(html, "Record shop") } pub fn public_detail_rows_hide_paid_and_from_when_present_test() { let entry = an_entry() let html = record.detail_rows(entry, model.entry_snapshot(entry), None) |> element.to_string assert !string.contains(html, "PAID") assert !string.contains(html, "EUR 40.00") assert !string.contains(html, "FROM") assert !string.contains(html, "Record shop") } pub fn record_view_omits_paid_and_from_rows_when_absent_test() { let html = record.view( logged_in(), detail(list_entries.Entry(..an_entry(), price: None, counterparty: None)), ) |> element.to_string assert !string.contains(html, "PAID") assert !string.contains(html, "FROM") } pub fn record_view_shows_the_provenance_line_when_source_is_present_test() { let source = src(Some("scan"), Some("discogs")) let html = record.view( logged_in(), detail(list_entries.Entry(..an_entry(), source: Some(source))), ) |> element.to_string assert string.contains(html, "class=\"layer-provenance\"") assert string.contains(html, "scanned ยท via Discogs") } pub fn record_view_shows_added_by_hand_for_manual_source_test() { let source = src(Some("manual"), None) let html = record.view( logged_in(), detail(list_entries.Entry(..an_entry(), source: Some(source))), ) |> element.to_string assert string.contains(html, "added by hand") } pub fn record_view_omits_the_provenance_line_when_source_is_absent_test() { let html = record.view( logged_in(), detail(list_entries.Entry(..an_entry(), source: None)), ) |> element.to_string assert !string.contains(html, "class=\"layer-provenance\"") } pub fn arm_remove_then_entry_action_disarms_and_sets_busy_test() { let #(armed, _) = update(logged_in(), ArmRemove) assert armed.confirm_remove == True let #(after, _) = update(armed, EntryAction("e1", "dropped")) assert after.confirm_remove == False assert after.busy == True } // DisarmRemove is what both the outside-click watcher and the auto-disarm // timeout dispatch; neither should touch `busy`. pub fn arm_remove_then_disarm_remove_clears_confirm_without_busy_test() { let #(armed, _) = update(logged_in(), ArmRemove) assert armed.confirm_remove == True let #(after, _) = update(armed, DisarmRemove) assert after.confirm_remove == False assert after.busy == False } // Re-arming (e.g. a tap on REMOVE while already armed) stays armed and still // confirms normally on the next EntryAction; the watcher/timer replacement // that guards against a stale disarm lives in effects.arm_confirm, not here. pub fn rearming_remove_stays_armed_and_still_confirms_test() { let #(armed_once, _) = update(logged_in(), ArmRemove) let #(rearmed, _) = update(armed_once, ArmRemove) assert rearmed.confirm_remove == True let #(after, _) = update(rearmed, EntryAction("e1", "dropped")) assert after.confirm_remove == False assert after.busy == True } fn a_release_info() -> ReleaseInfo { ReleaseInfo( artist_display: Some("Slint"), genres: ["Rock"], styles: ["Math Rock"], country: Some("US"), released: Some("1991"), ) } fn an_entry_detail(release: option.Option(ReleaseInfo)) -> EntryDetailData { EntryDetailData( did: "did:plc:a", handle: "alice.test", entry: an_entry(), events: [], release:, ) } pub fn got_entry_with_release_stores_release_info_test() { let #(model, _) = update(logged_in(), GotEntry(Ok(an_entry_detail(Some(a_release_info()))))) let assert EntryDetailLoaded(d) = model.entry_detail assert d.release_info == Some(a_release_info()) } pub fn got_entry_without_release_leaves_release_info_none_test() { let #(model, _) = update(logged_in(), GotEntry(Ok(an_entry_detail(None)))) let assert EntryDetailLoaded(d) = model.entry_detail assert d.release_info == None } pub fn got_entry_error_marks_the_detail_failed_test() { let #(model, _) = update(logged_in(), GotEntry(Error(support.unauthorized()))) assert model.entry_detail == model.EntryDetailFailed } // Drilling into a record resets its detail to loading before the fetch lands. pub fn route_change_to_record_sets_entry_detail_loading_test() { let seeded = Model(..logged_in(), entry_detail: EntryDetailLoaded(a_detail())) let #(after, _) = update(seeded, OnRouteChange(Record("e9"))) assert after.entry_detail == EntryDetailLoading } pub fn record_view_shows_a_copy_share_link_button_with_the_public_path_test() { let html = record.view(logged_in(), a_detail()) |> element.to_string assert string.contains(html, "COPY SHARE LINK") assert string.contains(html, "/u/alice.test/record/e1") } // The fold-out is gone: AMEND/SUGGEST A FIX is now a plain nav link to the // routed amend screen, not a button dispatching a toggle message. pub fn record_view_renders_the_amend_action_as_a_link_to_its_own_route_test() { let html = record.view(logged_in(), a_detail()) |> element.to_string assert string.contains( html, "AMEND", ) } pub fn record_view_labels_the_amend_link_as_suggest_a_fix_for_an_adopted_entry_test() { let via_handles = dict.from_list([#("e1", "bob.test")]) let html = record.view(Model(..logged_in(), via_handles:), a_detail()) |> element.to_string assert string.contains( html, "SUGGEST A FIX", ) }