Skip to content
Merged
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
76 changes: 76 additions & 0 deletions ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,82 @@ public void ChartVariants_TransitivelyPullInBlazorApexCharts(string componentNam
}
}

public class DependencyGraphTests
{
// Every dependency name declared by any registered component must itself resolve
// via ComponentRegistry.GetMetadata. Catches the class of bug where a sub-component
// template exists on disk but is never wired into the registry — the CLI then
// reports "Failed: <dep-name>" and the consumer project won't compile.
[Fact]
public void EveryDeclaredDependency_ResolvesInRegistry()
{
var missing = new List<string>();
foreach (var (name, metadata) in ComponentRegistry.Components)
{
foreach (var dep in metadata.Dependencies)
{
if (ComponentRegistry.GetMetadata(dep) is null)
missing.Add($"{name} -> {dep}");
}
}
Assert.True(missing.Count == 0,
"The following declared dependencies are not registered in ComponentRegistry:\n " +
string.Join("\n ", missing));
}

// Every installable component (IsAvailable = true) must route through the
// GetComponentContent switch. Sub-component stubs registered as IsAvailable = false
// with intentionally-empty Content are allowed — they exist only so the CLI can
// resolve them as dependency names when the parent renders their markup inline.
[Fact]
public void EveryInstallableComponent_HasContentMapping()
{
var missing = new List<string>();
foreach (var (name, metadata) in ComponentRegistry.Components)
{
if (!metadata.IsAvailable) continue;
var content = ComponentRegistry.GetComponentContent(name);
if (string.IsNullOrWhiteSpace(content))
missing.Add(name);
}
Assert.True(missing.Count == 0,
"The following installable components have no content mapping:\n " +
string.Join("\n ", missing));
}
}

public class HiddenNetworkDependencyTests
{
// ShellUI never links Google Material Symbols/Icons or Font Awesome and every
// component uses inline SVG for its chrome. When a template depends on an
// external icon font the raw icon name ("expand_more", "cloud_upload", ...)
// shows as literal text or the glyph slot renders empty on consumers.
// Fail loudly here so the SVG-swap convention stays enforced across the whole
// registry — the specific classes below cover both Material families and
// Font Awesome's solid/regular/brands.
[Theory]
[InlineData("material-symbols-outlined")] // Material Symbols (newer variable font)
[InlineData("material-symbols-rounded")]
[InlineData("material-symbols-sharp")]
[InlineData("material-icons")] // Material Icons (older static font)
[InlineData("fa-solid")] // Font Awesome v6+
[InlineData("fa-regular")]
[InlineData("fa-brands")]
public void NoTemplate_DependsOnExternalIconFont(string cssClass)
{
var offenders = new List<string>();
foreach (var (name, _) in ComponentRegistry.Components)
{
var content = ComponentRegistry.GetComponentContent(name);
if (content is not null && content.Contains(cssClass))
offenders.Add(name);
}
Assert.True(offenders.Count == 0,
$"The following templates reference `{cssClass}` (external icon font, not shipped by ShellUI):\n " +
string.Join("\n ", offenders));
}
}

public class DataTableTemplateContentTests
{
// The library-wide convention is `Components.Models` for model namespaces regardless
Expand Down
40 changes: 40 additions & 0 deletions ShellUI.Tests/TemplateCompileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public void CsharpTemplate_GeneratedContentParses(string componentName)
[InlineData("dashboard-02")]
[InlineData("button")]
[InlineData("dialog")]
[InlineData("tabs")]
[InlineData("select")]
public void RazorTemplate_CodeBlockParses(string componentName)
{
var content = ComponentRegistry.GetComponentContent(componentName);
Expand Down Expand Up @@ -76,6 +78,44 @@ public void RazorTemplate_CodeBlockParses(string componentName)
string.Join("\n", errors.Select(e => $" {e.Location.GetLineSpan().StartLinePosition}: {e.GetMessage()}")));
}

