244 lines
9.1 KiB
Rust
244 lines
9.1 KiB
Rust
//! Integration tests for the adaptive view sync flow.
|
|
//!
|
|
//! These tests exercise the full serialize→save→load→verify cycle
|
|
//! that backs the Desktop↔Mobile spreadsheet state sync, using only
|
|
//! persistence and workbook functions (no makepad widget runtime).
|
|
|
|
use spreadsheet_engine::data::SpreadsheetData;
|
|
use spreadsheet_engine::persistence::{load_saved_spreadsheet_state, save_spreadsheet_state};
|
|
use spreadsheet_engine::sheet::Sheet;
|
|
use spreadsheet_engine::workbook::{deserialize_workbook, serialize_workbook};
|
|
|
|
/// Helper: build a 3-sheet workbook with distinct cell data per sheet.
|
|
fn make_3_sheet_workbook() -> Vec<Sheet> {
|
|
let mut budget = SpreadsheetData::default();
|
|
budget.set_cell(0, 0, "100");
|
|
budget.set_cell(1, 0, "=A1+50");
|
|
budget.set_cell(0, 1, "Rent");
|
|
|
|
let mut q3 = SpreadsheetData::default();
|
|
q3.set_cell(0, 0, "Revenue");
|
|
q3.set_cell(1, 0, "50000");
|
|
q3.set_cell(2, 0, "Costs");
|
|
q3.set_cell(3, 0, "=B1-B2");
|
|
|
|
let mut roi = SpreadsheetData::default();
|
|
roi.set_cell(0, 0, "Investment");
|
|
roi.set_cell(0, 1, "10000");
|
|
roi.set_cell(1, 0, "Return");
|
|
roi.set_cell(1, 1, "15000");
|
|
|
|
vec![
|
|
Sheet::new("Budget", budget),
|
|
Sheet::new("Q3 Projections", q3),
|
|
Sheet::new("ROI Analysis", roi),
|
|
]
|
|
}
|
|
|
|
// ── Full sync round-trip tests ─────────────────────────────────
|
|
|
|
/// Desktop→Mobile→Desktop: data survives a full round-trip.
|
|
#[test]
|
|
fn full_sync_round_trip_preserves_all_sheets() {
|
|
// 1. Desktop creates a workbook with data on all 3 sheets
|
|
let sheets = make_3_sheet_workbook();
|
|
let saved_desktop = serialize_workbook(&sheets, 1);
|
|
|
|
// 2. Mobile loads the workbook, reads the active sheet (idx=1)
|
|
let (mut mobile_sheets, mobile_active) = deserialize_workbook(&saved_desktop).unwrap();
|
|
assert_eq!(mobile_active, 1);
|
|
assert_eq!(mobile_sheets.len(), 3);
|
|
assert_eq!(mobile_sheets[1].name, "Q3 Projections");
|
|
assert_eq!(mobile_sheets[1].data.get_raw(0, 0), "Revenue");
|
|
|
|
// 3. Mobile edits the active sheet
|
|
mobile_sheets[mobile_active]
|
|
.data
|
|
.set_cell(5, 0, "mobile_edit");
|
|
let saved_mobile = serialize_workbook(&mobile_sheets, mobile_active);
|
|
|
|
// 4. Desktop reloads
|
|
let (restored, restored_active) = deserialize_workbook(&saved_mobile).unwrap();
|
|
assert_eq!(restored.len(), 3);
|
|
assert_eq!(restored_active, 1);
|
|
assert_eq!(restored[0].name, "Budget");
|
|
assert_eq!(restored[1].name, "Q3 Projections");
|
|
assert_eq!(restored[2].name, "ROI Analysis");
|
|
assert_eq!(restored[1].data.get_raw(5, 0), "mobile_edit");
|
|
// Other sheet data intact
|
|
assert_eq!(restored[0].data.get_raw(0, 0), "100");
|
|
assert_eq!(restored[2].data.get_raw(1, 1), "15000");
|
|
}
|
|
|
|
/// Multiple switches back and forth accumulate edits correctly.
|
|
#[test]
|
|
fn multiple_switches_accumulate_edits() {
|
|
let sheets = make_3_sheet_workbook();
|
|
let mut current = serialize_workbook(&sheets, 0);
|
|
|
|
for i in 0..5 {
|
|
// Load
|
|
let (mut loaded, active) = deserialize_workbook(¤t).unwrap();
|
|
// Edit active sheet
|
|
loaded[active].data.set_cell(0, 0, &format!("edit_{}", i));
|
|
// Switch to next sheet
|
|
let next_active = (active + 1) % loaded.len();
|
|
current = serialize_workbook(&loaded, next_active);
|
|
}
|
|
|
|
// Final load: verify all edits landed on correct sheets
|
|
let (final_loaded, final_active) = deserialize_workbook(¤t).unwrap();
|
|
assert_eq!(final_loaded.len(), 3);
|
|
// After 5 switches starting at 0: 0→1→2→0→1→2, active=2
|
|
assert_eq!(final_active, 2);
|
|
// Sheet 0 was last written at i=3 (edit_3)
|
|
assert_eq!(final_loaded[0].data.get_raw(0, 0), "edit_3");
|
|
// Sheet 1 was last written at i=4 (edit_4)
|
|
assert_eq!(final_loaded[1].data.get_raw(0, 0), "edit_4");
|
|
// Sheet 2 was last written at i=2 (edit_2)
|
|
assert_eq!(final_loaded[2].data.get_raw(0, 0), "edit_2");
|
|
}
|
|
|
|
/// Formula computed values update correctly after round-trip.
|
|
#[test]
|
|
fn formula_recalculates_after_sync() {
|
|
let mut data = SpreadsheetData::default();
|
|
data.set_cell(0, 0, "10");
|
|
data.set_cell(1, 0, "=A1*3"); // should compute 30
|
|
|
|
let sheets = vec![Sheet::new("Formulas", data)];
|
|
let saved = serialize_workbook(&sheets, 0);
|
|
|
|
let (loaded, _) = deserialize_workbook(&saved).unwrap();
|
|
assert_eq!(loaded[0].data.get_display_value(1, 0), "30");
|
|
|
|
// Edit A1 and verify formula updates
|
|
let mut sheets2 = loaded;
|
|
sheets2[0].data.set_cell(0, 0, "25");
|
|
let saved2 = serialize_workbook(&sheets2, 0);
|
|
|
|
let (loaded2, _) = deserialize_workbook(&saved2).unwrap();
|
|
assert_eq!(loaded2[0].data.get_display_value(1, 0), "75");
|
|
}
|
|
|
|
// ── Disk persistence tests ─────────────────────────────────────
|
|
|
|
/// Write to disk, read back, verify workbook integrity.
|
|
#[test]
|
|
fn disk_persistence_round_trip() {
|
|
let sheets = make_3_sheet_workbook();
|
|
let serialized = serialize_workbook(&sheets, 2);
|
|
|
|
// Save to disk
|
|
save_spreadsheet_state(&serialized).expect("save failed");
|
|
|
|
// Load from disk
|
|
let loaded = load_saved_spreadsheet_state().expect("load returned None");
|
|
let (restored, active) = deserialize_workbook(&loaded).unwrap();
|
|
|
|
assert_eq!(restored.len(), 3);
|
|
assert_eq!(active, 2);
|
|
assert_eq!(restored[0].name, "Budget");
|
|
assert_eq!(restored[1].name, "Q3 Projections");
|
|
assert_eq!(restored[2].name, "ROI Analysis");
|
|
assert_eq!(restored[0].data.get_raw(0, 0), "100");
|
|
assert_eq!(restored[1].data.get_raw(0, 0), "Revenue");
|
|
assert_eq!(restored[2].data.get_raw(0, 0), "Investment");
|
|
}
|
|
|
|
/// Simulate Desktop→Mobile: save full workbook, then mobile reads it.
|
|
/// Uses in-memory serialization (no disk) to avoid parallel test interference.
|
|
#[test]
|
|
fn simulate_desktop_to_mobile_sync() {
|
|
// Desktop saves
|
|
let sheets = make_3_sheet_workbook();
|
|
let saved = serialize_workbook(&sheets, 1);
|
|
|
|
// Mobile reads from the serialized string (same as reading from disk)
|
|
let (mobile_sheets, mobile_active) = deserialize_workbook(&saved).unwrap();
|
|
|
|
assert_eq!(mobile_active, 1);
|
|
assert_eq!(mobile_sheets.len(), 3);
|
|
// Mobile can access the active sheet's data
|
|
let active_sheet = &mobile_sheets[mobile_active];
|
|
assert_eq!(active_sheet.name, "Q3 Projections");
|
|
assert_eq!(active_sheet.data.get_raw(0, 0), "Revenue");
|
|
assert_eq!(active_sheet.data.get_raw(1, 0), "50000");
|
|
}
|
|
|
|
/// Simulate Mobile→Desktop: mobile edits, writes back, desktop reloads.
|
|
/// Uses in-memory serialization (no disk) to avoid parallel test interference.
|
|
#[test]
|
|
fn simulate_mobile_to_desktop_sync() {
|
|
// Desktop had saved this workbook
|
|
let sheets = make_3_sheet_workbook();
|
|
let saved = serialize_workbook(&sheets, 0);
|
|
|
|
// Mobile loads (from serialized string, equivalent to disk read)
|
|
let (mut mobile_sheets, mobile_active) = deserialize_workbook(&saved).unwrap();
|
|
|
|
// Mobile edits
|
|
mobile_sheets[mobile_active]
|
|
.data
|
|
.set_cell(10, 0, "mobile_was_here");
|
|
|
|
// Mobile writes back: update active sheet, save full workbook
|
|
let updated = serialize_workbook(&mobile_sheets, mobile_active);
|
|
|
|
// Desktop reloads (from serialized string)
|
|
let (desktop_sheets, desktop_active) = deserialize_workbook(&updated).unwrap();
|
|
|
|
assert_eq!(desktop_sheets.len(), 3);
|
|
assert_eq!(desktop_active, 0);
|
|
assert_eq!(desktop_sheets[0].data.get_raw(10, 0), "mobile_was_here");
|
|
// Other sheets untouched
|
|
assert_eq!(desktop_sheets[1].data.get_raw(0, 0), "Revenue");
|
|
assert_eq!(desktop_sheets[2].data.get_raw(0, 0), "Investment");
|
|
}
|
|
|
|
/// Empty workbook round-trip.
|
|
#[test]
|
|
fn empty_workbook_round_trip() {
|
|
let sheets: Vec<Sheet> = vec![];
|
|
let serialized = serialize_workbook(&sheets, 0);
|
|
// An empty workbook should still produce valid output
|
|
let result = deserialize_workbook(&serialized);
|
|
assert!(result.is_some());
|
|
let (restored, active) = result.unwrap();
|
|
assert!(!restored.is_empty() || active == 0);
|
|
}
|
|
|
|
/// Single-sheet workbook (legacy format fallback).
|
|
#[test]
|
|
fn single_sheet_workbook_round_trip() {
|
|
let mut data = SpreadsheetData::default();
|
|
data.set_cell(0, 0, "solo");
|
|
|
|
let sheets = vec![Sheet::new("OnlySheet", data)];
|
|
let serialized = serialize_workbook(&sheets, 0);
|
|
let (restored, active) = deserialize_workbook(&serialized).unwrap();
|
|
|
|
assert_eq!(restored.len(), 1);
|
|
assert_eq!(restored[0].name, "OnlySheet");
|
|
assert_eq!(restored[0].data.get_raw(0, 0), "solo");
|
|
assert_eq!(active, 0);
|
|
}
|
|
|
|
/// Unicode in sheet names survives round-trip.
|
|
#[test]
|
|
fn unicode_sheet_names_round_trip() {
|
|
let sheets = vec![
|
|
Sheet::new("预算表", SpreadsheetData::default()),
|
|
Sheet::new("Übersicht", SpreadsheetData::default()),
|
|
Sheet::new("日本語シート", SpreadsheetData::default()),
|
|
];
|
|
|
|
let serialized = serialize_workbook(&sheets, 1);
|
|
let (restored, active) = deserialize_workbook(&serialized).unwrap();
|
|
|
|
assert_eq!(restored.len(), 3);
|
|
assert_eq!(restored[0].name, "预算表");
|
|
assert_eq!(restored[1].name, "Übersicht");
|
|
assert_eq!(restored[2].name, "日本語シート");
|
|
assert_eq!(active, 1);
|
|
}
|