From ebbb9450c0d115d8e3843b08f6e75d345bfea6f7 Mon Sep 17 00:00:00 2001 From: "Alex Gavrilov (DEV PROD)" Date: Thu, 19 Sep 2024 13:27:26 -0700 Subject: [PATCH 1/3] More hardening of the completion test We not wait for a specific item in the actual shown completion UI, and also wait for the correct text in the editor post-commit to account for actions such as OnAutoInsert --- .../CompletionIntegrationTests.cs | 336 ++++++++++++++++-- .../InProcess/EditorInProcess_Completion.cs | 39 ++ 2 files changed, 339 insertions(+), 36 deletions(-) diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CompletionIntegrationTests.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CompletionIntegrationTests.cs index 8c8c8a12945..495d10db1c4 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CompletionIntegrationTests.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CompletionIntegrationTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Roslyn.Test.Utilities; @@ -17,55 +18,268 @@ public class CompletionIntegrationTests(ITestOutputHelper testOutputHelper) : Ab [IdeFact] public async Task SnippetCompletion_Html() { - await TestServices.SolutionExplorer.AddFileAsync( - RazorProjectConstants.BlazorProjectName, - "Test.razor", - """ -@page "Test" + await VerifyTypeAndCommitCompletionAsync( + input: """ + @page "Test" -Test + Test + +

Test

+ + @code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } + } + """, + output: """ + @page "Test" -

Test

+ Test -@code { - private int currentCount = 0; +

Test

+
+
+
+
- private void IncrementCount() + @code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } + } + """, + search: "

Test

", + stringsToType: ["{ENTER}", "d", "d"]); + } + + [IdeFact, WorkItem("https://github.com/dotnet/razor/issues/10787")] + public async Task CompletionCommit_HtmlAttributeWithoutValue() { - currentCount++; + await VerifyTypeAndCommitCompletionAsync( + input: """ + @page "Test" + + Test + + + + @code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } + } + """, + output: """ + @page "Test" + + Test + + + + @code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } + } + """, + search: "Test", charsOffset: 1, ControlledHangMitigatingCancellationToken); - TestServices.Input.Send("{ENTER}"); - TestServices.Input.Send("d"); - TestServices.Input.Send("d"); + Test - await CommitCompletionAndVerifyAsync(""" -@page "Test" + + + @code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } + } + """, + output: """ + @page "Test" -Test + Test -

Test

