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