feat: first take at M2
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
//! App launch smoke tests.
|
||||
//!
|
||||
//! M0 validation: verifies that the core UI types can be constructed
|
||||
//! Validates that the core UI types can be constructed
|
||||
//! and that the message routing works at the type level.
|
||||
//!
|
||||
//! NOTE: muda::Menu on macOS requires the actual main thread (not just
|
||||
@@ -8,7 +8,11 @@
|
||||
//! be tested via `cargo test`. The full smoke test is launching the binary:
|
||||
//! cargo run -p bds-ui
|
||||
|
||||
use std::path::PathBuf;
|
||||
use bds_core::i18n::UiLocale;
|
||||
use bds_ui::app::Message;
|
||||
use bds_ui::state::navigation::{PanelTab, SidebarView};
|
||||
use bds_ui::state::tabs::{Tab, TabType};
|
||||
|
||||
// ── Smoke: Message enum is well-formed ──
|
||||
|
||||
@@ -16,7 +20,6 @@ use bds_ui::app::Message;
|
||||
fn message_variants_constructable() {
|
||||
let _noop = Message::Noop;
|
||||
let _menu = Message::MenuEvent(muda::MenuId::new("test"));
|
||||
// Verify Debug trait works
|
||||
assert!(format!("{:?}", Message::Noop).contains("Noop"));
|
||||
}
|
||||
|
||||
@@ -27,12 +30,62 @@ fn message_clone_works() {
|
||||
assert!(format!("{cloned:?}").contains("MenuEvent"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_message_variants_constructable() {
|
||||
// Navigation
|
||||
let _view = Message::SetActiveView(SidebarView::Posts);
|
||||
let _toggle_sb = Message::ToggleSidebar;
|
||||
let _toggle_p = Message::TogglePanel;
|
||||
|
||||
// Tabs
|
||||
let tab = Tab {
|
||||
id: "test".to_string(),
|
||||
tab_type: TabType::Post,
|
||||
title: "Test".to_string(),
|
||||
is_transient: false,
|
||||
};
|
||||
let _open = Message::OpenTab(tab);
|
||||
let _close = Message::CloseTab("test".into());
|
||||
let _select = Message::SelectTab("test".into());
|
||||
let _pin = Message::PinTab("test".into());
|
||||
|
||||
// Project
|
||||
let _switch = Message::SwitchProject("id".into());
|
||||
let _create = Message::CreateProject { name: "X".into(), data_path: None };
|
||||
let _delete = Message::DeleteProject("id".into());
|
||||
|
||||
// Dialogs
|
||||
let _folder = Message::FolderPicked(Some(PathBuf::from("/tmp")));
|
||||
let _media = Message::MediaFilesPicked(None);
|
||||
|
||||
// Tasks
|
||||
let _tick = Message::TaskTick;
|
||||
|
||||
// macOS lifecycle
|
||||
let _file = Message::FileOpenRequested(PathBuf::from("/test"));
|
||||
let _url = Message::UrlOpenRequested("bds://open".into());
|
||||
|
||||
// Panel
|
||||
let _panel = Message::SetPanelTab(PanelTab::Output);
|
||||
|
||||
// Settings
|
||||
let _offline = Message::SetOfflineMode(true);
|
||||
let _locale = Message::SetUiLocale(UiLocale::De);
|
||||
|
||||
// Blog actions
|
||||
let _rebuild = Message::RebuildDatabase;
|
||||
let _reindex = Message::ReindexText;
|
||||
let _diff = Message::RunMetadataDiff;
|
||||
let _finished = Message::BlogTaskFinished {
|
||||
label: "test".into(),
|
||||
result: Ok(()),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Smoke: BdsApp type is accessible from integration tests ──
|
||||
|
||||
#[test]
|
||||
fn bds_app_type_is_public() {
|
||||
// This test verifies the public API surface exists.
|
||||
// BdsApp, Message, platform::menu are all reachable.
|
||||
fn _assert_types() {
|
||||
let _: fn() -> (bds_ui::BdsApp, iced::Task<Message>) = bds_ui::BdsApp::new;
|
||||
}
|
||||
|
||||
44
crates/bds-ui/tests/menu_routing.rs
Normal file
44
crates/bds-ui/tests/menu_routing.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
//! Menu routing integration tests.
|
||||
//!
|
||||
//! Validates: all MenuAction variants registered, no collisions,
|
||||
//! bidirectional lookup, and i18n keys resolve in every locale.
|
||||
|
||||
use bds_core::i18n::{translate, UiLocale};
|
||||
use bds_ui::platform::menu::MenuAction;
|
||||
|
||||
#[test]
|
||||
fn all_menu_actions_have_i18n_keys() {
|
||||
for &action in MenuAction::ALL {
|
||||
let key = action.i18n_key();
|
||||
assert!(key.starts_with("menu.item."), "bad key prefix: {key}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_menu_actions_translate_in_all_locales() {
|
||||
for locale in UiLocale::all() {
|
||||
for &action in MenuAction::ALL {
|
||||
let key = action.i18n_key();
|
||||
let label = translate(*locale, key);
|
||||
assert_ne!(
|
||||
label, key,
|
||||
"missing translation for {key} in locale {locale}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn menu_action_count_matches_spec() {
|
||||
// M2 spec: 28 custom menu actions
|
||||
assert_eq!(MenuAction::ALL.len(), 28);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_duplicate_i18n_keys() {
|
||||
let mut keys = std::collections::HashSet::new();
|
||||
for &action in MenuAction::ALL {
|
||||
let key = action.i18n_key();
|
||||
assert!(keys.insert(key), "duplicate i18n key: {key}");
|
||||
}
|
||||
}
|
||||
124
crates/bds-ui/tests/project_flow.rs
Normal file
124
crates/bds-ui/tests/project_flow.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
//! Project flow integration tests.
|
||||
//!
|
||||
//! Tests the project lifecycle: create, list, set active, switch, delete.
|
||||
//! Uses in-memory DB — no Iced window required.
|
||||
|
||||
use bds_core::db::Database;
|
||||
use bds_core::engine::project;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn setup() -> (Database, TempDir) {
|
||||
let db = Database::open_in_memory().unwrap();
|
||||
db.migrate().unwrap();
|
||||
let dir = TempDir::new().unwrap();
|
||||
(db, dir)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_and_activate_default_project() {
|
||||
let (db, dir) = setup();
|
||||
let data_path = dir.path().join("my-blog");
|
||||
|
||||
let p = project::create_project(
|
||||
db.conn(),
|
||||
"My Blog",
|
||||
Some(data_path.to_str().unwrap()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(p.name, "My Blog");
|
||||
assert_eq!(p.slug, "my-blog");
|
||||
assert!(!p.is_active);
|
||||
|
||||
// Activate it
|
||||
project::set_active_project(db.conn(), &p.id).unwrap();
|
||||
let active = project::get_active_project(db.conn()).unwrap().unwrap();
|
||||
assert_eq!(active.id, p.id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switch_between_projects() {
|
||||
let (db, dir) = setup();
|
||||
let p1_path = dir.path().join("blog-a");
|
||||
let p2_path = dir.path().join("blog-b");
|
||||
|
||||
let p1 = project::create_project(db.conn(), "Blog A", Some(p1_path.to_str().unwrap())).unwrap();
|
||||
let p2 = project::create_project(db.conn(), "Blog B", Some(p2_path.to_str().unwrap())).unwrap();
|
||||
|
||||
project::set_active_project(db.conn(), &p1.id).unwrap();
|
||||
let active = project::get_active_project(db.conn()).unwrap().unwrap();
|
||||
assert_eq!(active.id, p1.id);
|
||||
|
||||
// Switch
|
||||
project::set_active_project(db.conn(), &p2.id).unwrap();
|
||||
let active = project::get_active_project(db.conn()).unwrap().unwrap();
|
||||
assert_eq!(active.id, p2.id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_inactive_project() {
|
||||
let (db, dir) = setup();
|
||||
let p1_path = dir.path().join("keep");
|
||||
let p2_path = dir.path().join("remove");
|
||||
|
||||
let p1 = project::create_project(db.conn(), "Keep", Some(p1_path.to_str().unwrap())).unwrap();
|
||||
let p2 = project::create_project(db.conn(), "Remove", Some(p2_path.to_str().unwrap())).unwrap();
|
||||
|
||||
project::set_active_project(db.conn(), &p1.id).unwrap();
|
||||
project::delete_project(db.conn(), &p2.id, Some(&p2_path)).unwrap();
|
||||
|
||||
let list = project::list_projects(db.conn()).unwrap();
|
||||
assert_eq!(list.len(), 1);
|
||||
assert_eq!(list[0].name, "Keep");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_delete_active_project() {
|
||||
let (db, dir) = setup();
|
||||
let p_path = dir.path().join("active");
|
||||
let p = project::create_project(db.conn(), "Active", Some(p_path.to_str().unwrap())).unwrap();
|
||||
|
||||
project::set_active_project(db.conn(), &p.id).unwrap();
|
||||
let result = project::delete_project(db.conn(), &p.id, None);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_directory_structure_created() {
|
||||
let (db, dir) = setup();
|
||||
let p_path = dir.path().join("structured");
|
||||
project::create_project(db.conn(), "Structured", Some(p_path.to_str().unwrap())).unwrap();
|
||||
|
||||
assert!(p_path.join("posts").is_dir());
|
||||
assert!(p_path.join("media").is_dir());
|
||||
assert!(p_path.join("meta").is_dir());
|
||||
assert!(p_path.join("thumbnails").is_dir());
|
||||
assert!(p_path.join("templates").is_dir());
|
||||
assert!(p_path.join("scripts").is_dir());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_meta_files_written() {
|
||||
let (db, dir) = setup();
|
||||
let p_path = dir.path().join("meta-test");
|
||||
project::create_project(db.conn(), "Meta Test", Some(p_path.to_str().unwrap())).unwrap();
|
||||
|
||||
let project_json: serde_json::Value = serde_json::from_str(
|
||||
&std::fs::read_to_string(p_path.join("meta/project.json")).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(project_json["name"], "Meta Test");
|
||||
assert_eq!(project_json["maxPostsPerPage"], 50);
|
||||
|
||||
let categories: Vec<String> = serde_json::from_str(
|
||||
&std::fs::read_to_string(p_path.join("meta/categories.json")).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(categories, vec!["article", "aside", "page", "picture"]);
|
||||
|
||||
let tags: Vec<String> = serde_json::from_str(
|
||||
&std::fs::read_to_string(p_path.join("meta/tags.json")).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(tags.is_empty());
|
||||
}
|
||||
Reference in New Issue
Block a user