[geary/bug/728002-webkit2: 50/140] Fix JS error getting F=F text from ComposerWebView. Add JS unit tests.



commit 22de6b122eb53f027d3b066f62fd20be67f7c4b3
Author: Michael James Gratton <mike vee net>
Date:   Sun Jan 1 12:37:08 2017 +1100

    Fix JS error getting F=F text from ComposerWebView. Add JS unit tests.
    
    * ui/composer-web-view.js (ComposerPageState::resolveNesting): Apply JS
      RegExp globally, to match default GLib RegEx behaviour.
    
    * test/js/composer-page-state-test.vala: New tests covering generation of
      HTML and F=F text from JS ComposerPageState object.
    
    * test/CMakeLists.txt: Add the new test.
    
    * test/main.vala (main): Add a test suite for JS tests, add the new test
      to it.
    
    * src/client/components/client-web-view.vala (ClientWebView): Add a
      reason to the JSError domain for when a JS exception is thrown.
    
    * bindings/vapi/javascriptcore-4.0.vapi (JS::Context): Add JS.Type and
      some additional methods needed for the unit tests. Move most
      GlobalContext methods to Context so we can pass the lowest common
      demominator around.

 bindings/vapi/javascriptcore-4.0.vapi      |   62 +++++++--
 src/client/components/client-web-view.vala |    2 +-
 test/CMakeLists.txt                        |    2 +
 test/js/composer-page-state-test.vala      |  221 ++++++++++++++++++++++++++++
 test/main.vala                             |    6 +
 ui/composer-web-view.js                    |    2 +-
 6 files changed, 283 insertions(+), 12 deletions(-)
---
diff --git a/bindings/vapi/javascriptcore-4.0.vapi b/bindings/vapi/javascriptcore-4.0.vapi
index f17c5d1..7601d1c 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 a206964..f886cc7 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 3214578..6e2e7a2 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 0000000..1b8a41c
--- /dev/null
+++ b/test/js/composer-page-state-test.vala
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2016 Michael Gratton <mike vee net>
+ *
+ * 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<AsyncResult> async_results = new AsyncQueue<AsyncResult>();
+
+    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 = "<p>para</p>";
+        load_body_fixture(html);
+        try {
+            assert(run_javascript(@"window.geary.getHtml();") == html + "<br><br>");
+        } 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("<p>para</p>");
+        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("<p>pre</p> <blockquote><p>quote</p></blockquote> <p>post</p>");
+        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("<p>pre</p> <blockquote><p>quote1</p> 
<blockquote><p>quote2</p></blockquote></blockquote> <p>post</p>");
+        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 be55e6a..5b733be 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 8fa0dee..5e87dc3 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);


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]