Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
# 1.1.3

Released on Friday, October 30 2026.

- Fixed prepared regular-expression literals ignoring the timeout configured during engine creation
- Fixed the console only having `log`; it now has all methods of the console standard (e.g. `error`, `warn`, `info`, `debug`, `assert`, `count`, `time`), as scripts calling or wrapping one of them failed
- Fixed reading the globals of scripts (e.g. `var x`, `x = 1`) and the built-ins (e.g. `Promise`, `JSON`) through `window`, which gave `undefined`
- Fixed named indexers (e.g. `item` of `NodeList` and `HTMLCollection`, `namedItem`) being exposed as properties instead of methods
- Fixed functions passed for delegates with value-type or no parameters failing to convert
- Fixed errors thrown by DOM members losing their message
- Fixed only the last of several extension methods of the same name (e.g. `scrollTo(x, y)` and `scrollTo(options)`) being callable; the one fitting the arguments is called
- Fixed the geometry stubs of this library (e.g. `offsetHeight` always 0) hiding the implementation of another library (e.g. AngleSharp.Renderer)
- Fixed plain objects not converting to dictionaries (classes with `DomName` members, e.g. `{ top: 100 }` to `ScrollToOptions`)
- Fixed `dataset` rejecting camel-cased names (e.g. `dataset.sideNavVisible` for `data-side-nav-visible`) with a syntax error
- Added `requestAnimationFrame`, `cancelAnimationFrame`, `queueMicrotask`, and `performance` (`now()`, `timeOrigin`) to the window

# 1.1.2