/// Exhaustive sweep: parse the @code block of every installable .razor template
/// in the registry. This is the safety net for the class of bugs where a template
/// ships with an unescaped quote inside its verbatim string (Tabs, PieChart, etc.).
/// Kept separate from the targeted Theory above so a regression's failure point
/// lists just the offending component(s) rather than aborting on the first one.
[Fact]
public void EveryRazorTemplate_CodeBlockParses()
{
var failures = new List<string>();
foreach (var (name, metadata) in ComponentRegistry.Components)
{
if (!metadata.IsAvailable) continue;
if (!metadata.FilePath.EndsWith(".razor", System.StringComparison.OrdinalIgnoreCase)) continue;

var content = ComponentRegistry.GetComponentContent(name);
if (string.IsNullOrWhiteSpace(content)) continue;
if (!content.Contains("@code")) continue;

var codeBlock = ExtractCodeBlock(content!);
if (string.IsNullOrWhiteSpace(codeBlock))
{
failures.Add($"{name}: @code block could not be extracted (likely unterminated string literal)");
continue;
}

var wrapped = $"class __Probe {{ {codeBlock} }}";
var errors = CSharpSyntaxTree.ParseText(wrapped).GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error)
.Select(d => $"{d.Location.GetLineSpan().StartLinePosition}: {d.GetMessage()}")
.ToList();
if (errors.Count > 0)
failures.Add($"{name}: {errors.Count} parse error(s) — {errors[0]}");
}
Assert.True(failures.Count == 0,
"The following templates have parse errors in their @code block:\n " +
string.Join("\n ", failures));
}

/// Strips Razor markup directives so the remaining text can be best-effort
/// parsed as C#. Not a real Razor parser — just enough to surface useful
/// diagnostics when ExtractCodeBlock fails.
Expand Down
2 changes: 1 addition & 1 deletion src/ShellUI.Components/Components/SidebarInset.razor
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@namespace ShellUI.Components

<main class="@Shell.Cn(
"relative flex min-h-svh flex-1 flex-col bg-background",
"relative flex min-h-svh min-w-0 flex-1 flex-col bg-background",
"peer-data-[variant=inset]:min-h-[calc(100svh-1rem)] md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
Class)"
@attributes="AdditionalAttributes">
Expand Down
4 changes: 4 additions & 0 deletions src/ShellUI.Templates/ComponentRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public static class ComponentRegistry
{ "slider", SliderTemplate.Metadata },
{ "toggle", ToggleTemplate.Metadata },
{ "accordion", AccordionTemplate.Metadata },
{ "accordion-type", AccordionTypeTemplate.Metadata },
{ "accordion-item", AccordionItemTemplate.Metadata },
{ "accordion-trigger", AccordionTriggerTemplate.Metadata },
{ "accordion-content", AccordionContentTemplate.Metadata },
Expand Down Expand Up @@ -150,6 +151,7 @@ public static class ComponentRegistry
{ "context-menu-trigger", ContextMenuTriggerTemplate.Metadata },
{ "context-menu-content", ContextMenuContentTemplate.Metadata },
{ "context-menu-option", ContextMenuOptionTemplate.Metadata },
{ "context-menu-models", ContextMenuModelsTemplate.Metadata },
{ "empty-state", EmptyStateTemplate.Metadata },
{ "stepper", StepperTemplate.Metadata },
{ "stepper-list", StepperListTemplate.Metadata },
Expand Down Expand Up @@ -258,6 +260,7 @@ public static class ComponentRegistry
"slider" => SliderTemplate.Content,
"toggle" => ToggleTemplate.Content,
"accordion" => AccordionTemplate.Content,
"accordion-type" => AccordionTypeTemplate.Content,
"accordion-item" => AccordionItemTemplate.Content,
"accordion-trigger" => AccordionTriggerTemplate.Content,
"accordion-content" => AccordionContentTemplate.Content,
Expand Down Expand Up @@ -327,6 +330,7 @@ public static class ComponentRegistry
"context-menu-trigger" => ContextMenuTriggerTemplate.Content,
"context-menu-content" => ContextMenuContentTemplate.Content,
"context-menu-option" => ContextMenuOptionTemplate.Content,
"context-menu-models" => ContextMenuModelsTemplate.Content,
"empty-state" => EmptyStateTemplate.Content,
"stepper" => StepperTemplate.Content,
"stepper-list" => StepperListTemplate.Content,
Expand Down
30 changes: 30 additions & 0 deletions src/ShellUI.Templates/Templates/ContextMenuModelsTemplate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using ShellUI.Core.Models;

