Extract subscribe_form's submit-result mapping into a testable outcome
Some checks failed
CI / check (pull_request) Successful in 1m47s
CI / test (pull_request) Successful in 3m30s
CI / audit (pull_request) Failing after 14s

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<usize, String>) -> 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 <noreply@anthropic.com>
This commit is contained in:
Austin Schaefer 2026-09-15 09:50:06 +02:00
parent 2ae3293c9d
commit bd086b0288
2 changed files with 43 additions and 11 deletions

View file

@ -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::api;
use crate::components::button::{Button, ButtonVariant}; use crate::components::button::{Button, ButtonVariant};
use crate::components::input::Input; use crate::components::input::Input;
@ -20,13 +20,14 @@ pub fn SubscribeForm(on_subscribed: EventHandler<()>) -> Element {
spawn(async move { spawn(async move {
submitting.set(true); submitting.set(true);
status.set(None); status.set(None);
match api::feeds::subscribe_feed(feed_url).await { let result = api::feeds::subscribe_feed(feed_url).await;
Ok(count) => { match resolve_submit_outcome(result.map_err(|err| err.to_string())) {
status.set(Some(Ok(subscribed_message(count)))); SubmitOutcome::Success { message } => {
status.set(Some(Ok(message)));
url.set(String::new()); url.set(String::new());
on_subscribed.call(()); on_subscribed.call(());
} }
Err(err) => status.set(Some(Err(err.to_string()))), SubmitOutcome::Failure { message } => status.set(Some(Err(message))),
} }
submitting.set(false); submitting.set(false);
}); });

View file

@ -10,10 +10,31 @@ pub fn normalize_feed_url(input: &str) -> Option<String> {
} }
/// Success message shown after a feed subscription pulls in new articles. /// 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).") 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<usize, String>) -> SubmitOutcome {
match result {
Ok(count) => SubmitOutcome::Success {
message: subscribed_message(count),
},
Err(message) => SubmitOutcome::Failure { message },
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -33,14 +54,24 @@ mod tests {
} }
#[test] #[test]
fn message_reports_the_article_count() { fn success_reports_the_article_count_and_clears_the_input() {
assert_eq!( assert_eq!(
subscribed_message(0), resolve_submit_outcome(Ok(3)),
"Subscribed — pulled in 0 article(s)." 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!( assert_eq!(
subscribed_message(3), resolve_submit_outcome(Err("feed unreachable".to_string())),
"Subscribed — pulled in 3 article(s)." SubmitOutcome::Failure {
message: "feed unreachable".to_string()
}
); );
} }
} }