561 lines
18 KiB
C#
561 lines
18 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Razor.Language;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.Host;
|
|
using Microsoft.CodeAnalysis.Razor;
|
|
using Microsoft.CodeAnalysis.Razor.Editor;
|
|
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
|
|
using Microsoft.VisualStudio.Editor.Razor.Documents;
|
|
using Microsoft.VisualStudio.Text;
|
|
using Microsoft.VisualStudio.Text.Editor;
|
|
using Microsoft.VisualStudio.Utilities;
|
|
using Moq;
|
|
using Xunit;
|
|
|
|
namespace Microsoft.VisualStudio.Editor.Razor
|
|
{
|
|
public class DefaultVisualStudioDocumentTrackerTest : ForegroundDispatcherTestBase
|
|
{
|
|
public DefaultVisualStudioDocumentTrackerTest()
|
|
{
|
|
RazorCoreContentType = Mock.Of<IContentType>(c => c.IsOfType(RazorLanguage.ContentType) == true);
|
|
TextBuffer = Mock.Of<ITextBuffer>(b => b.ContentType == RazorCoreContentType);
|
|
|
|
FilePath = "C:/Some/Path/TestDocumentTracker.cshtml";
|
|
ProjectPath = "C:/Some/Path/TestProject.csproj";
|
|
|
|
ImportDocumentManager = Mock.Of<ImportDocumentManager>();
|
|
WorkspaceEditorSettings = new DefaultWorkspaceEditorSettings(Mock.Of<ForegroundDispatcher>(), Mock.Of<EditorSettingsManager>());
|
|
|
|
TagHelperResolver = new TestTagHelperResolver();
|
|
SomeTagHelpers = new List<TagHelperDescriptor>()
|
|
{
|
|
TagHelperDescriptorBuilder.Create("test", "test").Build(),
|
|
};
|
|
|
|
HostServices = TestServices.Create(
|
|
new IWorkspaceService[] { },
|
|
new ILanguageService[] { TagHelperResolver, });
|
|
|
|
Workspace = TestWorkspace.Create(HostServices, w =>
|
|
{
|
|
WorkspaceProject = w.AddProject(ProjectInfo.Create(
|
|
ProjectId.CreateNewId(),
|
|
new VersionStamp(),
|
|
"Test1",
|
|
"TestAssembly",
|
|
LanguageNames.CSharp,
|
|
filePath: ProjectPath));
|
|
});
|
|
|
|
ProjectManager = new TestProjectSnapshotManager(Dispatcher, Workspace) { AllowNotifyListeners = true };
|
|
|
|
HostProject = new HostProject(ProjectPath, FallbackRazorConfiguration.MVC_2_1);
|
|
OtherHostProject = new HostProject(ProjectPath, FallbackRazorConfiguration.MVC_2_0);
|
|
|
|
DocumentTracker = new DefaultVisualStudioDocumentTracker(
|
|
Dispatcher,
|
|
FilePath,
|
|
ProjectPath,
|
|
ProjectManager,
|
|
WorkspaceEditorSettings,
|
|
Workspace,
|
|
TextBuffer,
|
|
ImportDocumentManager);
|
|
}
|
|
|
|
private IContentType RazorCoreContentType { get; }
|
|
|
|
private ITextBuffer TextBuffer { get; }
|
|
|
|
private string FilePath { get; }
|
|
|
|
private string ProjectPath { get; }
|
|
|
|
private HostProject HostProject { get; }
|
|
|
|
private HostProject OtherHostProject { get; }
|
|
|
|
private Project WorkspaceProject { get; set; }
|
|
|
|
private ImportDocumentManager ImportDocumentManager { get; }
|
|
|
|
private WorkspaceEditorSettings WorkspaceEditorSettings { get; }
|
|
|
|
private List<TagHelperDescriptor> SomeTagHelpers { get; }
|
|
|
|
private TestTagHelperResolver TagHelperResolver { get; }
|
|
|
|
private ProjectSnapshotManagerBase ProjectManager { get; }
|
|
|
|
private HostServices HostServices { get; }
|
|
|
|
private Workspace Workspace { get; }
|
|
|
|
private DefaultVisualStudioDocumentTracker DocumentTracker { get; }
|
|
|
|
[ForegroundFact]
|
|
public void Subscribe_NoopsIfAlreadySubscribed()
|
|
{
|
|
// Arrange
|
|
var callCount = 0;
|
|
DocumentTracker.ContextChanged += (sender, args) =>
|
|
{
|
|
callCount++;
|
|
};
|
|
DocumentTracker.Subscribe();
|
|
|
|
// Call count is 2 right now:
|
|
// 1 trigger for initial subscribe context changed.
|
|
// 1 trigger for TagHelpers being changed (computed).
|
|
|
|
// Act
|
|
DocumentTracker.Subscribe();
|
|
|
|
// Assert
|
|
Assert.Equal(2, callCount);
|
|
}
|
|
|
|
[ForegroundFact]
|
|
public void Unsubscribe_NoopsIfAlreadyUnsubscribed()
|
|
{
|
|
// Arrange
|
|
var callCount = 0;
|
|
DocumentTracker.Subscribe();
|
|
DocumentTracker.ContextChanged += (sender, args) =>
|
|
{
|
|
callCount++;
|
|
};
|
|
DocumentTracker.Unsubscribe();
|
|
|
|
// Act
|
|
DocumentTracker.Unsubscribe();
|
|
|
|
// Assert
|
|
Assert.Equal(1, callCount);
|
|
}
|
|
|
|
[ForegroundFact]
|
|
public void Unsubscribe_NoopsIfSubscribeHasBeenCalledMultipleTimes()
|
|
{
|
|
// Arrange
|
|
var callCount = 0;
|
|
DocumentTracker.Subscribe();
|
|
DocumentTracker.Subscribe();
|
|
DocumentTracker.ContextChanged += (sender, args) =>
|
|
{
|
|
callCount++;
|
|
};
|
|
|
|
// Act - 1
|
|
DocumentTracker.Unsubscribe();
|
|
|
|
// Assert - 1
|
|
Assert.Equal(0, callCount);
|
|
|
|
// Act - 2
|
|
DocumentTracker.Unsubscribe();
|
|
|
|
// Assert - 2
|
|
Assert.Equal(1, callCount);
|
|
}
|
|
|
|
[ForegroundFact]
|
|
public void EditorSettingsManager_Changed_TriggersContextChanged()
|
|
{
|
|
// Arrange
|
|
var called = false;
|
|
DocumentTracker.ContextChanged += (sender, args) =>
|
|
{
|
|
Assert.Equal(ContextChangeKind.EditorSettingsChanged, args.Kind);
|
|
called = true;
|
|
Assert.Equal(ContextChangeKind.EditorSettingsChanged, args.Kind);
|
|
};
|
|
|
|
// Act
|
|
DocumentTracker.EditorSettingsManager_Changed(null, null);
|
|
|
|
// Assert
|
|
Assert.True(called);
|
|
}
|
|
|
|
[ForegroundFact]
|
|
public void ProjectManager_Changed_ProjectAdded_TriggersContextChanged()
|
|
{
|
|
// Arrange
|
|
ProjectManager.HostProjectAdded(HostProject);
|
|
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
|
|
|
|
var e = new ProjectChangeEventArgs(ProjectPath, ProjectChangeKind.ProjectAdded);
|
|
|
|
var called = false;
|
|
DocumentTracker.ContextChanged += (sender, args) =>
|
|
{
|
|
Assert.Equal(ContextChangeKind.ProjectChanged, args.Kind);
|
|
called = true;
|
|
|
|
Assert.Same(ProjectManager.GetLoadedProject(DocumentTracker.ProjectPath), DocumentTracker.ProjectSnapshot);
|
|
};
|
|
|
|
// Act
|
|
DocumentTracker.ProjectManager_Changed(ProjectManager, e);
|
|
|
|
// Assert
|
|
Assert.True(called);
|
|
}
|
|
|
|
[ForegroundFact]
|
|
public void ProjectManager_Changed_ProjectChanged_TriggersContextChanged()
|
|
{
|
|
// Arrange
|
|
ProjectManager.HostProjectAdded(HostProject);
|
|
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
|
|
|
|
var e = new ProjectChangeEventArgs(ProjectPath, ProjectChangeKind.ProjectChanged);
|
|
|
|
var called = false;
|
|
DocumentTracker.ContextChanged += (sender, args) =>
|
|
{
|
|
Assert.Equal(ContextChangeKind.ProjectChanged, args.Kind);
|
|
called = true;
|
|
|
|
Assert.Same(ProjectManager.GetLoadedProject(DocumentTracker.ProjectPath), DocumentTracker.ProjectSnapshot);
|
|
};
|
|
|
|
// Act
|
|
DocumentTracker.ProjectManager_Changed(ProjectManager, e);
|
|
|
|
// Assert
|
|
Assert.True(called);
|
|
}
|
|
|
|
[ForegroundFact]
|
|
public void ProjectManager_Changed_ProjectRemoved_TriggersContextChanged_WithEphemeralProject()
|
|
{
|
|
// Arrange
|
|
var e = new ProjectChangeEventArgs(ProjectPath, ProjectChangeKind.ProjectRemoved);
|
|
|
|
var called = false;
|
|
DocumentTracker.ContextChanged += (sender, args) =>
|
|
{
|
|
// This can be called both with tag helper and project changes.
|
|
called = true;
|
|
|
|
Assert.IsType<EphemeralProjectSnapshot>(DocumentTracker.ProjectSnapshot);
|
|
};
|
|
|
|
// Act
|
|
DocumentTracker.ProjectManager_Changed(ProjectManager, e);
|
|
|
|
// Assert
|
|
Assert.True(called);
|
|
}
|
|
|
|
[ForegroundFact]
|
|
public void ProjectManager_Changed_IgnoresUnknownProject()
|
|
{
|
|
// Arrange
|
|
var e = new ProjectChangeEventArgs("c:/OtherPath/OtherProject.csproj", ProjectChangeKind.ProjectChanged);
|
|
|
|
var called = false;
|
|
DocumentTracker.ContextChanged += (sender, args) =>
|
|
{
|
|
called = true;
|
|
};
|
|
|
|
// Act
|
|
DocumentTracker.ProjectManager_Changed(ProjectManager, e);
|
|
|
|
// Assert
|
|
Assert.False(called);
|
|
}
|
|
|
|
[ForegroundFact]
|
|
public void Import_Changed_ImportAssociatedWithDocument_TriggersContextChanged()
|
|
{
|
|
// Arrange
|
|
var called = false;
|
|
DocumentTracker.ContextChanged += (sender, args) =>
|
|
{
|
|
Assert.Equal(ContextChangeKind.ImportsChanged, args.Kind);
|
|
called = true;
|
|
};
|
|
|
|
var importChangedArgs = new ImportChangedEventArgs("path/to/import", FileChangeKind.Changed, new[] { FilePath });
|
|
|
|
// Act
|
|
DocumentTracker.Import_Changed(null, importChangedArgs);
|
|
|
|
// Assert
|
|
Assert.True(called);
|
|
}
|
|
|
|
[ForegroundFact]
|
|
public void Import_Changed_UnrelatedImport_DoesNothing()
|
|
{
|
|
// Arrange
|
|
DocumentTracker.ContextChanged += (sender, args) =>
|
|
{
|
|
throw new InvalidOperationException();
|
|
};
|
|
|
|
var importChangedArgs = new ImportChangedEventArgs("path/to/import", FileChangeKind.Changed, new[] { "path/to/differentfile" });
|
|
|
|
// Act & Assert (Does not throw)
|
|
DocumentTracker.Import_Changed(null, importChangedArgs);
|
|
}
|
|
|
|
[ForegroundFact]
|
|
public void Subscribe_SetsSupportedProjectAndTriggersContextChanged()
|
|
{
|
|
// Arrange
|
|
var called = false;
|
|
DocumentTracker.ContextChanged += (sender, args) =>
|
|
{
|
|
called = true; // This will trigger both ContextChanged and TagHelprsChanged
|
|
};
|
|
|
|
// Act
|
|
DocumentTracker.Subscribe();
|
|
|
|
// Assert
|
|
Assert.True(called);
|
|
Assert.True(DocumentTracker.IsSupportedProject);
|
|
}
|
|
|
|
[ForegroundFact]
|
|
public void Unsubscribe_ResetsSupportedProjectAndTriggersContextChanged()
|
|
{
|
|
// Arrange
|
|
|
|
// Subscribe once to set supported project
|
|
DocumentTracker.Subscribe();
|
|
|
|
var called = false;
|
|
DocumentTracker.ContextChanged += (sender, args) =>
|
|
{
|
|
called = true;
|
|
Assert.Equal(ContextChangeKind.ProjectChanged, args.Kind);
|
|
};
|
|
|
|
// Act
|
|
DocumentTracker.Unsubscribe();
|
|
|
|
// Assert
|
|
Assert.False(DocumentTracker.IsSupportedProject);
|
|
Assert.True(called);
|
|
}
|
|
|
|
[ForegroundFact]
|
|
public void AddTextView_AddsToTextViewCollection()
|
|
{
|
|
// Arrange
|
|
var textView = Mock.Of<ITextView>();
|
|
|
|
// Act
|
|
DocumentTracker.AddTextView(textView);
|
|
|
|
// Assert
|
|
Assert.Collection(DocumentTracker.TextViews, v => Assert.Same(v, textView));
|
|
}
|
|
|
|
[ForegroundFact]
|
|
public void AddTextView_DoesNotAddDuplicateTextViews()
|
|
{
|
|
// Arrange
|
|
var textView = Mock.Of<ITextView>();
|
|
|
|
// Act
|
|
DocumentTracker.AddTextView(textView);
|
|
DocumentTracker.AddTextView(textView);
|
|
|
|
// Assert
|
|
Assert.Collection(DocumentTracker.TextViews, v => Assert.Same(v, textView));
|
|
}
|
|
|
|
[ForegroundFact]
|
|
public void AddTextView_AddsMultipleTextViewsToCollection()
|
|
{
|
|
// Arrange
|
|
var textView1 = Mock.Of<ITextView>();
|
|
var textView2 = Mock.Of<ITextView>();
|
|
|
|
// Act
|
|
DocumentTracker.AddTextView(textView1);
|
|
DocumentTracker.AddTextView(textView2);
|
|
|
|
// Assert
|
|
Assert.Collection(
|
|
DocumentTracker.TextViews,
|
|
v => Assert.Same(v, textView1),
|
|
v => Assert.Same(v, textView2));
|
|
}
|
|
|
|
[ForegroundFact]
|
|
public void RemoveTextView_RemovesTextViewFromCollection_SingleItem()
|
|
{
|
|
// Arrange
|
|
var textView = Mock.Of<ITextView>();
|
|
DocumentTracker.AddTextView(textView);
|
|
|
|
// Act
|
|
DocumentTracker.RemoveTextView(textView);
|
|
|
|
// Assert
|
|
Assert.Empty(DocumentTracker.TextViews);
|
|
}
|
|
|
|
[ForegroundFact]
|
|
public void RemoveTextView_RemovesTextViewFromCollection_MultipleItems()
|
|
{
|
|
// Arrange
|
|
var textView1 = Mock.Of<ITextView>();
|
|
var textView2 = Mock.Of<ITextView>();
|
|
var textView3 = Mock.Of<ITextView>();
|
|
DocumentTracker.AddTextView(textView1);
|
|
DocumentTracker.AddTextView(textView2);
|
|
DocumentTracker.AddTextView(textView3);
|
|
|
|
// Act
|
|
DocumentTracker.RemoveTextView(textView2);
|
|
|
|
// Assert
|
|
Assert.Collection(
|
|
DocumentTracker.TextViews,
|
|
v => Assert.Same(v, textView1),
|
|
v => Assert.Same(v, textView3));
|
|
}
|
|
|
|
[ForegroundFact]
|
|
public void RemoveTextView_NoopsWhenRemovingTextViewNotInCollection()
|
|
{
|
|
// Arrange
|
|
var textView1 = Mock.Of<ITextView>();
|
|
DocumentTracker.AddTextView(textView1);
|
|
var textView2 = Mock.Of<ITextView>();
|
|
|
|
// Act
|
|
DocumentTracker.RemoveTextView(textView2);
|
|
|
|
// Assert
|
|
Assert.Collection(DocumentTracker.TextViews, v => Assert.Same(v, textView1));
|
|
}
|
|
|
|
|
|
[ForegroundFact]
|
|
public void Subscribed_InitializesEphemeralProjectSnapshot()
|
|
{
|
|
// Arrange
|
|
|
|
// Act
|
|
DocumentTracker.Subscribe();
|
|
|
|
// Assert
|
|
Assert.IsType<EphemeralProjectSnapshot>(DocumentTracker.ProjectSnapshot);
|
|
}
|
|
|
|
[ForegroundFact]
|
|
public void Subscribed_InitializesRealProjectSnapshot()
|
|
{
|
|
// Arrange
|
|
ProjectManager.HostProjectAdded(HostProject);
|
|
|
|
// Act
|
|
DocumentTracker.Subscribe();
|
|
|
|
// Assert
|
|
Assert.IsType<DefaultProjectSnapshot>(DocumentTracker.ProjectSnapshot);
|
|
}
|
|
|
|
[ForegroundFact]
|
|
public async Task Subscribed_ListensToProjectChanges()
|
|
{
|
|
// Arrange
|
|
ProjectManager.HostProjectAdded(HostProject);
|
|
|
|
DocumentTracker.Subscribe();
|
|
|
|
await DocumentTracker.PendingTagHelperTask;
|
|
|
|
// There can be multiple args here because the tag helpers will return
|
|
// immediately and trigger another ContextChanged.
|
|
List<ContextChangeEventArgs> args = new List<ContextChangeEventArgs>();
|
|
DocumentTracker.ContextChanged += (sender, e) => { args.Add(e); };
|
|
|
|
// Act
|
|
ProjectManager.HostProjectChanged(OtherHostProject);
|
|
await DocumentTracker.PendingTagHelperTask;
|
|
|
|
// Assert
|
|
var snapshot = Assert.IsType<DefaultProjectSnapshot>(DocumentTracker.ProjectSnapshot);
|
|
|
|
Assert.Same(OtherHostProject, snapshot.HostProject);
|
|
|
|
Assert.Collection(
|
|
args,
|
|
e => Assert.Equal(ContextChangeKind.ProjectChanged, e.Kind),
|
|
e => Assert.Equal(ContextChangeKind.TagHelpersChanged, e.Kind));
|
|
}
|
|
|
|
[ForegroundFact]
|
|
public async Task Subscribed_ListensToProjectRemoval()
|
|
{
|
|
// Arrange
|
|
ProjectManager.HostProjectAdded(HostProject);
|
|
|
|
DocumentTracker.Subscribe();
|
|
|
|
await DocumentTracker.PendingTagHelperTask;
|
|
|
|
List<ContextChangeEventArgs> args = new List<ContextChangeEventArgs>();
|
|
DocumentTracker.ContextChanged += (sender, e) => { args.Add(e); };
|
|
|
|
// Act
|
|
ProjectManager.HostProjectRemoved(HostProject);
|
|
await DocumentTracker.PendingTagHelperTask;
|
|
|
|
// Assert
|
|
Assert.IsType<EphemeralProjectSnapshot>(DocumentTracker.ProjectSnapshot);
|
|
|
|
Assert.Collection(
|
|
args,
|
|
e => Assert.Equal(ContextChangeKind.ProjectChanged, e.Kind),
|
|
e => Assert.Equal(ContextChangeKind.TagHelpersChanged, e.Kind));
|
|
}
|
|
|
|
[ForegroundFact]
|
|
public async Task Subscribed_ListensToProjectChanges_ComputesTagHelpers()
|
|
{
|
|
// Arrange
|
|
TagHelperResolver.CompletionSource = new TaskCompletionSource<TagHelperResolutionResult>();
|
|
|
|
ProjectManager.HostProjectAdded(HostProject);
|
|
|
|
DocumentTracker.Subscribe();
|
|
|
|
// We haven't let the tag helpers complete yet
|
|
Assert.False(DocumentTracker.PendingTagHelperTask.IsCompleted);
|
|
Assert.Empty(DocumentTracker.TagHelpers);
|
|
|
|
List<ContextChangeEventArgs> args = new List<ContextChangeEventArgs>();
|
|
DocumentTracker.ContextChanged += (sender, e) => { args.Add(e); };
|
|
|
|
// Act
|
|
TagHelperResolver.CompletionSource.SetResult(new TagHelperResolutionResult(SomeTagHelpers, Array.Empty<RazorDiagnostic>()));
|
|
await DocumentTracker.PendingTagHelperTask;
|
|
|
|
// Assert
|
|
Assert.Same(DocumentTracker.TagHelpers, SomeTagHelpers);
|
|
|
|
Assert.Collection(
|
|
args,
|
|
e => Assert.Equal(ContextChangeKind.TagHelpersChanged, e.Kind));
|
|
}
|
|
}
|
|
}
|