Released on Sunday, October 25 2026.
Expand Down
11 changes: 11 additions & 0 deletions src/AngleSharp.Js.Tests/DomTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ namespace AngleSharp.Js.Tests
[TestFixture]
public class DomTests
{
[TestCase("(function () { var e = document.createElement('div'); e.setAttribute('data-side-nav-visible', 'true'); return e.dataset.sideNavVisible; })()", "true")]
[TestCase("(function () { var e = document.createElement('div'); e.dataset.sideNavVisible = 'false'; return e.getAttribute('data-side-nav-visible'); })()", "false")]
[TestCase("(function () { var e = document.createElement('div'); e.dataset.level = '2'; return e.getAttribute('data-level'); })()", "2")]
[TestCase("(function () { var e = document.createElement('div'); e.setAttribute('data-foo-bar', 'x'); return String(e.dataset['foo-bar']); })()", "undefined")]
public async Task DatasetMapsCamelCasedNamesToAttributes(string source, string expected)
{
// dev.to sets document.body.dataset.sideNavVisible, which used to throw a syntax error
var result = await source.EvalScriptAsync();
Assert.AreEqual(expected, result);
}

[Test]
public async Task NodeHasChildNodesIsAFunction()
{
Expand Down
46 changes: 46 additions & 0 deletions src/AngleSharp.Js.Tests/EngineConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace AngleSharp.Js.Tests
using AngleSharp.Js.Tests.Mocks;
using AngleSharp.Scripting;
using Jint;
using Jint.Native;
using Jint.Runtime;
using NUnit.Framework;
using System;
Expand All @@ -14,6 +15,51 @@ namespace AngleSharp.Js.Tests

public class EngineConfigurationTests
{
[TestCase(false)]
[TestCase(true)]
public async Task RegexTimeoutAppliesToInlineAndExplicitScripts(Boolean inline)
{
var timeout = TimeSpan.FromMilliseconds(37);
var config = Configuration.Default
.With(new EngineCreator((window, options) => new Engine(options.RegexTimeoutInterval(timeout))))
.WithJs()
.WithEventLoop();

using (var context = BrowsingContext.New(config))
{
var document = await context.OpenAsync(request => request.Content(inline
? "<script>var configuredRegex = /inlineEngineTimeout/;</script>"
: "")).ConfigureAwait(false);
var expression = (JsRegExp)document.ExecuteScript(inline ? "configuredRegex" : "/explicitEngineTimeout/");

Assert.AreEqual(timeout, expression.Value.MatchTimeout);
}
}

[TestCase(23, 71)]
[TestCase(71, 23)]
public async Task CachedScriptsKeepEachEngineRegexTimeout(Int32 firstMilliseconds, Int32 secondMilliseconds)
{
var source = "/cachedEngineTimeout" + firstMilliseconds + "/";

foreach (var milliseconds in new[] { firstMilliseconds, secondMilliseconds, firstMilliseconds })
{
var timeout = TimeSpan.FromMilliseconds(milliseconds);
var config = Configuration.Default
.With(new EngineCreator((window, options) => new Engine(options.RegexTimeoutInterval(timeout))))
.WithJs()
.WithEventLoop();

using (var context = BrowsingContext.New(config))
{
var document = await context.OpenNewAsync().ConfigureAwait(false);
var expression = (JsRegExp)document.ExecuteScript(source);

Assert.AreEqual(timeout, expression.Value.MatchTimeout);
}
}
}

[Test]
public async Task ConstraintsAreInstalledBeforeInlineScriptsRun()
{
Expand Down
88 changes: 88 additions & 0 deletions src/AngleSharp.Js.Tests/OverloadTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
namespace AngleSharp.Js.Tests
{
using AngleSharp.Attributes;
using AngleSharp.Dom;
using NUnit.Framework;
using System;
using System.Globalization;
using System.Threading.Tasks;

[DomName("ProbeMode")]
public enum ProbeMode
{
[DomName("near")]
Near,
[DomName("far")]
Far,
}

[DomName("ProbeOptions")]
public sealed class ProbeOptions
{
[DomName("left")]
public Double? Left { get; set; }

[DomName("mode")]
public ProbeMode Mode { get; set; } = ProbeMode.Near;
}

[DomExposed("Element")]
public static class ProbeElementExtensions
{
[DomName("probe")]
public static String Probe(this IElement element) => "none";

[DomName("offsetHeight")]
[DomAccessor(Accessors.Getter)]
public static Int32 GetOffsetHeight(this IElement element) => 42;

[DomName("probe")]
public static String Probe(this IElement element, Double x, Double y) =>
String.Format(CultureInfo.InvariantCulture, "xy:{0},{1}", x, y);

[DomName("probe")]
public static String Probe(this IElement element, Boolean flag) => "flag:" + flag;

[DomName("probe")]
public static String Probe(this IElement element, ProbeOptions options) =>
String.Format(CultureInfo.InvariantCulture, "options:{0},{1}", options.Left?.ToString(CultureInfo.InvariantCulture) ?? "-", options.Mode);
}

/// <summary>
/// Registered so that this assembly counts as a library the engine exposes.
/// </summary>
sealed class ProbeService
{
}

/// <summary>
/// Several extension methods may share a name (scrollTo(x, y) and scrollTo(options)); the one
/// fitting the arguments is called, and a plain object becomes a dictionary.
/// </summary>
[TestFixture]
public class OverloadTests
{
[TestCase("probe()", "none")]
[TestCase("probe(1, 2)", "xy:1,2")]
[TestCase("probe(true)", "flag:True")]
[TestCase("probe({ left: 5, mode: 'far' })", "options:5,Far")]
[TestCase("probe({})", "options:-,Near")]
[TestCase("probe(undefined)", "none")]
public async Task OverloadFittingTheArgumentsIsCalled(String call, String expected)
{
var cfg = Configuration.Default.WithJs().With(new ProbeService());
var document = await BrowsingContext.New(cfg).OpenAsync(m => m.Content("<!doctype html><div id=d></div>"));

Assert.AreEqual(expected, document.ExecuteScript($"document.getElementById('d').{call}")?.ToString());
}

[Test]
public async Task MemberOfALibraryWinsOverTheStubOfThisOne()
{
var cfg = Configuration.Default.WithJs().With(new ProbeService());
var document = await BrowsingContext.New(cfg).OpenAsync(m => m.Content("<!doctype html><div id=d></div>"));

Assert.AreEqual("42", document.ExecuteScript("document.getElementById('d').offsetHeight")?.ToString());
}
}
}
138 changes: 138 additions & 0 deletions src/AngleSharp.Js.Tests/WindowApiTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
namespace AngleSharp.Js.Tests
{
using AngleSharp.Dom;
using AngleSharp.Js.Tests.Mocks;
using NUnit.Framework;
using System;
using System.Threading.Tasks;

/// <summary>
/// The members of the window that real pages rely on - Honeybadger, e.g., reads
/// console.info.__hb_original, so a single missing console method stopped dev.to's base
/// bundle, and every script relying on its globals failed afterwards.
/// </summary>
[TestFixture]
public class WindowApiTests
{
[TestCase("error")]
[TestCase("warn")]
[TestCase("info")]
[TestCase("debug")]
[TestCase("trace")]
[TestCase("dir")]
[TestCase("table")]
[TestCase("group")]
[TestCase("groupEnd")]
[TestCase("assert")]
[TestCase("count")]
[TestCase("time")]
[TestCase("timeEnd")]
public async Task ConsoleHasMethod(String name)
{
var result = await $"typeof console.{name}".EvalScriptAsync();
Assert.AreEqual("function", result);
}

[Test]
public async Task ConsoleMethodsWriteToTheLogger()
{
var console = new ConsoleLogger();
var cfg = Configuration.Default.WithJs().WithEventLoop().WithConsoleLogger(context => console);
var html = "<!doctype html><script>console.error('e'); console.warn('w'); console.assert(true, 'hidden'); console.assert(false, 'shown'); console.count(); console.count();</script>";
await BrowsingContext.New(cfg).OpenAsync(m => m.Content(html));
var output = console.Content.ToString();

StringAssert.Contains("e", output);
StringAssert.Contains("w", output);
StringAssert.Contains("shown", output);
StringAssert.DoesNotContain("hidden", output);
StringAssert.Contains("default: 2", output);
}

[TestCase("var declared = 1; String(window.declared)", "1")]
[TestCase("implicitGlobal = 2; String(window.implicitGlobal)", "2")]
[TestCase("function declaredFunction() {} typeof window.declaredFunction", "function")]
[TestCase("String(window.Promise === Promise)", "true")]
[TestCase("String(window.JSON === JSON)", "true")]
[TestCase("String('Math' in window)", "true")]
[TestCase("var item = 3; String(window.item)", "3")]
[TestCase("window.assigned = 4; delete window.assigned; typeof assigned", "undefined")]
public async Task WindowSeesTheGlobalsOfScripts(String script, String expected)
{
var context = BrowsingContext.New(Configuration.Default.WithJs());
var document = await context.OpenAsync(m => m.Content("<!doctype html><p>x</p>"));

Assert.AreEqual(expected, document.ExecuteScript(script)?.ToString());
}

[TestCase("document.querySelectorAll('p').item(1).id", "b")]
[TestCase("document.getElementsByTagName('p').item(0).id", "a")]
[TestCase("document.body.children.item(1).id", "b")]
[TestCase("document.body.children.namedItem('b').id", "b")]
[TestCase("document.querySelector('p').attributes.item(0).name", "id")]
[TestCase("typeof NodeList.prototype.item", "function")]
public async Task NamedIndexersAreMethods(String script, String expected)
{
var context = BrowsingContext.New(Configuration.Default.WithJs());
var document = await context.OpenAsync(m => m.Content("<!doctype html><body><p id=a></p><p id=b></p></body>"));

Assert.AreEqual(expected, document.ExecuteScript(script)?.ToString());
}

[Test]
public async Task ErrorOfAMemberKeepsItsMessage()
{
var result = await "(function () { try { document.querySelector('!'); } catch (e) { return e.message; } })()".EvalScriptAsync();
Assert.IsNotEmpty(result);
}

[Test]
public async Task PerformanceNowIsMonotonic()
{
var result = await "(function () { var a = performance.now(); var b = performance.now(); return (a >= 0 && b >= a && typeof performance.timeOrigin === 'number'); })()".EvalScriptAsync();
Assert.AreEqual("True", result);
}

[Test]
public async Task AnimationFrameCallbackReceivesTheWindowAndATime()
{
var context = BrowsingContext.New(Configuration.Default.WithJs().WithEventLoop());
var document = await context.OpenAsync(m => m.Content("<!doctype html><script>requestAnimationFrame(function (t) { document.title = (this === window) + ',' + typeof t; });</script>"));

for (var i = 0; i < 100 && document.Title.Length == 0; i++)
{
await Task.Delay(20);
}

Assert.AreEqual("true,number", document.Title);
}

[Test]
public async Task CancelledAnimationFrameDoesNotRun()
{
var context = BrowsingContext.New(Configuration.Default.WithJs().WithEventLoop());
var document = await context.OpenAsync(m => m.Content("<!doctype html><script>var h = requestAnimationFrame(function () { document.title = 'ran'; }); cancelAnimationFrame(h); setTimeout(function () { document.title += 'done'; }, 50);</script>"));

for (var i = 0; i < 100 && document.Title.Length == 0; i++)
{
await Task.Delay(20);
}

Assert.AreEqual("done", document.Title);
}

[Test]
public async Task QueuedMicrotaskRunsAfterTheScript()
{
var context = BrowsingContext.New(Configuration.Default.WithJs().WithEventLoop());
var document = await context.OpenAsync(m => m.Content("<!doctype html><script>document.title = 'a'; queueMicrotask(function () { document.title += 'c'; }); document.title += 'b';</script>"));

for (var i = 0; i < 100 && document.Title != "abc"; i++)
{
await Task.Delay(20);
}

Assert.AreEqual("abc", document.Title);
}
}
}
Loading
Loading