using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using System.Collections.Generic; namespace Generator { [TestClass] public class RazorParser { public string BuildRazorFile( string razorFilePath ) { List SyntaxTree = new List(); var optn = new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary ) .WithConcurrentBuild( true ) .WithOptimizationLevel( OptimizationLevel.Debug ) .WithGeneralDiagnosticOption( ReportDiagnostic.Info ) .WithPlatform( Microsoft.CodeAnalysis.Platform.AnyCpu ) .WithAllowUnsafe( false ); var refs = new List(); var path = System.IO.Path.GetDirectoryName( typeof( System.Object ).Assembly.Location ); refs.Add( MetadataReference.CreateFromFile( typeof( System.Object ).Assembly.Location ) ); refs.Add( MetadataReference.CreateFromFile( $"{path}\\System.Runtime.dll" ) ); refs.Add( MetadataReference.CreateFromFile( typeof( Networking ).Assembly.Location ) ); refs.Add( MetadataReference.CreateFromFile( typeof( ConCmdAttribute ).Assembly.Location ) ); CSharpCompilation compiler = CSharpCompilation.Create( $"poopy.dll", SyntaxTree, refs, optn ); // Razor files are now processed via RazorProcessor.GenerateFromSource // Generate C# code from the razor file first var fullPath = System.IO.Path.GetFullPath( razorFilePath ); var razorText = System.IO.File.ReadAllText( fullPath ); var generatedCode = Sandbox.Razor.RazorProcessor.GenerateFromSource( razorText, razorFilePath ); // Parse into syntax tree and add to compilation var razorTree = CSharpSyntaxTree.ParseText( generatedCode, path: $"_gen_{System.IO.Path.GetFileName( razorFilePath )}.cs", encoding: System.Text.Encoding.UTF8 ); compiler = compiler.AddSyntaxTrees( razorTree ); var processor = new Sandbox.Generator.Processor(); processor.AddonName = $"poopy"; processor.Run( compiler ); compiler = processor.Compilation; Assert.AreEqual( 1, compiler.SyntaxTrees.Count() ); var code = compiler.SyntaxTrees.First(); var source = code.GetText().ToString(); System.Console.WriteLine( source ); return source; } [TestMethod] public void RazorTemplateEasy() { BuildRazorFile( "data/codegen/easymode.razor" ); } [TestMethod] public void RazorTemplateHard() { var code = BuildRazorFile( "data/codegen/HardMode.razor" ); Assert.IsTrue( code.Contains( "HardMode.razor\", 5, 0 );" ) ); Assert.IsTrue( code.Contains( "HardMode.razor\", 7, 0 );" ) ); } [TestMethod] public void RazorTemplateRef() { var code = BuildRazorFile( "data/codegen/Ref.razor" ); Assert.IsTrue( code.Contains( ".OpenElement(" ) ); Assert.IsTrue( code.Contains( ".CloseElement()" ) ); Assert.IsTrue( code.Contains( ".AddReferenceCapture(" ) ); } [TestMethod] public void RazorTemplateEvents() { var code = BuildRazorFile( "data/codegen/Events.razor" ); // @onclick=Clicked Assert.IsTrue( code.Contains( "__builder.AddAttribute( 1, \"onclick\", Clicked );" ) ); // @onclick=@(() => DoSomething()) Assert.IsTrue( code.Contains( "__builder.AddAttribute( 5, \"onclick\", () => DoSomething() );" ) ); // @OnEvent=@Clicked Assert.IsTrue( code.Contains( "__builder.AddAttribute( 9, ( o ) => { o.OnEvent = Clicked; } );" ) ); // @OnEvent=@(() => DoSomething()) Assert.IsTrue( code.Contains( "__builder.AddAttribute( 13, ( o ) => { o.OnEvent = () => DoSomething(); } );" ) ); } [TestMethod] public void RazorTemplateAttributes() { BuildRazorFile( "data/codegen/Attributes.razor" ); } [TestMethod] public void StylesheetAttribute() { var code = BuildRazorFile( "data/codegen/StylesheetAttribute.razor" ); Assert.IsTrue( code.Contains( "[StyleSheet(" ) ); } [TestMethod] public void StyleBlock() { var code = BuildRazorFile( "data/codegen/StyleBlock.razor" ); Assert.IsFalse( code.Contains( "" ) ); Assert.IsTrue( code.Contains( ".AddStyleDefinitions(" ), "Missing .AddStyleBlock(" ); } [TestMethod] public void RootElement() { var code = BuildRazorFile( "data/codegen/RootElement.razor" ); Assert.IsTrue( code.Contains( "__builder.OpenElement( 0, \"root\", null );" ) ); Assert.IsFalse( code.Contains( "internal partial class" ) ); Assert.IsTrue( code.Contains( "public partial class" ) ); } [TestMethod] public void InternalDirective() { var code = BuildRazorFile( "data/codegen/InternalDirective.razor" ); Assert.IsTrue( code.Contains( "internal partial class" ) ); } [TestMethod] public void Binds() { var code = BuildRazorFile( "data/codegen/Binds.razor" ); Assert.IsFalse( code.Contains( "internal partial class" ) ); Assert.IsTrue( code.Contains( "public partial class" ) ); Assert.IsTrue( code.Contains( ".AddBind(" ) ); } [TestMethod] public void Recursion() { // We want recursion to throw an exception at runtime (when building the render tree) // so that users can see what they've done wrong var code = BuildRazorFile( "data/codegen/RecursivePanel.razor" ); Assert.IsTrue( code.Contains( "throw new System.Exception" ) ); } [TestMethod] public void Nesting() { // We don't want nesting to fail at all var code = BuildRazorFile( "data/codegen/NestedPanel.razor" ); Assert.IsFalse( code.Contains( "throw new System.Exception" ) ); } [TestMethod] public void RenderFragment() { var code = BuildRazorFile( "data/codegen/RenderFragmentTest.razor" ); Assert.IsTrue( code.Contains( "__builder.SetRenderFragment" ) ); Assert.IsTrue( code.Contains( "__builder ) =>" ) ); } [TestMethod] public void Generic_WithOneParam() { var code = BuildRazorFile( "data/codegen/Generic1.razor" ); Assert.IsTrue( code.Contains( "public partial class Generic1" ) ); Assert.IsTrue( code.Contains( "ListComponent" ) ); } [TestMethod] public void Generic_WithTwoParam() { var code = BuildRazorFile( "data/codegen/Generic2.razor" ); Assert.IsTrue( code.Contains( "public partial class Generic2" ) ); Assert.IsTrue( code.Contains( "ListComponent" ) ); } [TestMethod] public void AutomaticNamespace() { // Test that namespace is automatically generated from folder structure var razorText = System.IO.File.ReadAllText( "data/codegen/NamespaceTest.razor" ); // Test 1: Generate code WITH a root namespace and folder structure // The path "data/codegen/NamespaceTest.razor" should produce "MyApp.UI.data.codegen" var generatedCodeWithFolders = Sandbox.Razor.RazorProcessor.GenerateFromSource( razorText, "data/codegen/NamespaceTest.razor", "MyApp.UI" ); System.Console.WriteLine( "Generated code with folder-based namespace:" ); System.Console.WriteLine( generatedCodeWithFolders ); // Should contain the root namespace + folders Assert.IsTrue( generatedCodeWithFolders.Contains( "namespace MyApp.UI.data.codegen" ), "Generated code should contain 'namespace MyApp.UI.data.codegen'" ); // Test 2: Generate code with a realistic addon path structure // Simulating: UI/Components/Loader/LoaderFullScreen.razor -> Sandbox.UI.Components.Loader var generatedCodeRealistic = Sandbox.Razor.RazorProcessor.GenerateFromSource( razorText, "UI/Components/Loader/LoaderFullScreen.razor", "Sandbox" ); System.Console.WriteLine( "\nGenerated code with realistic path:" ); System.Console.WriteLine( generatedCodeRealistic ); Assert.IsTrue( generatedCodeRealistic.Contains( "namespace Sandbox.UI.Components.Loader" ), "Generated code should contain 'namespace Sandbox.UI.Components.Loader'" ); // Test 3: Generate code WITHOUT a root namespace (should not have namespace directive) var generatedCodeWithoutNamespace = Sandbox.Razor.RazorProcessor.GenerateFromSource( razorText, "data/codegen/NamespaceTest.razor", null ); System.Console.WriteLine( "\nGenerated code without namespace:" ); System.Console.WriteLine( generatedCodeWithoutNamespace ); // Verify no namespace is added Assert.IsFalse( generatedCodeWithoutNamespace.Contains( "namespace MyApp" ), "Generated code without root namespace should not contain namespace declaration" ); // Test 4: File in root directory (no subfolders) should just use root namespace var generatedCodeRootLevel = Sandbox.Razor.RazorProcessor.GenerateFromSource( razorText, "TestComponent.razor", "MyApp" ); System.Console.WriteLine( "\nGenerated code for root-level file:" ); System.Console.WriteLine( generatedCodeRootLevel ); Assert.IsTrue( generatedCodeRootLevel.Contains( "namespace MyApp" ), "Root-level file should use root namespace" ); Assert.IsFalse( generatedCodeRootLevel.Contains( "namespace MyApp." ), "Root-level file should not have sub-namespaces" ); } } }