"""Tests for Priority 2 web UI features. Covers: templates, per-channel overrides, corridors, channel values, math expressions. """ from pathlib import Path from typing import Any import numpy as np import pytest from impakt.io.mme import MMEReader from impakt.web.state import AppState FIXTURE_DATA = Path(__file__).parent.parent / "fixtures" / "sample_mme" MME_DATA = Path(__file__).parent.parent / "mme_data" class TestTemplateManagement: def test_save_and_apply_template(self, tmp_path): from impakt.template.library import TemplateLibrary state = AppState() state._template_library = TemplateLibrary(tmp_path / "templates") state.load_test(FIXTURE_DATA) # Save current state as template selected = [ "IMPAKT_SYNTH_001::11HEAD0000ACXA", "IMPAKT_SYNTH_001::11HEAD0000ACYA", "IMPAKT_SYNTH_001::11HEAD0000ACZA", ] template = state.save_as_template( name="Test Head Analysis", description="Head acceleration channels", selected_keys=selected, cfc_value="1000", x1=0.0, x2=0.05, ) assert template.name == "Test Head Analysis" assert template.default_cfc == 1000 assert len(template.plots) == 1 assert len(template.plots[0].channel_patterns) == 3 # Apply the template resolved_keys, transforms = state.apply_template("test_head_analysis") assert len(resolved_keys) == 3 assert transforms["cfc"] == "1000" assert state.active_template is not None assert state.active_template.name == "Test Head Analysis" def test_template_names(self, tmp_path): from impakt.template.library import TemplateLibrary state = AppState() state._template_library = TemplateLibrary(tmp_path / "templates") assert state.template_names == [] state.load_test(FIXTURE_DATA) state.save_as_template( name="Test", description="", selected_keys=["IMPAKT_SYNTH_001::11HEAD0000ACXA"], cfc_value="none", x1=None, x2=None, ) assert "test" in state.template_names class TestPerChannelOverrides: def test_set_and_read_override(self): state = AppState() state.load_test(FIXTURE_DATA) key = "IMPAKT_SYNTH_001::11HEAD0000ACXA" state.channel_overrides[key] = {"cfc": "600"} assert state.channel_overrides[key]["cfc"] == "600" def test_override_clears(self): state = AppState() state.channel_overrides["key"] = {"cfc": "180"} state.channel_overrides.pop("key", None) assert "key" not in state.channel_overrides class TestCorridors: def test_add_corridor(self): state = AppState() corridor = { "name": "Test Corridor", "time": [0.0, 0.01, 0.02, 0.03], "lower": [-10, -20, -20, -10], "upper": [10, 20, 20, 10], "visible": True, } state.corridors.append(corridor) assert len(state.corridors) == 1 assert state.corridors[0]["name"] == "Test Corridor" class TestChannelValues: def test_build_channel_values_data(self): from impakt.web.components.channel_values import build_channel_values_data state = AppState() state.load_test(FIXTURE_DATA) channels = [] for name in ["11HEAD0000ACXA", "11HEAD0000ACYA"]: ch = state.get_channel("IMPAKT_SYNTH_001", name) if ch: channels.append((ch.code.short_label, ch)) rows = build_channel_values_data(channels, hover_x=0.05, x1=0.0, x2=0.1) assert len(rows) == 2 for row in rows: assert "ch_num" in row assert "iso_code" in row assert "description" in row assert "unit" in row assert "min" in row assert "min_time" in row assert "max" in row assert "max_time" in row assert "x1" in row assert "x2" in row assert "cursor" in row # Channel numbers should be sequential starting at 1 assert rows[0]["ch_num"] == 1 assert rows[1]["ch_num"] == 2 # Values should be parseable as floats for row in rows: float(row["min"]) float(row["max"]) float(row["x1"]) float(row["x2"]) float(row["cursor"]) def test_channel_values_no_hover(self): from impakt.web.components.channel_values import build_channel_values_data state = AppState() state.load_test(FIXTURE_DATA) ch = state.get_channel("IMPAKT_SYNTH_001", "11HEAD0000ACXA") rows = build_channel_values_data([("Head X", ch)], hover_x=None, x1=0.0, x2=0.1) assert len(rows) == 1 assert rows[0]["cursor"] == "" # No hover = empty cursor column @pytest.mark.skipif(not (MME_DATA / "3239").exists(), reason="Real data not available") def test_channel_values_real_data(self): from impakt.web.components.channel_values import build_channel_values_data state = AppState() state.load_test(MME_DATA / "3239") ch = state.get_channel("3239", "11HEAD0000H3ACXP") assert ch is not None rows = build_channel_values_data([("Head Accel X", ch)], hover_x=0.05, x1=0.0, x2=0.05) assert len(rows) == 1 # Min should be significant (frontal crash head X accel) assert abs(float(rows[0]["min"])) > 100 class TestMathExpression: def test_math_expr_in_state(self): from impakt.transform.math_expr import math_expr state = AppState() state.load_test(FIXTURE_DATA) ch_x = state.get_channel("IMPAKT_SYNTH_001", "11HEAD0000ACXA") ch_z = state.get_channel("IMPAKT_SYNTH_001", "11HEAD0000ACZA") assert ch_x is not None and ch_z is not None result = math_expr( expression="sqrt(a**2 + b**2)", channels={"a": ch_x, "b": ch_z}, name="head_xz_resultant", unit="g", ) assert result.name == "head_xz_resultant" assert result.unit == "g" assert result.peak > 0 assert len(result.data) == len(ch_x.data) # Store in primary test primary = state.primary_test primary.data._channels["head_xz_resultant"] = result # Should be retrievable retrieved = state.get_channel("IMPAKT_SYNTH_001", "head_xz_resultant") assert retrieved is not None assert retrieved.peak == result.peak class TestSessionAutoSave: def test_save_and_load_session(self, tmp_path): # Create a minimal MME fixture in tmp_path import shutil test_dir = tmp_path / "test_session" shutil.copytree(FIXTURE_DATA, test_dir) state = AppState() state.load_test(test_dir) # Save session selected = ["IMPAKT_SYNTH_001::11HEAD0000ACXA"] state.save_session(selected, "600") # Verify .impakt directory was created impakt_dir = test_dir / ".impakt" assert impakt_dir.exists() assert (impakt_dir / "session.yaml").exists() # Load session state session_data = state.load_session_state() assert session_data is not None assert session_data["cfc"] == "600" assert session_data["selected_channels"] == selected