File: ScopedCssIntegrationTests.cs
Web Access
Project: ..\..\..\test\Microsoft.NET.Sdk.StaticWebAssets.Tests\Microsoft.NET.Sdk.StaticWebAssets.Tests.csproj (Microsoft.NET.Sdk.StaticWebAssets.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
using System.Text.Json;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.StaticWebAssets.Tasks;
 
namespace Microsoft.NET.Sdk.StaticWebAssets.Tests
{
    public class ScopedCssIntegrationTest(ITestOutputHelper log)
        : IsolatedNuGetPackageFolderAspNetSdkBaselineTest(log, nameof(ScopedCssIntegrationTest))
    {
        [Fact]
        public void Build_NoOps_WhenScopedCssIsDisabled()
        {
            var testAsset = "RazorComponentApp";
            var projectDirectory = CreateAspNetSdkTestAsset(testAsset);
 
            var build = CreateBuildCommand(projectDirectory);
            ExecuteCommand(build, "/p:ScopedCssEnabled=false").Should().Pass();
 
            var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm);
 
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().NotExist();
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css")).Should().NotExist();
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "ComponentApp.styles.css")).Should().NotExist();
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "FetchData.razor.rz.scp.css")).Should().NotExist();
        }
 
        [Fact]
        public void Build_NoOps_ForMvcApp_WhenScopedCssIsDisabled()
        {
            var testAsset = "RazorSimpleMvc";
            var projectDirectory = CreateAspNetSdkTestAsset(testAsset);
 
            var build = CreateBuildCommand(projectDirectory);
            ExecuteCommand(build, "/p:ScopedCssEnabled=false").Should().Pass();
 
            var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm);
 
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Index.cshtml.rz.scp.css")).Should().NotExist();
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Contact.cshtml.rz.scp.css")).Should().NotExist();
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "SimpleMvc.styles.css")).Should().NotExist();
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "About.cshtml.rz.scp.css")).Should().NotExist();
        }
 
        [Fact]
        public void CanDisableDefaultDiscoveryConvention()
        {
            var testAsset = "RazorComponentApp";
            var projectDirectory = CreateAspNetSdkTestAsset(testAsset);
 
            var build = CreateBuildCommand(projectDirectory);
            ExecuteCommand(build, "/p:EnableDefaultScopedCssItems=false").Should().Pass();
 
            var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm);
 
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().NotExist();
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css")).Should().NotExist();
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "ComponentApp.styles.css")).Should().NotExist();
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "FetchData.razor.rz.scp.css")).Should().NotExist();
        }
 
        [CoreMSBuildOnlyFact]
        public void CanOverrideScopeIdentifiers()
        {
            var testAsset = "RazorComponentApp";
            var projectDirectory = CreateAspNetSdkTestAsset(testAsset)
                .WithProjectChanges(project =>
                {
                    var ns = project.Root.Name.Namespace;
                    var itemGroup = new XElement(ns + "ItemGroup");
                    var element = new XElement("ScopedCssInput", new XAttribute("Include", @"Styles\Pages\Counter.css"));
                    element.Add(new XElement("RazorComponent", @"Components\Pages\Counter.razor"));
                    element.Add(new XElement("CssScope", "b-overridden"));
                    itemGroup.Add(element);
                    project.Root.Add(itemGroup);
                });
 
            var stylesFolder = Path.Combine(projectDirectory.Path, "Styles", "Pages");
            Directory.CreateDirectory(stylesFolder);
            var styles = Path.Combine(stylesFolder, "Counter.css");
            File.Move(Path.Combine(projectDirectory.Path, "Components", "Pages", "Counter.razor.css"), styles);
 
            var build = CreateBuildCommand(projectDirectory);
            ExecuteCommand(build, "/p:EnableDefaultScopedCssItems=false", "/p:EmitCompilerGeneratedFiles=true").Should().Pass();
 
            var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm);
 
            var scoped = Path.Combine(intermediateOutputPath, "scopedcss", "Styles", "Pages", "Counter.rz.scp.css");
            new FileInfo(scoped).Should().Exist();
            new FileInfo(scoped).Should().Contain("b-overridden");
            var generated = Path.Combine(intermediateOutputPath, "generated", "Microsoft.CodeAnalysis.Razor.Compiler", "Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator", "Components_Pages_Counter_razor.g.cs");
            new FileInfo(generated).Should().Exist();
            new FileInfo(generated).Should().Contain("b-overridden");
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css")).Should().NotExist();
        }
 
        [Fact]
        public void Build_GeneratesTransformedFilesAndBundle_ForComponentsWithScopedCss()
        {
            var testAsset = "RazorComponentApp";
            var projectDirectory = CreateAspNetSdkTestAsset(testAsset);
 
            var build = CreateBuildCommand(projectDirectory);
            ExecuteCommand(build).Should().Pass();
 
            var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm);
 
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().Exist();
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css")).Should().Exist();
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "ComponentApp.styles.css")).Should().Exist();
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "projectbundle", "ComponentApp.bundle.scp.css")).Should().Exist();
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "FetchData.razor.rz.scp.css")).Should().NotExist();
        }
 
        [Fact]
        public void Build_GeneratesTransformedFilesAndBundle_ForViewsWithScopedCss()
        {
            var testAsset = "RazorSimpleMvc";
            var projectDirectory = CreateAspNetSdkTestAsset(testAsset);
 
            var build = CreateBuildCommand(projectDirectory);
            ExecuteCommand(build).Should().Pass();
 
            var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm);
 
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Index.cshtml.rz.scp.css")).Should().Exist();
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Contact.cshtml.rz.scp.css")).Should().Exist();
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "SimpleMvc.styles.css")).Should().Exist();
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "projectbundle", "SimpleMvc.bundle.scp.css")).Should().Exist();
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "About.cshtml.rz.scp.css")).Should().Exist();
        }
 
        [Fact]
        public void Build_ScopedCssFiles_ContainsUniqueScopesPerFile()
        {
            var testAsset = "RazorComponentApp";
            var projectDirectory = CreateAspNetSdkTestAsset(testAsset);
 
            var build = CreateBuildCommand(projectDirectory);
            ExecuteCommand(build).Should().Pass();
 
            var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm);
 
            var generatedCounter = Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css");
            new FileInfo(generatedCounter).Should().Exist();
            var generatedIndex = Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css");
            new FileInfo(generatedIndex).Should().Exist();
            var counterContent = File.ReadAllText(generatedCounter);
            var indexContent = File.ReadAllText(generatedIndex);
 
            var counterScopeMatch = Regex.Match(counterContent, ".*button\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase);
            Assert.True(counterScopeMatch.Success, "Couldn't find a scope id in the generated Counter scoped css file.");
            var counterScopeId = counterScopeMatch.Groups[1].Captures[0].Value;
 
            var indexScopeMatch = Regex.Match(indexContent, ".*h1\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase);
            Assert.True(indexScopeMatch.Success, "Couldn't find a scope id in the generated Index scoped css file.");
            var indexScopeId = indexScopeMatch.Groups[1].Captures[0].Value;
 
            Assert.NotEqual(counterScopeId, indexScopeId);
        }
 
        [Fact]
        public void Build_ScopedCssViews_ContainsUniqueScopesPerView()
        {
            var testAsset = "RazorSimpleMvc";
            var projectDirectory = CreateAspNetSdkTestAsset(testAsset);
 
            var build = CreateBuildCommand(projectDirectory);
            ExecuteCommand(build).Should().Pass();
 
            var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm);
 
            var generatedIndex = Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Index.cshtml.rz.scp.css");
            new FileInfo(generatedIndex).Should().Exist();
            var generatedAbout = Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "About.cshtml.rz.scp.css");
            new FileInfo(generatedAbout).Should().Exist();
            var generatedContact = Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Contact.cshtml.rz.scp.css");
            new FileInfo(generatedContact).Should().Exist();
            var indexContent = File.ReadAllText(generatedIndex);
            var aboutContent = File.ReadAllText(generatedAbout);
            var contactContent = File.ReadAllText(generatedContact);
 
            var indexScopeMatch = Regex.Match(indexContent, ".*p\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase);
            Assert.True(indexScopeMatch.Success, "Couldn't find a scope id in the generated Index scoped css file.");
            var indexScopeId = indexScopeMatch.Groups[1].Captures[0].Value;
 
            var aboutScopeMatch = Regex.Match(aboutContent, ".*h2\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase);
            Assert.True(aboutScopeMatch.Success, "Couldn't find a scope id in the generated About scoped css file.");
            var aboutScopeId = aboutScopeMatch.Groups[1].Captures[0].Value;
 
            var contactScopeMatch = Regex.Match(contactContent, ".*a\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase);
            Assert.True(contactScopeMatch.Success, "Couldn't find a scope id in the generated Contact scoped css file.");
            var contactScopeId = contactScopeMatch.Groups[1].Captures[0].Value;
 
            Assert.NotEqual(indexScopeId, aboutScopeId);
            Assert.NotEqual(indexScopeId, contactScopeId);
            Assert.NotEqual(aboutScopeId, contactScopeId);
        }
 
        [Fact]
        public void Build_WorksWhenViewsAndComponentsArePartOfTheSameProject_ContainsUniqueScopesPerFile()
        {
            var testAsset = "RazorMvcWithComponents";
            var projectDirectory = CreateAspNetSdkTestAsset(testAsset);
 
            var build = CreateBuildCommand(projectDirectory);
            ExecuteCommand(build).Should().Pass();
 
            var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm);
 
            var generatedIndex = Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Index.cshtml.rz.scp.css");
            new FileInfo(generatedIndex).Should().Exist();
 
            var generatedCounter = Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Counter.razor.rz.scp.css");
            new FileInfo(generatedCounter).Should().Exist();
 
            var indexContent = File.ReadAllText(generatedIndex);
            var counterContent = File.ReadAllText(generatedCounter);
 
            var indexScopeMatch = Regex.Match(indexContent, ".*p\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase);
            Assert.True(indexScopeMatch.Success, "Couldn't find a scope id in the generated Index scoped css file.");
            var indexScopeId = indexScopeMatch.Groups[1].Captures[0].Value;
 
            var counterScopeMatch = Regex.Match(counterContent, ".*div\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase);
            Assert.True(counterScopeMatch.Success, "Couldn't find a scope id in the generated Counter scoped css file.");
            var counterScopeId = counterScopeMatch.Groups[1].Captures[0].Value;
 
            Assert.NotEqual(indexScopeId, counterScopeId);
        }
 
        [Fact]
        public void Publish_PublishesScopedCssBundleToTheRightLocation()
        {
            var testAsset = "RazorComponentApp";
            var projectDirectory = CreateAspNetSdkTestAsset(testAsset);
 
            var publish = CreatePublishCommand(projectDirectory);
            ExecuteCommand(publish).Should().Pass();
 
            var publishOutputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString();
 
            new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "ComponentApp.styles.css")).Should().Exist();
            new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Index.razor.rz.scp.css")).Should().NotExist();
            new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().NotExist();
        }
 
        [Fact]
        public void Publish_NoBuild_PublishesBundleToTheRightLocation()
        {
            var testAsset = "RazorComponentApp";
            var projectDirectory = CreateAspNetSdkTestAsset(testAsset);
 
            var build = CreateBuildCommand(projectDirectory);
            var buildResult = ExecuteCommand(build);
            buildResult.Should().Pass();
 
            var publish = CreatePublishCommand(projectDirectory);
            ExecuteCommand(publish, "/p:NoBuild=true").Should().Pass();
 
            var publishOutputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString();
 
            new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "ComponentApp.styles.css")).Should().Exist();
            new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Index.razor.rz.scp.css")).Should().NotExist();
            new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().NotExist();
        }
 
        [Fact]
        public void Publish_DoesNotPublishAnyFile_WhenThereAreNoScopedCssFiles()
        {
            var testAsset = "RazorComponentApp";
            var projectDirectory = CreateAspNetSdkTestAsset(testAsset);
 
            File.Delete(Path.Combine(projectDirectory.Path, "Components", "Pages", "Counter.razor.css"));
            File.Delete(Path.Combine(projectDirectory.Path, "Components", "Pages", "Index.razor.css"));
 
            var publish = CreatePublishCommand(projectDirectory);
            ExecuteCommand(publish).Should().Pass();
 
            var publishOutputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString();
 
            new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "_framework", "scoped.styles.css")).Should().NotExist();
        }
 
        [Fact]
        public void Publish_Publishes_IndividualScopedCssFiles_WhenNoBundlingIsEnabled()
        {
            var testAsset = "RazorComponentApp";
            var projectDirectory = CreateAspNetSdkTestAsset(testAsset);
 
            var publish = CreatePublishCommand(projectDirectory);
            ExecuteCommand(publish, "/p:DisableScopedCssBundling=true").Should().Pass();
 
            var publishOutputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString();
 
            new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "ComponentApp.styles.css")).Should().NotExist();
 
            new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "Components", "Pages", "Index.razor.rz.scp.css")).Should().Exist();
            new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().Exist();
        }
 
        [CoreMSBuildOnlyFact]
        public void Build_RemovingScopedCssAndBuilding_UpdatesGeneratedCodeAndBundle()
        {
            var testAsset = "RazorComponentApp";
            var projectDirectory = CreateAspNetSdkTestAsset(testAsset);
 
            var build = CreateBuildCommand(projectDirectory);
            ExecuteCommand(build, "/p:EmitCompilerGeneratedFiles=true").Should().Pass();
 
            var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm);
 
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().Exist();
            var generatedBundle = Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "ComponentApp.styles.css");
            new FileInfo(generatedBundle).Should().Exist();
            var generatedProjectBundle = Path.Combine(intermediateOutputPath, "scopedcss", "projectbundle", "ComponentApp.bundle.scp.css");
            new FileInfo(generatedProjectBundle).Should().Exist();
            var generatedCounter = Path.Combine(intermediateOutputPath, "generated", "Microsoft.CodeAnalysis.Razor.Compiler", "Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator", "Components_Pages_Counter_razor.g.cs");
            new FileInfo(generatedCounter).Should().Exist();
 
            var componentThumbprint = FileThumbPrint.Create(generatedCounter);
            var bundleThumbprint = FileThumbPrint.Create(generatedBundle);
 
            File.Delete(Path.Combine(projectDirectory.Path, "Components", "Pages", "Counter.razor.css"));
 
            build = CreateBuildCommand(projectDirectory);
            ExecuteCommand(build, "/p:EmitCompilerGeneratedFiles=true").Should().Pass();
 
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().NotExist();
            new FileInfo(generatedCounter).Should().Exist();
 
            var newComponentThumbprint = FileThumbPrint.Create(generatedCounter);
            var newBundleThumbprint = FileThumbPrint.Create(generatedBundle);
 
            Assert.NotEqual(componentThumbprint, newComponentThumbprint);
            Assert.NotEqual(bundleThumbprint, newBundleThumbprint);
        }
 
        [Fact]
        public void Does_Nothing_WhenThereAreNoScopedCssFiles()
        {
            var testAsset = "RazorComponentApp";
            var projectDirectory = CreateAspNetSdkTestAsset(testAsset);
 
            File.Delete(Path.Combine(projectDirectory.Path, "Components", "Pages", "Counter.razor.css"));
            File.Delete(Path.Combine(projectDirectory.Path, "Components", "Pages", "Index.razor.css"));
 
            var build = CreateBuildCommand(projectDirectory);
            ExecuteCommand(build).Should().Pass();
 
            var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm);
 
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().NotExist();
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css")).Should().NotExist();
            new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "_framework", "scoped.styles.css")).Should().NotExist();
        }
 
        [Fact]
        public void Build_ScopedCssTransformation_AndBundling_IsIncremental()
        {
            // Arrange
            var thumbprintLookup = new Dictionary<string, FileThumbPrint>();
 
            var testAsset = "RazorComponentApp";
            var projectDirectory = CreateAspNetSdkTestAsset(testAsset);
 
            // Act & Assert 1
            var build = CreateBuildCommand(projectDirectory);
            ExecuteCommand(build).Should().Pass();
 
            var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm);
            var directoryPath = Path.Combine(intermediateOutputPath, "scopedcss");
 
            var files = Directory.GetFiles(directoryPath, "*", SearchOption.AllDirectories);
            foreach (var file in files)
            {
                var thumbprint = FileThumbPrint.Create(file);
                thumbprintLookup[file] = thumbprint;
            }
 
            // Act & Assert 2
            for (var i = 0; i < 2; i++)
            {
                build = CreateBuildCommand(projectDirectory);
                ExecuteCommand(build).Should().Pass();
 
                foreach (var file in files)
                {
                    var thumbprint = FileThumbPrint.Create(file);
                    Assert.Equal(thumbprintLookup[file], thumbprint);
                }
            }
        }
 
        // This test verifies if the targets that VS calls to update scoped css works to update these files
        [Fact]
        public void RegeneratingScopedCss_ForProject()
        {
            // Arrange
            var testAsset = "RazorComponentApp";
            ProjectDirectory = CreateAspNetSdkTestAsset(testAsset);
 
            var build = CreateBuildCommand(ProjectDirectory);
            ExecuteCommand(build).Should().Pass();
 
            var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
            var bundlePath = Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "ComponentApp.styles.css");
 
            new FileInfo(bundlePath).Should().Exist();
 
            // Make an edit
            var scopedCssFile = Path.Combine(ProjectDirectory.TestRoot, "Components", "Pages", "Index.razor.css");
            File.WriteAllLines(scopedCssFile, File.ReadAllLines(scopedCssFile).Concat(["body { background-color: orangered; }"]));
 
            build = CreateBuildCommand(ProjectDirectory);
            ExecuteCommand(build, "/t:UpdateStaticWebAssetsDesignTime").Should().Pass();
 
            // Verify the generated file contains newly added css
            AssertFileContains(bundlePath, "background-color: orangered");
 
            // Verify that CSS edits continue to apply after new JS modules are added to the project
            // https://github.com/dotnet/aspnetcore/issues/57599
            var collocatedJsFile = Path.Combine(ProjectDirectory.TestRoot, "Components", "Pages", "Index.razor.js");
            File.WriteAllLines(collocatedJsFile, ["console.log('Hello, world!');"]);
            File.WriteAllLines(scopedCssFile, File.ReadAllLines(scopedCssFile).Concat(["h1 { color: purple; }"]));
 
            build = CreateBuildCommand(ProjectDirectory);
            ExecuteCommand(build, "/t:UpdateStaticWebAssetsDesignTime").Should().Pass();
 
            // Verify the generated file contains newly added css
            AssertFileContains(bundlePath, "color: purple");
 
            static void AssertFileContains(string fileName, string content)
            {
                var fileInfo = new FileInfo(fileName);
                fileInfo.Should().Exist();
                fileInfo.ReadAllText().Should().Contain(content);
            }
        }
    }
 
    public class ScopedCssCompatibilityIntegrationTest(ITestOutputHelper log)
        : IsolatedNuGetPackageFolderAspNetSdkBaselineTest(log, Path.Combine(nameof(ScopedCssCompatibilityIntegrationTest), ".nuget"))
    {
        [Fact]
        public void ScopedCss_IsBackwardsCompatible_WithPreviousVersions()
        {
            var testAsset = "RazorAppWithPackageAndP2PReference";
            ProjectDirectory = CreateAspNetSdkTestAsset(testAsset)
                .WithProjectChanges((project, document) =>
                {
                    if (Path.GetFileName(project) == "AnotherClassLib.csproj")
                    {
                        document.Descendants("TargetFramework").Single().ReplaceNodes("net5.0");
                    }
                    if (Path.GetFileName(project) == "ClassLibrary.csproj")
                    {
                        document.Descendants("TargetFramework").Single().ReplaceNodes("net5.0");
                    }
                });
 
            EnsureLocalPackagesExists();
 
            var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference");
            ExecuteCommand(restore).Should().Pass();
 
            var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference");
            ExecuteCommand(build).Should().Pass();
 
            var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
            var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString();
 
            // GenerateStaticWebAssetsManifest should copy the file to the output folder.
            var finalPath = Path.Combine(outputPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json");
            new FileInfo(finalPath).Should().Exist();
            var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json")));
            AssertManifest(
                manifest,
                LoadBuildManifest());
 
            AssertBuildAssets(
                manifest,
                outputPath,
                intermediateOutputPath);
 
            var appBundle = new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "AppWithPackageAndP2PReference.styles.css"));
            appBundle.Should().Exist();
 
            appBundle.Should().Contain("_content/ClassLibrary/ClassLibrary.bundle.scp.css");
            appBundle.Should().Match(""".*_content/RazorPackageLibraryDirectDependency/RazorPackageLibraryDirectDependency\.[a-zA-Z0-9]+\.bundle\.scp\.css.*""");
        }
 
        [Fact]
        public void ScopedCss_PublishIsBackwardsCompatible_WithPreviousVersions()
        {
            var testAsset = "RazorAppWithPackageAndP2PReference";
            ProjectDirectory = CreateAspNetSdkTestAsset(testAsset)
                .WithProjectChanges((project, document) =>
                {
                    if (Path.GetFileName(project) == "AnotherClassLib.csproj")
                    {
                        document.Descendants("TargetFramework").Single().ReplaceNodes("net5.0");
                    }
                    if (Path.GetFileName(project) == "ClassLibrary.csproj")
                    {
                        document.Descendants("TargetFramework").Single().ReplaceNodes("net5.0");
                    }
                });
 
            EnsureLocalPackagesExists();
 
            var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference");
            ExecuteCommand(restore).Should().Pass();
 
            var build = CreatePublishCommand(ProjectDirectory, "AppWithPackageAndP2PReference");
            ExecuteCommand(build, "/bl").Should().Pass();
 
            var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
            var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString();
 
            var finalPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json");
            new FileInfo(finalPath).Should().Exist();
            var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.publish.json")));
            AssertManifest(
                publishManifest,
                LoadPublishManifest());
 
            AssertPublishAssets(
                publishManifest,
                outputPath,
                intermediateOutputPath);
 
            var appBundle = new FileInfo(Path.Combine(outputPath, "wwwroot", "AppWithPackageAndP2PReference.styles.css"));
            appBundle.Should().Exist();
 
            appBundle.Should().Contain("_content/ClassLibrary/ClassLibrary.bundle.scp.css");
            appBundle.Should().Match("""_content/RazorPackageLibraryDirectDependency/RazorPackageLibraryDirectDependency\.[a-zA-Z0-9]+\.bundle\.scp\.css""");
        }
    }
 
    public class ScopedCssPackageReferences(ITestOutputHelper log)
        : IsolatedNuGetPackageFolderAspNetSdkBaselineTest(log, Path.Combine(nameof(ScopedCssPackageReferences), ".nuget"))
    {
        [Fact]
        public void BuildProjectWithReferences_CorrectlyBundlesScopedCssFiles()
        {
            var testAsset = "RazorAppWithPackageAndP2PReference";
            ProjectDirectory = CreateAspNetSdkTestAsset(testAsset);
 
            EnsureLocalPackagesExists();
 
            var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference");
            ExecuteCommand(restore).Should().Pass();
 
            var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference");
            ExecuteCommand(build).Should().Pass();
 
            var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
            var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString();
 
            // GenerateStaticWebAssetsManifest should copy the file to the output folder.
            var finalPath = Path.Combine(outputPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json");
            new FileInfo(finalPath).Should().Exist();
            var buildManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json")));
            AssertManifest(
                buildManifest,
                LoadBuildManifest());
 
            AssertBuildAssets(
                buildManifest,
                outputPath,
                intermediateOutputPath);
 
            var appBundle = new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "AppWithPackageAndP2PReference.styles.css"));
            appBundle.Should().Exist();
 
            appBundle.Should().Match(""".*_content/RazorPackageLibraryDirectDependency/RazorPackageLibraryDirectDependency\.[a-zA-Z0-9]+\.bundle\.scp\.css.*""");
            appBundle.Should().Match(""".*_content/ClassLibrary/ClassLibrary\.[a-zA-Z0-9]+\.bundle\.scp\.css.*""");
        }
 
        // Regression test for https://github.com/dotnet/aspnetcore/issues/37592
        [Fact]
        public void RegeneratingScopedCss_ForProjectWithReferences()
        {
            var testAsset = "RazorAppWithPackageAndP2PReference";
            ProjectDirectory = CreateAspNetSdkTestAsset(testAsset);
 
            var scopedCssFile = Path.Combine(ProjectDirectory.Path, "AppWithPackageAndP2PReference", "Index.razor.css");
            File.WriteAllText(scopedCssFile, "/* Empty css */");
            File.WriteAllText(Path.Combine(ProjectDirectory.Path, "AppWithPackageAndP2PReference", "Index.razor"), "This is a test razor component.");
 
            EnsureLocalPackagesExists();
 
            var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference");
            ExecuteCommand(restore).Should().Pass();
 
            var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference");
            ExecuteCommand(build).Should().Pass();
 
            var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
            var bundlePath = Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "AppWithPackageAndP2PReference.styles.css");
 
            new FileInfo(bundlePath).Should().Exist();
 
            // Make an edit to a scoped css file
            File.WriteAllLines(scopedCssFile, File.ReadAllLines(scopedCssFile).Concat(new[] { "body { background-color: orangered; }" }));
 
            build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference");
            ExecuteCommand(build, "/t:UpdateStaticWebAssetsDesignTime").Should().Pass();
 
            var fileInfo = new FileInfo(bundlePath);
            fileInfo.Should().Exist();
            // Verify the generated file contains newly added css
            var text = fileInfo.ReadAllText();
            text.Should().Contain("background-color: orangered");
            text.Should().MatchRegex(""".*@import '_content/ClassLibrary/ClassLibrary\.[a-zA-Z0-9]+\.bundle\.scp\.css.*""");
        }
 
        [Fact]
        public void Build_GeneratesUrlEncodedLinkHeaderForNonAsciiProjectName()
        {
            var testAsset = "RazorAppWithPackageAndP2PReference";
            ProjectDirectory = CreateAspNetSdkTestAsset(testAsset);
 
            // Rename the ClassLibrary project to have non-ASCII characters
            var originalLibPath = Path.Combine(ProjectDirectory.Path, "AnotherClassLib");
            var newLibPath = Path.Combine(ProjectDirectory.Path, "项目");
            Directory.Move(originalLibPath, newLibPath);
 
            // Update the project file to set the assembly name and package ID
            var libProjectFile = Path.Combine(newLibPath, "AnotherClassLib.csproj");
            var newLibProjectFile = Path.Combine(newLibPath, "项目.csproj");
            File.Move(libProjectFile, newLibProjectFile);
 
            // Add assembly name property to ensure consistent naming
            var libProjectContent = File.ReadAllText(newLibProjectFile);
            // Find the first PropertyGroup closing tag and replace it
            var targetPattern = "</PropertyGroup>";
            var replacement = "    <AssemblyName>项目</AssemblyName>\n    <PackageId>项目</PackageId>\n  </PropertyGroup>";
            var index = libProjectContent.IndexOf(targetPattern);
            if (index >= 0)
            {
                libProjectContent = libProjectContent.Substring(0, index) + replacement + libProjectContent.Substring(index + targetPattern.Length);
            }
            File.WriteAllText(newLibProjectFile, libProjectContent);
 
            // Update the main project to reference the renamed library
            var mainProjectFile = Path.Combine(ProjectDirectory.Path, "AppWithPackageAndP2PReference", "AppWithPackageAndP2PReference.csproj");
            var mainProjectContent = File.ReadAllText(mainProjectFile);
            mainProjectContent = mainProjectContent.Replace(@"..\AnotherClassLib\AnotherClassLib.csproj", @"..\项目\项目.csproj");
            File.WriteAllText(mainProjectFile, mainProjectContent);
 
            // Ensure library has scoped CSS
            var libCssFile = Path.Combine(newLibPath, "Views", "Shared", "Index.cshtml.css");
            if (!File.Exists(libCssFile))
            {
                Directory.CreateDirectory(Path.GetDirectoryName(libCssFile));
                File.WriteAllText(libCssFile, ".test { color: red; }");
            }
 
            EnsureLocalPackagesExists();
 
            var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference");
            ExecuteCommand(restore).Should().Pass();
 
            var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference");
            ExecuteCommand(build).Should().Pass();
 
            var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
 
            // Check that the staticwebassets.build.endpoints.json file contains URL-encoded characters
            var endpointsFile = Path.Combine(intermediateOutputPath, "staticwebassets.build.endpoints.json");
            new FileInfo(endpointsFile).Should().Exist();
 
            var endpointsContent = File.ReadAllText(endpointsFile);
            var json = JsonSerializer.Deserialize<StaticWebAssetEndpointsManifest>(endpointsContent, new JsonSerializerOptions(JsonSerializerDefaults.Web));
 
            var styles = json.Endpoints.Where(e => e.Route.EndsWith("styles.css"));
 
            foreach (var styleEndpoint in styles)
            {
                styleEndpoint.ResponseHeaders.Should().Contain(h => h.Name.Equals("Link", StringComparison.OrdinalIgnoreCase) && h.Value.Contains("%E9%A1%B9%E7%9B%AE"));
            }
        }
    }
}