File: DiagnosticsTests.cs
Web Access
Project: src\src\Razor\src\Razor\test\Microsoft.VisualStudioCode.Razor.IntegrationTests\Microsoft.VisualStudioCode.Razor.IntegrationTests.csproj (Microsoft.VisualStudioCode.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 Microsoft.VisualStudioCode.Razor.IntegrationTests.Services;
using Xunit;
using Xunit.Abstractions;
 
namespace Microsoft.VisualStudioCode.Razor.IntegrationTests;
 
/// <summary>
/// E2E tests for diagnostics (error squiggles) in Razor files.
/// </summary>
public class DiagnosticsTests(ITestOutputHelper output) : VSCodeIntegrationTestBase(output)
{
    [Fact]
    public Task Diagnostics_CSharpSyntaxError_ShowsInProblemsPanel() => ScreenshotOnFailureAsync(async () =>
    {
        // Arrange
        await TestServices.Editor.OpenFileAsync("Components/Pages/Home.razor");
        await TestServices.Diagnostics.OpenProblemsPanelAsync();
 
        // Act - type something that will cause a C# error (missing semicolon)
        await TestServices.Editor.SelectAllAsync();
        await TestServices.Input.TypeAsync("@{ int x = 5 }"); // Missing semicolon
 
        // Assert - wait for CS1002 (semicolon expected) to appear in Problems panel
        await TestServices.Diagnostics.WaitForProblemAsync("CS1002", timeout: TimeSpan.FromSeconds(5));
    });
 
    [Fact]
    public Task Diagnostics_FixError_RemovesFromProblemsPanel() => ScreenshotOnFailureAsync(async () =>
    {
        // Arrange
        await TestServices.Editor.OpenFileAsync("Components/Pages/Home.razor");
        await TestServices.Diagnostics.OpenProblemsPanelAsync();
 
        // Introduce an error
        await TestServices.Editor.SelectAllAsync();
        await TestServices.Input.TypeAsync("@{ int x = }"); // Missing value - CS1525
 
        // Wait for error to appear
        await TestServices.Diagnostics.WaitForDiagnosticsAsync(expectErrors: true, timeout: TimeSpan.FromSeconds(5));
 
        // Act - fix the error by adding the missing value
        await TestServices.Input.PressAsync(SpecialKey.Backspace);
        await TestServices.Input.TypeAsync("5; }");
 
        // Assert - wait for errors to disappear
        await TestServices.Diagnostics.WaitForDiagnosticsAsync(expectErrors: false, timeout: TimeSpan.FromSeconds(5));
    });
 
    [Fact]
    public Task Diagnostics_ValidFile_NoErrors() => ScreenshotOnFailureAsync(async () =>
    {
        // Arrange
        await TestServices.Editor.OpenFileAsync("Components/Pages/Counter.razor");
        await TestServices.Diagnostics.OpenProblemsPanelAsync();
 
        // Wait for diagnostics to settle - expect no errors on a valid file
        await TestServices.Diagnostics.WaitForDiagnosticsAsync(expectErrors: false, timeout: TimeSpan.FromSeconds(10));
 
        // Act
        var hasErrors = await TestServices.Diagnostics.HasErrorsAsync();
 
        // Assert
        Assert.False(hasErrors, "Valid Razor file should have no error diagnostics");
    });
 
    [Fact]
    public Task Diagnostics_UnclosedTag_ShowsRZ9980() => ScreenshotOnFailureAsync(async () =>
    {
        var fileName = Path.Combine(TestServices.Workspace.WorkspacePath, "Components/Pages/Home.razor");
        var contents = File.ReadAllText(fileName);
        contents = contents.Replace("</PageTitle>", ""); // Remove closing tag to introduce error
        File.WriteAllText(fileName, contents);
 
        await TestServices.Editor.OpenFileAsync("Components/Pages/Home.razor");
 
        await TestServices.Diagnostics.OpenProblemsPanelAsync();
 
        await TestServices.Diagnostics.WaitForProblemAsync("RZ1034", timeout: TimeSpan.FromSeconds(5));
 
        var problems = await TestServices.Diagnostics.GetProblemsAsync();
        Assert.Contains(problems, p => p.Contains("RZ1034"));
    });
 
    [Fact]
    public Task Diagnostics_UnclosedComponentTag_ShowsRZ9980() => ScreenshotOnFailureAsync(async () =>
    {
        // Typing is problematic, and automatic close tag insertion gets in the way, so it's easier
        // to just modify the file on disk directly.
        var fileName = Path.Combine(TestServices.Workspace.WorkspacePath, "Components/Pages/Home.razor");
        var contents = File.ReadAllText(fileName);
        contents = contents.Replace("</h1>", ""); // Remove closing tag to introduce error
        File.WriteAllText(fileName, contents);
 
        await TestServices.Editor.OpenFileAsync("Components/Pages/Home.razor");
 
        await TestServices.Diagnostics.OpenProblemsPanelAsync();
 
        await TestServices.Diagnostics.WaitForProblemAsync("RZ9980", timeout: TimeSpan.FromSeconds(5));
 
        var problems = await TestServices.Diagnostics.GetProblemsAsync();
        Assert.Contains(problems, p => p.Contains("RZ9980"));
    });
 
    [Fact]
    public Task Diagnostics_UnusedDirective_IsFadedWithoutWarningSquiggle() => ScreenshotOnFailureAsync(async () =>
    {
        var fileName = Path.Combine(TestServices.Workspace.WorkspacePath, "UnusedDirective.cshtml");
        await File.WriteAllTextAsync(fileName, """
            @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
 
            <p>Hello</p>
            """);
 
        await TestServices.Editor.OpenFileAsync("UnusedDirective.cshtml");
 
        await TestServices.Diagnostics.WaitForUnnecessaryCodeAsync(timeout: TimeSpan.FromSeconds(10));
 
        var hasWarnings = await TestServices.Diagnostics.HasWarningsAsync();
        Assert.False(hasWarnings, "Unused directives should be faded in VS Code, not shown with warning squiggles.");
 
        await TestServices.Diagnostics.OpenProblemsPanelAsync();
        var problems = await TestServices.Diagnostics.GetProblemsAsync();
        Assert.DoesNotContain(problems, p => p.Contains("RZ0005", StringComparison.OrdinalIgnoreCase));
    });
}