Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="10.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\BlazorExpress.ChartJS.MCP\BlazorExpress.ChartJS.MCP.csproj" />
</ItemGroup>

</Project>
69 changes: 69 additions & 0 deletions BlazorExpress.ChartJS.MCP.Tests/ChartCatalogTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
namespace BlazorExpress.ChartJS.MCP.Tests;

public class ChartCatalogTests
{
[Fact]
public void All_Returns_Current_Public_Chart_Types()
{
var names = ChartCatalog.All.Select(x => x.Name).ToArray();

Assert.Equal(["Bar", "Bubble", "Doughnut", "Line", "Pie", "PolarArea", "Radar", "Scatter"], names);
}

[Theory]
[InlineData("Bar", "BarChart", "BarChartOptions", "BarChartDataset")]
[InlineData("polar-area", "PolarAreaChart", "PolarAreaChartOptions", "PolarAreaChartDataset")]
public void GetSchema_Returns_Metadata_For_Chart_Type(string chartType, string component, string options, string dataset)
{
var schema = ChartCatalog.GetSchema(chartType);

Assert.Equal(component, schema.Component);
Assert.Equal(options, schema.OptionsType);
Assert.Equal(dataset, schema.DatasetType);
Assert.NotEmpty(schema.CommonInputs);
Assert.NotEmpty(schema.Examples);
}

[Fact]
public void Radar_Is_Not_Datalabel_Safe()
{
var schema = ChartCatalog.GetSchema("Radar");

Assert.False(schema.SupportsDatalabels);
}

[Theory]
[InlineData("Bubble")]
[InlineData("Radar")]
public void Charts_Without_Plugins_Report_No_Title_Or_Legend_Support(string chartType)
{
var schema = ChartCatalog.GetSchema(chartType);

Assert.False(schema.SupportsPluginOptions);
Assert.False(schema.SupportsTitleOptions);
Assert.False(schema.SupportsLegendOptions);
}

[Theory]
[InlineData("Bar")]
[InlineData("Scatter")]
public void Charts_With_Plugins_Report_Title_And_Legend_Support(string chartType)
{
var schema = ChartCatalog.GetSchema(chartType);

Assert.True(schema.SupportsPluginOptions);
Assert.True(schema.SupportsTitleOptions);
Assert.True(schema.SupportsLegendOptions);
}

[Theory]
[InlineData("Bar", "numericDatasetsJson")]
[InlineData("Scatter", "pointDatasetsJson")]
[InlineData("Bubble", "pointDatasetsJson")]
public void GetSchema_Includes_Dataset_Examples(string chartType, string exampleKey)
{
var schema = ChartCatalog.GetSchema(chartType);

Assert.True(schema.Examples.ContainsKey(exampleKey));
}
}
231 changes: 231 additions & 0 deletions BlazorExpress.ChartJS.MCP.Tests/ChartExampleGeneratorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
namespace BlazorExpress.ChartJS.MCP.Tests;

