⚡️ (core): Registro de acciones inmutable
This commit is contained in:
parent
5b2ddeb6cc
commit
58d1fef4f1
15 changed files with 624 additions and 244 deletions
288
tests/action_dispatch.rs
Normal file
288
tests/action_dispatch.rs
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
// The action registry is built once per process, at startup: all the actions of this test are
|
||||
// declared in a single extension and the cases are different tests on top of it.
|
||||
|
||||
// Test action without referer, with configurable weight.
|
||||
struct Probe {
|
||||
name: &'static str,
|
||||
weight: Weight,
|
||||
}
|
||||
|
||||
impl ActionDispatcher for Probe {
|
||||
fn weight(&self) -> Weight {
|
||||
self.weight
|
||||
}
|
||||
}
|
||||
|
||||
// Test referers: `Node` has actions filtered by id; `Plain` only has general ones.
|
||||
struct Node(&'static str);
|
||||
struct Plain;
|
||||
|
||||
// Test action with referer `R`, with configurable weight and referer id.
|
||||
struct Tagged {
|
||||
name: &'static str,
|
||||
weight: Weight,
|
||||
referer: ActionReferer,
|
||||
}
|
||||
|
||||
impl Tagged {
|
||||
fn new<R: 'static>(name: &'static str, weight: Weight, referer_id: Option<&str>) -> Self {
|
||||
let referer = ActionReferer::of::<R>();
|
||||
Tagged {
|
||||
name,
|
||||
weight,
|
||||
referer: match referer_id {
|
||||
Some(id) => referer.with_id(id),
|
||||
None => referer,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ActionDispatcher for Tagged {
|
||||
fn referer(&self) -> Option<&ActionReferer> {
|
||||
Some(&self.referer)
|
||||
}
|
||||
fn weight(&self) -> Weight {
|
||||
self.weight
|
||||
}
|
||||
}
|
||||
|
||||
// Another action type, with nothing registered.
|
||||
struct Unregistered;
|
||||
impl ActionDispatcher for Unregistered {}
|
||||
|
||||
// Ids of the components the `BeforeRender` actions were applied to. Tests run concurrently and
|
||||
// render the same component types, so they are checked by id and not with a shared counter.
|
||||
static GENERAL_LOG: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
static ID_LOG: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
// Log of the transformations applied: (component id, which one).
|
||||
static MARKUP_LOG: Mutex<Vec<(String, &'static str)>> = Mutex::new(Vec::new());
|
||||
|
||||
struct Ext;
|
||||
|
||||
#[async_trait]
|
||||
impl Extension for Ext {
|
||||
fn actions(&self) -> Vec<ActionBox> {
|
||||
let probe = |name, weight| Probe { name, weight };
|
||||
actions![
|
||||
// Registered out of order: dispatch delivers them by weight and, at equal weight, in
|
||||
// registration order.
|
||||
probe("c-late", 10),
|
||||
probe("a-early", -5),
|
||||
probe("b-first-of-zero", 0),
|
||||
probe("d-second-of-zero", 0),
|
||||
// With referer: the ones for the type and, separately, the ones filtered by id.
|
||||
Tagged::new::<Node>("general", 0, None),
|
||||
Tagged::new::<Node>("x-2", 3, Some("x")),
|
||||
Tagged::new::<Node>("x-1", 1, Some("x")),
|
||||
Tagged::new::<Node>("y-1", 0, Some("y")),
|
||||
Tagged::new::<Plain>("plain", 0, None),
|
||||
action::component::BeforeRender::<Badge>::new(|badge, _cx| {
|
||||
GENERAL_LOG
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(badge.id().unwrap_or_default());
|
||||
}),
|
||||
action::component::BeforeRender::<Badge>::new(|badge, _cx| {
|
||||
ID_LOG.lock().unwrap().push(badge.id().unwrap_or_default());
|
||||
})
|
||||
.filter_by_referer_id("special-before"),
|
||||
action::component::TransformMarkup::<Badge>::new(|badge, _cx, markup| {
|
||||
let id = badge.id().unwrap_or_default();
|
||||
MARKUP_LOG.lock().unwrap().push((id, "general"));
|
||||
html! { div { (markup) } }
|
||||
}),
|
||||
action::component::TransformMarkup::<Badge>::new(|badge, _cx, markup| {
|
||||
let id = badge.id().unwrap_or_default();
|
||||
MARKUP_LOG.lock().unwrap().push((id, "by-id"));
|
||||
html! { section { (markup) } }
|
||||
})
|
||||
.filter_by_referer_id("special-markup"),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
async fn start() {
|
||||
// Starting more than once in the same process (as tests do) does not duplicate the actions.
|
||||
let _ = Application::prepare(&Ext).await;
|
||||
let _ = Application::prepare(&Ext).await;
|
||||
}
|
||||
|
||||
fn names() -> Vec<&'static str> {
|
||||
let mut seen = Vec::new();
|
||||
dispatch_actions(|action: &Probe| seen.push(action.name));
|
||||
seen
|
||||
}
|
||||
|
||||
// Names of the `Node` actions dispatched for a referer with that id, and how many times the id was
|
||||
// requested.
|
||||
fn node_names(id: &'static str) -> (Vec<&'static str>, usize) {
|
||||
let mut seen = Vec::new();
|
||||
let mut id_calls = 0;
|
||||
dispatch_referer(
|
||||
&mut Node(id),
|
||||
|node| {
|
||||
id_calls += 1;
|
||||
Some(node.0.to_owned())
|
||||
},
|
||||
|action: &Tagged, _| seen.push(action.name),
|
||||
);
|
||||
(seen, id_calls)
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn actions_are_dispatched_by_weight_then_registration_order() {
|
||||
start().await;
|
||||
assert_eq!(
|
||||
names(),
|
||||
["a-early", "b-first-of-zero", "d-second-of-zero", "c-late"]
|
||||
);
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn referer_actions_run_for_the_type_then_for_the_matching_id() {
|
||||
start().await;
|
||||
// First the ones for the type; then the ones filtered by id, each list sorted by weight.
|
||||
assert_eq!(node_names("x").0, ["general", "x-1", "x-2"]);
|
||||
assert_eq!(node_names("y").0, ["general", "y-1"]);
|
||||
// An id without actions only gets the ones for the type.
|
||||
assert_eq!(node_names("nobody").0, ["general"]);
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn the_id_is_only_requested_if_some_action_is_filtered_by_it() {
|
||||
start().await;
|
||||
assert_eq!(node_names("x").1, 1);
|
||||
|
||||
// `Plain` has no filtered action: there is no need to compute the id.
|
||||
let mut seen = Vec::new();
|
||||
let mut id_calls = 0;
|
||||
dispatch_referer(
|
||||
&mut Plain,
|
||||
|_| {
|
||||
id_calls += 1;
|
||||
None
|
||||
},
|
||||
|action: &Tagged, _| seen.push(action.name),
|
||||
);
|
||||
assert_eq!(seen, ["plain"]);
|
||||
assert_eq!(id_calls, 0);
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn an_action_with_nothing_registered_dispatches_nothing() {
|
||||
start().await;
|
||||
let mut calls = 0;
|
||||
dispatch_actions(|_: &Unregistered| calls += 1);
|
||||
// Not even with a referer type that does not exist.
|
||||
let mut id_calls = 0;
|
||||
dispatch_referer(
|
||||
&mut String::new(),
|
||||
|_| {
|
||||
id_calls += 1;
|
||||
None
|
||||
},
|
||||
|_: &Tagged, _| calls += 1,
|
||||
);
|
||||
assert_eq!((calls, id_calls), (0, 0));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn try_dispatch_stops_when_asked() {
|
||||
start().await;
|
||||
let mut seen = Vec::new();
|
||||
try_dispatch_actions(|action: &Probe| {
|
||||
seen.push(action.name);
|
||||
if action.name == "b-first-of-zero" {
|
||||
std::ops::ControlFlow::Break(())
|
||||
} else {
|
||||
std::ops::ControlFlow::Continue(())
|
||||
}
|
||||
});
|
||||
assert_eq!(seen, ["a-early", "b-first-of-zero"]);
|
||||
}
|
||||
|
||||
// Component actions: the ones for the type apply to all of them; the ones filtered by id only to
|
||||
// the component with that id.
|
||||
#[pagetop::test]
|
||||
async fn component_actions_run_for_the_type_and_for_the_matching_id_only() {
|
||||
start().await;
|
||||
let mut cx = Context::default();
|
||||
let count = |log: &Mutex<Vec<String>>, id: &str| {
|
||||
log.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|logged| *logged == id)
|
||||
.count()
|
||||
};
|
||||
|
||||
let mut other = Badge::labeled(Lc::n("other")).with_id("before-other");
|
||||
let _ = other.render(&mut cx).await;
|
||||
assert_eq!(count(&GENERAL_LOG, "before-other"), 1);
|
||||
assert_eq!(
|
||||
count(&ID_LOG, "before-other"),
|
||||
0,
|
||||
"other id: the filtered one is not applied"
|
||||
);
|
||||
|
||||
let mut special = Badge::labeled(Lc::n("special")).with_id("special-before");
|
||||
let _ = special.render(&mut cx).await;
|
||||
assert_eq!(count(&GENERAL_LOG, "special-before"), 1);
|
||||
assert_eq!(
|
||||
count(&ID_LOG, "special-before"),
|
||||
1,
|
||||
"same id: the filtered one is applied"
|
||||
);
|
||||
|
||||
// Without an id only the general one applies, and the filtered one is applied to no one else.
|
||||
let mut plain = Badge::labeled(Lc::n("plain"));
|
||||
let _ = plain.render(&mut cx).await;
|
||||
assert!(
|
||||
ID_LOG
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.all(|logged| logged == "special-before")
|
||||
);
|
||||
|
||||
// Another component type gets nothing.
|
||||
let mut button = Button::plain(Lc::n("x")).with_id("before-button");
|
||||
let _ = button.render(&mut cx).await;
|
||||
assert_eq!(count(&GENERAL_LOG, "before-button"), 0);
|
||||
}
|
||||
|
||||
// Each transformation receives the result of the previous one: first the ones for the type, then
|
||||
// the ones for the id.
|
||||
#[pagetop::test]
|
||||
async fn markup_transformations_chain_in_order() {
|
||||
start().await;
|
||||
let mut cx = Context::default();
|
||||
|
||||
// With another id only the general one applies.
|
||||
let mut other = Badge::labeled(Lc::n("chained")).with_id("chained-other");
|
||||
let html = other.render(&mut cx).await.into_string();
|
||||
assert!(html.starts_with("<div>"), "{html}");
|
||||
assert!(!html.contains("<section>"), "{html}");
|
||||
|
||||
// With the id of the filtered action, after the general one.
|
||||
let mut special = Badge::labeled(Lc::n("chained")).with_id("special-markup");
|
||||
let html = special.render(&mut cx).await.into_string();
|
||||
assert!(html.starts_with("<section><div>"), "{html}");
|
||||
assert!(html.ends_with("</div></section>"), "{html}");
|
||||
|
||||
// Each one was applied once and in that order (the log is shared with the other tests, which
|
||||
// run concurrently: it is filtered by id).
|
||||
let log = MARKUP_LOG.lock().unwrap();
|
||||
let applied = |id: &str| -> Vec<&'static str> {
|
||||
log.iter()
|
||||
.filter(|(logged, _)| logged == id)
|
||||
.map(|(_, which)| *which)
|
||||
.collect()
|
||||
};
|
||||
assert_eq!(applied("chained-other"), ["general"]);
|
||||
assert_eq!(applied("special-markup"), ["general", "by-id"]);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue