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",
)
}