Étend le port/usecases window et layout.customPluginLayout pour ouvrir et piloter des fenêtres OS hébergeant un layout plugin, en réutilisant contributes.layouts plutôt qu'un système de fenêtres parallèle. Câble la commande Tauri et l'état app-tauri correspondants. Cargo test -p domain --test window : 5/5 Cargo test -p application --test window_usecases : 7/7 Cargo test -p infrastructure --test window_state_store : 1/1 Cargo test -p app-tauri view_window_tests : 8/8 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
139 lines
4.8 KiB
Rust
139 lines
4.8 KiB
Rust
//! L10 tests for the pure `Workspace::move_tab_to_new_window` operation
|
|
//! (ARCHITECTURE §10): a tab is *moved*, never duplicated; an emptied source
|
|
//! window is dropped; an active moved tab hands activity back to a sibling.
|
|
|
|
use domain::{
|
|
LayoutError, LayoutNode, LayoutTree, LeafCell, NodeId, PersistedMonitorState,
|
|
PersistedWindowKind, PersistedWindowPosition, PersistedWindowSize, PersistedWindowState,
|
|
ProjectId, Tab, TabId, Window, WindowId, WindowStateSnapshot, Workspace,
|
|
};
|
|
use uuid::Uuid;
|
|
|
|
fn tid(n: u128) -> TabId {
|
|
TabId::from_uuid(Uuid::from_u128(n))
|
|
}
|
|
fn wid(n: u128) -> WindowId {
|
|
WindowId::from_uuid(Uuid::from_u128(n))
|
|
}
|
|
fn leaf_tree() -> LayoutTree {
|
|
LayoutTree::new(LayoutNode::Leaf(LeafCell {
|
|
id: NodeId::from_uuid(Uuid::from_u128(900)),
|
|
session: None,
|
|
agent: None,
|
|
conversation_id: None,
|
|
engine_session_id: None,
|
|
agent_was_running: false,
|
|
}))
|
|
}
|
|
fn tab(n: u128) -> Tab {
|
|
Tab {
|
|
id: tid(n),
|
|
project_id: ProjectId::from_uuid(Uuid::from_u128(1000 + n)),
|
|
layout: leaf_tree(),
|
|
}
|
|
}
|
|
|
|
/// Count how many windows contain a tab with the given id.
|
|
fn occurrences(ws: &Workspace, tab: TabId) -> usize {
|
|
ws.windows
|
|
.iter()
|
|
.filter(|w| w.tabs.iter().any(|t| t.id == tab))
|
|
.count()
|
|
}
|
|
|
|
#[test]
|
|
fn move_tab_from_multi_tab_window_keeps_source_and_creates_new() {
|
|
let src = Window::new(wid(1), vec![tab(1), tab(2)], tid(1)).unwrap();
|
|
let ws = Workspace { windows: vec![src] };
|
|
|
|
let next = ws.move_tab_to_new_window(tid(1), wid(99)).unwrap();
|
|
|
|
assert_eq!(next.windows.len(), 2, "source kept + new window");
|
|
// The moved tab appears exactly once (moved, not duplicated).
|
|
assert_eq!(occurrences(&next, tid(1)), 1);
|
|
// Source window kept tab 2 and fell back its active tab to it.
|
|
let source = next.windows.iter().find(|w| w.id == wid(1)).unwrap();
|
|
assert_eq!(source.tabs.len(), 1);
|
|
assert_eq!(source.active_tab, tid(2));
|
|
// New window holds the moved tab, active.
|
|
let detached = next.windows.iter().find(|w| w.id == wid(99)).unwrap();
|
|
assert_eq!(detached.tabs.len(), 1);
|
|
assert_eq!(detached.active_tab, tid(1));
|
|
}
|
|
|
|
#[test]
|
|
fn move_only_tab_removes_the_emptied_source_window() {
|
|
let src = Window::new(wid(1), vec![tab(1)], tid(1)).unwrap();
|
|
let ws = Workspace { windows: vec![src] };
|
|
|
|
let next = ws.move_tab_to_new_window(tid(1), wid(99)).unwrap();
|
|
|
|
assert_eq!(next.windows.len(), 1, "emptied source dropped");
|
|
assert_eq!(next.windows[0].id, wid(99));
|
|
assert_eq!(occurrences(&next, tid(1)), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn move_unknown_tab_is_rejected() {
|
|
let ws = Workspace {
|
|
windows: vec![Window::new(wid(1), vec![tab(1)], tid(1)).unwrap()],
|
|
};
|
|
assert!(matches!(
|
|
ws.move_tab_to_new_window(tid(404), wid(99)).unwrap_err(),
|
|
LayoutError::TabNotFound(t) if t == tid(404)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn move_non_active_tab_leaves_source_active_unchanged() {
|
|
let src = Window::new(wid(1), vec![tab(1), tab(2)], tid(1)).unwrap();
|
|
let ws = Workspace { windows: vec![src] };
|
|
|
|
let next = ws.move_tab_to_new_window(tid(2), wid(99)).unwrap();
|
|
|
|
let source = next.windows.iter().find(|w| w.id == wid(1)).unwrap();
|
|
assert_eq!(source.active_tab, tid(1), "active tab unchanged");
|
|
assert_eq!(source.tabs.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn window_state_snapshot_round_trips_with_camel_case_schema() {
|
|
let snapshot = WindowStateSnapshot::new(vec![PersistedWindowState {
|
|
label: "view-tickets-0000000000000000000000000000002a".to_owned(),
|
|
kind: PersistedWindowKind::View,
|
|
panel: Some("tickets".to_owned()),
|
|
project_id: Some(ProjectId::from_uuid(Uuid::from_u128(42))),
|
|
plugin_layout: None,
|
|
url: Some(
|
|
"index.html?panel=tickets&project=00000000-0000-0000-0000-00000000002a".to_owned(),
|
|
),
|
|
visible: true,
|
|
maximized: false,
|
|
fullscreen: false,
|
|
outer_position: Some(PersistedWindowPosition { x: 12, y: 34 }),
|
|
outer_size: Some(PersistedWindowSize {
|
|
width: 1200,
|
|
height: 800,
|
|
}),
|
|
monitor: Some(PersistedMonitorState {
|
|
name: Some("HDMI-1".to_owned()),
|
|
scale_factor: Some(1.25),
|
|
position: Some(PersistedWindowPosition { x: 0, y: 0 }),
|
|
size: Some(PersistedWindowSize {
|
|
width: 1920,
|
|
height: 1080,
|
|
}),
|
|
}),
|
|
last_focused_at: Some(123),
|
|
}]);
|
|
|
|
let json = serde_json::to_string(&snapshot).unwrap();
|
|
assert!(json.contains("\"projectId\""), "json was {json}");
|
|
assert!(json.contains("\"outerPosition\""), "json was {json}");
|
|
assert!(json.contains("\"scaleFactor\""), "json was {json}");
|
|
assert!(!json.contains("project_id"), "no snake_case leak: {json}");
|
|
|
|
let decoded: WindowStateSnapshot = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(decoded, snapshot);
|
|
}
|