namespace XFESystem.Browser.Tests;
public sealed class BrowserEventLoopTests
{
[Fact]
public void TimersWaitForExplicitMonotonicPumpAndCancellationWorks()
{
var runtime = new JavaScriptRuntime();
Assert.True(runtime.Evaluate("var values=[];setTimeout((a,b)=>values.push(a+b),10,20,22);const gone=setTimeout(()=>values.push(99),0);clearTimeout(gone);").Success);
Assert.True(runtime.Pump(9).Success); Assert.Equal("", runtime.Evaluate("values.join(',')").Value);
Assert.True(runtime.Pump(10).Success); Assert.Equal("42", runtime.Evaluate("values.join(',')").Value); Assert.Equal(0, runtime.PendingTaskCount);
}
[Fact]
public void IntervalsDoNotCatchUpInUnboundedLoopsAndCanCancelThemselves()
{
var runtime = new JavaScriptRuntime();
runtime.Evaluate("var ticks=0;var timer=setInterval(()=>{if(++ticks===2)clearInterval(timer);},1)");
runtime.Pump(1_000); Assert.Equal("1", runtime.Evaluate("ticks").Value);
runtime.Pump(1_000); Assert.Equal("1", runtime.Evaluate("ticks").Value);
runtime.Pump(1_001); Assert.Equal("2", runtime.Evaluate("ticks").Value); Assert.Equal(0, runtime.PendingTaskCount);
}
[Fact]
public void RafRecursionWaitsForNextFrameAndReceivesMonotonicTime()
{
var runtime = new JavaScriptRuntime(); runtime.Evaluate("var frames=[];function frame(t){frames.push(t);requestAnimationFrame(frame);}requestAnimationFrame(frame)");
runtime.Pump(15); Assert.Equal("15", runtime.Evaluate("frames.join(',')").Value);
runtime.Pump(14); Assert.Equal("15", runtime.Evaluate("frames.join(',')").Value);
runtime.Pump(16); Assert.Equal("15,16", runtime.Evaluate("frames.join(',')").Value);
}
[Fact]
public void RealPromiseAndQueueMicrotaskOrderPrecedesTimers()
{
var runtime = new JavaScriptRuntime(); var result = runtime.Evaluate("var order=[];setTimeout(()=>order.push('timer'),0);Promise.resolve().then(()=>order.push('promise'));queueMicrotask(()=>order.push('micro'));order.push('sync');");
Assert.True(result.Success, result.Error); Assert.Equal("sync,promise,micro", runtime.Evaluate("order.join(',')").Value);
runtime.Pump(0); Assert.Equal("sync,promise,micro,timer", runtime.Evaluate("order.join(',')").Value);
}
[Fact]
public void PromisePolyfillUsingQueueMicrotaskCannotRecursivelyReplaceTheHostsQueue()
{
using var runtime = new JavaScriptRuntime();
var result = runtime.Evaluate("""
var order=[],NativePromise=Promise;
globalThis.Promise={resolve(value){return {then(callback){queueMicrotask(()=>callback(value))}}}};
Promise.resolve('polyfill').then(value=>order.push(value));
queueMicrotask(()=>order.push('micro'));NativePromise.resolve().then(()=>order.push('native'));
order.push('sync');
""");
Assert.True(result.Success, result.Error);
Assert.Equal("sync,polyfill,micro,native", runtime.Evaluate("order.join(',')").Value);
Assert.Equal(0, runtime.PendingTaskCount);
}
[Fact]
public void HiddenMicrotaskAnchorKeepsItsOriginalThenAndSpeciesWhenPageReplacesThePrototype()
{
using var runtime = new JavaScriptRuntime();
var result = runtime.Evaluate("""
var ran=0;Promise.prototype.then=function(){throw Error('page then')};
Promise.prototype.constructor=function(){throw Error('page constructor')};
globalThis.Promise=function(){throw Error('page Promise')};queueMicrotask(()=>ran++);
""");
Assert.True(result.Success, result.Error); Assert.Equal("1", runtime.Evaluate("ran").Value);
}
[Fact]
public void CallbackCountQueueSizeAndCallbackExecutionAreBounded()
{
var runtime = new JavaScriptRuntime(new() { MaximumPendingTasks = 4, MaximumEventCallbacksPerPump = 2, MaximumStatements = 100 });
Assert.True(runtime.Evaluate("var count=0;for(let i=0;i<4;i++)setTimeout(()=>++count,0)").Success);
Assert.False(runtime.Evaluate("setTimeout(()=>{},0)").Success);
runtime.Pump(0); Assert.Equal("2", runtime.Evaluate("count").Value); Assert.Equal(2, runtime.PendingTaskCount);
runtime.Pump(0); Assert.Equal("4", runtime.Evaluate("count").Value);
runtime.Evaluate("setTimeout(()=>{while(true){}},0)"); Assert.False(runtime.Pump(0).Success);
Assert.Equal("42", runtime.Evaluate("6*7").Value);
}
[Fact]
public void StringTimerSourcesAreRejectedAndClockCannotRegress()
{
var runtime = new JavaScriptRuntime(); Assert.False(runtime.Evaluate("setTimeout('alert(1)',0)").Success);
runtime.Pump(100); runtime.Pump(2); Assert.Equal("100", runtime.Evaluate("performance.now()").Value);
}
[Fact]
public void BoundAndProxyFunctionsAreLegitimateTimerCallbacks()
{
var runtime = new JavaScriptRuntime();
var result = runtime.Evaluate("var values=[];setTimeout(function(x){values.push(this.base+x)}.bind({base:40},2),0);setTimeout(new Proxy(()=>values.push(43),{}),0)");
Assert.True(result.Success, result.Error); Assert.True(runtime.Pump(0).Success);
Assert.Equal("42,43", runtime.Evaluate("values.join(',')").Value);
}
[Fact]
public void PerformanceMarksAndMeasuresReflectActualPumpedTimeAndCanBeCleared()
{
var runtime = new JavaScriptRuntime(); Assert.True(runtime.Evaluate("performance.mark('start')").Success); runtime.Pump(25);
var result = runtime.Evaluate("performance.mark('end');performance.measure('elapsed','start','end');performance.getEntriesByType('measure')[0].duration");
Assert.True(result.Success, result.Error); Assert.Equal("25", result.Value);
Assert.Equal("2", runtime.Evaluate("performance.getEntriesByType('mark').length").Value);
runtime.Evaluate("performance.clearMarks('start');performance.clearMeasures();");
Assert.Equal("1", runtime.Evaluate("performance.getEntries().length").Value);
Assert.False(runtime.Evaluate("performance.measure('bad','unknown')").Success);
}
[Fact]
public void PerformanceBufferRetainsBoundedRecentEntriesAndReportsEviction()
{
var runtime = new JavaScriptRuntime(new() { MaximumPendingTasks = 4 });
var result = runtime.Evaluate("for(let i=0;i<10;i++)performance.mark('mark'+i);[performance.getEntries().length,performance.droppedEntriesCount,performance.getEntries()[0].name].join(':')");
Assert.True(result.Success, result.Error); Assert.Equal("4:6:mark6", result.Value);
}
}
namespace XFESystem.Browser.Tests;
public sealed class BrowserEventLoopTests
{
[Fact]
public void TimersWaitForExplicitMonotonicPumpAndCancellationWorks()
{
var runtime = new JavaScriptRuntime();
Assert.True(runtime.Evaluate("var values=[];setTimeout((a,b)=>values.push(a+b),10,20,22);const gone=setTimeout(()=>values.push(99),0);clearTimeout(gone);").Success);
Assert.True(runtime.Pump(9).Success); Assert.Equal("", runtime.Evaluate("values.join(',')").Value);
Assert.True(runtime.Pump(10).Success); Assert.Equal("42", runtime.Evaluate("values.join(',')").Value); Assert.Equal(0, runtime.PendingTaskCount);
}
[Fact]
public void IntervalsDoNotCatchUpInUnboundedLoopsAndCanCancelThemselves()
{
var runtime = new JavaScriptRuntime();
runtime.Evaluate("var ticks=0;var timer=setInterval(()=>{if(++ticks===2)clearInterval(timer);},1)");
runtime.Pump(1_000); Assert.Equal("1", runtime.Evaluate("ticks").Value);
runtime.Pump(1_000); Assert.Equal("1", runtime.Evaluate("ticks").Value);
runtime.Pump(1_001); Assert.Equal("2", runtime.Evaluate("ticks").Value); Assert.Equal(0, runtime.PendingTaskCount);
}
[Fact]
public void RafRecursionWaitsForNextFrameAndReceivesMonotonicTime()
{
var runtime = new JavaScriptRuntime(); runtime.Evaluate("var frames=[];function frame(t){frames.push(t);requestAnimationFrame(frame);}requestAnimationFrame(frame)");
runtime.Pump(15); Assert.Equal("15", runtime.Evaluate("frames.join(',')").Value);
runtime.Pump(14); Assert.Equal("15", runtime.Evaluate("frames.join(',')").Value);
runtime.Pump(16); Assert.Equal("15,16", runtime.Evaluate("frames.join(',')").Value);
}
[Fact]
public void RealPromiseAndQueueMicrotaskOrderPrecedesTimers()
{
var runtime = new JavaScriptRuntime(); var result = runtime.Evaluate("var order=[];setTimeout(()=>order.push('timer'),0);Promise.resolve().then(()=>order.push('promise'));queueMicrotask(()=>order.push('micro'));order.push('sync');");
Assert.True(result.Success, result.Error); Assert.Equal("sync,promise,micro", runtime.Evaluate("order.join(',')").Value);
runtime.Pump(0); Assert.Equal("sync,promise,micro,timer", runtime.Evaluate("order.join(',')").Value);
}
[Fact]
public void PromisePolyfillUsingQueueMicrotaskCannotRecursivelyReplaceTheHostsQueue()
{
using var runtime = new JavaScriptRuntime();
var result = runtime.Evaluate("""
var order=[],NativePromise=Promise;
globalThis.Promise={resolve(value){return {then(callback){queueMicrotask(()=>callback(value))}}}};
Promise.resolve('polyfill').then(value=>order.push(value));
queueMicrotask(()=>order.push('micro'));NativePromise.resolve().then(()=>order.push('native'));
order.push('sync');
""");
Assert.True(result.Success, result.Error);
Assert.Equal("sync,polyfill,micro,native", runtime.Evaluate("order.join(',')").Value);
Assert.Equal(0, runtime.PendingTaskCount);
}
[Fact]
public void HiddenMicrotaskAnchorKeepsItsOriginalThenAndSpeciesWhenPageReplacesThePrototype()
{
using var runtime = new JavaScriptRuntime();
var result = runtime.Evaluate("""
var ran=0;Promise.prototype.then=function(){throw Error('page then')};
Promise.prototype.constructor=function(){throw Error('page constructor')};
globalThis.Promise=function(){throw Error('page Promise')};queueMicrotask(()=>ran++);
""");
Assert.True(result.Success, result.Error); Assert.Equal("1", runtime.Evaluate("ran").Value);
}
[Fact]
public void CallbackCountQueueSizeAndCallbackExecutionAreBounded()
{
var runtime = new JavaScriptRuntime(new() { MaximumPendingTasks = 4, MaximumEventCallbacksPerPump = 2, MaximumStatements = 100 });
Assert.True(runtime.Evaluate("var count=0;for(let i=0;i<4;i++)setTimeout(()=>++count,0)").Success);
Assert.False(runtime.Evaluate("setTimeout(()=>{},0)").Success);
runtime.Pump(0); Assert.Equal("2", runtime.Evaluate("count").Value); Assert.Equal(2, runtime.PendingTaskCount);
runtime.Pump(0); Assert.Equal("4", runtime.Evaluate("count").Value);
runtime.Evaluate("setTimeout(()=>{while(true){}},0)"); Assert.False(runtime.Pump(0).Success);
Assert.Equal("42", runtime.Evaluate("6*7").Value);
}
[Fact]
public void StringTimerSourcesAreRejectedAndClockCannotRegress()
{
var runtime = new JavaScriptRuntime(); Assert.False(runtime.Evaluate("setTimeout('alert(1)',0)").Success);
runtime.Pump(100); runtime.Pump(2); Assert.Equal("100", runtime.Evaluate("performance.now()").Value);
}
[Fact]
public void BoundAndProxyFunctionsAreLegitimateTimerCallbacks()
{
var runtime = new JavaScriptRuntime();
var result = runtime.Evaluate("var values=[];setTimeout(function(x){values.push(this.base+x)}.bind({base:40},2),0);setTimeout(new Proxy(()=>values.push(43),{}),0)");
Assert.True(result.Success, result.Error); Assert.True(runtime.Pump(0).Success);
Assert.Equal("42,43", runtime.Evaluate("values.join(',')").Value);
}
[Fact]
public void PerformanceMarksAndMeasuresReflectActualPumpedTimeAndCanBeCleared()
{
var runtime = new JavaScriptRuntime(); Assert.True(runtime.Evaluate("performance.mark('start')").Success); runtime.Pump(25);
var result = runtime.Evaluate("performance.mark('end');performance.measure('elapsed','start','end');performance.getEntriesByType('measure')[0].duration");
Assert.True(result.Success, result.Error); Assert.Equal("25", result.Value);
Assert.Equal("2", runtime.Evaluate("performance.getEntriesByType('mark').length").Value);
runtime.Evaluate("performance.clearMarks('start');performance.clearMeasures();");
Assert.Equal("1", runtime.Evaluate("performance.getEntries().length").Value);
Assert.False(runtime.Evaluate("performance.measure('bad','unknown')").Success);
}
[Fact]
public void PerformanceBufferRetainsBoundedRecentEntriesAndReportsEviction()
{
var runtime = new JavaScriptRuntime(new() { MaximumPendingTasks = 4 });
var result = runtime.Evaluate("for(let i=0;i<10;i++)performance.mark('mark'+i);[performance.getEntries().length,performance.droppedEntriesCount,performance.getEntries()[0].name].join(':')");
Assert.True(result.Success, result.Error); Assert.Equal("4:6:mark6", result.Value);
}
}