import at_record_web/model.{ type EditProposal, type InboxCard, EditProposal, InboxCard, InboxFailed, InboxLoaded, InboxLoading, Model, ProposalApplied, ProposalApplying, ProposalFields, ProposalReviewing, } import at_record_web/msg.{ AppliedProposal, ApplyProposal, GotApplyProposal, GotEditInbox, IgnoreProposal, OnRouteChange, } import at_record_web/pages/edit_inbox import at_record_web/update.{update} import gleam/http/response.{Response} import gleam/option.{None, Some} import gleam/string import lustre/element import rsvp import support.{logged_in} fn an_edit_proposal(uri: String) -> EditProposal { EditProposal( uri:, cid: "cid-1", proposer_did: "did:plc:bob", proposer_handle: Some("bob.test"), target_uri: "at://did:plc:alice/dev.mokkenstorm.crate.catalog.release/r1", release_title: "Spiderland", current: ProposalFields( title: Some("Spiderland"), released: Some("1991"), country: Some("US"), genres: Some(["Rock"]), styles: None, ), fields: ProposalFields( title: Some("Spiderland (Remastered)"), released: None, country: None, genres: None, styles: None, ), created_at: "2026-02-01T00:00:00Z", ) } pub fn route_change_to_edit_inbox_starts_loading_and_dispatches_a_load_test() { let #(model, effect) = update(logged_in(), OnRouteChange(model.EditInbox)) assert model.inbox == InboxLoading assert model.busy == True assert effect != support.empty_effect() } pub fn got_edit_inbox_ok_stores_cards_as_reviewing_test() { let proposal = an_edit_proposal("at://proposer/edit/1") let #(model, _) = update(logged_in(), GotEditInbox(Ok([proposal]))) assert model.inbox == InboxLoaded([InboxCard(proposal:, status: ProposalReviewing)]) assert model.busy == False } pub fn got_edit_inbox_401_logs_out_test() { let #(model, _) = update( logged_in(), GotEditInbox(Error(rsvp.HttpError(Response(401, [], "")))), ) assert model.auth == model.LoggedOut } pub fn got_edit_inbox_500_marks_the_inbox_failed_with_a_notice_test() { let #(model, _) = update( logged_in(), GotEditInbox(Error(rsvp.HttpError(Response(500, [], "")))), ) assert model.inbox == InboxFailed assert model.notice != None } pub fn apply_proposal_marks_the_matching_card_applying_test() { let proposal = an_edit_proposal("at://proposer/edit/1") let seeded = Model( ..logged_in(), inbox: InboxLoaded([InboxCard(proposal:, status: ProposalReviewing)]), ) let #(model, effect) = update(seeded, ApplyProposal("at://proposer/edit/1", "cid-1")) let assert InboxLoaded([InboxCard(status:, ..)]) = model.inbox assert status == ProposalApplying assert effect != support.empty_effect() } pub fn got_apply_proposal_ok_marks_the_card_applied_and_refreshes_the_shelf_test() { let proposal = an_edit_proposal("at://proposer/edit/1") let seeded = Model( ..logged_in(), inbox: InboxLoaded([InboxCard(proposal:, status: ProposalApplying)]), ) let #(model, effect) = update( seeded, GotApplyProposal( "at://proposer/edit/1", Ok(AppliedProposal("at://alice/release/rNEW")), ), ) let assert InboxLoaded([InboxCard(status:, ..)]) = model.inbox assert status == ProposalApplied("at://alice/release/rNEW") // The shelf reload effect, so the superseding release shows up. assert effect != support.empty_effect() } pub fn got_apply_proposal_error_reverts_to_reviewing_with_a_notice_test() { let proposal = an_edit_proposal("at://proposer/edit/1") let seeded = Model( ..logged_in(), inbox: InboxLoaded([InboxCard(proposal:, status: ProposalApplying)]), ) let #(model, _) = update( seeded, GotApplyProposal("at://proposer/edit/1", Error(rsvp.NetworkError)), ) let assert InboxLoaded([InboxCard(status:, ..)]) = model.inbox assert status == ProposalReviewing assert model.notice != None } pub fn ignore_proposal_drops_the_card_from_the_model_test() { let proposal = an_edit_proposal("at://proposer/edit/1") let seeded = Model( ..logged_in(), inbox: InboxLoaded([InboxCard(proposal:, status: ProposalReviewing)]), ) let #(model, _) = update(seeded, IgnoreProposal("at://proposer/edit/1")) assert model.inbox == InboxLoaded([]) } fn rendered_inbox(cards: List(InboxCard)) -> String { Model(..logged_in(), inbox: InboxLoaded(cards)) |> edit_inbox.view |> element.to_string } pub fn edit_inbox_view_wraps_the_card_in_a_link_to_its_detail_route_test() { let proposal = an_edit_proposal("at://proposer/edit/1") let html = rendered_inbox([InboxCard(proposal:, status: ProposalReviewing)]) let expected_href = "href=\"/inbox/" <> model.proposal_route_id("at://proposer/edit/1") <> "\"" assert string.contains(html, expected_href) // The apply/ignore buttons stay outside the link, sibling to it. assert string.contains(html, "class=\"proposal-actions\"") } pub fn edit_inbox_view_renders_diff_row_old_arrow_new_and_the_proposer_test() { let proposal = an_edit_proposal("at://proposer/edit/1") let html = rendered_inbox([InboxCard(proposal:, status: ProposalReviewing)]) assert string.contains(html, "class=\"diff-row__old\"") assert string.contains(html, "class=\"diff-row__new\"") assert string.contains(html, "→") assert string.contains(html, "Spiderland (Remastered)") assert string.contains(html, "proposed by @bob.test") } pub fn edit_inbox_view_renders_not_set_for_an_absent_current_value_test() { let proposal = EditProposal( ..an_edit_proposal("at://proposer/edit/1"), current: ProposalFields( title: None, released: None, country: None, genres: None, styles: None, ), ) let html = rendered_inbox([InboxCard(proposal:, status: ProposalReviewing)]) assert string.contains(html, "(not set)") } pub fn edit_inbox_view_omits_a_row_for_a_field_the_proposal_does_not_touch_test() { let proposal = an_edit_proposal("at://proposer/edit/1") let html = rendered_inbox([InboxCard(proposal:, status: ProposalReviewing)]) // `fields` only carries `title`; released/country/genres/styles stay unset. assert !string.contains(html, "COUNTRY") assert !string.contains(html, "RELEASED") } pub fn edit_inbox_view_renders_the_applied_confirmation_strip_test() { let proposal = an_edit_proposal("at://proposer/edit/1") let html = rendered_inbox([ InboxCard(proposal:, status: ProposalApplied("at://alice/release/rNEW")), ]) assert string.contains(html, "class=\"applied-strip\"") // Element rendering HTML-escapes quotes, so match the escaped form. assert string.contains( html, "Applied: minted a superseding version of this pressing ("Spiderland")", ) // No diff/actions for an already-applied card. assert !string.contains(html, "class=\"proposal-actions\"") } pub fn edit_inbox_view_intro_counts_the_loaded_cards_test() { let a = an_edit_proposal("at://proposer/edit/1") let b = an_edit_proposal("at://proposer/edit/2") let html = rendered_inbox([ InboxCard(proposal: a, status: ProposalReviewing), InboxCard(proposal: b, status: ProposalReviewing), ]) assert string.contains(html, "2 proposals against your pressings") } pub fn edit_inbox_view_empty_state_has_no_intro_line_test() { let html = rendered_inbox([]) assert !string.contains(html, "class=\"inbox-intro\"") assert string.contains(html, "No proposals against your pressings") } pub fn inbox_pending_count_is_zero_before_the_inbox_has_ever_loaded_test() { assert model.inbox_pending_count(InboxLoading) == 0 assert model.inbox_pending_count(InboxFailed) == 0 } pub fn inbox_pending_count_counts_only_reviewing_cards_test() { let proposal = an_edit_proposal("at://proposer/edit/1") let applied = InboxCard(proposal:, status: ProposalApplied("at://alice/release/rNEW")) let reviewing = InboxCard(proposal:, status: ProposalReviewing) assert model.inbox_pending_count(InboxLoaded([applied])) == 0 assert model.inbox_pending_count(InboxLoaded([reviewing])) == 1 assert model.inbox_pending_count(InboxLoaded([applied, reviewing])) == 1 }