diff --git a/remote/webdriver-bidi/RemoteValue.jsm b/remote/webdriver-bidi/RemoteValue.jsm index a966882d099ef..dc8350686465b 100644 --- a/remote/webdriver-bidi/RemoteValue.jsm +++ b/remote/webdriver-bidi/RemoteValue.jsm @@ -4,7 +4,7 @@ "use strict"; -var EXPORTED_SYMBOLS = ["deserialize", "serialize"]; +var EXPORTED_SYMBOLS = ["deserialize", "serialize", "stringify"]; const { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" @@ -418,8 +418,32 @@ function serialize( } lazy.logger.warn( - `Unsupported type: ${type} for remote value: ${value.toString()}` + `Unsupported type: ${type} for remote value: ${stringify(value)}` ); return undefined; } + +/** + * Safely stringify a value. + * + * @param {Object} value + * Value of any type to be stringified. + * + * @returns {String} String representation of the value. + */ +function stringify(obj) { + let text; + try { + text = + obj !== null && typeof obj === "object" ? obj.toString() : String(obj); + } catch (e) { + // The error-case will also be handled in `finally {}`. + } finally { + if (typeof text != "string") { + text = Object.prototype.toString.apply(obj); + } + } + + return text; +} diff --git a/remote/webdriver-bidi/modules/windowglobal/script.jsm b/remote/webdriver-bidi/modules/windowglobal/script.jsm index 2b15682fec543..b3e1b57fb0594 100644 --- a/remote/webdriver-bidi/modules/windowglobal/script.jsm +++ b/remote/webdriver-bidi/modules/windowglobal/script.jsm @@ -23,6 +23,7 @@ XPCOMUtils.defineLazyModuleGetters(lazy, { getFramesFromStack: "chrome://remote/content/shared/Stack.jsm", isChromeFrame: "chrome://remote/content/shared/Stack.jsm", serialize: "chrome://remote/content/webdriver-bidi/RemoteValue.jsm", + stringify: "chrome://remote/content/webdriver-bidi/RemoteValue.jsm", }); XPCOMUtils.defineLazyGetter(lazy, "dbg", () => { @@ -88,10 +89,7 @@ class ScriptModule extends Module { exception: lazy.serialize(exception, 1), lineNumber: stack.line - 1, stackTrace: { callFrames }, - text: - typeof exception === "object" - ? exception.toString() - : String(exception), + text: lazy.stringify(exception), }; } diff --git a/remote/webdriver-bidi/test/xpcshell/test_RemoteValue.js b/remote/webdriver-bidi/test/xpcshell/test_RemoteValue.js index b7775d8940d8f..c607636a04128 100644 --- a/remote/webdriver-bidi/test/xpcshell/test_RemoteValue.js +++ b/remote/webdriver-bidi/test/xpcshell/test_RemoteValue.js @@ -223,7 +223,7 @@ const REMOTE_COMPLEX_VALUES = [ }, ]; -const { deserialize, serialize } = ChromeUtils.import( +const { deserialize, serialize, stringify } = ChromeUtils.import( "chrome://remote/content/webdriver-bidi/RemoteValue.jsm" ); @@ -557,3 +557,41 @@ add_test(function test_serializeRemoteComplexValues() { run_next_test(); }); + +add_test(function test_stringify() { + const STRINGIFY_TEST_CASES = [ + [undefined, "undefined"], + [null, "null"], + ["foobar", "foobar"], + ["2", "2"], + [-0, "0"], + [Infinity, "Infinity"], + [-Infinity, "-Infinity"], + [3, "3"], + [1.4, "1.4"], + [true, "true"], + [42n, "42"], + [{ toString: () => "bar" }, "bar", "toString: () => 'bar'"], + [{ toString: () => 4 }, "[object Object]", "toString: () => 4"], + [{ toString: undefined }, "[object Object]", "toString: undefined"], + [{ toString: null }, "[object Object]", "toString: null"], + [ + { + toString: () => { + throw new Error("toString error"); + }, + }, + "[object Object]", + "toString: () => { throw new Error('toString error'); }", + ], + ]; + + for (const [value, expectedString, description] of STRINGIFY_TEST_CASES) { + info(`Checking '${description || value}'`); + const stringifiedValue = stringify(value); + + Assert.strictEqual(expectedString, stringifiedValue, "Got expected string"); + } + + run_next_test(); +}); diff --git a/testing/web-platform/mozilla/tests/webdriver/bidi/script/exception_details.py b/testing/web-platform/mozilla/tests/webdriver/bidi/script/exception_details.py new file mode 100644 index 0000000000000..af1cf48290b11 --- /dev/null +++ b/testing/web-platform/mozilla/tests/webdriver/bidi/script/exception_details.py @@ -0,0 +1,70 @@ +import pytest + +from webdriver.bidi.modules.script import ContextTarget, ScriptEvaluateResultException + + +@pytest.mark.asyncio +@pytest.mark.parametrize("await_promise", [True, False]) +@pytest.mark.parametrize( + "expression", + [ + "null", + "{ toString: 'not a function' }", + "{ toString: () => {{ throw 'toString not allowed'; }} }", + "{ toString: () => true }", + ], +) +@pytest.mark.asyncio +async def test_call_function_without_to_string_interface( + bidi_session, top_context, await_promise, expression +): + function_declaration = "()=>{throw { toString: 'not a function' } }" + if await_promise: + function_declaration = "async" + function_declaration + + with pytest.raises(ScriptEvaluateResultException) as exception: + await bidi_session.script.call_function( + function_declaration=function_declaration, + await_promise=await_promise, + target=ContextTarget(top_context["context"]), + ) + + assert "exceptionDetails" in exception.value.result + exceptionDetails = exception.value.result["exceptionDetails"] + + assert "text" in exceptionDetails + assert isinstance(exceptionDetails["text"], str) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("await_promise", [True, False]) +@pytest.mark.parametrize( + "expression", + [ + "null", + "{ toString: 'not a function' }", + "{ toString: () => {{ throw 'toString not allowed'; }} }", + "{ toString: () => true }", + ], +) +@pytest.mark.asyncio +async def test_evaluate_without_to_string_interface( + bidi_session, top_context, await_promise, expression +): + if await_promise: + expression = f"Promise.reject({expression})" + else: + expression = f"throw {expression}" + + with pytest.raises(ScriptEvaluateResultException) as exception: + await bidi_session.script.evaluate( + expression=expression, + await_promise=await_promise, + target=ContextTarget(top_context["context"]), + ) + + assert "exceptionDetails" in exception.value.result + exceptionDetails = exception.value.result["exceptionDetails"] + + assert "text" in exceptionDetails + assert isinstance(exceptionDetails["text"], str) diff --git a/testing/web-platform/tests/webdriver/tests/bidi/conftest.py b/testing/web-platform/tests/webdriver/tests/bidi/conftest.py index 459ea0d958941..cba694b4b881c 100644 --- a/testing/web-platform/tests/webdriver/tests/bidi/conftest.py +++ b/testing/web-platform/tests/webdriver/tests/bidi/conftest.py @@ -34,9 +34,3 @@ def test_page_cross_origin_frame(inline, test_page_cross_origin): @pytest.fixture def test_page_same_origin_frame(inline, test_page): return inline(f"") - - -@pytest.fixture -async def top_context(bidi_session): - contexts = await bidi_session.browsing_context.get_tree() - return contexts[0] diff --git a/testing/web-platform/tests/webdriver/tests/support/fixtures.py b/testing/web-platform/tests/webdriver/tests/support/fixtures.py index 30f93d4cea0fb..e63c0d01d269d 100644 --- a/testing/web-platform/tests/webdriver/tests/support/fixtures.py +++ b/testing/web-platform/tests/webdriver/tests/support/fixtures.py @@ -248,3 +248,9 @@ def iframe(src, **kwargs): return "".format(inline(src, **kwargs)) return iframe + + +@pytest.fixture +async def top_context(bidi_session): + contexts = await bidi_session.browsing_context.get_tree() + return contexts[0]