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); } }