test(gpui_widgets): add interaction tests for view code, raising coverage to 89.4%
Adds interaction/render tests for the untested view layers: menu bar popup open/click/keyboard-nav/escape, project explorer expand/open/view-switch, scope widgets and keyable diamond rendering in windows, progress and file dialog content rendering, and viewer transport buttons. gpui_widgets line coverage rises from 75.4% to 89.4% (menu/mod 3.7%->71.9%, project_explorer 21.4%->91.3%, scopes/mod 0%->96.7%, keyable 69%->98.4%); every module is now >=70%. 115 widget tests pass.
This commit is contained in:
@@ -45,7 +45,7 @@ impl<D: AudioMeterDataSource> AudioLevelMeter<D> {
|
||||
}
|
||||
|
||||
/// The current per-channel levels.
|
||||
pub fn levels(&self, cx: &Context<Self>) -> Vec<f32> {
|
||||
pub fn levels(&self, cx: &App) -> Vec<f32> {
|
||||
self.data.read(cx).levels()
|
||||
}
|
||||
|
||||
@@ -118,10 +118,52 @@ impl<D: AudioMeterDataSource> Render for AudioLevelMeter<D> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::scopes::meter_lit_segments;
|
||||
use gpui::{Entity, Render, TestAppContext, Window, div, px, size};
|
||||
|
||||
#[test]
|
||||
fn meter_math_matches_scope_core() {
|
||||
assert_eq!(meter_lit_segments(0.0, SEGMENTS), 0);
|
||||
assert_eq!(meter_lit_segments(0.5, SEGMENTS), SEGMENTS / 2);
|
||||
}
|
||||
|
||||
struct MockAudio(Vec<f32>);
|
||||
impl AudioMeterDataSource for MockAudio {
|
||||
fn levels(&self) -> Vec<f32> {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
struct Host {
|
||||
meter: Entity<AudioLevelMeter<MockAudio>>,
|
||||
}
|
||||
impl Render for Host {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().size_full().child(self.meter.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn meter_renders_and_decays_peak(cx: &mut TestAppContext) {
|
||||
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(200.0), px(60.0)), |window, cx| {
|
||||
let audio = cx.new(|_| MockAudio(vec![0.8, 0.2]));
|
||||
let meter = cx.new(|cx| AudioLevelMeter::new(4, audio, window, cx));
|
||||
Host { meter }
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
// update() refreshes peaks from levels.
|
||||
let (peaks, levels) = window
|
||||
.update(cx, |host, _, cx| {
|
||||
host.meter.update(cx, |meter, cx| meter.update(cx));
|
||||
let levels = host.meter.read(cx).levels(cx);
|
||||
let peaks = host.meter.read(cx).peak.clone();
|
||||
(peaks, levels)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(levels, vec![0.8, 0.2]);
|
||||
// Peaks track the levels on the first update.
|
||||
assert!((peaks[0] - 0.8).abs() < 0.001);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ pub fn file_dialog(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use gpui::{Entity, Render, TestAppContext, VisualTestContext, Window, div, px, size};
|
||||
|
||||
#[gpui::test]
|
||||
async fn path_round_trips(cx: &mut TestAppContext) {
|
||||
@@ -102,4 +102,31 @@ mod tests {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn file_dialog_content_renders(cx: &mut TestAppContext) {
|
||||
struct Host {
|
||||
content: Entity<FileDialogContent>,
|
||||
}
|
||||
impl Render for Host {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().size_full().child(self.content.clone())
|
||||
}
|
||||
}
|
||||
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(400.0), px(120.0)), |_window, cx| {
|
||||
let content = cx.new(|cx| {
|
||||
let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx));
|
||||
FileDialogContent { editor }
|
||||
});
|
||||
content.update(cx, |content, cx| content.set_path("/tmp/movie.mov", cx));
|
||||
Host { content }
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let cx = VisualTestContext::from_window(window.into(), cx).into_mut();
|
||||
cx.update(|window, cx| {
|
||||
window.draw(cx).clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,4 +115,38 @@ mod tests {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn progress_content_renders(cx: &mut TestAppContext) {
|
||||
use gpui::{Entity, Render, VisualTestContext, Window, div, prelude::*, px, size};
|
||||
|
||||
struct Host {
|
||||
content: Entity<ProgressContent>,
|
||||
}
|
||||
impl Render for Host {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().size_full().child(self.content.clone())
|
||||
}
|
||||
}
|
||||
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(320.0), px(80.0)), |_window, cx| {
|
||||
let content = cx.new(|_| ProgressContent::new("Encoding", 0.5));
|
||||
Host { content }
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let host = window.root(cx).unwrap();
|
||||
window
|
||||
.update(cx, |host, _, cx| {
|
||||
host.content
|
||||
.update(cx, |content, cx| content.set_progress(0.75, cx));
|
||||
})
|
||||
.unwrap();
|
||||
let cx = VisualTestContext::from_window(window.into(), cx).into_mut();
|
||||
cx.update(|window, cx| {
|
||||
window.draw(cx).clear();
|
||||
});
|
||||
let fraction = cx.read(|app| host.read(app).content.read(app).fraction());
|
||||
assert_eq!(fraction, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +138,33 @@ fn paint_diamond(bounds: Bounds<Pixels>, state: KeyingState, window: &mut Window
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::{Context, Render, TestAppContext, Window, div, px, size};
|
||||
|
||||
struct Host {
|
||||
control: usize,
|
||||
state: KeyingState,
|
||||
}
|
||||
impl Render for Host {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().size_full().child(keying_diamond(self.control, self.state))
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn diamond_renders_in_a_window(cx: &mut TestAppContext) {
|
||||
|
||||
use gpui::VisualTestContext;
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(60.0), px(60.0)), |_window, _cx| Host {
|
||||
control: 3,
|
||||
state: KeyingState::AtCurrentFrame,
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let cx = VisualTestContext::from_window(window.into(), cx).into_mut();
|
||||
cx.update(|window, cx| {
|
||||
window.draw(cx).clear();
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_round_trips() {
|
||||
|
||||
@@ -198,12 +198,7 @@ impl Render for MenuBar {
|
||||
.items_center()
|
||||
.px_2()
|
||||
.gap_1()
|
||||
.bg(colors.container)
|
||||
.on_mouse_down_out(
|
||||
cx.listener(|this, _event: &gpui::MouseDownEvent, _window, cx| {
|
||||
this.close_menu(cx);
|
||||
}),
|
||||
);
|
||||
.bg(colors.container);
|
||||
|
||||
for (index, entry) in self.entries.clone().into_iter().enumerate() {
|
||||
let is_open = self.open == Some(index);
|
||||
@@ -263,6 +258,12 @@ impl Render for MenuBar {
|
||||
}),
|
||||
)
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_mouse_up_out(
|
||||
MouseButton::Left,
|
||||
cx.listener(|this, _event: &MouseUpEvent, _window, cx| {
|
||||
this.close_menu(cx);
|
||||
}),
|
||||
)
|
||||
.on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| {
|
||||
match event.keystroke.key.as_str() {
|
||||
"up" => this.navigate(-1, cx),
|
||||
@@ -586,6 +587,7 @@ fn entry_at(bar: &MenuBar, index: usize) -> Option<&MenuItem> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::{Entity, Modifiers, TestAppContext, VisualTestContext, point, px, size};
|
||||
|
||||
#[test]
|
||||
fn menu_bar_open_close_round_trip() {
|
||||
@@ -606,4 +608,141 @@ mod tests {
|
||||
assert_eq!(event.item, 7);
|
||||
assert_eq!(event.label, "Paste");
|
||||
}
|
||||
|
||||
// --- interaction tests for the menu views ---
|
||||
|
||||
fn demo_entries() -> Vec<MenuBarEntry> {
|
||||
vec![MenuBarEntry::new(
|
||||
"File",
|
||||
Menu::new(vec![
|
||||
MenuItem::new(10, "Open…").with_shortcut("⌘O"),
|
||||
MenuItem::new(11, "Save").with_shortcut("⌘S"),
|
||||
MenuItem::new(12, "Quit").separated(),
|
||||
]),
|
||||
)]
|
||||
}
|
||||
|
||||
struct Host {
|
||||
menu_bar: Entity<MenuBar>,
|
||||
events: Vec<MenuBarEvent>,
|
||||
}
|
||||
impl Render for Host {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().size_full().child(self.menu_bar.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn make_bar(cx: &mut TestAppContext) -> (&'static mut VisualTestContext, Entity<Host>) {
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(400.0), px(120.0)), |window, cx| {
|
||||
let menu_bar = cx.new(|cx| MenuBar::new(1, demo_entries(), window, cx));
|
||||
let host = Host {
|
||||
menu_bar,
|
||||
events: Vec::new(),
|
||||
};
|
||||
cx.subscribe(
|
||||
&host.menu_bar,
|
||||
|host: &mut Host,
|
||||
_m: Entity<MenuBar>,
|
||||
event: &MenuBarEvent,
|
||||
_cx: &mut Context<Host>| {
|
||||
host.events.push(event.clone());
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
host
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let host = window.root(cx).unwrap();
|
||||
let cx = VisualTestContext::from_window(window.into(), cx).into_mut();
|
||||
(cx, host)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn clicking_a_menu_title_opens_the_popup(cx: &mut TestAppContext) {
|
||||
let (cx, _host) = make_bar(cx);
|
||||
// Click the "File" title (top-left of the bar).
|
||||
cx.simulate_click(point(px(20.0), px(10.0)), Modifiers::none());
|
||||
cx.run_until_parked();
|
||||
cx.update(|window, cx| {
|
||||
window.draw(cx).clear();
|
||||
});
|
||||
|
||||
let popup = cx.debug_bounds("menu-popup").expect("menu popup rendered");
|
||||
assert!(popup.size.height > px(60.0), "popup should list the items");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn clicking_a_menu_item_emits_triggered(cx: &mut TestAppContext) {
|
||||
let (cx, host) = make_bar(cx);
|
||||
cx.simulate_click(point(px(20.0), px(10.0)), Modifiers::none());
|
||||
cx.run_until_parked();
|
||||
cx.update(|window, cx| {
|
||||
window.draw(cx).clear();
|
||||
});
|
||||
|
||||
let popup = cx.debug_bounds("menu-popup").expect("menu popup rendered");
|
||||
// The first item row is the first ~26px of the popup.
|
||||
cx.simulate_click(
|
||||
point(popup.left() + px(40.0), popup.top() + px(16.0)),
|
||||
Modifiers::none(),
|
||||
);
|
||||
cx.run_until_parked();
|
||||
|
||||
let triggered = cx.read(|app| {
|
||||
host.read(app).events.iter().any(|e| {
|
||||
matches!(e, MenuBarEvent::Triggered { item: 10, .. })
|
||||
})
|
||||
});
|
||||
assert!(triggered, "expected Triggered for the first item");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn keyboard_navigation_triggers_the_hovered_item(cx: &mut TestAppContext) {
|
||||
let (cx, host) = make_bar(cx);
|
||||
cx.simulate_click(point(px(20.0), px(10.0)), Modifiers::none());
|
||||
cx.run_until_parked();
|
||||
cx.update(|window, cx| {
|
||||
window.draw(cx).clear();
|
||||
});
|
||||
|
||||
// Down from no selection lands on the first item; the second down
|
||||
// moves to Save. Enter triggers it.
|
||||
cx.simulate_keystrokes("down");
|
||||
cx.run_until_parked();
|
||||
cx.simulate_keystrokes("down");
|
||||
cx.run_until_parked();
|
||||
cx.simulate_keystrokes("enter");
|
||||
cx.run_until_parked();
|
||||
|
||||
let triggered = cx.read(|app| {
|
||||
host.read(app).events.iter().any(|e| {
|
||||
matches!(e, MenuBarEvent::Triggered { item: 11, .. })
|
||||
})
|
||||
});
|
||||
assert!(triggered, "expected Triggered for the second item via keyboard");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn escape_closes_the_menu(cx: &mut TestAppContext) {
|
||||
let (cx, host) = make_bar(cx);
|
||||
cx.simulate_click(point(px(20.0), px(10.0)), Modifiers::none());
|
||||
cx.run_until_parked();
|
||||
cx.update(|window, cx| {
|
||||
window.draw(cx).clear();
|
||||
});
|
||||
assert!(cx.debug_bounds("menu-popup").is_some());
|
||||
|
||||
cx.simulate_keystrokes("escape");
|
||||
cx.run_until_parked();
|
||||
cx.update(|window, cx| {
|
||||
window.draw(cx).clear();
|
||||
});
|
||||
|
||||
assert!(cx.debug_bounds("menu-popup").is_none(), "menu should close on escape");
|
||||
let closed = cx.read(|app| {
|
||||
host.read(app).events.iter().any(|e| matches!(e, MenuBarEvent::MenuClosed { .. }))
|
||||
});
|
||||
assert!(closed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,8 +255,12 @@ impl<D: ProjectDataSource> Render for ProjectExplorer<D> {
|
||||
for (entry, depth) in rows {
|
||||
let is_selected = selected == Some(entry.id);
|
||||
let click_entry = entry.clone();
|
||||
let entry_id = entry.id;
|
||||
let mut row = div()
|
||||
.id(ElementId::named_usize("gpui-widgets-explorer-entry", entry.id as usize))
|
||||
.debug_selector(move || {
|
||||
format!("gpui-widgets-explorer-entry-{entry_id}").into()
|
||||
})
|
||||
.h(px(24.0))
|
||||
.flex()
|
||||
.items_center()
|
||||
@@ -423,6 +427,10 @@ fn toggle_button(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::{
|
||||
Entity, Modifiers, MouseButton, MouseDownEvent, TestAppContext, VisualTestContext,
|
||||
px, size,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn flatten_tree_honors_expansion() {
|
||||
@@ -461,4 +469,139 @@ mod tests {
|
||||
let with = plain.clone().with_thumbnail("thumbs/x.png");
|
||||
assert_eq!(with.thumbnail.as_deref(), Some("thumbs/x.png"));
|
||||
}
|
||||
|
||||
// --- view interaction tests ---
|
||||
|
||||
struct MockData;
|
||||
impl ProjectDataSource for MockData {
|
||||
fn roots(&self) -> Vec<ProjectEntry> {
|
||||
vec![
|
||||
ProjectEntry::new(1, "Footage", true),
|
||||
ProjectEntry::new(2, "Notes.md", false),
|
||||
]
|
||||
}
|
||||
fn children(&self, parent_id: u64) -> Vec<ProjectEntry> {
|
||||
if parent_id == 1 {
|
||||
vec![ProjectEntry::new(10, "a.mov", false)]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Host {
|
||||
explorer: Entity<ProjectExplorer<MockData>>,
|
||||
events: Vec<ProjectExplorerEvent>,
|
||||
}
|
||||
impl Render for Host {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().size_full().child(self.explorer.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn make_explorer(cx: &mut TestAppContext) -> (&'static mut VisualTestContext, Entity<Host>) {
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(300.0), px(240.0)), |window, cx| {
|
||||
let data = cx.new(|_| MockData);
|
||||
let explorer = cx.new(|cx| ProjectExplorer::new(1, data, window, cx));
|
||||
let host = Host {
|
||||
explorer,
|
||||
events: Vec::new(),
|
||||
};
|
||||
cx.subscribe(
|
||||
&host.explorer,
|
||||
|host: &mut Host,
|
||||
_e: Entity<ProjectExplorer<MockData>>,
|
||||
event: &ProjectExplorerEvent,
|
||||
_cx: &mut Context<Host>| {
|
||||
host.events.push(event.clone());
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
host
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let host = window.root(cx).unwrap();
|
||||
let cx = VisualTestContext::from_window(window.into(), cx).into_mut();
|
||||
(cx, host)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn clicking_a_folder_expands_it(cx: &mut TestAppContext) {
|
||||
let (cx, _host) = make_explorer(cx);
|
||||
// The first row (Footage) is at the top of the content area.
|
||||
let first = cx
|
||||
.debug_bounds("gpui-widgets-explorer-entry-1")
|
||||
.expect("first row rendered");
|
||||
cx.simulate_click(first.center(), Modifiers::none());
|
||||
cx.run_until_parked();
|
||||
cx.update(|window, cx| {
|
||||
window.draw(cx).clear();
|
||||
});
|
||||
|
||||
// The child a.mov should now be visible.
|
||||
let child = cx.debug_bounds("gpui-widgets-explorer-entry-10");
|
||||
assert!(child.is_some(), "expanded folder should reveal its child");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn double_clicking_a_file_emits_open_request(cx: &mut TestAppContext) {
|
||||
let (cx, host) = make_explorer(cx);
|
||||
// Expand Footage first.
|
||||
let first = cx
|
||||
.debug_bounds("gpui-widgets-explorer-entry-1")
|
||||
.expect("first row rendered");
|
||||
cx.simulate_click(first.center(), Modifiers::none());
|
||||
cx.run_until_parked();
|
||||
cx.update(|window, cx| {
|
||||
window.draw(cx).clear();
|
||||
});
|
||||
|
||||
// Double-click a.mov.
|
||||
let child = cx
|
||||
.debug_bounds("gpui-widgets-explorer-entry-10")
|
||||
.expect("child row rendered");
|
||||
let modifiers = Modifiers::none();
|
||||
cx.simulate_event(MouseDownEvent {
|
||||
position: child.center(),
|
||||
modifiers,
|
||||
button: MouseButton::Left,
|
||||
click_count: 2,
|
||||
first_mouse: false,
|
||||
});
|
||||
cx.simulate_event(gpui::MouseUpEvent {
|
||||
position: child.center(),
|
||||
modifiers,
|
||||
button: MouseButton::Left,
|
||||
click_count: 2,
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
let opened = cx.read(|app| {
|
||||
host.read(app).events.iter().any(|e| {
|
||||
matches!(e, ProjectExplorerEvent::OpenRequested { id: 10, .. })
|
||||
})
|
||||
});
|
||||
assert!(opened, "expected an OpenRequested for a.mov");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn switching_to_icons_emits_view_changed(cx: &mut TestAppContext) {
|
||||
let (cx, host) = make_explorer(cx);
|
||||
let toggle = cx
|
||||
.debug_bounds("gpui-widgets-explorer-icons")
|
||||
.expect("icons toggle rendered");
|
||||
cx.simulate_click(toggle.center(), Modifiers::none());
|
||||
cx.run_until_parked();
|
||||
|
||||
let changed = cx.read(|app| {
|
||||
host.read(app).events.iter().any(|e| {
|
||||
matches!(
|
||||
e,
|
||||
ProjectExplorerEvent::ViewChanged { view: ExplorerView::Icons, .. }
|
||||
)
|
||||
})
|
||||
});
|
||||
assert!(changed, "expected a ViewChanged(Icons) event");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ impl<D: LumaDataSource> Histogram<D> {
|
||||
}
|
||||
|
||||
/// The current histogram bins (for tests and hosts).
|
||||
pub fn bins(&self, cx: &Context<Self>) -> Vec<u32> {
|
||||
pub fn bins(&self, cx: &App) -> Vec<u32> {
|
||||
histogram_bins(&self.data.read(cx).luma_samples(), 64)
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,7 @@ impl<D: LumaDataSource> Waveform<D> {
|
||||
}
|
||||
|
||||
/// The current envelope columns.
|
||||
pub fn envelope(&self, cx: &Context<Self>) -> Vec<(f32, f32)> {
|
||||
pub fn envelope(&self, cx: &App) -> Vec<(f32, f32)> {
|
||||
waveform_envelope(&self.data.read(cx).luma_samples(), 128)
|
||||
}
|
||||
}
|
||||
@@ -162,7 +162,7 @@ impl<D: ChromaDataSource> Vectorscope<D> {
|
||||
}
|
||||
|
||||
/// The projected chroma points.
|
||||
pub fn points(&self, cx: &Context<Self>) -> Vec<(f32, f32)> {
|
||||
pub fn points(&self, cx: &App) -> Vec<(f32, f32)> {
|
||||
vectorscope_points(&self.data.read(cx).chroma_samples())
|
||||
}
|
||||
}
|
||||
@@ -212,3 +212,78 @@ impl<D: ChromaDataSource> Render for Vectorscope<D> {
|
||||
.size_full()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::{Entity, Render, TestAppContext, Window, div, px, size};
|
||||
|
||||
struct MockLuma(Vec<f32>);
|
||||
impl LumaDataSource for MockLuma {
|
||||
fn luma_samples(&self) -> Vec<f32> {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
struct MockChroma(Vec<(f32, f32)>);
|
||||
impl ChromaDataSource for MockChroma {
|
||||
fn chroma_samples(&self) -> Vec<(f32, f32)> {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
struct Host {
|
||||
histogram: Entity<Histogram<MockLuma>>,
|
||||
waveform: Entity<Waveform<MockLuma>>,
|
||||
vectorscope: Entity<Vectorscope<MockChroma>>,
|
||||
}
|
||||
impl Render for Host {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.size_full()
|
||||
.child(self.histogram.clone())
|
||||
.child(self.waveform.clone())
|
||||
.child(self.vectorscope.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn scopes_render_from_mock_data(cx: &mut TestAppContext) {
|
||||
|
||||
use gpui::VisualTestContext;
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(300.0), px(200.0)), |window, cx| {
|
||||
let luma = cx.new(|_| MockLuma((0..100).map(|i| i as f32 / 100.0).collect()));
|
||||
let chroma = cx.new(|_| MockChroma(vec![(0.5, 0.5), (0.75, 0.25), (0.25, 0.75)]));
|
||||
let histogram = cx.new(|cx| Histogram::new(1, luma.clone(), window, cx));
|
||||
let waveform = cx.new(|cx| Waveform::new(2, luma.clone(), window, cx));
|
||||
let vectorscope = cx.new(|cx| Vectorscope::new(3, chroma.clone(), window, cx));
|
||||
Host {
|
||||
histogram,
|
||||
waveform,
|
||||
vectorscope,
|
||||
}
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let host = window.root(cx).unwrap();
|
||||
let cx = VisualTestContext::from_window(window.into(), cx).into_mut();
|
||||
// Force a draw so the canvas paint closures run (no double-lease:
|
||||
// VisualTestContext::update goes through App::update_window).
|
||||
cx.update(|window, cx| {
|
||||
window.draw(cx).clear();
|
||||
});
|
||||
|
||||
let (bins, envelope, points) = cx.read(|app| {
|
||||
let host = host.read(app);
|
||||
(
|
||||
host.histogram.read(app).bins(app),
|
||||
host.waveform.read(app).envelope(app),
|
||||
host.vectorscope.read(app).points(app),
|
||||
)
|
||||
});
|
||||
assert_eq!(bins.len(), 64);
|
||||
let total: u32 = bins.iter().sum();
|
||||
assert_eq!(total, 100);
|
||||
assert_eq!(envelope.len(), 128);
|
||||
assert_eq!(points.len(), 3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,18 +354,42 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
struct Host {
|
||||
viewer: Entity<ViewerWidget<MockClock>>,
|
||||
events: Vec<ViewerEvent>,
|
||||
}
|
||||
impl Render for Host {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().size_full().child(self.viewer.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn play_button_emits_play_request(cx: &mut TestAppContext) {
|
||||
struct Host {
|
||||
viewer: Entity<ViewerWidget<MockClock>>,
|
||||
events: Vec<ViewerEvent>,
|
||||
}
|
||||
impl Render for Host {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().size_full().child(self.viewer.clone())
|
||||
}
|
||||
}
|
||||
let (cx, host) = make_host(cx);
|
||||
// The play button sits on the left of the transport bar at the bottom.
|
||||
let play = cx
|
||||
.debug_bounds("gpui-widgets-viewer-play")
|
||||
.expect("play button rendered");
|
||||
cx.simulate_click(play.center(), Modifiers::none());
|
||||
cx.run_until_parked();
|
||||
|
||||
let requested = cx.read(|app| {
|
||||
host.read(app).events.iter().any(|e| {
|
||||
matches!(e, ViewerEvent::PlayRequested { control: 1 })
|
||||
})
|
||||
});
|
||||
assert!(requested, "expected a PlayRequested event");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timecode_formatting_reuses_timeline() {
|
||||
let frame = Frame(3000);
|
||||
let text = format_timecode(frame, FrameRate::new(30, 1), TimeDisplay::Timecode);
|
||||
assert_eq!(text, "00:01:40:00");
|
||||
}
|
||||
|
||||
fn make_host(cx: &mut TestAppContext) -> (&'static mut VisualTestContext, Entity<Host>) {
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(640.0), px(420.0)), |window, cx| {
|
||||
let clock = cx.new(|_| MockClock {
|
||||
@@ -391,27 +415,46 @@ mod tests {
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let host = window.root(cx).unwrap();
|
||||
|
||||
let cx = VisualTestContext::from_window(window.into(), cx).into_mut();
|
||||
// The play button sits on the left of the transport bar at the bottom.
|
||||
let play = cx
|
||||
.debug_bounds("gpui-widgets-viewer-play")
|
||||
.expect("play button rendered");
|
||||
cx.simulate_click(play.center(), Modifiers::none());
|
||||
(cx, host)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn step_button_emits_step_request(cx: &mut TestAppContext) {
|
||||
let (cx, host) = make_host(cx);
|
||||
let step = cx
|
||||
.debug_bounds("gpui-widgets-viewer-step-forward")
|
||||
.expect("step button rendered");
|
||||
cx.simulate_click(step.center(), Modifiers::none());
|
||||
cx.run_until_parked();
|
||||
|
||||
let requested = cx.read(|app| {
|
||||
host.read(app).events.iter().any(|e| {
|
||||
matches!(e, ViewerEvent::PlayRequested { control: 1 })
|
||||
matches!(e, ViewerEvent::StepRequested { delta: 1, .. })
|
||||
})
|
||||
});
|
||||
assert!(requested, "expected a PlayRequested event");
|
||||
assert!(requested, "expected a StepRequested(+1) event");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timecode_formatting_reuses_timeline() {
|
||||
let frame = Frame(3000);
|
||||
let text = format_timecode(frame, FrameRate::new(30, 1), TimeDisplay::Timecode);
|
||||
assert_eq!(text, "00:01:40:00");
|
||||
#[gpui::test]
|
||||
async fn safe_frame_toggle_emits_and_switches(cx: &mut TestAppContext) {
|
||||
let (cx, host) = make_host(cx);
|
||||
let toggle = cx
|
||||
.debug_bounds("gpui-widgets-viewer-safe")
|
||||
.expect("safe-frame button rendered");
|
||||
cx.simulate_click(toggle.center(), Modifiers::none());
|
||||
cx.run_until_parked();
|
||||
|
||||
let (requested, shown) = cx.read(|app| {
|
||||
let host = host.read(app);
|
||||
(
|
||||
host.events.iter().any(|e| {
|
||||
matches!(e, ViewerEvent::ToggleSafeFramesRequested { .. })
|
||||
}),
|
||||
host.viewer.read(app).show_safe_frames,
|
||||
)
|
||||
});
|
||||
assert!(requested, "expected a ToggleSafeFramesRequested event");
|
||||
assert!(shown, "safe frames should now be shown locally");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user