Editor/Projection/Tests/GoldenFixture.cs
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using Grains.RazorDesigner.Document;
using Sandbox; // Color (engine-global; matches IReadOnlyNode.cs)
namespace Grains.RazorDesigner.Projection.Tests;
public sealed class GoldenFixture
{
[JsonPropertyName( "goldenFormatVersion" )]
public int GoldenFormatVersion { get; set; }
[JsonPropertyName( "kind" )]
public string Kind { get; set; } = "";
[JsonPropertyName( "node" )]
public NodeDto Node { get; set; } = new();
[JsonPropertyName( "context" )]
public ContextDto Context { get; set; } = new();
[JsonPropertyName( "expected" )]
public ExpectedDto Expected { get; set; } // null means "not yet authored"
}
public sealed class ContextDto
{
[JsonPropertyName( "forPreview" )]
public bool ForPreview { get; set; } = false;
}
public sealed class NodeDto
{
[JsonPropertyName( "id" )]
public string Id { get; set; } = Guid.NewGuid().ToString();
[JsonPropertyName( "kind" )]
public string Kind { get; set; } = "";
[JsonPropertyName( "className" )]
public string ClassName { get; set; } = "";
[JsonPropertyName( "appearance" )]
public Dictionary<string, JsonElement> Appearance { get; set; } = new();
// Flat map: payload field-name -> json value.
[JsonPropertyName( "payload" )]
public Dictionary<string, JsonElement> Payload { get; set; } = new();
[JsonPropertyName( "children" )]
public List<NodeDto> Children { get; set; } = new();
[JsonPropertyName( "slots" )]
public Dictionary<string, List<NodeDto>> Slots { get; set; } = new();
[JsonPropertyName( "states" )]
public List<StateDto> States { get; set; }
}
public sealed class StateDto
{
[JsonPropertyName( "state" )]
public string State { get; set; } = "hover";
[JsonPropertyName( "nthChildMode" )]
public string NthChildMode { get; set; } // null → default (Literal)
[JsonPropertyName( "nthChildArg" )]
public int NthChildArg { get; set; } = 1;
[JsonPropertyName( "delta" )]
public Dictionary<string, JsonElement> Delta { get; set; } = new();
}
public sealed class ExpectedDto
{
[JsonPropertyName( "panelOps" )]
public List<OpDto> PanelOps { get; set; } = new();
[JsonPropertyName( "scssLines" )]
public List<string> ScssLines { get; set; } = new();
[JsonPropertyName( "razorAttributes" )]
public List<string> RazorAttributes { get; set; } = new();
[JsonPropertyName( "razorInnerText" )]
public string RazorInnerText { get; set; }
}
// OpDto — union over the four PanelOp variants via a "$type" discriminant.
public sealed class OpDto
{
[JsonPropertyName( "$type" )]
public string Type { get; set; } = "";
// SetClass
[JsonPropertyName( "class" )]
public string Class { get; set; }
// SetStyle
[JsonPropertyName( "property" )]
public string Property { get; set; }
[JsonPropertyName( "value" )]
public string Value { get; set; }
// SetAttribute
[JsonPropertyName( "name" )]
public string Name { get; set; }
// SetAttribute shares Value; SetInnerText uses Text
[JsonPropertyName( "text" )]
public string Text { get; set; }
}
public static class GoldenJson
{
public static readonly JsonSerializerOptions Options = new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter( JsonNamingPolicy.CamelCase ) },
};
}
public static class OpMapping
{
public static OpDto FromOp( PanelOp op )
{
switch ( op )
{
case SetClass c:
return new OpDto { Type = "SetClass", Class = c.Class };
case SetStyle s:
return new OpDto { Type = "SetStyle", Property = s.Property, Value = s.Value };
case SetAttribute a:
return new OpDto { Type = "SetAttribute", Name = a.Name, Value = a.Value };
case SetInnerText t:
return new OpDto { Type = "SetInnerText", Text = t.Text };
default:
throw new InvalidOperationException( $"OpMapping.FromOp: unknown PanelOp variant '{op?.GetType().Name}'" );
}
}
public static PanelOp ToOp( OpDto dto )
{
switch ( dto.Type )
{
case "SetClass": return new SetClass( dto.Class ?? "" );
case "SetStyle": return new SetStyle( dto.Property ?? "", dto.Value ?? "" );
case "SetAttribute": return new SetAttribute( dto.Name ?? "", dto.Value ?? "" );
case "SetInnerText": return new SetInnerText( dto.Text ?? "" );
default:
throw new InvalidOperationException( $"OpMapping.ToOp: unknown $type '{dto.Type}'" );
}
}
}
public sealed class FixtureNode : IReadOnlyNode
{
private readonly NodeDto _dto;
private Guid _id;
private FixtureAppearance _appearance;
private FixturePayload _payload;
private List<IReadOnlyNode> _children;
private Dictionary<string, IReadOnlyList<IReadOnlyNode>> _slots;
public FixtureNode( NodeDto dto )
{
_dto = dto ?? throw new ArgumentNullException( nameof( dto ) );
_id = Guid.TryParse( dto.Id, out var g ) ? g : Guid.NewGuid();
}
public Guid Id => _id;
public string Kind => _dto.Kind;
public string ClassName => _dto.ClassName;
public IAppearance Appearance =>
_appearance ??= new FixtureAppearance( _dto.Appearance );
public IPayload Payload =>
_payload ??= new FixturePayload( _dto.Payload );
public IReadOnlyList<IReadOnlyNode> Children
{
get
{
if ( _children != null ) return _children;
_children = new List<IReadOnlyNode>();
foreach ( var child in _dto.Children )
_children.Add( new FixtureNode( child ) );
return _children;
}
}
public IReadOnlyDictionary<string, IReadOnlyList<IReadOnlyNode>> Slots
{
get
{
if ( _slots != null ) return _slots;
_slots = new Dictionary<string, IReadOnlyList<IReadOnlyNode>>();
foreach ( var kv in _dto.Slots )
{
var list = new List<IReadOnlyNode>();
foreach ( var child in kv.Value )
list.Add( new FixtureNode( child ) );
_slots[kv.Key] = list;
}
return _slots;
}
}
// Lazily built list of state rules from NodeDto.States (grd-74lj Task 7a).
private List<IReadOnlyStateRule> _stateRules;
public IReadOnlyList<IReadOnlyStateRule> StateRules
{
get
{
if ( _stateRules != null ) return _stateRules;
_stateRules = new List<IReadOnlyStateRule>();
if ( _dto.States != null )
{
foreach ( var s in _dto.States )
_stateRules.Add( new FixtureStateRule( s ) );
}
return _stateRules;
}
}
private sealed class FixtureStateRule : IReadOnlyStateRule
{
private readonly Document.PseudoKind _state;
private readonly Document.NthChildMode _nthChildMode;
private readonly int _nthChildArg;
private readonly IAppearance _delta;
public FixtureStateRule( StateDto dto )
{
if ( !System.Enum.TryParse<Document.PseudoKind>( dto.State, ignoreCase: true, out _state ) )
throw new System.ArgumentException( $"Golden state \"{dto.State}\" is not a valid PseudoKind." );
// Parse NthChildMode (optional — null means Literal default).
if ( dto.NthChildMode != null &&
System.Enum.TryParse<Document.NthChildMode>( dto.NthChildMode, ignoreCase: true, out var nm ) )
_nthChildMode = nm;
else
_nthChildMode = Document.NthChildMode.Literal;
_nthChildArg = dto.NthChildArg >= 1 ? dto.NthChildArg : 1;
_delta = new FixtureAppearance( dto.Delta ?? new Dictionary<string, JsonElement>() );
}
public Document.PseudoKind State => _state;
public Document.NthChildMode NthChildMode => _nthChildMode;
public int NthChildArg => _nthChildArg;
public IAppearance Delta => _delta;
}
}
public sealed class FixtureAppearance : IAppearance
{
private readonly Dictionary<string, JsonElement> _m;
public FixtureAppearance( Dictionary<string, JsonElement> map )
{
_m = map ?? new Dictionary<string, JsonElement>();
}
// Helpers
private bool Bool( string key, bool def = false ) =>
_m.TryGetValue( key, out var el ) ? el.GetBoolean() : def;
private int Int( string key, int def = 0 ) =>
_m.TryGetValue( key, out var el ) ? el.GetInt32() : def;
private float Float( string key, float def = 0f ) =>
_m.TryGetValue( key, out var el ) ? el.GetSingle() : def;
private string Str( string key, string def = "" ) =>
_m.TryGetValue( key, out var el ) ? el.GetString() ?? def : def;
private Length Len( string key, Length def = default )
{
if ( !_m.TryGetValue( key, out var el ) ) return def;
// Stored as "100px", "50%", "auto", etc.
if ( el.ValueKind == JsonValueKind.String )
return Length.TryParse( el.GetString(), out var v ) ? v : def;
// Or stored as number (bare px)
if ( el.ValueKind == JsonValueKind.Number )
return Length.Px( el.GetSingle() );
return def;
}
private Color Col( string key, Color def = default )
{
if ( !_m.TryGetValue( key, out var el ) ) return def;
if ( el.ValueKind == JsonValueKind.String )
{
// Accept "#RRGGBB" or "#RRGGBBAA". Color.Parse returns Color? (matches PaletteTemplateSerializer pattern).
var s = el.GetString() ?? "";
var parsed = Color.Parse( s );
if ( parsed.HasValue ) return parsed.Value;
}
return def;
}
private Edges EdgesVal( string key )
{
if ( !_m.TryGetValue( key, out var el ) ) return Edges.Zero;
if ( el.ValueKind == JsonValueKind.String )
{
// Delegate to Edges.TryParse which handles 1/2/4-value CSS shorthand forms.
return Edges.TryParse( el.GetString() ?? "", out var parsed ) ? parsed : Edges.Zero;
}
if ( el.ValueKind == JsonValueKind.Object )
{
// { top, right, bottom, left }
var top = el.TryGetProperty( "top", out var t ) && Length.TryParse( t.GetString(), out var tv ) ? tv : Length.Auto;
var right = el.TryGetProperty( "right", out var r ) && Length.TryParse( r.GetString(), out var rv ) ? rv : Length.Auto;
var bottom = el.TryGetProperty( "bottom", out var b ) && Length.TryParse( b.GetString(), out var bv ) ? bv : Length.Auto;
var left = el.TryGetProperty( "left", out var l ) && Length.TryParse( l.GetString(), out var lv ) ? lv : Length.Auto;
return new Edges( top, right, bottom, left );
}
return Edges.Zero;
}
private T EnumVal<T>( string key, T def ) where T : struct, System.Enum
{
if ( !_m.TryGetValue( key, out var el ) ) return def;
if ( el.ValueKind == JsonValueKind.String )
{
if ( System.Enum.TryParse<T>( el.GetString(), out var v ) ) return v;
}
return def;
}
// --- IAppearance implementation (defaults mirror ControlRecord field declarations) ---
public Length Width => Len( "width", Length.Auto );
public Length Height => Len( "height", Length.Auto );
public FlexDirection Direction => EnumVal<FlexDirection>( "direction", FlexDirection.Row );
public JustifyContent Justify => EnumVal<JustifyContent>( "justify", JustifyContent.Start );
public AlignItems Align => EnumVal<AlignItems>( "align", AlignItems.Stretch );
public float Gap => Float( "gap", 8f );
public Edges Padding => EdgesVal( "padding" );
public FlexWrap Wrap => EnumVal<FlexWrap>( "wrap", FlexWrap.NoWrap );
public PositionKind Position => EnumVal<PositionKind>( "position", PositionKind.Relative );
public Length Top => Len( "top", Length.Auto );
public Length Left => Len( "left", Length.Auto );
public Length Right => Len( "right", Length.Auto );
public Length Bottom => Len( "bottom", Length.Auto );
public float FlexGrow => Float( "flexGrow", 0f );
public float FlexShrink => Float( "flexShrink", 1f );
public Length FlexBasis => Len( "flexBasis", Length.Auto );
public AlignSelfKind AlignSelf => EnumVal<AlignSelfKind>( "alignSelf", AlignSelfKind.Auto );
public bool OverrideTypography => Bool( "overrideTypography", false );
public string FontFamily => Str( "fontFamily", "" );
public Length FontSize => Len( "fontSize", Length.Px( 14 ) );
public int FontWeight => Int( "fontWeight", 400 );
public Color Color => Col( "color", Color.White );
public TextAlignment TextAlign => EnumVal<TextAlignment>( "textAlign", TextAlignment.Left );
public bool FontStyleItalic => Bool( "fontStyleItalic", false );
public TextTransformKind TextTransform => EnumVal<TextTransformKind>( "textTransform", TextTransformKind.None );
public Length LetterSpacing => Len( "letterSpacing", Length.Auto );
public Length LineHeight => Len( "lineHeight", Length.Auto );
public bool OverrideBackground => Bool( "overrideBackground", false );
public Color BackgroundColor => Col( "backgroundColor", Color.White );
public string BackgroundImage => Str( "backgroundImage", "" );
public string BackgroundSize => Str( "backgroundSize", "" );
public string BackgroundPosition => Str( "backgroundPosition", "" );
public string BackgroundRepeat => Str( "backgroundRepeat", "" );
public bool OverrideBorder => Bool( "overrideBorder", false );
public Length BorderRadius => Len( "borderRadius", Length.Px( 0 ) );
public Color BorderColor => Col( "borderColor", Color.Transparent );
public Length BorderWidth => Len( "borderWidth", Length.Px( 0 ) );
public bool OverrideEffects => Bool( "overrideEffects", false );
public Length BoxShadowX => Len( "boxShadowX", Length.Px( 0 ) );
public Length BoxShadowY => Len( "boxShadowY", Length.Px( 2 ) );
public Length BoxShadowBlur => Len( "boxShadowBlur", Length.Px( 4 ) );
public Color BoxShadowColor => Col( "boxShadowColor", Color.Black );
public bool BoxShadowInset => Bool( "boxShadowInset", false );
public float Opacity => Float( "opacity", 1f );
public bool OverrideConstraints => Bool( "overrideConstraints", false );
public Edges Margin => EdgesVal( "margin" );
public Length MinWidth => Len( "minWidth", Length.Auto );
public Length MaxWidth => Len( "maxWidth", Length.Auto );
public Length MinHeight => Len( "minHeight", Length.Auto );
public Length MaxHeight => Len( "maxHeight", Length.Auto );
public bool OverrideInteraction => Bool( "overrideInteraction", false );
public CursorKind Cursor => EnumVal<CursorKind>( "cursor", CursorKind.Auto );
public OverflowKind Overflow => EnumVal<OverflowKind>( "overflow", OverflowKind.Visible );
public int ZIndex => Int( "zIndex", 0 );
public bool PointerEvents => Bool( "pointerEvents", true );
}
public sealed class FixturePayload : IPayload
{
private readonly Dictionary<string, JsonElement> _m;
public FixturePayload( Dictionary<string, JsonElement> map )
{
_m = map ?? new Dictionary<string, JsonElement>();
}
private string Str( string key, string def = "" ) =>
_m.TryGetValue( key, out var el ) ? el.GetString() ?? def : def;
private Length Len( string key, Length def = default )
{
if ( !_m.TryGetValue( key, out var el ) ) return def;
if ( el.ValueKind == JsonValueKind.String )
return Length.TryParse( el.GetString(), out var v ) ? v : def;
if ( el.ValueKind == JsonValueKind.Number )
return Length.Px( el.GetSingle() );
return def;
}
public string Content => Str( "content", "" );
public string Placeholder => Str( "placeholder", "" );
public string Source => Str( "source", "" );
public string IconName => Str( "iconName", "" );
public Length CheckboxSize => Len( "checkboxSize", Length.Px( 16 ) );
}