mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-13 10:47:21 +00:00
Changed all icons of AI Studio to Lucide (#1187)
This commit is contained in:
1 parent
9a676101f7
commit
7ef26cd5c8
352 files changed
+6261
-1620
No files matched your search
@@ -0,0 +1,115 @@
|
||||
using AIStudio.Tests.Models.Generation;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
using SourceCodeRules.UsageAnalyzers;
|
||||
|
||||
namespace AIStudio.Tests.Analyzers;
|
||||
|
||||
/// <summary>
|
||||
/// Checks that the app takes its icons only from the catalog AppIcons.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// AppIcons names every icon after its meaning and decides there which Lucide icon stands for it. MWAIS0017 refuses
|
||||
/// an icon of LucideIcons or of MudBlazor anywhere else while compiling, so no component picks an icon on its own.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class IconCatalogAnalyzerTests
|
||||
{
|
||||
[TestCase("public string Icon => LucideIcons.HOUSE;", "AIStudio.Tools.LucideIcons.HOUSE", TestName = "A Lucide icon")]
|
||||
[TestCase("public string Icon => MudBlazor.Icons.Material.Filled.Home;", "MudBlazor.Icons.Material.Filled.Home", TestName = "A Material icon of MudBlazor")]
|
||||
[TestCase("public string Icon => Icons.Material.Outlined.Home;", "MudBlazor.Icons.Material.Outlined.Home", TestName = "A Material icon of MudBlazor through its using")]
|
||||
[TestCase("public string Icon => Icons.Custom.Brands.GitHub;", "MudBlazor.Icons.Custom.Brands.GitHub", TestName = "A brand logo of MudBlazor")]
|
||||
[TestCase("public int Rank(string icon) => icon switch { LucideIcons.HOUSE => 1, _ => 0 };", "AIStudio.Tools.LucideIcons.HOUSE", TestName = "A Lucide icon in a pattern")]
|
||||
[TestCase("private const string ICON = LucideIcons.HOUSE;", "AIStudio.Tools.LucideIcons.HOUSE", TestName = "A Lucide icon in a constant of another class")]
|
||||
public async Task AnIconOutsideTheCatalogIsRefused(string member, string icon)
|
||||
{
|
||||
var reported = await AnalyzeAsync(member);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(reported, Has.Count.EqualTo(1));
|
||||
Assert.That(reported.FirstOrDefault()?.Id, Is.EqualTo("MWAIS0017"));
|
||||
Assert.That(reported.FirstOrDefault()?.GetMessage(), Does.Contain($"'{icon}'"));
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("public string Icon => AppIcons.DELETE;", TestName = "A constant of the catalog")]
|
||||
[TestCase("public bool Find(out string icon) => LucideIconRegistry.TryGet(\"house\", out icon);", TestName = "The registry plugins name their icons with")]
|
||||
[TestCase("public string Icon => \"lucide/x\";", TestName = "A plain string")]
|
||||
public async Task EverythingElseIsAllowed(string member)
|
||||
{
|
||||
var reported = await AnalyzeAsync(member);
|
||||
|
||||
Assert.That(reported, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TheCatalogMayUseBothIconSets()
|
||||
{
|
||||
//
|
||||
// The snippet declares its own AppIcons where the analyzer looks for it. It takes the place of the one
|
||||
// of the app, which the compilation references as well:
|
||||
//
|
||||
const string SOURCE =
|
||||
"""
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public static class AppIcons
|
||||
{
|
||||
public const string HOME = LucideIcons.HOUSE;
|
||||
|
||||
public const string GITHUB = MudBlazor.Icons.Custom.Brands.GitHub;
|
||||
|
||||
public static class Steps
|
||||
{
|
||||
public const string FIRST = MudBlazor.Icons.Material.Filled.LooksOne;
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var reported = await CompilationHarness.AnalyzeAsync(SOURCE, new IconCatalogAnalyzer());
|
||||
|
||||
Assert.That(reported, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GeneratedCodeIsCheckedAsWell()
|
||||
{
|
||||
//
|
||||
// The Razor compiler turns the markup of a component into generated code, where most icons are set:
|
||||
//
|
||||
const string SOURCE =
|
||||
"""
|
||||
// <auto-generated/>
|
||||
namespace Sample;
|
||||
|
||||
public sealed class Component
|
||||
{
|
||||
public string Icon => AIStudio.Tools.LucideIcons.HOUSE;
|
||||
}
|
||||
""";
|
||||
|
||||
var reported = await CompilationHarness.AnalyzeAsync(SOURCE, new IconCatalogAnalyzer());
|
||||
|
||||
Assert.That(reported, Has.Count.EqualTo(1));
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<Diagnostic>> AnalyzeAsync(string member)
|
||||
{
|
||||
var source =
|
||||
$$"""
|
||||
using AIStudio.Tools;
|
||||
using MudBlazor;
|
||||
|
||||
namespace Sample;
|
||||
|
||||
public sealed class Component
|
||||
{
|
||||
{{member}}
|
||||
}
|
||||
""";
|
||||
|
||||
return await CompilationHarness.AnalyzeAsync(source, new IconCatalogAnalyzer());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System.Reflection;
|
||||
|
||||
using AIStudio.Tools;
|
||||
|
||||
namespace AIStudio.Tests.Icons;
|
||||
|
||||
/// <summary>
|
||||
/// Checks that every icon of the app is a glyph of the Lucide font, apart from the few that keep an icon of MudBlazor.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// AppIcons is the one place where the app picks its icons. A constant that takes its value from anywhere
|
||||
/// but LucideIcons would bring back an icon of another set, which looks out of place next to all the others.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class AppIconsTests
|
||||
{
|
||||
/// <summary>
|
||||
/// The icons that keep the SVG of MudBlazor: brand logos keep their own marks, and the numbered steps keep
|
||||
/// their numbers, because Lucide has no numbered icons.
|
||||
/// </summary>
|
||||
private static readonly HashSet<string> KEPT_MUDBLAZOR_ICONS =
|
||||
[
|
||||
nameof(AppIcons.GITHUB),
|
||||
nameof(AppIcons.STEP_1),
|
||||
nameof(AppIcons.STEP_2),
|
||||
nameof(AppIcons.STEP_3),
|
||||
nameof(AppIcons.STEP_4),
|
||||
nameof(AppIcons.STEP_5),
|
||||
nameof(AppIcons.STEP_6)
|
||||
];
|
||||
|
||||
[Test]
|
||||
public void EveryIconIsALucideGlyphOrAKeptMudBlazorIcon()
|
||||
{
|
||||
var lucideGlyphs = Constants(typeof(LucideIcons)).Values.ToHashSet(StringComparer.Ordinal);
|
||||
var foreign = Constants(typeof(AppIcons))
|
||||
.Where(icon => !KEPT_MUDBLAZOR_ICONS.Contains(icon.Key) && !lucideGlyphs.Contains(icon.Value))
|
||||
.Select(icon => icon.Key)
|
||||
.ToList();
|
||||
|
||||
Assert.That(foreign, Is.Empty, "These icons are no glyph of the Lucide font.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EveryKeptMudBlazorIconStaysAnSvg()
|
||||
{
|
||||
var icons = Constants(typeof(AppIcons));
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
foreach (var icon in KEPT_MUDBLAZOR_ICONS)
|
||||
Assert.That(icons[icon], Does.StartWith("<"), $"{icon} has to stay an SVG of MudBlazor.");
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> Constants(Type type) => type
|
||||
.GetFields(BindingFlags.Public | BindingFlags.Static)
|
||||
.Where(field => field.IsLiteral)
|
||||
.ToDictionary(field => field.Name, field => (string)field.GetRawConstantValue()!);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using AIStudio.Tools;
|
||||
using AIStudio.Tools.PluginSystem.Assistants.DataModel;
|
||||
|
||||
namespace AIStudio.Tests.Icons;
|
||||
|
||||
/// <summary>
|
||||
/// Checks how the icons an assistant plugin names turn into icons of the app.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Assistant plugins name a Lucide icon as "lucide:" followed by its name on lucide.dev, and an icon of MudBlazor
|
||||
/// by its C# name. Both stay public API: plugins written before Lucide keep their icons.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class AssistantPluginIconTests
|
||||
{
|
||||
[Test]
|
||||
public void ALucideNameGivesItsGlyph()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(AssistantComponentPropHelper.GetIcon("lucide:house"), Is.EqualTo(LucideIcons.HOUSE));
|
||||
Assert.That(AssistantComponentPropHelper.GetIcon("lucide:trash"), Is.EqualTo(LucideIcons.TRASH));
|
||||
Assert.That(AssistantComponentPropHelper.GetIcon("@lucide:house"), Is.EqualTo(LucideIcons.HOUSE));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnAliasOrDeprecatedLucideNameKeepsWorking()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(AssistantComponentPropHelper.GetIcon("lucide:home"), Is.EqualTo(LucideIcons.HOUSE));
|
||||
Assert.That(AssistantComponentPropHelper.GetIcon("lucide:trash-2"), Is.EqualTo(LucideIcons.TRASH_2));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AMudBlazorNameKeepsItsIcon()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(AssistantComponentPropHelper.GetIcon("Icons.Material.Filled.Home"), Is.EqualTo(MudBlazor.Icons.Material.Filled.Home));
|
||||
Assert.That(AssistantComponentPropHelper.GetIcon("@Icons.Material.Filled.Home"), Is.EqualTo(MudBlazor.Icons.Material.Filled.Home));
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("")]
|
||||
[TestCase("lucide:")]
|
||||
[TestCase("lucide:House")]
|
||||
[TestCase("lucide: house")]
|
||||
[TestCase("lucide:no-such-icon")]
|
||||
[TestCase("Lucide:house")]
|
||||
[TestCase("house")]
|
||||
[TestCase("Icons.Material.Filled.NoSuchIcon")]
|
||||
public void AnUnknownNameGivesNoIcon(string name) => Assert.That(AssistantComponentPropHelper.GetIcon(name), Is.Empty);
|
||||
|
||||
[Test]
|
||||
public void TheIconOfASwitchFollowsTheSameNames()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(new AssistantSwitch { Icon = "lucide:house" }.GetIcon(), Is.EqualTo(LucideIcons.HOUSE));
|
||||
Assert.That(new AssistantSwitch { Icon = "Icons.Material.Filled.Home" }.GetIcon(), Is.EqualTo(MudBlazor.Icons.Material.Filled.Home));
|
||||
Assert.That(new AssistantSwitch { Icon = "lucide:no-such-icon" }.GetIcon(), Is.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
using AIStudio.Tests.Models.Generation;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
using SourceGeneratedMappings;
|
||||
|
||||
namespace AIStudio.Tests.Icons;
|
||||
|
||||
/// <summary>
|
||||
/// Checks that the code points of the Lucide font become icon strings which MudBlazor shows as font icons.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The generator runs against code points written for the purpose. Whether it also ran while the app was
|
||||
/// built, against the file the app ships, is asked in LucideIconsTests.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class LucideIconGeneratorTests
|
||||
{
|
||||
private const string CODEPOINTS_PATH = "/repository/app/SourceGeneratedMappings/Lucide/codepoints.json";
|
||||
|
||||
/// <summary>
|
||||
/// Three names in the wrong order, two of which share their glyph, as aliases do in the real file.
|
||||
/// </summary>
|
||||
private const string THREE_ICONS =
|
||||
"""
|
||||
{
|
||||
"trash-2": 57742,
|
||||
"house": 57589,
|
||||
"home": 57589
|
||||
}
|
||||
""";
|
||||
|
||||
[Test]
|
||||
public void EveryNameBecomesAConstantHoldingItsGlyph()
|
||||
{
|
||||
Generate(THREE_ICONS, CODEPOINTS_PATH, out var updated, out _);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(ConstantOf(updated, "TRASH_2"), Is.EqualTo(FontIcon(0xE18E)));
|
||||
Assert.That(ConstantOf(updated, "HOUSE"), Is.EqualTo(FontIcon(0xE0F5)));
|
||||
Assert.That(ConstantOf(updated, "HOME"), Is.EqualTo(FontIcon(0xE0F5)), "An alias is a name of its own.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnAliasNamesTheIconsItSharesItsGlyphWith()
|
||||
{
|
||||
var generated = Generate(THREE_ICONS, CODEPOINTS_PATH, out _, out _);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(generated["LucideIcons.g.cs"], Does.Contain("The Lucide icon \"home\", the same glyph as \"house\"."));
|
||||
Assert.That(generated["LucideIcons.g.cs"], Does.Contain("The Lucide icon \"trash-2\"."));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheRegistryKnowsEveryNameAliasesIncluded()
|
||||
{
|
||||
var generated = Generate(THREE_ICONS, CODEPOINTS_PATH, out _, out _);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(generated["LucideIconRegistry.g.cs"], Does.Contain("[\"trash-2\"] = LucideIcons.TRASH_2,"));
|
||||
Assert.That(generated["LucideIconRegistry.g.cs"], Does.Contain("[\"house\"] = LucideIcons.HOUSE,"));
|
||||
Assert.That(generated["LucideIconRegistry.g.cs"], Does.Contain("[\"home\"] = LucideIcons.HOME,"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheIconsAreWrittenInTheSameOrderEveryTime()
|
||||
{
|
||||
//
|
||||
// The order of the file is not something the generated code may depend on: the same icons have
|
||||
// to produce the same bytes, or an update of the font shows up as a reshuffled class.
|
||||
//
|
||||
var icons = Generate(THREE_ICONS, CODEPOINTS_PATH, out _, out _)["LucideIcons.g.cs"];
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(icons.IndexOf(" HOME = ", StringComparison.Ordinal), Is.LessThan(icons.IndexOf(" HOUSE = ", StringComparison.Ordinal)));
|
||||
Assert.That(icons.IndexOf(" HOUSE = ", StringComparison.Ordinal), Is.LessThan(icons.IndexOf(" TRASH_2 = ", StringComparison.Ordinal)));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhatIsGeneratedCompiles()
|
||||
{
|
||||
Generate(THREE_ICONS, CODEPOINTS_PATH, out var updated, out _);
|
||||
|
||||
Assert.That(CompilationHarness.ErrorsOf(updated), Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WithoutTheFileNothingIsGeneratedAndTheReasonIsReported()
|
||||
{
|
||||
var generated = Generate(null, CODEPOINTS_PATH, out _, out var diagnostics);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(generated, Is.Empty);
|
||||
Assert.That(diagnostics.Select(diagnostic => diagnostic.Id), Is.EqualTo(new[] { "LUC001" }));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AFileOfTheSameNameInAnotherFolderIsNotTaken()
|
||||
{
|
||||
var generated = Generate(THREE_ICONS, "/repository/app/SomethingElse/codepoints.json", out _, out var diagnostics);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(generated, Is.Empty);
|
||||
Assert.That(diagnostics.Select(diagnostic => diagnostic.Id), Is.EqualTo(new[] { "LUC001" }));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AFileWithoutIconsIsReported()
|
||||
{
|
||||
var generated = Generate("{}", CODEPOINTS_PATH, out _, out var diagnostics);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(generated, Is.Empty);
|
||||
Assert.That(diagnostics.Select(diagnostic => diagnostic.Id), Is.EqualTo(new[] { "LUC002" }));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnEntryWhichCannotBeAnIconIsReportedRatherThanSkippedQuietly()
|
||||
{
|
||||
const string CODEPOINTS =
|
||||
"""
|
||||
{
|
||||
"Not A Name": 57400,
|
||||
"3d-box": 57401,
|
||||
"plain-letter": 65,
|
||||
"trash-2": 57742
|
||||
}
|
||||
""";
|
||||
|
||||
var generated = Generate(CODEPOINTS, CODEPOINTS_PATH, out var updated, out var diagnostics);
|
||||
var skipped = diagnostics.Where(diagnostic => diagnostic.Id is "LUC003").Select(diagnostic => diagnostic.GetMessage()).ToList();
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(skipped, Has.Count.EqualTo(3));
|
||||
Assert.That(skipped, Has.Some.Contains("'Not A Name'"));
|
||||
Assert.That(skipped, Has.Some.Contains("'3d-box'"));
|
||||
Assert.That(skipped, Has.Some.Contains("'plain-letter'").And.Contains("private use"));
|
||||
Assert.That(ConstantOf(updated, "TRASH_2"), Is.EqualTo(FontIcon(0xE18E)), "The valid entries still count.");
|
||||
Assert.That(generated["LucideIcons.g.cs"], Does.Not.Contain("PLAIN_LETTER"));
|
||||
}
|
||||
}
|
||||
|
||||
private static string FontIcon(int codepoint) => "lucide/" + char.ConvertFromUtf32(codepoint);
|
||||
|
||||
//
|
||||
// Asked of the snippet's own assembly: the app assembly, which the snippet references, has a class of
|
||||
// the same name, generated from the real file.
|
||||
//
|
||||
private static object? ConstantOf(Compilation compilation, string name) => compilation.Assembly
|
||||
.GetTypeByMetadataName("AIStudio.Tools.LucideIcons")!
|
||||
.GetMembers(name)
|
||||
.OfType<IFieldSymbol>()
|
||||
.Single()
|
||||
.ConstantValue;
|
||||
|
||||
/// <summary>
|
||||
/// Runs the generator, with the code points as the only additional file, or without any file at all.
|
||||
/// </summary>
|
||||
/// <returns>The generated sources by their hint names.</returns>
|
||||
private static IReadOnlyDictionary<string, string> Generate(string? codepoints, string path, out Compilation updated, out ImmutableArray<Diagnostic> diagnostics)
|
||||
{
|
||||
var compilation = CompilationHarness.Compile("namespace Sample;\n\npublic sealed class NothingToDoWithIcons;");
|
||||
AdditionalText[] additionalTexts = codepoints is null ? [] : [new InMemoryAdditionalText(path, codepoints)];
|
||||
|
||||
var driver = CSharpGeneratorDriver.Create([new LucideIconGenerator().AsSourceGenerator()], additionalTexts);
|
||||
var afterwards = driver.RunGeneratorsAndUpdateCompilation(compilation, out updated, out diagnostics);
|
||||
|
||||
return afterwards.GetRunResult().Results.Single().GeneratedSources.ToDictionary(source => source.HintName, source => source.SourceText.ToString());
|
||||
}
|
||||
|
||||
private sealed class InMemoryAdditionalText(string path, string content) : AdditionalText
|
||||
{
|
||||
public override string Path => path;
|
||||
|
||||
public override SourceText GetText(CancellationToken cancellationToken = default) => SourceText.From(content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Tools;
|
||||
|
||||
namespace AIStudio.Tests.Icons;
|
||||
|
||||
/// <summary>
|
||||
/// Checks the Lucide icons the app was built with against the font it ships.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// LucideIconGeneratorTests runs the generator by hand. These tests ask whether it also ran while the app
|
||||
/// was built, against the code points in the repository, and whether the font those code points belong to
|
||||
/// ships with the app.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class LucideIconsTests
|
||||
{
|
||||
private const string FONT_ICON_PREFIX = "lucide/";
|
||||
|
||||
[Test]
|
||||
public void TheAppKnowsEveryIconOfTheShippedCodepointsUnderItsName()
|
||||
{
|
||||
var codepoints = ReadCodepoints();
|
||||
var wrong = codepoints
|
||||
.Where(entry => !LucideIconRegistry.TryGet(entry.Key, out var icon) || icon != FONT_ICON_PREFIX + char.ConvertFromUtf32(entry.Value))
|
||||
.Select(entry => entry.Key)
|
||||
.ToList();
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(codepoints, Has.Count.GreaterThan(1000), "The code points of the font were not found or not read.");
|
||||
Assert.That(wrong, Is.Empty, "These icons are missing from the app or point to another glyph.");
|
||||
Assert.That(IconConstants(), Has.Count.EqualTo(codepoints.Count), "Every name, aliases included, gets a constant of its own.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EveryIconIsAFontIconOfOneGlyph()
|
||||
{
|
||||
var malformed = IconConstants()
|
||||
.Where(icon => !IsFontIconOfOneGlyph(icon.Value))
|
||||
.Select(icon => icon.Key)
|
||||
.ToList();
|
||||
|
||||
Assert.That(malformed, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheRegistryFindsAnIconOnlyByItsExactName()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(LucideIconRegistry.TryGet("trash-2", out var trash), Is.True);
|
||||
Assert.That(trash, Is.EqualTo(LucideIcons.TRASH_2));
|
||||
|
||||
Assert.That(LucideIconRegistry.TryGet("home", out var home), Is.True, "An alias is a name of its own.");
|
||||
Assert.That(home, Is.EqualTo(LucideIcons.HOUSE));
|
||||
|
||||
Assert.That(LucideIconRegistry.TryGet("Trash-2", out _), Is.False);
|
||||
Assert.That(LucideIconRegistry.TryGet("trash 2", out _), Is.False);
|
||||
Assert.That(LucideIconRegistry.TryGet("lucide:trash-2", out _), Is.False, "The prefix of plugins is not part of the name.");
|
||||
Assert.That(LucideIconRegistry.TryGet(string.Empty, out _), Is.False);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheFontShipsWithTheAppAlongWithItsLicense()
|
||||
{
|
||||
var webRoot = Path.Combine(AppSourceDirectory(), "wwwroot");
|
||||
var css = File.ReadAllText(Path.Combine(webRoot, "app.css"));
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(File.Exists(Path.Combine(webRoot, "fonts", "lucide.woff2")), Is.True);
|
||||
Assert.That(File.Exists(Path.Combine(webRoot, "fonts", "LICENSE-lucide.txt")), Is.True);
|
||||
Assert.That(css, Does.Contain("url('fonts/lucide.woff2')"), "app.css has to declare the font.");
|
||||
Assert.That(css, Does.Contain(".mud-icon-root.lucide"), "app.css has to apply the font to the class every icon string names.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsFontIconOfOneGlyph(string icon)
|
||||
{
|
||||
if (!icon.StartsWith(FONT_ICON_PREFIX, StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
var glyph = icon[FONT_ICON_PREFIX.Length..];
|
||||
var isOneCharacter = glyph.Length is 1 && !char.IsSurrogate(glyph[0]) || glyph.Length is 2 && char.IsSurrogatePair(glyph, 0);
|
||||
|
||||
return isOneCharacter && CharUnicodeInfo.GetUnicodeCategory(glyph, 0) is UnicodeCategory.PrivateUse;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> IconConstants() => typeof(LucideIcons)
|
||||
.GetFields(BindingFlags.Public | BindingFlags.Static)
|
||||
.Where(field => field.IsLiteral)
|
||||
.ToDictionary(field => field.Name, field => (string)field.GetRawConstantValue()!);
|
||||
|
||||
private static Dictionary<string, int> ReadCodepoints()
|
||||
{
|
||||
var path = Path.Combine(AppSourceDirectory(), "..", "SourceGeneratedMappings", "Lucide", "codepoints.json");
|
||||
using var document = JsonDocument.Parse(File.ReadAllText(path));
|
||||
|
||||
return document.RootElement.EnumerateObject().ToDictionary(property => property.Name, property => property.Value.GetInt32());
|
||||
}
|
||||
|
||||
private static string AppSourceDirectory([CallerFilePath] string sourceFilePath = "") => Path.GetFullPath(Path.Combine(Path.GetDirectoryName(sourceFilePath)!, "..", "..", "MindWork AI Studio"));
|
||||
}
|
||||
Reference in new issue
Block a user