From bd086b0288db321fe1d5932a84a65c59eff8523b Mon Sep 17 00:00:00 2001 From: Austin Schaefer Date: Tue, 15 Sep 2026 09:50:06 +0200 Subject: [PATCH] Extract subscribe_form's submit-result mapping into a testable outcome The submit closure mixed pure decision logic (what should the form show after subscribe_feed resolves) with Dioxus signal orchestration. Splits out resolve_submit_outcome(Result) -> SubmitOutcome, so the success/failure mapping is unit-tested without a Dioxus runtime; the component now just applies the resulting outcome to its signals. Co-Authored-By: Claude Sonnet 5 --- .../web/src/app/subscribe_form/component.rs | 11 ++--- crates/web/src/app/subscribe_form/handlers.rs | 43 ++++++++++++++++--- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/crates/web/src/app/subscribe_form/component.rs b/crates/web/src/app/subscribe_form/component.rs index 9003e0a..c589038 100644 --- a/crates/web/src/app/subscribe_form/component.rs +++ b/crates/web/src/app/subscribe_form/component.rs @@ -1,4 +1,4 @@ -use super::handlers::{normalize_feed_url, subscribed_message}; +use super::handlers::{normalize_feed_url, resolve_submit_outcome, SubmitOutcome}; use crate::api; use crate::components::button::{Button, ButtonVariant}; use crate::components::input::Input; @@ -20,13 +20,14 @@ pub fn SubscribeForm(on_subscribed: EventHandler<()>) -> Element { spawn(async move { submitting.set(true); status.set(None); - match api::feeds::subscribe_feed(feed_url).await { - Ok(count) => { - status.set(Some(Ok(subscribed_message(count)))); + let result = api::feeds::subscribe_feed(feed_url).await; + match resolve_submit_outcome(result.map_err(|err| err.to_string())) { + SubmitOutcome::Success { message } => { + status.set(Some(Ok(message))); url.set(String::new()); on_subscribed.call(()); } - Err(err) => status.set(Some(Err(err.to_string()))), + SubmitOutcome::Failure { message } => status.set(Some(Err(message))), } submitting.set(false); }); diff --git a/crates/web/src/app/subscribe_form/handlers.rs b/crates/web/src/app/subscribe_form/handlers.rs index 7ed1901..08b9c2a 100644 --- a/crates/web/src/app/subscribe_form/handlers.rs +++ b/crates/web/src/app/subscribe_form/handlers.rs @@ -10,10 +10,31 @@ pub fn normalize_feed_url(input: &str) -> Option { } /// Success message shown after a feed subscription pulls in new articles. -pub fn subscribed_message(article_count: usize) -> String { +fn subscribed_message(article_count: usize) -> String { format!("Subscribed — pulled in {article_count} article(s).") } +/// What the form should show, and whether the URL input should be cleared, +/// after a subscription attempt resolves. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SubmitOutcome { + /// The subscription succeeded: show `message` and clear the input. + Success { message: String }, + /// The subscription failed: show `message` and leave the input as-is, + /// so the reader can fix it up and retry. + Failure { message: String }, +} + +/// Maps a `subscribe_feed` result to what the form should do next. +pub fn resolve_submit_outcome(result: Result) -> SubmitOutcome { + match result { + Ok(count) => SubmitOutcome::Success { + message: subscribed_message(count), + }, + Err(message) => SubmitOutcome::Failure { message }, + } +} + #[cfg(test)] mod tests { use super::*; @@ -33,14 +54,24 @@ mod tests { } #[test] - fn message_reports_the_article_count() { + fn success_reports_the_article_count_and_clears_the_input() { assert_eq!( - subscribed_message(0), - "Subscribed — pulled in 0 article(s)." + resolve_submit_outcome(Ok(3)), + SubmitOutcome::Success { + message: "Subscribed — pulled in 3 article(s).".to_string() + } ); + } + + /// A failed attempt should surface the error but leave the input alone + /// so the reader doesn't have to retype the URL to fix it. + #[test] + fn failure_surfaces_the_error_message() { assert_eq!( - subscribed_message(3), - "Subscribed — pulled in 3 article(s)." + resolve_submit_outcome(Err("feed unreachable".to_string())), + SubmitOutcome::Failure { + message: "feed unreachable".to_string() + } ); } }