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
20 changes: 13 additions & 7 deletions TASVideos/TagHelpers/WikiLinkTagHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,42 @@

namespace TASVideos.TagHelpers;

public class PubLinkTagHelper : TagHelper
public class PubLinkTagHelper(IHtmlGenerator generator) : AnchorTagHelper(generator)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this being used?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes?


Do you mean the constructor parameter? It's being passed through to the base class.

{
public int Id { get; set; }

public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a";
output.Attributes.Add("href", $"/{Id}M");
Page = "/Publications/View";
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wyy make this change? does it change any behaviors?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My earlier comment:

The idea was to use the same mechanism as <a asp-page/>. After this PR I wanted to add more link helpers, but existing calls mostly use that rather than <a href/>. Also, I'm hoping that <a asp-page/> would help with tracking page links (for generating referrer lists).

The unit tests should catch regressions, but I also did a bit of manual testing.

RouteValues.Add(nameof(Pages.Publications.ViewModel.Id), Id.ToString());
base.Process(context, output);
}
}

public class SubLinkTagHelper : TagHelper
public class SubLinkTagHelper(IHtmlGenerator generator) : AnchorTagHelper(generator)
{
public int Id { get; set; }

public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a";
output.Attributes.Add("href", $"/{Id}S");
Page = "/Submissions/View";
RouteValues.Add(nameof(Pages.Submissions.ViewModel.Id), Id.ToString());
base.Process(context, output);
}
}

public class GameLinkTagHelper : TagHelper
public class GameLinkTagHelper(IHtmlGenerator generator) : AnchorTagHelper(generator)
{
public int Id { get; set; }

public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a";
output.Attributes.Add("href", $"/{Id}G");
Page = "/Games/Index";
RouteValues.Add(nameof(Pages.Games.IndexModel.Id), Id.ToString());
base.Process(context, output);
}
}

Expand Down Expand Up @@ -85,7 +91,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu

output.TagName = "a";
Page = "/Users/Profile";
RouteValues.Add("UserName", Username);
RouteValues.Add(nameof(Pages.Users.ProfileModel.UserName), Username);
await base.ProcessAsync(context, output);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using TASVideos.Extensions;
using TASVideos.TagHelpers;

namespace TASVideos.RazorPages.Tests.TagHelpers;

