using Avalonia; using Avalonia.Automation; using Avalonia.Controls; using Avalonia.Controls.Documents; using Avalonia.Headless.XUnit; using Avalonia.Styling; using Avalonia.Threading; using Avalonia.VisualTree; using CSharpMath.Avalonia; using AgentWikiUi.Gui; namespace AgentWikiUi.Tests; /// Formule LaTeX nel markdown (ADR-0008): matrice MD-31…MD-38 in docs/TESTING.md. [Trait("Category", "Gui")] public sealed class MarkdownMathTests { static (Window Window, MarkdownView View) Show(string markdown, double width = 600) { var view = new MarkdownView(); var window = new Window { Width = width, Height = 800, Content = view }; window.Show(); view.Source = markdown; Dispatcher.UIThread.RunJobs(); window.UpdateLayout(); return (window, view); } static List Formulas(Visual v) => v.GetVisualDescendants().OfType().ToList(); static string Text(Visual v) => string.Concat(v.GetVisualDescendants().OfType() .Select(t => t.Inlines is { Count: > 0 } ? string.Concat(t.Inlines.OfType().Select(r => r.Text)) : t.Text)); [AvaloniaFact] public void Formula_inline() { var (_, view) = Show("Il gap è $E_g = E_{LUMO} - E_{HOMO}$ in eV."); var f = Assert.Single(Formulas(view)); Assert.Equal("E_g = E_{LUMO} - E_{HOMO}", AutomationProperties.GetName(f)); Assert.True(string.IsNullOrEmpty(f.ErrorMessage)); Assert.True(f.Bounds.Width > 0 && f.Bounds.Height > 0); var text = Text(view); Assert.Contains("Il gap è", text); Assert.Contains("in eV.", text); Assert.DoesNotContain("$", text); } [AvaloniaFact] public void Formula_a_blocco() { var (_, view) = Show("Prima.\n\n$$\n\\frac{a}{b} = c\n$$\n\nDopo."); var f = Assert.Single(Formulas(view)); Assert.Equal("\\frac{a}{b} = c", AutomationProperties.GetName(f)); Assert.True(string.IsNullOrEmpty(f.ErrorMessage)); Assert.True(f.Bounds.Height > 0); Assert.DoesNotContain("\\frac", Text(view)); // non è un blocco di codice col sorgente Assert.Contains("Prima.", Text(view)); Assert.Contains("Dopo.", Text(view)); } [AvaloniaTheory] [InlineData("Testo $\\frac{a$ fine")] [InlineData("$$\n\\unknowncommand{x}\n$$")] [InlineData("Testo $\\begin{matrix$ fine")] public void Formula_non_valida_ripiego(string markdown) { var (_, view) = Show(markdown); Assert.Empty(Formulas(view)); Assert.Contains("$", Text(view)); // il sorgente resta visibile come testo } [AvaloniaTheory] [InlineData("\\frac{a}{b}")] [InlineData("x^2 + y_i")] [InlineData("\\mathbf{A} \\times \\mathbf{B}")] [InlineData("\\overline{x}")] [InlineData("\\left( \\frac{1}{2} \\right)")] [InlineData("\\lambda_{max} \\approx \\pi \\cdot \\tau")] [InlineData("\\sum_{i=1}^{n} x_i")] [InlineData("\\text{Mn} = \\chi \\varepsilon")] public void Costrutti_latex_comuni(string latex) { var (_, view) = Show($"Prima $${latex}$$ dopo".Replace("$$", "$")); var f = Assert.Single(Formulas(view)); Assert.Equal(latex, AutomationProperties.GetName(f)); Assert.True(string.IsNullOrEmpty(f.ErrorMessage), f.ErrorMessage); Assert.True(f.Bounds.Width > 0); } [AvaloniaFact] public void Formula_cambio_tema() { var (window, view) = Show("Gap $E_g$ e\n\n$$\\frac{a}{b}$$"); Assert.Equal(2, Formulas(view).Count); window.RequestedThemeVariant = ThemeVariant.Dark; Dispatcher.UIThread.RunJobs(); Assert.Equal(2, Formulas(view).Count); window.RequestedThemeVariant = ThemeVariant.Light; Dispatcher.UIThread.RunJobs(); Assert.Equal(2, Formulas(view).Count); } [AvaloniaFact] public void Formula_in_contesti_annidati() { var markdown = """ Testo $a_1$ e un [link](https://example.com). [vai a $b^2$](https://example.com) - voce $c_3$ | x | y | |---|---| | $d_4$ | z | """; var (_, view) = Show(markdown); Assert.Equal(4, Formulas(view).Count); Assert.Contains("link", Text(view)); Assert.Contains("vai a", Text(view)); } [AvaloniaFact] public void Formula_nome_accessibile() { var (_, view) = Show("Gap $E_g$."); var f = Assert.Single(Formulas(view)); Assert.Equal("E_g", AutomationProperties.GetName(f)); } [AvaloniaFact] public void Dollari_isolati_restano_testo() { var (_, view) = Show("Costa 10 $ e poi 20 $ in totale."); Assert.Empty(Formulas(view)); Assert.Contains("10 $ e poi 20 $", Text(view)); } }