test(timeline): add a test that an edit to a thread's latest event updates the thread summary

This commit is contained in:
Benjamin Bouvier
2025-09-30 16:52:01 +02:00
parent d01a28c9b2
commit a6a590aa1f
2 changed files with 134 additions and 2 deletions
@@ -28,7 +28,9 @@ use matrix_sdk_test::{
ALICE, BOB, JoinedRoomBuilder, RoomAccountDataTestEvent, async_test,
event_factory::EventFactory,
};
use matrix_sdk_ui::timeline::{RoomExt as _, TimelineBuilder, TimelineDetails, TimelineFocus};
use matrix_sdk_ui::timeline::{
RoomExt as _, TimelineBuilder, TimelineDetails, TimelineEventItemId, TimelineFocus,
};
use ruma::{
MilliSecondsSinceUnixEpoch,
api::client::receipt::create_receipt::v3::ReceiptType as SendReceiptType,
@@ -42,7 +44,10 @@ use ruma::{
receipt::{ReceiptThread, ReceiptType},
room::{
ImageInfo,
message::{Relation, ReplacementMetadata, RoomMessageEventContent},
message::{
Relation, ReplacementMetadata, RoomMessageEventContent,
RoomMessageEventContentWithoutRelation,
},
},
sticker::{StickerEventContent, StickerMediaSource},
},
@@ -450,6 +455,132 @@ async fn test_new_thread_reply_causes_thread_summary_update() {
assert_eq!(summary.num_replies, 2);
}
#[async_test]
async fn test_thread_edit_reflects_in_summary() {
// A new edit of a threaded reply received in sync will cause the thread root's
// thread summary to be updated with the latest content.
let server = MatrixMockServer::new().await;
let client = client_with_threading_support(&server).await;
let room_id = room_id!("!a:b.c");
let room = server.sync_joined_room(&client, room_id).await;
let timeline = room
.timeline_builder()
.with_focus(TimelineFocus::Live { hide_threaded_events: true })
.build()
.await
.unwrap();
let (initial_items, mut stream) = timeline.subscribe().await;
assert!(initial_items.is_empty());
// Start with a message (with no bundled thread info), and a threaded reply to
// it.
let f = EventFactory::new().room(room_id).sender(&ALICE);
let thread_event_id = event_id!("$thread_root");
let reply_event_id = event_id!("$thread_reply");
server
.sync_room(
&client,
JoinedRoomBuilder::new(room_id)
.add_timeline_event(
f.text_msg("thready thread mcthreadface").event_id(thread_event_id),
)
.add_timeline_event(
f.text_msg("threaded reply")
.sender(&BOB)
.in_thread(thread_event_id, thread_event_id)
.event_id(reply_event_id),
),
)
.await;
assert_let_timeout!(Some(timeline_updates) = stream.next());
// Thread root + new implicit read receipt + new thread summary + day divider.
// TODO: could we optimize this, to have only a single timeline update?
assert_eq!(timeline_updates.len(), 4);
// Sanity check the timeline diffs.
{
assert_let!(VectorDiff::PushBack { value } = &timeline_updates[0]);
let event_item = value.as_event().unwrap();
assert_eq!(event_item.event_id().unwrap(), thread_event_id);
// At first, the summary isn't here.
assert!(event_item.content().thread_summary().is_none());
// And it only has the read receipt of its author.
assert_eq!(event_item.read_receipts().len(), 1);
// Then, a read-receipt "set" because Bob having sent a reply means he's seen
// the thread root.
assert_let!(VectorDiff::Set { index: 0, value } = &timeline_updates[1]);
let event_item = value.as_event().unwrap();
assert_eq!(event_item.event_id().unwrap(), thread_event_id);
assert!(event_item.content().thread_summary().is_none());
// Now, with Bob's read receipt.
assert_eq!(event_item.read_receipts().len(), 2);
// Eventually the summary comes in.
assert_let!(VectorDiff::Set { index: 0, value } = &timeline_updates[2]);
let event_item = value.as_event().unwrap();
assert_eq!(event_item.event_id().unwrap(), thread_event_id);
// Now there is a summary!
assert_let!(Some(summary) = event_item.content().thread_summary());
assert_let!(TimelineDetails::Ready(latest_event) = summary.latest_event);
assert_eq!(
latest_event.identifier,
TimelineEventItemId::EventId(reply_event_id.to_owned())
);
assert_eq!(latest_event.content.as_message().unwrap().body(), "threaded reply");
assert_eq!(summary.num_replies, 1);
// And finally, the day divider.
assert_let!(VectorDiff::PushFront { value } = &timeline_updates[3]);
assert!(value.is_date_divider());
}
// When I receive an edit to that event, via sync,
let edit_reply_event_id = event_id!("$thread_reply_edit");
server
.sync_room(
&client,
JoinedRoomBuilder::new(room_id).add_timeline_event(
f.text_msg("*edited* threaded reply")
.edit(
reply_event_id,
RoomMessageEventContentWithoutRelation::text_plain("edited threaded reply"),
)
.sender(&BOB)
.event_id(edit_reply_event_id),
),
)
.await;
// The root message is updated.
// TODO: not happening, fix here!
assert_let_timeout!(Some(timeline_updates) = stream.next());
assert_eq!(timeline_updates.len(), 1);
assert_let!(VectorDiff::Set { index: 1, value } = &timeline_updates[0]);
let event_item = value.as_event().unwrap();
assert_eq!(event_item.event_id().unwrap(), thread_event_id);
assert_let!(Some(summary) = event_item.content().thread_summary());
// The latest event has been updated.
assert_let!(TimelineDetails::Ready(latest_event) = summary.latest_event);
assert_eq!(latest_event.identifier, TimelineEventItemId::EventId(reply_event_id.to_owned()));
assert_eq!(latest_event.content.as_message().unwrap().body(), "*edited* threaded reply");
// Still only one reply; it's been edited.
assert_eq!(summary.num_replies, 1);
// That's all, folks!
assert_pending!(stream);
}
#[async_test]
async fn test_thread_filtering_for_sync() {
// Make sure that:
@@ -1539,6 +1539,7 @@ mod private {
}
// Trigger an update to observers.
trace!(%thread_root, "updating thread summary: {new_summary:?}");
target_event.thread_summary = ThreadSummaryStatus::Some(new_summary);
self.replace_event_at(location, target_event).await?;
}