using AngleSharp.Dom;
using XFESystem.Desktop;
namespace XFESystem.Browser.Tests;
public sealed class PreviewWorkReuseTests
{
private sealed class Transport(string html) : IWebTransport
{ public WebResponse Get(Uri address,BrowserLimits limits)=>new(address,200,"text/html",html); }
private static BrowserService Page(string body,string css="")
{
var browser=new BrowserService(new Transport("<style>body{margin:0}p{margin:0;height:40px}"+css+"</style>"+body));
browser.Navigate("http://example.test/");browser.Layout(new(300,100));return browser;
}
[Fact] public void VerticalScrollReusesBoxesAndKeepsOldSnapshotGeometryAndCanvasBackground()
{
var browser=Page("<p id=A>one</p><p id=B>two</p><p>three</p><p>four</p>","body{background:#102538}");
var node=browser.Document!.GetElementById("B")!;var first=browser.Layout(new(300,100));long passes=browser.LayoutPassCount;
var next=browser.Layout(new(300,100,0,20));
Assert.Equal(passes,browser.LayoutPassCount);Assert.Equal(1,browser.ScrollLayoutReuseCount);
Assert.Equal(40,first.GetBoundingClientRect(node).Y);Assert.Equal(20,next.GetBoundingClientRect(node).Y);
Assert.Equal(next.GetBoundingClientRect(node).Y,next.HitTest(10,25)!.BorderBox.Y-20);
Assert.All(next.Commands.Where(c=>c.ViewportPinned),c=>Assert.Equal(20,c.Bounds.Y));
Assert.All(first.Commands.Where(c=>c.ViewportPinned),c=>Assert.Equal(0,c.Bounds.Y));
}
[Theory][InlineData("fixed;top:0",0,20,false)][InlineData("sticky;top:0",0,20,false)]
[InlineData("sticky;left:0",0,20,true)][InlineData("sticky;left:0",20,0,false)]
public void PositionedScrollFallbackDependsOnAffectedAxis(string position,double x,double y,bool reuse)
{
var browser=Page("<p id=A>one</p><p>two</p><p>three</p>","#A{position:"+position+"}");long passes=browser.LayoutPassCount;
browser.Layout(new(300,100,x,y));Assert.Equal(passes+(reuse?0:1),browser.LayoutPassCount);
}
[Fact] public void MutationAndResizeStillRebuildAfterAReusedScroll()
{
var browser=Page("<p id=A>one</p><p>two</p><p>three</p>");browser.Layout(new(300,100,0,20));long passes=browser.LayoutPassCount;
browser.ExecuteJavaScript("document.getElementById('A').style.height='80px'");
var changed=browser.Layout(new(300,100,0,20));Assert.Equal(80,changed.GetBoundingClientRect(browser.Document!.GetElementById("A")!).Height);
Assert.True(browser.LayoutPassCount>passes);passes=browser.LayoutPassCount;
browser.Layout(new(200,100,0,20));Assert.Equal(passes+1,browser.LayoutPassCount);
}
[Fact] public void MovingAcrossPlainParagraphsDoesNotRebuildForUnrelatedButtonHoverRules()
{
var browser=Page("<p id=A>one</p><p id=B>two</p><button>button</button>","button:hover{color:red}");
long passes=browser.LayoutPassCount,revision=browser.Revision;
browser.Pointer(new("mousemove",10,10));browser.Pointer(new("mousemove",10,50));browser.Layout(new(300,100));
Assert.Equal(passes,browser.LayoutPassCount);Assert.Equal(revision,browser.Revision);
}
[Fact] public void HoverAncestorDescendantAndNotSelectorsStillRebuildAndApplyStyles()
{
var browser=Page("<div id=P><p id=A>one</p></div><p id=B>two</p>","#P:hover #A{color:#ff0000}#P:not(:hover) #A{color:#0000ff}");
browser.Pointer(new("mousemove",10,10));var list=browser.Layout(new(300,100));
Assert.Equal(0xff0000u,list.GetComputedStyle(browser.Document!.GetElementById("A")!)!.Color.Rgb);
browser.Pointer(new("mousemove",10,50));list=browser.Layout(new(300,100));
Assert.Equal(0x0000ffu,list.GetComputedStyle(browser.Document!.GetElementById("A")!)!.Color.Rgb);
}
[Fact] public void IndexedSimpleClassRulesKeepSpecificityOrderAndGeneratedContent()
{
var browser=Page("<p id=A class='Target'>one</p>",".Target{color:red}#A{color:green}.Target{color:blue!important}.Target::before{content:'icon'}");
var list=browser.Layout(new(300,100));var node=browser.Document!.GetElementById("A")!;
Assert.Equal(0x0000ffu,list.GetComputedStyle(node)!.Color.Rgb);
Assert.Contains(list.Commands,c=>c.Kind==BrowserDrawKind.Text&&c.Text=="icon");
browser.ExecuteJavaScript("document.getElementById('A').className='Other'");list=browser.Layout(new(300,100));
Assert.Equal(0x008000u,list.GetComputedStyle(node)!.Color.Rgb);
Assert.DoesNotContain(list.Commands,c=>c.Kind==BrowserDrawKind.Text&&c.Text=="icon");
}
[Fact] public void ComplexClassSelectorsStayOnTheFullMatcherPath()
{
var browser=Page("<div class='Parent'><p id=A class='Target'>one</p></div>",".Parent .Target{color:red}:is(.Target){color:blue}.Parent:not(.Missing) .Target{color:#123456}");
var list=browser.Layout(new(300,100));Assert.Equal(0x123456u,list.GetComputedStyle(browser.Document!.GetElementById("A")!)!.Color.Rgb);
}
}
using AngleSharp.Dom;
using XFESystem.Desktop;
namespace XFESystem.Browser.Tests;
public sealed class PreviewWorkReuseTests
{
private sealed class Transport(string html) : IWebTransport
{ public WebResponse Get(Uri address,BrowserLimits limits)=>new(address,200,"text/html",html); }
private static BrowserService Page(string body,string css="")
{
var browser=new BrowserService(new Transport("<style>body{margin:0}p{margin:0;height:40px}"+css+"</style>"+body));
browser.Navigate("http://example.test/");browser.Layout(new(300,100));return browser;
}
[Fact] public void VerticalScrollReusesBoxesAndKeepsOldSnapshotGeometryAndCanvasBackground()
{
var browser=Page("<p id=A>one</p><p id=B>two</p><p>three</p><p>four</p>","body{background:#102538}");
var node=browser.Document!.GetElementById("B")!;var first=browser.Layout(new(300,100));long passes=browser.LayoutPassCount;
var next=browser.Layout(new(300,100,0,20));
Assert.Equal(passes,browser.LayoutPassCount);Assert.Equal(1,browser.ScrollLayoutReuseCount);
Assert.Equal(40,first.GetBoundingClientRect(node).Y);Assert.Equal(20,next.GetBoundingClientRect(node).Y);
Assert.Equal(next.GetBoundingClientRect(node).Y,next.HitTest(10,25)!.BorderBox.Y-20);
Assert.All(next.Commands.Where(c=>c.ViewportPinned),c=>Assert.Equal(20,c.Bounds.Y));
Assert.All(first.Commands.Where(c=>c.ViewportPinned),c=>Assert.Equal(0,c.Bounds.Y));
}
[Theory][InlineData("fixed;top:0",0,20,false)][InlineData("sticky;top:0",0,20,false)]
[InlineData("sticky;left:0",0,20,true)][InlineData("sticky;left:0",20,0,false)]
public void PositionedScrollFallbackDependsOnAffectedAxis(string position,double x,double y,bool reuse)
{
var browser=Page("<p id=A>one</p><p>two</p><p>three</p>","#A{position:"+position+"}");long passes=browser.LayoutPassCount;
browser.Layout(new(300,100,x,y));Assert.Equal(passes+(reuse?0:1),browser.LayoutPassCount);
}
[Fact] public void MutationAndResizeStillRebuildAfterAReusedScroll()
{
var browser=Page("<p id=A>one</p><p>two</p><p>three</p>");browser.Layout(new(300,100,0,20));long passes=browser.LayoutPassCount;
browser.ExecuteJavaScript("document.getElementById('A').style.height='80px'");
var changed=browser.Layout(new(300,100,0,20));Assert.Equal(80,changed.GetBoundingClientRect(browser.Document!.GetElementById("A")!).Height);
Assert.True(browser.LayoutPassCount>passes);passes=browser.LayoutPassCount;
browser.Layout(new(200,100,0,20));Assert.Equal(passes+1,browser.LayoutPassCount);
}
[Fact] public void MovingAcrossPlainParagraphsDoesNotRebuildForUnrelatedButtonHoverRules()
{
var browser=Page("<p id=A>one</p><p id=B>two</p><button>button</button>","button:hover{color:red}");
long passes=browser.LayoutPassCount,revision=browser.Revision;
browser.Pointer(new("mousemove",10,10));browser.Pointer(new("mousemove",10,50));browser.Layout(new(300,100));
Assert.Equal(passes,browser.LayoutPassCount);Assert.Equal(revision,browser.Revision);
}
[Fact] public void HoverAncestorDescendantAndNotSelectorsStillRebuildAndApplyStyles()
{
var browser=Page("<div id=P><p id=A>one</p></div><p id=B>two</p>","#P:hover #A{color:#ff0000}#P:not(:hover) #A{color:#0000ff}");
browser.Pointer(new("mousemove",10,10));var list=browser.Layout(new(300,100));
Assert.Equal(0xff0000u,list.GetComputedStyle(browser.Document!.GetElementById("A")!)!.Color.Rgb);
browser.Pointer(new("mousemove",10,50));list=browser.Layout(new(300,100));
Assert.Equal(0x0000ffu,list.GetComputedStyle(browser.Document!.GetElementById("A")!)!.Color.Rgb);
}
[Fact] public void IndexedSimpleClassRulesKeepSpecificityOrderAndGeneratedContent()
{
var browser=Page("<p id=A class='Target'>one</p>",".Target{color:red}#A{color:green}.Target{color:blue!important}.Target::before{content:'icon'}");
var list=browser.Layout(new(300,100));var node=browser.Document!.GetElementById("A")!;
Assert.Equal(0x0000ffu,list.GetComputedStyle(node)!.Color.Rgb);
Assert.Contains(list.Commands,c=>c.Kind==BrowserDrawKind.Text&&c.Text=="icon");
browser.ExecuteJavaScript("document.getElementById('A').className='Other'");list=browser.Layout(new(300,100));
Assert.Equal(0x008000u,list.GetComputedStyle(node)!.Color.Rgb);
Assert.DoesNotContain(list.Commands,c=>c.Kind==BrowserDrawKind.Text&&c.Text=="icon");
}
[Fact] public void ComplexClassSelectorsStayOnTheFullMatcherPath()
{
var browser=Page("<div class='Parent'><p id=A class='Target'>one</p></div>",".Parent .Target{color:red}:is(.Target){color:blue}.Parent:not(.Missing) .Target{color:#123456}");
var list=browser.Layout(new(300,100));Assert.Equal(0x123456u,list.GetComputedStyle(browser.Document!.GetElementById("A")!)!.Color.Rgb);
}
}