[geary/wip/728002-webkit2: 60/96] Fix JS error getting F=F text from ComposerWebView. Add JS unit tests.
- From: Michael Gratton <mjog src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/wip/728002-webkit2: 60/96] Fix JS error getting F=F text from ComposerWebView. Add JS unit tests.
- Date: Sat, 14 Jan 2017 12:16:52 +0000 (UTC)
commit 8cc37b061aaab6037af83dbbbbc70546d99d9d7a
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]