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
116 changes: 116 additions & 0 deletions src/Tests/Grand.Modules.Tests/Views/ViewComponentTagTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using System.Text.RegularExpressions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Grand.Modules.Tests.Views;

/// <summary>
/// Razor turns <c>&lt;vc:...&gt;</c> into a tag helper only for the view components of an assembly
/// the view registers with <c>@addTagHelper</c>. Any other <c>&lt;vc:...&gt;</c> compiles without a
/// warning and reaches the browser as an empty unknown element, so the component never runs.
/// </summary>
[TestClass]
public class ViewComponentTagTests
{
private static readonly string Src = Path.Combine(Installer.RepositoryPaths.Root, "src");
private static readonly HashSet<string> SkippedFolders = ["bin", "obj", "node_modules", "wwwroot"];

[TestMethod]
public void EveryVcTagResolvesToAComponentItsViewRegisters()
{
var components = ComponentAssemblies();
var problems = new List<string>();
var checkedTags = 0;

foreach (var view in SourceFiles(Src, "*.cshtml"))
{
var tags = Regex.Matches(File.ReadAllText(view), "<vc:([a-z0-9-]+)")
.Select(m => m.Groups[1].Value).Distinct().ToList();
if (tags.Count == 0) continue;

var project = ProjectOf(view);
var registered = RegisteredAssemblies(view, project.Dir);
foreach (var tag in tags)
{
checkedTags++;
if (!components.TryGetValue(tag, out var assemblies))
problems.Add($"{Path.GetRelativePath(Src, view)}: <vc:{tag}> matches no view component");
else if (!assemblies.Overlaps(registered))
problems.Add($"{Path.GetRelativePath(Src, view)}: <vc:{tag}> lives in {string.Join(", ", assemblies)}, " +
$"which {project.Name} does not register with @addTagHelper");
}
}

Assert.IsTrue(checkedTags > 0, "No <vc:...> tag found, so the scan itself is broken");
Assert.AreEqual(0, problems.Count, Environment.NewLine + string.Join(Environment.NewLine, problems));
}

[TestMethod]
public void ComponentScan_FindsWidgetComponentsOfEveryPanel()
{
var components = ComponentAssemblies();

CollectionAssert.AreEquivalent(new[] { "Grand.Web.Common" }, components["widget"].ToList());
CollectionAssert.AreEquivalent(new[] { "Grand.Web.Admin" }, components["admin-widget"].ToList());
CollectionAssert.AreEquivalent(new[] { "Grand.Web.Store" }, components["store-widget"].ToList());
CollectionAssert.AreEquivalent(new[] { "Grand.Web.Vendor" }, components["vendor-widget"].ToList());
CollectionAssert.AreEquivalent(new[] { "Grand.Web" }, components["related-products"].ToList());
}

//tag name -> assemblies that declare a view component with that name
private static Dictionary<string, HashSet<string>> ComponentAssemblies()
{
var declaration = new Regex(
@"(?:\[ViewComponent\(Name\s*=\s*""(?<name>[^""]+)""\)\]\s*)?public\s+(?:sealed\s+)?class\s+(?<class>\w+)(?:\s*:\s*(?<base>\w+))?");
var components = new Dictionary<string, HashSet<string>>();
foreach (var file in SourceFiles(Src, "*.cs").Where(f => !f.Contains($"{Path.DirectorySeparatorChar}Tests{Path.DirectorySeparatorChar}")))
foreach (Match m in declaration.Matches(File.ReadAllText(file)))
{
var className = m.Groups["class"].Value;
var isComponent = m.Groups["name"].Success || className.EndsWith("ViewComponent") ||
m.Groups["base"].Value.EndsWith("ViewComponent");
if (!isComponent) continue;

var name = m.Groups["name"].Success ? m.Groups["name"].Value : className[..^"ViewComponent".Length];
var tag = Regex.Replace(name, "(?<=[a-z0-9])(?=[A-Z])", "-").ToLowerInvariant();
if (!components.TryGetValue(tag, out var assemblies))
components[tag] = assemblies = [];
assemblies.Add(ProjectOf(file).Name);
}

return components;
}

private static HashSet<string> RegisteredAssemblies(string view, string projectDir)
{
var assemblies = new HashSet<string>();
for (var dir = Path.GetDirectoryName(view); dir != null && dir.Length >= projectDir.Length; dir = Path.GetDirectoryName(dir))
{
var imports = Path.Combine(dir, "_ViewImports.cshtml");
if (!File.Exists(imports)) continue;
foreach (Match m in Regex.Matches(File.ReadAllText(imports), @"@addTagHelper\s+\*\s*,\s*([\w.]+)"))
assemblies.Add(m.Groups[1].Value);
}

return assemblies;
}

private static (string Dir, string Name) ProjectOf(string file)
{
for (var dir = Path.GetDirectoryName(file); dir != null; dir = Path.GetDirectoryName(dir))
{
var project = Directory.GetFiles(dir, "*.csproj").FirstOrDefault();
if (project != null) return (dir, Path.GetFileNameWithoutExtension(project));
}

throw new InvalidOperationException($"{file} belongs to no project");
}

private static IEnumerable<string> SourceFiles(string dir, string pattern)
{
foreach (var file in Directory.GetFiles(dir, pattern))
yield return file;
foreach (var sub in Directory.GetDirectories(dir).Where(d => !SkippedFolders.Contains(Path.GetFileName(d))))
foreach (var file in SourceFiles(sub, pattern))
yield return file;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
store's own) and no export or import. The ids the script below looks up are unchanged. *@
<admin-page title="@Loc["Admin.Catalog.Categories.Manage"]" icon="bi-diagram-3">
<page-actions>
<vc:admin-widget widget-zone="category_list_buttons" additional-data="null"/>
<vc:store-widget widget-zone="category_list_buttons" additional-data="null"/>
<a href="@Url.Action("Create", new { area = Constants.AreaStore })" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> @Loc["Admin.Common.AddNew"]
</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
it is shown. The field order is the one this tab always had, except the picture, last
because its upload box is the tallest control. The ids the script above shows and hides -
pnlSearchBoxOrder, pnlPageSize, pnlPageSizeOptions - sit on the same fields as before. *@
<vc:admin-widget widget-zone="category_details_info_top" additional-data="Model"/>
<vc:store-widget widget-zone="category_details_info_top" additional-data="Model"/>
<admin-card>
<localized-editor localized-template="@template" name="category-info-localized" language-ids="@Model.Locales.Select(c => c.LanguageId).ToList()">
<div class="grand-fields">
Expand Down Expand Up @@ -110,4 +110,4 @@
<admin-field asp-for="ExternalId"/>
</div>
</admin-card>
<vc:admin-widget widget-zone="category_details_info_bottom" additional-data="Model"/>
<vc:store-widget widget-zone="category_details_info_bottom" additional-data="Model"/>
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<input id="active-menu-item" type="hidden" value="/Store"/>
<admin-page title="@Loc["Admin.AccessDenied.Title"]" icon="bi-shield-lock">
<vc:vendor-widget widget-zone="store_security_access_denied_top" additional-data="null"/>
<vc:store-widget widget-zone="store_security_access_denied_top" additional-data="null"/>
@* the panel's empty state: nothing here the account may see *@
<admin-card>
<div class="grand-empty">
<i class="bi bi-shield-lock grand-empty__icon"></i>
<p class="mb-0">@Loc["Admin.AccessDenied.Description"]</p>
</div>
</admin-card>
<vc:vendor-widget widget-zone="store_security_access_denied_bottom" additional-data="null"/>
<vc:store-widget widget-zone="store_security_access_denied_bottom" additional-data="null"/>
</admin-page>
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<form asp-area="@Constants.AreaStore" asp-controller="Product" asp-action="List" method="post">
<admin-page title="@Loc["Admin.Catalog.Products.Manage"]" icon="bi-boxes">
<page-actions>
<vc:admin-widget widget-zone="product_list_buttons" additional-data="null"/>
<vc:store-widget widget-zone="product_list_buttons" additional-data="null"/>
<a href="@Url.Action("Create", new { area = Constants.AreaStore })" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> @Loc["Admin.Common.AddNew"]
</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@
</admin-field>

<div class="grand-popup__actions">
<vc:admin-widget widget-zone="product_details_attribute_combination_buttons" additional-data="Model"/>
<vc:store-widget widget-zone="product_details_attribute_combination_buttons" additional-data="Model"/>
<button class="btn btn-primary" type="submit" name="save">
<i class="bi bi-check-lg"></i> @Loc["Admin.Common.Save"]
</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@model SalesSettingsModel

<vc:admin-widget widget-zone="loyalty_points_settings_top" additional-data="null"/>
<vc:store-widget widget-zone="loyalty_points_settings_top" additional-data="null"/>
@* the program and what a point is worth, then how points are earned; the rate and the purchase
rule read as the sentences they are, with the fields inside them *@
<admin-card>
Expand Down Expand Up @@ -38,4 +38,4 @@
<admin-field asp-for="LoyaltyPointsSettings.DisplayHowMuchWillBeEarned"/>
</div>
</admin-card>
<vc:admin-widget widget-zone="loyalty_points_settings_bottom" additional-data="null"/>
<vc:store-widget widget-zone="loyalty_points_settings_bottom" additional-data="null"/>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@model SalesSettingsModel

<vc:admin-widget widget-zone="shopping_cart_settings_top" additional-data="null"/>
<vc:store-widget widget-zone="shopping_cart_settings_top" additional-data="null"/>
@* what adding to the cart or wishlist does and how many items they hold, what the cart shows,
the wishlist e-mail and the mini cart with what they reveal, then the cart's behaviour *@
<admin-card>
Expand Down Expand Up @@ -50,7 +50,7 @@
<admin-field asp-for="ShoppingCartSettings.ReservationDateFormat" size="md"/>
</div>
</admin-card>
<vc:admin-widget widget-zone="shopping_cart_settings_bottom" additional-data="null"/>
<vc:store-widget widget-zone="shopping_cart_settings_bottom" additional-data="null"/>

<script>
$(document).ready(function () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
Value = contextAccessor.WorkContext.StoreManager.Id
});
}
<vc:admin-widget widget-zone="userfield_details_info_top" additional-data="Model"/>
<vc:store-widget widget-zone="userfield_details_info_top" additional-data="Model"/>
@* one card: the add form on top, the fields under it - addUserFields() appends a new one to
#form-userfield, so it lands right below the form; every row keeps .form-body, which
deleteUserFields() renumbers by, and one text box, one hidden key and one select *@
Expand Down Expand Up @@ -133,4 +133,4 @@
});
}
</script>
<vc:admin-widget widget-zone="userfield_details_info_bottom" additional-data="Model"/>
<vc:store-widget widget-zone="userfield_details_info_bottom" additional-data="Model"/>
Loading