public class ChartExampleGeneratorTests
{
[Theory]
[InlineData("Bar", "BarChart", "BarChartOptions", "BarChartDataset")]
[InlineData("Bubble", "BubbleChart", "BubbleChartOptions", "BubbleChartDataset")]
[InlineData("Doughnut", "DoughnutChart", "DoughnutChartOptions", "DoughnutChartDataset")]
[InlineData("Line", "LineChart", "LineChartOptions", "LineChartDataset")]
[InlineData("Pie", "PieChart", "PieChartOptions", "PieChartDataset")]
[InlineData("PolarArea", "PolarAreaChart", "PolarAreaChartOptions", "PolarAreaChartDataset")]
[InlineData("Radar", "RadarChart", "RadarChartOptions", "RadarChartDataset")]
[InlineData("Scatter", "ScatterChart", "ScatterChartOptions", "ScatterChartDataset")]
public void Generate_Includes_Expected_Razor_Types_For_All_Charts(string chartType, string component, string options, string dataset)
{
var generator = new ChartExampleGenerator();

var generated = generator.Generate(new ChartGenerationRequest { ChartType = chartType, Title = $"{chartType} Sample" });

Assert.Contains($"<{component} @ref=", generated.Code);
Assert.Contains($"private {options}", generated.Code);
Assert.Contains($"new {dataset}", generated.Code);
Assert.Contains("@using BlazorExpress.ChartJS", generated.Code);
}

[Fact]
public void Generate_Bar_Datalabels_Uses_Plugin()
{
var generator = new ChartExampleGenerator();

var generated = generator.Generate(new ChartGenerationRequest { ChartType = "Bar", Datalabels = true });

Assert.Contains("ChartDataLabels", generated.Code);
Assert.Contains("chartjs-plugin-datalabels", string.Join(" ", generated.RequiredScripts));
}

[Fact]
public void Generate_Radar_Does_Not_Use_Datalabels()
{
var generator = new ChartExampleGenerator();

var generated = generator.Generate(new ChartGenerationRequest { ChartType = "Radar", Datalabels = true });

Assert.DoesNotContain("ChartDataLabels", generated.Code);
Assert.DoesNotContain("chartjs-plugin-datalabels", string.Join(" ", generated.RequiredScripts));
}

[Theory]
[InlineData("Bubble")]
[InlineData("Radar")]
public void Generate_Charts_Without_Plugin_Options_Do_Not_Emit_Plugin_Title(string chartType)
{
var generator = new ChartExampleGenerator();

var generated = generator.Generate(new ChartGenerationRequest { ChartType = chartType });

Assert.DoesNotContain(".Plugins.Title", generated.Code);
Assert.DoesNotContain(".Plugins.Legend", generated.Code);
}

[Fact]
public void Generate_Scatter_Uses_Point_Data()
{
var generator = new ChartExampleGenerator();

var generated = generator.Generate(new ChartGenerationRequest
{
ChartType = "Scatter",
Datasets =
[
new()
{
Label = "Samples",
Points =
[
new ChartPointRequest { X = 1, Y = 2 },
new ChartPointRequest { X = 3, Y = 4 },
],
},
],
});

Assert.Contains("new List<ScatterChartDataPoint> { new(1, 2), new(3, 4)", generated.Code);
}

[Theory]
[InlineData(null, "new(1, 2, 8)")]
[InlineData(9.0, "new(1, 2, 9)")]
public void Generate_Bubble_Uses_Point_Data_With_Defaultable_Radius(double? radius, string expected)
{
var generator = new ChartExampleGenerator();

var generated = generator.Generate(new ChartGenerationRequest
{
ChartType = "Bubble",
Datasets =
[
new()
{
Label = "Samples",
Points = [new ChartPointRequest { X = 1, Y = 2, R = radius }],
},
],
});

Assert.Contains(expected, generated.Code);
}

[Fact]
public void GenerateDashboard_Default_Includes_All_Chart_Components()
{
var generator = new ChartExampleGenerator();

var generated = generator.GenerateDashboard(new ChartDashboardRequest());

foreach (var definition in ChartCatalog.All)
Assert.Contains($"<{definition.ComponentName} @ref=", generated.Code);

Assert.Equal(8, generated.ChartTypes.Count);
}

[Fact]
public void GenerateDashboard_Custom_Uses_Unique_Field_Names_And_Deduplicated_Scripts()
{
var generator = new ChartExampleGenerator();

var generated = generator.GenerateDashboard(new ChartDashboardRequest
{
Charts =
[
new ChartGenerationRequest { ChartType = "Bar", Datalabels = true },
new ChartGenerationRequest { ChartType = "Bar", Datalabels = true },
],
});

Assert.Contains("barChart1", generated.Code);
Assert.Contains("barChart2", generated.Code);
Assert.Equal(generated.RequiredScripts.Count, generated.RequiredScripts.Distinct().Count());
}

[Fact]
public void Generated_Single_And_Dashboard_Pages_Compile()
{
using var workspace = new RazorCompileWorkspace();
var generator = new ChartExampleGenerator();
var single = generator.Generate(new ChartGenerationRequest { ChartType = "Bubble", Title = "Bubble Compile" });
var dashboard = generator.GenerateDashboard(new ChartDashboardRequest());

workspace.AddPage(single.PageName, single.Code);
workspace.AddPage(dashboard.PageName, dashboard.Code);

workspace.Build();
}

private sealed class RazorCompileWorkspace : IDisposable
{
private readonly string root = Path.Combine(Path.GetTempPath(), $"bex-chartjs-compile-{Guid.NewGuid():N}");

public RazorCompileWorkspace()
{
Directory.CreateDirectory(root);
Directory.CreateDirectory(Path.Combine(root, "Pages"));

File.WriteAllText(Path.Combine(root, "CompileHost.csproj"), $$"""
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="{{Path.Combine(FindRepoRoot(), "BlazorExpress.ChartJS", "BlazorExpress.ChartJS.csproj")}}" />
</ItemGroup>
</Project>
""");

File.WriteAllText(Path.Combine(root, "_Imports.razor"), "@using BlazorExpress.ChartJS" + Environment.NewLine);
}

public void AddPage(string pageName, string code) =>
File.WriteAllText(Path.Combine(root, "Pages", $"{pageName}.razor"), code);

public void Build()
{
using var process = new System.Diagnostics.Process();
process.StartInfo.FileName = "dotnet";
process.StartInfo.ArgumentList.Add("build");
process.StartInfo.ArgumentList.Add(Path.Combine(root, "CompileHost.csproj"));
process.StartInfo.ArgumentList.Add("--nologo");
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.UseShellExecute = false;
process.Start();

var outputTask = process.StandardOutput.ReadToEndAsync();
var errorTask = process.StandardError.ReadToEndAsync();
var exited = process.WaitForExit(120_000);
if (!exited)
{
process.Kill(entireProcessTree: true);
process.WaitForExit(30_000);
}

var output = outputTask.GetAwaiter().GetResult();
var error = errorTask.GetAwaiter().GetResult();

Assert.True(exited && process.ExitCode == 0, output + Environment.NewLine + error);
}

public void Dispose()
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}

private static string FindRepoRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "BlazorExpress.ChartJS.sln")))
return directory.FullName;

directory = directory.Parent;
}

throw new DirectoryNotFoundException("Could not locate repository root.");
}
}
}
Loading
Loading