diff --git a/.gitignore b/.gitignore index 2e785c9..61eefd6 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ _dotTrace* # NuGet tools .nuget/ + +**/TestResult.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 50abfea..060855a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog All notable changes to this project will be documented in this file. -## 26.09.04XX +## 26.09.19XX - "Go to Handler" now also works with the caret anywhere inside a `mediator.Send(...)` / `Publish(...)` call: the first argument's type is resolved, so there is no need to navigate to the request declaration first. diff --git a/src/dotnet/MediatorPlugin.Tests/Infrastructure/TestSolutionFiles.cs b/src/dotnet/MediatorPlugin.Tests/Infrastructure/TestSolutionFiles.cs index 7f328d6..43a4301 100644 --- a/src/dotnet/MediatorPlugin.Tests/Infrastructure/TestSolutionFiles.cs +++ b/src/dotnet/MediatorPlugin.Tests/Infrastructure/TestSolutionFiles.cs @@ -14,6 +14,13 @@ public static class TestSolutionFiles { private static readonly string TestSolutionPath = ResolveTestSolutionPath(); + /// + /// Absolute path to the TestSolution folder, used to turn an on-disk file path (e.g. a + /// resolved handler's source file) back into the relative path form used elsewhere in these + /// tests (e.g. "MediatR/Handlers/GetEntityHandler.cs"). + /// + public static string RootPath => TestSolutionPath; + public static string[] MediatR { get; } = LibraryFiles("MediatR"); public static string[] Mediator { get; } = LibraryFiles("Mediator"); diff --git a/src/dotnet/MediatorPlugin.Tests/LookupTestBase.cs b/src/dotnet/MediatorPlugin.Tests/LookupTestBase.cs index 3bcc55e..609686b 100644 --- a/src/dotnet/MediatorPlugin.Tests/LookupTestBase.cs +++ b/src/dotnet/MediatorPlugin.Tests/LookupTestBase.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Linq; using JetBrains.Lifetimes; using JetBrains.ProjectModel; @@ -9,6 +10,7 @@ using JetBrains.ReSharper.Resources.Shell; using JetBrains.ReSharper.TestFramework; using NUnit.Framework; +using Octelys.MediatorPlugin.Tests.Infrastructure; using ReSharper.MediatorPlugin.Services.Libraries; namespace Octelys.MediatorPlugin.Tests; @@ -23,6 +25,7 @@ public abstract class LookupTestBase : BaseTestWithSingleProject { private string _requestTypeName; private string[] _expectedHandlerNames; + private string[] _expectedHandlerFilePaths; protected override string RelativeTestDataPath => "TestSolution"; @@ -42,7 +45,18 @@ ISolution solution { IIdentifier requestIdentifier = FindDeclarationIdentifier(solution, _requestTypeName); - IEnumerable handlers = new LibraryAdaptor().FindHandlers(requestIdentifier); + ITypeElement[] handlers = new LibraryAdaptor().FindHandlers(requestIdentifier).ToArray(); + + if (_expectedHandlerFilePaths is not null) + { + string[] handlerFilePaths = handlers + .SelectMany(GetRelativeFilePaths) + .OrderBy(path => path) + .ToArray(); + + Assert.That(handlerFilePaths, Is.EqualTo(_expectedHandlerFilePaths.OrderBy(path => path).ToArray())); + return; + } string[] handlerNames = handlers .Select(handler => handler.ShortName) @@ -66,10 +80,49 @@ params string[] fileNames { _requestTypeName = requestTypeName; _expectedHandlerNames = expectedHandlerNames; + _expectedHandlerFilePaths = null; + + DoTestSolution(fileNames); + } + + /// + /// Loads the given files as a test solution and asserts that the request or notification + /// resolves to handlers declared in exactly the expected files (relative to + /// test/data/TestSolution, e.g. "MediatR/Handlers/GetEntityHandler.cs") - i.e. the file(s) + /// "Go to Handler" would navigate to when the caret sits on the request. The lookup itself + /// runs in . + /// + protected void AssertHandlerFilesFor + ( + string requestTypeName, + string[] expectedHandlerFilePaths, + params string[] fileNames + ) + { + _requestTypeName = requestTypeName; + _expectedHandlerNames = null; + _expectedHandlerFilePaths = expectedHandlerFilePaths; DoTestSolution(fileNames); } + /// + /// Resolves the on-disk file(s) declaring the given handler back to their path relative to + /// test/data/TestSolution, matching the relative path form used to load test files (e.g. via + /// ). + /// + private static IEnumerable GetRelativeFilePaths + ( + ITypeElement handler + ) + { + return handler + .GetSourceFiles() + .Select(sourceFile => sourceFile.GetLocation().FullPath) + .Select(fullPath => fullPath.Substring(TestSolutionFiles.RootPath.Length + 1)) + .Select(relativePath => relativePath.Replace(Path.DirectorySeparatorChar, '/')); + } + /// /// Returns the name token of the type declaration, which is the node the plugin receives /// when the caret sits on a request in the editor. may be a diff --git a/src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.V12.cs b/src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.V12.cs new file mode 100644 index 0000000..fb84666 --- /dev/null +++ b/src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.V12.cs @@ -0,0 +1,8 @@ +using Octelys.MediatorPlugin.Tests.Infrastructure; + +namespace Octelys.MediatorPlugin.Tests; + +[MediatRV12TestPackages] +public sealed class MediatrGoToHandlerCaretTestsV12 : MediatrGoToHandlerCaretTestsBase +{ +} diff --git a/src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.V13.cs b/src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.V13.cs new file mode 100644 index 0000000..d609537 --- /dev/null +++ b/src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.V13.cs @@ -0,0 +1,8 @@ +using Octelys.MediatorPlugin.Tests.Infrastructure; + +namespace Octelys.MediatorPlugin.Tests; + +[MediatRV13TestPackages] +public sealed class MediatrGoToHandlerCaretTestsV13 : MediatrGoToHandlerCaretTestsBase +{ +} diff --git a/src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.V14.cs b/src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.V14.cs new file mode 100644 index 0000000..7049d62 --- /dev/null +++ b/src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.V14.cs @@ -0,0 +1,8 @@ +using Octelys.MediatorPlugin.Tests.Infrastructure; + +namespace Octelys.MediatorPlugin.Tests; + +[MediatRV14TestPackages] +public sealed class MediatrGoToHandlerCaretTestsV14 : MediatrGoToHandlerCaretTestsBase +{ +} diff --git a/src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.cs b/src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.cs new file mode 100644 index 0000000..9d400a3 --- /dev/null +++ b/src/dotnet/MediatorPlugin.Tests/MediatR.GoToHandler.CaretTests.cs @@ -0,0 +1,84 @@ +using System.IO; +using System.Linq; +using JetBrains.Lifetimes; +using JetBrains.ProjectModel; +using JetBrains.ReSharper.Feature.Services.Util; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.Tree; +using JetBrains.ReSharper.Resources.Shell; +using JetBrains.ReSharper.TestFramework; +using JetBrains.TextControl; +using NUnit.Framework; +using ReSharper.MediatorPlugin.Services.Libraries; + +namespace Octelys.MediatorPlugin.Tests; + +/// +/// Positions a real caret (via the "{caret}" marker in each test's data file) and asserts which +/// handler file the plugin resolves, if any, for the identifier under it. Unlike , +/// which locates the request's declaration identifier directly through the PSI, this test goes +/// through the same caret-to-PSI resolution () that the plugin's +/// own actions use (e.g. GoToHandlerAction.GetSelectedMediatrRequest), so it exercises the literal +/// "caret sits on this token" scenario rather than simulating it by name. +/// and place the caret on identifiers that are not a Mediator request +/// declaration and assert that no handler is resolved. +/// +[TestNetCoreLatest] +[TestReferences("System.Runtime")] +public abstract class MediatrGoToHandlerCaretTestsBase : BaseTestWithTextControl +{ + private static readonly string[] TestsExpectingNoHandler = + { + nameof(CaretInNamespace), + nameof(CaretOnSendCall) + }; + + protected override string RelativeTestDataPath => "GoToHandlerCaretTests"; + + [Test] + public void Request() => DoNamedTest("Handler.cs"); + + [Test] + public void RequestCaretAtBeginning() => DoNamedTest("Handler.cs"); + + [Test] + public void CaretInNamespace() => DoNamedTest("Handler.cs"); + + [Test] + public void CaretOnSendCall() => DoNamedTest("Handler.cs"); + + protected override void DoTest + ( + Lifetime lifetime, + ISolution solution + ) + { + solution.GetPsiServices().Files.CommitAllDocuments(); + + using (ReadLockCookie.Create()) + { + ITextControl textControl = OpenTextControl(lifetime, GetCaretPosition()); + + IIdentifier requestIdentifier = TextControlToPsi.GetElement(solution, textControl); + + Assert.That(requestIdentifier, Is.Not.Null, "No identifier was found at the caret."); + + ITypeElement[] handlers = new LibraryAdaptor().FindHandlers(requestIdentifier).ToArray(); + + if (TestsExpectingNoHandler.Contains(TestMethodName)) + { + Assert.That(handlers, Is.Empty, "Expected no handler to be resolved from this caret position."); + + return; + } + + string handlerFileName = handlers + .Single() + .GetSourceFiles() + .Select(sourceFile => Path.GetFileName(sourceFile.GetLocation().FullPath)) + .Single(); + + Assert.That(handlerFileName, Is.EqualTo("Handler.cs")); + } + } +} diff --git a/src/dotnet/MediatorPlugin.Tests/MediatR.Requests.Tests.cs b/src/dotnet/MediatorPlugin.Tests/MediatR.Requests.Tests.cs index 5232371..f902267 100644 --- a/src/dotnet/MediatorPlugin.Tests/MediatR.Requests.Tests.cs +++ b/src/dotnet/MediatorPlugin.Tests/MediatR.Requests.Tests.cs @@ -115,4 +115,16 @@ public void FindHandlers_HandlerImplementsSharedGenericHandlerInterface_HandlerR TestSolutionFiles.MediatR ); } + + [Test] + public void FindHandlers_CaretOnRequest_HandlerFileReturned() + { + // Act & Assert. + AssertHandlerFilesFor + ( + "Domain.GetEntityRequest", + ["MediatR/Handlers/GetEntityHandler.cs"], + TestSolutionFiles.MediatR + ); + } } \ No newline at end of file diff --git a/src/dotnet/MediatorPlugin.Tests/MediatorPlugin.Tests.csproj b/src/dotnet/MediatorPlugin.Tests/MediatorPlugin.Tests.csproj index f502f60..0baf2db 100644 --- a/src/dotnet/MediatorPlugin.Tests/MediatorPlugin.Tests.csproj +++ b/src/dotnet/MediatorPlugin.Tests/MediatorPlugin.Tests.csproj @@ -52,6 +52,15 @@ Mediator.Requests.Tests.cs + + MediatR.GoToHandler.CaretTests.cs + + + MediatR.GoToHandler.CaretTests.cs + + + MediatR.GoToHandler.CaretTests.cs + diff --git a/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/CaretInNamespace.cs b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/CaretInNamespace.cs new file mode 100644 index 0000000..17f285b --- /dev/null +++ b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/CaretInNamespace.cs @@ -0,0 +1,7 @@ +using MediatR; + +namespace {caret}Domain; + +public class GetEntityRequest : IRequest +{ +} diff --git a/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/CaretOnSendCall.cs b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/CaretOnSendCall.cs new file mode 100644 index 0000000..1f97e88 --- /dev/null +++ b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/CaretOnSendCall.cs @@ -0,0 +1,24 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; + +namespace Domain; + +public class GetEntityRequest : IRequest +{ +} + +public class GetEntityRequestSender +{ + private readonly IMediator _mediator; + + public GetEntityRequestSender(IMediator mediator) + { + _mediator = mediator; + } + + public Task SendAsync(CancellationToken cancellationToken) + { + return _mediator.{caret}Send(new GetEntityRequest(), cancellationToken); + } +} diff --git a/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/Handler.cs b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/Handler.cs new file mode 100644 index 0000000..341c369 --- /dev/null +++ b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/Handler.cs @@ -0,0 +1,14 @@ +using System.Threading; +using System.Threading.Tasks; +using Domain; +using MediatR; + +namespace Application; + +internal sealed class GetEntityRequestHandler : IRequestHandler +{ + public Task Handle(GetEntityRequest request, CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } +} diff --git a/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/Request.cs b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/Request.cs new file mode 100644 index 0000000..f087915 --- /dev/null +++ b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/Request.cs @@ -0,0 +1,7 @@ +using MediatR; + +namespace Domain; + +public class GetEntity{caret}Request : IRequest +{ +} diff --git a/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/RequestCaretAtBeginning.cs b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/RequestCaretAtBeginning.cs new file mode 100644 index 0000000..afde079 --- /dev/null +++ b/src/dotnet/MediatorPlugin.Tests/test/data/GoToHandlerCaretTests/RequestCaretAtBeginning.cs @@ -0,0 +1,7 @@ +using MediatR; + +namespace Domain; + +public class {caret}GetEntityRequest : IRequest +{ +} diff --git a/test-solution/DotNetCore6Project/MediatR/MediatRCallsite.cs b/test-solution/DotNetCore6Project/MediatR/MediatRCallsite.cs new file mode 100644 index 0000000..b47912d --- /dev/null +++ b/test-solution/DotNetCore6Project/MediatR/MediatRCallsite.cs @@ -0,0 +1,19 @@ +using DotNetCore6_Domain.MediatR.Requests; +using MediatR; + +namespace DotNetCore6_Domain.MediatR; + +public class MediatRCallsite +{ + private readonly IMediator _mediator; + + public MediatRCallsite(IMediator mediator) + { + _mediator = mediator; + } + + public async Task RunAsync(CancellationToken cancellationToken) + { + await _mediator.Send(new CustomClassRequest(), cancellationToken); + } +} \ No newline at end of file diff --git a/test-solution/DotNetCore6Project/Mediator/MediatorCallsite.cs b/test-solution/DotNetCore6Project/Mediator/MediatorCallsite.cs new file mode 100644 index 0000000..73d81d8 --- /dev/null +++ b/test-solution/DotNetCore6Project/Mediator/MediatorCallsite.cs @@ -0,0 +1,19 @@ +using DotNetCore6_Domain.Mediator.Requests; +using MediatR; + +namespace DotNetCore6_Domain.Mediator; + +public class MediatorCallsite +{ + private readonly IMediator _mediator; + + public MediatorCallsite(IMediator mediator) + { + _mediator = mediator; + } + + public async Task RunAsync(CancellationToken cancellationToken) + { + await _mediator.Send(new NewMediatorRequest(), cancellationToken); + } +} \ No newline at end of file