5e8020aa38
Editor previews (colour swatches, arrows, embeds), the 18 obsidian-basics tasks (find and replace with regex, back/forward, auto-pair, footnotes, folding, search operators, split view, PDF/audio/ video embeds, spelling, drag and drop, pinned tabs, settings) and round 2: ```query blocks, stacked tabs, pop-out windows, tasks ticked inside embeds, text around an embed, embeds that follow edits, block menu shortcut. New crates: notescraft-regex (linear-time engine) and notescraft-syntax. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
160 lines
6.3 KiB
Rust
160 lines
6.3 KiB
Rust
//! Integration tests for the app shell: layout contract across resolutions, empty state,
|
|
//! tab/breadcrumb/backlinks wiring and palette runs (all headless, no GPU).
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::path::PathBuf;
|
|
|
|
use egui::{Context, RawInput};
|
|
|
|
use notescraft_engine::Session;
|
|
use notescraft_themes::{CalloutKind, Colors, GraphColors, Radii, Rgba, SyntaxColors, Theme, Variant};
|
|
use notescraft_ui_egui::app::NotesApp;
|
|
use notescraft_ui_egui::layout::{self, CardVisibility, SIDEBAR_WIDTH};
|
|
use notescraft_ui_egui::theme::{PANEL_GAP, Tokens};
|
|
|
|
/// A compact theme: tokens are arbitrary but distinct.
|
|
fn theme() -> Theme {
|
|
let c = Rgba::new(64, 64, 64, 255);
|
|
let mut callouts = BTreeMap::new();
|
|
for kind in CalloutKind::ALL {
|
|
callouts.insert(kind, Rgba::new(120, 80, 200, 255));
|
|
}
|
|
Theme {
|
|
id: String::from("test"),
|
|
name: String::from("Test"),
|
|
variant: Variant::Dark,
|
|
colors: Colors {
|
|
background: c,
|
|
surface: Rgba::new(40, 40, 40, 255),
|
|
surface_2: Rgba::new(56, 56, 56, 255),
|
|
canvas: Rgba::new(24, 24, 24, 255),
|
|
border: c,
|
|
text: Rgba::new(230, 230, 230, 255),
|
|
text_muted: Rgba::new(150, 150, 150, 255),
|
|
accent: Rgba::new(180, 120, 250, 255),
|
|
on_accent: Rgba::new(20, 20, 20, 255),
|
|
link: Rgba::new(120, 160, 250, 255),
|
|
link_unresolved: Rgba::new(250, 120, 130, 255),
|
|
selection: Rgba::new(80, 80, 90, 255),
|
|
code_background: Rgba::new(20, 20, 26, 255),
|
|
highlight: Rgba::new(240, 220, 160, 100),
|
|
success: Rgba::new(120, 200, 120, 255),
|
|
warning: Rgba::new(220, 180, 100, 255),
|
|
danger: Rgba::new(230, 110, 110, 255),
|
|
},
|
|
callouts,
|
|
graph: GraphColors { node: c, node_active: c, edge: c, unresolved: c },
|
|
syntax: SyntaxColors::uniform(c),
|
|
radii: Radii { small: 4.0, medium: 6.0 },
|
|
fonts: notescraft_themes::Fonts { ui: String::new(), editor: String::new(), mono: String::new() },
|
|
}
|
|
}
|
|
|
|
fn tokens() -> Tokens {
|
|
Tokens::new(theme())
|
|
}
|
|
|
|
fn input(width: f32, height: f32) -> RawInput {
|
|
RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(width, height))), ..RawInput::default() }
|
|
}
|
|
|
|
/// The three cards keep the panel gap to the window edge and between each other at every
|
|
/// resolution the UI must survive.
|
|
#[test]
|
|
fn shell_cards_keep_the_gap_at_every_resolution() {
|
|
let t = tokens();
|
|
for (w, h) in [(3840.0, 2160.0), (1440.0, 900.0), (800.0, 600.0)] {
|
|
let ctx = Context::default();
|
|
let mut out = ctx.run_ui(input(w, h), |ui| {
|
|
let rects = layout::shell(ui.ctx(), &t, CardVisibility { sidebar: true, right: true }, |_| {});
|
|
if let Some(sidebar) = rects.sidebar {
|
|
assert_eq!(sidebar.left(), PANEL_GAP, "gap at {w}x{h}");
|
|
assert_eq!(sidebar.width(), SIDEBAR_WIDTH);
|
|
}
|
|
let editor = rects.editor.expect("editor card");
|
|
assert_eq!(editor.height(), h - 2.0 * PANEL_GAP);
|
|
});
|
|
assert!(!out.shapes.is_empty(), "cards painted at {w}x{h}");
|
|
out.textures_delta.clear();
|
|
}
|
|
}
|
|
|
|
/// A temp vault mirroring the defect scenario, then the full shell behaviour.
|
|
fn shell_app(name: &str) -> Option<NotesApp> {
|
|
let vault = temp_vault(name);
|
|
for (path, content) in [
|
|
("Daily/2026-10-13.md", "# Daily\n\nToday.\n"),
|
|
("Projects/NotesCraft spec.md", "# Spec\n\nSee [[Roadmap]].\n"),
|
|
("Projects/Roadmap.md", "# Roadmap\n\nThe spec drives this.\n"),
|
|
("notes/alpha.md", "hello alpha\n"),
|
|
] {
|
|
let target = vault.join(path);
|
|
let _ = std::fs::create_dir_all(target.parent()?);
|
|
std::fs::write(&target, content).ok()?;
|
|
}
|
|
let mut session = Session::open(&vault).ok()?;
|
|
session.use_settings_dir(vault.join(".notescraft/test-config"));
|
|
Some(NotesApp::new(session, theme()))
|
|
}
|
|
|
|
fn temp_vault(name: &str) -> PathBuf {
|
|
let dir = std::env::temp_dir().join(format!("notescraft-shell-{name}-{}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
let _ = std::fs::create_dir_all(&dir);
|
|
dir
|
|
}
|
|
|
|
/// Empty state with no tab; tab, breadcrumb and note view once a note opened.
|
|
#[test]
|
|
fn app_shell_wiring_across_resolutions() {
|
|
let Some(mut app) = shell_app("wiring") else { panic!("vault setup failed") };
|
|
let ctx = Context::default();
|
|
let mut seen_empty = Vec::new();
|
|
for _ in 0..3 {
|
|
let mut out = ctx.run_ui(input(1440.0, 900.0), |ui| app.ui(ui.ctx()));
|
|
seen_empty.clear();
|
|
for shape in &out.shapes {
|
|
if let egui::epaint::Shape::Text(label) = &shape.shape {
|
|
seen_empty.push(label.galley.text().to_owned());
|
|
}
|
|
}
|
|
out.textures_delta.clear();
|
|
}
|
|
assert!(seen_empty.iter().any(|t| t == "Open a note"), "empty state: {seen_empty:?}");
|
|
|
|
app.open_note("Projects/NotesCraft spec.md");
|
|
let mut opened = Vec::new();
|
|
for _ in 0..3 {
|
|
let mut out = ctx.run_ui(input(3840.0, 2160.0), |ui| app.ui(ui.ctx()));
|
|
opened.clear();
|
|
for shape in &out.shapes {
|
|
if let egui::epaint::Shape::Text(label) = &shape.shape {
|
|
opened.push(label.galley.text().to_owned());
|
|
}
|
|
}
|
|
out.textures_delta.clear();
|
|
}
|
|
assert!(opened.iter().any(|t| t == "NotesCraft spec"), "tab/breadcrumb title: {opened:?}");
|
|
assert!(opened.iter().any(|t| t.contains("LINKED MENTIONS")), "right card wired: {opened:?}");
|
|
}
|
|
|
|
/// Opening a note in a narrow window still paints (800x600: only sidebar and centre).
|
|
#[test]
|
|
fn app_shell_survives_narrow_windows() {
|
|
let Some(mut app) = shell_app("narrow") else { panic!("vault setup failed") };
|
|
app.open_note("notes/alpha.md");
|
|
let ctx = Context::default();
|
|
// Warm-up: the fonts install on the first frame.
|
|
let mut warm = ctx.run_ui(input(800.0, 600.0), |ui| app.ui(ui.ctx()));
|
|
warm.textures_delta.clear();
|
|
for (w, h) in [(800.0, 600.0), (500.0, 400.0)] {
|
|
let shapes = {
|
|
let mut out = ctx.run_ui(input(w, h), |ui| app.ui(ui.ctx()));
|
|
let count = out.shapes.len();
|
|
out.textures_delta.clear();
|
|
count
|
|
};
|
|
assert!(shapes > 0, "painted at {w}x{h}");
|
|
}
|
|
}
|