Files
Marcos Paulo 5e8020aa38
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
feat: Obsidian basics, editor previews, graph rework, query blocks, stacked tabs, pop-out windows
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>
2026-10-19 11:21:19 -03:00

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}");
}
}