diff --git a/bindings/vapi/javascriptcore-4.0.vapi b/bindings/vapi/javascriptcore-4.0.vapi index f17c5d19..7601d1cd 100644 --- a/bindings/vapi/javascriptcore-4.0.vapi +++ b/bindings/vapi/javascriptcore-4.0.vapi @@ -3,9 +3,9 @@ [CCode (cprefix = "JS", gir_namespace = "JavaScriptCore", gir_version = "4.0", lower_case_cprefix = "JS_", cheader_filename = "JavaScriptCore/JavaScript.h")] namespace JS { - [CCode (cname = "JSGlobalContextRef")] + [CCode (cname = "JSContextRef")] [SimpleType] - public struct GlobalContext : Context { + public struct Context { [CCode (cname = "JSValueIsBoolean")] public bool is_boolean(JS.Value value); @@ -13,23 +13,21 @@ namespace JS { [CCode (cname = "JSValueIsNumber")] public bool is_number(JS.Value value); + [CCode (cname = "JSValueIsObject")] + public bool is_object(JS.Value value); + [CCode (cname = "JSValueToBoolean")] public bool to_boolean(JS.Value value); [CCode (cname = "JSValueToNumber")] public double to_number(JS.Value value, out JS.Value exception); + [CCode (cname = "JSValueToObject")] + public Object to_object(JS.Value value, out JS.Value exception); + [CCode (cname = "JSValueToStringCopy")] public String to_string_copy(JS.Value value, out JS.Value exception); - [CCode (cname = "JSGlobalContextRelease")] - public bool release(); - } - - [CCode (cname = "JSContextRef")] - [SimpleType] - public struct Context { - [CCode (cname = "JSEvaluateScript")] public Value evaluate_script(String script, Object? thisObject, @@ -55,14 +53,35 @@ namespace JS { } + [CCode (cname = "JSGlobalContextRef")] + [SimpleType] + public struct GlobalContext : Context { + + [CCode (cname = "JSGlobalContextRelease")] + public bool release(); + } + [CCode (cname = "JSObjectRef")] [SimpleType] public struct Object { + + [CCode (cname = "JSObjectHasProperty", instance_pos = 1.1)] + public bool has_property(Context ctx, String property_name); + + [CCode (cname = "JSObjectGetProperty", instance_pos = 1.1)] + public String get_property(Context ctx, + String property_name, + out Value? exception); + } [CCode (cname = "JSValueRef")] [SimpleType] public struct Value { + + [CCode (cname = "JSValueGetType", instance_pos = 1.1)] + public JS.Type get_type(JS.Context context); + } [CCode (cname = "JSStringRef", ref_function = "JSStringRetain", unref_function = "JSStringRelease")] @@ -88,4 +107,27 @@ namespace JS { public void String.release(); } + + [CCode (cname = "JSType", has_type_id = false)] + public enum Type { + + [CCode (cname = "kJSTypeUndefined")] + UNDEFINED, + + [CCode (cname = "kJSTypeNull")] + NULL, + + [CCode (cname = "kJSTypeBoolean")] + BOOLEAN, + + [CCode (cname = "kJSTypeNumber")] + NUMBER, + + [CCode (cname = "kJSTypeString")] + STRING, + + [CCode (cname = "kJSTypeObject")] + OBJECT + } + } diff --git a/src/client/components/client-web-view.vala b/src/client/components/client-web-view.vala index a2069643..f886cc7f 100644 --- a/src/client/components/client-web-view.vala +++ b/src/client/components/client-web-view.vala @@ -6,7 +6,7 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -protected errordomain JSError { TYPE } +protected errordomain JSError { EXCEPTION, TYPE } public class ClientWebView : WebKit.WebView { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 32145782..6e2e7a2e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -13,6 +13,8 @@ set(TEST_SRC engine/util-html-test.vala client/application/geary-configuration-test.vala + + js/composer-page-state-test.vala ) # Vala diff --git a/test/js/composer-page-state-test.vala b/test/js/composer-page-state-test.vala new file mode 100644 index 00000000..1b8a41c4 --- /dev/null +++ b/test/js/composer-page-state-test.vala @@ -0,0 +1,221 @@ +/* + * Copyright 2016 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +// Defined by CMake build script. +extern const string _BUILD_ROOT_DIR; + +class ComposerPageStateTest : Gee.TestCase { + + private ComposerWebView test_view = null; + private AsyncQueue async_results = new AsyncQueue(); + + public ComposerPageStateTest() { + base("ComposerPageStateTest"); + add_test("get_html", get_html); + add_test("get_text", get_text); + add_test("get_text_with_quote", get_text_with_quote); + add_test("get_text_with_nested_quote", get_text_with_nested_quote); + add_test("resolve_nesting", resolve_nesting); + add_test("quote_lines", quote_lines); + } + + public override void set_up() { + ClientWebView.init_web_context(File.new_for_path(_BUILD_ROOT_DIR).get_child("src"), true); + try { + ClientWebView.load_scripts(); + ComposerWebView.load_resources(); + } catch (Error err) { + print("\nComposerPageStateTest::set_up: %s\n", err.message); + assert_not_reached(); + } + Configuration config = new Configuration(GearyApplication.APP_ID); + this.test_view = new ComposerWebView(config); + } + + public void get_html() { + string html = "

para

"; + load_body_fixture(html); + try { + assert(run_javascript(@"window.geary.getHtml();") == html + "

"); + } catch (JSError err) { + print("JSError: %s", err.message); + assert_not_reached(); + } catch (Error err) { + print("WKError: %s", err.message); + assert_not_reached(); + } + } + + public void get_text() { + load_body_fixture("

para

"); + try { + assert(run_javascript(@"window.geary.getText();") == "para\n\n\n\n\n"); + } catch (JSError err) { + print("JSError: %s", err.message); + assert_not_reached(); + } catch (Error err) { + print("WKError: %s", err.message); + assert_not_reached(); + } + } + + public void get_text_with_quote() { + load_body_fixture("

pre

quote

post

"); + try { + assert(run_javascript(@"window.geary.getText();") == + "pre\n\n> quote\n> \npost\n\n\n\n\n"); + } catch (JSError err) { + print("JSError: %s", err.message); + assert_not_reached(); + } catch (Error err) { + print("WKError: %s", err.message); + assert_not_reached(); + } + } + + public void get_text_with_nested_quote() { + load_body_fixture("

pre

quote1

quote2

post

"); + try { + assert(run_javascript(@"window.geary.getText();") == + "pre\n\n> quote1\n> \n>> quote2\n>> \npost\n\n\n\n\n"); + } catch (JSError err) { + print("JSError: %s", err.message); + assert_not_reached(); + } catch (Error err) { + print("WKError: %s", err.message); + assert_not_reached(); + } + } + + public void resolve_nesting() { + load_body_fixture(); + unichar q_marker = Geary.RFC822.Utils.QUOTE_MARKER; + unichar q_start = '‘'; + unichar q_end = '’'; + string js_no_quote = "foo"; + string js_spaced_quote = @"foo $(q_start)0$(q_end) bar"; + string js_leading_quote = @"$(q_start)0$(q_end) bar"; + string js_hanging_quote = @"foo $(q_start)0$(q_end)"; + string js_cosy_quote1 = @"foo$(q_start)0$(q_end)bar"; + string js_cosy_quote2 = @"foo$(q_start)0$(q_end)$(q_start)1$(q_end)bar"; + string js_values = "['quote1','quote2']"; + try { + assert(run_javascript(@"ComposerPageState.resolveNesting('$(js_no_quote)', $(js_values));") == + @"foo"); + assert(run_javascript(@"ComposerPageState.resolveNesting('$(js_spaced_quote)', $(js_values));") == + @"foo \n$(q_marker)quote1\n bar"); + assert(run_javascript(@"ComposerPageState.resolveNesting('$(js_leading_quote)', $(js_values));") == + @"$(q_marker)quote1\n bar"); + assert(run_javascript(@"ComposerPageState.resolveNesting('$(js_hanging_quote)', $(js_values));") == + @"foo \n$(q_marker)quote1"); + assert(run_javascript(@"ComposerPageState.resolveNesting('$(js_cosy_quote1)', $(js_values));") == + @"foo\n$(q_marker)quote1\nbar"); + assert(run_javascript(@"ComposerPageState.resolveNesting('$(js_cosy_quote2)', $(js_values));") == + @"foo\n$(q_marker)quote1\n$(q_marker)quote2\nbar"); + } catch (JSError err) { + print("JSError: %s", err.message); + assert_not_reached(); + } catch (Error err) { + print("WKError: %s", err.message); + assert_not_reached(); + } + } + + public void quote_lines() { + load_body_fixture(); + unichar q_marker = Geary.RFC822.Utils.QUOTE_MARKER; + try { + assert(run_javascript("ComposerPageState.quoteLines('');") == + @"$(q_marker)"); + assert(run_javascript("ComposerPageState.quoteLines('line1');") == + @"$(q_marker)line1"); + assert(run_javascript("ComposerPageState.quoteLines('line1\\nline2');") == + @"$(q_marker)line1\n$(q_marker)line2"); + } catch (JSError err) { + print("JSError: %s", err.message); + assert_not_reached(); + } catch (Error err) { + print("WKError: %s", err.message); + assert_not_reached(); + } + } + + protected void load_body_fixture(string? html = null) { + this.test_view.load_html(html, null, false); + while (this.test_view.is_loading) { + Gtk.main_iteration(); + } + } + + protected string run_javascript(string command) throws Error { + this.test_view.run_javascript.begin( + command, null, (obj, res) => { async_complete(res); } + ); + + WebKit.JavascriptResult result = + this.test_view.run_javascript.end(async_result()); + return get_string_result(result); + } + + protected void async_complete(AsyncResult result) { + this.async_results.push(result); + } + + protected AsyncResult async_result() { + AsyncResult? result = null; + while (result == null) { + Gtk.main_iteration(); + result = this.async_results.try_pop(); + } + return result; + } + + protected static string? get_string_result(WebKit.JavascriptResult result) + throws JSError { + JS.GlobalContext context = result.get_global_context(); + JS.Value js_str_value = result.get_value(); + JS.Value? err = null; + JS.String js_str = context.to_string_copy(js_str_value, out err); + + check_exception(context, err); + return to_string_released(js_str); + } + + protected static inline void check_exception(JS.Context exe, JS.Value? err_value) + throws JSError { + if (!is_null(exe, err_value)) { + JS.Value? nested_err = null; + JS.Type err_type = err_value.get_type(exe); + JS.String err_str = exe.to_string_copy(err_value, out nested_err); + + if (!is_null(exe, nested_err)) { + throw new JSError.EXCEPTION( + "Nested exception getting exception %s as a string", + err_type.to_string() + ); + } + + throw new JSError.EXCEPTION( + "JS exception thrown [%s]: %s" + .printf(err_type.to_string(), to_string_released(err_str)) + ); + } + } + + protected static inline bool is_null(JS.Context exe, JS.Value? js) { + return (js == null || js.get_type(exe) == JS.Type.NULL); + } + + protected static string to_string_released(JS.String js) { + int len = js.get_maximum_utf8_cstring_size(); + string str = string.nfill(len, 0); + js.get_utf8_cstring(str, len); + js.release(); + return str; + } + +} diff --git a/test/main.vala b/test/main.vala index be55e6ab..5b733be0 100644 --- a/test/main.vala +++ b/test/main.vala @@ -29,6 +29,7 @@ int main(string[] args) { Geary.RFC822.init(); Geary.HTML.init(); + Geary.Logging.init(); /* * Hook up all tests into appropriate suites @@ -46,12 +47,17 @@ int main(string[] args) { client.add_suite(new ConfigurationTest().get_suite()); + TestSuite js = new TestSuite("js"); + + js.add_suite(new ComposerPageStateTest().get_suite()); + /* * Run the tests */ TestSuite root = TestSuite.get_root(); root.add_suite(engine); root.add_suite(client); + root.add_suite(js); int ret = -1; Idle.add(() => { diff --git a/ui/composer-web-view.js b/ui/composer-web-view.js index 8fa0dee9..5e87dc36 100644 --- a/ui/composer-web-view.js +++ b/ui/composer-web-view.js @@ -197,7 +197,7 @@ ComposerPageState.resolveNesting = function(text, values) { ComposerPageState.QUOTE_START + "([0-9]*)" + ComposerPageState.QUOTE_END + - "(?=(.?))" + "(?=(.?))", "g" ); return text.replace(tokenregex, function(match, p1, p2, p3, offset, str) { let key = new Number(p2);