diff --git a/examples/html_examples/src/main.rs b/examples/html_examples/src/main.rs
index e6f2b78..50e49dc 100644
--- a/examples/html_examples/src/main.rs
+++ b/examples/html_examples/src/main.rs
@@ -880,8 +880,10 @@ mod tests {
.unwrap()
.batch,
);
- assert!(lazy.updates_text(gallery::lazy_panel));
- assert!(lazy.payload_contains("Lazy content loaded by server update #1"));
+ lazy.assert_updates_text_containing(
+ gallery::lazy_panel,
+ "Lazy content loaded by server update #1",
+ );
let more = inspect_batch(
InteractionRequest::from(form(gallery::load_more, &[("request", "more")]))
diff --git a/hemx-test/src/lib.rs b/hemx-test/src/lib.rs
index a2481fb..14d9fc6 100644
--- a/hemx-test/src/lib.rs
+++ b/hemx-test/src/lib.rs
@@ -462,17 +462,41 @@ impl EffectInspector {
self.has_resource(target.__hemx_resource_id())
}
- /// Assert that a generated target receives a text update, without matching raw effects.
+ /// Check that a generated target receives a text update, without matching raw effects.
/// req: test/001 req: dx/006
pub fn updates_text(&self, target: impl GeneratedTarget) -> bool {
+ self.has_text_update_containing(target.__hemx_resource_id(), "")
+ }
+
+ /// Check that a generated target receives a text update containing a fragment.
+ ///
+ /// This associates the payload condition with the intended target, unlike a separate global
+ /// [`Self::payload_contains`] check that can accidentally match another operation.
+ /// req: test/001 req: test/018 req: dx/006
+ pub fn updates_text_containing(&self, target: impl GeneratedTarget, needle: &str) -> bool {
+ self.has_text_update_containing(target.__hemx_resource_id(), needle)
+ }
+
+ /// Assert that a generated target receives a text update containing a fragment.
+ /// req: test/001 req: test/018 req: dx/006
+ #[track_caller]
+ pub fn assert_updates_text_containing(&self, target: impl GeneratedTarget, needle: &str) {
let resource = target.__hemx_resource_id();
+ assert!(
+ self.has_text_update_containing(resource, needle),
+ "expected a text update for {resource:?} containing {needle:?}; actual effects: {:#?}",
+ self.batch.ops
+ );
+ }
+
+ fn has_text_update_containing(&self, resource: ResourceId, needle: &str) -> bool {
self.batch.ops.iter().any(|op| {
matches!(
op,
Effect::Put {
target,
- payload: Payload::Text(_),
- } if target.resource == resource
+ payload: Payload::Text(text),
+ } if target.resource == resource && text.contains(needle)
)
})
}
diff --git a/hemx-test/tests/inspector.rs b/hemx-test/tests/inspector.rs
index 4e8edc0..877211f 100644
--- a/hemx-test/tests/inspector.rs
+++ b/hemx-test/tests/inspector.rs
@@ -17,6 +17,40 @@ fn inspects_tuple_effects() {
}));
}
+#[test]
+fn text_update_condition_is_bound_to_the_expected_target() {
+ // req: test/018
+ let expected_target = TestTarget(ResourceKind::Slot, 42);
+ let inspected = hemx_test::inspect(vec![
+ Effect::Put {
+ target: ResourceRef::unscoped(ResourceId::new(ResourceKind::Slot, 42)),
+ payload: Payload::Text(String::from("wrong payload")),
+ },
+ Effect::Put {
+ target: ResourceRef::unscoped(ResourceId::new(ResourceKind::Slot, 7)),
+ payload: Payload::Text(String::from("expected fragment")),
+ },
+ ]);
+
+ assert!(inspected.updates_text(expected_target));
+ assert!(inspected.payload_contains("expected fragment"));
+ assert!(!inspected.updates_text_containing(expected_target, "expected fragment"));
+
+ let panic = std::panic::catch_unwind(|| {
+ inspected.assert_updates_text_containing(expected_target, "expected fragment");
+ })
+ .expect_err("a payload on another target must not satisfy the assertion");
+ let message = panic
+ .downcast_ref::()
+ .map(String::as_str)
+ .or_else(|| panic.downcast_ref::<&str>().copied())
+ .expect("assertion panic must carry a message");
+
+ assert!(message.contains("expected fragment"), "{message}");
+ assert!(message.contains("wrong payload"), "{message}");
+ assert!(message.contains("ResourceId"), "{message}");
+}
+
#[test]
fn html_update_assertion_reports_expectation_and_actual_effects() {
// req: test/018