From 5bab54836f2daa5d91d3fa9097f4e5bf1f47c3be Mon Sep 17 00:00:00 2001 From: marcarro Date: Wed, 7 Aug 2024 14:27:53 -0700 Subject: [PATCH] Set up basic end to end test for extract component --- .../CodeActionEndToEndTest.NetFx.cs | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs index 33b35e3df98..f5c15499e68 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Castle.Core.Logging; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Components; using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; @@ -38,6 +39,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions; public class CodeActionEndToEndTest(ITestOutputHelper testOutput) : SingleServerDelegatingEndpointTestBase(testOutput) { private const string GenerateEventHandlerTitle = "Generate Event Handler 'DoesNotExist'"; + private const string ExtractToComponentTitle = "Extract element to new component"; private const string GenerateAsyncEventHandlerTitle = "Generate Async Event Handler 'DoesNotExist'"; private const string GenerateEventHandlerReturnType = "void"; private const string GenerateAsyncEventHandlerReturnType = "global::System.Threading.Tasks.Task"; @@ -59,6 +61,17 @@ private GenerateMethodCodeActionResolver[] CreateRazorCodeActionResolvers( razorFormattingService) ]; + // TODO: Make this func + private ExtractToComponentCodeActionResolver[] CreateExtractComponentCodeActionResolvers(string filePath, RazorCodeDocument codeDocument) + { + var emptyDocumentContextFactory = new TestDocumentContextFactory(); + return [ + new ExtractToComponentCodeActionResolver( + new GenerateMethodResolverDocumentContextFactory(filePath, codeDocument), + TestLanguageServerFeatureOptions.Instance) + ]; + } + #region CSharp CodeAction Tests [Fact] @@ -1005,6 +1018,36 @@ await ValidateCodeActionAsync(input, diagnostics: [new Diagnostic() { Code = "CS0103", Message = "The name 'DoesNotExist' does not exist in the current context" }]); } + [Fact] + public async Task Handle_ExtractComponent() + { + var input = """ + <[||]div id="a"> +

Div a title

+ +

Div a par

+ +
+ +
+ """; + + var expectedRazorComponent = """ +
+

Div a title

+ +

Div a par

