mirror of
https://github.com/flectar/mail.git
synced 2026-11-13 10:34:15 +00:00
562 lines
22 KiB
Rust
562 lines
22 KiB
Rust
//! Headless integration regression: real Slint projection and retained renderer.
|
|
use super::*;
|
|
use slint::Rgb8Pixel;
|
|
use slint::platform::{
|
|
Platform, WindowAdapter,
|
|
software_renderer::{MinimalSoftwareWindow, RepaintBufferType},
|
|
};
|
|
struct Headless(Rc<MinimalSoftwareWindow>, Rc<RefCell<String>>);
|
|
impl Platform for Headless {
|
|
fn create_window_adapter(&self) -> Result<Rc<dyn WindowAdapter>, slint::PlatformError> {
|
|
Ok(self.0.clone())
|
|
}
|
|
fn set_clipboard_text(&self, text: &str, _: slint::platform::Clipboard) {
|
|
*self.1.borrow_mut() = text.to_owned();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn automatic_selection_hydrates_and_reader_controls_are_responsive_and_keyboard_accessible() {
|
|
let window = MinimalSoftwareWindow::new(RepaintBufferType::NewBuffer);
|
|
let clipboard = Rc::new(RefCell::new(String::new()));
|
|
slint::platform::set_platform(Box::new(Headless(window.clone(), clipboard.clone()))).unwrap();
|
|
let app = AppWindow::new().unwrap();
|
|
app.global::<ZoomApi>()
|
|
.on_resolve(|action, current, min, max| {
|
|
crate::preview_controls::zoom(&action, current, min, max)
|
|
});
|
|
app.set_startup_ready(true);
|
|
app.set_startup_hydrated(true);
|
|
app.set_connected_accounts(ModelRc::new(VecModel::from(vec![AccountRow {
|
|
id: 1,
|
|
..Default::default()
|
|
}])));
|
|
app.set_active_view("mail".into());
|
|
app.window().set_size(slint::PhysicalSize::new(1280, 900));
|
|
app.show().unwrap();
|
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
|
.enable_all()
|
|
.build()
|
|
.unwrap();
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let (fav, _) = bounded_ui_channel();
|
|
let (avatars, _) = bounded_ui_channel();
|
|
let mut state = InboxState::empty(
|
|
None,
|
|
UiSender::new(fav, UiWake::new(app.as_weak(), |_| {})),
|
|
None,
|
|
UiSender::new(avatars, UiWake::new(app.as_weak(), |_| {})),
|
|
false,
|
|
false,
|
|
WarmStartCacheWriter::spawn(&runtime, dir.path().join("warm.json")),
|
|
);
|
|
state.messages = mail::fixture_messages();
|
|
state.messages.truncate(2);
|
|
for (i, message) in state.messages.iter_mut().enumerate() {
|
|
message.folder = "Inbox".into();
|
|
message.body_pending = true;
|
|
message.thread_id = Some(i as i64 + 1);
|
|
message.html = Some(format!(
|
|
"<body style='margin:0;padding:20px'><h2>Reader verification {i}</h2><p>Selectable <b>bold text</b> and <a href='https://example.com'>a working link</a>.</p><p><a style='display:inline-block;padding:16px;background:#e8ecff' href='https://example.com/shop'>Open details</a></p><img src='https://example.com/image.png' width='220' height='75' alt='A blocked image with an alternative'><p>Use Find, zoom, or the accessible Reader view.</p></body>"
|
|
));
|
|
}
|
|
state.mailboxes = fixture_mailboxes(&state.messages);
|
|
let ids: Vec<_> = state.messages.iter().map(|m| m.id).collect();
|
|
let renderer = state.email_renderer.clone();
|
|
renderer
|
|
.borrow_mut()
|
|
.configure_resources(runtime.handle().clone(), false)
|
|
.unwrap();
|
|
register_renderer_input_callbacks(&app, &renderer, false);
|
|
let requests = Rc::new(RefCell::new(Vec::new()));
|
|
let requests2 = requests.clone();
|
|
app.global::<EmailReader>()
|
|
.on_ensure_body(move |id| requests2.borrow_mut().push(id));
|
|
app.set_emails(state.email_rows.clone().into());
|
|
app.set_sidebar_rows(Rc::clone(&state.sidebar_rows).into());
|
|
let state = Rc::new(RefCell::new(state));
|
|
render_current(&app, &state, &runtime).unwrap();
|
|
assert_eq!(requests.borrow().last(), Some(&ids[0]));
|
|
let sidebar = app.get_sidebar_rows();
|
|
assert!(sidebar.row_count() > 0);
|
|
render_current(&app, &state, &runtime).unwrap();
|
|
assert_eq!(
|
|
sidebar,
|
|
app.get_sidebar_rows(),
|
|
"selecting a message must not rebuild unchanged folders"
|
|
);
|
|
// Archive removes the current row, and render_current chooses the next.
|
|
state.borrow_mut().messages.remove(0);
|
|
render_current(&app, &state, &runtime).unwrap();
|
|
assert_eq!(requests.borrow().last(), Some(&ids[1]));
|
|
assert_eq!(app.global::<EmailReader>().get_message_id(), ids[1]);
|
|
assert!(
|
|
app.get_selected_plain_text()
|
|
.contains("Reader verification 1")
|
|
);
|
|
app.global::<EmailReader>().set_body_pending(false);
|
|
let draw = |name: &str, width: u32, height: u32| {
|
|
slint::platform::update_timers_and_animations();
|
|
app.window()
|
|
.set_size(slint::PhysicalSize::new(width, height));
|
|
app.window().request_redraw();
|
|
let mut pixels = vec![Rgb8Pixel::default(); (width * height) as usize];
|
|
window.draw_if_needed(|r| {
|
|
r.render(&mut pixels, width as usize);
|
|
});
|
|
let (w, h) = email_viewport_size(&app);
|
|
if let Some(frame) = renderer
|
|
.borrow_mut()
|
|
.render_cpu_if_needed(w, h, 1.0)
|
|
.unwrap()
|
|
{
|
|
apply_cpu_frame(&app, frame);
|
|
}
|
|
sync_reader_metadata(&app, &renderer);
|
|
app.window().request_redraw();
|
|
window.draw_if_needed(|r| {
|
|
r.render(&mut pixels, width as usize);
|
|
});
|
|
let bytes: Vec<u8> = pixels.iter().flat_map(|p| [p.r, p.g, p.b]).collect();
|
|
std::fs::create_dir_all("tmp/blitz-fixes").unwrap();
|
|
image::save_buffer(
|
|
format!("tmp/blitz-fixes/{name}.png"),
|
|
&bytes,
|
|
width,
|
|
height,
|
|
image::ColorType::Rgb8,
|
|
)
|
|
.unwrap();
|
|
};
|
|
app.set_theme_mode("light".into());
|
|
let loaded_rows = app.get_emails();
|
|
app.set_emails(ModelRc::default());
|
|
app.set_mail_page_loading(true);
|
|
draw("navigation-loading", 1280, 900);
|
|
app.set_mail_page_loading(false);
|
|
app.set_emails(loaded_rows);
|
|
draw("desktop", 1280, 900);
|
|
let pointer = |x: f32, y: f32| {
|
|
app.window()
|
|
.dispatch_event(slint::platform::WindowEvent::PointerPressed {
|
|
position: slint::LogicalPosition::new(x, y),
|
|
button: slint::platform::PointerEventButton::Left,
|
|
});
|
|
app.window()
|
|
.dispatch_event(slint::platform::WindowEvent::PointerReleased {
|
|
position: slint::LogicalPosition::new(x, y),
|
|
button: slint::platform::PointerEventButton::Left,
|
|
});
|
|
};
|
|
let key = |text: slint::SharedString, pressed: bool| {
|
|
app.window().dispatch_event(if pressed {
|
|
slint::platform::WindowEvent::KeyPressed { text }
|
|
} else {
|
|
slint::platform::WindowEvent::KeyReleased { text }
|
|
});
|
|
};
|
|
// Icon controls must remain keyboard operable after replacing native buttons.
|
|
pointer(721.0, 222.0);
|
|
assert_eq!(
|
|
app.get_email_scroll_y(),
|
|
0.0,
|
|
"zoom must not hide the remote-image consent banner"
|
|
);
|
|
let zoom_after_click = renderer.borrow().zoom;
|
|
assert!(zoom_after_click < 1.0);
|
|
key(slint::platform::Key::Return.into(), true);
|
|
key(slint::platform::Key::Return.into(), false);
|
|
assert!(renderer.borrow().zoom < zoom_after_click);
|
|
key(slint::platform::Key::Tab.into(), true);
|
|
key(slint::platform::Key::Tab.into(), false);
|
|
key(slint::platform::Key::Space.into(), true);
|
|
key(slint::platform::Key::Space.into(), false);
|
|
assert_eq!(
|
|
renderer.borrow().zoom,
|
|
1.0,
|
|
"Tab and Space must reach reset zoom"
|
|
);
|
|
app.global::<EmailReader>()
|
|
.invoke_command("zoom-set:125%".into());
|
|
assert_eq!(renderer.borrow().zoom, 1.25);
|
|
app.global::<EmailReader>()
|
|
.invoke_command("zoom-set:NaN".into());
|
|
assert_eq!(renderer.borrow().zoom, 1.25);
|
|
app.global::<EmailReader>()
|
|
.invoke_command("zoom-set:100%".into());
|
|
// Edit the real percentage field, then exercise buttons after its initial
|
|
// text binding has been overwritten. Clipboard observes the displayed value.
|
|
let shortcut = |letter: &str| {
|
|
key(slint::platform::Key::Control.into(), true);
|
|
key(letter.into(), true);
|
|
key(letter.into(), false);
|
|
key(slint::platform::Key::Control.into(), false);
|
|
};
|
|
let copy_zoom = || {
|
|
pointer(820.0, 222.0);
|
|
shortcut("a");
|
|
shortcut("c");
|
|
clipboard.borrow().clone()
|
|
};
|
|
pointer(820.0, 222.0);
|
|
shortcut("a");
|
|
key("21%".into(), true);
|
|
key("21%".into(), false);
|
|
key(slint::platform::Key::Return.into(), true);
|
|
key(slint::platform::Key::Return.into(), false);
|
|
draw("zoom-clamped", 1280, 900);
|
|
assert_eq!(renderer.borrow().zoom, 0.5);
|
|
assert_eq!(
|
|
copy_zoom(),
|
|
"50%",
|
|
"typed values must show the applied clamp"
|
|
);
|
|
pointer(874.0, 222.0);
|
|
draw("zoom-in", 1280, 900);
|
|
assert_eq!(copy_zoom(), "60%");
|
|
pointer(721.0, 222.0);
|
|
draw("zoom-out", 1280, 900);
|
|
assert_eq!(copy_zoom(), "50%");
|
|
// Fit may leave zoom unchanged; it must still discard an unsubmitted draft.
|
|
shortcut("a");
|
|
key("21%".into(), true);
|
|
key("21%".into(), false);
|
|
pointer(919.0, 222.0);
|
|
draw("zoom-fit", 1280, 900);
|
|
assert_eq!(
|
|
copy_zoom(),
|
|
format!(
|
|
"{}%",
|
|
(app.global::<EmailReader>().get_zoom() * 100.0).round()
|
|
)
|
|
);
|
|
pointer(764.0, 222.0);
|
|
draw("zoom-reset", 1280, 900);
|
|
assert_eq!(copy_zoom(), "100%");
|
|
shortcut("a");
|
|
key("NaN".into(), true);
|
|
key("NaN".into(), false);
|
|
key(slint::platform::Key::Return.into(), true);
|
|
key(slint::platform::Key::Return.into(), false);
|
|
draw("zoom-invalid", 1280, 900);
|
|
assert_eq!(copy_zoom(), "100%");
|
|
draw("desktop-tooltip", 1280, 900);
|
|
pointer(750.0, 390.0);
|
|
key(slint::platform::Key::Control.into(), true);
|
|
key("a".into(), true);
|
|
key("a".into(), false);
|
|
assert!(
|
|
app.get_has_selection(),
|
|
"Ctrl+A must reach the rendered body"
|
|
);
|
|
key("c".into(), true);
|
|
key("c".into(), false);
|
|
key(slint::platform::Key::Control.into(), false);
|
|
assert!(
|
|
clipboard.borrow().contains("Reader verification"),
|
|
"The hidden clipboard bridge must remain functional"
|
|
);
|
|
let activated = Rc::new(RefCell::new(String::new()));
|
|
let target = activated.clone();
|
|
app.on_open_email_link(move |url| *target.borrow_mut() = url.to_string());
|
|
key(slint::platform::Key::Tab.into(), true);
|
|
key(slint::platform::Key::Tab.into(), false);
|
|
key(slint::platform::Key::Return.into(), true);
|
|
key(slint::platform::Key::Return.into(), false);
|
|
assert!(
|
|
activated.borrow().starts_with("https://example.com"),
|
|
"Tab must leave the document for its first actionable link"
|
|
);
|
|
app.global::<EmailReader>()
|
|
.invoke_find("selectable".into(), 0);
|
|
assert_eq!(app.global::<EmailReader>().get_find_status(), "1 / 1");
|
|
app.global::<EmailReader>().invoke_command("zoom-in".into());
|
|
assert!(renderer.borrow().zoom > 1.0);
|
|
app.set_theme_mode("dark".into());
|
|
draw("desktop-dark", 1280, 900);
|
|
app.set_theme_mode("light".into());
|
|
draw("phone-list", 390, 844);
|
|
app.window()
|
|
.dispatch_event(slint::platform::WindowEvent::PointerPressed {
|
|
position: slint::LogicalPosition::new(210.0, 180.0),
|
|
button: slint::platform::PointerEventButton::Left,
|
|
});
|
|
app.window()
|
|
.dispatch_event(slint::platform::WindowEvent::PointerReleased {
|
|
position: slint::LogicalPosition::new(210.0, 180.0),
|
|
button: slint::platform::PointerEventButton::Left,
|
|
});
|
|
draw("phone", 390, 844);
|
|
draw("tablet", 768, 1024);
|
|
draw("phone-landscape", 844, 390);
|
|
app.global::<EmailReader>().set_reader_mode(true);
|
|
app.global::<EmailReader>().set_dark_reader(true);
|
|
app.set_theme_mode("dark".into());
|
|
draw("reader-dark", 390, 844);
|
|
app.invoke_select_email_text();
|
|
assert!(app.get_has_selection());
|
|
// Hydrated attachments must survive a body-rendering failure and remain
|
|
// projected independently of the HTML/plain-text reader.
|
|
state.borrow_mut().messages[0].attachments = vec![flectar_mail_core::models::AttachmentMeta {
|
|
id: 42,
|
|
filename: Some("Project overview.pdf".into()),
|
|
mime_type: Some("application/pdf".into()),
|
|
size: Some(1024),
|
|
is_inline: false,
|
|
}];
|
|
render_current(&app, &state, &runtime).unwrap();
|
|
assert_eq!(app.global::<MailAttachments>().get_rows().row_count(), 1);
|
|
// A structural rejection while the reader is open must never leave the
|
|
// previous message's accessible content visible over the fallback.
|
|
state.borrow_mut().messages[0].html = Some("<span>x</span>".repeat(16_000));
|
|
render_current(&app, &state, &runtime).unwrap();
|
|
assert!(app.get_text_mode());
|
|
assert!(!app.global::<EmailReader>().get_available());
|
|
assert!(!app.get_has_selection());
|
|
assert!(app.get_selected_text().is_empty());
|
|
assert!(!app.global::<EmailReader>().get_reader_mode());
|
|
assert_eq!(app.global::<EmailReader>().get_items().row_count(), 0);
|
|
assert!(
|
|
!app.get_selected_plain_text()
|
|
.contains("Reader verification")
|
|
);
|
|
assert_eq!(app.global::<MailAttachments>().get_rows().row_count(), 1);
|
|
app.set_theme_mode("light".into());
|
|
draw("mail-attachments-fallback", 1280, 900);
|
|
draw("mail-attachments-landscape", 844, 390);
|
|
assert!(
|
|
app.get_email_viewport_height() >= 40.0,
|
|
"Short windows must retain room for the body while attachments remain available in the toolbar"
|
|
);
|
|
|
|
// Exercise the same percentage editor in the attachment dialog. A wide
|
|
// bitmap verifies fit on resize and manual zoom preservation without a
|
|
// network dependency or loading a native PDF library in this UI test.
|
|
let attachments = app.global::<MailAttachments>();
|
|
attachments.set_name("Project overview.pdf".into());
|
|
let mut page_pixels = slint::SharedPixelBuffer::<slint::Rgba8Pixel>::new(2400, 1600);
|
|
for (index, pixel) in page_pixels.make_mut_slice().iter_mut().enumerate() {
|
|
*pixel = slint::Rgba8Pixel::new(
|
|
(index % 2400 / 10) as u8,
|
|
(index / 2400 / 7) as u8,
|
|
128,
|
|
255,
|
|
);
|
|
}
|
|
attachments.set_image(slint::Image::from_rgba8(page_pixels));
|
|
attachments.set_pages(2);
|
|
attachments.set_is_image(true);
|
|
attachments.set_open(true);
|
|
draw("mail-preview-dialog", 1280, 900);
|
|
let copy_preview_zoom = || {
|
|
pointer(280.0, 128.0);
|
|
shortcut("a");
|
|
shortcut("c");
|
|
clipboard.borrow().clone()
|
|
};
|
|
let fitted = copy_preview_zoom();
|
|
assert_ne!(
|
|
fitted, "100%",
|
|
"Wide pages should fit the dialog on opening"
|
|
);
|
|
shortcut("a");
|
|
key("150%".into(), true);
|
|
key("150%".into(), false);
|
|
key(slint::platform::Key::Return.into(), true);
|
|
key(slint::platform::Key::Return.into(), false);
|
|
draw("mail-preview-zoom", 1280, 900);
|
|
assert_eq!(copy_preview_zoom(), "150%");
|
|
app.window()
|
|
.dispatch_event(slint::platform::WindowEvent::PointerPressed {
|
|
position: slint::LogicalPosition::new(900.0, 500.0),
|
|
button: slint::platform::PointerEventButton::Left,
|
|
});
|
|
for (x, y) in [(700.0, 450.0), (600.0, 400.0)] {
|
|
app.window()
|
|
.dispatch_event(slint::platform::WindowEvent::PointerMoved {
|
|
position: slint::LogicalPosition::new(x, y),
|
|
});
|
|
}
|
|
draw("mail-preview-pan", 1280, 900);
|
|
app.window()
|
|
.dispatch_event(slint::platform::WindowEvent::PointerReleased {
|
|
position: slint::LogicalPosition::new(600.0, 400.0),
|
|
button: slint::platform::PointerEventButton::Left,
|
|
});
|
|
let before = image::open("tmp/blitz-fixes/mail-preview-zoom.png")
|
|
.unwrap()
|
|
.to_rgb8();
|
|
let after = image::open("tmp/blitz-fixes/mail-preview-pan.png")
|
|
.unwrap()
|
|
.to_rgb8();
|
|
assert_ne!(
|
|
before.get_pixel(700, 500),
|
|
after.get_pixel(700, 500),
|
|
"Dragging must pan an enlarged preview"
|
|
);
|
|
draw("mail-preview-resize", 1200, 800);
|
|
draw("mail-preview-restored", 1280, 900);
|
|
assert_eq!(
|
|
copy_preview_zoom(),
|
|
"150%",
|
|
"Resizing must preserve manual zoom"
|
|
);
|
|
pointer(378.0, 128.0);
|
|
draw("mail-preview-fit", 1280, 900);
|
|
assert_eq!(copy_preview_zoom(), fitted);
|
|
let weak = app.as_weak();
|
|
attachments.on_command(move |action, _| {
|
|
if action == "close" {
|
|
weak.unwrap().global::<MailAttachments>().set_open(false);
|
|
}
|
|
});
|
|
key(slint::platform::Key::Escape.into(), true);
|
|
key(slint::platform::Key::Escape.into(), false);
|
|
assert!(
|
|
!attachments.get_open(),
|
|
"Escape must close a preview while its zoom editor is focused"
|
|
);
|
|
// High-frequency scroll input updates the final position immediately,
|
|
// but raster work is deferred and coalesced into one timer callback.
|
|
app.set_remote_images_blocked(false);
|
|
app.set_text_mode(false);
|
|
let html = format!(
|
|
"<body>{}</body>",
|
|
"<p style='height:110px'>Scroll integration</p>".repeat(100)
|
|
);
|
|
renderer.borrow_mut().set_zoom(1.0);
|
|
renderer.borrow_mut().set_auto_fit(false);
|
|
renderer
|
|
.borrow_mut()
|
|
.set_email(renderer::prepare_email_html(&html).unwrap());
|
|
let (width, height) = email_viewport_size(&app);
|
|
let frame = renderer
|
|
.borrow_mut()
|
|
.render_cpu_if_needed(width, height, 1.0)
|
|
.unwrap()
|
|
.unwrap();
|
|
apply_cpu_frame(&app, frame);
|
|
let tiles_model = app.get_email_tiles();
|
|
let tiles_before = renderer.borrow().tile_count;
|
|
let layouts_before = renderer.borrow().layout_count;
|
|
for y in [600.0, 1500.0, 3000.0] {
|
|
app.invoke_email_scroll(y, height as f32);
|
|
}
|
|
assert_eq!(
|
|
renderer.borrow().tile_count,
|
|
tiles_before,
|
|
"input must not rasterize synchronously"
|
|
);
|
|
std::thread::sleep(Duration::from_millis(25));
|
|
slint::platform::update_timers_and_animations();
|
|
assert!(renderer.borrow().tile_count > tiles_before);
|
|
assert!(renderer.borrow().tile_count <= tiles_before + 5);
|
|
assert_eq!(renderer.borrow().layout_count, layouts_before);
|
|
assert_eq!(
|
|
app.get_email_tiles(),
|
|
tiles_model,
|
|
"scroll must retain Slint's tile model"
|
|
);
|
|
|
|
state.borrow_mut().messages.clear();
|
|
render_current(&app, &state, &runtime).unwrap();
|
|
assert_eq!(app.global::<MailAttachments>().get_rows().row_count(), 0);
|
|
assert_eq!(app.global::<EmailReader>().get_message_id(), -1);
|
|
assert!(app.global::<EmailReader>().get_notice().is_empty());
|
|
assert!(!app.global::<EmailReader>().get_available());
|
|
// Renderer preferences persist independently of database startup and take
|
|
// effect only after restart. This test never initializes a GPU.
|
|
let preferences = tempfile::tempdir().unwrap();
|
|
let preference_path = preferences.path().join("renderer.json");
|
|
crate::renderer_preferences::register(
|
|
&app,
|
|
preference_path.clone(),
|
|
crate::renderer_preferences::RendererMode::Cpu,
|
|
false,
|
|
true,
|
|
);
|
|
let renderer_settings = app.global::<RendererSettings>();
|
|
renderer_settings.invoke_choose("gpu".into());
|
|
assert_eq!(
|
|
crate::renderer_preferences::load(&preference_path),
|
|
crate::renderer_preferences::RendererMode::Gpu
|
|
);
|
|
assert_eq!(renderer_settings.get_active(), "cpu");
|
|
assert!(renderer_settings.get_restart_required());
|
|
app.set_theme_mode("light".into());
|
|
app.set_settings_tab("General".into());
|
|
app.set_settings_open(true);
|
|
draw("renderer-settings-desktop", 1280, 900);
|
|
draw("renderer-settings-phone", 390, 844);
|
|
renderer_settings.invoke_choose("cpu".into());
|
|
assert!(!renderer_settings.get_restart_required());
|
|
assert_eq!(
|
|
crate::renderer_preferences::load(&preference_path),
|
|
crate::renderer_preferences::RendererMode::Cpu
|
|
);
|
|
// A failed atomic write must retain the actual saved choice.
|
|
crate::renderer_preferences::register(
|
|
&app,
|
|
preferences.path().to_owned(),
|
|
crate::renderer_preferences::RendererMode::Cpu,
|
|
false,
|
|
true,
|
|
);
|
|
renderer_settings.invoke_choose("gpu".into());
|
|
assert_eq!(renderer_settings.get_preferred(), "cpu");
|
|
assert!(!renderer_settings.get_error().is_empty());
|
|
app.set_settings_open(false);
|
|
for view in ["calendar", "contacts", "files", "mail", "calendar", "mail"] {
|
|
app.set_active_view(view.into());
|
|
draw(&format!("product-switch-{view}"), 1280, 900);
|
|
if view == "mail" {
|
|
assert!(app.get_email_viewport_width() > 1.0);
|
|
assert!(app.get_email_viewport_height() > 1.0);
|
|
}
|
|
}
|
|
// Releasing a product must retain the source record of an edited contact.
|
|
let contact = ContactRecord {
|
|
id: 1,
|
|
name: "Original name".into(),
|
|
email: "contact@example.com".into(),
|
|
phone: String::new(),
|
|
company: String::new(),
|
|
job_title: String::new(),
|
|
website: String::new(),
|
|
birthday: String::new(),
|
|
postal_address: String::new(),
|
|
notes: String::new(),
|
|
tags: String::new(),
|
|
is_favorite: false,
|
|
interactions: 0,
|
|
last_interacted: None,
|
|
account_ids: Vec::new(),
|
|
is_managed: false,
|
|
};
|
|
let directory = Rc::new(RefCell::new(ContactDirectoryState::new(
|
|
vec![contact],
|
|
false,
|
|
)));
|
|
apply_contact_directory(&app, &directory);
|
|
app.set_contact_name("Unsaved name".into());
|
|
assert!(!contacts::release_directory(
|
|
&app,
|
|
&mut directory.borrow_mut()
|
|
));
|
|
app.set_active_view("contacts".into());
|
|
draw("contact-edit-before-switch", 1280, 900);
|
|
app.set_active_view("mail".into());
|
|
draw("contact-edit-away", 1280, 900);
|
|
app.set_active_view("contacts".into());
|
|
draw("contact-edit-after-switch", 1280, 900);
|
|
assert_eq!(app.get_contact_name(), "Unsaved name");
|
|
assert_eq!(directory.borrow().contacts.len(), 1);
|
|
app.set_contact_name("Original name".into());
|
|
assert!(contacts::release_directory(
|
|
&app,
|
|
&mut directory.borrow_mut()
|
|
));
|
|
assert!(directory.borrow().contacts.is_empty());
|
|
app.hide().unwrap();
|
|
}
|