Skip to content

Commit

Permalink
test(timeline): add tests for multiple caption edits and local reacti…
Browse files Browse the repository at this point in the history
…on to a media upload
  • Loading branch information
bnjbvr committed Nov 19, 2024
1 parent 52f559b commit d16775c
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 71 deletions.
2 changes: 1 addition & 1 deletion crates/matrix-sdk-ui/src/timeline/controller/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ impl<P: RoomDataProvider> TimelineController<P> {
let Some(prev_status) = prev_status else {
match &item.kind {
EventTimelineItemKind::Local(local) => {
if let Some(send_handle) = local.send_handle.clone() {
if let Some(send_handle) = &local.send_handle {
if send_handle
.react(key.to_owned())
.await
Expand Down
214 changes: 144 additions & 70 deletions crates/matrix-sdk-ui/tests/integration/timeline/media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::{fs::File, io::Write as _, time::Duration};
use std::{fs::File, io::Write as _, path::PathBuf, time::Duration};

use assert_matches::assert_matches;
use assert_matches2::assert_let;
use eyeball_im::VectorDiff;
use futures_util::{FutureExt, StreamExt};
use matrix_sdk::{
assert_let_timeout,
attachment::AttachmentConfig,
room::edit::EditedContent,
test_utils::{events::EventFactory, mocks::MatrixMockServer},
assert_let_timeout, attachment::AttachmentConfig, room::edit::EditedContent,
test_utils::mocks::MatrixMockServer,
};
use matrix_sdk_test::{async_test, JoinedRoomBuilder, ALICE};
use matrix_sdk_test::async_test;
use matrix_sdk_ui::timeline::{EventSendState, RoomExt, TimelineItemContent};
use ruma::{
event_id,
Expand All @@ -36,6 +34,24 @@ use tempfile::TempDir;
use tokio::time::sleep;
use wiremock::ResponseTemplate;

fn create_temporary_file(filename: &str) -> (TempDir, PathBuf) {
let tmp_dir = TempDir::new().unwrap();
let file_path = tmp_dir.path().join(filename);
let mut file = File::create(&file_path).unwrap();
file.write_all(b"hello world").unwrap();
(tmp_dir, file_path)
}

fn get_filename_and_caption(msg: &MessageType) -> (&str, Option<&str>) {
match msg {
MessageType::File(event) => (event.filename(), event.caption()),
MessageType::Image(event) => (event.filename(), event.caption()),
MessageType::Video(event) => (event.filename(), event.caption()),
MessageType::Audio(event) => (event.filename(), event.caption()),
_ => panic!("unexpected message type"),
}
}

#[async_test]
async fn test_send_attachment() {
let mock = MatrixMockServer::new().await;
Expand All @@ -49,31 +65,11 @@ async fn test_send_attachment() {

let (items, mut timeline_stream) =
timeline.subscribe_filter_map(|item| item.as_event().cloned()).await;
assert!(items.is_empty());

let f = EventFactory::new();
mock.sync_room(
&client,
JoinedRoomBuilder::new(room_id).add_timeline_event(f.text_msg("hello").sender(&ALICE)),
)
.await;

// Sanity check.
assert_let_timeout!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next());
assert_let!(TimelineItemContent::Message(msg) = item.content());
assert_eq!(msg.body(), "hello");

// No other updates.
assert!(items.is_empty());
assert!(timeline_stream.next().now_or_never().is_none());

// Store a file in a temporary directory.
let tmp_dir = TempDir::new().unwrap();

let file_path = tmp_dir.path().join("test.bin");
{
let mut file = File::create(&file_path).unwrap();
file.write_all(b"hello world").unwrap();
}

// Set up mocks for the file upload.
mock.mock_upload()
Expand All @@ -88,70 +84,148 @@ async fn test_send_attachment() {

mock.mock_room_send().ok(event_id!("$media")).mock_once().mount().await;

// Queue sending of an attachment.
let (_tmp_dir, file_path) = create_temporary_file("test.bin");

// Queue sending of an attachment, with a caption.
let config = AttachmentConfig::new().caption(Some("caption".to_owned()));
timeline.send_attachment(&file_path, mime::TEXT_PLAIN, config).use_send_queue().await.unwrap();

assert_let_timeout!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next());
assert_matches!(item.send_state(), Some(EventSendState::NotSentYet));
let item_id = {
assert_let_timeout!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next());
assert_matches!(item.send_state(), Some(EventSendState::NotSentYet));

assert_let!(TimelineItemContent::Message(msg) = item.content());
assert_let!(TimelineItemContent::Message(msg) = item.content());

// Body is the caption, because there's both a caption and filename.
assert_eq!(msg.body(), "caption");
assert_eq!(get_filename_and_caption(msg.msgtype()), ("test.bin", Some("caption")));

// Body is the caption, because there's both a caption and filename.
assert_eq!(msg.body(), "caption");
assert_let!(MessageType::File(file) = msg.msgtype());
assert_eq!(file.filename(), "test.bin");
assert_eq!(file.caption(), Some("caption"));
// The URI refers to the local cache.
assert_let!(MessageType::File(file) = msg.msgtype());
assert_let!(MediaSource::Plain(uri) = &file.source);
assert!(uri.to_string().contains("localhost"));

// The URI refers to the local cache.
assert_let!(MediaSource::Plain(uri) = &file.source);
assert!(uri.to_string().contains("localhost"));
item.identifier()
};

// Remove the caption while the file's uploading.
timeline
.edit(
&item.identifier(),
EditedContent::MediaCaption { caption: None, formatted_caption: None },
)
.edit(&item_id, EditedContent::MediaCaption { caption: None, formatted_caption: None })
.await
.unwrap();

// We receive an update for the timeline item.
assert_let_timeout!(Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next());
assert_matches!(item.send_state(), Some(EventSendState::NotSentYet));
{
assert_let_timeout!(
Some(VectorDiff::Set { index: 0, value: item }) = timeline_stream.next()
);
assert_matches!(item.send_state(), Some(EventSendState::NotSentYet));

assert_let!(TimelineItemContent::Message(msg) = item.content());
assert_let!(TimelineItemContent::Message(msg) = item.content());

// The filename is the body, because there's no caption anymore.
assert_eq!(msg.body(), "test.bin");
assert_let!(MessageType::File(file) = msg.msgtype());
assert_eq!(file.filename(), "test.bin");
assert_eq!(file.caption(), None);
// The filename is the body, because there's no caption anymore.
assert_eq!(msg.body(), "test.bin");
assert_eq!(get_filename_and_caption(msg.msgtype()), ("test.bin", None));

// The URI still refers to the local cache.
assert_let!(MediaSource::Plain(uri) = &file.source);
assert!(uri.to_string().contains("localhost"));
// The URI still refers to the local cache.
assert_let!(MessageType::File(file) = msg.msgtype());
assert_let!(MediaSource::Plain(uri) = &file.source);
assert!(uri.to_string().contains("localhost"));
}

// Eventually, the media is updated with the final MXC IDs…
sleep(Duration::from_secs(2)).await;
assert_let_timeout!(Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next());

{
assert_let_timeout!(
Some(VectorDiff::Set { index: 0, value: item }) = timeline_stream.next()
);
assert_let!(TimelineItemContent::Message(msg) = item.content());
assert_eq!(get_filename_and_caption(msg.msgtype()), ("test.bin", None));

// The URI now refers to the final MXC URI.
assert_let!(MessageType::File(file) = msg.msgtype());
assert_let!(MediaSource::Plain(uri) = &file.source);
assert_eq!(uri.to_string(), "mxc://sdk.rs/media");

// And eventually the event itself is sent.
assert_let_timeout!(
Some(VectorDiff::Set { index: 0, value: item }) = timeline_stream.next()
);
assert_matches!(item.send_state(), Some(EventSendState::Sent{ event_id }) => {
assert_eq!(event_id, event_id!("$media"));
});
}

// That's all, folks!
assert!(timeline_stream.next().now_or_never().is_none());
}

#[async_test]
async fn test_edit_and_react_to_caption_while_offline() {
let mock = MatrixMockServer::new().await;
let client = mock.client_builder().build().await;

// Disable the sending queue, to simulate offline mode.
client.send_queue().set_enabled(false).await;

mock.mock_room_state_encryption().plain().mount().await;

let room_id = room_id!("!a98sd12bjh:example.org");
let room = mock.sync_joined_room(&client, room_id).await;
let timeline = room.timeline().await.unwrap();

let (items, mut timeline_stream) =
timeline.subscribe_filter_map(|item| item.as_event().cloned()).await;

assert!(items.is_empty());
assert!(timeline_stream.next().now_or_never().is_none());

// Store a file in a temporary directory.
let (_tmp_dir, file_path) = create_temporary_file("test.bin");

// Queue sending of an attachment (no captions).
let config = AttachmentConfig::new();
timeline.send_attachment(&file_path, mime::TEXT_PLAIN, config).use_send_queue().await.unwrap();

let item_id = {
assert_let_timeout!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next());
assert_let!(TimelineItemContent::Message(msg) = item.content());
assert_eq!(get_filename_and_caption(msg.msgtype()), ("test.bin", None));
item.identifier()
};

// Remove and edit the caption multiple times.
for cap in [None, Some("caption"), Some("caption2"), None, Some("caption override")] {
timeline
.edit(
&item_id,
EditedContent::MediaCaption {
caption: cap.map(ToOwned::to_owned),
formatted_caption: None,
},
)
.await
.unwrap();

assert_let_timeout!(
Some(VectorDiff::Set { index: 0, value: item }) = timeline_stream.next()
);
assert_let!(TimelineItemContent::Message(msg) = item.content());
assert_eq!(get_filename_and_caption(msg.msgtype()), ("test.bin", cap));
}

// Add a reaction to the file attachment.
timeline.toggle_reaction(&item_id, "🤪").await.unwrap();

assert_let_timeout!(Some(VectorDiff::Set { index: 0, value: item }) = timeline_stream.next());
assert_let!(TimelineItemContent::Message(msg) = item.content());
assert_eq!(get_filename_and_caption(msg.msgtype()), ("test.bin", Some("caption override")));

// The filename is the body, because there's no caption anymore.
assert_eq!(msg.body(), "test.bin");
assert_let!(MessageType::File(file) = msg.msgtype());
assert_eq!(file.filename(), "test.bin");
assert_eq!(file.caption(), None);

// The URI now refers to the final MXC URI.
assert_let!(MediaSource::Plain(uri) = &file.source);
assert_eq!(uri.to_string(), "mxc://sdk.rs/media");

// And eventually the event itself is sent.
assert_let_timeout!(Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next());
assert_matches!(item.send_state(), Some(EventSendState::Sent{ event_id }) => {
assert_eq!(event_id, event_id!("$media"));
});
// There's a reaction for the current user for the given emoji.
let reactions = item.reactions();
let own_user_id = client.user_id().unwrap();
reactions.get("🤪").unwrap().get(own_user_id).unwrap();

// That's all, folks!
assert!(timeline_stream.next().now_or_never().is_none());
Expand Down

0 comments on commit d16775c

Please sign in to comment.