Skip to content

Commit a6c4240

Browse files
committed
tests: add integration tests for the new timeline focus mode
1 parent 7856dab commit a6c4240

File tree

3 files changed

+333
-4
lines changed

3 files changed

+333
-4
lines changed

crates/matrix-sdk-ui/tests/integration/main.rs

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
use itertools::Itertools as _;
16+
use matrix_sdk::deserialized_responses::TimelineEvent;
1517
use matrix_sdk_test::test_json;
18+
use ruma::{events::AnyStateEvent, serde::Raw, EventId, RoomId};
1619
use serde::Serialize;
20+
use serde_json::json;
1721
use wiremock::{
1822
matchers::{header, method, path, path_regex, query_param, query_param_is_missing},
1923
Mock, MockServer, ResponseTemplate,
@@ -32,22 +36,79 @@ matrix_sdk_test::init_tracing_for_tests!();
3236
/// an optional `since` param that returns a 200 status code with the given
3337
/// response body.
3438
async fn mock_sync(server: &MockServer, response_body: impl Serialize, since: Option<String>) {
35-
let mut builder = Mock::given(method("GET"))
39+
let mut mock_builder = Mock::given(method("GET"))
3640
.and(path("/_matrix/client/r0/sync"))
3741
.and(header("authorization", "Bearer 1234"));
3842

3943
if let Some(since) = since {
40-
builder = builder.and(query_param("since", since));
44+
mock_builder = mock_builder.and(query_param("since", since));
4145
} else {
42-
builder = builder.and(query_param_is_missing("since"));
46+
mock_builder = mock_builder.and(query_param_is_missing("since"));
4347
}
4448

45-
builder
49+
mock_builder
4650
.respond_with(ResponseTemplate::new(200).set_body_json(response_body))
4751
.mount(server)
4852
.await;
4953
}
5054

55+
/// Mocks the /context endpoint
56+
///
57+
/// Note: pass `events_before` in the normal order, I'll revert the order for
58+
/// you.
59+
#[allow(clippy::too_many_arguments)] // clippy you've got such a fixed mindset
60+
async fn mock_context(
61+
server: &MockServer,
62+
room_id: &RoomId,
63+
event_id: &EventId,
64+
prev_batch_token: Option<String>,
65+
events_before: Vec<TimelineEvent>,
66+
event: TimelineEvent,
67+
events_after: Vec<TimelineEvent>,
68+
next_batch_token: Option<String>,
69+
state: Vec<Raw<AnyStateEvent>>,
70+
) {
71+
Mock::given(method("GET"))
72+
.and(path(format!("/_matrix/client/r0/rooms/{room_id}/context/{event_id}")))
73+
.and(header("authorization", "Bearer 1234"))
74+
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
75+
"events_before": events_before.into_iter().rev().map(|ev| ev.event).collect_vec(),
76+
"event": event.event,
77+
"events_after": events_after.into_iter().map(|ev| ev.event).collect_vec(),
78+
"state": state,
79+
"start": prev_batch_token,
80+
"end": next_batch_token
81+
})))
82+
.mount(server)
83+
.await;
84+
}
85+
86+
/// Mocks the /messages endpoint.
87+
///
88+
/// Note: pass `chunk` in the correct order: topological for forward pagination,
89+
/// reverse topological for backwards pagination.
90+
async fn mock_messages(
91+
server: &MockServer,
92+
start: String,
93+
end: Option<String>,
94+
chunk: Vec<TimelineEvent>,
95+
state: Vec<Raw<AnyStateEvent>>,
96+
) {
97+
Mock::given(method("GET"))
98+
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$"))
99+
.and(header("authorization", "Bearer 1234"))
100+
.and(query_param("from", start.clone()))
101+
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
102+
"start": start,
103+
"end": end,
104+
"chunk": chunk.into_iter().map(|ev| ev.event).collect_vec(),
105+
"state": state,
106+
})))
107+
.expect(1)
108+
.mount(server)
109+
.await;
110+
}
111+
51112
/// Mount a Mock on the given server to handle the `GET
52113
/// /rooms/.../state/m.room.encryption` endpoint with an option whether it
53114
/// should return an encryption event or not.
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
// Copyright 2024 The Matrix.org Foundation C.I.C.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//! Tests specific to a timeline focused on an event.
16+
17+
use std::time::Duration;
18+
19+
use assert_matches2::assert_let;
20+
use eyeball_im::VectorDiff;
21+
use futures_util::StreamExt;
22+
use matrix_sdk::{
23+
config::SyncSettings,
24+
test_utils::{events::EventFactory, logged_in_client_with_server},
25+
};
26+
use matrix_sdk_test::{
27+
async_test, sync_timeline_event, JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB,
28+
};
29+
use matrix_sdk_ui::{timeline::TimelineFocus, Timeline};
30+
use ruma::{event_id, room_id};
31+
use stream_assert::assert_pending;
32+
33+
use crate::{mock_context, mock_messages, mock_sync};
34+
35+
#[async_test]
36+
async fn test_new_focused() {
37+
let room_id = room_id!("!a98sd12bjh:example.org");
38+
let (client, server) = logged_in_client_with_server().await;
39+
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
40+
41+
let mut sync_response_builder = SyncResponseBuilder::new();
42+
sync_response_builder.add_joined_room(JoinedRoomBuilder::new(room_id));
43+
44+
// Mark the room as joined.
45+
mock_sync(&server, sync_response_builder.build_json_sync_response(), None).await;
46+
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
47+
server.reset().await;
48+
49+
let f = EventFactory::new().room(room_id);
50+
let target_event = event_id!("$1");
51+
52+
mock_context(
53+
&server,
54+
room_id,
55+
target_event,
56+
Some("prev1".to_owned()),
57+
vec![
58+
f.text_msg("i tried so hard").sender(*ALICE).into_timeline(),
59+
f.text_msg("and got so far").sender(*ALICE).into_timeline(),
60+
],
61+
f.text_msg("in the end").event_id(target_event).sender(*BOB).into_timeline(),
62+
vec![
63+
f.text_msg("it doesn't even").sender(*ALICE).into_timeline(),
64+
f.text_msg("matter").sender(*ALICE).into_timeline(),
65+
],
66+
Some("next1".to_owned()),
67+
vec![],
68+
)
69+
.await;
70+
71+
let room = client.get_room(room_id).unwrap();
72+
let timeline = Timeline::builder(&room)
73+
.with_focus(TimelineFocus::Event {
74+
target: target_event.to_owned(),
75+
num_context_events: 20,
76+
})
77+
.build()
78+
.await
79+
.unwrap();
80+
81+
server.reset().await;
82+
83+
let (items, mut timeline_stream) = timeline.subscribe().await;
84+
85+
assert_eq!(items.len(), 5 + 1); // event items + a day divider
86+
assert!(items[0].is_day_divider());
87+
assert_eq!(
88+
items[1].as_event().unwrap().content().as_message().unwrap().body(),
89+
"i tried so hard"
90+
);
91+
assert_eq!(
92+
items[2].as_event().unwrap().content().as_message().unwrap().body(),
93+
"and got so far"
94+
);
95+
assert_eq!(items[3].as_event().unwrap().content().as_message().unwrap().body(), "in the end");
96+
assert_eq!(
97+
items[4].as_event().unwrap().content().as_message().unwrap().body(),
98+
"it doesn't even"
99+
);
100+
assert_eq!(items[5].as_event().unwrap().content().as_message().unwrap().body(), "matter");
101+
102+
assert_pending!(timeline_stream);
103+
104+
// Now trigger a backward pagination.
105+
mock_messages(
106+
&server,
107+
"prev1".to_owned(),
108+
None,
109+
vec![
110+
// reversed manually here
111+
f.text_msg("And even though I tried, it all fell apart").sender(*BOB).into_timeline(),
112+
f.text_msg("I kept everything inside").sender(*BOB).into_timeline(),
113+
],
114+
vec![],
115+
)
116+
.await;
117+
118+
let hit_start = timeline.focused_paginate_backwards(20).await.unwrap();
119+
assert!(hit_start);
120+
121+
server.reset().await;
122+
123+
assert_let!(Some(VectorDiff::PushFront { value: message }) = timeline_stream.next().await);
124+
assert_eq!(
125+
message.as_event().unwrap().content().as_message().unwrap().body(),
126+
"And even though I tried, it all fell apart"
127+
);
128+
129+
assert_let!(Some(VectorDiff::PushFront { value: message }) = timeline_stream.next().await);
130+
assert_eq!(
131+
message.as_event().unwrap().content().as_message().unwrap().body(),
132+
"I kept everything inside"
133+
);
134+
135+
// Day divider post processing.
136+
assert_let!(Some(VectorDiff::PushFront { value: item }) = timeline_stream.next().await);
137+
assert!(item.is_day_divider());
138+
assert_let!(Some(VectorDiff::Remove { index }) = timeline_stream.next().await);
139+
assert_eq!(index, 3);
140+
141+
// Now trigger a forward pagination.
142+
mock_messages(
143+
&server,
144+
"next1".to_owned(),
145+
Some("next2".to_owned()),
146+
vec![
147+
f.text_msg("I had to fall, to lose it all").sender(*BOB).into_timeline(),
148+
f.text_msg("But in the end, it doesn't event matter").sender(*BOB).into_timeline(),
149+
],
150+
vec![],
151+
)
152+
.await;
153+
154+
let hit_start = timeline.focused_paginate_forwards(20).await.unwrap();
155+
assert!(!hit_start); // because we gave it another next2 token.
156+
157+
server.reset().await;
158+
159+
assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await);
160+
assert_eq!(
161+
message.as_event().unwrap().content().as_message().unwrap().body(),
162+
"I had to fall, to lose it all"
163+
);
164+
165+
assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await);
166+
assert_eq!(
167+
message.as_event().unwrap().content().as_message().unwrap().body(),
168+
"But in the end, it doesn't event matter"
169+
);
170+
171+
assert_pending!(timeline_stream);
172+
}
173+
174+
#[async_test]
175+
async fn test_focused_timeline_reacts() {
176+
let room_id = room_id!("!a98sd12bjh:example.org");
177+
let (client, server) = logged_in_client_with_server().await;
178+
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
179+
180+
let mut sync_response_builder = SyncResponseBuilder::new();
181+
sync_response_builder.add_joined_room(JoinedRoomBuilder::new(room_id));
182+
183+
// Mark the room as joined.
184+
mock_sync(&server, sync_response_builder.build_json_sync_response(), None).await;
185+
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
186+
server.reset().await;
187+
188+
// Start a focused timeline.
189+
let f = EventFactory::new().room(room_id);
190+
let target_event = event_id!("$1");
191+
192+
mock_context(
193+
&server,
194+
room_id,
195+
target_event,
196+
None,
197+
vec![],
198+
f.text_msg("yolo").event_id(target_event).sender(*BOB).into_timeline(),
199+
vec![],
200+
None,
201+
vec![],
202+
)
203+
.await;
204+
205+
let room = client.get_room(room_id).unwrap();
206+
let timeline = Timeline::builder(&room)
207+
.with_focus(TimelineFocus::Event {
208+
target: target_event.to_owned(),
209+
num_context_events: 20,
210+
})
211+
.build()
212+
.await
213+
.unwrap();
214+
215+
server.reset().await;
216+
217+
let (items, mut timeline_stream) = timeline.subscribe().await;
218+
219+
assert_eq!(items.len(), 1 + 1); // event items + a day divider
220+
assert!(items[0].is_day_divider());
221+
222+
let event_item = items[1].as_event().unwrap();
223+
assert_eq!(event_item.content().as_message().unwrap().body(), "yolo");
224+
assert_eq!(event_item.reactions().len(), 0);
225+
226+
assert_pending!(timeline_stream);
227+
228+
// Now simulate a sync that returns a new message-like event, and a reaction
229+
// to the $1 event.
230+
sync_response_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_bulk([
231+
// This event must be ignored.
232+
f.text_msg("this is a sync event").sender(*ALICE).into_raw_sync(),
233+
// This event must not be ignored.
234+
sync_timeline_event!({
235+
"content": {
236+
"m.relates_to": {
237+
"event_id": "$1",
238+
"key": "👍",
239+
"rel_type": "m.annotation"
240+
}
241+
},
242+
"event_id": "$15275047031IXQRi:localhost",
243+
"origin_server_ts": 159027581000000_u64,
244+
"sender": *BOB,
245+
"type": "m.reaction",
246+
"unsigned": {
247+
"age": 85
248+
}
249+
}),
250+
]));
251+
252+
// Sync the room.
253+
mock_sync(&server, sync_response_builder.build_json_sync_response(), None).await;
254+
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
255+
server.reset().await;
256+
257+
assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next().await);
258+
259+
let event_item = item.as_event().unwrap();
260+
// Text hasn't changed.
261+
assert_eq!(event_item.content().as_message().unwrap().body(), "yolo");
262+
// But now there's one reaction to the event.
263+
assert_eq!(event_item.reactions().len(), 1);
264+
265+
// And nothing more.
266+
assert_pending!(timeline_stream);
267+
}

crates/matrix-sdk-ui/tests/integration/timeline/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use crate::mock_sync;
3030

3131
mod echo;
3232
mod edit;
33+
mod focus_event;
3334
mod pagination;
3435
mod profiles;
3536
mod queue;

0 commit comments

Comments
 (0)