diff --git a/examples/DemoRunner/Builds/Android/app/CMakeLists.txt b/examples/DemoRunner/Builds/Android/app/CMakeLists.txt index 243f021309..c96abc47e3 100644 --- a/examples/DemoRunner/Builds/Android/app/CMakeLists.txt +++ b/examples/DemoRunner/Builds/Android/app/CMakeLists.txt @@ -2524,6 +2524,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_javascript/choc/LICENSE.md" "../../../../../modules/juce_javascript/javascript/juce_Javascript.cpp" "../../../../../modules/juce_javascript/javascript/juce_Javascript.h" + "../../../../../modules/juce_javascript/javascript/juce_Javascript_test.cpp" "../../../../../modules/juce_javascript/juce_javascript.cpp" "../../../../../modules/juce_javascript/juce_javascript.h" "../../../../../modules/juce_opengl/geometry/juce_Draggable3DOrientation.h" @@ -5113,6 +5114,7 @@ set_source_files_properties( "../../../../../modules/juce_javascript/choc/LICENSE.md" "../../../../../modules/juce_javascript/javascript/juce_Javascript.cpp" "../../../../../modules/juce_javascript/javascript/juce_Javascript.h" + "../../../../../modules/juce_javascript/javascript/juce_Javascript_test.cpp" "../../../../../modules/juce_javascript/juce_javascript.cpp" "../../../../../modules/juce_javascript/juce_javascript.h" "../../../../../modules/juce_opengl/geometry/juce_Draggable3DOrientation.h" diff --git a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj index a8f6d6358e..2b47e96c47 100644 --- a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj @@ -3095,6 +3095,9 @@ true + + true + true diff --git a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters index 9a53ea3a2e..e250cae838 100644 --- a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters +++ b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters @@ -3937,6 +3937,9 @@ JUCE Modules\juce_javascript\javascript + + JUCE Modules\juce_javascript\javascript + JUCE Modules\juce_javascript diff --git a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj index 387098a49b..2e430ce67a 100644 --- a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj @@ -3095,6 +3095,9 @@ true + + true + true diff --git a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters index 14c245cff5..ed754a8979 100644 --- a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters +++ b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters @@ -3937,6 +3937,9 @@ JUCE Modules\juce_javascript\javascript + + JUCE Modules\juce_javascript\javascript + JUCE Modules\juce_javascript diff --git a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj index e5ff46061a..f6ad6633a7 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj +++ b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj @@ -2930,6 +2930,9 @@ true + + true + true diff --git a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters index 19f2199863..c09875552a 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters +++ b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters @@ -3709,6 +3709,9 @@ JUCE Modules\juce_javascript\javascript + + JUCE Modules\juce_javascript\javascript + JUCE Modules\juce_javascript diff --git a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj index 79df62405e..720f962f13 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj +++ b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj @@ -2930,6 +2930,9 @@ true + + true + true diff --git a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters index 5e57e1c11e..53565aa419 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters +++ b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters @@ -3709,6 +3709,9 @@ JUCE Modules\juce_javascript\javascript + + JUCE Modules\juce_javascript\javascript + JUCE Modules\juce_javascript diff --git a/modules/juce_javascript/javascript/juce_Javascript.cpp b/modules/juce_javascript/javascript/juce_Javascript.cpp index 9e97a4d3ca..2c9bbfd388 100644 --- a/modules/juce_javascript/javascript/juce_Javascript.cpp +++ b/modules/juce_javascript/javascript/juce_Javascript.cpp @@ -1256,426 +1256,4 @@ std::optional JSCursor::getFullResolution() const return std::nullopt; } -//============================================================================== -#if JUCE_UNIT_TESTS - -static constexpr const char javascriptTestSource[] = R"x( -var testObject = new Object(); -testObject.value = 9; -testObject.add = function(a, b) - { - return a + b; - }; -var array = [1.1, 1.9, -1.25, -1.9]; -)x"; - -static constexpr const char accessNewObject[] = R"x( -var ref = newObject; -)x"; - -static constexpr const char createAccumulator[] = R"x( -class CommunicationsObject -{ - constructor() - { - this.value = 0; - } -} - -class DataAccumulator -{ - constructor() - { - this.commObject = new CommunicationsObject(); - this.sum = 0; - } - - getCommObject() - { - return this.commObject; - } - - accumulate() - { - this.sum += this.commObject.value; - this.commObject.value = 0; - return this.sum; - } -} - -var accumulator = new DataAccumulator(); -var commObject = accumulator.getCommObject(); -)x"; - -static constexpr const char replaceObjectAtCommHandleLocation[] = R"x( -var commObject = new CommunicationsObject(); -)x"; - - -class JavascriptTests : public UnitTest -{ -public: - JavascriptTests() : UnitTest ("Javascript", UnitTestCategories::gui) - { - } - - void runTest() override - { - JavascriptEngine engine; - engine.maximumExecutionTime = RelativeTime::seconds (5); - - beginTest ("Basic evaluations"); - { - auto result = Result::ok(); - - auto value = engine.evaluate ("[]", &result); - expect (result.wasOk() && value == var { Array{} }, "An empty array literal should evaluate correctly"); - } - - //============================================================================== - engine.evaluate (javascriptTestSource); - - beginTest ("JSCursor::invokeMethod"); - { - JSCursor root { engine.getRootObject() }; - const auto result = root["testObject"]["add"] (Span { std::initializer_list { 5, 2 } }); - expect (result.isDouble()); - expect (exactlyEqual ((double) result, 7.0)); - } - - beginTest ("JSCursor Array access"); - { - JSCursor root { engine.getRootObject() }; - expect (root["array"].isArray()); - expectEquals ((double) root["array"][2].get(), -1.25); - } - - beginTest ("JSObjectCursor references"); - { - auto rootObject = engine.getRootObject(); - rootObject["child"]["value"]; - - JSCursor root { rootObject }; - auto child = root["child"]; - auto value = child["value"]; - value.set (9); - - auto directReference = value; - directReference.set (10); - expectEquals ((double) value.get(), 10.0); - - auto indirectReference = child["value"]; - indirectReference.set (11); - expectEquals ((double) value.get(), 11.0); - - auto indirectReference2 = root["child"]["value"]; - indirectReference2.set (12); - expectEquals ((double) value.get(), 12.0); - } - - //============================================================================== - beginTest ("The object referenced by the cursor should be accessible from Javascript"); - { - auto rooObject = engine.getRootObject(); - auto newObject = rooObject["newObject"]; - - auto result = Result::ok(); - engine.evaluate (accessNewObject, &result); - expect (result.wasOk(), "Failed to access newObject: " + result.getErrorMessage()); - } - - beginTest ("The object referenced by the cursor shouldn't disappear/change"); - { - engine.execute (createAccumulator); - JSCursor rootCursor { engine.getRootObject() }; - auto commObjectCursor = rootCursor["commObject"]; - commObjectCursor["value"].set (5); - auto accumulatorCursor = rootCursor["accumulator"]; - - // The Accumulator and our cursor refer to the same object, through which they can - // communicate. - expectEquals ((int) accumulatorCursor["accumulate"]({}), 5); - - // A cursor contains an owning reference to the Object passed into its constructor. We - // can bind a cursor to the Object at the current location by reseating it. Without this - // step the test would fail. - commObjectCursor = JSCursor { commObjectCursor.getOrCreateObject() }; - - // This changes the object under the previous location. - engine.execute (replaceObjectAtCommHandleLocation); - commObjectCursor["value"].set (2); - - expectEquals ((int) accumulatorCursor["accumulate"]({}), 7, - "We aren't referring to the Accumulator's object anymore"); - } - - beginTest ("A JSCursor instance can be used to retrieve whatever value is at a given location"); - { - engine.execute ("var path = new Object();" - "path.to = new Object();" - "path.to.location = 5;"); - - auto cursor = JSCursor { engine.getRootObject() }["path"]["to"]["location"]; - - expectEquals ((int) cursor.get(), 5); - - engine.execute ("path.to = new Object();" - "path.to.location = 6;"); - - expectEquals ((int) cursor.get(), 6); - } - - beginTest ("Native functions returning objects with native functions work as expected"); - { - JavascriptEngine temporaryEngine; - - temporaryEngine.registerNativeObject ("ObjGetter", [&] - { - auto* objGetter = new DynamicObject(); - - objGetter->setMethod ("getObj", [&] (const auto&) - { - auto* obj = new DynamicObject(); - - obj->setMethod ("getVal", [] (const auto&) - { - return 42; - }); - - return obj; - }); - - return objGetter; - }()); - - auto res = juce::Result::fail (""); - const auto val = temporaryEngine.evaluate ("let objGetter = ObjGetter; let obj = objGetter.getObj(); obj.getVal();", &res); - expect (res.wasOk()); - expect (static_cast (val) == 42); - } - - beginTest ("Methods of javascript objects can be called from C++"); - { - JavascriptEngine temporaryEngine; - auto res = juce::Result::fail (""); - const auto val = temporaryEngine.evaluate ("var result = { bar: 5, foo (a) { return a + this.bar; } }; result;", &res); - expect (res.wasOk()); - - auto* obj = val.getDynamicObject(); - - if (obj == nullptr) - { - expect (false); - return; - } - - expect (obj->hasMethod ("foo")); - expect (obj->hasProperty ("bar")); - - expect (obj->getProperty ("bar") == var (5)); - - const var a[] { var { 10 } }; - const auto aResult = obj->invokeMethod ("foo", { val, std::data (a), (int) std::size (a) }); - expect (aResult == var (15)); - - temporaryEngine.evaluate ("result.bar = -5;", &res); - expect (res.wasOk()); - - const var b[] { var { -10 } }; - const auto bResult = obj->invokeMethod ("foo", { val, std::data (b), (int) std::size (b) }); - expect (bResult == var (-15)); - } - - beginTest ("Destructors of custom callables are called, eventually"); - { - struct CustomCallable - { - explicit CustomCallable (int& instances) - : liveInstances (instances) - { - ++liveInstances; - } - - CustomCallable (const CustomCallable& other) - : liveInstances (other.liveInstances) - { - ++liveInstances; - } - - CustomCallable (CustomCallable&& other) noexcept - : liveInstances (other.liveInstances) - { - ++liveInstances; - } - - ~CustomCallable() - { - --liveInstances; - } - - CustomCallable& operator= (const CustomCallable&) = delete; - CustomCallable& operator= (CustomCallable&&) noexcept = delete; - - var operator() (const var::NativeFunctionArgs&) const { return "hello world"; } - - int& liveInstances; - }; - - int methodInstances = 0; - - { - JavascriptEngine temporaryEngine; - - temporaryEngine.registerNativeObject ("ObjGetter", [&] - { - auto* objGetter = new DynamicObject(); - - objGetter->setMethod ("getObj", [&] (const auto&) - { - auto* obj = new DynamicObject; - obj->setMethod ("getVal", CustomCallable { methodInstances }); - return obj; - }); - - return objGetter; - }()); - - auto res = juce::Result::fail (""); - const auto value = temporaryEngine.evaluate ("ObjGetter.getObj().getVal();", &res); - expect (res.wasOk()); - expect (value == "hello world"); - } - - expect (methodInstances == 0); - } - - beginTest ("null and undefined return values are distinctly represented"); - { - JavascriptEngine temporaryEngine; - auto res = juce::Result::fail (""); - const auto val = temporaryEngine.evaluate ("var result = { returnsNull (a) { return null; }, returnsUndefined (a) { 5 + 2; } }; result;", &res); - expect (res.wasOk()); - - auto* obj = val.getDynamicObject(); - - if (obj == nullptr) - { - expect (false); - return; - } - - expect (obj->hasMethod ("returnsNull")); - const auto aResult = obj->invokeMethod ("returnsNull", { val, nullptr, 0 }); - expect (aResult.isVoid()); - - expect (obj->hasMethod ("returnsUndefined")); - const auto bResult = obj->invokeMethod ("returnsUndefined", { val, nullptr, 0 }); - expect (bResult.isUndefined()); - } - - beginTest ("calling a C function that returns void is converted correctly"); - { - int numCalls = 0; - - JavascriptEngine temporaryEngine; - - temporaryEngine.registerNativeObject ("Obj", [&] - { - auto* objGetter = new DynamicObject(); - - objGetter->setMethod ("getObj", [&] (const auto&) - { - auto* obj = new DynamicObject; - - obj->setMethod ("mutate", [&] (const auto&) - { - ++numCalls; - return var{}; - }); - - return obj; - }); - - return objGetter; - }()); - - auto res = juce::Result::fail (""); - const auto val = temporaryEngine.evaluate ("let foo = Obj.getObj(); foo.mutate(); foo.mutate();", &res); - expect (res.wasOk()); - - expect (numCalls == 2); - } - - beginTest ("Properties of registered native objects are enumerable"); - { - auto obj = rawToUniquePtr (new DynamicObject); - obj->setMethod ("methodA", nullptr); - obj->setProperty ("one", 1); - obj->setMethod ("methodB", nullptr); - obj->setProperty ("hello", "world"); - obj->setMethod ("methodC", nullptr); - obj->setProperty ("nested", - std::invoke ([] - { - auto result = rawToUniquePtr (new DynamicObject); - result->setProperty ("present", true); - return result.release(); - })); - - JavascriptEngine temporaryEngine; - temporaryEngine.registerNativeObject ("obj", obj.release()); - - auto res = juce::Result::fail (""); - const auto val = temporaryEngine.evaluate ("JSON.stringify (obj);", &res); - expect (res.wasOk()); - expectEquals (val.toString(), String (R"({"nested":{"present":true},"one":1,"hello":"world"})")); - } - - beginTest ("native objects survive being passed as arguments and return values"); - { - JavascriptEngine temporaryEngine; - - int numCalls = 0; - - auto objWithProps = rawToUniquePtr (new DynamicObject); - objWithProps->setProperty ("one", 1); - objWithProps->setProperty ("hello", "world"); - objWithProps->setMethod ("nativeFn", [&numCalls] (const auto&) - { - ++numCalls; - return "called a native fn"; - }); - - auto objWithFn = rawToUniquePtr (new DynamicObject); - - var passedToFn; - objWithFn->setMethod ("fn", [&passedToFn] (const auto& v) - { - passedToFn = v.arguments[0]; - return passedToFn; - }); - - temporaryEngine.registerNativeObject ("withProps", objWithProps.release()); - temporaryEngine.registerNativeObject ("withFn", objWithFn.release()); - - auto res = juce::Result::fail (""); - const auto val = temporaryEngine.evaluate ("withFn.fn (withProps);", &res); - expect (res.wasOk()); - - for (auto& v : { val, passedToFn }) - { - expect (v.getProperty ("one", 0) == var { 1 }); - expect (v.getProperty ("hello", "") == var { "world" }); - expect (v.call ("nativeFn") == var ("called a native fn")); - } - - expect (numCalls == 2); - } - } -}; - -static JavascriptTests javascriptTests; - -#endif - } // namespace juce diff --git a/modules/juce_javascript/javascript/juce_Javascript_test.cpp b/modules/juce_javascript/javascript/juce_Javascript_test.cpp new file mode 100644 index 0000000000..2a9e2cfcfe --- /dev/null +++ b/modules/juce_javascript/javascript/juce_Javascript_test.cpp @@ -0,0 +1,455 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +static constexpr const char javascriptTestSource[] = R"x( +var testObject = new Object(); +testObject.value = 9; +testObject.add = function(a, b) + { + return a + b; + }; +var array = [1.1, 1.9, -1.25, -1.9]; +)x"; + +static constexpr const char accessNewObject[] = R"x( +var ref = newObject; +)x"; + +static constexpr const char createAccumulator[] = R"x( +class CommunicationsObject +{ + constructor() + { + this.value = 0; + } +} + +class DataAccumulator +{ + constructor() + { + this.commObject = new CommunicationsObject(); + this.sum = 0; + } + + getCommObject() + { + return this.commObject; + } + + accumulate() + { + this.sum += this.commObject.value; + this.commObject.value = 0; + return this.sum; + } +} + +var accumulator = new DataAccumulator(); +var commObject = accumulator.getCommObject(); +)x"; + +static constexpr const char replaceObjectAtCommHandleLocation[] = R"x( +var commObject = new CommunicationsObject(); +)x"; + + +class JavascriptTests : public UnitTest +{ +public: + JavascriptTests() : UnitTest ("Javascript", UnitTestCategories::gui) + { + } + + void runTest() override + { + JavascriptEngine engine; + engine.maximumExecutionTime = RelativeTime::seconds (5); + + beginTest ("Basic evaluations"); + { + auto result = Result::ok(); + + auto value = engine.evaluate ("[]", &result); + expect (result.wasOk() && value == var { Array{} }, "An empty array literal should evaluate correctly"); + } + + //============================================================================== + engine.evaluate (javascriptTestSource); + + beginTest ("JSCursor::invokeMethod"); + { + JSCursor root { engine.getRootObject() }; + const auto result = root["testObject"]["add"] (Span { std::initializer_list { 5, 2 } }); + expect (result.isDouble()); + expect (exactlyEqual ((double) result, 7.0)); + } + + beginTest ("JSCursor Array access"); + { + JSCursor root { engine.getRootObject() }; + expect (root["array"].isArray()); + expectEquals ((double) root["array"][2].get(), -1.25); + } + + beginTest ("JSObjectCursor references"); + { + auto rootObject = engine.getRootObject(); + rootObject["child"]["value"]; + + JSCursor root { rootObject }; + auto child = root["child"]; + auto value = child["value"]; + value.set (9); + + auto directReference = value; + directReference.set (10); + expectEquals ((double) value.get(), 10.0); + + auto indirectReference = child["value"]; + indirectReference.set (11); + expectEquals ((double) value.get(), 11.0); + + auto indirectReference2 = root["child"]["value"]; + indirectReference2.set (12); + expectEquals ((double) value.get(), 12.0); + } + + //============================================================================== + beginTest ("The object referenced by the cursor should be accessible from Javascript"); + { + auto rooObject = engine.getRootObject(); + auto newObject = rooObject["newObject"]; + + auto result = Result::ok(); + engine.evaluate (accessNewObject, &result); + expect (result.wasOk(), "Failed to access newObject: " + result.getErrorMessage()); + } + + beginTest ("The object referenced by the cursor shouldn't disappear/change"); + { + engine.execute (createAccumulator); + JSCursor rootCursor { engine.getRootObject() }; + auto commObjectCursor = rootCursor["commObject"]; + commObjectCursor["value"].set (5); + auto accumulatorCursor = rootCursor["accumulator"]; + + // The Accumulator and our cursor refer to the same object, through which they can + // communicate. + expectEquals ((int) accumulatorCursor["accumulate"]({}), 5); + + // A cursor contains an owning reference to the Object passed into its constructor. We + // can bind a cursor to the Object at the current location by reseating it. Without this + // step the test would fail. + commObjectCursor = JSCursor { commObjectCursor.getOrCreateObject() }; + + // This changes the object under the previous location. + engine.execute (replaceObjectAtCommHandleLocation); + commObjectCursor["value"].set (2); + + expectEquals ((int) accumulatorCursor["accumulate"]({}), 7, + "We aren't referring to the Accumulator's object anymore"); + } + + beginTest ("A JSCursor instance can be used to retrieve whatever value is at a given location"); + { + engine.execute ("var path = new Object();" + "path.to = new Object();" + "path.to.location = 5;"); + + auto cursor = JSCursor { engine.getRootObject() }["path"]["to"]["location"]; + + expectEquals ((int) cursor.get(), 5); + + engine.execute ("path.to = new Object();" + "path.to.location = 6;"); + + expectEquals ((int) cursor.get(), 6); + } + + beginTest ("Native functions returning objects with native functions work as expected"); + { + JavascriptEngine temporaryEngine; + + temporaryEngine.registerNativeObject ("ObjGetter", [&] + { + auto* objGetter = new DynamicObject(); + + objGetter->setMethod ("getObj", [&] (const auto&) + { + auto* obj = new DynamicObject(); + + obj->setMethod ("getVal", [] (const auto&) + { + return 42; + }); + + return obj; + }); + + return objGetter; + }()); + + auto res = juce::Result::fail (""); + const auto val = temporaryEngine.evaluate ("let objGetter = ObjGetter; let obj = objGetter.getObj(); obj.getVal();", &res); + expect (res.wasOk()); + expect (static_cast (val) == 42); + } + + beginTest ("Methods of javascript objects can be called from C++"); + { + JavascriptEngine temporaryEngine; + auto res = juce::Result::fail (""); + const auto val = temporaryEngine.evaluate ("var result = { bar: 5, foo (a) { return a + this.bar; } }; result;", &res); + expect (res.wasOk()); + + auto* obj = val.getDynamicObject(); + + if (obj == nullptr) + { + expect (false); + return; + } + + expect (obj->hasMethod ("foo")); + expect (obj->hasProperty ("bar")); + + expect (obj->getProperty ("bar") == var (5)); + + const var a[] { var { 10 } }; + const auto aResult = obj->invokeMethod ("foo", { val, std::data (a), (int) std::size (a) }); + expect (aResult == var (15)); + + temporaryEngine.evaluate ("result.bar = -5;", &res); + expect (res.wasOk()); + + const var b[] { var { -10 } }; + const auto bResult = obj->invokeMethod ("foo", { val, std::data (b), (int) std::size (b) }); + expect (bResult == var (-15)); + } + + beginTest ("Destructors of custom callables are called, eventually"); + { + struct CustomCallable + { + explicit CustomCallable (int& instances) + : liveInstances (instances) + { + ++liveInstances; + } + + CustomCallable (const CustomCallable& other) + : liveInstances (other.liveInstances) + { + ++liveInstances; + } + + CustomCallable (CustomCallable&& other) noexcept + : liveInstances (other.liveInstances) + { + ++liveInstances; + } + + ~CustomCallable() + { + --liveInstances; + } + + CustomCallable& operator= (const CustomCallable&) = delete; + CustomCallable& operator= (CustomCallable&&) noexcept = delete; + + var operator() (const var::NativeFunctionArgs&) const { return "hello world"; } + + int& liveInstances; + }; + + int methodInstances = 0; + + { + JavascriptEngine temporaryEngine; + + temporaryEngine.registerNativeObject ("ObjGetter", [&] + { + auto* objGetter = new DynamicObject(); + + objGetter->setMethod ("getObj", [&] (const auto&) + { + auto* obj = new DynamicObject; + obj->setMethod ("getVal", CustomCallable { methodInstances }); + return obj; + }); + + return objGetter; + }()); + + auto res = juce::Result::fail (""); + const auto value = temporaryEngine.evaluate ("ObjGetter.getObj().getVal();", &res); + expect (res.wasOk()); + expect (value == "hello world"); + } + + expect (methodInstances == 0); + } + + beginTest ("null and undefined return values are distinctly represented"); + { + JavascriptEngine temporaryEngine; + auto res = juce::Result::fail (""); + const auto val = temporaryEngine.evaluate ("var result = { returnsNull (a) { return null; }, returnsUndefined (a) { 5 + 2; } }; result;", &res); + expect (res.wasOk()); + + auto* obj = val.getDynamicObject(); + + if (obj == nullptr) + { + expect (false); + return; + } + + expect (obj->hasMethod ("returnsNull")); + const auto aResult = obj->invokeMethod ("returnsNull", { val, nullptr, 0 }); + expect (aResult.isVoid()); + + expect (obj->hasMethod ("returnsUndefined")); + const auto bResult = obj->invokeMethod ("returnsUndefined", { val, nullptr, 0 }); + expect (bResult.isUndefined()); + } + + beginTest ("calling a C function that returns void is converted correctly"); + { + int numCalls = 0; + + JavascriptEngine temporaryEngine; + + temporaryEngine.registerNativeObject ("Obj", [&] + { + auto* objGetter = new DynamicObject(); + + objGetter->setMethod ("getObj", [&] (const auto&) + { + auto* obj = new DynamicObject; + + obj->setMethod ("mutate", [&] (const auto&) + { + ++numCalls; + return var{}; + }); + + return obj; + }); + + return objGetter; + }()); + + auto res = juce::Result::fail (""); + const auto val = temporaryEngine.evaluate ("let foo = Obj.getObj(); foo.mutate(); foo.mutate();", &res); + expect (res.wasOk()); + + expect (numCalls == 2); + } + + beginTest ("Properties of registered native objects are enumerable"); + { + auto obj = rawToUniquePtr (new DynamicObject); + obj->setMethod ("methodA", nullptr); + obj->setProperty ("one", 1); + obj->setMethod ("methodB", nullptr); + obj->setProperty ("hello", "world"); + obj->setMethod ("methodC", nullptr); + obj->setProperty ("nested", + std::invoke ([] + { + auto result = rawToUniquePtr (new DynamicObject); + result->setProperty ("present", true); + return result.release(); + })); + + JavascriptEngine temporaryEngine; + temporaryEngine.registerNativeObject ("obj", obj.release()); + + auto res = juce::Result::fail (""); + const auto val = temporaryEngine.evaluate ("JSON.stringify (obj);", &res); + expect (res.wasOk()); + expectEquals (val.toString(), String (R"({"nested":{"present":true},"one":1,"hello":"world"})")); + } + + beginTest ("native objects survive being passed as arguments and return values"); + { + JavascriptEngine temporaryEngine; + + int numCalls = 0; + + auto objWithProps = rawToUniquePtr (new DynamicObject); + objWithProps->setProperty ("one", 1); + objWithProps->setProperty ("hello", "world"); + objWithProps->setMethod ("nativeFn", [&numCalls] (const auto&) + { + ++numCalls; + return "called a native fn"; + }); + + auto objWithFn = rawToUniquePtr (new DynamicObject); + + var passedToFn; + objWithFn->setMethod ("fn", [&passedToFn] (const auto& v) + { + passedToFn = v.arguments[0]; + return passedToFn; + }); + + temporaryEngine.registerNativeObject ("withProps", objWithProps.release()); + temporaryEngine.registerNativeObject ("withFn", objWithFn.release()); + + auto res = juce::Result::fail (""); + const auto val = temporaryEngine.evaluate ("withFn.fn (withProps);", &res); + expect (res.wasOk()); + + for (auto& v : { val, passedToFn }) + { + expect (v.getProperty ("one", 0) == var { 1 }); + expect (v.getProperty ("hello", "") == var { "world" }); + expect (v.call ("nativeFn") == var ("called a native fn")); + } + + expect (numCalls == 2); + } + } +}; + +static JavascriptTests javascriptTests; + +} // namespace juce diff --git a/modules/juce_javascript/juce_javascript.cpp b/modules/juce_javascript/juce_javascript.cpp index 47c1754cbc..7a22d8f33c 100644 --- a/modules/juce_javascript/juce_javascript.cpp +++ b/modules/juce_javascript/juce_javascript.cpp @@ -48,3 +48,7 @@ #undef choc #include "javascript/juce_Javascript.cpp" + +#if JUCE_UNIT_TESTS + #include "javascript/juce_Javascript_test.cpp" +#endif