feat: finalizations for M0-M2

This commit is contained in:
2026-04-05 07:41:33 +02:00
parent e46294a022
commit ee61ad56ea
48 changed files with 1296 additions and 498 deletions

View File

@@ -13,6 +13,7 @@ use bds_core::i18n::UiLocale;
use bds_ui::app::Message;
use bds_ui::state::navigation::{PanelTab, SidebarView};
use bds_ui::state::tabs::{Tab, TabType};
use bds_ui::state::toast::ToastLevel;
// ── Smoke: Message enum is well-formed ──
@@ -87,6 +88,11 @@ fn new_message_variants_constructable() {
label: "test".into(),
result: Ok("ok".into()),
};
// Toast
let _show = Message::ShowToast(ToastLevel::Info, "hello".into());
let _dismiss = Message::DismissToast(1);
let _expire = Message::ExpireToasts;
}
// ── Smoke: BdsApp type is accessible from integration tests ──

View File

@@ -0,0 +1,163 @@
//! M2: Native Workspace validation tests.
//!
//! Validates toast system, menu sync logic, dialog i18n,
//! and keyboard shortcut coverage.
use bds_core::i18n::{translate, UiLocale};
use bds_ui::state::toast::{Toast, ToastLevel};
use bds_ui::platform::menu::MenuAction;
// ── Toast system ──
#[test]
fn toast_ids_are_monotonically_increasing() {
let a = Toast::new(ToastLevel::Info, "first".into());
let b = Toast::new(ToastLevel::Warning, "second".into());
let c = Toast::new(ToastLevel::Error, "third".into());
assert!(b.id > a.id);
assert!(c.id > b.id);
}
#[test]
fn toast_level_variants() {
let info = Toast::new(ToastLevel::Info, "info".into());
let success = Toast::new(ToastLevel::Success, "ok".into());
let warning = Toast::new(ToastLevel::Warning, "warn".into());
let error = Toast::new(ToastLevel::Error, "err".into());
assert_eq!(info.level, ToastLevel::Info);
assert_eq!(success.level, ToastLevel::Success);
assert_eq!(warning.level, ToastLevel::Warning);
assert_eq!(error.level, ToastLevel::Error);
}
#[test]
fn fresh_toast_is_not_expired() {
let t = Toast::new(ToastLevel::Info, "test".into());
assert!(!t.is_expired());
}
#[test]
fn toast_preserves_message() {
let t = Toast::new(ToastLevel::Error, "something failed".into());
assert_eq!(t.message, "something failed");
}
// ── Menu enable/disable rules ──
// (These test the expected invariants, not the BdsApp method directly,
// since BdsApp::new() requires main thread for muda.)
#[test]
fn menu_actions_that_need_project() {
let project_actions = [
MenuAction::NewPost,
MenuAction::ImportMedia,
MenuAction::OpenDataFolder,
MenuAction::EditMenu,
MenuAction::RebuildDatabase,
MenuAction::ReindexText,
MenuAction::MetadataDiff,
MenuAction::RegenerateCalendar,
MenuAction::ValidateTranslations,
MenuAction::GenerateSitemap,
MenuAction::ValidateSite,
];
// All should have i18n keys
for action in &project_actions {
let key = action.i18n_key();
let label = translate(UiLocale::En, key);
assert_ne!(label, key, "missing translation for {key}");
}
assert_eq!(project_actions.len(), 11);
}
#[test]
fn menu_actions_that_need_tab() {
let tab_actions = [
MenuAction::Save,
MenuAction::OpenInBrowser,
MenuAction::Find,
MenuAction::Replace,
];
for action in &tab_actions {
let key = action.i18n_key();
let label = translate(UiLocale::En, key);
assert_ne!(label, key, "missing translation for {key}");
}
assert_eq!(tab_actions.len(), 4);
}
#[test]
fn menu_actions_gated_by_offline() {
let online_only = [
MenuAction::FillMissingTranslations,
MenuAction::UploadSite,
];
for action in &online_only {
let key = action.i18n_key();
let label = translate(UiLocale::En, key);
assert_ne!(label, key, "missing translation for {key}");
}
assert_eq!(online_only.len(), 2);
}
// ── Dialog i18n ──
#[test]
fn dialog_keys_exist_in_all_locales() {
let keys = ["dialog.selectFolder", "dialog.importMedia", "dialog.imageFilter"];
for locale in UiLocale::all() {
for key in &keys {
let label = translate(*locale, key);
assert_ne!(label, *key, "missing {key} for locale {locale}");
}
}
}
// ── Keyboard shortcut coverage ──
#[test]
fn accelerator_actions_match_spec() {
// M2 spec: these actions must have keyboard shortcuts
let accelerated = [
MenuAction::NewPost, // Cmd+N
MenuAction::ImportMedia, // Cmd+I
MenuAction::Save, // Cmd+S
MenuAction::Find, // Cmd+F
MenuAction::Replace, // Cmd+H
MenuAction::EditPreferences, // Cmd+,
MenuAction::ViewPosts, // Cmd+1
MenuAction::ViewMedia, // Cmd+2
MenuAction::ToggleSidebar, // Cmd+B
MenuAction::TogglePanel, // Cmd+J
MenuAction::PublishSelected, // Cmd+Shift+P
MenuAction::PreviewPost, // Cmd+Shift+V
MenuAction::GenerateSitemap, // Cmd+R
MenuAction::ValidateSite, // Cmd+Shift+L
MenuAction::UploadSite, // Cmd+Shift+U
];
// All must be valid MenuAction variants with i18n keys
for action in &accelerated {
assert!(!action.i18n_key().is_empty());
}
assert_eq!(accelerated.len(), 15, "M2 spec has 15 accelerator-bound actions");
}
// ── Toast i18n keys ──
#[test]
fn toast_keys_exist_in_all_locales() {
let keys = [
"projectSelector.toast.switched",
"projectSelector.toast.switchFailed",
"projectSelector.toast.created",
"projectSelector.toast.createFailed",
"projectSelector.toast.deleteFailed",
];
for locale in UiLocale::all() {
for key in &keys {
let label = translate(*locale, key);
assert_ne!(label, *key, "missing {key} for locale {locale}");
}
}
}

View File

@@ -8,7 +8,7 @@ use bds_core::engine::project;
use tempfile::TempDir;
fn setup() -> (Database, TempDir) {
let db = Database::open_in_memory().unwrap();
let mut db = Database::open_in_memory().unwrap();
db.migrate().unwrap();
let dir = TempDir::new().unwrap();
(db, dir)