Files
calctext/calcpad-engine/tests/sheet_scenarios.rs
C. Cassel 0d38bd3108 feat(web): implement complete workspace with themes, tabs, sidebar, and mobile
Transform CalcText from a single-document calculator into a full workspace
application with multi-document support, theming, and responsive mobile experience.

- Theme system: 5 presets (Light, Dark, Matrix, Midnight, Warm) + accent colors
- Document model with localStorage persistence and auto-save
- Tab bar with keyboard shortcuts (Ctrl+N/W/Tab/1-9), rename, close
- Sidebar with search, recent, favorites, folders, templates, drag-and-drop
- 5 templates: Budget, Invoice, Unit Converter, Trip Planner, Loan Calculator
- Status bar with cursor position, engine status, dedication to Igor Cassel
- Results panel: type-specific colors, click-to-copy, error hints
- Format toolbar: H, B, I, //, color labels with live preview toggle
- Syntax highlighting using theme CSS variables
- Error hover tooltips
- Mobile: bottom results tray, sidebar drawer, touch targets, safe areas
- Docker multi-stage build (Rust WASM + Vite + Nginx)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 09:12:05 -04:00

314 lines
10 KiB
Rust

//! Integration tests covering real-world sheet scenarios.
//!
//! These exercise the full pipeline (lexer -> parser -> interpreter) through
//! the eval_line / eval_sheet / SheetContext public APIs, verifying end-to-end
//! behavior rather than individual module internals.
use calcpad_engine::context::EvalContext;
use calcpad_engine::pipeline::{eval_line, eval_sheet};
use calcpad_engine::types::ResultType;
use calcpad_engine::SheetContext;
// =========================================================================
// Variable assignment and reference across lines
// =========================================================================
#[test]
fn sheet_variable_assignment_and_reference() {
let mut ctx = EvalContext::new();
let results = eval_sheet(
&["price = 100", "quantity = 5", "total = price * quantity"],
&mut ctx,
);
assert_eq!(results.len(), 3);
assert_eq!(results[0].metadata.display, "100");
assert_eq!(results[1].metadata.display, "5");
assert_eq!(results[2].metadata.display, "500");
}
#[test]
fn sheet_variable_used_in_multiple_lines() {
let mut ctx = EvalContext::new();
let results = eval_sheet(
&[
"rate = 25",
"hours = 8",
"daily = rate * hours",
"weekly = daily * 5",
],
&mut ctx,
);
assert_eq!(results[2].metadata.display, "200");
assert_eq!(results[3].metadata.display, "1000");
}
// =========================================================================
// Chained dependencies
// =========================================================================
#[test]
fn sheet_chained_dependencies() {
let mut ctx = EvalContext::new();
let results = eval_sheet(
&["a = 10", "b = a + 5", "c = b * 2", "d = c - a"],
&mut ctx,
);
assert_eq!(results[0].metadata.display, "10");
assert_eq!(results[1].metadata.display, "15");
assert_eq!(results[2].metadata.display, "30");
assert_eq!(results[3].metadata.display, "20");
}
#[test]
fn sheet_deep_chain() {
let mut ctx = EvalContext::new();
let results = eval_sheet(
&[
"v1 = 1",
"v2 = v1 + 1",
"v3 = v2 + 1",
"v4 = v3 + 1",
"v5 = v4 + 1",
],
&mut ctx,
);
assert_eq!(results[4].metadata.display, "5");
}
// =========================================================================
// Forward references and undefined variables
// =========================================================================
#[test]
fn sheet_forward_reference_error() {
let mut ctx = EvalContext::new();
let results = eval_sheet(&["a = b + 1", "b = 10"], &mut ctx);
// eval_sheet processes sequentially; b isn't defined yet when line 0 runs
assert_eq!(results[0].result_type(), ResultType::Error);
assert_eq!(results[1].metadata.display, "10");
}
#[test]
fn sheet_undefined_variable_in_chain() {
let mut ctx = EvalContext::new();
let results = eval_sheet(&["x = undefined_var + 1"], &mut ctx);
assert_eq!(results[0].result_type(), ResultType::Error);
}
// =========================================================================
// Unit conversion across lines
// =========================================================================
#[test]
fn sheet_unit_arithmetic_same_unit() {
let mut ctx = EvalContext::new();
let results = eval_sheet(&["a = 5kg", "b = 3kg", "total = a + b"], &mut ctx);
assert_eq!(results[2].result_type(), ResultType::UnitValue);
assert_eq!(results[2].metadata.raw_value, Some(8.0));
}
// =========================================================================
// Mixed expressions: comments, empty lines, text
// =========================================================================
#[test]
fn sheet_with_comments() {
let mut ctx = EvalContext::new();
let results = eval_sheet(
&[
"// Budget calculation",
"income = 5000",
"expenses = 3000",
"savings = income - expenses",
],
&mut ctx,
);
assert_eq!(results[0].result_type(), ResultType::NonCalculable); // comment
assert_eq!(results[3].metadata.display, "2000");
}
#[test]
fn sheet_with_empty_lines() {
let mut ctx = EvalContext::new();
let results = eval_sheet(&["x = 10", "", "y = x + 5"], &mut ctx);
assert_eq!(results[0].metadata.display, "10");
assert_eq!(results[1].result_type(), ResultType::Empty); // empty line
assert_eq!(results[2].metadata.display, "15");
}
#[test]
fn sheet_mixed_comments_and_calcs() {
let mut ctx = EvalContext::new();
let results = eval_sheet(
&[
"// Shopping list",
"apples = 3 * 2",
"// bananas are on sale",
"bananas = 5 * 1",
"total = apples + bananas",
],
&mut ctx,
);
assert_eq!(results[1].metadata.display, "6");
assert_eq!(results[3].metadata.display, "5");
assert_eq!(results[4].metadata.display, "11");
}
// =========================================================================
// Variable reassignment / shadowing
// =========================================================================
#[test]
fn sheet_variable_reassignment() {
let mut ctx = EvalContext::new();
let results = eval_sheet(&["x = 5", "y = x * 2", "x = 20", "z = x * 2"], &mut ctx);
assert_eq!(results[0].metadata.display, "5");
assert_eq!(results[1].metadata.display, "10");
assert_eq!(results[2].metadata.display, "20");
assert_eq!(results[3].metadata.display, "40"); // uses reassigned x=20
}
// =========================================================================
// Real-world calculation scenarios
// =========================================================================
#[test]
fn sheet_invoice_calculation() {
let mut ctx = EvalContext::new();
let results = eval_sheet(
&[
"// Invoice",
"subtotal = 1500",
"tax_rate = 8.5",
"tax = subtotal * tax_rate / 100",
"total = subtotal + tax",
],
&mut ctx,
);
assert_eq!(results[1].metadata.display, "1500");
assert_eq!(results[3].metadata.display, "127.5");
assert_eq!(results[4].metadata.display, "1627.5");
}
#[test]
fn sheet_tip_calculation() {
let mut ctx = EvalContext::new();
let results = eval_sheet(
&["bill = 85", "tip = bill + 20%", "per_person = tip / 4"],
&mut ctx,
);
assert_eq!(results[0].metadata.display, "85");
assert_eq!(results[1].metadata.display, "102");
assert!((results[2].metadata.raw_value.unwrap() - 25.5).abs() < 0.01);
}
#[test]
fn sheet_accumulator_pattern() {
let mut ctx = EvalContext::new();
let lines: Vec<String> = (0..20)
.map(|i| {
if i == 0 {
"acc = 0".to_string()
} else {
format!("acc = acc + {}", i)
}
})
.collect();
let refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
let results = eval_sheet(&refs, &mut ctx);
// Sum of 1..19 = 190
let last = results.last().unwrap();
assert_eq!(last.metadata.raw_value, Some(190.0));
}
// =========================================================================
// Context isolation
// =========================================================================
#[test]
fn context_variables_independent() {
let mut ctx1 = EvalContext::new();
let mut ctx2 = EvalContext::new();
eval_line("x = 100", &mut ctx1);
let r = eval_line("x", &mut ctx2);
assert_eq!(r.result_type(), ResultType::Error); // x not defined in ctx2
}
// =========================================================================
// SheetContext: incremental evaluation
// =========================================================================
#[test]
fn sheet_context_incremental_independent_lines() {
let mut sheet = SheetContext::new();
sheet.set_line(0, "a = 10");
sheet.set_line(1, "b = 20");
sheet.set_line(2, "c = a + 5");
sheet.set_line(3, "d = b + 5");
let results = sheet.eval();
assert_eq!(results.len(), 4);
// Change only b: a and c should be unaffected
sheet.set_line(1, "b = 99");
let results = sheet.eval();
assert_eq!(results[0].metadata.raw_value, Some(10.0));
assert_eq!(results[1].metadata.raw_value, Some(99.0));
assert_eq!(results[2].metadata.raw_value, Some(15.0)); // a + 5, unchanged
assert_eq!(results[3].metadata.raw_value, Some(104.0)); // b + 5
}
#[test]
fn sheet_context_aggregator_invoice() {
let mut sheet = SheetContext::new();
sheet.set_line(0, "## Monthly Expenses");
sheet.set_line(1, "1200"); // rent
sheet.set_line(2, "150"); // utilities
sheet.set_line(3, "400"); // groceries
sheet.set_line(4, "subtotal");
sheet.set_line(5, "## One-Time Costs");
sheet.set_line(6, "500"); // furniture
sheet.set_line(7, "200"); // electronics
sheet.set_line(8, "subtotal");
sheet.set_line(9, "grand total");
let results = sheet.eval();
assert_eq!(results[4].metadata.raw_value, Some(1750.0));
assert_eq!(results[8].metadata.raw_value, Some(700.0));
assert_eq!(results[9].metadata.raw_value, Some(2450.0));
}
#[test]
fn sheet_context_prev_through_sections() {
let mut sheet = SheetContext::new();
sheet.set_line(0, "100");
sheet.set_line(1, "prev + 50"); // 150
sheet.set_line(2, "prev * 2"); // 300
let results = sheet.eval();
assert_eq!(results[0].metadata.raw_value, Some(100.0));
assert_eq!(results[1].metadata.raw_value, Some(150.0));
assert_eq!(results[2].metadata.raw_value, Some(300.0));
}
#[test]
fn sheet_context_comparison_result() {
let mut ctx = EvalContext::new();
let results = eval_sheet(
&["budget = 1000", "spent = 750", "budget - spent > 0"],
&mut ctx,
);
assert_eq!(results[2].result_type(), ResultType::Boolean);
assert_eq!(results[2].metadata.display, "true");
}
#[test]
fn sheet_context_percentage_discount() {
let mut ctx = EvalContext::new();
let results = eval_sheet(
&["original = $200", "discounted = $200 - 30%"],
&mut ctx,
);
assert_eq!(results[0].result_type(), ResultType::CurrencyValue);
assert_eq!(results[1].result_type(), ResultType::CurrencyValue);
assert!((results[1].metadata.raw_value.unwrap() - 140.0).abs() < 0.01);
}