Files
sbox-public/engine/Sandbox.Test.Unit/UI/Styles/PropertyCoverage/TextFontProperties.cs
Garry Newman d27c236689 UI additions and optimizations (#4968)
ADDED
css min(), max() and clamp() for lengths
css-wide keywords inherit, initial, unset, revert (incl. shorthands)
added currentColor keyword to css
colors can parse oklch(), lab() and hwb()
added css text-align: justify
css white-space: pre-wrap and break-spaces
added css word-break: break-word
parses css image-rendering: crisp-edges (point sampling)
added css inset shorthand
now parses 1–4 values in the css border-width shorthand
added css transition-duration, -delay, -property and -timing-function longhands
css :has() now supports descendant selectors
added css opacity percentages
added css overflow: auto (maps to scroll)
added css gap: normal
added css filter: none
css transform: scale() now takes comma-separated args
css animations now accept ms units
added more css justify-content, align-* and text-align keywords (start, end, …)
parses css dvh/svh/lvh/dvw viewport units (treated as vh/vw)
parses css object-fit: scale-down (treated as contain)
added css margin-block / margin-inline (+ the logical margin sides)
added css padding-block / padding-inline (+ the logical padding sides)
added css inset-block / inset-inline (+ the logical inset sides)
added the css flex-flow shorthand
added the css font shorthand
added css font-size keywords (xx-small … xxx-large)
css letter-spacing and word-spacing now accept normal
added css font-smooth: none
css aspect-ratio now accepts the auto form
css font-family now maps generic families (serif, sans-serif, monospace)
added css flex-flow
added css font

FIXED
fixed css !important dropping the whole declaration
css line-height without units is now a multiplier
fixed css calc() division and full-expression parsing
fixed the css flex shorthand for single-number and three-value forms
fixed the css transition shorthand when the property is omitted
css font-family now uses the first family in a comma stack
colors now parse fully in the css background shorthand
an unknown css @-rule no longer drops the whole stylesheet
css variables no longer match on partial tokens
injected css variables are kept across stylesheet hotloads
fixed css selector specificity overflowing when comparing rules
css background: none now resets the background (image and colour)
css filter: none now overrides a filter set by a base class
css transform: none now overrides a transform set by a base class

IMPROVED
css rule matching is ~3–5× faster on large stylesheets (rules indexed by class)
~2× fewer allocations in css transitions (snapshot styles once, not per property)
finished css animations no longer re-layout every frame
less GC in css sibling/child selector queries (no list copies or wrappers)
less GC diffing active css rules (no LINQ or hashsets)
faster, lower-GC Yoga layout wrapper
css stylesheet hotload now watches newly imported files
2026-06-02 19:19:23 +01:00

1225 lines
34 KiB
C#

using Sandbox.UI;
namespace UITest.PropertyCoverage;
/// <summary>
/// Exhaustive property coverage for the Text &amp; Font CSS property group.
/// Normal [TestMethod]s assert the actual parsed value and are expected to pass.
/// Methods ending in _Gap document known web-syntax gaps and are expected to fail
/// until the engine is fixed.
/// </summary>
[TestClass]
public class TextFontProperties
{
// ---------------------------------------------------------------------
// font-size -> FontSize (Length?)
// ---------------------------------------------------------------------
[TestMethod]
public void FontSize_Pixels()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-size", "20px" ) );
Assert.IsTrue( s.FontSize.HasValue );
Assert.AreEqual( 20, s.FontSize.Value.Value );
Assert.AreEqual( LengthUnit.Pixels, s.FontSize.Value.Unit );
}
[TestMethod]
public void FontSize_Percent()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-size", "150%" ) );
Assert.IsTrue( s.FontSize.HasValue );
Assert.AreEqual( 150, s.FontSize.Value.Value );
Assert.AreEqual( LengthUnit.Percentage, s.FontSize.Value.Unit );
}
[TestMethod]
public void FontSize_Em()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-size", "1.5em" ) );
Assert.IsTrue( s.FontSize.HasValue );
Assert.AreEqual( 1.5f, s.FontSize.Value.Value );
Assert.AreEqual( LengthUnit.Em, s.FontSize.Value.Unit );
}
[TestMethod]
public void FontSize_Rem()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-size", "2rem" ) );
Assert.IsTrue( s.FontSize.HasValue );
Assert.AreEqual( 2, s.FontSize.Value.Value );
Assert.AreEqual( LengthUnit.RootEm, s.FontSize.Value.Unit );
}
[TestMethod]
public void FontSize_Unitless_TreatedAsPixels()
{
// s&box treats a bare number as pixels.
var s = new Styles();
Assert.IsTrue( s.Set( "font-size", "16" ) );
Assert.IsTrue( s.FontSize.HasValue );
Assert.AreEqual( 16, s.FontSize.Value.Value );
Assert.AreEqual( LengthUnit.Pixels, s.FontSize.Value.Unit );
}
[TestMethod]
public void FontSize_Invalid()
{
var s = new Styles();
Assert.IsFalse( s.Set( "font-size", "bullshit" ) );
Assert.IsFalse( s.FontSize.HasValue );
}
// ---------------------------------------------------------------------
// color / font-color -> FontColor (Color?) ('color' is an alias)
// ---------------------------------------------------------------------
[TestMethod]
public void FontColor_NamedRed_ViaFontColor()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-color", "red" ) );
Assert.IsTrue( s.FontColor.HasValue );
Assert.AreEqual( new Color( 1, 0, 0, 1 ), s.FontColor.Value );
}
[TestMethod]
public void FontColor_NamedRed_ViaColorAlias()
{
// 'color' is an alias for 'font-color'
var s = new Styles();
Assert.IsTrue( s.Set( "color", "red" ) );
Assert.IsTrue( s.FontColor.HasValue );
Assert.AreEqual( new Color( 1, 0, 0, 1 ), s.FontColor.Value );
}
[TestMethod]
public void FontColor_Hex()
{
var s = new Styles();
Assert.IsTrue( s.Set( "color", "#f0f0" ) );
Assert.IsTrue( s.FontColor.HasValue );
Assert.AreEqual( 1.0f, s.FontColor.Value.r );
Assert.AreEqual( 0.0f, s.FontColor.Value.g );
Assert.AreEqual( 1.0f, s.FontColor.Value.b );
Assert.AreEqual( 0.0f, s.FontColor.Value.a );
}
[TestMethod]
public void FontColor_Rgba()
{
var s = new Styles();
Assert.IsTrue( s.Set( "color", "rgba( 0, 0, 0, 0.5 )" ) );
Assert.IsTrue( s.FontColor.HasValue );
Assert.AreEqual( new Color( 0, 0, 0, 0.5f ), s.FontColor.Value );
}
[TestMethod]
public void FontColor_Invalid()
{
var s = new Styles();
Assert.IsFalse( s.Set( "color", "notacolor" ) );
Assert.IsFalse( s.FontColor.HasValue );
}
// ---------------------------------------------------------------------
// font-weight -> FontWeight (int?)
// ---------------------------------------------------------------------
[TestMethod]
public void FontWeight_Numeric()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-weight", "500" ) );
Assert.IsTrue( s.FontWeight.HasValue );
Assert.AreEqual( 500, s.FontWeight.Value );
}
[TestMethod]
public void FontWeight_KeywordNormal()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-weight", "normal" ) );
Assert.AreEqual( 400, s.FontWeight.Value );
}
[TestMethod]
public void FontWeight_KeywordBold()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-weight", "bold" ) );
Assert.AreEqual( 700, s.FontWeight.Value );
}
[TestMethod]
public void FontWeight_KeywordThin()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-weight", "thin" ) );
Assert.AreEqual( 100, s.FontWeight.Value );
}
[TestMethod]
public void FontWeight_KeywordExtraLight()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-weight", "extralight" ) );
Assert.AreEqual( 200, s.FontWeight.Value );
}
[TestMethod]
public void FontWeight_KeywordLight()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-weight", "light" ) );
Assert.AreEqual( 300, s.FontWeight.Value );
}
[TestMethod]
public void FontWeight_KeywordMedium()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-weight", "medium" ) );
Assert.AreEqual( 500, s.FontWeight.Value );
}
[TestMethod]
public void FontWeight_KeywordSemiBold()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-weight", "semibold" ) );
Assert.AreEqual( 600, s.FontWeight.Value );
}
[TestMethod]
public void FontWeight_KeywordExtraBold()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-weight", "extrabold" ) );
Assert.AreEqual( 800, s.FontWeight.Value );
}
[TestMethod]
public void FontWeight_KeywordBlack()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-weight", "black" ) );
Assert.AreEqual( 900, s.FontWeight.Value );
}
[TestMethod]
public void FontWeight_KeywordExtraBlack()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-weight", "extrablack" ) );
Assert.AreEqual( 950, s.FontWeight.Value );
}
[TestMethod]
public void FontWeight_Invalid()
{
var s = new Styles();
Assert.IsFalse( s.Set( "font-weight", "extra-fat" ) );
Assert.IsFalse( s.FontWeight.HasValue );
}
// ---------------------------------------------------------------------
// font-family -> FontFamily (string)
// ---------------------------------------------------------------------
[TestMethod]
public void FontFamily_Single()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-family", "Arial" ) );
Assert.AreEqual( "Arial", s.FontFamily );
}
[TestMethod]
public void FontFamily_Quoted()
{
// TrimQuoted strips the surrounding quotes
var s = new Styles();
Assert.IsTrue( s.Set( "font-family", "\"Segoe UI\"" ) );
Assert.AreEqual( "Segoe UI", s.FontFamily );
}
/// <summary>
/// A font stack resolves to the first family (we don't resolve fallbacks).
/// </summary>
[TestMethod]
public void FontFamily_StackResolvesFirst()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-family", "\"Segoe UI\", Arial, sans-serif" ) );
Assert.AreEqual( "Segoe UI", s.FontFamily );
}
// ---------------------------------------------------------------------
// font-style -> FontStyle (flags: Italic / Oblique)
// ---------------------------------------------------------------------
[TestMethod]
public void FontStyle_Italic()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-style", "italic" ) );
Assert.AreEqual( FontStyle.Italic, s.FontStyle );
}
[TestMethod]
public void FontStyle_Oblique()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-style", "oblique" ) );
Assert.AreEqual( FontStyle.Oblique, s.FontStyle );
}
[TestMethod]
public void FontStyle_NormalIsNone()
{
// SetFontStyle always succeeds; 'normal' contains neither keyword => None
var s = new Styles();
Assert.IsTrue( s.Set( "font-style", "normal" ) );
Assert.AreEqual( FontStyle.None, s.FontStyle );
}
// ---------------------------------------------------------------------
// font-variant-numeric -> FontVariantNumeric
// ---------------------------------------------------------------------
[TestMethod]
public void FontVariantNumeric_Normal()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-variant-numeric", "normal" ) );
Assert.AreEqual( FontVariantNumeric.Normal, s.FontVariantNumeric );
}
[TestMethod]
public void FontVariantNumeric_TabularNums()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-variant-numeric", "tabular-nums" ) );
Assert.AreEqual( FontVariantNumeric.TabularNums, s.FontVariantNumeric );
}
[TestMethod]
public void FontVariantNumeric_Invalid()
{
var s = new Styles();
Assert.IsFalse( s.Set( "font-variant-numeric", "oldstyle-nums" ) );
}
// ---------------------------------------------------------------------
// font-smooth -> FontSmooth (Auto / Never / Always)
// ---------------------------------------------------------------------
[TestMethod]
public void FontSmooth_Auto()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-smooth", "auto" ) );
Assert.AreEqual( FontSmooth.Auto, s.FontSmooth );
}
[TestMethod]
public void FontSmooth_Never()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-smooth", "never" ) );
Assert.AreEqual( FontSmooth.Never, s.FontSmooth );
}
[TestMethod]
public void FontSmooth_Always()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-smooth", "always" ) );
Assert.AreEqual( FontSmooth.Always, s.FontSmooth );
}
[TestMethod]
public void FontSmooth_Invalid()
{
var s = new Styles();
Assert.IsFalse( s.Set( "font-smooth", "subpixel-antialiased" ) );
}
// ---------------------------------------------------------------------
// line-height -> LineHeight (Length?)
// ---------------------------------------------------------------------
[TestMethod]
public void LineHeight_Pixels()
{
var s = new Styles();
Assert.IsTrue( s.Set( "line-height", "24px" ) );
Assert.IsTrue( s.LineHeight.HasValue );
Assert.AreEqual( 24, s.LineHeight.Value.Value );
Assert.AreEqual( LengthUnit.Pixels, s.LineHeight.Value.Unit );
}
[TestMethod]
public void LineHeight_Percent()
{
var s = new Styles();
Assert.IsTrue( s.Set( "line-height", "120%" ) );
Assert.IsTrue( s.LineHeight.HasValue );
Assert.AreEqual( 120, s.LineHeight.Value.Value );
Assert.AreEqual( LengthUnit.Percentage, s.LineHeight.Value.Unit );
}
/// <summary>
/// A unitless line-height is a multiple of the font size (1.5 == 150%).
/// </summary>
[TestMethod]
public void LineHeight_Unitless_IsMultiplier()
{
var s = new Styles();
Assert.IsTrue( s.Set( "line-height", "1.5" ) );
Assert.IsTrue( s.LineHeight.HasValue );
Assert.AreEqual( 150, s.LineHeight.Value.Value );
Assert.AreEqual( LengthUnit.Percentage, s.LineHeight.Value.Unit );
}
/// <summary>
/// 'line-height: normal' uses the default line height (100% of the font size).
/// </summary>
[TestMethod]
public void LineHeight_Normal()
{
var s = new Styles();
Assert.IsTrue( s.Set( "line-height", "normal" ) );
Assert.AreEqual( 100, s.LineHeight.Value.Value );
Assert.AreEqual( LengthUnit.Percentage, s.LineHeight.Value.Unit );
}
// ---------------------------------------------------------------------
// letter-spacing -> LetterSpacing (Length?)
// ---------------------------------------------------------------------
[TestMethod]
public void LetterSpacing_Pixels()
{
var s = new Styles();
Assert.IsTrue( s.Set( "letter-spacing", "2px" ) );
Assert.IsTrue( s.LetterSpacing.HasValue );
Assert.AreEqual( 2, s.LetterSpacing.Value.Value );
Assert.AreEqual( LengthUnit.Pixels, s.LetterSpacing.Value.Unit );
}
[TestMethod]
public void LetterSpacing_Em()
{
var s = new Styles();
Assert.IsTrue( s.Set( "letter-spacing", "0.1em" ) );
Assert.IsTrue( s.LetterSpacing.HasValue );
Assert.AreEqual( 0.1f, s.LetterSpacing.Value.Value );
Assert.AreEqual( LengthUnit.Em, s.LetterSpacing.Value.Unit );
}
[TestMethod]
public void LetterSpacing_Invalid()
{
var s = new Styles();
Assert.IsFalse( s.Set( "letter-spacing", "wide" ) );
Assert.IsFalse( s.LetterSpacing.HasValue );
}
// ---------------------------------------------------------------------
// word-spacing -> WordSpacing (Length?)
// ---------------------------------------------------------------------
[TestMethod]
public void WordSpacing_Pixels()
{
var s = new Styles();
Assert.IsTrue( s.Set( "word-spacing", "5px" ) );
Assert.IsTrue( s.WordSpacing.HasValue );
Assert.AreEqual( 5, s.WordSpacing.Value.Value );
Assert.AreEqual( LengthUnit.Pixels, s.WordSpacing.Value.Unit );
}
[TestMethod]
public void WordSpacing_Percent()
{
var s = new Styles();
Assert.IsTrue( s.Set( "word-spacing", "50%" ) );
Assert.IsTrue( s.WordSpacing.HasValue );
Assert.AreEqual( 50, s.WordSpacing.Value.Value );
Assert.AreEqual( LengthUnit.Percentage, s.WordSpacing.Value.Unit );
}
[TestMethod]
public void WordSpacing_Invalid()
{
var s = new Styles();
Assert.IsFalse( s.Set( "word-spacing", "spaced" ) );
Assert.IsFalse( s.WordSpacing.HasValue );
}
// ---------------------------------------------------------------------
// word-break -> WordBreak (Normal / BreakAll)
// ---------------------------------------------------------------------
[TestMethod]
public void WordBreak_Normal()
{
var s = new Styles();
Assert.IsTrue( s.Set( "word-break", "normal" ) );
Assert.AreEqual( WordBreak.Normal, s.WordBreak );
}
[TestMethod]
public void WordBreak_BreakAll()
{
var s = new Styles();
Assert.IsTrue( s.Set( "word-break", "break-all" ) );
Assert.AreEqual( WordBreak.BreakAll, s.WordBreak );
}
[TestMethod]
public void WordBreak_Invalid()
{
var s = new Styles();
Assert.IsFalse( s.Set( "word-break", "nonsense" ) );
}
/// <summary>
/// break-word == normal here: word breaking already character-breaks an over-long word on overflow.
/// </summary>
[TestMethod]
public void WordBreak_BreakWord()
{
var s = new Styles();
Assert.IsTrue( s.Set( "word-break", "break-word" ) );
Assert.AreEqual( WordBreak.Normal, s.WordBreak );
}
/// <summary>
/// keep-all is accepted but currently does nothing (no suppress-breaking mode); behaves like normal.
/// </summary>
[TestMethod]
public void WordBreak_KeepAll()
{
var s = new Styles();
Assert.IsTrue( s.Set( "word-break", "keep-all" ) );
Assert.AreEqual( WordBreak.Normal, s.WordBreak );
}
// ---------------------------------------------------------------------
// white-space -> WhiteSpace (Normal / NoWrap / PreLine / Pre)
// ---------------------------------------------------------------------
[TestMethod]
public void WhiteSpace_Normal()
{
var s = new Styles();
Assert.IsTrue( s.Set( "white-space", "normal" ) );
Assert.AreEqual( WhiteSpace.Normal, s.WhiteSpace );
}
[TestMethod]
public void WhiteSpace_NoWrap()
{
var s = new Styles();
Assert.IsTrue( s.Set( "white-space", "nowrap" ) );
Assert.AreEqual( WhiteSpace.NoWrap, s.WhiteSpace );
}
[TestMethod]
public void WhiteSpace_PreLine()
{
var s = new Styles();
Assert.IsTrue( s.Set( "white-space", "pre-line" ) );
Assert.AreEqual( WhiteSpace.PreLine, s.WhiteSpace );
}
[TestMethod]
public void WhiteSpace_Pre()
{
var s = new Styles();
Assert.IsTrue( s.Set( "white-space", "pre" ) );
Assert.AreEqual( WhiteSpace.Pre, s.WhiteSpace );
}
[TestMethod]
public void WhiteSpace_Invalid()
{
var s = new Styles();
Assert.IsFalse( s.Set( "white-space", "collapse" ) );
}
/// <summary>
/// pre-wrap preserves whitespace and wraps.
/// </summary>
[TestMethod]
public void WhiteSpace_PreWrap()
{
var s = new Styles();
Assert.IsTrue( s.Set( "white-space", "pre-wrap" ) );
Assert.AreEqual( WhiteSpace.PreWrap, s.WhiteSpace );
}
[TestMethod]
public void WhiteSpace_BreakSpaces()
{
var s = new Styles();
Assert.IsTrue( s.Set( "white-space", "break-spaces" ) );
Assert.AreEqual( WhiteSpace.BreakSpaces, s.WhiteSpace );
}
// ---------------------------------------------------------------------
// text-align -> TextAlign (Left / Right / Center)
// ---------------------------------------------------------------------
[TestMethod]
public void TextAlign_Left()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-align", "left" ) );
Assert.AreEqual( TextAlign.Left, s.TextAlign );
}
[TestMethod]
public void TextAlign_Right()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-align", "right" ) );
Assert.AreEqual( TextAlign.Right, s.TextAlign );
}
[TestMethod]
public void TextAlign_Center()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-align", "center" ) );
Assert.AreEqual( TextAlign.Center, s.TextAlign );
}
[TestMethod]
public void TextAlign_Invalid()
{
var s = new Styles();
Assert.IsFalse( s.Set( "text-align", "middle" ) );
}
/// <summary>
/// 'justify' is accepted and maps to the Justify alignment.
/// </summary>
[TestMethod]
public void TextAlign_Justify()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-align", "justify" ) );
Assert.AreEqual( Sandbox.UI.TextAlign.Justify, s.TextAlign );
}
/// <summary>
/// start/end map to left/right (logical direction not tracked).
/// </summary>
[TestMethod]
public void TextAlign_Start()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-align", "start" ) );
Assert.AreEqual( TextAlign.Left, s.TextAlign );
}
[TestMethod]
public void TextAlign_End()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-align", "end" ) );
Assert.AreEqual( TextAlign.Right, s.TextAlign );
}
// ---------------------------------------------------------------------
// text-overflow -> TextOverflow (Ellipsis / Clip)
// ---------------------------------------------------------------------
[TestMethod]
public void TextOverflow_Ellipsis()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-overflow", "ellipsis" ) );
Assert.AreEqual( TextOverflow.Ellipsis, s.TextOverflow );
}
[TestMethod]
public void TextOverflow_Clip()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-overflow", "clip" ) );
Assert.AreEqual( TextOverflow.Clip, s.TextOverflow );
}
[TestMethod]
public void TextOverflow_Invalid()
{
var s = new Styles();
Assert.IsFalse( s.Set( "text-overflow", "fade" ) );
}
// ---------------------------------------------------------------------
// text-transform -> TextTransform (Capitalize / Uppercase / Lowercase / None)
// ---------------------------------------------------------------------
[TestMethod]
public void TextTransform_Capitalize()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-transform", "capitalize" ) );
Assert.AreEqual( TextTransform.Capitalize, s.TextTransform );
}
[TestMethod]
public void TextTransform_Uppercase()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-transform", "uppercase" ) );
Assert.AreEqual( TextTransform.Uppercase, s.TextTransform );
}
[TestMethod]
public void TextTransform_Lowercase()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-transform", "lowercase" ) );
Assert.AreEqual( TextTransform.Lowercase, s.TextTransform );
}
[TestMethod]
public void TextTransform_None()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-transform", "none" ) );
Assert.AreEqual( TextTransform.None, s.TextTransform );
}
[TestMethod]
public void TextTransform_Invalid()
{
var s = new Styles();
Assert.IsFalse( s.Set( "text-transform", "smallcaps" ) );
}
// ---------------------------------------------------------------------
// text-decoration (shorthand) -> TextDecorationLine (+color/style/thickness)
// ---------------------------------------------------------------------
[TestMethod]
public void TextDecoration_LineOnly()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-decoration", "underline" ) );
Assert.AreEqual( TextDecoration.Underline, s.TextDecorationLine );
}
[TestMethod]
public void TextDecoration_MultipleLines()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-decoration", "underline overline" ) );
Assert.AreEqual( TextDecoration.Underline | TextDecoration.Overline, s.TextDecorationLine );
}
[TestMethod]
public void TextDecoration_LineColorStyle()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-decoration", "underline red wavy" ) );
Assert.AreEqual( TextDecoration.Underline, s.TextDecorationLine );
Assert.IsTrue( s.TextDecorationColor.HasValue );
Assert.AreEqual( new Color( 1, 0, 0, 1 ), s.TextDecorationColor.Value );
Assert.AreEqual( TextDecorationStyle.Wavy, s.TextDecorationStyle );
}
[TestMethod]
public void TextDecoration_WithThickness()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-decoration", "line-through 3px" ) );
Assert.AreEqual( TextDecoration.LineThrough, s.TextDecorationLine );
Assert.IsTrue( s.TextDecorationThickness.HasValue );
Assert.AreEqual( 3, s.TextDecorationThickness.Value.Value );
Assert.AreEqual( LengthUnit.Pixels, s.TextDecorationThickness.Value.Unit );
}
[TestMethod]
public void TextDecoration_Invalid()
{
// An unrecognised token that is neither a line/color/style/length fails.
var s = new Styles();
Assert.IsFalse( s.Set( "text-decoration", "underline blink" ) );
}
// ---------------------------------------------------------------------
// text-decoration-line -> TextDecorationLine
// ---------------------------------------------------------------------
[TestMethod]
public void TextDecorationLine_Underline()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-decoration-line", "underline" ) );
Assert.AreEqual( TextDecoration.Underline, s.TextDecorationLine );
}
[TestMethod]
public void TextDecorationLine_LineThrough()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-decoration-line", "line-through" ) );
Assert.AreEqual( TextDecoration.LineThrough, s.TextDecorationLine );
}
[TestMethod]
public void TextDecorationLine_Overline()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-decoration-line", "overline" ) );
Assert.AreEqual( TextDecoration.Overline, s.TextDecorationLine );
}
[TestMethod]
public void TextDecorationLine_None()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-decoration-line", "none" ) );
Assert.AreEqual( TextDecoration.None, s.TextDecorationLine );
}
[TestMethod]
public void TextDecorationLine_Combined()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-decoration-line", "underline line-through" ) );
Assert.AreEqual( TextDecoration.Underline | TextDecoration.LineThrough, s.TextDecorationLine );
}
// ---------------------------------------------------------------------
// text-decoration-color -> TextDecorationColor (Color?)
// ---------------------------------------------------------------------
[TestMethod]
public void TextDecorationColor_Named()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-decoration-color", "red" ) );
Assert.IsTrue( s.TextDecorationColor.HasValue );
Assert.AreEqual( new Color( 1, 0, 0, 1 ), s.TextDecorationColor.Value );
}
[TestMethod]
public void TextDecorationColor_Invalid()
{
var s = new Styles();
Assert.IsFalse( s.Set( "text-decoration-color", "notacolor" ) );
Assert.IsFalse( s.TextDecorationColor.HasValue );
}
// ---------------------------------------------------------------------
// text-decoration-style -> TextDecorationStyle
// ---------------------------------------------------------------------
[TestMethod]
public void TextDecorationStyle_Solid()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-decoration-style", "solid" ) );
Assert.AreEqual( TextDecorationStyle.Solid, s.TextDecorationStyle );
}
[TestMethod]
public void TextDecorationStyle_Double()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-decoration-style", "double" ) );
Assert.AreEqual( TextDecorationStyle.Double, s.TextDecorationStyle );
}
[TestMethod]
public void TextDecorationStyle_Dotted()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-decoration-style", "dotted" ) );
Assert.AreEqual( TextDecorationStyle.Dotted, s.TextDecorationStyle );
}
[TestMethod]
public void TextDecorationStyle_Dashed()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-decoration-style", "dashed" ) );
Assert.AreEqual( TextDecorationStyle.Dashed, s.TextDecorationStyle );
}
[TestMethod]
public void TextDecorationStyle_Wavy()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-decoration-style", "wavy" ) );
Assert.AreEqual( TextDecorationStyle.Wavy, s.TextDecorationStyle );
}
[TestMethod]
public void TextDecorationStyle_Invalid()
{
var s = new Styles();
Assert.IsFalse( s.Set( "text-decoration-style", "zigzag" ) );
}
// ---------------------------------------------------------------------
// text-decoration-thickness -> TextDecorationThickness (Length?)
// ---------------------------------------------------------------------
[TestMethod]
public void TextDecorationThickness_Pixels()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-decoration-thickness", "4px" ) );
Assert.IsTrue( s.TextDecorationThickness.HasValue );
Assert.AreEqual( 4, s.TextDecorationThickness.Value.Value );
Assert.AreEqual( LengthUnit.Pixels, s.TextDecorationThickness.Value.Unit );
}
[TestMethod]
public void TextDecorationThickness_Invalid()
{
var s = new Styles();
Assert.IsFalse( s.Set( "text-decoration-thickness", "thick" ) );
Assert.IsFalse( s.TextDecorationThickness.HasValue );
}
// ---------------------------------------------------------------------
// text-decoration-skip-ink -> TextDecorationSkipInk (All / None)
// ---------------------------------------------------------------------
[TestMethod]
public void TextDecorationSkipInk_Auto()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-decoration-skip-ink", "auto" ) );
Assert.AreEqual( TextSkipInk.All, s.TextDecorationSkipInk );
}
[TestMethod]
public void TextDecorationSkipInk_All()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-decoration-skip-ink", "all" ) );
Assert.AreEqual( TextSkipInk.All, s.TextDecorationSkipInk );
}
[TestMethod]
public void TextDecorationSkipInk_None()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-decoration-skip-ink", "none" ) );
Assert.AreEqual( TextSkipInk.None, s.TextDecorationSkipInk );
}
[TestMethod]
public void TextDecorationSkipInk_Invalid()
{
var s = new Styles();
Assert.IsFalse( s.Set( "text-decoration-skip-ink", "maybe" ) );
}
// ---------------------------------------------------------------------
// text-underline-offset / text-overline-offset / text-line-through-offset
// ---------------------------------------------------------------------
[TestMethod]
public void TextUnderlineOffset_Pixels()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-underline-offset", "2px" ) );
Assert.IsTrue( s.TextUnderlineOffset.HasValue );
Assert.AreEqual( 2, s.TextUnderlineOffset.Value.Value );
Assert.AreEqual( LengthUnit.Pixels, s.TextUnderlineOffset.Value.Unit );
}
[TestMethod]
public void TextUnderlineOffset_Invalid()
{
var s = new Styles();
Assert.IsFalse( s.Set( "text-underline-offset", "low" ) );
Assert.IsFalse( s.TextUnderlineOffset.HasValue );
}
[TestMethod]
public void TextOverlineOffset_Pixels()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-overline-offset", "3px" ) );
Assert.IsTrue( s.TextOverlineOffset.HasValue );
Assert.AreEqual( 3, s.TextOverlineOffset.Value.Value );
Assert.AreEqual( LengthUnit.Pixels, s.TextOverlineOffset.Value.Unit );
}
[TestMethod]
public void TextLineThroughOffset_Pixels()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-line-through-offset", "1px" ) );
Assert.IsTrue( s.TextLineThroughOffset.HasValue );
Assert.AreEqual( 1, s.TextLineThroughOffset.Value.Value );
Assert.AreEqual( LengthUnit.Pixels, s.TextLineThroughOffset.Value.Unit );
}
// ---------------------------------------------------------------------
// text-stroke (shorthand) -> TextStrokeWidth + TextStrokeColor
// ---------------------------------------------------------------------
[TestMethod]
public void TextStroke_Shorthand()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-stroke", "2px red" ) );
Assert.IsTrue( s.TextStrokeWidth.HasValue );
Assert.AreEqual( 2, s.TextStrokeWidth.Value.Value );
Assert.AreEqual( LengthUnit.Pixels, s.TextStrokeWidth.Value.Unit );
Assert.IsTrue( s.TextStrokeColor.HasValue );
Assert.AreEqual( new Color( 1, 0, 0, 1 ), s.TextStrokeColor.Value );
}
[TestMethod]
public void TextStroke_MissingColor()
{
// SetTextStroke requires both a width and a color.
var s = new Styles();
Assert.IsFalse( s.Set( "text-stroke", "2px" ) );
}
[TestMethod]
public void TextStrokeWidth_Longhand()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-stroke-width", "5px" ) );
Assert.IsTrue( s.TextStrokeWidth.HasValue );
Assert.AreEqual( 5, s.TextStrokeWidth.Value.Value );
Assert.AreEqual( LengthUnit.Pixels, s.TextStrokeWidth.Value.Unit );
}
[TestMethod]
public void TextStrokeColor_Longhand()
{
var s = new Styles();
Assert.IsTrue( s.Set( "text-stroke-color", "red" ) );
Assert.IsTrue( s.TextStrokeColor.HasValue );
Assert.AreEqual( new Color( 1, 0, 0, 1 ), s.TextStrokeColor.Value );
}
[TestMethod]
public void TextStrokeColor_Invalid()
{
var s = new Styles();
Assert.IsFalse( s.Set( "text-stroke-color", "notacolor" ) );
Assert.IsFalse( s.TextStrokeColor.HasValue );
}
// ---------------------------------------------------------------------
// caret-color -> CaretColor (Color?)
// ---------------------------------------------------------------------
[TestMethod]
public void CaretColor_Named()
{
var s = new Styles();
Assert.IsTrue( s.Set( "caret-color", "red" ) );
Assert.IsTrue( s.CaretColor.HasValue );
Assert.AreEqual( new Color( 1, 0, 0, 1 ), s.CaretColor.Value );
}
[TestMethod]
public void CaretColor_Rgba()
{
var s = new Styles();
Assert.IsTrue( s.Set( "caret-color", "rgba( 0, 0, 0, 0.25 )" ) );
Assert.IsTrue( s.CaretColor.HasValue );
Assert.AreEqual( new Color( 0, 0, 0, 0.25f ), s.CaretColor.Value );
}
[TestMethod]
public void CaretColor_Invalid()
{
var s = new Styles();
Assert.IsFalse( s.Set( "caret-color", "notacolor" ) );
Assert.IsFalse( s.CaretColor.HasValue );
}
// ---------------------------------------------------------------------
// content -> Content (string)
// ---------------------------------------------------------------------
[TestMethod]
public void Content_Quoted()
{
// TrimQuoted strips surrounding quotes.
var s = new Styles();
Assert.IsTrue( s.Set( "content", "\"hello\"" ) );
Assert.AreEqual( "hello", s.Content );
}
[TestMethod]
public void Content_Empty()
{
var s = new Styles();
Assert.IsTrue( s.Set( "content", "\"\"" ) );
Assert.AreEqual( "", s.Content );
}
[TestMethod]
public void Content_Unquoted()
{
var s = new Styles();
Assert.IsTrue( s.Set( "content", "foo" ) );
Assert.AreEqual( "foo", s.Content );
}
// ---------------------------------------------------------------------
// cursor -> Cursor (string)
// ---------------------------------------------------------------------
[TestMethod]
public void Cursor_Pointer()
{
var s = new Styles();
Assert.IsTrue( s.Set( "cursor", "pointer" ) );
Assert.AreEqual( "pointer", s.Cursor );
}
[TestMethod]
public void Cursor_None()
{
var s = new Styles();
Assert.IsTrue( s.Set( "cursor", "none" ) );
Assert.AreEqual( "none", s.Cursor );
}
// ---------------------------------------------------------------------
// mix-blend-mode -> MixBlendMode (string)
// ---------------------------------------------------------------------
[TestMethod]
public void MixBlendMode_Multiply()
{
var s = new Styles();
Assert.IsTrue( s.Set( "mix-blend-mode", "multiply" ) );
Assert.AreEqual( "multiply", s.MixBlendMode );
}
[TestMethod]
public void MixBlendMode_Normal()
{
var s = new Styles();
Assert.IsTrue( s.Set( "mix-blend-mode", "normal" ) );
Assert.AreEqual( "normal", s.MixBlendMode );
}
/// <summary>
/// The font shorthand dispatches to style/weight/size/line-height/family longhands.
/// </summary>
[TestMethod]
public void FontShorthand()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font", "italic bold 16px/1.5 Arial" ) );
Assert.IsTrue( s.FontStyle.Value.HasFlag( Sandbox.UI.FontStyle.Italic ) );
Assert.AreEqual( 700, s.FontWeight.Value );
Assert.AreEqual( 16, s.FontSize.Value.Value );
Assert.AreEqual( 150, s.LineHeight.Value.Value ); // 1.5 -> 150%
Assert.AreEqual( "Arial", s.FontFamily );
}
/// <summary>
/// The minimal font shorthand (just size and family) works.
/// </summary>
[TestMethod]
public void FontShorthandMinimal()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font", "14px Tahoma" ) );
Assert.AreEqual( 14, s.FontSize.Value.Value );
Assert.AreEqual( "Tahoma", s.FontFamily );
}
/// <summary>
/// font-size accepts the CSS absolute-size keywords as well as lengths.
/// </summary>
[TestMethod]
public void FontSizeKeywords()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-size", "large" ) );
Assert.AreEqual( 18, s.FontSize.Value.Value );
Assert.IsTrue( s.Set( "font-size", "xx-large" ) );
Assert.AreEqual( 32, s.FontSize.Value.Value );
Assert.IsTrue( s.Set( "font-size", "13px" ) );
Assert.AreEqual( 13, s.FontSize.Value.Value );
}
/// <summary>
/// letter-spacing and word-spacing accept 'normal' (no extra spacing).
/// </summary>
[TestMethod]
public void LetterAndWordSpacingNormal()
{
var s = new Styles();
Assert.IsTrue( s.Set( "letter-spacing", "normal" ) );
Assert.AreEqual( 0, s.LetterSpacing.Value.Value );
Assert.IsTrue( s.Set( "word-spacing", "normal" ) );
Assert.AreEqual( 0, s.WordSpacing.Value.Value );
Assert.IsTrue( s.Set( "letter-spacing", "2px" ) );
Assert.AreEqual( 2, s.LetterSpacing.Value.Value );
}
/// <summary>
/// font-smooth: none maps to no smoothing.
/// </summary>
[TestMethod]
public void FontSmoothNone()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-smooth", "none" ) );
Assert.AreEqual( Sandbox.UI.FontSmooth.Never, s.FontSmooth.Value );
}
/// <summary>
/// Generic font families map to a concrete font; real family names pass through unchanged.
/// </summary>
[TestMethod]
public void FontFamilyGenerics()
{
var s = new Styles();
Assert.IsTrue( s.Set( "font-family", "sans-serif" ) );
Assert.AreEqual( "Arial", s.FontFamily );
Assert.IsTrue( s.Set( "font-family", "serif" ) );
Assert.AreEqual( "Times New Roman", s.FontFamily );
Assert.IsTrue( s.Set( "font-family", "monospace" ) );
Assert.AreEqual( "Consolas", s.FontFamily );
// A real family name is left alone.
Assert.IsTrue( s.Set( "font-family", "Poppins" ) );
Assert.AreEqual( "Poppins", s.FontFamily );
// We still take the first family from a stack (no fallback resolution).
Assert.IsTrue( s.Set( "font-family", "Inter, sans-serif" ) );
Assert.AreEqual( "Inter", s.FontFamily );
}
}