[TestClass]
public sealed class GameLinkTagHelperTests : LinkTagHelperTestsBase
{
[DataRow(123, "some game", """<a href="/123G">some game</a>""")]
[TestMethod]
public async Task TestGameLinkHelper(int id, string label, string expected)
{
var generator = TestableHtmlGenerator.Create(out var viewCtx, ServiceCollectionExtensions.Aliases.First(kvp => kvp.Key is "/Games/Index"));
GameLinkTagHelper gameLinkHelper = new(generator) { Id = id, ViewContext = viewCtx };
var output = GetOutputObj(contentsUnencoded: label, tagName: "game-link");
await gameLinkHelper.ProcessAsync(GetHelperContext(), output);
Assert.AreEqual(expected, GetHtmlString(output));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Collections.Immutable;

using Microsoft.AspNetCore.Razor.TagHelpers;

namespace TASVideos.RazorPages.Tests.TagHelpers;

public abstract class LinkTagHelperTestsBase
{
public static TagHelperContext GetHelperContext()
=> new(
[],
ImmutableDictionary<object, object>.Empty,
Guid.NewGuid().ToString("N"));

public static string GetHtmlString(TagHelperOutput output)
{
using var writer = new StringWriter();
output.WriteTo(writer, NullHtmlEncoder.Default);
return writer.ToString();
}

public static TagHelperOutput GetOutputObj(string contentsUnencoded, string tagName = "")
{
TagHelperOutput output = new(
tagName,
[],
(_, _) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
output.Content.SetContent(contentsUnencoded);
return output;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using TASVideos.Extensions;
using TASVideos.TagHelpers;

namespace TASVideos.RazorPages.Tests.TagHelpers;

[TestClass]
public class MovieLinkTagHelperTests : LinkTagHelperTestsBase
{
[DataRow(1234, "some movie", """<a href="/1234M">some movie</a>""")]
[TestMethod]
public async Task TestPubLinkHelper(int id, string label, string expected)
{
var generator = TestableHtmlGenerator.Create(out var viewCtx, ServiceCollectionExtensions.Aliases.First(kvp => kvp.Key is "/Publications/View"));
PubLinkTagHelper pubLinkHelper = new(generator) { Id = id, ViewContext = viewCtx };
var output = GetOutputObj(contentsUnencoded: label, tagName: "pub-link");
await pubLinkHelper.ProcessAsync(GetHelperContext(), output);
Assert.AreEqual(expected, GetHtmlString(output));
}

[DataRow(1234, "some movie", """<a href="/1234S">some movie</a>""")]
[TestMethod]
public async Task TestSubLinkHelper(int id, string label, string expected)
{
var generator = TestableHtmlGenerator.Create(out var viewCtx, ServiceCollectionExtensions.Aliases.First(kvp => kvp.Key is "/Submissions/View"));
SubLinkTagHelper subLinkHelper = new(generator) { Id = id, ViewContext = viewCtx };
var output = GetOutputObj(contentsUnencoded: label, tagName: "sub-link");
await subLinkHelper.ProcessAsync(GetHelperContext(), output);
Assert.AreEqual(expected, GetHtmlString(output));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using TASVideos.TagHelpers;

namespace TASVideos.RazorPages.Tests.TagHelpers;

[TestClass]
public class ProfileLinkTagHelperTests : LinkTagHelperTestsBase
{
[DataRow("YoshiRulz", "unused", """<a href="/Users/Profile/YoshiRulz">YoshiRulz</a>""")]
[TestMethod]
public async Task TestProfileLinkHelper(string username, string label, string expected)
{
var generator = TestableHtmlGenerator.Create(out var viewCtx, [new("/Users/Profile", "Users/Profile/{Username}")]);
ProfileLinkTagHelper profileLinkHelper = new(generator) { Username = username, ViewContext = viewCtx };
var output = GetOutputObj(contentsUnencoded: label, tagName: "profile-link");
await profileLinkHelper.ProcessAsync(GetHelperContext(), output);
Assert.AreEqual(expected, GetHtmlString(output));
}
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,22 @@
using Microsoft.AspNetCore.Razor.TagHelpers;
using TASVideos.TagHelpers;

namespace TASVideos.RazorPages.Tests.TagHelpers;

[TestClass]
public class WikiLinkTagHelperTests
public sealed class WikiLinkTagHelperTests : LinkTagHelperTestsBase
{
[DataRow("GameResources/NES/SuperMarioBros", "unused", """<a href="/GameResources/NES/SuperMarioBros">GameResources/NES/SuperMarioBros</a>""")]
[DataRow("WelcomeToTASVideos", "unused", """<a href="/WelcomeToTASVideos">WelcomeToTASVideos</a>""")]
[TestMethod]
public void WikiLinkTagHelper_Process_RendersCorrectHtml()
public async Task WikiLinkTagHelper_Process_RendersCorrectHtml(string wikiPageName, string label, string expected)
{
var tagHelper = new WikiLinkTagHelper { PageName = "GameResources/NES/SuperMarioBros" };
var context = new TagHelperContext(
allAttributes: [],
items: new Dictionary<object, object>(),
uniqueId: "test");
var output = new TagHelperOutput(
tagName: "wiki-link",
attributes: [],
getChildContentAsync: (_, _) =>
Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
var tagHelper = new WikiLinkTagHelper { PageName = wikiPageName };
var context = GetHelperContext();
var output = GetOutputObj(contentsUnencoded: label, tagName: "wiki-link");

tagHelper.Process(context, output);
await tagHelper.ProcessAsync(context, output);

var htmlString = GetHtmlString(output);
Assert.AreEqual("<a href=\"/GameResources/NES/SuperMarioBros\">GameResources/NES/SuperMarioBros</a>", htmlString);
}

private static string GetHtmlString(TagHelperOutput output)
{
using var writer = new StringWriter();
output.WriteTo(writer, NullHtmlEncoder.Default);
return writer.ToString();
Assert.AreEqual(expected, htmlString);
}
}
134 changes: 134 additions & 0 deletions tests/TASVideos.RazorPages.Tests/TestableHtmlGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* taken from .NET 8 source, MIT-licensed
* specifically https://github.com/dotnet/aspnetcore/blob/v8.0.0/src/Mvc/Mvc.TagHelpers/test/TestableHtmlGenerator.cs
* but also I cut out most of it anyway
*/

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.WebEncoders.Testing;

namespace TASVideos.RazorPages.Tests;

// ReSharper disable EmptyConstructor
internal class TestableHtmlGenerator(
IOptions<MvcViewOptions> options,
IModelMetadataProvider metadataProvider) : DefaultHtmlGenerator(
Substitute.For<IAntiforgery>(),
options,
metadataProvider,
CreateUrlHelperFactory(),
new HtmlTestEncoder(),
new DefaultValidationHtmlAttributeProvider(options, metadataProvider, new()))
{
private static IUrlHelperFactory CreateUrlHelperFactory()
{
var urlHelperFactory = Substitute.For<IUrlHelperFactory>();
urlHelperFactory.GetUrlHelper(Arg.Any<ActionContext>())
.Returns(callInfo => new FakeUrlHelper(callInfo.ArgAt<ActionContext>(0)));
return urlHelperFactory;
}

private sealed class FakeUrlHelper(ActionContext context) : UrlHelperBase(context)
{
public override string Action(UrlActionContext actionContext)
=> throw new NotSupportedException();

public override string? RouteUrl(UrlRouteContext routeContext)
{
var virtualPath = ActionContext.RouteData.Routers[0].GetVirtualPath(new(
ActionContext.HttpContext,
AmbientValues,
GetValuesDictionary(routeContext.Values),
routeContext.RouteName))?.VirtualPath;

return GenerateUrl(
protocol: routeContext.Protocol,
host: routeContext.Host,
virtualPath: virtualPath,
fragment: routeContext.Fragment);
}
}

public static TestableHtmlGenerator Create(out ViewContext viewCtx, params KeyValuePair<string, string>[] routeStrs)
{
ServiceCollection services = [];
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();

var routeOptionsWrapper = Substitute.For<IOptions<RouteOptions>>();
routeOptionsWrapper.Value.Returns(new RouteOptions());
services.AddSingleton(routeOptionsWrapper);

// equivalent to:
// ObjectPool<UriBuildingContext> objPool = new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy());
// DefaultTemplateBinderFactory defaultFactoryImpl = new(Substitute.For<ParameterPolicyFactory>(), objPool);
var aspNetRoutingAsm = typeof(TemplateBinderFactory).Assembly;
var objPool = typeof(ObjectPoolProvider).GetMethods()
.First(mi => mi.Name is "Create" && mi.GetParameters().Length is 1)
.MakeGenericMethod(aspNetRoutingAsm.GetType("Microsoft.AspNetCore.Routing.UriBuildingContext")!)
.Invoke(new DefaultObjectPoolProvider(), parameters: [
Activator.CreateInstance(aspNetRoutingAsm.GetType("Microsoft.AspNetCore.Routing.UriBuilderContextPooledObjectPolicy")!),
]);
var defaultFactoryImpl = (TemplateBinderFactory)Activator.CreateInstance(
aspNetRoutingAsm.GetType("Microsoft.AspNetCore.Routing.Template.DefaultTemplateBinderFactory")!,
Substitute.For<ParameterPolicyFactory>(),
objPool)!;
services.AddSingleton(defaultFactoryImpl);
var serviceProvider = services.BuildServiceProvider();

RouteHandler routeHandler = new(_ => Task.CompletedTask);
DefaultInlineConstraintResolver constraintResolver = new(routeOptionsWrapper, serviceProvider);
RouteCollection router = new();
foreach (var (name, template) in routeStrs)
{
router.Add(new Route(
routeHandler,
routeName: name,
routeTemplate: template,
defaults: new([new KeyValuePair<string, string?>("page", name)]),
constraints: null,
dataTokens: null,
constraintResolver));
}

ModelStateDictionary modelState = new();
EmptyModelMetadataProvider metadataProvider = new();
viewCtx = new(
new(
new DefaultHttpContext { RequestServices = serviceProvider },
new() { Routers = { router } },
new(),
modelState),
Substitute.For<IView>(),
new(metadataProvider, modelState) { Model = null },
Substitute.For<ITempDataDictionary>(),
TextWriter.Null,
new());

IOptions<MvcViewOptions> mvcViewOptions = Substitute.For<IOptions<MvcViewOptions>>();
mvcViewOptions.Value.Returns(new MvcViewOptions());
return new(mvcViewOptions, metadataProvider);
}

protected override void AddValidationAttributes(
ViewContext viewContext,
TagBuilder tagBuilder,
ModelExplorer modelExplorer,
string expression)
=> throw new NotSupportedException();

public override TagBuilder GenerateAntiforgery(ViewContext viewContext)
=> throw new NotSupportedException();
}
Loading