launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #27027
[Merge] ~pappacena/launchpad:comment-editing-ui into launchpad:master
Thiago F. Pappacena has proposed merging ~pappacena/launchpad:comment-editing-ui into launchpad:master with ~pappacena/launchpad:comment-editing-revisions-api as a prerequisite.
Commit message:
Javascript component to edit messages, and its first usage in QuestionMessage view
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/402522
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:comment-editing-ui into launchpad:master.
diff --git a/lib/canonical/launchpad/icing/css/base.scss b/lib/canonical/launchpad/icing/css/base.scss
index 68bcce1..6ee2779 100644
--- a/lib/canonical/launchpad/icing/css/base.scss
+++ b/lib/canonical/launchpad/icing/css/base.scss
@@ -2,7 +2,8 @@
body {
/* line-height is the same as the sprite height. */
- font-family: Ubuntu, 'Bitstream Vera Sans', 'DejaVu Sans', Tahoma, sans-serif;
+ font-family: Ubuntu, 'Bitstream Vera Sans', 'DejaVu Sans', Tahoma,
+ sans-serif;
font-size: 12px;
line-height: 18px;
color: #333;
@@ -444,7 +445,8 @@ body {
table {
th, td {
- /* We don't want extra padding on nested tables, like batch navigation. */
+ /* We don't want extra padding on nested tables,
+ like batch navigation. */
padding: 0;
}
}
@@ -543,6 +545,42 @@ body {
border-bottom-left-radius: 5px;
}
+ .editable-message {
+ .editable-message-notification {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ background-color: white;
+ opacity: 0.9;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ align-items: center;
+
+ p {
+ display: block;
+ flex-basis: 100%;
+ margin-top: 10px;
+ }
+ .editable-message-notification-dismiss {
+ flex-basis: 100%;
+ text-align: center;
+ padding: 1px;
+ margin-top: -10px;
+ }
+ }
+
+ .editable-message-form {
+ padding: 0.5em 12px 0;
+ input[type="button"] {
+ padding: 1px;
+ margin: 5px;
+ }
+ }
+ }
+
@import 'typography',
'colours',
'forms',
diff --git a/lib/lp/answers/browser/question.py b/lib/lp/answers/browser/question.py
index 18b4c47..104017e 100644
--- a/lib/lp/answers/browser/question.py
+++ b/lib/lp/answers/browser/question.py
@@ -1191,8 +1191,14 @@ class QuestionMessageDisplayView(LaunchpadView):
# If a comment that isn't visible is being rendered, it's being
# rendered for an admin or registry_expert.
css_classes.append("adminHiddenComment")
+ if self.can_edit:
+ css_classes.append("editable-message")
return " ".join(css_classes)
+ @property
+ def can_edit(self):
+ return check_permission('launchpad.Edit', self.context)
+
def canConfirmAnswer(self):
"""Return True if the user can confirm this answer."""
return (self.display_confirm_button and
diff --git a/lib/lp/answers/stories/question-workflow.txt b/lib/lp/answers/stories/question-workflow.txt
index 984aaf4..7d23e23 100755
--- a/lib/lp/answers/stories/question-workflow.txt
+++ b/lib/lp/answers/stories/question-workflow.txt
@@ -219,7 +219,8 @@ The confirmed answer is also highlighted.
<img ... src="/@@/favourite-yes" ... title="Marked as best answer"/>
>>> print(soup.find(
- ... 'div', 'boardCommentBody highlighted').decode_contents())
+ ... 'div', 'boardCommentBody highlighted editable-message-body'
+ ... ).decode_contents())
<p>New version of the firefox package are available with SVG support
enabled. You can use apt-get or adept to upgrade.</p>
@@ -289,9 +290,9 @@ answerer back to None.
>>> bestAnswer.find('strong') is None
True
- >>> bestAnswer.find('div', 'boardCommentBody')
- <div class="boardCommentBody" itemprop="commentText"><p>New version
- of the firefox package
+ >>> bestAnswer.find('div', 'boardCommentBody editable-message-body')
+ <div class="boardCommentBody editable-message-body"
+ itemprop="commentText"><p>New version of the firefox package
are available with SVG support enabled. You can use apt-get or adept to
upgrade.</p></div>
@@ -356,9 +357,9 @@ The answer's message is also highlighted as the best answer.
No Privileges Person (no-priv)
>>> message = soup.find(
- ... 'div', 'boardCommentBody highlighted')
+ ... 'div', 'boardCommentBody highlighted editable-message-body')
>>> print(message)
- <div class="boardCommentBody highlighted"
+ <div class="boardCommentBody highlighted editable-message-body"
itemprop="commentText"><p>New version of the firefox package are
available with SVG support enabled. You can use apt-get or adept to
upgrade.</p></div>
diff --git a/lib/lp/answers/templates/question-index.pt b/lib/lp/answers/templates/question-index.pt
index e7e233d..13f6270 100644
--- a/lib/lp/answers/templates/question-index.pt
+++ b/lib/lp/answers/templates/question-index.pt
@@ -17,7 +17,8 @@
</style>
<script type="text/javascript">
LPJS.use('base', 'node', 'event',
- 'lp.app.comment', 'lp.answers.subscribers',
+ 'lp.app.comment', 'lp.answers.subscribers',
+ 'lp.services.messages.edit',
function(Y) {
Y.on('domready', function() {
LP.cache.comment_context = LP.cache.context;
@@ -29,6 +30,7 @@
cl.render();
}
new Y.lp.answers.subscribers.createQuestionSubscribersLoader();
+ Y.lp.services.messages.edit.setup();
});
});
</script>
diff --git a/lib/lp/answers/templates/questionmessage-display.pt b/lib/lp/answers/templates/questionmessage-display.pt
index b69b4b6..c5a6869 100644
--- a/lib/lp/answers/templates/questionmessage-display.pt
+++ b/lib/lp/answers/templates/questionmessage-display.pt
@@ -5,9 +5,10 @@
<div
itemscope=""
itemtype="http://schema.org/UserComments"
- tal:define="css_classes view/getBoardCommentCSSClass"
+ tal:define="css_classes python: view.getBoardCommentCSSClass()"
tal:attributes="class string:${css_classes};
- id string:comment-${context/index}">
+ id string:comment-${context/index};
+ data-baseurl context/fmt:url">
<div class="boardCommentDetails">
<table>
<tbody>
@@ -26,7 +27,18 @@
tal:attributes="title context/datecreated/fmt:datetime;
datetime context/datecreated/fmt:isodate"
tal:content="context/datecreated/fmt:displaydate">Thursday
- 13:21</time>:
+ 13:21</time>
+ <span tal:condition="context/date_last_edit">
+ (last edit <time
+ itemprop="editTime"
+ tal:attributes="title context/date_last_edit/fmt:datetime;
+ datetime context/date_last_edit/fmt:isodate"
+ tal:content="context/date_last_edit/fmt:displaydate" />)
+ </span>
+ </td>
+ <td>
+ <img class="sprite edit action-icon editable-message-edit-btn"
+ tal:condition="view/can_edit"/>
</td>
<td class="bug-comment-index">
<a
@@ -36,13 +48,20 @@
</div>
<div class="boardCommentBody"
- tal:attributes="class view/getBodyCSSClass"
+ tal:attributes="class python: view.getBodyCSSClass() + ' editable-message-body'"
itemprop="commentText"
tal:content="structure
context/text_contents/fmt:obfuscate-email/fmt:email-to-html">
Message text.
</div>
+ <div class="editable-message-form" style="display: none">
+ <textarea style="width: 100%" rows="10"
+ tal:content="context/text_contents" />
+ <input type="button" value="Update" class="editable-message-update-btn" />
+ <input type="button" value="Cancel" class="editable-message-cancel-btn" />
+ </div>
+
<div class="confirmBox"
tal:condition="view/canConfirmAnswer">
<form action=""
diff --git a/lib/lp/services/messages/interfaces/message.py b/lib/lp/services/messages/interfaces/message.py
index 7dea2f8..7ac4fb4 100644
--- a/lib/lp/services/messages/interfaces/message.py
+++ b/lib/lp/services/messages/interfaces/message.py
@@ -58,7 +58,7 @@ class IMessageEdit(Interface):
@export_write_operation()
@operation_parameters(
- new_content=TextLine(
+ new_content=Text(
title=_("Message content"),
description=_("The new message content string"),
required=True))
diff --git a/lib/lp/services/messages/javascript/messages.edit.js b/lib/lp/services/messages/javascript/messages.edit.js
new file mode 100644
index 0000000..d54a36e
--- /dev/null
+++ b/lib/lp/services/messages/javascript/messages.edit.js
@@ -0,0 +1,168 @@
+/* Copyright 2015-2021 Canonical Ltd. This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ *
+ * This modules controls HTML comments in order to make them editable. To do
+ * so, it requires:
+ * - A div container with the class .editable-message containing everything
+ * else related to the message
+ * - A data-baseurl="/path/to/msg" on the .editable-message container
+ * - A .editable-message-body container with the original msg content
+ * - A .editable-message-edit-btn element inside the main container, that will
+ * switch the view to edit form when clicked.
+ * - A .editable-message-form, with a textarea and 2 buttons:
+ * .editable-message-update-btn and .editable-message-cancel-btn.
+ *
+ * Once those HTML elements are available in the page, this module should be
+ * initialized with `lp.services.messages.edit.setup()`.
+ *
+ * @module Y.lp.services.messages.edit
+ * @requires node, DOM, lp.client
+ */
+YUI.add('lp.services.messages.edit', function(Y) {
+ var module = Y.namespace('lp.services.messages.edit');
+
+ module.msg_edit_success_notification = (
+ "Message edited, but the original content may still be publicly " +
+ "visible using the API.<br />Please, " +
+ "<a href='https://launchpad.net/+apidoc/devel.html#message'>" +
+ "check the API documentation</a> in case " +
+ "need to remove old message revisions."
+ );
+ module.msg_edit_error_notification = (
+ "There was an error updating the comment. " +
+ "Please, try again in some minutes."
+ );
+
+ module.htmlify_msg = function(text) {
+ text = text.replace(/</g, "<");
+ text = text.replace(/>/g, ">");
+ text = text.replace(/\n/g, "<br/>");
+ return "<p>" + text + "</p>";
+ };
+
+ module.show_edit_message_field = function(msg_body, msg_form) {
+ msg_body.setStyle('display', 'none');
+ msg_form.setStyle('display', 'block');
+ };
+
+ module.hide_edit_message_field = function(msg_body, msg_form) {
+ msg_body.setStyle('display', 'block');
+ msg_form.setStyle('display', 'none');
+ };
+
+ module.save_message_content = function(
+ msg_path, new_content, on_success, on_failure) {
+ var msg_url = "/api/devel" + msg_path;
+ var config = {
+ on: {
+ success: on_success,
+ failure: on_failure
+ },
+ parameters: {"new_content": new_content}
+ };
+ this.lp_client.named_post(msg_url, 'editContent', config);
+ };
+
+ module.show_notification = function(container, msg, can_dismiss) {
+ can_dismiss = can_dismiss || false;
+ // Clean up previous notification.
+ module.hide_notification(container);
+ container.setStyle('position', 'relative');
+ var node = Y.Node.create(
+ "<div class='editable-message-notification'>" +
+ " <p class='block-sprite large-warning'>" +
+ msg +
+ " </p>" +
+ "</div>");
+ container.append(node);
+ if (can_dismiss) {
+ var dismiss = Y.Node.create(
+ "<div class='editable-message-notification-dismiss'>" +
+ " <input type='button' value=' Ok ' />" +
+ "</div>");
+ dismiss.on('click', function() {
+ module.hide_notification(container);
+ });
+ node.append(dismiss);
+ }
+ };
+
+ module.hide_notification = function(container) {
+ var notification = container.one(".editable-message-notification");
+ if(notification) {
+ notification.remove();
+ }
+ };
+
+ module.show_loading = function(container) {
+ module.show_notification(
+ container,
+ '<img class="spinner" src="/@@/spinner" alt="Loading..." />');
+ };
+
+ module.hide_loading = function(container) {
+ module.hide_notification(container);
+ };
+
+ module.setup = function() {
+ this.lp_client = new Y.lp.client.Launchpad();
+
+ Y.all('.editable-message').each(function(container) {
+ var node = container.getDOMNode();
+ var baseurl = node.dataset.baseurl;
+ var msg_body = container.one('.editable-message-body');
+ var msg_form = container.one('.editable-message-form');
+ var edit_btn = container.one('.editable-message-edit-btn');
+ var update_btn = msg_form.one('.editable-message-update-btn');
+ var cancel_btn = msg_form.one('.editable-message-cancel-btn');
+
+ module.hide_edit_message_field(msg_body, msg_form);
+
+ // When clicking edit icon, show the edit form and focus on the
+ // text area.
+ edit_btn.on('click', function(e) {
+ module.show_edit_message_field(msg_body, msg_form);
+ msg_form.one('textarea').getDOMNode().focus();
+ });
+
+ // When clicking on "update" button, disable UI elements and send a
+ // request to update the message at the backend.
+ update_btn.on('click', function(e) {
+ module.show_loading(container);
+ var textarea = msg_form.one('textarea').getDOMNode();
+ var new_content = textarea.value;
+ textarea.disabled = true;
+ update_btn.getDOMNode().disabled = true;
+
+ module.save_message_content(
+ baseurl, new_content, function() {
+ // When finished updating at the backend, re-enable UI
+ // elements and display the new message.
+ var html_msg = module.htmlify_msg(new_content);
+ msg_body.getDOMNode().innerHTML = html_msg;
+ module.hide_edit_message_field(msg_body, msg_form);
+ textarea.disabled = false;
+ update_btn.getDOMNode().disabled = false;
+ module.hide_loading(container);
+ module.show_notification(
+ container,
+ module.msg_edit_success_notification, true);
+ },
+ function(err) {
+ // When something goes wrong at the backend, re-enable
+ // UI elements and display an error.
+ module.show_notification(
+ container,
+ module.msg_edit_error_notification, true);
+ textarea.disabled = false;
+ update_btn.getDOMNode().disabled = false;
+ }
+ );
+ });
+
+ cancel_btn.on('click', function(e) {
+ module.hide_edit_message_field(msg_body, msg_form);
+ });
+ });
+ };
+}, '0.1', {'requires': ['lp.client', 'node', 'DOM']});
diff --git a/lib/lp/services/messages/javascript/tests/test_messages.edit.html b/lib/lp/services/messages/javascript/tests/test_messages.edit.html
new file mode 100644
index 0000000..0fd6056
--- /dev/null
+++ b/lib/lp/services/messages/javascript/tests/test_messages.edit.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<!--
+Copyright 2021 Canonical Ltd. This software is licensed under the
+GNU Affero General Public License version 3 (see the file LICENSE).
+-->
+
+<html>
+ <head>
+ <title>Test message edit</title>
+
+ <!-- YUI and test setup -->
+ <script type="text/javascript"
+ src="../../../../../../build/js/yui/yui/yui.js">
+ </script>
+ <link rel="stylesheet"
+ href="../../../../../../build/js/yui/console/assets/console-core.css" />
+ <link rel="stylesheet"
+ href="../../../../../../build/js/yui/test-console/assets/skins/sam/test-console.css" />
+ <link rel="stylesheet"
+ href="../../../../../../build/js/yui/test/assets/skins/sam/test.css" />
+
+ <script type="text/javascript"
+ src="../../../../../../build/js/lp/app/testing/testrunner.js"></script>
+
+ <link rel="stylesheet"
+ href="../../../../app/javascript/testing/test.css" />
+
+ <!-- Dependencies -->
+ <script type="text/javascript"
+ src="../../../../../../build/js/lp/app/client.js"></script>
+ <script type="text/javascript"
+ src="../../../../../../build/js/lp/app/lp.js"></script>
+ <script type="text/javascript"
+ src="../../../../../../build/js/lp/app/anim/anim.js"></script>
+ <script type="text/javascript"
+ src="../../../../../../build/js/lp/app/extras/extras.js"></script>
+ <script type="text/javascript"
+ src="../../../../../../build/js/lp/app/testing/mockio.js"></script>
+
+ <!-- The module under test. -->
+ <script type="text/javascript" src="../messages.edit.js"></script>
+
+ <!-- Any css assert for this module. -->
+ <!-- <link rel="stylesheet" href="../assets/archive-packages-core.css" /> -->
+
+ <!-- The test suite. -->
+ <script type="text/javascript" src="test_messages.edit.js"></script>
+
+ </head>
+ <body class="yui3-skin-sam">
+ <ul id="suites">
+ <li>lp.services.messages.edit.test</li>
+ </ul>
+
+ <div class="editable-message" id="first-message"
+ data-baseurl="/message/1">
+ <div class="editable-message-body"></div>
+ <img class="sprite edit action-icon editable-message-edit-btn">
+
+ <div class="editable-message-form">
+ <textarea></textarea>
+ <input type="button" value="Update" class="editable-message-update-btn" />
+ <input type="button" value="Cancel" class="editable-message-cancel-btn" />
+ </div>
+ </div>
+
+ <div class="editable-message" id="second-message"
+ data-baseurl="/message/2">
+ <div class="editable-message-body"></div>
+ <img class="sprite edit action-icon editable-message-edit-btn">
+
+ <div class="editable-message-form">
+ <textarea></textarea>
+ <input type="button" value="Update" class="editable-message-update-btn" />
+ <input type="button" value="Cancel" class="editable-message-cancel-btn" />
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/lib/lp/services/messages/javascript/tests/test_messages.edit.js b/lib/lp/services/messages/javascript/tests/test_messages.edit.js
new file mode 100644
index 0000000..9f9f549
--- /dev/null
+++ b/lib/lp/services/messages/javascript/tests/test_messages.edit.js
@@ -0,0 +1,155 @@
+/**
+ * Copyright 2012-2021 Canonical Ltd. This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ *
+ * Tests for lp.services.messages.edit.
+ *
+ * @module lp.services.messages.edit
+ * @submodule test
+ */
+
+YUI.add('lp.services.messages.edit.test', function(Y) {
+
+ var namespace = Y.namespace('lp.services.messages.edit.test');
+
+ var suite = new Y.Test.Suite("lp.services.messages.edit Tests");
+ var module = Y.lp.services.messages.edit;
+
+ function assertDisplayStyles(items, visibility) {
+ for(var i=items ; i<items.length ; i++) {
+ Y.Assert.areSame(visibility, items[i].getStyle("display"));
+ }
+ }
+
+ function assertDisplayStyle(item, visibility) {
+ Y.Assert.areSame(visibility, item.getStyle("display"));
+ }
+
+ var TestMessageEdit = {
+ name: "TestMessageEdit",
+
+ setUp: function() {
+ this.containers = [
+ Y.one("#first-message"), Y.one("#second-message")];
+ this.msg_bodies = [
+ this.containers[0].one(".editable-message-body"),
+ this.containers[1].one(".editable-message-body")
+ ];
+ this.msg_forms = [
+ this.containers[0].one(".editable-message-form"),
+ this.containers[1].one(".editable-message-form")
+ ];
+ this.edit_icons = [
+ this.containers[0].one(".editable-message-edit-btn"),
+ this.containers[1].one(".editable-message-edit-btn")
+ ];
+ this.cancel_btns = [
+ this.containers[0].one(".editable-message-cancel-btn"),
+ this.containers[1].one(".editable-message-cancel-btn")
+ ];
+ this.textareas = [
+ this.msg_forms[0].one("textarea"),
+ this.msg_forms[1].one("textarea")
+ ];
+ this.update_btns = [
+ this.containers[0].one(".editable-message-update-btn"),
+ this.containers[1].one(".editable-message-update-btn")
+ ];
+
+ for(var i=0 ; i<this.containers.length ; i++) {
+ this.msg_bodies[i].getDOMNode().innerHTML = (
+ "Message number " + i);
+ this.msg_bodies[i].setStyle('display', '');
+ this.msg_forms[i].setStyle('display', '');
+ this.textareas[i].getDOMNode().value = '';
+ }
+ },
+
+ test_instantiation_hides_forms: function() {
+ // When editable messages are initialized, the forms should be
+ // hidden.
+ module.setup();
+
+ assertDisplayStyles(this.msg_bodies, 'block');
+ assertDisplayStyles(this.msg_forms, 'none');
+ },
+
+ test_click_edit_icon_shows_form: function() {
+ // Makes sure the form is shown when we click one of the edit icons.
+ module.setup();
+ this.edit_icons[1].simulate('click');
+
+ // Form 1 should be visible...
+ assertDisplayStyle(this.msg_bodies[1], 'none');
+ assertDisplayStyle(this.msg_forms[1], 'block');
+
+ // ... but form 0 should have not be affected.
+ assertDisplayStyle(this.msg_bodies[0], 'block');
+ assertDisplayStyle(this.msg_forms[0], 'none');
+ },
+
+ test_cancel_button_hides_form: function() {
+ // Makes sure the form is hidden again if the user, after clicking
+ // edit icons, decides to cancel edition.
+ module.setup();
+ this.edit_icons[1].simulate('click');
+ this.cancel_btns[1].simulate('click');
+
+ assertDisplayStyle(this.msg_bodies[0], 'block');
+ assertDisplayStyle(this.msg_forms[0], 'none');
+ assertDisplayStyle(this.msg_bodies[1], 'block');
+ assertDisplayStyle(this.msg_forms[1], 'none');
+ },
+
+ test_success_save_comment_edition: function() {
+ module.setup();
+ module.lp_client.io_provider = new Y.lp.testing.mockio.MockIo();
+
+ this.edit_icons[1].simulate('click');
+ this.textareas[1].getDOMNode().value = 'edited\nmessage <foo>';
+ this.update_btns[1].simulate('click');
+
+ // Checks that only the current form interactions are blocked.
+ Y.Assert.isTrue(this.textareas[1].getDOMNode().disabled);
+ Y.Assert.isTrue(this.update_btns[1].getDOMNode().disabled);
+ Y.Assert.isFalse(this.textareas[0].getDOMNode().disabled);
+ Y.Assert.isFalse(this.update_btns[0].getDOMNode().disabled);
+
+ module.lp_client.io_provider.success({
+ responseText:'null',
+ responseHeaders: {'Content-Type': 'application/json'}
+ });
+ Y.Assert.areSame(
+ '<p>edited<br>message <foo></p>',
+ this.msg_bodies[1].getDOMNode().innerHTML);
+
+ // All forms should be released.
+ Y.Assert.isFalse(this.textareas[1].getDOMNode().disabled);
+ Y.Assert.isFalse(this.update_btns[1].getDOMNode().disabled);
+ Y.Assert.isFalse(this.textareas[0].getDOMNode().disabled);
+ Y.Assert.isFalse(this.update_btns[0].getDOMNode().disabled);
+
+ // Check forms and msg bodies visibility are back to normal.
+ assertDisplayStyle(this.msg_bodies[0], 'block');
+ assertDisplayStyle(this.msg_forms[0], 'none');
+ assertDisplayStyle(this.msg_bodies[1], 'block');
+ assertDisplayStyle(this.msg_forms[1], 'none');
+
+ // Check that the request was made correctly.
+ var last_request = module.lp_client.io_provider.last_request;
+ Y.Assert.areSame("/api/devel/message/2", last_request.url);
+ Y.Assert.areSame("POST", last_request.config.method);
+ Y.Assert.areSame(
+ "ws.op=editContent&new_content=edited%0Amessage%20%3Cfoo%3E",
+ last_request.config.data);
+ }
+
+ };
+
+ suite.add(new Y.Test.Case(TestMessageEdit));
+
+ namespace.suite = suite;
+
+}, "0.1", {"requires": [
+ "lp.services.messages.edit", "node", "lp.testing.mockio",
+ "node-event-simulate", "test", "lp.anim"]});
diff --git a/lib/lp/services/messages/tests/test_yuitests.py b/lib/lp/services/messages/tests/test_yuitests.py
new file mode 100644
index 0000000..e614a1c
--- /dev/null
+++ b/lib/lp/services/messages/tests/test_yuitests.py
@@ -0,0 +1,26 @@
+# Copyright 2011-2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Run YUI.test tests."""
+
+__metaclass__ = type
+__all__ = []
+
+from lp.testing import (
+ build_yui_unittest_suite,
+ YUIUnitTestCase,
+ )
+from lp.testing.layers import YUITestLayer
+
+
+class MessagesYUIUnitTestCase(YUIUnitTestCase):
+
+ layer = YUITestLayer
+ suite_name = 'MessagesYUIUnitTests'
+
+
+def test_suite():
+ app_testing_path = 'lp/services/messages'
+ return build_yui_unittest_suite(
+ app_testing_path,
+ MessagesYUIUnitTestCase)
Follow ups