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.
This commit is contained in:
parent
5dc20f4273
commit
22de6b122e
6 changed files with 283 additions and 12 deletions
|
|
@ -3,9 +3,9 @@
|
||||||
[CCode (cprefix = "JS", gir_namespace = "JavaScriptCore", gir_version = "4.0", lower_case_cprefix = "JS_", cheader_filename = "JavaScriptCore/JavaScript.h")]
|
[CCode (cprefix = "JS", gir_namespace = "JavaScriptCore", gir_version = "4.0", lower_case_cprefix = "JS_", cheader_filename = "JavaScriptCore/JavaScript.h")]
|
||||||
namespace JS {
|
namespace JS {
|
||||||
|
|
||||||
[CCode (cname = "JSGlobalContextRef")]
|
[CCode (cname = "JSContextRef")]
|
||||||
[SimpleType]
|
[SimpleType]
|
||||||
public struct GlobalContext : Context {
|
public struct Context {
|
||||||
|
|
||||||
[CCode (cname = "JSValueIsBoolean")]
|
[CCode (cname = "JSValueIsBoolean")]
|
||||||
public bool is_boolean(JS.Value value);
|
public bool is_boolean(JS.Value value);
|
||||||
|
|
@ -13,23 +13,21 @@ namespace JS {
|
||||||
[CCode (cname = "JSValueIsNumber")]
|
[CCode (cname = "JSValueIsNumber")]
|
||||||
public bool is_number(JS.Value value);
|
public bool is_number(JS.Value value);
|
||||||
|
|
||||||
|
[CCode (cname = "JSValueIsObject")]
|
||||||
|
public bool is_object(JS.Value value);
|
||||||
|
|
||||||
[CCode (cname = "JSValueToBoolean")]
|
[CCode (cname = "JSValueToBoolean")]
|
||||||
public bool to_boolean(JS.Value value);
|
public bool to_boolean(JS.Value value);
|
||||||
|
|
||||||
[CCode (cname = "JSValueToNumber")]
|
[CCode (cname = "JSValueToNumber")]
|
||||||
public double to_number(JS.Value value, out JS.Value exception);
|
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")]
|
[CCode (cname = "JSValueToStringCopy")]
|
||||||
public String to_string_copy(JS.Value value, out JS.Value exception);
|
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")]
|
[CCode (cname = "JSEvaluateScript")]
|
||||||
public Value evaluate_script(String script,
|
public Value evaluate_script(String script,
|
||||||
Object? thisObject,
|
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")]
|
[CCode (cname = "JSObjectRef")]
|
||||||
[SimpleType]
|
[SimpleType]
|
||||||
public struct Object {
|
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")]
|
[CCode (cname = "JSValueRef")]
|
||||||
[SimpleType]
|
[SimpleType]
|
||||||
public struct Value {
|
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")]
|
[CCode (cname = "JSStringRef", ref_function = "JSStringRetain", unref_function = "JSStringRelease")]
|
||||||
|
|
@ -88,4 +107,27 @@ namespace JS {
|
||||||
public void String.release();
|
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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
* (version 2.1 or later). See the COPYING file in this distribution.
|
* (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 {
|
public class ClientWebView : WebKit.WebView {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ set(TEST_SRC
|
||||||
engine/util-html-test.vala
|
engine/util-html-test.vala
|
||||||
|
|
||||||
client/application/geary-configuration-test.vala
|
client/application/geary-configuration-test.vala
|
||||||
|
|
||||||
|
js/composer-page-state-test.vala
|
||||||
)
|
)
|
||||||
|
|
||||||
# Vala
|
# Vala
|
||||||
|
|
|
||||||
221
test/js/composer-page-state-test.vala
Normal file
221
test/js/composer-page-state-test.vala
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -29,6 +29,7 @@ int main(string[] args) {
|
||||||
|
|
||||||
Geary.RFC822.init();
|
Geary.RFC822.init();
|
||||||
Geary.HTML.init();
|
Geary.HTML.init();
|
||||||
|
Geary.Logging.init();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Hook up all tests into appropriate suites
|
* Hook up all tests into appropriate suites
|
||||||
|
|
@ -46,12 +47,17 @@ int main(string[] args) {
|
||||||
|
|
||||||
client.add_suite(new ConfigurationTest().get_suite());
|
client.add_suite(new ConfigurationTest().get_suite());
|
||||||
|
|
||||||
|
TestSuite js = new TestSuite("js");
|
||||||
|
|
||||||
|
js.add_suite(new ComposerPageStateTest().get_suite());
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Run the tests
|
* Run the tests
|
||||||
*/
|
*/
|
||||||
TestSuite root = TestSuite.get_root();
|
TestSuite root = TestSuite.get_root();
|
||||||
root.add_suite(engine);
|
root.add_suite(engine);
|
||||||
root.add_suite(client);
|
root.add_suite(client);
|
||||||
|
root.add_suite(js);
|
||||||
|
|
||||||
int ret = -1;
|
int ret = -1;
|
||||||
Idle.add(() => {
|
Idle.add(() => {
|
||||||
|
|
|
||||||
|
|
@ -197,7 +197,7 @@ ComposerPageState.resolveNesting = function(text, values) {
|
||||||
ComposerPageState.QUOTE_START +
|
ComposerPageState.QUOTE_START +
|
||||||
"([0-9]*)" +
|
"([0-9]*)" +
|
||||||
ComposerPageState.QUOTE_END +
|
ComposerPageState.QUOTE_END +
|
||||||
"(?=(.?))"
|
"(?=(.?))", "g"
|
||||||
);
|
);
|
||||||
return text.replace(tokenregex, function(match, p1, p2, p3, offset, str) {
|
return text.replace(tokenregex, function(match, p1, p2, p3, offset, str) {
|
||||||
let key = new Number(p2);
|
let key = new Number(p2);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue