File: RenameTests.cs
Web Access
Project: src\src\Razor\src\Razor\test\Microsoft.VisualStudio.Razor.IntegrationTests\Microsoft.VisualStudio.Razor.IntegrationTests.csproj (Microsoft.VisualStudio.Razor.IntegrationTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Razor.IntegrationTests.Extensions;
using Microsoft.VisualStudio.Razor.IntegrationTests.InProcess;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
using Xunit;
using Xunit.Abstractions;
 
namespace Microsoft.VisualStudio.Razor.IntegrationTests;
 
public class RenameTests(ITestOutputHelper testOutputHelper) : AbstractRazorEditorTest(testOutputHelper)
{
    [IdeFact]
    public async Task Rename_ComponentAttribute_FromRazor()
    {
        // Open the file
        await TestServices.SolutionExplorer.OpenFileAsync(RazorProjectConstants.BlazorProjectName, RazorProjectConstants.IndexRazorFile, ControlledHangMitigatingCancellationToken);
 
        await TestServices.Editor.PlaceCaretAsync("Title=", charsOffset: -1, ControlledHangMitigatingCancellationToken);
 
        await TestServices.Editor.WaitForComponentClassificationAsync(ControlledHangMitigatingCancellationToken);
 
        await Task.Delay(500);
 
        // Act
        await TestServices.Editor.InvokeRenameAsync(ControlledHangMitigatingCancellationToken);
        TestServices.Input.Send("ZooperDooper{ENTER}");
 
        // Assert
        // The rename operation causes SurveyPrompt.razor to be opened
        await TestServices.Editor.WaitForActiveWindowByFileAsync("SurveyPrompt.razor", ControlledHangMitigatingCancellationToken);
        await TestServices.Editor.VerifyTextContainsAsync("public string? ZooperDooper { get; set; }", ControlledHangMitigatingCancellationToken);
        await TestServices.Editor.VerifyTextContainsAsync("@ZooperDooper", ControlledHangMitigatingCancellationToken);
 
        await TestServices.SolutionExplorer.OpenFileAsync(RazorProjectConstants.BlazorProjectName, RazorProjectConstants.IndexRazorFile, ControlledHangMitigatingCancellationToken);
        await TestServices.Editor.VerifyTextContainsAsync("<SurveyPrompt ZooperDooper=", ControlledHangMitigatingCancellationToken);
    }
 
    [IdeFact]
    public async Task Rename_ComponentAttribute_FromCSharpInRazor()
    {
        // Open the file
        await TestServices.SolutionExplorer.OpenFileAsync(RazorProjectConstants.BlazorProjectName, RazorProjectConstants.SurveyPromptFile, ControlledHangMitigatingCancellationToken);
 
        await TestServices.Editor.PlaceCaretAsync("Title", charsOffset: 0, occurrence: 2, extendSelection: false, selectBlock: false, ControlledHangMitigatingCancellationToken);
 
        await Task.Delay(1500);
 
        // Act
        await TestServices.Editor.InvokeRenameAsync(ControlledHangMitigatingCancellationToken);
        TestServices.Input.Send("ZooperDooper{ENTER}");
 
        // Assert
        // The rename operation causes Index.razor to be opened
        await TestServices.Editor.WaitForActiveWindowByFileAsync("Index.razor", ControlledHangMitigatingCancellationToken);
        await TestServices.SolutionExplorer.OpenFileAsync(RazorProjectConstants.BlazorProjectName, RazorProjectConstants.IndexRazorFile, ControlledHangMitigatingCancellationToken);
        await TestServices.Editor.VerifyTextContainsAsync("<SurveyPrompt ZooperDooper=", ControlledHangMitigatingCancellationToken);
 
        await TestServices.SolutionExplorer.OpenFileAsync(RazorProjectConstants.BlazorProjectName, RazorProjectConstants.SurveyPromptFile, ControlledHangMitigatingCancellationToken);
        await TestServices.Editor.VerifyTextContainsAsync("public string? ZooperDooper { get; set; }", ControlledHangMitigatingCancellationToken);
        await TestServices.Editor.VerifyTextContainsAsync("@ZooperDooper", ControlledHangMitigatingCancellationToken);
    }
 
    [IdeFact(Skip = "https://github.com/dotnet/razor/issues/10820")]
    public async Task Rename_ComponentAttribute_FromCSharpInCSharp()
    {
        // Create the file
        const string MyComponentRazorPath = "MyComponent.razor";
        await TestServices.SolutionExplorer.AddFileAsync(RazorProjectConstants.BlazorProjectName,
            MyComponentRazorPath,
            """
                @MyProperty
                """,
            open: true, // We create these open and then close them to try to force Component initialization while testing edits of closed documents
            cancellationToken: ControlledHangMitigatingCancellationToken);
        await TestServices.Editor.CloseCodeFileAsync(RazorProjectConstants.BlazorProjectName, MyComponentRazorPath, saveFile: true, ControlledHangMitigatingCancellationToken);
 
        const string MyComponentCSharpPath = "MyComponent.razor.cs";
        await TestServices.SolutionExplorer.AddFileAsync(RazorProjectConstants.BlazorProjectName,
            MyComponentCSharpPath,
            """
                namespace BlazorProject;
 
                public partial class MyComponent
                {
                    [Microsoft.AspNetCore.Components.ParameterAttribute]
                    public string? MyProperty { get; set; }
                }
            
                """,
            open: true,
            cancellationToken: ControlledHangMitigatingCancellationToken);
        await TestServices.Editor.CloseCodeFileAsync(RazorProjectConstants.BlazorProjectName, MyComponentCSharpPath, saveFile: true, ControlledHangMitigatingCancellationToken);
 
        await TestServices.SolutionExplorer.AddFileAsync(RazorProjectConstants.BlazorProjectName,
            "MyPage.razor",
            """
                <MyComponent MyProperty="123" />
                """,
            open: true,
            cancellationToken: ControlledHangMitigatingCancellationToken);
 
        await TestServices.Editor.WaitForComponentClassificationAsync(ControlledHangMitigatingCancellationToken);
 
        await TestServices.SolutionExplorer.OpenFileAsync(RazorProjectConstants.BlazorProjectName, "MyComponent.razor.cs", ControlledHangMitigatingCancellationToken);
 
        await TestServices.Editor.PlaceCaretAsync("MyProperty", charsOffset: 0, occurrence: 2, extendSelection: false, selectBlock: false, ControlledHangMitigatingCancellationToken);
 
        await Task.Delay(500);
 
        // Act
        await TestServices.Editor.InvokeRenameAsync(ControlledHangMitigatingCancellationToken);
        TestServices.Input.Send("ZooperDooper{ENTER}");
 
        // Assert
        // The rename operation causes MyPage.razor to be opened
        await TestServices.Editor.WaitForActiveWindowByFileAsync("MyComponent.razor.cs", ControlledHangMitigatingCancellationToken);
        await TestServices.Editor.VerifyTextContainsAsync("public string? ZooperDooper { get; set; }", ControlledHangMitigatingCancellationToken);
 
        await TestServices.SolutionExplorer.OpenFileAsync(RazorProjectConstants.BlazorProjectName, "MyComponent.razor", ControlledHangMitigatingCancellationToken);
        await TestServices.Editor.VerifyTextContainsAsync("@ZooperDooper", ControlledHangMitigatingCancellationToken);
 
        await TestServices.SolutionExplorer.OpenFileAsync(RazorProjectConstants.BlazorProjectName, "MyPage.razor", ControlledHangMitigatingCancellationToken);
        await TestServices.Editor.VerifyTextContainsAsync("<MyComponent ZooperDooper=", ControlledHangMitigatingCancellationToken);
    }
 
    [IdeFact]
    public async Task Rename_ComponentAttribute_BoundAttribute()
    {
        // Create the files
        const string MyComponentPath = "MyComponent.razor";
        await TestServices.SolutionExplorer.AddFileAsync(RazorProjectConstants.BlazorProjectName,
            MyComponentPath,
            """
            <div></div>
 
            @code
            {
                [Parameter]
                public string? Value { get; set; }
 
                [Parameter]
                public EventCallback<string?> ValueChanged { get; set; }
            }
            """,
            open: true,
            cancellationToken: ControlledHangMitigatingCancellationToken);
 
        await TestServices.Editor.CloseCodeFileAsync(RazorProjectConstants.BlazorProjectName, MyComponentPath, saveFile: true, ControlledHangMitigatingCancellationToken);
 
        await TestServices.SolutionExplorer.AddFileAsync(RazorProjectConstants.BlazorProjectName,
            "MyPage.razor",
            """
            <MyComponent @bind-Value="value"></MyComponent>
 
            @code{
                string? value = "";
            }
            """,
            open: true,
            cancellationToken: ControlledHangMitigatingCancellationToken);
 
        await TestServices.Editor.WaitForComponentClassificationAsync(ControlledHangMitigatingCancellationToken);
 
        await TestServices.Editor.PlaceCaretAsync("Value=", charsOffset: -1, ControlledHangMitigatingCancellationToken);
 
        await Task.Delay(500);
 
        // Act
        await TestServices.Editor.InvokeRenameAsync(ControlledHangMitigatingCancellationToken);
        TestServices.Input.Send("ZooperDooper{ENTER}");
 
        // Assert
        // The rename operation causes MyPage.razor to be opened
        await TestServices.Editor.WaitForActiveWindowByFileAsync("MyComponent.razor", ControlledHangMitigatingCancellationToken);
        await TestServices.Editor.VerifyTextContainsAsync("public string? ZooperDooper { get; set; }", ControlledHangMitigatingCancellationToken);
 
        await TestServices.SolutionExplorer.OpenFileAsync(RazorProjectConstants.BlazorProjectName, "MyPage.razor", ControlledHangMitigatingCancellationToken);
        await TestServices.Editor.VerifyTextContainsAsync("<MyComponent @bind-ZooperDooper=\"value\"></MyComponent>", ControlledHangMitigatingCancellationToken);
    }
 
    [IdeFact]
    public async Task Rename_ComponentDefinedInCSharp_FromCSharp()
    {
        // Create the files
        var position = await TestServices.SolutionExplorer.AddFileAsync(RazorProjectConstants.BlazorProjectName,
            "MyComponent.cs",
            """
            using Microsoft.AspNetCore.Components;
 
            public class MyComp$$onent : ComponentBase
            {
            }
            """,
            open: true,
            cancellationToken: ControlledHangMitigatingCancellationToken);
 
        await TestServices.SolutionExplorer.AddFileAsync(RazorProjectConstants.BlazorProjectName,
            "MyPage.razor",
            """
            <MyComponent></MyComponent>
            """,
            open: true,
            cancellationToken: ControlledHangMitigatingCancellationToken);
        await TestServices.Editor.WaitForComponentClassificationAsync(ControlledHangMitigatingCancellationToken);
 
        await TestServices.SolutionExplorer.OpenFileAsync(RazorProjectConstants.BlazorProjectName, "MyComponent.cs", ControlledHangMitigatingCancellationToken);
 
        await TestServices.Editor.PlaceCaretAsync(position, ControlledHangMitigatingCancellationToken);
 
        // For some reason, this particular rename exercise is particularly flaky so we have a few hail marys to recite
        await TestServices.Shell.WaitForOperationProgressAsync(ControlledHangMitigatingCancellationToken);
        await WaitForRoslynRenameReadyAsync(ControlledHangMitigatingCancellationToken);
 
        // Act
        await TestServices.Editor.InvokeRenameAsync(ControlledHangMitigatingCancellationToken);
 
        // Even though we waited for the command to be ready and available, it can still be a bit slow to come up
        await Task.Delay(500);
 
        TestServices.Input.Send("ZooperDooper{ENTER}");
 
        await TestServices.Editor.WaitForCurrentLineTextAsync("public class ZooperDooper : ComponentBase", ControlledHangMitigatingCancellationToken);
 
        // Assert
        await TestServices.SolutionExplorer.OpenFileAsync(RazorProjectConstants.BlazorProjectName, "MyPage.razor", ControlledHangMitigatingCancellationToken);
        await TestServices.Editor.WaitForTextContainsAsync("<ZooperDooper></ZooperDooper>", ControlledHangMitigatingCancellationToken);
    }
 
    [IdeFact]
    public async Task Rename_ComponentDefinedInCSharp_FromRazor()
    {
        // Create the files
        await TestServices.SolutionExplorer.AddFileAsync(RazorProjectConstants.BlazorProjectName,
            "MyComponent.cs",
            """
            using Microsoft.AspNetCore.Components;
 
            namespace My.Fancy.Namespace;
 
            public class MyComponent : ComponentBase
            {
            }
            """,
            open: true,
            cancellationToken: ControlledHangMitigatingCancellationToken);
 
        var position = await TestServices.SolutionExplorer.AddFileAsync(RazorProjectConstants.BlazorProjectName,
            "MyPage.razor",
            """
            @using My.Fancy.Namespace
 
            <MyComp$$onent></MyComponent>
            """,
            open: true,
            cancellationToken: ControlledHangMitigatingCancellationToken);
        await TestServices.Editor.WaitForComponentClassificationAsync(ControlledHangMitigatingCancellationToken);
 
        await TestServices.Editor.PlaceCaretAsync(position, ControlledHangMitigatingCancellationToken);
 
        // Act
        await TestServices.Editor.InvokeRenameAsync(ControlledHangMitigatingCancellationToken);
        TestServices.Input.Send("ZooperDooper{ENTER}");
 
        // Assert
        await TestServices.Editor.WaitForCurrentLineTextAsync("<ZooperDooper></ZooperDooper>", ControlledHangMitigatingCancellationToken);
 
        await TestServices.SolutionExplorer.OpenFileAsync(RazorProjectConstants.BlazorProjectName, "MyComponent.cs", ControlledHangMitigatingCancellationToken);
        await TestServices.Editor.VerifyTextContainsAsync("public class ZooperDooper : ComponentBase", ControlledHangMitigatingCancellationToken);
    }
 
    [IdeFact]
    public async Task Rename_ComponentDefinedInCSharp_FromRazor_GlobalNamespace()
    {
        // Create the files
        await TestServices.SolutionExplorer.AddFileAsync(RazorProjectConstants.BlazorProjectName,
            "MyComponent.cs",
            """
            using Microsoft.AspNetCore.Components;
 
            public class MyComponent : ComponentBase
            {
            }
            """,
            open: true,
            cancellationToken: ControlledHangMitigatingCancellationToken);
 
        var position = await TestServices.SolutionExplorer.AddFileAsync(RazorProjectConstants.BlazorProjectName,
            "MyPage.razor",
            """
            <MyComp$$onent></MyComponent>
            """,
            open: true,
            cancellationToken: ControlledHangMitigatingCancellationToken);
        await TestServices.Editor.WaitForComponentClassificationAsync(ControlledHangMitigatingCancellationToken);
 
        await TestServices.Editor.PlaceCaretAsync(position, ControlledHangMitigatingCancellationToken);
 
        // Act
        await TestServices.Editor.InvokeRenameAsync(ControlledHangMitigatingCancellationToken);
        TestServices.Input.Send("ZooperDooper{ENTER}");
 
        // Assert
        await TestServices.Editor.WaitForCurrentLineTextAsync("<ZooperDooper></ZooperDooper>", ControlledHangMitigatingCancellationToken);
 
        await TestServices.SolutionExplorer.OpenFileAsync(RazorProjectConstants.BlazorProjectName, "MyComponent.cs", ControlledHangMitigatingCancellationToken);
        await TestServices.Editor.VerifyTextContainsAsync("public class ZooperDooper : ComponentBase", ControlledHangMitigatingCancellationToken);
    }
 
    [IdeFact]
    public async Task Rename_ComponentDefinedInCSharp_FromCSharpInRazor()
    {
        // Create the files
        await TestServices.SolutionExplorer.AddFileAsync(RazorProjectConstants.BlazorProjectName,
            "MyComponent.cs",
            """
            using Microsoft.AspNetCore.Components;
 
            public class MyComponent : ComponentBase
            {
            }
            """,
            open: true,
            cancellationToken: ControlledHangMitigatingCancellationToken);
 
        var position = await TestServices.SolutionExplorer.AddFileAsync(RazorProjectConstants.BlazorProjectName,
            "MyPage.razor",
            """
            <MyComponent></MyComponent>
 
            @nameof(MyComp$$onent)
            """,
            open: true,
            cancellationToken: ControlledHangMitigatingCancellationToken);
        await TestServices.Editor.WaitForComponentClassificationAsync(ControlledHangMitigatingCancellationToken);
 
        await TestServices.Editor.PlaceCaretAsync(position, ControlledHangMitigatingCancellationToken);
 
        // Act
        await TestServices.Editor.InvokeRenameAsync(ControlledHangMitigatingCancellationToken);
        TestServices.Input.Send("ZooperDooper{ENTER}");
 
        // Assert
        await TestServices.Editor.WaitForTextContainsAsync("<ZooperDooper></ZooperDooper>", ControlledHangMitigatingCancellationToken);
 
        await TestServices.SolutionExplorer.OpenFileAsync(RazorProjectConstants.BlazorProjectName, "MyComponent.cs", ControlledHangMitigatingCancellationToken);
        await TestServices.Editor.VerifyTextContainsAsync("public class ZooperDooper : ComponentBase", ControlledHangMitigatingCancellationToken);
    }
 
    private async Task WaitForRoslynRenameReadyAsync(CancellationToken cancellationToken)
    {
        await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
 
        var view = await TestServices.Editor.GetActiveTextViewAsync(cancellationToken);
        var buffer = view.GetBufferContainingCaret();
 
        var commandArgs = new RenameCommandArgs(view, buffer);
 
        // We don't have EA from this project, so we have to resort to reflection. Fortunately it's pretty simple
        var roslynHandler = Type.GetType("Microsoft.CodeAnalysis.Editor.Implementation.InlineRename.AbstractRenameCommandHandler, Microsoft.CodeAnalysis.EditorFeatures");
        var canRename = roslynHandler.GetMethod("CanRename", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
 
        await Helper.RetryAsync(ct =>
        {
            return Task.FromResult((bool)canRename.Invoke(null, [commandArgs]));
        }, TimeSpan.FromMilliseconds(100), cancellationToken);
    }
}