Running JavaScript

edit

Create a JavaScriptScriptEngine and give it to the browsing context. Navigate with NavigateAsync, which waits for the scripts that block the parser, then run the event loop:

using var engine = new JavaScriptScriptEngine(new JavaScriptScriptEngineOptions
{
    Timeout = TimeSpan.FromSeconds(5), // per script or callback
});

using var context = new BrowsingContext(new BrowsingContextOptions
{
    ScriptEngine = engine,
    DocumentLoader = DocumentLoader.Create(), // optional: external scripts, modules, fetch, XMLHttpRequest, WebSocket, EventSource...
    UnhandledScriptError = (_, error) => Console.Error.WriteLine(error),
    ConsoleMessage = (_, message) => Console.WriteLine(message.Text), // console.log(), console.count()...
});

var document = await context.NavigateAsync("https://example.com/", _ => """
    <title>Example</title>
    <ul id=list></ul>
    <script>
      setTimeout(() => document.getElementById('list').innerHTML = '<li>a<li>b', 100);
    </script>
    """);

// Run the tasks and the timers until there is nothing left
do { await context.EventLoop.RunUntilIdleAsync(); } while (await context.EventLoop.WaitForNextTimerAsync());

Assert.Equal("<li>a</li><li>b</li>", document.GetElementById("list")!.InnerHtml);

// Evaluate code in the realm of a window
var title = engine.Evaluate(context.Window, "document.title");
Assert.Equal("Example", title.AsString());
  • The scripts run on the thread that runs the event loop, when the event loop runs their tasks: the inline scripts while the document is parsed, the timers and the event listeners when their tasks run.
  • The external scripts and the modules are fetched by the DocumentLoader: without a loader, only the inline scripts run.
  • The engine is disposable. Several browsing contexts can share it: each window is a realm of its own, with its own global object.
  • A worker gets its own engine, with the same options, on its own thread.

Options

JavaScriptScriptEngineOptionsDefaultEffect
Timeout10 secondsThe longest time an entry into JavaScript runs: a script, an event listener, a timer callback
MemoryLimit256 MBThe memory an entry into JavaScript allocates
RecursionLimit512 callsThe depth of the calls; a deeper call throws a RangeError
MaxStatementsnoneThe number of loop iterations and calls of an entry
RegexTimeout2 secondsThe longest time of a regular expression match
TimeProviderthe system clockThe clock of Date and the local time zone
ConfigureEnginenoneSets the other options of the engine, such as the time zone and the default locale of Intl

When a limit is exceeded, the script stops and its error is reported: see Running untrusted pages.

Console messages and errors

BrowsingContextOptions.ConsoleMessage receives the messages of console.log(), console.error() and the other methods of the console, formatted as the console of a browser formats them (ConsoleMessage.Text), with their level and their arguments. BrowsingContextOptions.UnhandledScriptError receives the errors that no script handled: the exceptions of the scripts and the rejected promises, with their message, their file and their position (ScriptError). The errors do not stop the page: the next scripts run, as in a browser.

Modules, workers and worklets

<script type="module">, import(), the import maps, the JSON and CSS modules, the dedicated and shared workers and the worklets are supported. Each worker runs its own event loop on its own thread; the event loop of the page waits for its workers in RunUntilIdleAsync().