+
+ """; + + await ValidateExtractComponentCodeActionAsync( + input, + expectedRazorComponent, + ExtractToComponentTitle, + razorCodeActionProviders: [new ExtractToComponentCodeActionProvider(LoggerFactory)], + codeActionResolversCreator: CreateExtractComponentCodeActionResolvers); + } + #endregion private async Task ValidateCodeBehindFileAsync( @@ -1148,6 +1191,66 @@ private async Task ValidateCodeActionAsync( AssertEx.EqualOrDiff(expected, actual); } + private async Task ValidateExtractComponentCodeActionAsync( + string input, + string? expected, + string codeAction, + int childActionIndex = 0, + IRazorCodeActionProvider[]? razorCodeActionProviders = null, + Func? codeActionResolversCreator = null, + RazorLSPOptionsMonitor? optionsMonitor = null, + Diagnostic[]? diagnostics = null) + { + TestFileMarkupParser.GetSpan(input, out input, out var textSpan); + + var razorFilePath = "C:/path/test.razor"; + var componentFilePath = "C:/path/Component.razor"; + var codeDocument = CreateCodeDocument(input, filePath: razorFilePath); + var sourceText = codeDocument.GetSourceText(); + var uri = new Uri(razorFilePath); + var languageServer = await CreateLanguageServerAsync(codeDocument, razorFilePath); + var documentContext = CreateDocumentContext(uri, codeDocument); + var requestContext = new RazorRequestContext(documentContext, null!, "lsp/method", uri: null); + + var result = await GetCodeActionsAsync( + uri, + textSpan, + sourceText, + requestContext, + languageServer, + razorCodeActionProviders, + diagnostics); + + Assert.NotEmpty(result); + var codeActionToRun = GetCodeActionToRun(codeAction, childActionIndex, result); + + if (expected is null) + { + Assert.Null(codeActionToRun); + return; + } + + Assert.NotNull(codeActionToRun); + + var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory, codeDocument, documentContext.Snapshot, optionsMonitor?.CurrentValue); + var changes = await GetEditsAsync( + codeActionToRun, + requestContext, + languageServer, + codeActionResolversCreator?.Invoke(razorFilePath, codeDocument) ?? []); + + var edits = new List(); + + // Only get changes made in the new component file + foreach (var change in changes.Where(e => e.TextDocument.Uri.AbsolutePath == componentFilePath)) + { + edits.AddRange(change.Edits.Select(e => e.ToTextChange(sourceText))); + } + + var actual = sourceText.WithChanges(edits).ToString(); + AssertEx.EqualOrDiff(expected, actual); + } + private static VSInternalCodeAction? GetCodeActionToRun(string codeAction, int childActionIndex, SumType[] result) { var codeActionToRun = (VSInternalCodeAction?)result.SingleOrDefault(e => ((RazorVSInternalCodeAction)e.Value!).Name == codeAction || ((RazorVSInternalCodeAction)e.Value!).Title == codeAction).Value; @@ -1306,4 +1409,78 @@ static IEnumerable BuildTagHelpers() } } } + + private class ExtractToComponentResolverDocumentContextFactory : TestDocumentContextFactory + { + private readonly List _tagHelperDescriptors; + + public ExtractToComponentResolverDocumentContextFactory + (string filePath, + RazorCodeDocument codeDocument, + TagHelperDescriptor[]? tagHelpers = null, + int? version = null) + : base(filePath, codeDocument, version) + { + _tagHelperDescriptors = CreateTagHelperDescriptors(); + if (tagHelpers is not null) + { + _tagHelperDescriptors.AddRange(tagHelpers); + } + } + + public override bool TryCreate( + Uri documentUri, + VSProjectContext? projectContext, + bool versioned, + [NotNullWhen(true)] out DocumentContext? context) + { + if (FilePath is null || CodeDocument is null) + { + context = null; + return false; + } + + var projectWorkspaceState = ProjectWorkspaceState.Create(_tagHelperDescriptors.ToImmutableArray()); + var testDocumentSnapshot = TestDocumentSnapshot.Create(FilePath, CodeDocument.GetSourceText().ToString(), CodeAnalysis.VersionStamp.Default, projectWorkspaceState); + testDocumentSnapshot.With(CodeDocument); + + context = CreateDocumentContext(new Uri(FilePath), testDocumentSnapshot); + return true; + } + + private static List CreateTagHelperDescriptors() + { + return BuildTagHelpers().ToList(); + + static IEnumerable BuildTagHelpers() + { + var builder = TagHelperDescriptorBuilder.Create("oncontextmenu", "Microsoft.AspNetCore.Components"); + builder.SetMetadata( + new KeyValuePair(ComponentMetadata.EventHandler.EventArgsType, "Microsoft.AspNetCore.Components.Web.MouseEventArgs"), + new KeyValuePair(ComponentMetadata.SpecialKindKey, ComponentMetadata.EventHandler.TagHelperKind)); + yield return builder.Build(); + + builder = TagHelperDescriptorBuilder.Create("onclick", "Microsoft.AspNetCore.Components"); + builder.SetMetadata( + new KeyValuePair(ComponentMetadata.EventHandler.EventArgsType, "Microsoft.AspNetCore.Components.Web.MouseEventArgs"), + new KeyValuePair(ComponentMetadata.SpecialKindKey, ComponentMetadata.EventHandler.TagHelperKind)); + + yield return builder.Build(); + + builder = TagHelperDescriptorBuilder.Create("oncopy", "Microsoft.AspNetCore.Components"); + builder.SetMetadata( + new KeyValuePair(ComponentMetadata.EventHandler.EventArgsType, "Microsoft.AspNetCore.Components.Web.ClipboardEventArgs"), + new KeyValuePair(ComponentMetadata.SpecialKindKey, ComponentMetadata.EventHandler.TagHelperKind)); + + yield return builder.Build(); + + builder = TagHelperDescriptorBuilder.Create("ref", "Microsoft.AspNetCore.Components"); + builder.SetMetadata( + new KeyValuePair(ComponentMetadata.SpecialKindKey, ComponentMetadata.Ref.TagHelperKind), + new KeyValuePair(ComponentMetadata.Common.DirectiveAttribute, bool.TrueString)); + + yield return builder.Build(); + } + } + } }