This lets us implement changing signatures and deleting bottom-quoted messages without having to reload the whole view, and makes it possible to target only the user's content when modifying for send, etc. * src/client/composer/composer-web-view.vala (ComposerWebView): Move composer CSS into composer-web-view.css resource file, load it when loading JS resource and add it to the view's user content manager. (ComposerWebView::load_html): Split up body, signature and quote into a DIV container for each. (ComposerWebView::linkify_content): Replaced with ::clean_content, which will also tidy up internal markup before sending. Update call site and unit test. * src/engine/rfc822/rfc822-utils.vala (Geary.RFC822.Utils): Remove some more obtrusive white space when sending replies/forwards. * test/client/composer/composer-web-view-test.vala, test/js/composer-page-state-test.vala: Update tests to expect new HTML and text output from ComposerWebView and use of individual parts for composer markup. * ui/composer-web-view.js (ComposerPageState): Replace messageBody property and uses with bodyPart, signaturePart and quotePart. Set these content-editable on load. Move listeners from messageBody back to the document.body so they also listen for events on the additional parts. Keep track of text cursor location within the parts and set a class if so, to work around the lack of :focus-inside support. (ComposerPageState::updateSignature): Implement by updating the inner content of the signature part. (ComposerPageState::deleteQuotedMessage): Implement by removing the quote part from the DOM tree. (ComposerPageState::containsAttachmentKeyword): Consider only the bodyPart when scanning for attachments, remove hacks for ignoring the signature any any quoted message. (ComposerPageState::linkifyContent): Mirror ClientWebView change and replace with ::cleanContent. Ensure existing parts have contenteditable and focus class removed, remove signature and quote parts if empty. (ComposerPageState::getHtml): Generate HTML using clones of the three parts, so we can rmeove contenteditable and focus classes without modifying the actual DOM. (ComposerPageState::selectionChanged): Update focus class on parts as needed.
363 lines
15 KiB
Vala
363 lines
15 KiB
Vala
/*
|
||
* 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.
|
||
*/
|
||
|
||
class ComposerPageStateTest : ClientWebViewTestCase<ComposerWebView> {
|
||
|
||
private const string COMPLETE_BODY_TEMPLATE = """<div id="geary-body">%s<div><br></div><div><br></div><div><br></div></div>""";
|
||
private const string CLEAN_BODY_TEMPLATE = "%s<div><br></div><div><br></div><div><br></div>";
|
||
|
||
public ComposerPageStateTest() {
|
||
base("ComposerPageStateTest");
|
||
add_test("edit_context_font", edit_context_font);
|
||
add_test("edit_context_link", edit_context_link);
|
||
add_test("indent_line", indent_line);
|
||
add_test("contains_attachment_keywords", contains_attachment_keywords);
|
||
add_test("clean_content", clean_content);
|
||
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("contains_keywords", contains_keywords);
|
||
add_test("quote_lines", quote_lines);
|
||
add_test("resolve_nesting", resolve_nesting);
|
||
add_test("replace_non_breaking_space", replace_non_breaking_space);
|
||
|
||
try {
|
||
ComposerWebView.load_resources();
|
||
} catch (Error err) {
|
||
assert_not_reached();
|
||
}
|
||
}
|
||
|
||
public void edit_context_link() {
|
||
string html = "<a id=\"test\" href=\"url\">para</a>";
|
||
load_body_fixture(html);
|
||
|
||
try {
|
||
assert(WebKitUtil.to_string(run_javascript(@"new EditContext(document.getElementById('test')).encode()"))
|
||
.has_prefix("1,url,"));
|
||
} catch (Geary.JS.Error err) {
|
||
print("Geary.JS.Error: %s\n", err.message);
|
||
assert_not_reached();
|
||
} catch (Error err) {
|
||
print("WKError: %s\n", err.message);
|
||
assert_not_reached();
|
||
}
|
||
}
|
||
|
||
public void edit_context_font() {
|
||
string html = "<p id=\"test\" style=\"font-family: Comic Sans; font-size: 144\">para</p>";
|
||
load_body_fixture(html);
|
||
|
||
try {
|
||
assert(WebKitUtil.to_string(run_javascript(@"new EditContext(document.getElementById('test')).encode()")) ==
|
||
"0,,Comic Sans,144");
|
||
} catch (Geary.JS.Error err) {
|
||
print("Geary.JS.Error: %s\n", err.message);
|
||
assert_not_reached();
|
||
} catch (Error err) {
|
||
print("WKError: %s\n", err.message);
|
||
assert_not_reached();
|
||
}
|
||
}
|
||
|
||
public void indent_line() {
|
||
load_body_fixture("""<span id="test">some text</span>""");
|
||
try {
|
||
run_javascript(@"SelectionUtil.selectNode(document.getElementById('test'))");
|
||
run_javascript(@"geary.indentLine()");
|
||
assert(WebKitUtil.to_number(run_javascript(@"document.querySelectorAll('blockquote[type=cite]').length")) == 1);
|
||
assert(WebKitUtil.to_string(run_javascript(@"document.querySelectorAll('blockquote[type=cite]').item(0).innerText")) ==
|
||
"some text");
|
||
} catch (Geary.JS.Error err) {
|
||
print("Geary.JS.Error: %s\n", err.message);
|
||
assert_not_reached();
|
||
} catch (Error err) {
|
||
print("WKError: %s\n", err.message);
|
||
assert_not_reached();
|
||
}
|
||
}
|
||
|
||
public void contains_attachment_keywords() {
|
||
load_body_fixture_full("""
|
||
<blockquote>innerquote</blockquote>
|
||
|
||
<p>some text</p>
|
||
|
||
some text
|
||
""",
|
||
"-- <br>sig",
|
||
"<p>outerquote text</p>",
|
||
true
|
||
);
|
||
try {
|
||
assert(WebKitUtil.to_bool(run_javascript(
|
||
@"geary.containsAttachmentKeyword(\"some\", \"subject text\");"
|
||
)));
|
||
assert(WebKitUtil.to_bool(run_javascript(
|
||
@"geary.containsAttachmentKeyword(\"subject\", \"subject text\");"
|
||
)));
|
||
assert(!WebKitUtil.to_bool(run_javascript(
|
||
@"geary.containsAttachmentKeyword(\"innerquote\", \"subject text\");"
|
||
)));
|
||
assert(!WebKitUtil.to_bool(run_javascript(
|
||
@"geary.containsAttachmentKeyword(\"sig\", \"subject text\");"
|
||
)));
|
||
assert(!WebKitUtil.to_bool(run_javascript(
|
||
@"geary.containsAttachmentKeyword(\"outerquote\", \"subject text\");"
|
||
)));
|
||
} catch (Geary.JS.Error err) {
|
||
print("Geary.JS.Error: %s\n", err.message);
|
||
assert_not_reached();
|
||
} catch (Error err) {
|
||
print("WKError: %s\n", err.message);
|
||
assert_not_reached();
|
||
}
|
||
}
|
||
|
||
public void clean_content() {
|
||
// XXX split these up into multiple tests
|
||
load_body_fixture("""
|
||
http://example1.com
|
||
|
||
<p>http://example2.com</p>
|
||
|
||
<p>http://example3.com http://example4.com</p>
|
||
|
||
<a href="blarg">http://example5.com</a>
|
||
|
||
unknown://example6.com
|
||
""");
|
||
|
||
string expected = """
|
||
<a href="http://example1.com">http://example1.com</a>
|
||
|
||
<p><a href="http://example2.com">http://example2.com</a></p>
|
||
|
||
<p><a href="http://example3.com">http://example3.com</a> <a href="http://example4.com">http://example4.com</a></p>
|
||
|
||
<a href="blarg">http://example5.com</a>
|
||
|
||
unknown://example6.com
|
||
""";
|
||
|
||
try {
|
||
run_javascript("geary.cleanContent();");
|
||
assert(WebKitUtil.to_string(run_javascript("geary.bodyPart.innerHTML;")) ==
|
||
CLEAN_BODY_TEMPLATE.printf(expected));
|
||
} catch (Geary.JS.Error err) {
|
||
print("Geary.JS.Error: %s\n", err.message);
|
||
assert_not_reached();
|
||
} catch (Error err) {
|
||
print("WKError: %s\n", err.message);
|
||
assert_not_reached();
|
||
}
|
||
}
|
||
|
||
public void get_html() {
|
||
string html = "<p>para</p>";
|
||
load_body_fixture(html);
|
||
try {
|
||
assert(WebKitUtil.to_string(run_javascript(@"window.geary.getHtml();")) ==
|
||
COMPLETE_BODY_TEMPLATE.printf(html));
|
||
} catch (Geary.JS.Error err) {
|
||
print("Geary.JS.Error: %s\n", err.message);
|
||
assert_not_reached();
|
||
} catch (Error err) {
|
||
print("WKError: %s\n", err.message);
|
||
assert_not_reached();
|
||
}
|
||
}
|
||
|
||
public void get_text() {
|
||
load_body_fixture("<p>para</p>");
|
||
try {
|
||
assert(WebKitUtil.to_string(run_javascript(@"window.geary.getText();")) ==
|
||
"para\n\n\n\n\n");
|
||
} catch (Geary.JS.Error err) {
|
||
print("Geary.JS.Error: %s\n", err.message);
|
||
assert_not_reached();
|
||
} catch (Error err) {
|
||
print("WKError: %s\n", err.message);
|
||
assert_not_reached();
|
||
}
|
||
}
|
||
|
||
public void get_text_with_quote() {
|
||
unichar q_marker = Geary.RFC822.Utils.QUOTE_MARKER;
|
||
load_body_fixture("<p>pre</p> <blockquote><p>quote</p></blockquote> <p>post</p>");
|
||
try {
|
||
assert(WebKitUtil.to_string(run_javascript(@"window.geary.getText();")) ==
|
||
@"pre\n\n$(q_marker)quote\n$(q_marker)\npost\n\n\n\n\n");
|
||
} catch (Geary.JS.Error err) {
|
||
print("Geary.JS.Error: %s", err.message);
|
||
assert_not_reached();
|
||
} catch (Error err) {
|
||
print("WKError: %s", err.message);
|
||
assert_not_reached();
|
||
}
|
||
}
|
||
|
||
public void get_text_with_nested_quote() {
|
||
unichar q_marker = Geary.RFC822.Utils.QUOTE_MARKER;
|
||
load_body_fixture("<p>pre</p> <blockquote><p>quote1</p> <blockquote><p>quote2</p></blockquote></blockquote> <p>post</p>");
|
||
try {
|
||
assert(WebKitUtil.to_string(run_javascript(@"window.geary.getText();")) ==
|
||
@"pre\n\n$(q_marker)quote1\n$(q_marker)\n$(q_marker)$(q_marker)quote2\n$(q_marker)$(q_marker)\npost\n\n\n\n\n");
|
||
} catch (Geary.JS.Error err) {
|
||
print("Geary.JS.Error: %s\n", err.message);
|
||
assert_not_reached();
|
||
} catch (Error err) {
|
||
print("WKError: %s\n", err.message);
|
||
assert_not_reached();
|
||
}
|
||
}
|
||
|
||
public void contains_keywords() {
|
||
load_body_fixture();
|
||
string complete_keys = """new Set(["keyword1", "keyword2"])""";
|
||
string suffix_keys = """new Set(["sf1", "sf2"])""";
|
||
try {
|
||
// Doesn't contain
|
||
assert(!WebKitUtil.to_bool(run_javascript(
|
||
@"ComposerPageState.containsKeywords('notcontained', $complete_keys, $suffix_keys);"
|
||
)));
|
||
assert(!WebKitUtil.to_bool(run_javascript(
|
||
@"ComposerPageState.containsKeywords('not contained', $complete_keys, $suffix_keys);"
|
||
)));
|
||
assert(!WebKitUtil.to_bool(run_javascript(
|
||
@"ComposerPageState.containsKeywords('not\tcontained', $complete_keys, $suffix_keys);"
|
||
)));
|
||
assert(!WebKitUtil.to_bool(run_javascript(
|
||
@"ComposerPageState.containsKeywords('http://www.keyword1.com', $complete_keys, $suffix_keys);"
|
||
)));
|
||
assert(!WebKitUtil.to_bool(run_javascript(
|
||
@"ComposerPageState.containsKeywords('http://www.something.com/something.sf1', $complete_keys, $suffix_keys);"
|
||
)));
|
||
assert(!WebKitUtil.to_bool(run_javascript(
|
||
@"ComposerPageState.containsKeywords('sf1', $complete_keys, $suffix_keys);"
|
||
)));
|
||
assert(!WebKitUtil.to_bool(run_javascript(
|
||
@"ComposerPageState.containsKeywords('.sf1', $complete_keys, $suffix_keys);"
|
||
)));
|
||
|
||
// Does contain
|
||
assert(WebKitUtil.to_bool(run_javascript(
|
||
@"ComposerPageState.containsKeywords('keyword1', $complete_keys, $suffix_keys);"
|
||
)));
|
||
assert(WebKitUtil.to_bool(run_javascript(
|
||
@"ComposerPageState.containsKeywords('keyword2 contained', $complete_keys, $suffix_keys);"
|
||
)));
|
||
assert(WebKitUtil.to_bool(run_javascript(
|
||
@"ComposerPageState.containsKeywords('keyword2\tcontained', $complete_keys, $suffix_keys);"
|
||
)));
|
||
assert(WebKitUtil.to_bool(run_javascript(
|
||
@"ComposerPageState.containsKeywords('something.sf1', $complete_keys, $suffix_keys);"
|
||
)));
|
||
assert(WebKitUtil.to_bool(run_javascript(
|
||
@"ComposerPageState.containsKeywords('something.something.sf2', $complete_keys, $suffix_keys);"
|
||
)));
|
||
} catch (Geary.JS.Error err) {
|
||
print("Geary.JS.Error: %s\n", err.message);
|
||
assert_not_reached();
|
||
} catch (Error err) {
|
||
print("WKError: %s\n", 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(WebKitUtil.to_string(run_javascript(@"ComposerPageState.resolveNesting('$(js_no_quote)', $(js_values));")) ==
|
||
@"foo");
|
||
assert(WebKitUtil.to_string(run_javascript(@"ComposerPageState.resolveNesting('$(js_spaced_quote)', $(js_values));")) ==
|
||
@"foo \n$(q_marker)quote1\n bar");
|
||
assert(WebKitUtil.to_string(run_javascript(@"ComposerPageState.resolveNesting('$(js_leading_quote)', $(js_values));")) ==
|
||
@"$(q_marker)quote1\n bar");
|
||
assert(WebKitUtil.to_string(run_javascript(@"ComposerPageState.resolveNesting('$(js_hanging_quote)', $(js_values));")) ==
|
||
@"foo \n$(q_marker)quote1");
|
||
assert(WebKitUtil.to_string(run_javascript(@"ComposerPageState.resolveNesting('$(js_cosy_quote1)', $(js_values));")) ==
|
||
@"foo\n$(q_marker)quote1\nbar");
|
||
assert(WebKitUtil.to_string(run_javascript(@"ComposerPageState.resolveNesting('$(js_cosy_quote2)', $(js_values));")) ==
|
||
@"foo\n$(q_marker)quote1\n$(q_marker)quote2\nbar");
|
||
} catch (Geary.JS.Error err) {
|
||
print("Geary.JS.Error: %s\n", err.message);
|
||
assert_not_reached();
|
||
} catch (Error err) {
|
||
print("WKError: %s\n", err.message);
|
||
assert_not_reached();
|
||
}
|
||
}
|
||
|
||
public void quote_lines() {
|
||
load_body_fixture();
|
||
unichar q_marker = Geary.RFC822.Utils.QUOTE_MARKER;
|
||
try {
|
||
assert(WebKitUtil.to_string(run_javascript("ComposerPageState.quoteLines('');")) ==
|
||
@"$(q_marker)");
|
||
assert(WebKitUtil.to_string(run_javascript("ComposerPageState.quoteLines('line1');")) ==
|
||
@"$(q_marker)line1");
|
||
assert(WebKitUtil.to_string(run_javascript("ComposerPageState.quoteLines('line1\\nline2');")) ==
|
||
@"$(q_marker)line1\n$(q_marker)line2");
|
||
} catch (Geary.JS.Error err) {
|
||
print("Geary.JS.Error: %s\n", err.message);
|
||
assert_not_reached();
|
||
} catch (Error err) {
|
||
print("WKError: %s\n", err.message);
|
||
assert_not_reached();
|
||
}
|
||
}
|
||
|
||
public void replace_non_breaking_space() {
|
||
load_body_fixture();
|
||
string single_nbsp = "a b";
|
||
string multiple_nbsp = "a b c";
|
||
try {
|
||
assert(WebKitUtil.to_string(run_javascript(@"ComposerPageState.replaceNonBreakingSpace('$(single_nbsp)');")) ==
|
||
"a b");
|
||
assert(WebKitUtil.to_string(run_javascript(@"ComposerPageState.replaceNonBreakingSpace('$(multiple_nbsp)');")) ==
|
||
"a b c");
|
||
} catch (Geary.JS.Error err) {
|
||
print("Geary.JS.Error: %s\n", err.message);
|
||
assert_not_reached();
|
||
} catch (Error err) {
|
||
print("WKError: %s\n", err.message);
|
||
assert_not_reached();
|
||
}
|
||
}
|
||
|
||
protected override ComposerWebView set_up_test_view() {
|
||
return new ComposerWebView(this.config);
|
||
}
|
||
|
||
protected override void load_body_fixture(string body = "") {
|
||
load_body_fixture_full(body, "", "", true);
|
||
}
|
||
|
||
protected void load_body_fixture_full(string body,
|
||
string sig,
|
||
string quote,
|
||
bool top_posting) {
|
||
this.test_view.load_html(body, sig, quote, top_posting, false);
|
||
while (this.test_view.is_loading) {
|
||
Gtk.main_iteration();
|
||
}
|
||
}
|
||
|
||
}
|