using AngleSharp.Dom; using AngleSharp.Html.Parser; using XFESystem.Browser; using XFESystem.Desktop; namespace XFESystem.Browser.Tests; public sealed class WebsiteCssParityTests { private static IDocument Page(string body, string css) => new HtmlParser().ParseDocument("" + body); private static BrowserDisplayList Layout(IDocument page) => new BrowserLayoutEngine().Layout(page, new(800, 600)); [Fact] public void ColorMixUsesPremultipliedSrgbAndTransparentAlpha() { var page = Page("
", "#A{height:10px;color:color-mix(in srgb,#512db4 40%,white);background:color-mix(in srgb,#512db4 15%,transparent);border:1px solid color-mix(in srgb,#512db4 24%,#454545)}"); var list = Layout(page); var style = list.GetComputedStyle(page.GetElementById("A")!)!; Assert.Equal(0xB9ABE1u, style.Color.Rgb); Assert.Equal(0x512DB4u,style.Background.Rgb); Assert.Equal(.15,style.Background.Alpha,6); Assert.Equal(0x483F60u,Assert.Single(list.Commands,c=>c.Kind==BrowserDrawKind.Border && c.Bounds.Y==0).Color); } [Theory][InlineData("min(50%,18rem)",288)][InlineData("max(50%,18rem)",400)][InlineData("clamp(200px,50%,300px)",300)] public void CssMathFunctionsUseContainingBlockDimensions(string expression, double expected) { var page = Page("
", "#A{height:10px;width:" + expression + "}"); Assert.Equal(expected, Layout(page).GetBoundingClientRect(page.GetElementById("A")!).Width); } [Fact] public void FontInheritKeepsComputedSizeAndResolvedLengthLineHeight() { var page = Page("
ValueToken
", "#A{font:italic bold 20px/1.6em Consolas}#B{font:inherit}#C{font-size:.5em}"); var list = Layout(page); var code = list.GetComputedStyle(page.GetElementById("B")!)!; var token = list.GetComputedStyle(page.GetElementById("C")!)!; Assert.Equal(20, code.Font.Size); Assert.Equal(700,code.Font.Weight); Assert.True(code.Font.Italic); Assert.Equal(32,code.LineHeight); Assert.Equal(32,token.LineHeight); Assert.Equal(10,token.Font.Size); } [Fact] public void UnitlessLineHeightIsInheritedAsNumber() { var page = Page("
Small
","div{font-size:20px;line-height:1.8}span{font-size:.5em}"); Assert.Equal(18, Layout(page).GetComputedStyle(page.GetElementById("A")!)!.LineHeight); } [Fact] public void HeadingDefaultsHaveDistinctFontSizesWithoutAuthorOverride() { var page = Page("

Title

Section

", ""); var list = Layout(page); Assert.Equal(32,list.GetComputedStyle(page.GetElementById("A")!)!.Font.Size); Assert.Equal(24,list.GetComputedStyle(page.GetElementById("B")!)!.Font.Size); } [Fact] public void InsetShorthandPositionsExpandedElement() { var page = Page("
","#A{position:fixed;inset:1rem 2rem 3rem 4rem}"); var rect = Layout(page).GetBoundingClientRect(page.GetElementById("A")!); Assert.Equal(new BrowserRect(64,16,704,536),rect); } [Fact] public void RealListMarkersFollowNumberingAndAuthorPseudoStyle() { var page = Page("
  1. Alpha
  2. Beta
  3. Gamma
", "li{margin-top:.4em}li::marker{color:#512db4;font-weight:bold;font-size:1.05em}"); var list=Layout(page); foreach (string text in new[]{"3.","8.","9."}) { var marker=Assert.Single(list.Commands,c=>c.Text==text); Assert.Equal(0x512DB4u,marker.Color); Assert.Equal(700,marker.Font.Weight); Assert.Equal(16.8,marker.Font.Size,4); } } [Fact] public void NestedListMarkersInheritListTypeAndMayBeRemoved() { var page=Page("","ul ul{list-style-type:circle}.task{list-style-type:none}"); var list=Layout(page); Assert.Single(list.Commands,c=>c.Text=="•"); Assert.Single(list.Commands,c=>c.Text=="◦"); } [Fact] public void TableCellsShareColumnsAndRowsHaveActualTwoDimensionalBounds() { var page=Page("
KeyDescription
C#Code
","table{display:block;width:max-content;max-width:100%;border-collapse:collapse}td,th{padding:6px 13px;border:1px solid gray}"); var list=Layout(page); var a=list.GetBoundingClientRect(page.GetElementById("A")!); var b=list.GetBoundingClientRect(page.GetElementById("B")!); var c=list.GetBoundingClientRect(page.GetElementById("C")!); var d=list.GetBoundingClientRect(page.GetElementById("D")!); Assert.Equal(a.X,c.X); Assert.Equal(b.X,d.X); Assert.Equal(a.Width,c.Width); Assert.Equal(b.Width,d.Width); Assert.Equal(a.Right-1,b.Left); Assert.Equal(a.Bottom-1,c.Top); Assert.Equal(c.Top,d.Top); } [Fact] public void RadiusCommandsContainNormalizedCornersAndShadowBounds() { var page=Page("
","#A{width:100px;height:40px;border:1px solid #454545;border-radius:12px 8px 4px 0;background:#2b2b2b;box-shadow:0 6px 18px rgba(0,0,0,.18)}"); var list=Layout(page); var command=Assert.Single(list.Commands,c=>c.Decoration is not null); var box=command.Decoration!; Assert.Equal(new BrowserEdges(12,8,4,0),box.Radius); Assert.Equal(0x2B2B2Bu,box.Background.Rgb); Assert.Equal(18,Assert.Single(box.Shadows).Blur); Assert.True(command.Bounds.Leftbox.Bounds.Bottom); } [Fact] public void CoverageLeavesRoundedCornerTransparentAndSmoothsBoundary() { var rect=new BrowserRect(0,0,40,40); var radius=new BrowserEdges(12,12,12,12); Assert.Equal(0,BrowserBoxRasterPainter.Coverage(rect,radius,.5,.5)); Assert.Equal(1,BrowserBoxRasterPainter.Coverage(rect,radius,20,20)); Assert.InRange(BrowserBoxRasterPainter.Coverage(rect,radius,3.5,3.5),.1,.95); } [Fact] public void LinearGradientUsesCssDirectionAndPremultipliedStops() { var page=Page("
","#A{width:100px;height:20px;background:linear-gradient(90deg,red,blue)}"); var decoration=Assert.Single(Layout(page).Commands,c=>c.Decoration is not null).Decoration!; var gradient=Assert.Single(decoration.Gradients); Assert.Equal(0xFF0000u,gradient.Sample(0,10,100,20).Rgb); Assert.Equal(0x0000FFu,gradient.Sample(100,10,100,20).Rgb); Assert.Equal(0x800080u,gradient.Sample(50,10,100,20).Rgb); } [Fact] public void WebsiteLayeredRadialAndLinearGradientsUseAuthoredStops() { var page=Page("
","#A{width:100px;height:100px;background:radial-gradient(circle at 8% 8%,color-mix(in srgb,#512db4 10%,transparent),transparent 28%),linear-gradient(145deg,#ffffff,#eeeeee)}"); var decoration=Assert.Single(Layout(page).Commands,c=>c.Decoration is not null).Decoration!; Assert.Equal(2,decoration.Gradients.Count); var radial=decoration.Gradients[0]; Assert.True(radial.Radial); Assert.Equal(.08,radial.CenterX); Assert.Equal(.1,radial.Sample(8,8,100,100).Alpha,6); Assert.Equal(0,radial.Sample(100,100,100,100).Alpha); } [Fact] public void DeepColorAndLengthFunctionsFailWithinBoundedDepth() { string color=string.Concat(Enumerable.Repeat("color-mix(in srgb,red,",100))+"blue"+new string(')',100); string length=string.Concat(Enumerable.Repeat("min(1px,",100))+"2px"+new string(')',100); var page=Page("
","#A{color:"+color+";width:"+length+";height:10px}"); Assert.Equal(800,Layout(page).GetBoundingClientRect(page.GetElementById("A")!).Width); } [Fact] public void SiblingBlockMarginsCollapseToLargestPositiveAndMostNegative() { var page=Page("

","#A{height:10px;margin-bottom:20px}#B{height:10px;margin-top:30px;margin-bottom:-5px}#C{height:10px;margin-top:8px}");var list=Layout(page); Assert.Equal(40,list.GetBoundingClientRect(page.GetElementById("B")!).Top);Assert.Equal(53,list.GetBoundingClientRect(page.GetElementById("C")!).Top); } [Fact] public void AnywhereAndCjkTextWrapAtMeasuredGlyphBoundaries() { var page=Page("

ABCDEFGHIJ

你好世界你好

","p{width:32px;font-size:10px;line-height:1;overflow-wrap:anywhere}");var list=Layout(page); Assert.Equal(20,list.GetBoundingClientRect(page.GetElementById("A")!).Height);Assert.Equal(20,list.GetBoundingClientRect(page.GetElementById("B")!).Height); Assert.Equal("ABCDEFGHIJ",string.Concat(list.Commands.Where(c=>c.Node?.ParentElement?.Id=="A").Select(c=>c.Text))); } [Fact] public void RoundedOverflowClipsDescendantHitTestingAndPaintsActualCoverage() { var page=Page("
","#A{width:40px;height:40px;border-radius:12px;overflow:hidden;background:white}#B{width:40px;height:40px;background:red}");var list=Layout(page); Assert.NotSame(page.GetElementById("B"),list.HitTest(1,1)?.Node);Assert.Same(page.GetElementById("B"),list.HitTest(20,20)?.Node); Assert.Single(list.Commands,c=>c.Kind==BrowserDrawKind.PushClip&&c.RoundedClip is not null); var pixels=new Pixels();BrowserRasterPainter.Paint(pixels,new(0,0,80,80),list); Assert.Equal(0xFFFFFFu,pixels.At(0,0));Assert.Equal(0xFF0000u,pixels.At(20,20));Assert.NotEqual(0xFF0000u,pixels.At(3,3)); } [Fact] public void ColumnFlexIntrinsicHeightMeasuresInlineSyntaxRunsInActualSharedLines() { var page=Page("
csharp
using System;\n\npublic class Example {}
","#A{display:flex;flex-direction:column;width:500px;font-size:14px;line-height:20px}.toolbar{height:40px;flex:0 0 auto}.viewport{display:flex}code{display:block;white-space:pre}"); var list=Layout(page);Assert.Equal(100,list.GetBoundingClientRect(page.GetElementById("A")!).Height); Assert.Single(list.Commands,c=>c.Text=="using");Assert.Single(list.Commands,c=>c.Text=="Example"); } [Fact] public void FlexItemPercentageMaxWidthUsesContainingBlockInsteadOfItsOwnBasis() { var page=Page("
Label textTools
","#A{display:flex;width:600px}#B{max-width:50%;flex-shrink:0}");var list=Layout(page); Assert.Equal(96,list.GetBoundingClientRect(page.GetElementById("B")!).Width,4); } [Fact] public void VariableOnlyBackgroundShorthandResolvesTheOriginalAuthorColor() { var page=Page("
",":root{--surface:#323232}#A{height:10px;background:var(--surface,#ffffff)}"); Assert.Equal(0x323232u,Layout(page).GetComputedStyle(page.GetElementById("A")!)!.Background.Rgb); } [Fact] public void WebsiteArticleRadialLengthStopStaysTransparentAndZeroCenterIsTheTopEdge() { var page=Page("
",":root{--main-color:#512db4;--bg-color:white}article{width:80px;height:80px;background:radial-gradient(circle at 100% 0,color-mix(in srgb,var(--main-color) 10%,transparent),transparent 28rem),var(--bg-color)}"); var list=Layout(page);var decoration=Assert.Single(list.Commands,c=>c.Decoration is not null).Decoration!;var gradient=Assert.Single(decoration.Gradients); Assert.Equal(1,gradient.CenterX);Assert.Equal(0,gradient.CenterY); Assert.Equal(0,gradient.Stops[^1].Color.Alpha);Assert.Equal(448/Math.Sqrt(80*80+80*80),gradient.Stops[^1].Position,6); Assert.Equal(.1,gradient.Sample(80,0,80,80).Alpha,6);Assert.InRange(gradient.Sample(0,0,80,80).Alpha,.07,.09); var pixels=new Pixels();BrowserRasterPainter.Paint(pixels,new(0,0,80,80),list); foreach(var point in new[]{(1,1),(40,20),(78,1),(20,70)}) {uint rgb=pixels.At(point.Item1,point.Item2);Assert.InRange(rgb>>16&255,230u,255u);Assert.InRange(rgb>>8&255,230u,255u);Assert.InRange(rgb&255,240u,255u);} } [Theory][InlineData("transparent 20px",20)][InlineData("transparent 2rem",32)][InlineData("transparent 2em",32)] public void GradientLengthStopsUseTheirActualCssCoordinateSpace(string stop,double pixels) { var page=Page("
","#A{width:80px;height:80px;font-size:16px;background:linear-gradient(90deg,red,"+stop+")}");var gradient=Assert.Single(Assert.Single(Layout(page).Commands,c=>c.Decoration is not null).Decoration!.Gradients); Assert.Equal(pixels/80,gradient.Stops[^1].Position,6);Assert.Equal(0,gradient.Sample(pixels+1,1,80,80).Alpha); } private sealed class Pixels:IDesktopCoverageCanvas { private readonly uint[] values=new uint[80*80];public int Width=>80;public int Height=>80; public uint At(int x,int y)=>values[y*80+x]; public void Fill(uint rgb,int x,int y,int width,int height) {for(int py=Math.Max(0,y);py mask) { for(int py=0;py=80||yy<0||yy>=80)continue;uint old=values[yy*80+xx];int alpha=mask[py*width+px];uint Channel(int shift)=>(uint)((((rgb>>shift)&255)*alpha+((old>>shift)&255)*(255-alpha)+127)/255);values[yy*80+xx]=Channel(16)<<16|Channel(8)<<8|Channel(0);} } public void Line(uint rgb,int x1,int y1,int x2,int y2){}public void Text(string text,uint rgb,int x,int y){}public void Present(){} } }