namespace ShellUI.Templates.Templates;

public static class ContextMenuModelsTemplate
{
public static ComponentMetadata Metadata => new()
{
Name = "context-menu-models",
DisplayName = "ContextMenu Models",
Description = "Models for ContextMenu component",
Category = ComponentCategory.Navigation,
FilePath = "Models/ContextMenuModels.cs",
IsAvailable = false,
Dependencies = new List<string>()
};

public static string Content => @"namespace YourProjectNamespace.Components.Models;

public class ContextMenuItem
{
public string Label { get; set; } = string.Empty;
public string? Icon { get; set; }
public string? Shortcut { get; set; }
public bool Disabled { get; set; }
public bool IsSeparator { get; set; }
public object? Data { get; set; }
}
";
}
2 changes: 1 addition & 1 deletion src/ShellUI.Templates/Templates/ContextMenuTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public static class ContextMenuTemplate
Category = ComponentCategory.Navigation,
FilePath = "ContextMenu.razor",
Tags = new List<string> { "menu", "context", "right-click", "dropdown" },
Dependencies = new List<string>()
Dependencies = new List<string> { "context-menu-models" }
};

public const string Content = """
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ private void BuildBreadcrumb()
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);

_breadcrumbItems.Clear();
if (segments.Length == 0 || (segments.Length == 1 && segments[0] == ""))
if (segments.Length == 0 || (segments.Length == 1 && segments[0] == """"))
{
_breadcrumbItems.Add(new BreadcrumbItemData { Label = ""Home"", Href = ""/"", IsLast = true });
return;
Expand Down
8 changes: 7 additions & 1 deletion src/ShellUI.Templates/Templates/FileUploadTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ @using Microsoft.JSInterop
@ondragover:preventDefault=""true""
@ondrop:preventDefault=""true"">
<div class=""text-center pointer-events-none"">
<span class=""material-symbols-outlined mx-auto text-5xl text-muted-foreground block"">cloud_upload</span>
<svg class=""mx-auto h-12 w-12 text-muted-foreground block""
xmlns=""http://www.w3.org/2000/svg"" fill=""none"" viewBox=""0 0 24 24"" stroke=""currentColor"">
<path stroke-linecap=""round"" stroke-linejoin=""round"" stroke-width=""1.5""
d=""M7 16a4 4 0 01-.88-7.9 5 5 0 019.9-1A5.5 5.5 0 0118.5 16H7z"" />
<path stroke-linecap=""round"" stroke-linejoin=""round"" stroke-width=""1.5""
d=""M12 12v6m0-6l-2 2m2-2l2 2"" />
</svg>
<div class=""mt-4"">
<p class=""text-sm text-foreground font-medium"">@Text</p>
@if (Multiple && AllowedFileCount > 1)
Expand Down
5 changes: 4 additions & 1 deletion src/ShellUI.Templates/Templates/SelectTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ public static class SelectTemplate
@attributes=""AdditionalAttributes"">
@ChildContent
</select>
<span class=""material-symbols-outlined absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none"">expand_more</span>
<svg class=""pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground""
xmlns=""http://www.w3.org/2000/svg"" fill=""none"" viewBox=""0 0 24 24"" stroke=""currentColor"">
<path stroke-linecap=""round"" stroke-linejoin=""round"" stroke-width=""2"" d=""M19 9l-7 7-7-7"" />
</svg>
</div>

@code {
Expand Down
2 changes: 1 addition & 1 deletion src/ShellUI.Templates/Templates/SidebarInsetTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public static class SidebarInsetTemplate
public static string Content => @"@namespace YourProjectNamespace.Components.UI

<main class=""@Shell.Cn(
""relative flex min-h-svh flex-1 flex-col bg-background"",
""relative flex min-h-svh min-w-0 flex-1 flex-col bg-background"",
""peer-data-[variant=inset]:min-h-[calc(100svh-1rem)] md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow"",
Class)""
@attributes=""AdditionalAttributes"">
Expand Down
2 changes: 1 addition & 1 deletion src/ShellUI.Templates/Templates/TabsTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public static class TabsTemplate
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object>? AdditionalAttributes { get; set; }

private string _effectiveValue = "";
private string _effectiveValue = """";

protected override void OnInitialized()
{
Expand Down
Loading