diff --git a/CHANGELOG.md b/CHANGELOG.md
index 566cfe1..d92bb14 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
Released on ?.
- Fixed `StackOverflowException` from deeply recursive scripts (#75) @lahma
+- Fixed invoking JavaScript functions with DOM objects created in C#, which reached script as a plain CLR wrapper instead of as the DOM object (#76)
- Fixed usage of document ready state (#87) @Sebbs128
- Fixed `HasChildNodes` is now exposed as a method to DOM (#106) @arekdygas
- Fixed missing `parent` on Window objects (#68)
diff --git a/src/AngleSharp.Js.Tests/InteractionTests.cs b/src/AngleSharp.Js.Tests/InteractionTests.cs
index 178524a..7e0ea07 100644
--- a/src/AngleSharp.Js.Tests/InteractionTests.cs
+++ b/src/AngleSharp.Js.Tests/InteractionTests.cs
@@ -2,8 +2,10 @@ namespace AngleSharp.Js.Tests
{
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
+ using AngleSharp.Html.Parser;
using AngleSharp.Scripting;
using Jint;
+ using Jint.Native;
using Jint.Runtime;
using NUnit.Framework;
using System;
@@ -38,6 +40,59 @@ public async Task RunJavaScriptFunctionFromCSharp()
Assert.AreEqual(16.0, result.AsNumber());
}
+ [Test]
+ public async Task InvokeFunctionWithCSharpCreatedElement()
+ {
+ var service = new JsScriptingService();
+ var cfg = Configuration.Default.With(service);
+ var html = "";
+ var document = await BrowsingContext.New(cfg).OpenAsync(m => m.Content(html));
+ var engine = service.GetOrCreateJint(document);
+ var section = document.CreateElement("section");
+ var f1 = engine.GetValue("f1");
+ var jsSection = JsValue.FromObject(engine, section);
+
+ engine.Invoke(f1, jsSection);
+
+ Assert.AreSame(section, document.Body.LastElementChild);
+ // One node, one JS object: were a hand-over to mint a fresh proxy, the expandos and
+ // the event handlers script attached through the previous one would be lost.
+ Assert.AreSame(jsSection, JsValue.FromObject(engine, section));
+ Assert.AreSame(jsSection, engine.Evaluate("document.body.lastElementChild"));
+ }
+
+ [Test]
+ public async Task InvokeFunctionWithCSharpCreatedElementUsesDomMembers()
+ {
+ var service = new JsScriptingService();
+ var cfg = Configuration.Default.With(service);
+ var html = "";
+ var document = await BrowsingContext.New(cfg).OpenAsync(m => m.Content(html));
+ var engine = service.GetOrCreateJint(document);
+ var section = document.CreateElement("section");
+
+ engine.Invoke(engine.GetValue("f1"), section);
+
+ Assert.AreEqual("SECTION", section.TextContent);
+ }
+
+ [Test]
+ public async Task ExternalNonDomObjectKeepsItsClrMembers()
+ {
+ // A parser is an EventTarget, but it is not part of the DOM - a host object reaches
+ // script the way every other host object does, through its CLR members.
+ var service = new JsScriptingService();
+ service.External.Add("parser", new HtmlParser());
+ var cfg = Configuration.Default.With(service);
+ var html = "";
+ var document = await BrowsingContext.New(cfg).OpenAsync(m => m.Content(html));
+ var engine = service.GetOrCreateJint(document);
+
+ var result = engine.Evaluate("typeof parser.ParseDocument");
+
+ Assert.AreEqual("function", result.AsString());
+ }
+
[Test]
public async Task RunCSharpFunctionFromJavaScript()
{
diff --git a/src/AngleSharp.Js/Cache/PrototypeTypeCache.cs b/src/AngleSharp.Js/Cache/PrototypeTypeCache.cs
index 29e9f16..de69cd3 100644
--- a/src/AngleSharp.Js/Cache/PrototypeTypeCache.cs
+++ b/src/AngleSharp.Js/Cache/PrototypeTypeCache.cs
@@ -22,6 +22,30 @@ static class PrototypeTypeCache
{
private static readonly ConcurrentDictionary> _definingTypes = new();
private static readonly ConcurrentDictionary> _exposedTypes = new();
+ private static readonly ConcurrentDictionary _domTypes = new();
+
+ // AngleSharp hangs its non-DOM infrastructure - the parsers, the browsing context, the
+ // requesters - off the very same EventTarget class the DOM is built on, so that name
+ // alone says nothing about being part of the DOM. Everything the DOM does expose
+ // carries a more specific name of its own.
+ private const String EventTargetName = "EventTarget";
+
+ ///
+ /// Gets whether instances of the type are represented by a DOM prototype rather than by
+ /// Jint's ordinary CLR wrapper.
+ ///
+ ///
+ /// The name may well be inherited - a "b" element answers true through HTMLElement -
+ /// which is exactly what makes the DOM view the right one for it. Unlike
+ /// the answer depends on the type alone and not on the
+ /// engine's set of libraries, so the cache is process-wide.
+ ///
+ public static Boolean IsDomType(this Type type) =>
+ _domTypes.GetOrAdd(type, static current =>
+ {
+ var name = GetCanonicalName(current);
+ return name != null && !String.Equals(name, EventTargetName, StringComparison.Ordinal);
+ });
///
/// Gets what the constructor object of the type a prototype belongs to is built from,
diff --git a/src/AngleSharp.Js/EngineInstance.cs b/src/AngleSharp.Js/EngineInstance.cs
index 7edb1a1..de09f1f 100644
--- a/src/AngleSharp.Js/EngineInstance.cs
+++ b/src/AngleSharp.Js/EngineInstance.cs
@@ -8,6 +8,7 @@ namespace AngleSharp.Js
using Jint.Native;
using Jint.Native.Json;
using Jint.Native.Object;
+ using Jint.Runtime.Interop;
using System;
using System.Collections.Generic;
using System.Reflection;
@@ -37,6 +38,11 @@ public EngineInstance(IWindow window, IDictionary assignments, I
_engine = new Engine((o) =>
{
o.EnableModules(new JsModuleLoader(this, window.Document, false));
+ // The handler answers out of the caches assigned right below, which only exist
+ // once this constructor returns. Jint wraps nothing while it is configuring
+ // itself, so that is safe - and Engine.Options is internal, so registering the
+ // handler afterwards is not an option.
+ o.SetWrapObjectHandler(WrapObject);
// Left alone, the JS call stack is the native one, and a script recursing
// deeper than it holds takes the whole process down - a StackOverflowException
// cannot be caught. Guarded, the engine continues on a fresh stack and finally
@@ -213,6 +219,19 @@ private JsValue ImportModule(String specifier, String source)
private ObjectInstance CreatePrototype(Type type) => new DomPrototypeInstance(this, type);
+ ///
+ /// Converts a value handed over from C#, which reaches Jint through JsValue.FromObject
+ /// rather than through .
+ ///
+ ///
+ /// A DOM object has to arrive as the DOM proxy for the very same reason it does when the
+ /// DOM itself yields one: the proxy is what carries the DOM members, and it is what makes
+ /// the object script already holds and the one passed in from C# the same object. Anything
+ /// else stays with Jint's CLR wrapper, which is what a host object is expected to be.
+ ///
+ private ObjectInstance WrapObject(Engine engine, Object target, Type type) =>
+ target.GetType().IsDomType() ? GetDomNode(target) : ObjectWrapper.Create(engine, target, type);
+
#endregion
}
}