-
-
Notifications
You must be signed in to change notification settings - Fork 34
Add unit tests for link tag helpers #2197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d190eca
ec9ebb3
3c6f04c
44dd39a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,36 +5,42 @@ | |
|
|
||
| namespace TASVideos.TagHelpers; | ||
|
|
||
| public class PubLinkTagHelper : TagHelper | ||
| public class PubLinkTagHelper(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}M"); | ||
| Page = "/Publications/View"; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wyy make this change? does it change any behaviors?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My earlier comment:
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); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -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); | ||
| } | ||
| } |
| 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( | ||
YoshiRulz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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(); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this being used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes?
tasvideos/TASVideos/Pages/Shared/_DisplayMiniMovie.cshtml
Line 10 in c68804d
Do you mean the constructor parameter? It's being passed through to the base class.