419 lines
15 KiB
Rust
419 lines
15 KiB
Rust
//! Test grid view mode of the `DataFusionTableWidget`.
|
|
|
|
mod common;
|
|
|
|
use std::sync::Arc;
|
|
|
|
use arrow::array::{BooleanArray, Float64Array, RecordBatch, StringArray};
|
|
use arrow::datatypes::{DataType, Field, Schema};
|
|
use datafusion::prelude::SessionContext;
|
|
use egui::accesskit::Role;
|
|
use egui_kittest::SnapshotResults;
|
|
use egui_kittest::kittest::Queryable as _;
|
|
use re_dataframe_ui::DataFusionTableWidget;
|
|
use re_test_context::TestContext;
|
|
use re_viewer_context::AsyncRuntimeHandle;
|
|
|
|
use common::run_async_harness;
|
|
|
|
/// Basic grid view rendering in dark and light theme.
|
|
#[tokio::test(flavor = "multi_thread")] // `multi_thread` required because `ConnectionRegistryHandle::credentials` uses `block_in_place`.
|
|
async fn test_grid_view() {
|
|
let (session_context, table_ref) = setup_test_table(false);
|
|
let mut snapshot_results = SnapshotResults::new();
|
|
|
|
for (theme, suffix) in [(egui::Theme::Dark, "dark"), (egui::Theme::Light, "light")] {
|
|
let mut test_context = TestContext::new();
|
|
test_context
|
|
.app_options
|
|
.experimental
|
|
.table_cards_and_blueprints = true;
|
|
let runtime_handle =
|
|
AsyncRuntimeHandle::from_current_tokio_runtime_or_wasmbindgen().unwrap();
|
|
|
|
let mut harness = test_context
|
|
.setup_kittest_for_rendering_ui([800.0, 600.0])
|
|
.with_theme(theme)
|
|
.build_ui(|ui| {
|
|
test_context.run_recording(&ui.ctx().clone(), |ctx| {
|
|
DataFusionTableWidget::new(Arc::clone(&session_context), table_ref)
|
|
.title("Grid view test")
|
|
.show(
|
|
ctx,
|
|
&runtime_handle,
|
|
ui,
|
|
&mut test_context.view_states.lock(),
|
|
);
|
|
});
|
|
});
|
|
|
|
run_async_harness(&mut harness).await;
|
|
|
|
// Switch to grid mode.
|
|
harness.get_by_label("Grid view").click();
|
|
run_async_harness(&mut harness).await;
|
|
|
|
harness.snapshot(format!("grid_view_basic_{suffix}"));
|
|
snapshot_results.extend_harness(&mut harness);
|
|
}
|
|
}
|
|
|
|
/// Test that the grid reflows when rendered at different widths.
|
|
#[tokio::test(flavor = "multi_thread")] // `multi_thread` required because `ConnectionRegistryHandle::credentials` uses `block_in_place`.
|
|
async fn test_grid_view_resize() {
|
|
let (session_context, table_ref) = setup_test_table(false);
|
|
let mut snapshot_results = SnapshotResults::new();
|
|
|
|
for (width, suffix) in [(400.0, "narrow"), (1200.0, "wide")] {
|
|
let mut test_context = TestContext::new();
|
|
test_context
|
|
.app_options
|
|
.experimental
|
|
.table_cards_and_blueprints = true;
|
|
let runtime_handle =
|
|
AsyncRuntimeHandle::from_current_tokio_runtime_or_wasmbindgen().unwrap();
|
|
|
|
let mut harness = test_context
|
|
.setup_kittest_for_rendering_ui([width, 600.0])
|
|
.build_ui(|ui| {
|
|
test_context.run_recording(&ui.ctx().clone(), |ctx| {
|
|
DataFusionTableWidget::new(Arc::clone(&session_context), table_ref)
|
|
.title("Grid resize test")
|
|
.show(
|
|
ctx,
|
|
&runtime_handle,
|
|
ui,
|
|
&mut test_context.view_states.lock(),
|
|
);
|
|
});
|
|
});
|
|
|
|
run_async_harness(&mut harness).await;
|
|
|
|
// Switch to grid mode.
|
|
harness.get_by_label("Grid view").click();
|
|
run_async_harness(&mut harness).await;
|
|
|
|
harness.snapshot(format!("grid_view_resize_{suffix}"));
|
|
snapshot_results.extend_harness(&mut harness);
|
|
}
|
|
}
|
|
|
|
/// Test flag toggle interaction in dark and light theme (in-memory only).
|
|
///
|
|
/// Verifies that flag buttons appear and clicking toggles the visual state.
|
|
/// Server-side persistence of flag changes is tested in
|
|
/// `re_integration_test::tests::grid_view_flagging`.
|
|
#[tokio::test(flavor = "multi_thread")] // `multi_thread` required because `ConnectionRegistryHandle::credentials` uses `block_in_place`.
|
|
async fn test_grid_view_flagging() {
|
|
let (session_context, table_ref) = setup_test_table(true);
|
|
let mut snapshot_results = SnapshotResults::new();
|
|
|
|
// Fake remote URI — flagging requires remote_table + table index to be enabled.
|
|
let remote_uri: re_uri::EntryUri =
|
|
"rerun+http://localhost:1234/entry/00000000000000000000000000000001"
|
|
.parse()
|
|
.unwrap();
|
|
|
|
for (theme, suffix) in [(egui::Theme::Dark, "dark"), (egui::Theme::Light, "light")] {
|
|
let mut test_context = TestContext::new();
|
|
test_context
|
|
.app_options
|
|
.experimental
|
|
.table_cards_and_blueprints = true;
|
|
let runtime_handle =
|
|
AsyncRuntimeHandle::from_current_tokio_runtime_or_wasmbindgen().unwrap();
|
|
|
|
let mut harness = test_context
|
|
.setup_kittest_for_rendering_ui([800.0, 600.0])
|
|
.with_theme(theme)
|
|
.build_ui(|ui| {
|
|
test_context.run_recording(&ui.ctx().clone(), |ctx| {
|
|
DataFusionTableWidget::new(Arc::clone(&session_context), table_ref)
|
|
.title("Flag test")
|
|
.url(remote_uri.to_string())
|
|
.show(
|
|
ctx,
|
|
&runtime_handle,
|
|
ui,
|
|
&mut test_context.view_states.lock(),
|
|
);
|
|
});
|
|
});
|
|
|
|
run_async_harness(&mut harness).await;
|
|
harness.get_by_label("Grid view").click();
|
|
run_async_harness(&mut harness).await;
|
|
harness.snapshot(format!("grid_view_flagging_{suffix}"));
|
|
|
|
// Toggle the first flag.
|
|
harness
|
|
.query_all_by_role_and_label(Role::CheckBox, "Flag")
|
|
.next()
|
|
.expect("Expected at least one flag button.")
|
|
.click();
|
|
run_async_harness(&mut harness).await;
|
|
harness.snapshot(format!("grid_view_flagging_toggled_{suffix}"));
|
|
|
|
snapshot_results.extend_harness(&mut harness);
|
|
}
|
|
}
|
|
|
|
/// Test grid view with non-uniform card heights to exercise virtualized layout.
|
|
///
|
|
/// Creates 30 rows with varying content lengths — some with long multi-word notes
|
|
/// that wrap, some with short or missing values — so cards end up at different heights.
|
|
#[tokio::test(flavor = "multi_thread")] // `multi_thread` required because `ConnectionRegistryHandle::credentials` uses `block_in_place`.
|
|
async fn test_grid_view_non_uniform_cards() {
|
|
let (session_context, table_ref) = setup_non_uniform_table();
|
|
let mut test_context = TestContext::new();
|
|
test_context
|
|
.app_options
|
|
.experimental
|
|
.table_cards_and_blueprints = true;
|
|
let runtime_handle = AsyncRuntimeHandle::from_current_tokio_runtime_or_wasmbindgen().unwrap();
|
|
|
|
let mut harness = test_context
|
|
.setup_kittest_for_rendering_ui([800.0, 600.0])
|
|
.build_ui(|ui| {
|
|
test_context.run_recording(&ui.ctx().clone(), |ctx| {
|
|
DataFusionTableWidget::new(Arc::clone(&session_context), table_ref)
|
|
.title("Non-uniform cards")
|
|
.show(
|
|
ctx,
|
|
&runtime_handle,
|
|
ui,
|
|
&mut test_context.view_states.lock(),
|
|
);
|
|
});
|
|
});
|
|
|
|
run_async_harness(&mut harness).await;
|
|
|
|
// Switch to grid mode.
|
|
harness.get_by_label("Grid view").click();
|
|
run_async_harness(&mut harness).await;
|
|
|
|
harness.snapshot("grid_view_non_uniform_cards");
|
|
}
|
|
|
|
// ---
|
|
|
|
/// Sets up a test table.
|
|
///
|
|
/// When `with_flagging` is true, the schema is configured for flagging:
|
|
/// - `id` column gets `rerun:is_table_index` metadata (required for upsert)
|
|
/// - Schema gets `rerun:flag_column` metadata pointing at the `flagged` column
|
|
fn setup_test_table(with_flagging: bool) -> (Arc<SessionContext>, &'static str) {
|
|
let mut id_field = Field::new("id", DataType::Int64, false);
|
|
let mut flagged_field = Field::new("flagged", DataType::Boolean, true);
|
|
|
|
if with_flagging {
|
|
id_field = id_field.with_metadata(
|
|
[(
|
|
re_sorbet::metadata::SORBET_IS_TABLE_INDEX.to_owned(),
|
|
"true".to_owned(),
|
|
)]
|
|
.into(),
|
|
);
|
|
flagged_field = flagged_field.with_metadata(
|
|
[(
|
|
re_dataframe_ui::experimental_field_metadata::IS_FLAG_COLUMN.to_owned(),
|
|
"true".to_owned(),
|
|
)]
|
|
.into(),
|
|
);
|
|
}
|
|
|
|
let schema = Arc::new(Schema::new_with_metadata(
|
|
vec![
|
|
id_field,
|
|
Field::new("score", DataType::Float64, false),
|
|
Field::new("category", DataType::Utf8, false),
|
|
Field::new("name", DataType::Utf8, false),
|
|
flagged_field,
|
|
Field::new("notes", DataType::Utf8, true),
|
|
],
|
|
Default::default(),
|
|
));
|
|
let batch = RecordBatch::try_new_with_options(
|
|
schema.clone(),
|
|
vec![
|
|
Arc::new(arrow::array::Int64Array::from(vec![1, 2, 3, 4, 5])),
|
|
Arc::new(Float64Array::from(vec![95.0, 82.5, 91.0, 88.0, 76.5])),
|
|
Arc::new(StringArray::from(vec![
|
|
"robotics", "vision", "robotics", "spatial", "vision",
|
|
])),
|
|
Arc::new(StringArray::from(vec![
|
|
"Alice", "Bob", "Charlie", "Diana", "Eve",
|
|
])),
|
|
Arc::new(BooleanArray::from(vec![
|
|
Some(true),
|
|
Some(false),
|
|
Some(false),
|
|
Some(true),
|
|
Some(false),
|
|
])),
|
|
Arc::new(StringArray::from(vec![
|
|
Some("top performer"),
|
|
None,
|
|
Some("needs review"),
|
|
Some("promoted"),
|
|
None,
|
|
])),
|
|
],
|
|
&Default::default(),
|
|
)
|
|
.expect("Failed to create a record batch");
|
|
|
|
let session_context = Arc::new(SessionContext::new());
|
|
let table_ref = "test_table";
|
|
session_context
|
|
.register_batch(table_ref, batch)
|
|
.expect("Failed to register the table");
|
|
|
|
(session_context, table_ref)
|
|
}
|
|
|
|
/// Sets up a table with 30 rows of wildly varying content lengths.
|
|
///
|
|
/// Rows differ in: name length, number of nullable fields that are present,
|
|
/// description length (from absent to multi-sentence paragraphs), and tag count.
|
|
/// This produces cards with very different heights to stress the virtualized
|
|
/// layout's height caching and row assignment.
|
|
fn setup_non_uniform_table() -> (Arc<SessionContext>, &'static str) {
|
|
let ids: Vec<i64> = (1..=20).collect();
|
|
let n = ids.len();
|
|
let scores: Vec<f64> = (0..n).map(|i| 50.0 + (i as f64 * 1.7) % 50.0).collect();
|
|
|
|
let categories: Vec<&str> = (0..n)
|
|
.map(|i| match i % 5 {
|
|
0 => "robotics",
|
|
1 => "computer-vision",
|
|
2 => "spatial-computing",
|
|
3 => "motion-planning",
|
|
_ => "multi-modal-perception",
|
|
})
|
|
.collect();
|
|
|
|
let names: Vec<&str> = [
|
|
"Al",
|
|
"Bob",
|
|
"Charlie Chaplin",
|
|
"Di",
|
|
"Eve",
|
|
"Ferdinand von Zeppelin III",
|
|
"G",
|
|
"Hank",
|
|
"Iris Apfel-Strudel",
|
|
"Jo",
|
|
"Kai",
|
|
"Luna Moonbeam Stargazer the Magnificent",
|
|
"Mo",
|
|
"Nia",
|
|
"Olaf",
|
|
"Pippi Longstocking",
|
|
"Q",
|
|
"Raj",
|
|
"Sue",
|
|
"Tiberius Maximus Aurelius",
|
|
]
|
|
.into();
|
|
|
|
// Descriptions vary from None to very long paragraphs.
|
|
let descriptions: Vec<Option<&str>> = (0..n)
|
|
.map(|i| match i % 8 {
|
|
0 => Some(
|
|
"Top performer in the quarterly assessment with outstanding marks across \
|
|
all evaluation criteria and team collaboration metrics. Recommended for \
|
|
leadership track. Has consistently demonstrated excellence in cross-functional \
|
|
projects spanning multiple divisions.",
|
|
),
|
|
1 | 3 | 5 => None,
|
|
2 => Some("OK"),
|
|
4 => Some(
|
|
"Needs review: flagged by automated pipeline for anomalous sensor readings \
|
|
during the third calibration pass. Investigate before clearing. The anomaly \
|
|
pattern matches a known firmware regression in batch 7B units.",
|
|
),
|
|
6 => Some(
|
|
"Extended field trial participant. Deployed for 847 hours across arctic, \
|
|
desert, and underwater environments. All subsystems nominal except minor \
|
|
thermal drift in IMU cluster B which self-corrected after 72h acclimatization. \
|
|
Full telemetry archive available in dataset DS-2024-0891. Recommend continued \
|
|
deployment with monthly check-ins.",
|
|
),
|
|
_ => Some("No issues found."),
|
|
})
|
|
.collect();
|
|
|
|
// Tags: some rows have none, some have short tags, some long comma-separated lists.
|
|
let tags: Vec<Option<&str>> = (0..n)
|
|
.map(|i| match i % 6 {
|
|
0 => Some("priority, review-needed, Q4-2024"),
|
|
1 | 4 => None,
|
|
2 => Some("stable"),
|
|
3 => Some("arctic, underwater, extreme-conditions, long-duration, telemetry, thermal-drift, imu"),
|
|
_ => Some("regression, firmware, batch-7B, calibration, sensor-anomaly, high-priority"),
|
|
})
|
|
.collect();
|
|
|
|
// Location: mix of present/absent with varying lengths.
|
|
let locations: Vec<Option<&str>> = (0..n)
|
|
.map(|i| match i % 5 {
|
|
0 => Some("Building 4, Lab 2A"),
|
|
1 => Some("Remote — Svalbard Arctic Station, Sector 7G, Cold Storage Unit #12"),
|
|
2 | 4 => None,
|
|
_ => Some("HQ"),
|
|
})
|
|
.collect();
|
|
|
|
// Status: short field, always present but varying.
|
|
let statuses: Vec<&str> = (0..n)
|
|
.map(|i| match i % 4 {
|
|
0 => "active",
|
|
1 => "inactive",
|
|
2 => "pending-review",
|
|
_ => "deployed",
|
|
})
|
|
.collect();
|
|
|
|
let schema = Arc::new(Schema::new_with_metadata(
|
|
vec![
|
|
Field::new("id", DataType::Int64, false),
|
|
Field::new("name", DataType::Utf8, false),
|
|
Field::new("score", DataType::Float64, false),
|
|
Field::new("category", DataType::Utf8, false),
|
|
Field::new("status", DataType::Utf8, false),
|
|
Field::new("description", DataType::Utf8, true),
|
|
Field::new("tags", DataType::Utf8, true),
|
|
Field::new("location", DataType::Utf8, true),
|
|
],
|
|
Default::default(),
|
|
));
|
|
|
|
let batch = RecordBatch::try_new_with_options(
|
|
schema,
|
|
vec![
|
|
Arc::new(arrow::array::Int64Array::from(ids)),
|
|
Arc::new(StringArray::from(names)),
|
|
Arc::new(Float64Array::from(scores)),
|
|
Arc::new(StringArray::from(categories)),
|
|
Arc::new(StringArray::from(statuses)),
|
|
Arc::new(StringArray::from(descriptions)),
|
|
Arc::new(StringArray::from(tags)),
|
|
Arc::new(StringArray::from(locations)),
|
|
],
|
|
&Default::default(),
|
|
)
|
|
.expect("Failed to create a record batch");
|
|
|
|
let session_context = Arc::new(SessionContext::new());
|
|
let table_ref = "non_uniform_table";
|
|
session_context
|
|
.register_batch(table_ref, batch)
|
|
.expect("Failed to register the table");
|
|
|
|
(session_context, table_ref)
|
|
}
|