- tests/FakeClaude: eseguibile finto 'claude' configurabile con variabili d'ambiente (registro di argomenti/cwd/stdin, output, exit code, ritardo), con le prove FK-01…FK-05. - Test organizzati per livello (Gui/, Process/, Support/) e struttura descritta in TESTING.md. - MarkdownView: la parte sulle immagini passa in MarkdownView.Images.cs (partial), senza cambiare il comportamento. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
266 lines
9.8 KiB
C#
266 lines
9.8 KiB
C#
using Avalonia;
|
|
using Avalonia.Automation;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Documents;
|
|
using Avalonia.Headless.XUnit;
|
|
using Avalonia.Media.Imaging;
|
|
using Avalonia.Styling;
|
|
using Avalonia.Threading;
|
|
using Avalonia.VisualTree;
|
|
using AgentWikiUi.Gui;
|
|
|
|
namespace AgentWikiUi.Tests;
|
|
|
|
/// <summary>Immagini nel markdown (ADR-0006): matrice MD-14 e MD-19…MD-26 in docs/TESTING.md.</summary>
|
|
[Trait("Category", "Gui")]
|
|
public sealed class MarkdownImageTests : IDisposable
|
|
{
|
|
readonly string dir = Directory.CreateTempSubdirectory("awui-img-").FullName;
|
|
readonly List<string> inBase = [];
|
|
|
|
public void Dispose()
|
|
{
|
|
Directory.Delete(dir, true);
|
|
foreach (var f in inBase) File.Delete(f);
|
|
}
|
|
|
|
string Png(string name, int w, int h)
|
|
{
|
|
var path = Path.Combine(dir, name);
|
|
File.WriteAllBytes(path, PngFactory.Create(w, h));
|
|
return path;
|
|
}
|
|
|
|
static (Window Window, MarkdownView View) Show(string markdown, double width = 500, string? baseDir = null)
|
|
{
|
|
var view = new MarkdownView();
|
|
if (baseDir is not null) view.BaseDirectory = baseDir;
|
|
var window = new Window { Width = width, Height = 800, Content = view };
|
|
window.Show();
|
|
view.Source = markdown;
|
|
WaitForImages(view);
|
|
return (window, view);
|
|
}
|
|
|
|
// il caricamento è in background (ADR-0006): si fa girare la coda della GUI finché tutte le immagini sono decodificate
|
|
static void WaitForImages(MarkdownView view)
|
|
{
|
|
var deadline = DateTime.UtcNow.AddSeconds(10);
|
|
while (view.PendingImages > 0 && DateTime.UtcNow < deadline) { Dispatcher.UIThread.RunJobs(); Thread.Sleep(5); }
|
|
Dispatcher.UIThread.RunJobs();
|
|
(TopLevel.GetTopLevel(view) as Window)?.UpdateLayout();
|
|
Assert.Equal(0, view.PendingImages);
|
|
}
|
|
|
|
static List<Image> Images(Visual v) => v.GetVisualDescendants().OfType<Image>().ToList();
|
|
|
|
static string Text(Visual v) => string.Concat(v.GetVisualDescendants().OfType<TextBlock>()
|
|
.Select(t => t.Inlines is { Count: > 0 } ? string.Concat(t.Inlines.OfType<Run>().Select(r => r.Text)) : t.Text));
|
|
|
|
[AvaloniaTheory]
|
|
[InlineData("https://example.com/a.png")]
|
|
[InlineData("http://example.com/a.png")]
|
|
[InlineData("ftp://example.com/a.png")]
|
|
[InlineData("data:image/png;base64,AAAA")]
|
|
public void Immagini_remote_come_testo(string url)
|
|
{
|
|
var (_, view) = Show($"Vedi  qui");
|
|
Assert.Empty(Images(view));
|
|
Assert.Contains("[logo]", Text(view));
|
|
}
|
|
|
|
[AvaloniaFact]
|
|
public void Immagine_percorso_assoluto()
|
|
{
|
|
var path = Png("a.png", 40, 20);
|
|
var (_, view) = Show($"");
|
|
var img = Assert.Single(Images(view));
|
|
var bmp = Assert.IsAssignableFrom<Bitmap>(img.Source);
|
|
Assert.Equal(new PixelSize(40, 20), bmp.PixelSize);
|
|
Assert.Equal(40, img.Bounds.Width, 0.5);
|
|
Assert.DoesNotContain("[griglia]", Text(view));
|
|
}
|
|
|
|
[AvaloniaFact]
|
|
public void Immagine_percorso_relativo_e_file_uri()
|
|
{
|
|
var name = $"awui-{Guid.NewGuid():N}.png";
|
|
var inBasePath = Path.Combine(AppContext.BaseDirectory, name);
|
|
File.WriteAllBytes(inBasePath, PngFactory.Create(10, 10));
|
|
inBase.Add(inBasePath);
|
|
var (_, relative) = Show($"");
|
|
Assert.Single(Images(relative));
|
|
|
|
var uri = new Uri(Png("b.png", 12, 12)).AbsoluteUri;
|
|
var (_, fileUri) = Show($"");
|
|
Assert.Single(Images(fileUri));
|
|
}
|
|
|
|
[AvaloniaTheory]
|
|
[InlineData("manca")]
|
|
[InlineData("corrotto")]
|
|
[InlineData("estensione")]
|
|
public void Immagine_non_valida_ripiego(string caso)
|
|
{
|
|
var path = caso switch
|
|
{
|
|
"manca" => Path.Combine(dir, "non-esiste.png"),
|
|
"corrotto" => Write("rotto.png", "questo non è un png"u8.ToArray()),
|
|
_ => Write("dati.txt", PngFactory.Create(8, 8)),
|
|
};
|
|
var (_, view) = Show($"");
|
|
Assert.Empty(Images(view));
|
|
Assert.Contains("[alt]", Text(view));
|
|
}
|
|
|
|
string Write(string name, byte[] bytes)
|
|
{
|
|
var path = Path.Combine(dir, name);
|
|
File.WriteAllBytes(path, bytes);
|
|
return path;
|
|
}
|
|
|
|
[AvaloniaFact]
|
|
public void Immagine_oltre_limite()
|
|
{
|
|
var path = Path.Combine(dir, "grande.png");
|
|
using (var fs = File.Create(path)) { fs.Write(PngFactory.Create(8, 8)); fs.SetLength(20L * 1024 * 1024 + 1); }
|
|
var (_, view) = Show($"");
|
|
Assert.Empty(Images(view));
|
|
Assert.Contains("[enorme]", Text(view));
|
|
}
|
|
|
|
[AvaloniaFact]
|
|
public void Immagine_larghezza_massima()
|
|
{
|
|
var (_, wide) = Show($"})", width: 500);
|
|
var img = Assert.Single(Images(wide));
|
|
Assert.True(img.Bounds.Width <= 500, $"larghezza {img.Bounds.Width}");
|
|
Assert.Equal(img.Bounds.Width / 20, img.Bounds.Height, 1); // proporzioni 20:1
|
|
|
|
var (_, small) = Show($"})", width: 500);
|
|
Assert.Equal(50, Assert.Single(Images(small)).Bounds.Width, 0.5); // mai ingrandita
|
|
}
|
|
|
|
[AvaloniaFact]
|
|
public void Immagine_testo_alternativo_accessibile()
|
|
{
|
|
var (_, view) = Show($"})");
|
|
var img = Assert.Single(Images(view));
|
|
Assert.Equal("un grafico", AutomationProperties.GetName(img));
|
|
Assert.Equal("un grafico", ToolTip.GetTip(img));
|
|
}
|
|
|
|
[AvaloniaFact]
|
|
public void Immagine_in_contesti_annidati()
|
|
{
|
|
var p = Png("n.png", 10, 10);
|
|
var markdown = $"""
|
|
Testo  e ancora testo con [un link](https://example.com).
|
|
|
|
[](https://example.com)
|
|
|
|
- voce 
|
|
|
|
| a | b |
|
|
|---|---|
|
|
|  | x |
|
|
""";
|
|
var (_, view) = Show(markdown);
|
|
Assert.Equal(4, Images(view).Count);
|
|
Assert.Contains("un link", Text(view));
|
|
}
|
|
|
|
[AvaloniaFact]
|
|
public void Immagine_cambio_tema()
|
|
{
|
|
var (window, view) = Show($"})");
|
|
window.RequestedThemeVariant = ThemeVariant.Dark;
|
|
WaitForImages(view);
|
|
Assert.Single(Images(view));
|
|
window.RequestedThemeVariant = ThemeVariant.Light;
|
|
WaitForImages(view);
|
|
Assert.Single(Images(view));
|
|
}
|
|
|
|
[AvaloniaFact]
|
|
public void Immagine_caricamento_in_background()
|
|
{
|
|
var path = Png("bg.png", 30, 30);
|
|
var view = new MarkdownView();
|
|
var window = new Window { Width = 500, Height = 800, Content = view };
|
|
window.Show();
|
|
view.Source = $"";
|
|
// subito dopo il rendering: immagine in attesa e solo il segnaposto (il completamento passa dalla coda della GUI)
|
|
Assert.Equal(1, view.PendingImages);
|
|
Assert.Empty(Images(view));
|
|
Assert.Contains("[segnaposto]", Text(view));
|
|
WaitForImages(view);
|
|
Assert.Single(Images(view));
|
|
Assert.DoesNotContain("[segnaposto]", Text(view));
|
|
|
|
// file corrotto: la decodifica fallisce e il segnaposto resta
|
|
var rotto = Write("rotto2.png", "no"u8.ToArray());
|
|
view.Source = $"";
|
|
WaitForImages(view);
|
|
Assert.Empty(Images(view));
|
|
Assert.Contains("[guasta]", Text(view));
|
|
}
|
|
|
|
// Struttura tipo wiki: <radice>/raw/immagini.json, <radice>/raw/immagini/<file>, exe in <radice>/agent-wiki-ui/
|
|
string Wiki(string name, params string[] files)
|
|
{
|
|
var root = Path.Combine(dir, name);
|
|
Directory.CreateDirectory(Path.Combine(root, "raw", "immagini"));
|
|
Directory.CreateDirectory(Path.Combine(root, "agent-wiki-ui"));
|
|
foreach (var f in files) File.WriteAllBytes(Path.Combine(root, "raw", "immagini", f), PngFactory.Create(24, 12));
|
|
File.WriteAllText(Path.Combine(root, "raw", "immagini.json"), "{\"figures\":[]}");
|
|
return root;
|
|
}
|
|
|
|
[AvaloniaFact]
|
|
public void Immagine_percorso_relativo_alla_radice()
|
|
{
|
|
var root = Wiki("wiki-a", "fig1.png");
|
|
var (_, view) = Show("", baseDir: Path.Combine(root, "agent-wiki-ui"));
|
|
Assert.Single(Images(view));
|
|
Assert.DoesNotContain("[Figura 1]", Text(view));
|
|
}
|
|
|
|
[AvaloniaFact]
|
|
public void Immagine_cartella_piu_vicina()
|
|
{
|
|
var root = Wiki("wiki-b");
|
|
File.WriteAllBytes(Path.Combine(root, "raw", "immagini", "x.png"), PngFactory.Create(30, 30));
|
|
var exeDir = Path.Combine(root, "agent-wiki-ui");
|
|
Directory.CreateDirectory(Path.Combine(exeDir, "raw", "immagini"));
|
|
File.WriteAllBytes(Path.Combine(exeDir, "raw", "immagini", "x.png"), PngFactory.Create(10, 10));
|
|
|
|
var (_, view) = Show("", baseDir: exeDir);
|
|
var bmp = Assert.IsAssignableFrom<Bitmap>(Assert.Single(Images(view)).Source);
|
|
Assert.Equal(new PixelSize(10, 10), bmp.PixelSize); // vince la cartella più vicina all'exe
|
|
|
|
var (_, nowhere) = Show("", baseDir: exeDir);
|
|
Assert.Empty(Images(nowhere));
|
|
Assert.Contains("[y]", Text(nowhere));
|
|
}
|
|
|
|
[AvaloniaFact]
|
|
public void Immagine_risposta_stile_wiki()
|
|
{
|
|
var root = Wiki("wiki-c", "455dd3d0.png");
|
|
var risposta = """
|
|
La struttura generica è in Figura 1.
|
|
|
|

|
|
|
|
Figura 1 - struttura generica di un dichetopirrolopirrolo · §1.1 r.73 · [[fig-001]]
|
|
""";
|
|
var (_, view) = Show(risposta, baseDir: Path.Combine(root, "agent-wiki-ui"));
|
|
Assert.Single(Images(view));
|
|
var text = Text(view);
|
|
Assert.Contains("Figura 1 - struttura generica", text);
|
|
Assert.Contains("[[fig-001]]", text); // il link alla nota resta testo: l'app non legge il catalogo JSON
|
|
}
|
|
}
|