-
-
-
-
+ -@code { - private int currentCount = 0; + @code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } + } + """, + search: "Test + + @code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } + } + """, + output: """ + @page "Test" + + Test + + ", + stringsToType: ["{ENTER}", "{ENTER}", "<", "s", "p", "a"]); } -} -"""); + + [IdeFact] + public async Task CompletionCommit_WithAngleBracket_HtmlTag() + { + await VerifyTypeAndCommitCompletionAsync( + input: """ + @page "Test" + + Test + + @code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } + } + """, + output: """ + @page "Test" + + Test + + + + @code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } + } + """, + search: "", + stringsToType: ["{ENTER}", "{ENTER}", "<", "s", "p", "a"], + commitChar: '>', + "span"); + } + + [IdeFact] + public async Task CompletionCommit_CSharp() + { + await VerifyTypeAndCommitCompletionAsync( + input: """ + @page "Test" + + Test + + @code { + private int myCurrentCount = 0; + + private void IncrementCount() + { + myCurrentCount++; + } + } + """, + output: """ + @page "Test" + + Test + + @code { + private int myCurrentCount = 0; + + private void IncrementCount() + { + myCurrentCount++; + + myCurrentCount + } + } + """, + search: "myCurrentCount++;", + stringsToType: ["{ENTER}", "{ENTER}", "m", "y", "C", "u", "r"]); + } + + private async Task VerifyTypeAndCommitCompletionAsync(string input, string output, string search, string[] stringsToType, char? commitChar = null, string? expectedSelectedItemLabel = null) + { + await TestServices.SolutionExplorer.AddFileAsync( + RazorProjectConstants.BlazorProjectName, + "Test.razor", + input, + open: true, + ControlledHangMitigatingCancellationToken); + + await TestServices.Editor.WaitForComponentClassificationAsync(ControlledHangMitigatingCancellationToken); + + await TestServices.Editor.PlaceCaretAsync(search, charsOffset: 1, ControlledHangMitigatingCancellationToken); + foreach (var stringToType in stringsToType) + { + TestServices.Input.Send(stringToType); + } + + if (expectedSelectedItemLabel is not null) + { + await CommitCompletionAndVerifyAsync(output, expectedSelectedItemLabel, commitChar); + } + else + { + await CommitCompletionAndVerifyAsync(output, commitChar); + } } [IdeFact] @@ -225,12 +439,25 @@ public enum MyEnum """); } - private async Task CommitCompletionAndVerifyAsync(string expected) + private async Task CommitCompletionAndVerifyAsync(string expected, char? commitChar = null) { var session = await TestServices.Editor.WaitForCompletionSessionAsync(HangMitigatingCancellationToken); Assert.NotNull(session); - Assert.True(session.CommitIfUnique(HangMitigatingCancellationToken)); + if (commitChar.HasValue) + { + // Commit using the specified commit character + session.Commit(commitChar.Value, HangMitigatingCancellationToken); + + // session.Commit call above commits as if the commit character was typed, + // but doesn't actually insert the character into the buffer. + // So we still need to insert the character into the buffer ourselves. + TestServices.Input.Send(commitChar.Value.ToString()); + } + else + { + Assert.True(session.CommitIfUnique(HangMitigatingCancellationToken)); + } var textView = await TestServices.Editor.GetActiveTextViewAsync(HangMitigatingCancellationToken); var text = textView.TextBuffer.CurrentSnapshot.GetText(); @@ -239,4 +466,41 @@ private async Task CommitCompletionAndVerifyAsync(string expected) // tests allow for it as long as the content is correct AssertEx.AssertEqualToleratingWhitespaceDifferences(expected, text); } + + private async Task CommitCompletionAndVerifyAsync(string expected, string expectedSelectedItemLabel, char? commitChar = null) + { + // Actually open completion UI and wait for it have selected item we are interested in + var session = await TestServices.Editor.OpenCompletionSessionAndWaitForItemAsync(TimeSpan.FromSeconds(10), expectedSelectedItemLabel, HangMitigatingCancellationToken); + + Assert.NotNull(session); + if (commitChar.HasValue) + { + // Commit using the specified commit character + session.Commit(commitChar.Value, HangMitigatingCancellationToken); + + // session.Commit call above commits as if the commit character was typed, + // but doesn't actually insert the character into the buffer. + // So we still need to insert the character into the buffer ourselves. + TestServices.Input.Send(commitChar.Value.ToString()); + } + else + { + Assert.True(session.CommitIfUnique(HangMitigatingCancellationToken)); + } + + var textView = await TestServices.Editor.GetActiveTextViewAsync(HangMitigatingCancellationToken); + + var stopwatch = new Stopwatch(); + string text; + while ((text = textView.TextBuffer.CurrentSnapshot.GetText()) != expected && stopwatch.ElapsedMilliseconds < 10000) + { + // Text might get updated *after* completion by something like auto-insert, so wait for the desired text + await Task.Delay(100); + } + + // Snippets may have slight whitespace differences due to line endings. These + // tests allow for it as long as the content is correct + Assert.Equal(expected, text); + } + } diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/EditorInProcess_Completion.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/EditorInProcess_Completion.cs index 838e2eb79e8..10f9aef9634 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/EditorInProcess_Completion.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/EditorInProcess_Completion.cs @@ -55,4 +55,43 @@ public async Task DismissCompletionSessionsAsync(CancellationToken cancellationT return session; } + + /// + /// Open completion pop-up window UI and wait for the specified item to be present selected + /// + /// + /// + /// + /// Completion session that has matching selected item, or null otherwise + public async Task OpenCompletionSessionAndWaitForItemAsync(TimeSpan timeOut, string selectedItemLabel, CancellationToken cancellationToken) + { + await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + + // Returns completion session that might or might not be visible in the IDE + var session = await WaitForCompletionSessionAsync(timeOut, cancellationToken); + + if (session is null) + { + return null; + } + + var textView = await GetActiveTextViewAsync(cancellationToken); + var stopWatch = Stopwatch.StartNew(); + + // Actually open the completion pop-up window and force visible items to be computed or re-computed + session.OpenOrUpdate(new CompletionTrigger(CompletionTriggerReason.Insertion, textView.TextSnapshot), textView.Caret.Position.BufferPosition, cancellationToken); + while (session.GetComputedItems(cancellationToken).SelectedItem?.DisplayText != selectedItemLabel) + { + if (stopWatch.ElapsedMilliseconds >= timeOut.TotalMilliseconds) + { + return null; + } + + await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken); + + session.OpenOrUpdate(new CompletionTrigger(CompletionTriggerReason.Insertion, textView.TextSnapshot), textView.Caret.Position.BufferPosition, cancellationToken); + } + + return session; + } } From 5bce4adb98710187616e58fc7b89280f08c682b2 Mon Sep 17 00:00:00 2001 From: "Alex Gavrilov (DEV PROD)" Date: Mon, 23 Sep 2024 12:42:46 -0700 Subject: [PATCH 2/3] Switch the other "commit tag name" test to the new way of waiting for completion --- .../CompletionIntegrationTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CompletionIntegrationTests.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CompletionIntegrationTests.cs index 495d10db1c4..ec28481d867 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CompletionIntegrationTests.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CompletionIntegrationTests.cs @@ -174,7 +174,9 @@ private void IncrementCount() } """, search: "", - stringsToType: ["{ENTER}", "{ENTER}", "<", "s", "p", "a"]); + stringsToType: ["{ENTER}", "{ENTER}", "<", "s", "p", "a"], + commitChar: null, + expectedSelectedItemLabel: "span"); } [IdeFact] From 12286cb5f5b790856e3bd1d90dd9981bebf3b342 Mon Sep 17 00:00:00 2001 From: "Alex Gavrilov (DEV PROD)" Date: Wed, 25 Sep 2024 14:24:36 -0700 Subject: [PATCH 3/3] PR feedback --- .../CompletionIntegrationTests.cs | 11 ++++++----- .../InProcess/EditorInProcess_Completion.cs | 4 +++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CompletionIntegrationTests.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CompletionIntegrationTests.cs index ec28481d867..baf48dd0985 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CompletionIntegrationTests.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CompletionIntegrationTests.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using Microsoft.VisualStudio.Extensibility.Testing; using Roslyn.Test.Utilities; using Xunit; using Xunit.Abstractions; @@ -475,15 +476,15 @@ private async Task CommitCompletionAndVerifyAsync(string expected, string expect var session = await TestServices.Editor.OpenCompletionSessionAndWaitForItemAsync(TimeSpan.FromSeconds(10), expectedSelectedItemLabel, HangMitigatingCancellationToken); Assert.NotNull(session); - if (commitChar.HasValue) + if (commitChar is char commitCharValue) { // Commit using the specified commit character - session.Commit(commitChar.Value, HangMitigatingCancellationToken); + session.Commit(commitCharValue, HangMitigatingCancellationToken); // session.Commit call above commits as if the commit character was typed, // but doesn't actually insert the character into the buffer. // So we still need to insert the character into the buffer ourselves. - TestServices.Input.Send(commitChar.Value.ToString()); + TestServices.Input.Send(commitCharValue.ToString()); } else { @@ -494,10 +495,10 @@ private async Task CommitCompletionAndVerifyAsync(string expected, string expect var stopwatch = new Stopwatch(); string text; - while ((text = textView.TextBuffer.CurrentSnapshot.GetText()) != expected && stopwatch.ElapsedMilliseconds < 10000) + while ((text = textView.TextBuffer.CurrentSnapshot.GetText()) != expected && stopwatch.ElapsedMilliseconds < EditorInProcess.DefaultCompletionWaitTimeMilliseconds) { // Text might get updated *after* completion by something like auto-insert, so wait for the desired text - await Task.Delay(100); + await Task.Delay(100, HangMitigatingCancellationToken); } // Snippets may have slight whitespace differences due to line endings. These diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/EditorInProcess_Completion.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/EditorInProcess_Completion.cs index 10f9aef9634..caf687e31ee 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/EditorInProcess_Completion.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/EditorInProcess_Completion.cs @@ -12,6 +12,8 @@ namespace Microsoft.VisualStudio.Extensibility.Testing; internal partial class EditorInProcess { + public const int DefaultCompletionWaitTimeMilliseconds = 10000; + public async Task DismissCompletionSessionsAsync(CancellationToken cancellationToken) { await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); @@ -87,7 +89,7 @@ public async Task DismissCompletionSessionsAsync(CancellationToken cancellationT return null; } - await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken); + await Task.Delay(100, cancellationToken); session.OpenOrUpdate(new CompletionTrigger(CompletionTriggerReason.Insertion, textView.TextSnapshot), textView.Caret.Position.BufferPosition, cancellationToken); }