Files
sbox-public/engine/Sandbox.Compiling.Test/Tests/Razor.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

229 lines
8.7 KiB
C#

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System.Collections.Generic;
namespace Generator
{
[TestClass]
public class RazorParser
{
public string BuildRazorFile( string razorFilePath )
{
List<SyntaxTree> SyntaxTree = new List<SyntaxTree>();
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<MetadataReference>();
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<MyPanel>( 9, ( o ) => { o.OnEvent = Clicked; } );" ) );
// @OnEvent=@(() => DoSomething())
Assert.IsTrue( code.Contains( "__builder.AddAttribute<MyPanel>( 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( "<style>" ) );
Assert.IsFalse( code.Contains( "</style>" ) );
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<T1>" ) );
Assert.IsTrue( code.Contains( "ListComponent<string>" ) );
}
[TestMethod]
public void Generic_WithTwoParam()
{
var code = BuildRazorFile( "data/codegen/Generic2.razor" );
Assert.IsTrue( code.Contains( "public partial class Generic2<T1, T2>" ) );
Assert.IsTrue( code.Contains( "ListComponent<string,int>" ) );
}
[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" );
}
}
}