← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rharding/launchpad/combo_yui_tests into lp:launchpad

 

Richard Harding has proposed merging lp:~rharding/launchpad/combo_yui_tests into lp:launchpad with lp:~stevenk/launchpad/combo-url as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rharding/launchpad/combo_yui_tests/+merge/91478

= Summary =
Note: This change depends on the branch from Steve in order to make sure the build directory is populated correctly with both YUI and LP JS code. Otherwise the links will fail and tests won't run/load.

With the move to convoy we have a concept of a js build directory. We should be running tests against that directory so that we can easily switch/test different scenarios.

One example is to run our tests from one version of YUI to another. Since the build directory can contain the following:
yui-3.3
yui-3.4
yui (symlink to the current version of yui)

We'd want the tests to be pulling from the yui symlink.

While we're updating tests, we also want to make sure that tests are using a common format for defining/running using our testrunner.js.

This branch starts the work of updating the default test templates for JS tests and the process of updating the modules to use this format pulling from the build directory.

== Implementation Details ==
All files that aren't under direct test should be pulled from the build directory. In this way any tests that fail to work with the build setup should fail and help bring to light issues.

== Tests ==
lib/lp/app/javascript/activator/tests/test_activator.html
lib/lp/app/javascript/anim/tests/test_anim.html
lib/lp/app/javascript/autocomplete/tests/test_autocomplete.html
lib/lp/app/javascript/indicator/tests/test_indicator.html
lib/lp/app/javascript/inlineedit/tests/test_inline_edit.html
lib/lp/app/javascript/inlinehelp/tests/test_inlinehelp.html
lib/lp/app/javascript/ordering/tests/test_orderby_widget.html
lib/lp/app/javascript/overlay/tests/test_overlay.html

== Demo and Q/A ==
We're not touching any production code, so this is a matter of tests passing
-- 
https://code.launchpad.net/~rharding/launchpad/combo_yui_tests/+merge/91478
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rharding/launchpad/combo_yui_tests into lp:launchpad.
=== modified file 'lib/lp/app/javascript/activator/tests/test_activator.html'
--- lib/lp/app/javascript/activator/tests/test_activator.html	2011-08-10 08:43:17 +0000
+++ lib/lp/app/javascript/activator/tests/test_activator.html	2012-02-06 19:47:42 +0000
@@ -1,27 +1,49 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
-  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd";>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+  "http://www.w3.org/TR/html4/strict.dtd";>
+<!--
+Copyright 2012 Canonical Ltd.  This software is licensed under the
+GNU Affero General Public License version 3 (see the file LICENSE).
+-->
+
 <html>
   <head>
-  <title>Activator</title>
-
-  <!-- YUI and test setup -->
-  <script type="text/javascript"
-          src="../../../../../canonical/launchpad/icing/yui/yui/yui.js">
-  </script>
-  <link rel="stylesheet" href="../../../../app/javascript/testing/test.css" />
-  <script type="text/javascript"
-          src="../../../../app/javascript/testing/testrunner.js"></script>
-
-  <!-- The module under test -->
-  <script type="text/javascript" src="../activator.js"></script>
-  <script type="text/javascript" src="../../anim/anim.js"></script>
-  <script type="text/javascript" src="../../lazr/lazr.js"></script>
-  <script type="text/javascript" src="../../extras/extras.js"></script>
-
-  <!-- The test suite -->
-  <script type="text/javascript" src="test_activator.js"></script>
-
-</head>
-<body class="yui3-skin-sam">
-</body>
+      <title>Activator Tests</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/console/assets/skins/sam/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/anim/anim.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/lazr/lazr.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/extras/extras.js"></script>
+
+      <!-- The module under test. -->
+      <script type="text/javascript" src="../activator.js"></script>
+
+      <!-- Any css assert for this module. -->
+      <!-- <link rel="stylesheet" href="../assets/activator-core.css" /> -->
+
+      <!-- The test suite. -->
+      <script type="text/javascript" src="test_activator.js"></script>
+
+    </head>
+    <body class="yui3-skin-sam">
+        <ul id="suites">
+            <!-- <li>lp.large_indicator.test</li> -->
+            <li>lp.activator.test</li>
+        </ul>
+    </body>
 </html>

=== modified file 'lib/lp/app/javascript/activator/tests/test_activator.js'
--- lib/lp/app/javascript/activator/tests/test_activator.js	2011-07-08 05:12:39 +0000
+++ lib/lp/app/javascript/activator/tests/test_activator.js	2012-02-06 19:47:42 +0000
@@ -1,7 +1,9 @@
-/* Copyright (c) 2009, Canonical Ltd. All rights reserved. */
-
-YUI().use('lp.testing.runner', 'test', 'console', 'node', 'lazr.activator',
-           'event', 'event-simulate', function(Y) {
+/* Copyright (c) 2012, Canonical Ltd. All rights reserved. */
+
+YUI.add('lp.activator.test', function (Y) {
+
+var tests = Y.namespace('lp.activator.test');
+tests.suite = new Y.Test.Suite('Activator Tests');
 
 var Assert = Y.Assert;  // For easy access to isTrue(), etc.
 
@@ -25,12 +27,9 @@
     widget.destroy();
 }
 
-var suite = new Y.Test.Suite("Activator Tests");
-
-
-suite.add(new Y.Test.Case({
-
-    name: 'activator_basics',
+tests.suite.add(new Y.Test.Case({
+    name: 'activator_tests',
+
 
     setUp: function() {
         this.workspace = Y.one('#workspace');
@@ -68,6 +67,11 @@
         this.workspace.set('innerHTML', '');
     },
 
+    test_library_exists: function () {
+        Y.Assert.isObject(Y.lazr.activator,
+            "We should be able to locate the lazr.activator module");
+    },
+
     test_correct_animation_node: function() {
         // Check that the correct animation node is used.
         // First check the default.
@@ -268,6 +272,8 @@
     }
 }));
 
-Y.lp.testing.Runner.run(suite);
 
+}, '0.1', {
+    'requires': ['test', 'console', 'node', 'lazr.activator', 'event',
+        'event-simulate']
 });

=== modified file 'lib/lp/app/javascript/anim/tests/test_anim.html'
--- lib/lp/app/javascript/anim/tests/test_anim.html	2011-08-09 14:18:02 +0000
+++ lib/lp/app/javascript/anim/tests/test_anim.html	2012-02-06 19:47:42 +0000
@@ -1,29 +1,48 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
-  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd";>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+  "http://www.w3.org/TR/html4/strict.dtd";>
+<!--
+Copyright 2012 Canonical Ltd.  This software is licensed under the
+GNU Affero General Public License version 3 (see the file LICENSE).
+-->
+
 <html>
   <head>
-  <title>Anim</title>
-
-  <!-- YUI and test setup -->
-  <script type="text/javascript"
-          src="../../../../../canonical/launchpad/icing/yui/yui/yui.js">
-  </script>
-  <link rel="stylesheet" href="../../../../app/javascript/testing/test.css" />
-  <script type="text/javascript"
-          src="../../../../app/javascript/testing/testrunner.js"></script>
-  <script type="text/javascript"
-          src="../../extras/extras.js"></script>
-
-  <!-- The module under test -->
-  <script type="text/javascript" src="../anim.js"></script>
-  <script type="text/javascript" src="../../lazr/lazr.js"></script>
-
-  <!-- The test suite -->
-  <script type="text/javascript" src="test_anim.js"></script>
-  </head>
-<body class="yui3-skin-sam">
-  <ul id="suites">
-    <li>lp.anim.test</li>
-  </ul>
-</body>
+      <title>Anim Tests</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/console/assets/skins/sam/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/extras/extras.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/lazr/lazr.js"></script>
+
+      <!-- The module under test. -->
+      <script type="text/javascript" src="../anim.js"></script>
+
+      <!-- Any css assert for this module. -->
+      <!-- <link rel="stylesheet" href="../assets/anim-core.css" /> -->
+
+      <!-- The test suite. -->
+      <script type="text/javascript" src="test_anim.js"></script>
+
+    </head>
+    <body class="yui3-skin-sam">
+        <ul id="suites">
+            <!-- <li>lp.large_indicator.test</li> -->
+            <li>lp.anim.test</li>
+        </ul>
+    </body>
 </html>

=== modified file 'lib/lp/app/javascript/anim/tests/test_anim.js'
--- lib/lp/app/javascript/anim/tests/test_anim.js	2011-08-10 08:41:49 +0000
+++ lib/lp/app/javascript/anim/tests/test_anim.js	2012-02-06 19:47:42 +0000
@@ -1,202 +1,194 @@
 /* Copyright (c) 2009-2011, Canonical Ltd. All rights reserved. */
 
 YUI.add('lp.anim.test', function(Y) {
-
-var namespace = Y.namespace('lp.anim.test');
-
-var Assert = Y.Assert,  // For easy access to isTrue(), etc.
-    ArrayAssert = Y.ArrayAssert;
-
-var suite = new Y.Test.Suite("Anim Tests");
-
-var TestAnim = {
-    name: 'TestAnim',
-
-    setUp: function() {
-        this.workspace = Y.Node.create(
-            '<div id="workspace">'
-            + '<table id="anim-table">'
-            + '<tr id="anim-table-tr">'
-            + '<td id="anim-table-td1" style="background: #eeeeee">foo</td>'
-            + '<td id="anim-table-td2" style="background: #eeeeee">bar</td>'
-            + '</tr></table></div>'
-        );
-        Y.one(document.body).append(this.workspace);
-    },
-
-    tearDown: function() {
-        this.workspace.remove(true);
-    },
-
-    test_resolveNodeListFrom_selector: function() {
-        var selector = '#anim-table-td1';
-        var nodelist = namespace.resolveNodeListFrom(selector);
-        Assert.isInstanceOf(Y.NodeList, nodelist);
-        ArrayAssert.itemsAreSame(
-            [Y.one(selector)], nodelist._nodes.map(Y.one));
-    },
-
-    test_resolveNodeListFrom_node: function() {
-        var node = Y.one('#anim-table-td1');
-        var nodelist = namespace.resolveNodeListFrom(node);
-        Assert.isInstanceOf(Y.NodeList, nodelist);
-        ArrayAssert.itemsAreSame(
-            [node], nodelist._nodes.map(Y.one));
-    },
-
-    test_resolveNodeListFrom_node_list: function() {
-        var nodelist_orig = Y.all('#anim-table td');
-        var nodelist = namespace.resolveNodeListFrom(nodelist_orig);
-        Assert.isInstanceOf(Y.NodeList, nodelist);
-        Assert.areSame(nodelist, nodelist_orig);
-    },
-
-    test_resolveNodeListFrom_anythine_else: function() {
-        var succeed = true;
-        try {
-            var nodelist = namespace.resolveNodeListFrom(
-                {crazy: true, broken: 'definitely'});
-        } catch(e) {
-            succeed = false;
+    var Assert = Y.Assert,  // For easy access to isTrue(), etc.
+        ArrayAssert = Y.ArrayAssert;
+    var tests = Y.namespace('lp.anim.test');
+
+    tests.suite = new Y.Test.Suite('Anim Tets');
+    tests.suite.add(new Y.Test.Case({
+        name: 'TestAnim',
+
+        setUp: function() {
+            this.workspace = Y.Node.create(
+                '<div id="workspace">'
+                + '<table id="anim-table">'
+                + '<tr id="anim-table-tr">'
+                + '<td id="anim-table-td1" style="background: #eeeeee">foo</td>'
+                + '<td id="anim-table-td2" style="background: #eeeeee">bar</td>'
+                + '</tr></table></div>'
+            );
+            Y.one(document.body).append(this.workspace);
+        },
+
+        tearDown: function() {
+            this.workspace.remove(true);
+        },
+
+        test_resolveNodeListFrom_selector: function() {
+            var selector = '#anim-table-td1';
+            var nodelist = tests.resolveNodeListFrom(selector);
+            Assert.isInstanceOf(Y.NodeList, nodelist);
+            ArrayAssert.itemsAreSame(
+                [Y.one(selector)], nodelist._nodes.map(Y.one));
+        },
+
+        test_resolveNodeListFrom_node: function() {
+            var node = Y.one('#anim-table-td1');
+            var nodelist = tests.resolveNodeListFrom(node);
+            Assert.isInstanceOf(Y.NodeList, nodelist);
+            ArrayAssert.itemsAreSame(
+                [node], nodelist._nodes.map(Y.one));
+        },
+
+        test_resolveNodeListFrom_node_list: function() {
+            var nodelist_orig = Y.all('#anim-table td');
+            var nodelist = tests.resolveNodeListFrom(nodelist_orig);
+            Assert.isInstanceOf(Y.NodeList, nodelist);
+            Assert.areSame(nodelist, nodelist_orig);
+        },
+
+        test_resolveNodeListFrom_anythine_else: function() {
+            var succeed = true;
+            try {
+                var nodelist = tests.resolveNodeListFrom(
+                    {crazy: true, broken: 'definitely'});
+            } catch(e) {
+                succeed = false;
+            }
+            Assert.isFalse(succeed, "Somehow, we're cleverer than we thought.");
+        },
+
+        test_green_flash_td1: function() {
+            // works as expected on a single node,
+            // without coercion into a NodeList here
+            var node = Y.one('#anim-table-td1');
+            var bgcolor = node.getStyle('backgroundColor');
+            var anim = Y.lp.anim.green_flash(
+                {node: node,
+                 to: {backgroundColor: bgcolor},
+                 duration: 0.2}
+            );
+            anim.run();
+            this.wait(function() {
+                Assert.areEqual(
+                    bgcolor,
+                    node.getStyle('backgroundColor'),
+                    'background colors do not match'
+                    );
+                }, 500
+            );
+        },
+
+        test_green_flash_td1_by_selector: function() {
+            // works as expected on a single node selector,
+            // without coercion into a NodeList here
+            var node = Y.one('#anim-table-td1');
+            var bgcolor = node.getStyle('backgroundColor');
+            var anim = Y.lp.anim.green_flash(
+                {node: '#anim-table-td1',
+                 to: {backgroundColor: bgcolor},
+                 duration: 0.2}
+            );
+            anim.run();
+            this.wait(function() {
+                Assert.areEqual(
+                    bgcolor,
+                    node.getStyle('backgroundColor'),
+                    'background colors do not match'
+                    );
+                }, 500
+            );
+        },
+
+        test_green_flash_multi: function() {
+            // works with a native NodeList as well
+            var nodelist = Y.all('#anim-table td');
+            var red = '#ff0000';
+            var backgrounds = [];
+            Y.each(nodelist, function(n) {
+                backgrounds.push({bg: n.getStyle('backgroundColor'), node: n});
+            });
+            var anim = Y.lp.anim.green_flash(
+                {node: nodelist,
+                 to: {backgroundColor: red},
+                 duration: 5}
+            );
+            anim.run();
+            this.wait(function() {
+                Assert.areNotEqual(
+                    backgrounds[0].node.getStyle('backgroundColor'),
+                    red,
+                    'background of 0 has mysteriously jumped to the end color.'
+                );
+                Assert.areNotEqual(
+                    backgrounds[1].node.getStyle('backgroundColor'),
+                    red,
+                    'background of 1 has mysteriously jumped to the end color.'
+                );
+                Assert.areNotEqual(
+                    backgrounds[0].node.getStyle('backgroundColor'),
+                    backgrounds[0].bg,
+                    'background of 0 has not changed at all.'
+                );
+                Assert.areNotEqual(
+                    backgrounds[1].node.getStyle('backgroundColor'),
+                    backgrounds[1].bg,
+                    'background of 1 has not changed at all.'
+                );
+            }, 1500);
+        },
+
+        test_green_flash_multi_by_selector: function() {
+            // works with a native NodeList as well
+            var nodelist = Y.all('#anim-table td');
+            var red = '#ff0000';
+            var backgrounds = [];
+            Y.each(nodelist, function(n) {
+                backgrounds.push({bg: n.getStyle('backgroundColor'), node: n});
+            });
+            var anim = Y.lp.anim.green_flash(
+                {node: '#anim-table td',
+                 to: {backgroundColor: red},
+                 duration: 2}
+            );
+            anim.run();
+            this.wait(function() {
+                Assert.areNotEqual(
+                    backgrounds[0].node.getStyle('backgroundColor'),
+                    red,
+                    'background of 0 has mysteriously jumped to the end color.'
+                );
+                Assert.areNotEqual(
+                    backgrounds[1].node.getStyle('backgroundColor'),
+                    red,
+                    'background of 1 has mysteriously jumped to the end color.'
+                );
+                Assert.areNotEqual(
+                    backgrounds[0].node.getStyle('backgroundColor'),
+                    backgrounds[0].bg,
+                    'background of 0 has not changed at all.'
+                );
+                Assert.areNotEqual(
+                    backgrounds[1].node.getStyle('backgroundColor'),
+                    backgrounds[1].bg,
+                    'background of 1 has not changed at all.'
+                );
+            }, 500);
+        },
+
+        test_on: function() {
+            // Anim.on delegates to its component animations.
+            var targets = [];
+            var anim = new Y.lp.anim.Anim({node: "#anim-table td"});
+            Assert.areSame(2, anim._anims.length);
+            anim.on("start", function(event) { targets.push(event.target); });
+            anim.run();
+            Assert.areSame(2, targets.length);
+            ArrayAssert.containsItems(anim._anims, targets);
         }
-        Assert.isFalse(succeed, "Somehow, we're cleverer than we thought.");
-    },
-
-    test_green_flash_td1: function() {
-        // works as expected on a single node,
-        // without coercion into a NodeList here
-        var node = Y.one('#anim-table-td1');
-        var bgcolor = node.getStyle('backgroundColor');
-        var anim = Y.lp.anim.green_flash(
-            {node: node,
-             to: {backgroundColor: bgcolor},
-             duration: 0.2}
-        );
-        anim.run();
-        this.wait(function() {
-            Assert.areEqual(
-                bgcolor,
-                node.getStyle('backgroundColor'),
-                'background colors do not match'
-                );
-            }, 500
-        );
-    },
-
-    test_green_flash_td1_by_selector: function() {
-        // works as expected on a single node selector,
-        // without coercion into a NodeList here
-        var node = Y.one('#anim-table-td1');
-        var bgcolor = node.getStyle('backgroundColor');
-        var anim = Y.lp.anim.green_flash(
-            {node: '#anim-table-td1',
-             to: {backgroundColor: bgcolor},
-             duration: 0.2}
-        );
-        anim.run();
-        this.wait(function() {
-            Assert.areEqual(
-                bgcolor,
-                node.getStyle('backgroundColor'),
-                'background colors do not match'
-                );
-            }, 500
-        );
-    },
-
-    test_green_flash_multi: function() {
-        // works with a native NodeList as well
-        var nodelist = Y.all('#anim-table td');
-        var red = '#ff0000';
-        var backgrounds = [];
-        Y.each(nodelist, function(n) {
-            backgrounds.push({bg: n.getStyle('backgroundColor'), node: n});
-        });
-        var anim = Y.lp.anim.green_flash(
-            {node: nodelist,
-             to: {backgroundColor: red},
-             duration: 5}
-        );
-        anim.run();
-        this.wait(function() {
-            Assert.areNotEqual(
-                backgrounds[0].node.getStyle('backgroundColor'),
-                red,
-                'background of 0 has mysteriously jumped to the end color.'
-            );
-            Assert.areNotEqual(
-                backgrounds[1].node.getStyle('backgroundColor'),
-                red,
-                'background of 1 has mysteriously jumped to the end color.'
-            );
-            Assert.areNotEqual(
-                backgrounds[0].node.getStyle('backgroundColor'),
-                backgrounds[0].bg,
-                'background of 0 has not changed at all.'
-            );
-            Assert.areNotEqual(
-                backgrounds[1].node.getStyle('backgroundColor'),
-                backgrounds[1].bg,
-                'background of 1 has not changed at all.'
-            );
-        }, 1500);
-    },
-
-    test_green_flash_multi_by_selector: function() {
-        // works with a native NodeList as well
-        var nodelist = Y.all('#anim-table td');
-        var red = '#ff0000';
-        var backgrounds = [];
-        Y.each(nodelist, function(n) {
-            backgrounds.push({bg: n.getStyle('backgroundColor'), node: n});
-        });
-        var anim = Y.lp.anim.green_flash(
-            {node: '#anim-table td',
-             to: {backgroundColor: red},
-             duration: 2}
-        );
-        anim.run();
-        this.wait(function() {
-            Assert.areNotEqual(
-                backgrounds[0].node.getStyle('backgroundColor'),
-                red,
-                'background of 0 has mysteriously jumped to the end color.'
-            );
-            Assert.areNotEqual(
-                backgrounds[1].node.getStyle('backgroundColor'),
-                red,
-                'background of 1 has mysteriously jumped to the end color.'
-            );
-            Assert.areNotEqual(
-                backgrounds[0].node.getStyle('backgroundColor'),
-                backgrounds[0].bg,
-                'background of 0 has not changed at all.'
-            );
-            Assert.areNotEqual(
-                backgrounds[1].node.getStyle('backgroundColor'),
-                backgrounds[1].bg,
-                'background of 1 has not changed at all.'
-            );
-        }, 500);
-    },
-
-    test_on: function() {
-        // Anim.on delegates to its component animations.
-        var targets = [];
-        var anim = new Y.lp.anim.Anim({node: "#anim-table td"});
-        Assert.areSame(2, anim._anims.length);
-        anim.on("start", function(event) { targets.push(event.target); });
-        anim.run();
-        Assert.areSame(2, targets.length);
-        ArrayAssert.containsItems(anim._anims, targets);
-    }
-
-};
-
-suite.add(new Y.Test.Case(TestAnim));
-
-// Exports.
-namespace.suite = suite;
+
+    }));
 
 }, "0.1", {"requires": [
                'test', 'node', 'lp.anim', 'event',

=== modified file 'lib/lp/app/javascript/autocomplete/tests/test_autocomplete.html'
--- lib/lp/app/javascript/autocomplete/tests/test_autocomplete.html	2011-07-08 06:06:15 +0000
+++ lib/lp/app/javascript/autocomplete/tests/test_autocomplete.html	2012-02-06 19:47:42 +0000
@@ -1,24 +1,47 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
-  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd";>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+  "http://www.w3.org/TR/html4/strict.dtd";>
+<!--
+Copyright 2012 Canonical Ltd.  This software is licensed under the
+GNU Affero General Public License version 3 (see the file LICENSE).
+-->
+
 <html>
   <head>
-  <title>autocomplete unit tests</title>
-
-  <!-- YUI and test setup -->
-  <script type="text/javascript"
-          src="../../../../../canonical/launchpad/icing/yui/yui/yui.js">
-  </script>
-  <link rel="stylesheet" href="../../../../app/javascript/testing/test.css" />
-  <script type="text/javascript"
-          src="../../../../app/javascript/testing/testrunner.js"></script>
-
-  <!-- The module under test -->
-  <script type="text/javascript" src="../autocomplete.js"></script>
-
-  <!-- The test suite -->
-  <script type="text/javascript" src="test_autocomplete.js"></script>
-
-</head>
-<body class="yui3-skin-sam">
-</body>
+      <title>Autocomplete Tests</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/console/assets/skins/sam/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/..."></script> -->
+
+      <!-- The module under test. -->
+      <script type="text/javascript" src="../autocomplete.js"></script>
+
+      <!-- Any css assert for this module. -->
+      <!-- <link rel="stylesheet" href="../assets/autocomplete-core.css" /> -->
+
+      <!-- The test suite. -->
+      <script type="text/javascript" src="test_autocomplete.js"></script>
+
+    </head>
+    <body class="yui3-skin-sam">
+        <ul id="suites">
+            <!-- <li>lp.large_indicator.test</li> -->
+            <li>lp.autocomplete.test</li>
+        </ul>
+    </body>
 </html>

=== modified file 'lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js'
--- lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js	2011-07-08 05:12:39 +0000
+++ lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js	2012-02-06 19:47:42 +0000
@@ -1,569 +1,564 @@
-/* Copyright (c) 2009, Canonical Ltd. All rights reserved. */
-
-YUI().use('lp.testing.runner', 'test', 'console', 'node', 'lazr.autocomplete',
-           'event', 'event-simulate', function(Y) {
-
-/*****************************
- *
- *  Helper methods and aliases
- *
- */
-var Assert = Y.Assert;
-
-/* Helper function to clean up a dynamically added widget instance. */
-function cleanup_widget(widget) {
-    // Nuke the boundingBox, but only if we've touched the DOM.
-    if (widget.get('rendered')) {
-        var bb = widget.get('boundingBox');
-        bb.get('parentNode').removeChild(bb);
-    }
-    // Kill the widget itself.
-    widget.destroy();
-}
-
-/* A helper to create a simple text input box */
-function make_input(value) {
-    var input = document.createElement('input');
-    input.setAttribute('type', 'text');
-    input.setAttribute('value', value || '');
-    Y.one('body').appendChild(input);
-    return input;
-}
-
-/* A helper to destroy a generic input: make_input()'s inverse */
-function kill_input(input) {
-    Y.one('body').removeChild(input);
-}
-
-
-/****************************
- *
- *  Tests
- *
- */
-
-var suite = new Y.Test.Suite('autocomplete Test Suite');
-
-
-suite.add(new Y.Test.Case({
-
-    name:'test widget setup',
-
-    setUp: function() {
-        this.input = make_input();
-    },
-
-    tearDown: function() {
-        kill_input(this.input);
-    },
-
-    test_widget_starts_hidden: function() {
-        var autocomp = new Y.lazr.AutoComplete({ input: this.input });
-        autocomp.render();
-        Assert.isFalse(
-            autocomp.get('visible'),
-            "The widget should start out hidden.");
-    }
-}));
-
-
-suite.add(new Y.Test.Case({
-
-    name:'test display of matching results',
-
-    setUp: function() {
-        this.input = make_input();
-        this.autocomp = new Y.lazr.AutoComplete({
-            input: this.input
-        });
-    },
-
-    tearDown: function() {
-        cleanup_widget(this.autocomp);
-        kill_input(this.input);
-    },
-
-    /* A helper to option the completions list for a given input string. */
-    complete_input: function(value) {
-        this.input.value = value;
-        var last_charcode = value.charCodeAt(value.length - 1);
-        Y.Event.simulate(this.input, 'keyup', { keyCode: last_charcode });
-    },
-
-    /* Extract the matching text from the widget's autocompletion list. */
-    get_completions: function() {
-        if (!this.autocomp.get('rendered')) {
-            Y.fail("Tried find matches for an unrendered widget.");
-            return;
-        }
-
-        var matches = [];
-        this.autocomp
-            .get('boundingBox')
-            .all('.item')
-            .each(function(item) {
-                matches.push(item.get('text'));
-            });
-        return matches;
-    },
-
-    test_autocomplete_is_visible_if_results_match: function() {
-        this.autocomp.set('data', ['aaa']);
-        this.autocomp.render();
-
-        // We want to match the one and only data set element.
-        this.complete_input('aa');
-        Assert.isTrue(
-            this.autocomp.get('visible'),
-            "The widget should be visible if matching input was found.");
-    },
-
-    test_autocomplete_is_hidden_if_no_query_is_given: function() {
-        this.autocomp.set('data', ['aaa']);
-        this.autocomp.render();
-
-        // We want to simulate an empty input field, but some action triggers
-        // matching.
-        this.complete_input('');
-        Assert.isFalse(
-            this.autocomp.get('visible'),
-            "The widget should be hidden if the input field is empty.");
-    },
-
-    test_autocomplete_is_hidden_if_results_do_not_match: function() {
-        this.autocomp.set('data', ['bbb']);
-        this.autocomp.render();
-
-        if (this.autocomp.get('visible')) {
-            Y.fail("The autocomplete widget should start out hidden.");
-        }
-
-
-        // 'aa' shouldn't match any of the data.
-        this.complete_input('aa');
-        Assert.isFalse(
-            this.autocomp.get('visible'),
-            "The widget should be hidden if the query doesn't match any " +
-            "possible completions.");
-    },
-
-    test_display_should_contain_all_matches: function() {
-        var data = [
-            'aaa',
-            'baa'
-        ];
-
-        this.autocomp.set('data', data);
-        this.autocomp.render();
-
-        // Trigger autocompletion, should match all data items.
-        this.complete_input('aa');
-
-        // Grab the now-open menu
-        var option_list = Y.one('.yui3-autocomplete-list');
-        Assert.isObject(option_list,
-            "The list of completion options should be open.");
-
-        Y.ArrayAssert.itemsAreEqual(
-            this.get_completions(),
-            data,
-            "Every autocomplete item should be present in the available " +
-            "match keys.");
-    },
-
-    test_display_is_updated_with_new_completions: function() {
-        // Create two pieces of data, each narrower than the other.
-        this.autocomp.set('data', ['aaa', 'aab']);
-        this.autocomp.render();
-
-        // Trigger autocompletion for the loosest matches
-        this.complete_input('aa');
-        // Complete the narrower set
-        this.complete_input('aaa');
-
-        var completions = this.get_completions();
-
-        Y.ArrayAssert.itemsAreEqual(
-            ['aaa'],
-            completions,
-            "'aaa' should be the data item displayed after narrowing the " +
-            "search with the query 'aaa'.");
-    },
-
-    test_matching_text_in_item_is_marked: function() {
-        this.autocomp.set('data', ['aaa']);
-        this.autocomp.render();
-
-        // Display the matching input.
-        var query = 'aa';
-        this.complete_input(query);
-
-        // Grab the matching item
-        var matching_text = this.autocomp
-            .get('boundingBox')
-            .one('.item .matching-text');
-
-        Assert.isNotNull(matching_text,
-            "Some of the matching item's text should be marked as matching.");
-
-        Assert.areEqual(
-            query,
-            matching_text.get('text'),
-            "The matching text should be the same as the query text.");
-    },
-
-    test_escape_key_should_close_completions_list: function() {
-        this.autocomp.set('data', ['aaa']);
-        this.autocomp.render();
-
-        // Open the completions list
-        this.complete_input('aa');
-
-        // Hit the escape key to close the list
-        Y.Event.simulate(this.input, 'keydown', { keyCode: 27 });
-
-        Assert.isFalse(
-            this.autocomp.get('visible'),
-            "The list of completions should be closed after pressing the " +
-            "escape key.");
-    }
-}));
-
-suite.add(new Y.Test.Case({
-
-    name:'test result text marking method',
-
-    test_match_at_beginning_should_be_marked: function() {
-        var autocomp    = new Y.lazr.AutoComplete();
-        var marked_text = autocomp.markMatchingText('aabb', 'aa', 0);
-
-        Assert.areEqual(
-            '<span class="matching-text">aa</span>bb',
-            marked_text,
-            "The text at the beginning of the result should have been " +
-            "marked.");
-    },
-
-    test_match_in_middle_should_be_marked: function() {
-        var autocomp    = new Y.lazr.AutoComplete();
-        var marked_text = autocomp.markMatchingText('baab', 'aa', 1);
-
-        Assert.areEqual(
-            'b<span class="matching-text">aa</span>b',
-            marked_text,
-            "The text in the middle of the result should have been " +
-            "marked.");
-    },
-
-    test_match_at_end_should_be_marked: function() {
-        var autocomp    = new Y.lazr.AutoComplete();
-        var marked_text = autocomp.markMatchingText('bbaa', 'aa', 2);
-
-        Assert.areEqual(
-            'bb<span class="matching-text">aa</span>',
-            marked_text,
-            "The text at the end of the result should have been " +
-            "marked.");
-    }
-}));
-
-
-suite.add(new Y.Test.Case({
-
-    name:'test query parsing',
-
-    setUp: function() {
-        this.autocomplete = new Y.lazr.AutoComplete({
-            delimiter: ' '
-        });
-    },
-
-    test_space_for_delimiter: function() {
-        Assert.areEqual(
-            'b',
-            this.autocomplete.parseQuery('a b').text,
-            "Input should be split around the 'space' character.");
-        Assert.isNull(
-            this.autocomplete.parseQuery(' '),
-            "Space for input and delimiter should not parse.");
-    },
-
-    test_parsed_query_is_stripped_of_leading_whitespace: function() {
-        this.autocomplete.set('delimiter', ',');
-
-        Assert.areEqual(
-            'a',
-            this.autocomplete.parseQuery(' a').text,
-            "Leading whitespace at the start of the input string should " +
-            "be stripped.");
-
-        Assert.areEqual(
-            'b',
-            this.autocomplete.parseQuery('a, b').text,
-            "Leading whitespace between the last separator and the current " +
-            "query should be stripped.");
-    },
-
-    test_query_is_taken_from_middle_of_input: function() {
-        // Pick a caret position that is in the middle of the second result.
-        var input = "aaa bbb ccc";
-        var caret = 6;
-
-        Assert.areEqual(
-            'bbb',
-            this.autocomplete.parseQuery(input, caret).text,
-            "The current query should be picked out of the middle of the " +
-            "text input if the caret has been positioned there.");
-    },
-
-    test_query_is_taken_from_beginning_of_input: function() {
-        // Pick a caret position that is in the first input's query
-        var input = "aaa bbb";
-        var caret = 2;
-
-        Assert.areEqual(
-            'aaa',
-            this.autocomplete.parseQuery(input, caret).text,
-            "The first block of text should become the current query if " +
-            "the caret is positioned within it.");
-    }
-}));
-
-suite.add(new Y.Test.Case({
-
-    name:'test results matching algorithm',
-
-    /* A helper function to determine if two match result items are equal */
-    matches_are_equal: function(a, b) {
-        if (typeof a == 'undefined') {
-            Assert.fail("Match set 'a' is of type 'undefined'!");
-        }
-        if (typeof b == 'undefined') {
-            Assert.fail("Match set 'b' is of type 'undefined'!");
-        }
-        return (a.text == b.text) && (a.offset == b.offset);
-    },
-
-    test_no_matches_returns_an_empty_array: function() {
-        var autocomplete = new Y.lazr.AutoComplete({
-            data: ['ccc']
-        });
-
-        var matches = autocomplete.findMatches('aa');
-        Y.ArrayAssert.isEmpty(matches,
-            "No data should have matched the query 'aa'");
-    },
-
-    test_match_last_item: function() {
-        var autocomplete = new Y.lazr.AutoComplete({
-            data: [
-                'ccc',
+/* Copyright (c) 2012, Canonical Ltd. All rights reserved. */
+
+YUI.add('lp.autocomplete.test', function (Y) {
+    var tests = Y.namespace('lp.autocomplete.test');
+    tests.suite = new Y.Test.Suite('autocomplete Tests');
+
+    /*****************************
+     *
+     *  Helper methods and aliases
+     *
+     */
+    var Assert = Y.Assert;
+
+    /* Helper function to clean up a dynamically added widget instance. */
+    function cleanup_widget(widget) {
+        // Nuke the boundingBox, but only if we've touched the DOM.
+        if (widget.get('rendered')) {
+            var bb = widget.get('boundingBox');
+            bb.get('parentNode').removeChild(bb);
+        }
+        // Kill the widget itself.
+        widget.destroy();
+    }
+
+    /* A helper to create a simple text input box */
+    function make_input(value) {
+        var input = document.createElement('input');
+        input.setAttribute('type', 'text');
+        input.setAttribute('value', value || '');
+        Y.one('body').appendChild(input);
+        return input;
+    }
+
+    /* A helper to destroy a generic input: make_input()'s inverse */
+    function kill_input(input) {
+        Y.one('body').removeChild(input);
+    }
+
+    tests.suite.add(new Y.Test.Case({
+        name:'test widget setup',
+
+        setUp: function() {
+            this.input = make_input();
+        },
+
+        tearDown: function() {
+            kill_input(this.input);
+        },
+
+        test_library_exists: function () {
+            Y.Assert.isObject(Y.lazr.AutoComplete,
+                "We should be able to locate the lazr.autocomplete module");
+        },
+
+        test_widget_starts_hidden: function() {
+            var autocomp = new Y.lazr.AutoComplete({ input: this.input });
+            autocomp.render();
+            Assert.isFalse(
+                autocomp.get('visible'),
+                "The widget should start out hidden.");
+        }
+    }));
+
+    tests.suite.add(new Y.Test.Case({
+
+        name:'test display of matching results',
+
+        setUp: function() {
+            this.input = make_input();
+            this.autocomp = new Y.lazr.AutoComplete({
+                input: this.input
+            });
+        },
+
+        tearDown: function() {
+            cleanup_widget(this.autocomp);
+            kill_input(this.input);
+        },
+
+        /* A helper to option the completions list for a given input string. */
+        complete_input: function(value) {
+            this.input.value = value;
+            var last_charcode = value.charCodeAt(value.length - 1);
+            Y.Event.simulate(this.input, 'keyup', { keyCode: last_charcode });
+        },
+
+        /* Extract the matching text from the widget's autocompletion list. */
+        get_completions: function() {
+            if (!this.autocomp.get('rendered')) {
+                Y.fail("Tried find matches for an unrendered widget.");
+                return;
+            }
+
+            var matches = [];
+            this.autocomp
+                .get('boundingBox')
+                .all('.item')
+                .each(function(item) {
+                    matches.push(item.get('text'));
+                });
+            return matches;
+        },
+
+        test_autocomplete_is_visible_if_results_match: function() {
+            this.autocomp.set('data', ['aaa']);
+            this.autocomp.render();
+
+            // We want to match the one and only data set element.
+            this.complete_input('aa');
+            Assert.isTrue(
+                this.autocomp.get('visible'),
+                "The widget should be visible if matching input was found.");
+        },
+
+        test_autocomplete_is_hidden_if_no_query_is_given: function() {
+            this.autocomp.set('data', ['aaa']);
+            this.autocomp.render();
+
+            // We want to simulate an empty input field, but some action
+            // triggers matching.
+            this.complete_input('');
+            Assert.isFalse(
+                this.autocomp.get('visible'),
+                "The widget should be hidden if the input field is empty.");
+        },
+
+        test_autocomplete_is_hidden_if_results_do_not_match: function() {
+            this.autocomp.set('data', ['bbb']);
+            this.autocomp.render();
+
+            if (this.autocomp.get('visible')) {
+                Y.fail("The autocomplete widget should start out hidden.");
+            }
+
+
+            // 'aa' shouldn't match any of the data.
+            this.complete_input('aa');
+            Assert.isFalse(
+                this.autocomp.get('visible'),
+                "The widget should be hidden if the query doesn't match any " +
+                "possible completions.");
+        },
+
+        test_display_should_contain_all_matches: function() {
+            var data = [
+                'aaa',
+                'baa'
+            ];
+
+            this.autocomp.set('data', data);
+            this.autocomp.render();
+
+            // Trigger autocompletion, should match all data items.
+            this.complete_input('aa');
+
+            // Grab the now-open menu
+            var option_list = Y.one('.yui3-autocomplete-list');
+            Assert.isObject(option_list,
+                "The list of completion options should be open.");
+
+            Y.ArrayAssert.itemsAreEqual(
+                this.get_completions(),
+                data,
+                "Every autocomplete item should be present in the available " +
+                "match keys.");
+        },
+
+        test_display_is_updated_with_new_completions: function() {
+            // Create two pieces of data, each narrower than the other.
+            this.autocomp.set('data', ['aaa', 'aab']);
+            this.autocomp.render();
+
+            // Trigger autocompletion for the loosest matches
+            this.complete_input('aa');
+            // Complete the narrower set
+            this.complete_input('aaa');
+
+            var completions = this.get_completions();
+
+            Y.ArrayAssert.itemsAreEqual(
+                ['aaa'],
+                completions,
+                "'aaa' should be the data item displayed after narrowing the " +
+                "search with the query 'aaa'.");
+        },
+
+        test_matching_text_in_item_is_marked: function() {
+            this.autocomp.set('data', ['aaa']);
+            this.autocomp.render();
+
+            // Display the matching input.
+            var query = 'aa';
+            this.complete_input(query);
+
+            // Grab the matching item
+            var matching_text = this.autocomp
+                .get('boundingBox')
+                .one('.item .matching-text');
+
+            Assert.isNotNull(matching_text,
+                "Some of the matching item's text should be marked matching.");
+
+            Assert.areEqual(
+                query,
+                matching_text.get('text'),
+                "The matching text should be the same as the query text.");
+        },
+
+        test_escape_key_should_close_completions_list: function() {
+            this.autocomp.set('data', ['aaa']);
+            this.autocomp.render();
+
+            // Open the completions list
+            this.complete_input('aa');
+
+            // Hit the escape key to close the list
+            Y.Event.simulate(this.input, 'keydown', { keyCode: 27 });
+
+            Assert.isFalse(
+                this.autocomp.get('visible'),
+                "The list of completions should be closed after pressing the " +
+                "escape key.");
+        }
+    }));
+
+    tests.suite.add(new Y.Test.Case({
+
+        name:'test result text marking method',
+
+        test_match_at_beginning_should_be_marked: function() {
+            var autocomp    = new Y.lazr.AutoComplete();
+            var marked_text = autocomp.markMatchingText('aabb', 'aa', 0);
+
+            Assert.areEqual(
+                '<span class="matching-text">aa</span>bb',
+                marked_text,
+                "The text at the beginning of the result should have been " +
+                "marked.");
+        },
+
+        test_match_in_middle_should_be_marked: function() {
+            var autocomp    = new Y.lazr.AutoComplete();
+            var marked_text = autocomp.markMatchingText('baab', 'aa', 1);
+
+            Assert.areEqual(
+                'b<span class="matching-text">aa</span>b',
+                marked_text,
+                "The text in the middle of the result should have been " +
+                "marked.");
+        },
+
+        test_match_at_end_should_be_marked: function() {
+            var autocomp    = new Y.lazr.AutoComplete();
+            var marked_text = autocomp.markMatchingText('bbaa', 'aa', 2);
+
+            Assert.areEqual(
+                'bb<span class="matching-text">aa</span>',
+                marked_text,
+                "The text at the end of the result should have been " +
+                "marked.");
+        }
+    }));
+
+
+    tests.suite.add(new Y.Test.Case({
+
+        name:'test query parsing',
+
+        setUp: function() {
+            this.autocomplete = new Y.lazr.AutoComplete({
+                delimiter: ' '
+            });
+        },
+
+        test_space_for_delimiter: function() {
+            Assert.areEqual(
+                'b',
+                this.autocomplete.parseQuery('a b').text,
+                "Input should be split around the 'space' character.");
+            Assert.isNull(
+                this.autocomplete.parseQuery(' '),
+                "Space for input and delimiter should not parse.");
+        },
+
+        test_parsed_query_is_stripped_of_leading_whitespace: function() {
+            this.autocomplete.set('delimiter', ',');
+
+            Assert.areEqual(
+                'a',
+                this.autocomplete.parseQuery(' a').text,
+                "Leading whitespace at the start of the input string should " +
+                "be stripped.");
+
+            Assert.areEqual(
+                'b',
+                this.autocomplete.parseQuery('a, b').text,
+                "Leading whitespace between the last separator and the " +
+                "current query should be stripped.");
+        },
+
+        test_query_is_taken_from_middle_of_input: function() {
+            // Pick a caret position that is in the middle of the second result.
+            var input = "aaa bbb ccc";
+            var caret = 6;
+
+            Assert.areEqual(
                 'bbb',
-                'aaa'
-            ]
-        });
-
-        var matches = autocomplete.findMatches('aa');
-
-        Y.ArrayAssert.itemsAreEquivalent(
-            [{text: 'aaa', offset: 0}],
-            matches,
-            this.matches_are_equal,
-            "One row should have matched the query 'aa'.");
-    },
-
-    test_match_ordering: function() {
-        // Matches, in reverse order.
-        var autocomplete = new Y.lazr.AutoComplete({
-            data: [
-                'bbaa',
-                'baab',
-                'aabb'
-            ]
-        });
-
-        var matches = autocomplete.findMatches('aa');
-
-        Y.ArrayAssert.itemsAreEquivalent(
-            [{text: 'aabb', offset: 0},
-             {text: 'baab', offset: 1},
-             {text: 'bbaa', offset: 2}],
-            matches,
-            this.matches_are_equal,
-            "The match array should have all of it's keys in order.");
-    },
-
-    test_mixed_case_text_matches: function() {
-        var autocomplete = new Y.lazr.AutoComplete({
-            data: ['aBc']
-        });
-
-        var matches = autocomplete.findMatches('b');
-
-        Y.ArrayAssert.itemsAreEquivalent(
-            [{text:'aBc', offset: 1}],
-            matches,
-            this.matches_are_equal,
-            "The match algorithm should be case insensitive.");
-    },
-
-    test_mixed_case_matches_come_in_stable_order: function() {
-        // Data with the mixed-case coming first in order.
-        var autocomplete = new Y.lazr.AutoComplete({
-            data: ['aBc', 'aaa', 'abc']
-        });
-
-        var matches = autocomplete.findMatches('b');
-
-        Y.ArrayAssert.itemsAreEquivalent(
-            [{text: 'aBc', offset: 1},
-             {text: 'abc', offset: 1}],
-            matches,
-            this.matches_are_equal,
-            "Mixed-case matches should arrive in stable order.");
-    }
-}));
-
-
-suite.add(new Y.Test.Case({
-
-    name:'test selecting results',
-
-    setUp: function() {
-        this.input = make_input();
-        this.autocomp = new Y.lazr.AutoComplete({
-            input: this.input
-        });
-        this.autocomp.render();
-    },
-
-    tearDown: function() {
-        cleanup_widget(this.autocomp);
-        kill_input(this.input);
-    },
-
-    /* A helper to option the completions list for a given input string. */
-    complete_input: function(value) {
-        this.input.value = value;
-        var last_charcode = value.charCodeAt(value.length - 1);
-        Y.Event.simulate(this.input, 'keyup', { keyCode: last_charcode });
-    },
-
-    /* A helper to select the selected completion result with the Tab key. */
-    press_selection_key: function() {
-        Y.Event.simulate(this.input, "keydown", { keyCode: 9 });
-    },
-
-    test_pressing_enter_completes_current_input: function() {
-        this.autocomp.set('data', ['aaaa', 'aabb']);
-
-        // Open the completion options
-        this.complete_input('aa');
-
-        // Press 'Enter'
-        Y.Event.simulate(this.input, "keydown", { keyCode: 13 });
-
-        Assert.areEqual(
-            'aaaa ',
-            this.input.value,
-            "The first completion should have been appended to the input's " +
-            "value after pressing the 'Enter' key.");
-    },
-
-    test_pressing_tab_completes_current_input: function() {
-        this.autocomp.set('data', ['aaaa', 'aabb']);
-
-        // Open the completion options
-        this.complete_input('aa');
-
-        // Press 'Tab'
-        Y.Event.simulate(this.input, "keydown", { keyCode: 9 });
-
-        Assert.areEqual(
-            'aaaa ',
-            this.input.value,
-            "The first completion should have been appended to the input's " +
-            "value after pressing the 'Enter' key.");
-    },
-
-    test_clicking_on_first_result_completes_input: function() {
-        this.autocomp.set('data', ['aaaa', 'aabb']);
-        this.complete_input('aa');
-
-        // Click on the first displayed result
-        var options = this.autocomp.get('contentBox').all('.item');
-        var first_item = Y.Node.getDOMNode(options.item(0));
-        Y.Event.simulate(first_item, 'click');
-
-        Assert.areEqual(
-            'aaaa ',
-            this.input.value,
-            "The first completion should have been appended to the input's " +
-            "value after clicking it's list node.");
-    },
-
-    test_selecting_results_hides_completion_list: function() {
-        this.autocomp.set('data', 'aaa');
-        this.complete_input('a');
-        this.press_selection_key();
-
-        Assert.isFalse(
-            this.autocomp.get('visible'),
-            "The completion list should be hidden after a result is " +
-            "selected.");
-    },
-
-    test_completed_input_replaces_current_input: function() {
-        this.autocomp.set('data', ['abba']);
-
-        // Match the one and only result, but match the second character in
-        // it.  Throw in some pre-existing user input just to be sure things
-        // work.
-        this.complete_input('xxx b');
-        this.press_selection_key();
-
-        Assert.areEqual(
-           'xxx abba ',
-           this.input.value,
-           "The user's current query should have been replaced with the " +
-           "selected value.");
-    },
-
-    test_completed_input_has_delimiter_appended_to_it: function() {
-        var delimiter = ' ';
-        this.autocomp.set('data', ['aaaa']);
-        this.autocomp.set('delimiter', delimiter);
-
-        this.complete_input('a');
-        this.press_selection_key();
-
-        Assert.areEqual(
-            delimiter,
-            this.input.value.charAt(this.input.value.length - 1),
-            "The last character of the input should be the current " +
-            "query delimiter.");
-    },
-
-    test_down_arrow_selects_second_result_in_list: function() {
-        this.autocomp.set('data', ['first_item', 'second_item']);
-
-        // Match the first result.  It should be selected by default.
-        this.complete_input('item');
-
-        // Simulate pressing the down arrow key.
-        Y.Event.simulate(this.input, 'keydown', { keyCode: 40 });
-
-        // Now, select the second result.
-        this.press_selection_key();
-
-        Assert.areEqual(
-            'second_item ',
-            this.input.value,
-            "Pressing the down-arrow key should select the second option " +
-            "in the completions list.");
-    }
-}));
-
-Y.lp.testing.Runner.run(suite);
-
+                this.autocomplete.parseQuery(input, caret).text,
+                "The current query should be picked out of the middle of the " +
+                "text input if the caret has been positioned there.");
+        },
+
+        test_query_is_taken_from_beginning_of_input: function() {
+            // Pick a caret position that is in the first input's query
+            var input = "aaa bbb";
+            var caret = 2;
+
+            Assert.areEqual(
+                'aaa',
+                this.autocomplete.parseQuery(input, caret).text,
+                "The first block of text should become the current query if " +
+                "the caret is positioned within it.");
+        }
+    }));
+
+    tests.suite.add(new Y.Test.Case({
+
+        name:'test results matching algorithm',
+
+        /* A helper function to determine if two match result items are equal */
+        matches_are_equal: function(a, b) {
+            if (typeof a == 'undefined') {
+                Assert.fail("Match set 'a' is of type 'undefined'!");
+            }
+            if (typeof b == 'undefined') {
+                Assert.fail("Match set 'b' is of type 'undefined'!");
+            }
+            return (a.text == b.text) && (a.offset == b.offset);
+        },
+
+        test_no_matches_returns_an_empty_array: function() {
+            var autocomplete = new Y.lazr.AutoComplete({
+                data: ['ccc']
+            });
+
+            var matches = autocomplete.findMatches('aa');
+            Y.ArrayAssert.isEmpty(matches,
+                "No data should have matched the query 'aa'");
+        },
+
+        test_match_last_item: function() {
+            var autocomplete = new Y.lazr.AutoComplete({
+                data: [
+                    'ccc',
+                    'bbb',
+                    'aaa'
+                ]
+            });
+
+            var matches = autocomplete.findMatches('aa');
+
+            Y.ArrayAssert.itemsAreEquivalent(
+                [{text: 'aaa', offset: 0}],
+                matches,
+                this.matches_are_equal,
+                "One row should have matched the query 'aa'.");
+        },
+
+        test_match_ordering: function() {
+            // Matches, in reverse order.
+            var autocomplete = new Y.lazr.AutoComplete({
+                data: [
+                    'bbaa',
+                    'baab',
+                    'aabb'
+                ]
+            });
+
+            var matches = autocomplete.findMatches('aa');
+
+            Y.ArrayAssert.itemsAreEquivalent(
+                [{text: 'aabb', offset: 0},
+                 {text: 'baab', offset: 1},
+                 {text: 'bbaa', offset: 2}],
+                matches,
+                this.matches_are_equal,
+                "The match array should have all of it's keys in order.");
+        },
+
+        test_mixed_case_text_matches: function() {
+            var autocomplete = new Y.lazr.AutoComplete({
+                data: ['aBc']
+            });
+
+            var matches = autocomplete.findMatches('b');
+
+            Y.ArrayAssert.itemsAreEquivalent(
+                [{text:'aBc', offset: 1}],
+                matches,
+                this.matches_are_equal,
+                "The match algorithm should be case insensitive.");
+        },
+
+        test_mixed_case_matches_come_in_stable_order: function() {
+            // Data with the mixed-case coming first in order.
+            var autocomplete = new Y.lazr.AutoComplete({
+                data: ['aBc', 'aaa', 'abc']
+            });
+
+            var matches = autocomplete.findMatches('b');
+
+            Y.ArrayAssert.itemsAreEquivalent(
+                [{text: 'aBc', offset: 1},
+                 {text: 'abc', offset: 1}],
+                matches,
+                this.matches_are_equal,
+                "Mixed-case matches should arrive in stable order.");
+        }
+    }));
+
+
+    tests.suite.add(new Y.Test.Case({
+
+        name:'test selecting results',
+
+        setUp: function() {
+            this.input = make_input();
+            this.autocomp = new Y.lazr.AutoComplete({
+                input: this.input
+            });
+            this.autocomp.render();
+        },
+
+        tearDown: function() {
+            cleanup_widget(this.autocomp);
+            kill_input(this.input);
+        },
+
+        // A helper to option the completions list for a given input string.
+        complete_input: function(value) {
+            this.input.value = value;
+            var last_charcode = value.charCodeAt(value.length - 1);
+            Y.Event.simulate(this.input, 'keyup', { keyCode: last_charcode });
+        },
+
+        // A helper to select the selected completion result with the Tab key.
+        press_selection_key: function() {
+            Y.Event.simulate(this.input, "keydown", { keyCode: 9 });
+        },
+
+        test_pressing_enter_completes_current_input: function() {
+            this.autocomp.set('data', ['aaaa', 'aabb']);
+
+            // Open the completion options
+            this.complete_input('aa');
+
+            // Press 'Enter'
+            Y.Event.simulate(this.input, "keydown", { keyCode: 13 });
+
+            Assert.areEqual(
+                'aaaa ',
+                this.input.value,
+                "The first completion should have been appended to the " +
+                "input's value after pressing the 'Enter' key.");
+        },
+
+        test_pressing_tab_completes_current_input: function() {
+            this.autocomp.set('data', ['aaaa', 'aabb']);
+
+            // Open the completion options
+            this.complete_input('aa');
+
+            // Press 'Tab'
+            Y.Event.simulate(this.input, "keydown", { keyCode: 9 });
+
+            Assert.areEqual(
+                'aaaa ',
+                this.input.value,
+                "The first completion should have been appended to the " +
+                "input's value after pressing the 'Enter' key.");
+        },
+
+        test_clicking_on_first_result_completes_input: function() {
+            this.autocomp.set('data', ['aaaa', 'aabb']);
+            this.complete_input('aa');
+
+            // Click on the first displayed result
+            var options = this.autocomp.get('contentBox').all('.item');
+            var first_item = Y.Node.getDOMNode(options.item(0));
+            Y.Event.simulate(first_item, 'click');
+
+            Assert.areEqual(
+                'aaaa ',
+                this.input.value,
+                "The first completion should have been appended to the " +
+                "input's value after clicking it's list node.");
+        },
+
+        test_selecting_results_hides_completion_list: function() {
+            this.autocomp.set('data', 'aaa');
+            this.complete_input('a');
+            this.press_selection_key();
+
+            Assert.isFalse(
+                this.autocomp.get('visible'),
+                "The completion list should be hidden after a result is " +
+                "selected.");
+        },
+
+        test_completed_input_replaces_current_input: function() {
+            this.autocomp.set('data', ['abba']);
+
+            // Match the one and only result, but match the second character in
+            // it.  Throw in some pre-existing user input just to be sure things
+            // work.
+            this.complete_input('xxx b');
+            this.press_selection_key();
+
+            Assert.areEqual(
+               'xxx abba ',
+               this.input.value,
+               "The user's current query should have been replaced with the " +
+               "selected value.");
+        },
+
+        test_completed_input_has_delimiter_appended_to_it: function() {
+            var delimiter = ' ';
+            this.autocomp.set('data', ['aaaa']);
+            this.autocomp.set('delimiter', delimiter);
+
+            this.complete_input('a');
+            this.press_selection_key();
+
+            Assert.areEqual(
+                delimiter,
+                this.input.value.charAt(this.input.value.length - 1),
+                "The last character of the input should be the current " +
+                "query delimiter.");
+        },
+
+        test_down_arrow_selects_second_result_in_list: function() {
+            this.autocomp.set('data', ['first_item', 'second_item']);
+
+            // Match the first result.  It should be selected by default.
+            this.complete_input('item');
+
+            // Simulate pressing the down arrow key.
+            Y.Event.simulate(this.input, 'keydown', { keyCode: 40 });
+
+            // Now, select the second result.
+            this.press_selection_key();
+
+            Assert.areEqual(
+                'second_item ',
+                this.input.value,
+                "Pressing the down-arrow key should select the second option " +
+                "in the completions list.");
+        }
+    }));
+
+}, '0.1', {
+    'requires': ['test', 'console', 'lp.autocomplete', 'node', 'event',
+        'event-simulate', 'lazr.autocomplete']
 });

=== modified file 'lib/lp/app/javascript/indicator/tests/test_indicator.html'
--- lib/lp/app/javascript/indicator/tests/test_indicator.html	2011-12-01 20:55:49 +0000
+++ lib/lp/app/javascript/indicator/tests/test_indicator.html	2012-02-06 19:47:42 +0000
@@ -1,28 +1,47 @@
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
   "http://www.w3.org/TR/html4/strict.dtd";>
+<!--
+Copyright 2012 Canonical Ltd.  This software is licensed under the
+GNU Affero General Public License version 3 (see the file LICENSE).
+-->
+
 <html>
   <head>
-  <title>Indicator Tests</title>
-
-  <!-- YUI and test setup -->
-  <script type="text/javascript"
-          src="../../../../../canonical/launchpad/icing/yui/yui/yui.js">
-  </script>
-  <link rel="stylesheet" href="../../../../app/javascript/testing/test.css" />
-  <link rel="stylesheet" href="../assets/indicator-core.css" />
-  <script type="text/javascript"
-          src="../../../../app/javascript/testing/testrunner.js"></script>
-
-  <!-- The module under test -->
-  <script type="text/javascript" src="../indicator.js"></script>
-
-  <!-- The test suite -->
-  <script type="text/javascript" src="test_indicator.js"></script>
-
-</head>
-<body class="yui3-skin-sam">
-    <ul id="suites">
-        <li>lp.large_indicator.test</li>
-    </ul>
-</body>
+      <title>Indicator Tests</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/console/assets/skins/sam/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/..."></script> -->
+
+      <!-- The module under test. -->
+      <script type="text/javascript" src="../indicator.js"></script>
+
+      <!-- Any css assert for this module. -->
+      <link rel="stylesheet" href="../assets/indicator-core.css" />
+
+      <!-- The test suite. -->
+      <script type="text/javascript" src="test_indicator.js"></script>
+
+    </head>
+    <body class="yui3-skin-sam">
+        <ul id="suites">
+            <!-- <li>lp.large_indicator.test</li> -->
+            <li>lp.indicator.test</li>
+        </ul>
+    </body>
 </html>

=== modified file 'lib/lp/app/javascript/indicator/tests/test_indicator.js'
--- lib/lp/app/javascript/indicator/tests/test_indicator.js	2012-01-10 21:25:39 +0000
+++ lib/lp/app/javascript/indicator/tests/test_indicator.js	2012-02-06 19:47:42 +0000
@@ -1,12 +1,13 @@
 /* Copyright (c) 2011, Canonical Ltd. All rights reserved. */
 
-YUI.add('lp.large_indicator.test', function (Y) {
+YUI.add('lp.indicator.test', function (Y) {
 
-var large_indicator = Y.namespace('lp.large_indicator.test');
-var suite = new Y.Test.Suite('Indicator Tests');
+var tests = Y.namespace('lp.indicator.test');
+tests.suite = new Y.Test.Suite('Indicator Tests');
 var Assert = Y.Assert;
 
-suite.add(new Y.Test.Case({
+// add the suite to the NS for the testrunner.js to find
+tests.suite.add(new Y.Test.Case({
 
     name: 'indicator_tests',
 
@@ -215,6 +216,5 @@
     }
 }));
 
-large_indicator.suite = suite;
 
 }, '0.1', {'requires': ['test', 'lp.app.indicator']});

=== modified file 'lib/lp/app/javascript/inlineedit/tests/test_inline_edit.html'
--- lib/lp/app/javascript/inlineedit/tests/test_inline_edit.html	2011-11-30 14:05:07 +0000
+++ lib/lp/app/javascript/inlineedit/tests/test_inline_edit.html	2012-02-06 19:47:42 +0000
@@ -1,28 +1,55 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
-  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd";>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+  "http://www.w3.org/TR/html4/strict.dtd";>
+<!--
+Copyright 2012 Canonical Ltd.  This software is licensed under the
+GNU Affero General Public License version 3 (see the file LICENSE).
+-->
+
 <html>
   <head>
-  <title>Inline Edit</title>
-
-  <!-- YUI and test setup -->
-  <script type="text/javascript"
-          src="../../../../../canonical/launchpad/icing/yui/yui/yui.js">
-  </script>
-  <link rel="stylesheet" href="../../../../app/javascript/testing/test.css" />
-  <script type="text/javascript"
-          src="../../../../app/javascript/testing/testrunner.js"></script>
-
-  <!-- The module under test -->
-  <script type="text/javascript" src="../editor.js"></script>
-  <script type="text/javascript" src="../../anim/anim.js"></script>
-  <script type="text/javascript" src="../../lazr/lazr.js"></script>
-  <script type="text/javascript" src="../../extras/extras.js"></script>
-  <script type="text/javascript" src="../../formwidgets/resizing_textarea.js"></script>
-
-  <!-- The test suite -->
-  <script type="text/javascript" src="test_inline_edit.js"></script>
-
-</head>
-<body class="yui3-skin-sam">
-</body>
+      <title>inline_edit tests</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/console/assets/skins/sam/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/anim/anim.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/lazr/lazr.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/extras/extras.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/formwidgets/resizing_textarea.js"></script>
+
+
+      <!-- The module under test. -->
+      <script type="text/javascript" src="../editor.js"></script>
+
+      <!-- Any css assert for this module. -->
+      <!-- <link rel="stylesheet" href="../assets/inline_edit-core.css" /> -->
+
+      <!-- The test suite. -->
+      <script type="text/javascript" src="test_inline_edit.js"></script>
+
+    </head>
+    <body class="yui3-skin-sam">
+        <ul id="suites">
+            <!-- <li>lp.large_indicator.test</li> -->
+            <li>lp.inline_edit.test</li>
+        </ul>
+    </body>
 </html>

=== modified file 'lib/lp/app/javascript/inlineedit/tests/test_inline_edit.js'
--- lib/lp/app/javascript/inlineedit/tests/test_inline_edit.js	2011-12-21 07:54:40 +0000
+++ lib/lp/app/javascript/inlineedit/tests/test_inline_edit.js	2012-02-06 19:47:42 +0000
@@ -1,993 +1,1006 @@
-/* Copyright (c) 2009, Canonical Ltd. All rights reserved. */
-
-YUI().use('lp.testing.runner', 'test', 'console', 'node', 'lazr.editor',
-          'lp.app.formwidgets.resizing_textarea', 'event', 'event-simulate',
-          'plugin', function(Y) {
-
-var SAMPLE_HTML = [
-    '<h1>Single-line editing</h1>',
-    ' <div id="editable_single_text">',
-    ' <span id="single_text"',
-    '  class="yui3-editable_text-text">Some editable inline text.</span>',
-    ' <button id="single_edit" class="yui3-editable_text-trigger">',
-    '  Edit this</button>',
-    ' </div>',
-    ' <hr />',
-    ' <h1>Multi-line editing</h1>',
-    ' <div id="editable_multi_text">',
-    ' <button id="multi_edit" class="yui3-editable_text-trigger">',
-    '  Edit this</button>',
-    ' <span id="multi_text" class="yui3-editable_text-text">',
-    ' <p>Some editable multi-line text.</p></span>',
-    ' </div>'
-].join('');
-
-var Assert = Y.Assert;  // For easy access to isTrue(), etc.
-
-/* Helper to stamp a Node with an ID attribute.  Needed for YUI 2.X
- * testing, which is heavily ID-based.
- *
- * Returns the node's 'id' attribute.
- */
-function id_for(node) {
-    if (!node.getAttribute('id')) {
-        var id = Y.stamp(node);
-        node.setAttribute('id', id);
-    }
-    return node.getAttribute('id');
-}
-
-/*
- * A wrapper for the Y.Event.simulate() function.  The wrapper accepts
- * CSS selectors and Node instances instead of raw nodes.
- */
-function simulate(selector, evtype) {
-    var rawnode = Y.Node.getDOMNode(Y.one(selector));
-    Y.Event.simulate(rawnode, evtype);
-}
-
-/* Helper function that creates a new editor instance. */
-function make_editor(cfg) {
-    return new Y.InlineEditor(cfg);
-}
-
-/* Helper function to clean up a dynamically added widget instance. */
-function cleanup_widget(widget) {
-    // Nuke the boundingBox, but only if we've touched the DOM.
-    if (widget.get('rendered')) {
-        var bb = widget.get('boundingBox');
-        if (bb && Y.Node.getDOMNode(bb)) {
-            var parentNode = bb.get('parentNode');
-            if (parentNode && Y.Node.getDOMNode(parentNode)) {
-                parentNode.removeChild(bb);
-            }
-        }
-    }
-    // Kill the widget itself.
-    widget.destroy();
-}
-
-function setup_sample_html() {
-    if (! Y.one("#scaffolding")) {
-        Y.one(document.body).appendChild(
-            Y.Node.create("<div id='scaffolding'></div>"));
-    }
-
-    Y.one("#scaffolding").set("innerHTML", SAMPLE_HTML);
-}
-
-function make_editable_text(cfg) {
-    // For the editor
-    // TODO: fix this ugly hack
-    var defaults = {
-        contentBox: '#editable_single_text',
-        boundingBox: '#inline-edit-container'
-    };
-    return new Y.EditableText(Y.merge(defaults, cfg));
-}
-
-// Helper: convert size specification like "120px" to a number (in casu, 120).
-var strip_px = /px$/;
-function parse_size(size) {
-    return parseInt(size.replace(strip_px, ''), 10);
-}
-
-var suite = new Y.Test.Suite("Inline Editor Tests");
-
-suite.add(new Y.Test.Case({
-
-    name: 'inline_editor_basics',
-
-    setUp: function() {
-        this.editor = make_editor();
-    },
-
-    tearDown: function() {
-        cleanup_widget(this.editor);
-    },
-
-    test_input_value_set_during_sync: function() {
-        /* The input element's value should be set during the syncUI()
-         * call.
-         */
-        var ed = this.editor,
-            desired_value = 'x';
-
-        Assert.areNotEqual(
-            desired_value,
-            ed.get('value'),
-            "Sanity check: the editor's value shouldn't equal our " +
-            "desired value.");
-        Assert.isFalse(
-            ed.get('rendered'),
-            "Sanity check: the widget shouldn't be rendered yet.");
-
-        ed.set('value', desired_value);
-        ed.render();
-        Assert.areEqual(
-            desired_value,
-            ed.get('input_field').get('value'),
-            "The editor's input field's value should have been set.");
-    },
-
-    test_getInput_method: function() {
-        this.editor.render();
-        Assert.areEqual(
-            this.editor.get('input_field').get('value'),
-            this.editor.getInput(),
-            "The getInput() method should return the same value as " +
-            "the editor's input field's current value.");
-    },
-
-    test_validate_values: function() {
-        Assert.isFalse(this.editor.get('accept_empty'),
-            "The editor shouldn't accept empty values by default.");
-
-        var prev = this.editor.get('value');
-        this.editor.set('value', null);
-        Assert.areEqual(
-            prev,
-            this.editor.get('value'),
-            "The editor's value should not have changed.");
-
-        this.editor.set('value', '');
-        Assert.areEqual(
-            prev,
-            this.editor.get('value'),
-            "The editor should not accept the empty string as a " +
-            "value if 'accept_empty' is false.");
-
-        /* The control can be asked to accept empty values. */
-        this.editor.set('accept_empty', true);
-        this.editor.set('value', '');
-        Assert.areEqual(
-            '',
-            this.editor.get('value'),
-            "The editor should have accepted the empty string as a " +
-            "valid value if 'accept_empty' is true.");
-    },
-
-    test_validate_empty_editor_input: function() {
-        var ed = this.editor;
-
-        // A helper to catch the 'save' event.
-        var got_save = false;
-        var after_save = function(ev) { got_save = true; };
-        ed.after('ieditor:save', after_save);
-
-        ed.render();
-
-        Assert.isFalse(ed.hasErrors(),
-            "Sanity check: the editor shouldn't be displaying any " +
-            "errors.");
-        Assert.isFalse(ed.get('accept_empty'),
-            "Sanity check: the editor shouldn't accept empty inputs.");
-
-        ed.get('input_field').set('value', '');
-        ed.save();
-
-        Assert.isTrue(ed.hasErrors(),
-            "The editor should be displaying an error after the " +
-            "trying to save an empty input.");
-        Assert.isFalse(got_save,
-            "The editor should not have fired a 'save' event.");
-    },
-
-    test_set_and_clear_error_message: function() {
-        this.editor.render();
-
-        var ed       = this.editor,
-            edisplay = ed.get('error_message'),
-            c_hidden   = 'yui3-ieditor-errors-hidden';
-
-        Assert.isNotNull(
-            edisplay,
-            "The editor should have a valid error display node.");
-
-        Assert.isTrue(
-            edisplay.hasClass(c_hidden),
-            "The error display should start out hidden.");
-        Assert.isFalse(
-            ed.get("in_error"),
-            "The editor's 'in_error' attribute should not be set.");
-
-        var msg = "An error has occured.";
-        ed.showError(msg);
-
-        Assert.areEqual(
-            msg,
-            edisplay.get('text'),
-            "The error display's text should be set.");
-        Assert.isFalse(
-            edisplay.hasClass(c_hidden),
-            "The error display should be visible when an error is set.");
-        Assert.isTrue(
-            ed.hasErrors(),
-            "The editor .hasErrors() method should return true if " +
-            "there are errors being displayed.");
-        Assert.isTrue(
-            ed.get("in_error"),
-            "The editor's 'in_error' attribute should be set.");
-
-        ed.clearErrors();
-        Assert.isTrue(
-            edisplay.hasClass(c_hidden),
-            "The error display should be hidden when the error " +
-            "is cleared.");
-        Assert.isFalse(
-            ed.hasErrors(),
-            "The editor .hasErrors() method should return false " +
-            "if there are no errors being displayed.");
-    },
-
-    test_save_input_to_editor: function() {
-        var expected_value = 'abc',
-            ed = this.editor;
-
-        Assert.areNotEqual(
-            expected_value,
-            ed.get('value'),
-            "Sanity check");
-
-        ed.render();
-        ed.get('input_field').set('value', expected_value);
-        ed.save();
-
-        Assert.areEqual(
-            expected_value,
-            ed.get('value'),
-            "The value of the editor's input field should have been " +
-            "saved to the editor's 'value' attribute.");
-    },
-
-    test_focus_method_focuses_editor_input: function() {
-        this.editor.render();
-
-        var input = this.editor.get('input_field'),
-            test = this,
-            focused = false;
-
-        Y.on('focus', function() {
-            focused = true;
-        }, input);
-
-        this.editor.focus();
-
-        Assert.isTrue(focused,
-            "The editor's input field should have received focus " +
-            "after calling the editor's focus method.");
-    },
-
-    test_input_receives_focus_after_editor_errors: function() {
-        this.editor.render();
-
-        var ed = this.editor,
-            input = this.editor.get('input_field'),
-            got_focus = false;
-
-        Assert.isFalse(
-            ed.get('in_error'),
-            "Sanity check: the editor should be clear of errors.");
-        Assert.isFalse(
-            ed.get('accept_empty'),
-            "Sanity check: the editor should not accept empty " +
-            "values.");
-
-        // Force an error by setting the editor's input to the
-        // empty string.
-        input.set('value', '');
-
-        var test = this;
-        // Add our focus event listener.
-        Y.on('focus', function() {
-            got_focus = true;
-        }, input);
-
-        ed.save();
-        Assert.isTrue(
-            ed.get('in_error'),
-            "Sanity check: the editor should be in an error state " +
-            "after saving an empty value.");
-
-        Assert.isTrue(
-            got_focus,
-            "The editor's input field should have the current " +
-            "focus.");
-    },
-
-    test_widget_has_a_disabled_tabindex_when_focused: function() {
-        // The tabindex attribute appears when the widget is focused.
-        this.editor.render();
-        this.editor.focus();
-
-        // Be aware that in IE, get('tabIndex') and getAttribute('tabIndex')
-        // return different values when set to -1. This is due to YUI's
-        // getAttribute() calling dom_node.getAttribute('tabIndex', 2), which
-        // is an IE extension.
-        // http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
-        Assert.areEqual(
-            -1,
-            this.editor.get('boundingBox').get('tabIndex'),
-            "The widget should have a tabindex of -1 (disabled).");
-    },
-
-    test_enter_key_saves_input: function() {
-        this.editor.render();
-
-        var ed = this.editor,
-            input_element = Y.Node.getDOMNode(
+/* Copyright (c) 2012, Canonical Ltd. All rights reserved. */
+
+YUI.add('lp.inline_edit.test', function (Y) {
+
+    var tests = Y.namespace('lp.inline_edit.test');
+    tests.suite = new Y.Test.Suite('inline_edit Tests');
+    var Assert = Y.Assert;  // For easy access to isTrue(), etc.
+
+    var SAMPLE_HTML = [
+        '<h1>Single-line editing</h1>',
+        ' <div id="editable_single_text">',
+        ' <span id="single_text"',
+        '  class="yui3-editable_text-text">Some editable inline text.</span>',
+        ' <button id="single_edit" class="yui3-editable_text-trigger">',
+        '  Edit this</button>',
+        ' </div>',
+        ' <hr />',
+        ' <h1>Multi-line editing</h1>',
+        ' <div id="editable_multi_text">',
+        ' <button id="multi_edit" class="yui3-editable_text-trigger">',
+        '  Edit this</button>',
+        ' <span id="multi_text" class="yui3-editable_text-text">',
+        ' <p>Some editable multi-line text.</p></span>',
+        ' </div>'
+    ].join('');
+
+
+    /* Helper to stamp a Node with an ID attribute.  Needed for YUI 2.X
+     * testing, which is heavily ID-based.
+     *
+     * Returns the node's 'id' attribute.
+     */
+    function id_for(node) {
+        if (!node.getAttribute('id')) {
+            var id = Y.stamp(node);
+            node.setAttribute('id', id);
+        }
+        return node.getAttribute('id');
+    }
+
+    /*
+     * A wrapper for the Y.Event.simulate() function.  The wrapper accepts
+     * CSS selectors and Node instances instead of raw nodes.
+     */
+    function simulate(selector, evtype) {
+        var rawnode = Y.Node.getDOMNode(Y.one(selector));
+        Y.Event.simulate(rawnode, evtype);
+    }
+
+    /* Helper function that creates a new editor instance. */
+    function make_editor(cfg) {
+        return new Y.InlineEditor(cfg);
+    }
+
+    /* Helper function to clean up a dynamically added widget instance. */
+    function cleanup_widget(widget) {
+        // Nuke the boundingBox, but only if we've touched the DOM.
+        if (widget.get('rendered')) {
+            var bb = widget.get('boundingBox');
+            if (bb && Y.Node.getDOMNode(bb)) {
+                var parentNode = bb.get('parentNode');
+                if (parentNode && Y.Node.getDOMNode(parentNode)) {
+                    parentNode.removeChild(bb);
+                }
+            }
+        }
+        // Kill the widget itself.
+        widget.destroy();
+    }
+
+    function setup_sample_html() {
+        if (! Y.one("#scaffolding")) {
+            Y.one(document.body).appendChild(
+                Y.Node.create("<div id='scaffolding'></div>"));
+        }
+
+        Y.one("#scaffolding").set("innerHTML", SAMPLE_HTML);
+    }
+
+    function make_editable_text(cfg) {
+        // For the editor
+        // TODO: fix this ugly hack
+        var defaults = {
+            contentBox: '#editable_single_text',
+            boundingBox: '#inline-edit-container'
+        };
+        return new Y.EditableText(Y.merge(defaults, cfg));
+    }
+
+    // Helper: convert size specification like "120px" to a number (in casu,
+    // 120).
+    var strip_px = /px$/;
+    function parse_size(size) {
+        return parseInt(size.replace(strip_px, ''), 10);
+    }
+
+    tests.suite.add(new Y.Test.Case({
+        name: 'inline_editor_basics',
+
+        setUp: function() {
+            this.editor = make_editor();
+        },
+
+        tearDown: function() {
+            cleanup_widget(this.editor);
+        },
+        test_library_exists: function () {
+            Y.Assert.isObject(Y.InlineEditor,
+                "We should be able to locate the lp.${LIBRARY} module");
+        },
+
+        test_input_value_set_during_sync: function() {
+            /* The input element's value should be set during the syncUI()
+             * call.
+             */
+            var ed = this.editor,
+                desired_value = 'x';
+
+            Assert.areNotEqual(
+                desired_value,
+                ed.get('value'),
+                "Sanity check: the editor's value shouldn't equal our " +
+                "desired value.");
+            Assert.isFalse(
+                ed.get('rendered'),
+                "Sanity check: the widget shouldn't be rendered yet.");
+
+            ed.set('value', desired_value);
+            ed.render();
+            Assert.areEqual(
+                desired_value,
+                ed.get('input_field').get('value'),
+                "The editor's input field's value should have been set.");
+        },
+
+        test_getInput_method: function() {
+            this.editor.render();
+            Assert.areEqual(
+                this.editor.get('input_field').get('value'),
+                this.editor.getInput(),
+                "The getInput() method should return the same value as " +
+                "the editor's input field's current value.");
+        },
+
+        test_validate_values: function() {
+            Assert.isFalse(this.editor.get('accept_empty'),
+                "The editor shouldn't accept empty values by default.");
+
+            var prev = this.editor.get('value');
+            this.editor.set('value', null);
+            Assert.areEqual(
+                prev,
+                this.editor.get('value'),
+                "The editor's value should not have changed.");
+
+            this.editor.set('value', '');
+            Assert.areEqual(
+                prev,
+                this.editor.get('value'),
+                "The editor should not accept the empty string as a " +
+                "value if 'accept_empty' is false.");
+
+            /* The control can be asked to accept empty values. */
+            this.editor.set('accept_empty', true);
+            this.editor.set('value', '');
+            Assert.areEqual(
+                '',
+                this.editor.get('value'),
+                "The editor should have accepted the empty string as a " +
+                "valid value if 'accept_empty' is true.");
+        },
+
+        test_validate_empty_editor_input: function() {
+            var ed = this.editor;
+
+            // A helper to catch the 'save' event.
+            var got_save = false;
+            var after_save = function(ev) { got_save = true; };
+            ed.after('ieditor:save', after_save);
+
+            ed.render();
+
+            Assert.isFalse(ed.hasErrors(),
+                "Sanity check: the editor shouldn't be displaying any " +
+                "errors.");
+            Assert.isFalse(ed.get('accept_empty'),
+                "Sanity check: the editor shouldn't accept empty inputs.");
+
+            ed.get('input_field').set('value', '');
+            ed.save();
+
+            Assert.isTrue(ed.hasErrors(),
+                "The editor should be displaying an error after the " +
+                "trying to save an empty input.");
+            Assert.isFalse(got_save,
+                "The editor should not have fired a 'save' event.");
+        },
+
+        test_set_and_clear_error_message: function() {
+            this.editor.render();
+
+            var ed       = this.editor,
+                edisplay = ed.get('error_message'),
+                c_hidden   = 'yui3-ieditor-errors-hidden';
+
+            Assert.isNotNull(
+                edisplay,
+                "The editor should have a valid error display node.");
+
+            Assert.isTrue(
+                edisplay.hasClass(c_hidden),
+                "The error display should start out hidden.");
+            Assert.isFalse(
+                ed.get("in_error"),
+                "The editor's 'in_error' attribute should not be set.");
+
+            var msg = "An error has occured.";
+            ed.showError(msg);
+
+            Assert.areEqual(
+                msg,
+                edisplay.get('text'),
+                "The error display's text should be set.");
+            Assert.isFalse(
+                edisplay.hasClass(c_hidden),
+                "The error display should be visible when an error is set.");
+            Assert.isTrue(
+                ed.hasErrors(),
+                "The editor .hasErrors() method should return true if " +
+                "there are errors being displayed.");
+            Assert.isTrue(
+                ed.get("in_error"),
+                "The editor's 'in_error' attribute should be set.");
+
+            ed.clearErrors();
+            Assert.isTrue(
+                edisplay.hasClass(c_hidden),
+                "The error display should be hidden when the error " +
+                "is cleared.");
+            Assert.isFalse(
+                ed.hasErrors(),
+                "The editor .hasErrors() method should return false " +
+                "if there are no errors being displayed.");
+        },
+
+        test_save_input_to_editor: function() {
+            var expected_value = 'abc',
+                ed = this.editor;
+
+            Assert.areNotEqual(
+                expected_value,
+                ed.get('value'),
+                "Sanity check");
+
+            ed.render();
+            ed.get('input_field').set('value', expected_value);
+            ed.save();
+
+            Assert.areEqual(
+                expected_value,
+                ed.get('value'),
+                "The value of the editor's input field should have been " +
+                "saved to the editor's 'value' attribute.");
+        },
+
+        test_focus_method_focuses_editor_input: function() {
+            this.editor.render();
+
+            var input = this.editor.get('input_field'),
+                test = this,
+                focused = false;
+
+            Y.on('focus', function() {
+                focused = true;
+            }, input);
+
+            this.editor.focus();
+
+            Assert.isTrue(focused,
+                "The editor's input field should have received focus " +
+                "after calling the editor's focus method.");
+        },
+
+        test_input_receives_focus_after_editor_errors: function() {
+            this.editor.render();
+
+            var ed = this.editor,
+                input = this.editor.get('input_field'),
+                got_focus = false;
+
+            Assert.isFalse(
+                ed.get('in_error'),
+                "Sanity check: the editor should be clear of errors.");
+            Assert.isFalse(
+                ed.get('accept_empty'),
+                "Sanity check: the editor should not accept empty " +
+                "values.");
+
+            // Force an error by setting the editor's input to the
+            // empty string.
+            input.set('value', '');
+
+            var test = this;
+            // Add our focus event listener.
+            Y.on('focus', function() {
+                got_focus = true;
+            }, input);
+
+            ed.save();
+            Assert.isTrue(
+                ed.get('in_error'),
+                "Sanity check: the editor should be in an error state " +
+                "after saving an empty value.");
+
+            Assert.isTrue(
+                got_focus,
+                "The editor's input field should have the current " +
+                "focus.");
+        },
+
+        test_widget_has_a_disabled_tabindex_when_focused: function() {
+            // The tabindex attribute appears when the widget is focused.
+            this.editor.render();
+            this.editor.focus();
+
+            // Be aware that in IE, get('tabIndex') and
+            // getAttribute('tabIndex') return different values when set to
+            // -1. This is due to YUI's getAttribute() calling
+            // dom_node.getAttribute('tabIndex', 2), which is an IE extension.
+            // http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
+            Assert.areEqual(
+                -1,
+                this.editor.get('boundingBox').get('tabIndex'),
+                "The widget should have a tabindex of -1 (disabled).");
+        },
+
+        test_enter_key_saves_input: function() {
+            this.editor.render();
+
+            var ed = this.editor,
+                input_element = Y.Node.getDOMNode(
+                    this.editor.get('input_field'));
+
+            input_element.value = 'abc';
+
+            // A helper to flag the 'save' event.
+            var saved = false;
+            function saveCheck(e) {
+                saved = true;
+            }
+
+            ed.after('ieditor:save', saveCheck, this);
+
+            // Simulate an 'Enter' key event in the editor's input field.
+            Y.Event.simulate(input_element, "keydown", { keyCode: 13 });
+
+            Assert.isFalse(ed.hasErrors());
+            Assert.isTrue(saved,
+                "Pressing the 'Enter' key inside the editor's input field " +
+                "should save the input.");
+        },
+
+        test_enter_key_ignored_in_multiline: function() {
+            this.editor.set('multiline', true);
+            this.editor.render();
+
+            var ed = this.editor;
+            var input_element = Y.Node.getDOMNode(
                 this.editor.get('input_field'));
 
-        input_element.value = 'abc';
-
-        // A helper to flag the 'save' event.
-        var saved = false;
-        function saveCheck(e) {
-            saved = true;
-        }
-
-        ed.after('ieditor:save', saveCheck, this);
-
-        // Simulate an 'Enter' key event in the editor's input field.
-        Y.Event.simulate(input_element, "keydown", { keyCode: 13 });
-
-        Assert.isFalse(ed.hasErrors());
-        Assert.isTrue(saved,
-            "Pressing the 'Enter' key inside the editor's input field " +
-            "should save the input.");
-    },
-
-    test_enter_key_ignored_in_multiline: function() {
-        this.editor.set('multiline', true);
-        this.editor.render();
-
-        var ed = this.editor;
-        var input_element = Y.Node.getDOMNode(this.editor.get('input_field'));
-
-        input_element.value = 'abc';
-
-        // A helper to flag the 'save' event.
-        var saved = false;
-        function saveCheck(e) {
-            saved = true;
-        }
-
-        ed.after('ieditor:save', saveCheck, this);
-
-        // Simulate an 'Enter' key event in the editor's input field.
-        Y.Event.simulate(input_element, "keydown", { keyCode: 13 });
-
-        // Restore to previous state.
-        this.editor.set('multiline', false);
-
-        Assert.isFalse(ed.hasErrors());
-        Assert.isFalse(saved,
-            "Pressing the 'Enter' key in multiline mode " +
-            "should not trigger a save.");
-    },
-
-    test_input_should_be_trimmed_of_whitespace: function() {
-        this.editor.render();
-
-        var input = this.editor.get('input_field');
-
-        // Set a whitespace value as the input.
-        input.set('value', '  ');
-
-        this.editor.save();
-
-        Assert.isTrue(
-            this.editor.hasErrors(),
-            "The editor should be displaying an error after trying to " +
-            "save a whitespace value.");
-    }
-}));
-
-suite.add(new Y.Test.Case({
-    name: 'Initial value',
-
-    setUp: function() {
-        this.editor = make_editor({initial_value_override: 'Initial value'});
-    },
-
-    tearDown: function() {
-        cleanup_widget(this.editor);
-    },
-
-    test_initial_value_override: function() {
-        this.editor.render();
-        Assert.areEqual(
-            'Initial value',
-            this.editor.get('input_field').get('value'),
-            "The editor's input field should have the initial value.");
-    }
-}));
-
-suite.add(new Y.Test.Case({
-    name: 'Editable text initial values',
-
-    setUp: function() {
-        setup_sample_html();
-        this.etext = make_editable_text(
-            {initial_value_override: 'Initial value'});
-    },
-
-    tearDown: function() {
-        // Reset the <span>.
-        cleanup_widget(this.etext);
-    },
-
-    test_save_initial_value_override: function() {
-        this.etext.render();
-
-        Assert.areEqual(
-            'Initial value',
-            this.etext.editor.get('input_field').get('value'),
-            "The input_field should have been set to the initial value.");
-
-        this.etext.editor.save();
-        Assert.areEqual(
-            'Initial value',
-            this.etext.editor.get('value'),
-            "The editor's initial value did not get saved.");
-        Assert.areEqual(
-            null,
-            this.etext.editor.get('initial_value_override'),
-            "The editor's initial_value_override should be null.");
-    },
-
-    test_cancel_does_not_modify_value: function() {
-        this.etext.render();
-
-        Assert.areEqual(
-            'Some editable inline text.',
-            this.etext.editor.get('value'),
-            "The editor's value is not what it should be.");
-        Assert.areEqual(
-            'Initial value',
-            this.etext.editor.get('initial_value_override'),
-            "The editor's initial_value_override is not what it should be.");
-
-        this.etext.editor.cancel();
-        Assert.areEqual(
-            'Some editable inline text.',
-            this.etext.editor.get('value'),
-            "The editor's value did not get reset.");
-        Assert.areEqual(
-            'Initial value',
-            this.etext.editor.get('initial_value_override'),
-            "The editor's initial_value_override did not get preserved.");
-    }
-}));
-
-suite.add(new Y.Test.Case({
-
-    name: "Inline editor input sizing for a positive size value",
-
-    setUp: function() {
-        this.expected_size = 32;
-        this.editor = make_editor({size: this.expected_size});
-    },
-
-    tearDown: function() {
-        cleanup_widget(this.editor);
-    },
-
-    test_editor_size_attribute_matches_user_value: function() {
-        Assert.areEqual(
-            this.editor.get('size'),
-            this.expected_size,
-            "The editor's 'size' attribute should match the user's " +
-            "specified size.");
-    },
-
-    test_input_field_size_matches_the_editor_size: function() {
-        this.editor.render();
-        var input = this.editor.get('input_field');
-        Assert.areEqual(
-            this.expected_size + 'px',
-            input.getStyle('width'),
-            "The editor's input field size should have been set from the " +
-            "'size' attribute.");
-    }
-
-}));
-
-suite.add(new Y.Test.Case({
-
-    name: "Inline editor input sizing for a null size value",
-
-    setUp: function() {
-        this.editor = make_editor();
-        this.editor.render();
-    },
-
-    tearDown: function() {
-        cleanup_widget(this.editor);
-    },
-
-    test_editor_size_attribute_is_null: function() {
-        Assert.areEqual(
-            null,
-            this.editor.get('size'),
-            "The editor's 'size' attribute should default to 'null'.");
-    },
-
-    test_editor_input_has_browser_default_size: function() {
-        var input = this.editor.get('input_field');
-        Assert.isFalse(
-            input.hasAttribute('size'),
-            "The editor's input field should have the browser default " +
-            "size if the editor's size is 'null'.");
-    }
-}));
-
-/*
- * XXX mars 20090206
- *
- * The following test is just for the attribute validators.  Most of this is
- * made necessary because YUI doesn't publish attribute validation errors.
- *
- * See ticket http://yuilibrary.com/projects/yui3/ticket/2525946
- */
-suite.add(new Y.Test.Case({
-
-    name: "Inline editor size attribute validation",
-
-    setUp: function() {
-        this.initial_size = null;
-        this.editor = make_editor({size: this.initial_size});
-    },
-
-    test_editor_accepts_null_as_size: function() {
-        this.editor.set('size', null);
-        Assert.areEqual(
-            null,
-            this.editor.get('size'),
-            "The editor should accept a null value for the size attribute.");
-    },
-
-    test_editor_accepts_positive_numbers_as_size: function() {
-        this.editor.set('size', 123);
-        Assert.areEqual(
-            123,
-            this.editor.get('size'),
-            "The editor should accept a positive number as a valid size.");
-    },
-
-    test_editor_rejects_negative_numbers_for_size: function() {
-        this.editor.set('size', -2);
-        Assert.areEqual(
-            this.initial_size,
-            this.editor.get('size'),
-            "The editor should not accept negative numbers for its size.");
-    },
-
-    test_editor_rejects_characters_for_size: function() {
-        this.editor.set('size', 'a');
-        Assert.areEqual(
-            this.initial_size,
-            this.editor.get('size'),
-            "The editor should not accept strings for its size.");
-    }
-}));
-
-
-suite.add(new Y.Test.Case({
-
-    name: 'editor_save_state_change',
-
-    setUp: function() {
-        this.editor = make_editor();
-    },
-
-    tearDown: function() {
-        cleanup_widget(this.editor);
-    },
-
-    test_ui_initial_state_is_not_waiting: function() {
-        this.editor.render();
-        Assert.isFalse(
-            this.editor.get('boundingBox').hasClass('yui3-ieditor-waiting'),
-            "The editor UI should not start out in the 'waiting' state.");
-    },
-
-    test_set_ui_waiting_state: function() {
-        var ed = this.editor;
-        ed.render();
-
-        ed._uiSetWaiting();
-
-        Assert.isTrue(
-            ed.get('input_field').get('disabled'),
-            "The editor's input should be disabled while in the " +
-            "'waiting' state.");
-        Assert.isTrue(
-            ed.get('boundingBox').hasClass('yui3-ieditor-waiting'),
-            "The editor's UI should reflect the 'waiting' state " +
-            "with an appropriate class.");
-    },
-
-    test_clear_ui_waiting_state: function() {
-        var ed = this.editor;
-        ed.render();
-
-        ed._uiSetWaiting();
-        ed._uiClearWaiting();
-
-        Assert.isFalse(
-            ed.get('input_field').get('disabled'),
-            "The editor's input should be re-enabled when clearing " +
-            "the 'waiting' state.");
-        Assert.isFalse(
-            ed.get('boundingBox').hasClass('yui3-ieditor-waiting'),
-            "The editor's UI should have the 'waiting' state " +
-            "class removed.");
-    }
-}));
-
-
-suite.add(new Y.Test.Case({
-
-    name: 'editable_text',
-
-    setUp: function() {
-        setup_sample_html();
-        this.etext = make_editable_text();
-    },
-
-    tearDown: function() {
-        cleanup_widget(this.etext);
-    },
-
-    test_initial_values_from_DOM: function() {
-        Assert.areEqual(
-            Y.one("#single_text"),
-            this.etext.get('text'),
-            "The editor's text node should have been set from the " +
-            "DOM.");
-
-        Assert.areEqual(
-            Y.one('#single_edit'),
-            this.etext.get('trigger'),
-            "The editor's trigger node should have been set from " +
-            "the DOM.");
-
-        Assert.areEqual(
-            'Some editable inline text.',
-            this.etext.editor.get('value'),
-            "The editor's initial value should be set from it's " +
-            "text node.");
-
-        Assert.areEqual(
-            this.etext.editor.get('value'),
-            this.etext.get('value'),
-            "The editable text's value should be the same as the " +
-            "editor's.");
-    },
-
-    test_show: function() {
-        /* The show() method should display the editor, and hide the
-         * existing contents.
-         */
-        this.etext.render();
-        this.etext.show_editor();
-        Assert.isTrue(this.etext.editor.get('visible'),
-            "The editor's 'visible' attribute should be true.");
-    },
-
-    test_hide: function() {
-        /* The hide() method should hide the editor, and display the
-         * original contents.
-         */
-        this.etext.render();
-        this.etext.show_editor();
-        this.etext.hide_editor();
-        Assert.isFalse(this.etext.editor.get('visible'),
-            "The editor's 'visible' attribute should be False.");
-    },
-
-    test_trigger_edit: function() {
-        /* Clicking on the editable text's "Edit" button should
-         * make the editor visible.
-         */
-        Assert.isFalse(this.etext.editor.get('visible'),
-            "Sanity check, the editor should be hidden.");
-
-        this.etext.render();
-        simulate('#single_edit', 'click');
-
-        Assert.isTrue(this.etext.editor.get('visible'),
-            "The editor should be visible.");
-    },
-
-    test_text_is_updated_to_saved_value: function() {
-        this.etext.render();
-
-        // Grab the normalized text.
-        var expected_value = 'abc';
-
-        Assert.areNotEqual(
-            expected_value,
-            this.etext.get('value'),
-            "Sanity check");
-
-        simulate('#single_edit', 'click');
-        this.etext.editor
-            .get('input_field')
-            .set('value', expected_value);
-
-        this.etext.editor.save();
-
-        Assert.areEqual(
-            expected_value,
-            this.etext.editor.get('value'),
-            "Sanity check: the editor's value should have been " +
-            "saved.");
-
-        Assert.areEqual(
-            expected_value,
-            this.etext.get('value'),
-            "The editable text's current value should be updated " +
-            "after saving some new text in the editor.");
-    },
-
-    test_text_is_escaped: function() {
-        this.etext.render();
-
-        var input_value = '<i>l33t inject0r d00d</i> 0wnz y00';
-        var shown_value = '&lt;i&gt;l33t inject0r d00d&lt;/i&gt; 0wnz y00';
-
-        simulate('#single_edit', 'click');
-        this.etext.editor.setInput(input_value);
-        this.etext.editor.save();
-
-        Assert.areEqual(
-            shown_value,
-            this.etext.get('text').get('innerHTML'),
-            "Input text should be escaped before being inserted in HTML.");
-        Assert.areEqual(
-            input_value,
-            this.etext.editor.getInput(),
-            "Input text should be retained verbatim.");
-    },
-
-    test_accept_empty_attribute_passthrough: function() {
-        var et = this.etext;
-
-        Assert.areEqual(
-            et.get('accept_empty'),
-            et.editor.get('accept_empty'),
-            "The editor and inline editor's 'accept_empty " +
-            "should start out the same.");
-
-        et.set('accept_empty', true);
-        Assert.isTrue(
-            et.editor.get('accept_empty'),
-            "The inline editor's 'accept_empty' attribute should " +
-            "also be set to 'true'.");
-        Assert.isTrue(
-            et.get('accept_empty'),
-            "The editor's 'accept_empty' attribute should be true.");
-
-        et.set('accept_empty', false);
-        Assert.isFalse(
-            et.get('accept_empty'),
-            "The editor's 'accept_empty' attribute should be false.");
-        Assert.isFalse(
-            et.editor.get('accept_empty'),
-            "The inline editor's 'accept_empty' attribute should " +
-            "also be set to 'false'.");
-    },
-
-    test_widget_has_a_disabled_tabindex_when_focused: function() {
-        // The tabindex attribute appears when the widget is focused.
-        this.etext.render();
-        this.etext.focus();
-
-        // Be aware that in IE, get('tabIndex') and getAttribute('tabIndex')
-        // return different values when set to -1. This is due to YUI's
-        // getAttribute() calling dom_node.getAttribute('tabIndex', 2), which
-        // is an IE extension.
-        // http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
-
-        // On IE and KHTML, EditableText._onRender() will prevent the
-        // default widget rendering that would set the tabIndex on the
-        // boundingBox, so this test will fail for those browsers.
-        Assert.areEqual(
-            -1,
-            this.etext.get('boundingBox').get('tabIndex'),
-            "The widget should have a tabindex of -1 (disabled).");
-    },
-
-    test_trigger_is_disabled_if_the_widget_is_not_rendered: function() {
-        var trigger = this.etext.get('trigger');
-        Assert.isInstanceOf(
-            Y.Node, trigger,
-            "Sanity check: the editor's trigger should be a valid node.");
-        Assert.isFalse(
-            this.etext.get('rendered'),
-            "Sanity check: the editor should not be rendered.");
-
-        simulate(trigger, 'click');
-        // Peek inside the box a bit, and check that the nested editor
-        // instance is still invisible.  Assume that if it is, then
-        // the show_editor() method was never called.
-        Assert.isFalse(
-            this.etext.editor.get('visible'),
-            "Triggering an unrendered editor should not display the widget.");
-    }
-}));
-
-suite.add(new Y.Test.Case({
-
-    name: "EditableText single-line/multi-line modes",
-
-    setUp: function() {
-        setup_sample_html();
-        this.single = make_editable_text({
-            contentBox: '#editable_single_text',
-            multiline: false
-        });
-        this.single.render();
-        this.single.show_editor();
-        this.multi = make_editable_text({
-            contentBox: '#editable_multi_text',
-            multiline: true
-        });
-        this.multi.render();
-        this.multi.show_editor();
-    },
-
-    tearDown: function() {
-        cleanup_widget(this.single);
-        cleanup_widget(this.multi);
-    },
-
-    test_multi_line_has_larger_minimum: function() {
-        var single = this.single.editor;
-        var multi = this.multi.editor;
-
-        single.setInput('');
-        multi.setInput('');
-
-        var single_height = single.get('input_field').getStyle('height');
-        var multi_height = multi.get('input_field').getStyle('height');
-
-        single_height = parse_size(single_height);
-        multi_height = parse_size(multi_height);
-
-        Assert.areNotEqual(
-            multi_height,
-            single_height,
-            "Multi-line and single-line should have different sizes.");
-        Assert.isTrue(
-            multi_height > single_height,
-            "Multi-line editor should start out larger.");
-    },
-
-    test_single_line_top_button_box: function() {
-        var box = this.single.editor.get("top_buttons");
-        Assert.areEqual(
-            null,
-            box,
-            "Single-line editor should not have a top button box.");
-    },
-
-    test_multi_line_top_button_box: function() {
-        var box = this.multi.editor.get("top_buttons");
-        Assert.areNotEqual(
-            null,
-            box,
-            "Multi-line editor should have a top button box.");
-    }
-}));
-
-suite.add(new Y.Test.Case({
-    name: "EditableText text value",
-
-    setUp: function() {
-        setup_sample_html();
-        this.multi = make_editable_text({
-
-            contentBox: '#editable_multi_text',
-            multiline: true
-        });
-        this.multi.render();
-    },
-
-    tearDown: function() {
-        cleanup_widget(this.multi);
-    },
-
-    test_text_value_no_trailing_newlines: function() {
-        var text = this.multi.get('value');
-        Assert.areEqual(
-           "Some editable multi-line text.",
-           text,
-           "The editor kills trailing whitespace.");
-    }
-}));
-
-function FailedSavePlugin() {
-  FailedSavePlugin.superclass.constructor.apply(this, arguments);
-}
-
-FailedSavePlugin.NAME = 'failedsave';
-FailedSavePlugin.NS = 'test';
-
-Y.extend(FailedSavePlugin, Y.Plugin.Base, {
-    initializer: function(config) {
-      this.doBefore("_saveData", this._altSave);
-    },
-
-    _altSave: function() {
-      var host  = this.get('host');
-      // Set the UI 'waiting' status.
-      host._uiSetWaiting();
-      host.showError("Some error occurred.");
-      // Make sure we clear the 'waiting' status.
-      host._uiClearWaiting();
-      return new Y.Do.Halt();
-    }
-  });
-
-suite.add(new Y.Test.Case({
-    name: "Edit buttons enabled on error",
-
-    setUp: function() {
-        setup_sample_html();
-        this.multi = make_editable_text({
-
-            contentBox: '#editable_multi_text',
-            multiline: true
-        });
-        this.multi.render();
-        this.multi.show_editor();
-    },
-
-    tearDown: function() {
-        cleanup_widget(this.multi);
-    },
-
-    test_error_on_save_enabled_buttons: function() {
-        var editor = this.multi.editor;
-        editor.plug({fn:FailedSavePlugin});
-        // Now saving should invoke an error.
-        editor.save();
-        Assert.isTrue(editor.get('in_error'), "Editor should be in error");
-        // Both the submit and cancel buttons should be visible.
-        Assert.areEqual(
-            'inline-block',
-            editor.get('submit_button').getStyle('display'),
-            "Submit should be set to display:inline");
-        Assert.areEqual(
-            'inline-block',
-            editor.get('cancel_button').getStyle('display'),
-            "Cancel should be set to display:inline");
-    }
-}));
-
-Y.lp.testing.Runner.run(suite);
-
+            input_element.value = 'abc';
+
+            // A helper to flag the 'save' event.
+            var saved = false;
+            function saveCheck(e) {
+                saved = true;
+            }
+
+            ed.after('ieditor:save', saveCheck, this);
+
+            // Simulate an 'Enter' key event in the editor's input field.
+            Y.Event.simulate(input_element, "keydown", { keyCode: 13 });
+
+            // Restore to previous state.
+            this.editor.set('multiline', false);
+
+            Assert.isFalse(ed.hasErrors());
+            Assert.isFalse(saved,
+                "Pressing the 'Enter' key in multiline mode " +
+                "should not trigger a save.");
+        },
+
+        test_input_should_be_trimmed_of_whitespace: function() {
+            this.editor.render();
+
+            var input = this.editor.get('input_field');
+
+            // Set a whitespace value as the input.
+            input.set('value', '  ');
+
+            this.editor.save();
+
+            Assert.isTrue(
+                this.editor.hasErrors(),
+                "The editor should be displaying an error after trying to " +
+                "save a whitespace value.");
+        }
+
+
+    }));
+
+    tests.suite.add(new Y.Test.Case({
+        name: 'Initial value',
+
+        setUp: function() {
+            this.editor = make_editor({
+                initial_value_override: 'Initial value'
+            });
+        },
+
+        tearDown: function() {
+            cleanup_widget(this.editor);
+        },
+
+        test_initial_value_override: function() {
+            this.editor.render();
+            Assert.areEqual(
+                'Initial value',
+                this.editor.get('input_field').get('value'),
+                "The editor's input field should have the initial value.");
+        }
+    }));
+
+    tests.suite.add(new Y.Test.Case({
+        name: 'Editable text initial values',
+
+        setUp: function() {
+            setup_sample_html();
+            this.etext = make_editable_text(
+                {initial_value_override: 'Initial value'});
+        },
+
+        tearDown: function() {
+            // Reset the <span>.
+            cleanup_widget(this.etext);
+        },
+
+        test_save_initial_value_override: function() {
+            this.etext.render();
+
+            Assert.areEqual(
+                'Initial value',
+                this.etext.editor.get('input_field').get('value'),
+                "The input_field should have been set to the initial value.");
+
+            this.etext.editor.save();
+            Assert.areEqual(
+                'Initial value',
+                this.etext.editor.get('value'),
+                "The editor's initial value did not get saved.");
+            Assert.areEqual(
+                null,
+                this.etext.editor.get('initial_value_override'),
+                "The editor's initial_value_override should be null.");
+        },
+
+        test_cancel_does_not_modify_value: function() {
+            this.etext.render();
+
+            Assert.areEqual(
+                'Some editable inline text.',
+                this.etext.editor.get('value'),
+                "The editor's value is not what it should be.");
+            Assert.areEqual(
+                'Initial value',
+                this.etext.editor.get('initial_value_override'),
+                "The editor's initial_value_override is not correct.");
+
+            this.etext.editor.cancel();
+            Assert.areEqual(
+                'Some editable inline text.',
+                this.etext.editor.get('value'),
+                "The editor's value did not get reset.");
+            Assert.areEqual(
+                'Initial value',
+                this.etext.editor.get('initial_value_override'),
+                "The editor's initial_value_override did not get preserved.");
+        }
+    }));
+
+    tests.suite.add(new Y.Test.Case({
+
+        name: "Inline editor input sizing for a positive size value",
+
+        setUp: function() {
+            this.expected_size = 32;
+            this.editor = make_editor({size: this.expected_size});
+        },
+
+        tearDown: function() {
+            cleanup_widget(this.editor);
+        },
+
+        test_editor_size_attribute_matches_user_value: function() {
+            Assert.areEqual(
+                this.editor.get('size'),
+                this.expected_size,
+                "The editor's 'size' attribute should match the user's " +
+                "specified size.");
+        },
+
+        test_input_field_size_matches_the_editor_size: function() {
+            this.editor.render();
+            var input = this.editor.get('input_field');
+            Assert.areEqual(
+                this.expected_size + 'px',
+                input.getStyle('width'),
+                "The editor's input field size should have been set from the " +
+                "'size' attribute.");
+        }
+
+    }));
+
+    tests.suite.add(new Y.Test.Case({
+
+        name: "Inline editor input sizing for a null size value",
+
+        setUp: function() {
+            this.editor = make_editor();
+            this.editor.render();
+        },
+
+        tearDown: function() {
+            cleanup_widget(this.editor);
+        },
+
+        test_editor_size_attribute_is_null: function() {
+            Assert.areEqual(
+                null,
+                this.editor.get('size'),
+                "The editor's 'size' attribute should default to 'null'.");
+        },
+
+        test_editor_input_has_browser_default_size: function() {
+            var input = this.editor.get('input_field');
+            Assert.isFalse(
+                input.hasAttribute('size'),
+                "The editor's input field should have the browser default " +
+                "size if the editor's size is 'null'.");
+        }
+    }));
+
+    /*
+     * XXX mars 20090206
+     *
+     * The following test is just for the attribute validators.  Most of this is
+     * made necessary because YUI doesn't publish attribute validation errors.
+     *
+     * See ticket http://yuilibrary.com/projects/yui3/ticket/2525946
+     */
+    tests.suite.add(new Y.Test.Case({
+
+        name: "Inline editor size attribute validation",
+
+        setUp: function() {
+            this.initial_size = null;
+            this.editor = make_editor({size: this.initial_size});
+        },
+
+        test_editor_accepts_null_as_size: function() {
+            this.editor.set('size', null);
+            Assert.areEqual(
+                null,
+                this.editor.get('size'),
+                "The editor should accept a null value for the size attr.");
+        },
+
+        test_editor_accepts_positive_numbers_as_size: function() {
+            this.editor.set('size', 123);
+            Assert.areEqual(
+                123,
+                this.editor.get('size'),
+                "The editor should accept a positive number as a valid size.");
+        },
+
+        test_editor_rejects_negative_numbers_for_size: function() {
+            this.editor.set('size', -2);
+            Assert.areEqual(
+                this.initial_size,
+                this.editor.get('size'),
+                "The editor should not accept negative numbers for its size.");
+        },
+
+        test_editor_rejects_characters_for_size: function() {
+            this.editor.set('size', 'a');
+            Assert.areEqual(
+                this.initial_size,
+                this.editor.get('size'),
+                "The editor should not accept strings for its size.");
+        }
+    }));
+
+
+    tests.suite.add(new Y.Test.Case({
+
+        name: 'editor_save_state_change',
+
+        setUp: function() {
+            this.editor = make_editor();
+        },
+
+        tearDown: function() {
+            cleanup_widget(this.editor);
+        },
+
+        test_ui_initial_state_is_not_waiting: function() {
+            this.editor.render();
+            Assert.isFalse(
+                this.editor.get('boundingBox').hasClass('yui3-ieditor-waiting'),
+                "The editor UI should not start out in the 'waiting' state.");
+        },
+
+        test_set_ui_waiting_state: function() {
+            var ed = this.editor;
+            ed.render();
+
+            ed._uiSetWaiting();
+
+            Assert.isTrue(
+                ed.get('input_field').get('disabled'),
+                "The editor's input should be disabled while in the " +
+                "'waiting' state.");
+            Assert.isTrue(
+                ed.get('boundingBox').hasClass('yui3-ieditor-waiting'),
+                "The editor's UI should reflect the 'waiting' state " +
+                "with an appropriate class.");
+        },
+
+        test_clear_ui_waiting_state: function() {
+            var ed = this.editor;
+            ed.render();
+
+            ed._uiSetWaiting();
+            ed._uiClearWaiting();
+
+            Assert.isFalse(
+                ed.get('input_field').get('disabled'),
+                "The editor's input should be re-enabled when clearing " +
+                "the 'waiting' state.");
+            Assert.isFalse(
+                ed.get('boundingBox').hasClass('yui3-ieditor-waiting'),
+                "The editor's UI should have the 'waiting' state " +
+                "class removed.");
+        }
+    }));
+
+
+    tests.suite.add(new Y.Test.Case({
+
+        name: 'editable_text',
+
+        setUp: function() {
+            setup_sample_html();
+            this.etext = make_editable_text();
+        },
+
+        tearDown: function() {
+            cleanup_widget(this.etext);
+        },
+
+        test_initial_values_from_DOM: function() {
+            Assert.areEqual(
+                Y.one("#single_text"),
+                this.etext.get('text'),
+                "The editor's text node should have been set from the " +
+                "DOM.");
+
+            Assert.areEqual(
+                Y.one('#single_edit'),
+                this.etext.get('trigger'),
+                "The editor's trigger node should have been set from " +
+                "the DOM.");
+
+            Assert.areEqual(
+                'Some editable inline text.',
+                this.etext.editor.get('value'),
+                "The editor's initial value should be set from it's " +
+                "text node.");
+
+            Assert.areEqual(
+                this.etext.editor.get('value'),
+                this.etext.get('value'),
+                "The editable text's value should be the same as the " +
+                "editor's.");
+        },
+
+        test_show: function() {
+            /* The show() method should display the editor, and hide the
+             * existing contents.
+             */
+            this.etext.render();
+            this.etext.show_editor();
+            Assert.isTrue(this.etext.editor.get('visible'),
+                "The editor's 'visible' attribute should be true.");
+        },
+
+        test_hide: function() {
+            /* The hide() method should hide the editor, and display the
+             * original contents.
+             */
+            this.etext.render();
+            this.etext.show_editor();
+            this.etext.hide_editor();
+            Assert.isFalse(this.etext.editor.get('visible'),
+                "The editor's 'visible' attribute should be False.");
+        },
+
+        test_trigger_edit: function() {
+            /* Clicking on the editable text's "Edit" button should
+             * make the editor visible.
+             */
+            Assert.isFalse(this.etext.editor.get('visible'),
+                "Sanity check, the editor should be hidden.");
+
+            this.etext.render();
+            simulate('#single_edit', 'click');
+
+            Assert.isTrue(this.etext.editor.get('visible'),
+                "The editor should be visible.");
+        },
+
+        test_text_is_updated_to_saved_value: function() {
+            this.etext.render();
+
+            // Grab the normalized text.
+            var expected_value = 'abc';
+
+            Assert.areNotEqual(
+                expected_value,
+                this.etext.get('value'),
+                "Sanity check");
+
+            simulate('#single_edit', 'click');
+            this.etext.editor
+                .get('input_field')
+                .set('value', expected_value);
+
+            this.etext.editor.save();
+
+            Assert.areEqual(
+                expected_value,
+                this.etext.editor.get('value'),
+                "Sanity check: the editor's value should have been " +
+                "saved.");
+
+            Assert.areEqual(
+                expected_value,
+                this.etext.get('value'),
+                "The editable text's current value should be updated " +
+                "after saving some new text in the editor.");
+        },
+
+        test_text_is_escaped: function() {
+            this.etext.render();
+
+            var input_value = '<i>l33t inject0r d00d</i> 0wnz y00';
+            var shown_value = '&lt;i&gt;l33t inject0r d00d&lt;/i&gt; 0wnz y00';
+
+            simulate('#single_edit', 'click');
+            this.etext.editor.setInput(input_value);
+            this.etext.editor.save();
+
+            Assert.areEqual(
+                shown_value,
+                this.etext.get('text').get('innerHTML'),
+                "Input text should be escaped before being inserted in HTML.");
+            Assert.areEqual(
+                input_value,
+                this.etext.editor.getInput(),
+                "Input text should be retained verbatim.");
+        },
+
+        test_accept_empty_attribute_passthrough: function() {
+            var et = this.etext;
+
+            Assert.areEqual(
+                et.get('accept_empty'),
+                et.editor.get('accept_empty'),
+                "The editor and inline editor's 'accept_empty " +
+                "should start out the same.");
+
+            et.set('accept_empty', true);
+            Assert.isTrue(
+                et.editor.get('accept_empty'),
+                "The inline editor's 'accept_empty' attribute should " +
+                "also be set to 'true'.");
+            Assert.isTrue(
+                et.get('accept_empty'),
+                "The editor's 'accept_empty' attribute should be true.");
+
+            et.set('accept_empty', false);
+            Assert.isFalse(
+                et.get('accept_empty'),
+                "The editor's 'accept_empty' attribute should be false.");
+            Assert.isFalse(
+                et.editor.get('accept_empty'),
+                "The inline editor's 'accept_empty' attribute should " +
+                "also be set to 'false'.");
+        },
+
+        test_widget_has_a_disabled_tabindex_when_focused: function() {
+            // The tabindex attribute appears when the widget is focused.
+            this.etext.render();
+            this.etext.focus();
+
+            // Be aware that in IE, get('tabIndex') and
+            // getAttribute('tabIndex') return different values when set to
+            // -1. This is due to YUI's getAttribute() calling
+            // dom_node.getAttribute('tabIndex', 2), which is an IE extension.
+            // http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
+
+            // On IE and KHTML, EditableText._onRender() will prevent the
+            // default widget rendering that would set the tabIndex on the
+            // boundingBox, so this test will fail for those browsers.
+            Assert.areEqual(
+                -1,
+                this.etext.get('boundingBox').get('tabIndex'),
+                "The widget should have a tabindex of -1 (disabled).");
+        },
+
+        test_trigger_is_disabled_if_the_widget_is_not_rendered: function() {
+            var trigger = this.etext.get('trigger');
+            Assert.isInstanceOf(
+                Y.Node, trigger,
+                "Sanity check: the editor's trigger should be a valid node.");
+            Assert.isFalse(
+                this.etext.get('rendered'),
+                "Sanity check: the editor should not be rendered.");
+
+            simulate(trigger, 'click');
+            // Peek inside the box a bit, and check that the nested editor
+            // instance is still invisible.  Assume that if it is, then
+            // the show_editor() method was never called.
+            Assert.isFalse(
+                this.etext.editor.get('visible'),
+                "Triggering an unrendered editor should not display widget.");
+        }
+    }));
+
+    tests.suite.add(new Y.Test.Case({
+
+        name: "EditableText single-line/multi-line modes",
+
+        setUp: function() {
+            setup_sample_html();
+            this.single = make_editable_text({
+                contentBox: '#editable_single_text',
+                multiline: false
+            });
+            this.single.render();
+            this.single.show_editor();
+            this.multi = make_editable_text({
+                contentBox: '#editable_multi_text',
+                multiline: true
+            });
+            this.multi.render();
+            this.multi.show_editor();
+        },
+
+        tearDown: function() {
+            cleanup_widget(this.single);
+            cleanup_widget(this.multi);
+        },
+
+        test_multi_line_has_larger_minimum: function() {
+            var single = this.single.editor;
+            var multi = this.multi.editor;
+
+            single.setInput('');
+            multi.setInput('');
+
+            var single_height = single.get('input_field').getStyle('height');
+            var multi_height = multi.get('input_field').getStyle('height');
+
+            single_height = parse_size(single_height);
+            multi_height = parse_size(multi_height);
+
+            Assert.areNotEqual(
+                multi_height,
+                single_height,
+                "Multi-line and single-line should have different sizes.");
+            Assert.isTrue(
+                multi_height > single_height,
+                "Multi-line editor should start out larger.");
+        },
+
+        test_single_line_top_button_box: function() {
+            var box = this.single.editor.get("top_buttons");
+            Assert.areEqual(
+                null,
+                box,
+                "Single-line editor should not have a top button box.");
+        },
+
+        test_multi_line_top_button_box: function() {
+            var box = this.multi.editor.get("top_buttons");
+            Assert.areNotEqual(
+                null,
+                box,
+                "Multi-line editor should have a top button box.");
+        }
+    }));
+
+    tests.suite.add(new Y.Test.Case({
+        name: "EditableText text value",
+
+        setUp: function() {
+            setup_sample_html();
+            this.multi = make_editable_text({
+
+                contentBox: '#editable_multi_text',
+                multiline: true
+            });
+            this.multi.render();
+        },
+
+        tearDown: function() {
+            cleanup_widget(this.multi);
+        },
+
+        test_text_value_no_trailing_newlines: function() {
+            var text = this.multi.get('value');
+            Assert.areEqual(
+               "Some editable multi-line text.",
+               text,
+               "The editor kills trailing whitespace.");
+        }
+    }));
+
+    function FailedSavePlugin() {
+      FailedSavePlugin.superclass.constructor.apply(this, arguments);
+    }
+
+    FailedSavePlugin.NAME = 'failedsave';
+    FailedSavePlugin.NS = 'test';
+
+    Y.extend(FailedSavePlugin, Y.Plugin.Base, {
+        initializer: function(config) {
+          this.doBefore("_saveData", this._altSave);
+        },
+
+        _altSave: function() {
+          var host  = this.get('host');
+          // Set the UI 'waiting' status.
+          host._uiSetWaiting();
+          host.showError("Some error occurred.");
+          // Make sure we clear the 'waiting' status.
+          host._uiClearWaiting();
+          return new Y.Do.Halt();
+        }
+      });
+
+    tests.suite.add(new Y.Test.Case({
+        name: "Edit buttons enabled on error",
+
+        setUp: function() {
+            setup_sample_html();
+            this.multi = make_editable_text({
+
+                contentBox: '#editable_multi_text',
+                multiline: true
+            });
+            this.multi.render();
+            this.multi.show_editor();
+        },
+
+        tearDown: function() {
+            cleanup_widget(this.multi);
+        },
+
+        test_error_on_save_enabled_buttons: function() {
+            var editor = this.multi.editor;
+            editor.plug({fn:FailedSavePlugin});
+            // Now saving should invoke an error.
+            editor.save();
+            Assert.isTrue(editor.get('in_error'), "Editor should be in error");
+            // Both the submit and cancel buttons should be visible.
+            Assert.areEqual(
+                'inline-block',
+                editor.get('submit_button').getStyle('display'),
+                "Submit should be set to display:inline");
+            Assert.areEqual(
+                'inline-block',
+                editor.get('cancel_button').getStyle('display'),
+                "Cancel should be set to display:inline");
+        }
+    }));
+
+}, '0.1', {'requires': ['test', 'console', 'lazr.editor', 'node',
+    'lp.app.formwidgets.resizing_textarea', 'event',
+    'event-simulate', 'plugin']
 });
+
+
+
+

=== modified file 'lib/lp/app/javascript/inlinehelp/tests/test_inlinehelp.html'
--- lib/lp/app/javascript/inlinehelp/tests/test_inlinehelp.html	2011-12-22 19:13:18 +0000
+++ lib/lp/app/javascript/inlinehelp/tests/test_inlinehelp.html	2012-02-06 19:47:42 +0000
@@ -1,31 +1,47 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+  "http://www.w3.org/TR/html4/strict.dtd";>
 <!--
-Copyright 2011 Canonical Ltd.  This software is licensed under the
+Copyright 2012 Canonical Ltd.  This software is licensed under the
 GNU Affero General Public License version 3 (see the file LICENSE).
 -->
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd";>
 <html>
   <head>
-    <title>Test InlineHelp </title>
-    <!-- YUI and test setup -->
-    <script type="text/javascript"
-            src="../../../../../canonical/launchpad/icing/yui/yui/yui.js">
-    </script>
-    <link rel="stylesheet" href="../../../../app/javascript/testing/test.css" />
-    <script type="text/javascript"
-            src="../../../../app/javascript/testing/testrunner.js"></script>
-
-    <script type="text/javascript" src="../../overlay/overlay.js"></script>
-
-    <!-- The module under test -->
-    <script type="text/javascript" src="../inlinehelp.js"></script>
-
-    <!-- The test suite -->
-    <script type="text/javascript" src="test_inlinehelp.js"></script>
-  </head>
-  <body class="yui3-skin-sam">
-      <ul id="suites">
-          <li>lp.app.inlinehelp.test</li>
-      </ul>
-  </body>
+      <title>Test inlinehelp</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/console/assets/skins/sam/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/overlay/overlay.js"></script>
+
+      <!-- The module under test. -->
+      <script type="text/javascript" src="../inlinehelp.js"></script>
+
+      <!-- Any css assert for this module. -->
+      <!-- <link rel="stylesheet" href="../assets/inlinehelp-core.css" /> -->
+
+      <!-- The test suite. -->
+      <script type="text/javascript" src="test_inlinehelp.js"></script>
+
+    </head>
+    <body class="yui3-skin-sam">
+        <ul id="suites">
+            <!-- <li>lp.large_indicator.test</li> -->
+            <li>lp.app.inlinehelp.test</li>
+        </ul>
+    </body>
 </html>
-

=== modified file 'lib/lp/app/javascript/inlinehelp/tests/test_inlinehelp.js'
--- lib/lp/app/javascript/inlinehelp/tests/test_inlinehelp.js	2012-01-03 16:23:11 +0000
+++ lib/lp/app/javascript/inlinehelp/tests/test_inlinehelp.js	2012-02-06 19:47:42 +0000
@@ -2,11 +2,10 @@
 
 YUI.add('lp.app.inlinehelp.test', function (Y) {
 
-    var suite = new Y.Test.Suite('InlineHelp Tests');
+    var tests = Y.namespace('lp.app.inlinehelp.test');
+    tests.suite = new Y.Test.Suite('InlineHelp Tests');
     var Assert = Y.Assert;
-    var test_module = Y.namespace('lp.app.inlinehelp.test');
-
-    suite.add(new Y.Test.Case({
+    tests.suite.add(new Y.Test.Case({
         name: 'inlinehelp.init_help',
 
         setUp: function () {
@@ -129,7 +128,6 @@
         }
     }));
 
-test_module.suite = suite;
 
 }, '0.1', {
     'requires': ['node', 'console', 'test', 'lp.app.inlinehelp',

=== modified file 'lib/lp/app/javascript/ordering/tests/test_orderby_widget.html'
--- lib/lp/app/javascript/ordering/tests/test_orderby_widget.html	2011-11-02 19:06:42 +0000
+++ lib/lp/app/javascript/ordering/tests/test_orderby_widget.html	2012-02-06 19:47:42 +0000
@@ -1,28 +1,47 @@
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
   "http://www.w3.org/TR/html4/strict.dtd";>
+<!--
+Copyright 2012 Canonical Ltd.  This software is licensed under the
+GNU Affero General Public License version 3 (see the file LICENSE).
+-->
+
 <html>
   <head>
-  <title>Order By Widget Tests</title>
-
-  <!-- YUI and test setup -->
-  <script type="text/javascript"
-          src="../../../../../canonical/launchpad/icing/yui/yui/yui.js">
-  </script>
-  <link rel="stylesheet" href="../../../../app/javascript/testing/test.css" />
-  <script type="text/javascript"
-          src="../../../../app/javascript/testing/testrunner.js"></script>
-
-  <!-- The module under test -->
-  <script type="text/javascript" src="../ordering.js"></script>
-  <link rel="stylesheet" href="../assets/ordering-core.css" />
-
-  <!-- The test suite -->
-  <script type="text/javascript" src="test_orderby_widget.js"></script>
-
-</head>
-<body class="yui3-skin-sam">
-    <ul id="suites">
-        <li>lp.orderbybar.test</li>
-    </ul>
-</body>
+      <title>Test orderbybar</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/console/assets/skins/sam/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/..."></script> -->
+
+      <!-- The module under test. -->
+      <script type="text/javascript" src="../ordering.js"></script>
+
+      <!-- Any css assert for this module. -->
+      <link rel="stylesheet" href="../assets/ordering-core.css" />
+
+      <!-- The test suite. -->
+      <script type="text/javascript" src="test_orderby_widget.js"></script>
+
+    </head>
+    <body class="yui3-skin-sam">
+        <ul id="suites">
+            <!-- <li>lp.large_indicator.test</li> -->
+            <li>lp.orderbybar.test</li>
+        </ul>
+    </body>
 </html>

=== modified file 'lib/lp/app/javascript/ordering/tests/test_orderby_widget.js'
--- lib/lp/app/javascript/ordering/tests/test_orderby_widget.js	2011-12-21 13:23:46 +0000
+++ lib/lp/app/javascript/ordering/tests/test_orderby_widget.js	2012-02-06 19:47:42 +0000
@@ -1,386 +1,380 @@
-/* Copyright (c) 2011, Canonical Ltd. All rights reserved. */
-
+/* Copyright (c) 2012, Canonical Ltd. All rights reserved. */
 YUI.add('lp.orderbybar.test', function(Y) {
 
-var basic_test = Y.namespace('lp.orderbybar.test');
-
-var suite = new Y.Test.Suite('OrderByBar Tests');
-
-var Assert = Y.Assert;
-var ArrayAssert = Y.ArrayAssert;
-
-suite.add(new Y.Test.Case({
-
-    name: 'orderbybar_widget_tests',
-
-    orderby: null,
-
-    _should: {
-        error: {
-            test_sort_order_validator:
-                new Error('sort_order must be either "asc" or "desc"'),
-            test_active_sort_validator:
-                new Error('active attribute was not found in sort_keys')
-        }
-    },
-
-    tearDown: function() {
-        if (Y.Lang.isValue(this.orderby)) {
-            this.orderby.destroy();
-        }
-    },
-
-    /**
-     * Unpack a list of key, name pairs into individual lists.
-     *
-     * [[Foo, 'Foo Item'], ['Bar', 'Bar item']] becomes
-     * ['Foo', 'Bar'] and ['Foo Item', 'Bar Item'].
-     */
-    getIdsAndNames: function(keys) {
-        var ids = [];
-        var names = [];
-        var len = keys.length;
-        var i;
-        for (i=0; i<len; i++) {
-            ids.push(keys[i][0]);
-            names.push(keys[i][1]);
-        }
-        return [ids, names];
-    },
-
-    /*
-     * Helper function to create the srcNode on the page.  Widgets
-     * will append to the body tag if srcNode is not supplied.
-     */
-    makeSrcNode: function(id) {
-        // Calling the widget's destroy method, which teardown does,
-        // will clean this up.
-        var parent_node = Y.Node.create('<div></div>');
-        parent_node.set('id', id);
-        Y.one('body').appendChild(parent_node);
-    },
-
-    test_default_sort_keys: function() {
-        // The default sort keys should exist in a newly created widget.
-        this.orderby = new Y.lp.ordering.OrderByBar();
-        var expected_sort_keys = [
-            ['bugnumber', 'Number'],
-            ['bugtitle', 'Title'],
-            ['status', 'Status'],
-            ['importance', 'Importance'],
-            ['bug-heat-icons', 'Heat'],
-            ['package', 'Package name'],
-            ['milestone', 'Milestone'],
-            ['assignee', 'Assignee'],
-            ['bug-age', 'Age']
-        ];
-        var expected = this.getIdsAndNames(expected_sort_keys);
-        var actual = this.getIdsAndNames(this.orderby.get('sort_keys'));
-        ArrayAssert.itemsAreSame(expected[0], actual[0]);
-        ArrayAssert.itemsAreSame(expected[1], actual[1]);
-    },
-
-    test_user_supplied_sort_keys: function() {
-        // Call sites can supply their own sort keys to a widget.
-        var user_supplied_sort_keys = [
-            ['foo', 'Foo item', 'asc'],
-            ['bar', 'Bar item', 'asc'],
-            ['baz', 'Baz item', 'asc']
-        ];
-        this.orderby = new Y.lp.ordering.OrderByBar({
-            sort_keys: user_supplied_sort_keys});
-        var expected = this.getIdsAndNames(user_supplied_sort_keys);
-        var actual = this.getIdsAndNames(this.orderby.get('sort_keys'));
-        ArrayAssert.itemsAreSame(expected[0], actual[0]);
-        ArrayAssert.itemsAreSame(expected[1], actual[1]);
-    },
-
-    test_rendered_items_html: function() {
-        // We should be able to get a node from the DOM via an ID
-        // created from sort keys, and the name should be used as
-        // a button display name in HTML.
-        var test_sort_keys = [
-            ['foo', 'Foo item', 'asc'],
-            ['bar', 'Bar item', 'asc']
-        ];
-        this.makeSrcNode('test-div');
-        this.orderby = new Y.lp.ordering.OrderByBar({
-            sort_keys: test_sort_keys,
-            srcNode: Y.one('#test-div'),
-            active: 'foo'
-        });
-        this.orderby.render();
-        var foo_node = Y.one('#sort-foo');
-        Assert.isNotNull(foo_node);
-        Assert.areEqual(foo_node.get('firstChild').get('text'), 'Foo item');
-        var bar_node = Y.one('#sort-bar');
-        Assert.isNotNull(bar_node);
-        Assert.areEqual(bar_node.get('firstChild').get('text'), 'Bar item');
-    },
-
-    test_render_active_sort_default: function() {
-        // Confirm that there is a default active sort class applied.
-        this.makeSrcNode('test-div');
-        this.orderby = new Y.lp.ordering.OrderByBar({
-            srcNode: Y.one('#test-div')
-        });
-        this.orderby.render();
-        var li_node = Y.one('#sort-importance');
-        Assert.isTrue(li_node.hasClass('active-sort'));
-    },
-
-    test_render_active_sort_user_supplied: function() {
-        // The active sort class is also set when "active"
-        // is supplied via config.
-        this.makeSrcNode('test-div');
-        this.orderby = new Y.lp.ordering.OrderByBar({
-            srcNode: Y.one('#test-div'),
-            active: 'status'
-        });
-        this.orderby.render();
-        var li_node = Y.one('#sort-status');
-        Assert.isTrue(li_node.hasClass('active-sort'));
-    },
-
-    test_active_sort_arrow_display_asc: function() {
-        // Buttons using "asc" order get a down arrow added to the li.
-        this.makeSrcNode('test-div');
-        this.orderby = new Y.lp.ordering.OrderByBar({
-            srcNode: Y.one('#test-div'),
-            sort_order: 'asc'
-        });
-        this.orderby.render();
-        var arrow_span = Y.one('.active-sort span');
-        var expected_text = '<span class="sprite order-ascending"></span>';
-        Assert.areEqual(expected_text, arrow_span.get('innerHTML'));
-    },
-
-    test_active_sort_arrow_display_desc: function() {
-        // Buttons using "desc" order get an up arrow added to the li.
-        this.makeSrcNode('test-div');
-        this.orderby = new Y.lp.ordering.OrderByBar({
-            srcNode: Y.one('#test-div'),
-            sort_order: 'desc'
-        });
-        this.orderby.render();
-        var arrow_span = Y.one('.active-sort span');
-        var expected_text = '<span class="sprite order-descending"></span>';
-        Assert.areEqual(expected_text, arrow_span.get('innerHTML'));
-    },
-
-    test_active_sort_click_class_change: function() {
-        // Click a node should add the active_sort class
-        // and remove that class from the previously active node.
-        this.makeSrcNode('test-div');
-        this.orderby = new Y.lp.ordering.OrderByBar({
-            srcNode: Y.one('#test-div')
-        });
-        this.orderby.render();
-        var importance_node = Y.one('#sort-importance');
-        Assert.isTrue(importance_node.hasClass('active-sort'));
-        var status_node = Y.one('#sort-status');
-        status_node.simulate('click');
-        Assert.isTrue(status_node.hasClass('active-sort'));
-    },
-
-    test_active_sort_validator: function() {
-        // This should fail because we do not allow
-        // a "active" value not found in sort_keys.
-        var test_sort_keys = [
-            ['foo', 'Foo item', 'asc'],
-            ['bar', 'Bar item', 'asc']
-        ];
-        this.orderby = new Y.lp.ordering.OrderByBar({
-            sort_keys: test_sort_keys,
-            active: 'foobarbazdonotexists'
-        });
-        this.orderby.render();
-    },
-
-    test_sort_order_validator: function() {
-        // This should fail when using a sort order
-        // other than "asc" or "desc".
-        this.orderby = new Y.lp.ordering.OrderByBar({
-            sort_order: 'foobar'
-        });
-        this.orderby.render();
-    },
-
-    test_click_current_sort_arrow_changes: function() {
-        // Clicking the currently sorted on button should change
-        // the arrow and widget state to show a sort change should
-        // happen.
-        this.makeSrcNode('test-div');
-        var test_sort_keys = [
-            ['foo', 'Foo item', 'asc'],
-            ['bar', 'Bar item', 'asc']
-        ];
-        this.orderby = new Y.lp.ordering.OrderByBar({
-            srcNode: Y.one('#test-div'),
-            sort_keys: test_sort_keys,
-            active: 'foo',
-            sort_order: 'asc'
-        });
-        this.orderby.render();
-        var foo_node = Y.one('#sort-foo');
-        var expected_starting_text =
-            '<span class="sprite order-ascending"></span>';
-        var expected_ending_text =
-            '<span class="sprite order-descending"></span>';
-        Assert.areEqual(
-            expected_starting_text, foo_node.one('span').get('innerHTML'));
-        Assert.isTrue(foo_node.one('span').hasClass('asc'));
-        foo_node.simulate('click');
-        Assert.areEqual(
-            expected_ending_text, foo_node.one('span').get('innerHTML'));
-        Assert.isTrue(foo_node.one('span').hasClass('desc'));
-    },
-
-    test_click_different_sort_arrows_change: function() {
-        // Clicking a button other than the currently sorted on button
-        // should change the arrow and widget state to show a sort
-        // change should happen.
-        this.makeSrcNode('test-div');
-        var test_sort_keys = [
-            ['foo', 'Foo item', 'asc'],
-            ['bar', 'Bar item', 'asc']
-        ];
-        this.orderby = new Y.lp.ordering.OrderByBar({
-            srcNode: Y.one('#test-div'),
-            sort_keys: test_sort_keys,
-            active: 'foo',
-            sort_order: 'asc'
-        });
-        this.orderby.render();
-        var bar_node = Y.one('#sort-bar');
-        bar_node.simulate('click');
-        var expected_arrow = '<span class="sprite order-ascending"></span>';
-        Assert.areEqual(
-            expected_arrow, bar_node.one('span').get('innerHTML'));
-        Assert.isTrue(bar_node.one('span').hasClass('asc'));
-        // Ensure the original button doesn't have sort classes.
-        Assert.isFalse(Y.one('#sort-foo').one('span').hasClass('asc'));
-        Assert.isFalse(Y.one('#sort-foo').one('span').hasClass('desc'));
-    },
-
-    test_click_different_sort_arrows_change_default_order: function() {
-        // A newly active sort button has the order as specified by
-        // the constructor parameter sort_keys.
-        this.makeSrcNode('test-div');
-        var test_sort_keys = [
-            ['foo', 'Foo item', 'asc'],
-            ['bar', 'Bar item', 'desc'],
-            ['baz', 'Baz item', 'asc']
-        ];
-        this.orderby = new Y.lp.ordering.OrderByBar({
-            srcNode: Y.one('#test-div'),
-            sort_keys: test_sort_keys,
-            active: 'foo',
-            sort_order: 'asc'
-        });
-        this.orderby.render();
-        var bar_node = Y.one('#sort-bar');
-        bar_node.simulate('click');
-        Assert.isTrue(bar_node.one('span').hasClass('desc'));
-        Assert.areEqual('desc', this.orderby.get('sort_order'));
-        var baz_node = Y.one('#sort-baz');
-        baz_node.simulate('click');
-        Assert.isTrue(baz_node.one('span').hasClass('asc'));
-        Assert.areEqual('asc', this.orderby.get('sort_order'));
-    },
-
-    test_sort_clause_default: function() {
-        // sort_clause defaults to "importance".
-        this.orderby = new Y.lp.ordering.OrderByBar();
-        this.orderby.render();
-        Assert.areEqual('importance', this.orderby.get('sort_clause'));
-    },
-
-    test_sort_event_fires_with_data: function() {
-        // A custom sort event fires from the widget to signal a
-        // sort order change should happen in the page.  The
-        // callback receives the objects sort_clause for use in
-        // a URL.
-        this.makeSrcNode('test-div');
-        var test_sort_keys = [
-            ['foo', 'Foo item', 'asc'],
-            ['bar', 'Bar item', 'asc']
-        ];
-        this.orderby = new Y.lp.ordering.OrderByBar({
-            srcNode: Y.one('#test-div'),
-            sort_keys: test_sort_keys,
-            active: 'foo',
-            sort_order: 'asc'
-        });
-        this.orderby.render();
-        var foo_node = Y.one('#sort-foo');
-        var event_fired = false;
-        Y.on('orderbybar:sort', function(e) {
-            event_fired = true;
-            // Confirm that we get the sort statement we expect, too.
-            Assert.areEqual('-foo', e);
-        });
-        foo_node.simulate('click');
-        Assert.isTrue(event_fired);
-    },
-
-    test_add_settings_slot: function() {
-        // The widget optionally can add a div for settings/config
-        // widgets to hook onto.
-        this.makeSrcNode('test-div');
-        this.orderby = new Y.lp.ordering.OrderByBar({
-            srcNode: Y.one('#test-div'),
-            config_slot: true
-        });
-        this.orderby.render();
-        var config_slot = Y.one('#test-div').one('.config-widget');
-        Assert.isNotNull(config_slot);
-    },
-
-    test_settings_slot_node_attribute: function() {
-        // The widget keeps a reference to the settings slot
-        // node if config_slot is true.
-        this.makeSrcNode('test-div');
-        this.orderby = new Y.lp.ordering.OrderByBar({
-            srcNode: Y.one('#test-div'),
-            config_slot: true
-        });
-        this.orderby.render();
-        var config_slot = Y.one('#test-div').one('.config-widget');
-        Assert.areEqual(config_slot, this.orderby.get('config_node'));
-    },
-
-    test_hide_show_sort_buttons: function() {
-        // By default, all sort buttons are shown.
-        this.orderby = new Y.lp.ordering.OrderByBar();
-        this.orderby.render();
-        Y.each(this.orderby.get('li_nodes'), function(node) {
-            Assert.isFalse(node._isHidden());
-        });
-
-        var visibility_rules = {
-            'bugnumber': true,
-            'bugtitle': true,
-            'status': true,
-            'importance': false,
-            'bug-heat-icons': false,
-            'package': false,
-            'milestone': false,
-            'assignee': false,
-            'bug-age': false
-        };
-        this.orderby.updateVisibility(visibility_rules);
-        Y.each(this.orderby.get('li_nodes'), function(node) {
-            sort_name = node.get('id').replace('sort-', '');
-            if (visibility_rules[sort_name] === true) {
+    var tests = Y.namespace('lp.orderbybar.test');
+    tests.suite = new Y.Test.Suite('OrderByBar Tests');
+
+    var Assert = Y.Assert;
+    var ArrayAssert = Y.ArrayAssert;
+
+    tests.suite.add(new Y.Test.Case({
+        name: 'orderbybar_widget_tests',
+        orderby: null,
+
+        _should: {
+            error: {
+                test_sort_order_validator:
+                    new Error('sort_order must be either "asc" or "desc"'),
+                test_active_sort_validator:
+                    new Error('active attribute was not found in sort_keys')
+            }
+        },
+
+        tearDown: function() {
+            if (Y.Lang.isValue(this.orderby)) {
+                this.orderby.destroy();
+            }
+        },
+
+        /**
+         * Unpack a list of key, name pairs into individual lists.
+         *
+         * [[Foo, 'Foo Item'], ['Bar', 'Bar item']] becomes
+         * ['Foo', 'Bar'] and ['Foo Item', 'Bar Item'].
+         */
+        getIdsAndNames: function(keys) {
+            var ids = [];
+            var names = [];
+            var len = keys.length;
+            var i;
+            for (i=0; i<len; i++) {
+                ids.push(keys[i][0]);
+                names.push(keys[i][1]);
+            }
+            return [ids, names];
+        },
+
+        /*
+         * Helper function to create the srcNode on the page.  Widgets
+         * will append to the body tag if srcNode is not supplied.
+         */
+        makeSrcNode: function(id) {
+            // Calling the widget's destroy method, which teardown does,
+            // will clean this up.
+            var parent_node = Y.Node.create('<div></div>');
+            parent_node.set('id', id);
+            Y.one('body').appendChild(parent_node);
+        },
+
+        test_default_sort_keys: function() {
+            // The default sort keys should exist in a newly created widget.
+            this.orderby = new Y.lp.ordering.OrderByBar();
+            var expected_sort_keys = [
+                ['bugnumber', 'Number'],
+                ['bugtitle', 'Title'],
+                ['status', 'Status'],
+                ['importance', 'Importance'],
+                ['bug-heat-icons', 'Heat'],
+                ['package', 'Package name'],
+                ['milestone', 'Milestone'],
+                ['assignee', 'Assignee'],
+                ['bug-age', 'Age']
+            ];
+            var expected = this.getIdsAndNames(expected_sort_keys);
+            var actual = this.getIdsAndNames(this.orderby.get('sort_keys'));
+            ArrayAssert.itemsAreSame(expected[0], actual[0]);
+            ArrayAssert.itemsAreSame(expected[1], actual[1]);
+        },
+
+        test_user_supplied_sort_keys: function() {
+            // Call sites can supply their own sort keys to a widget.
+            var user_supplied_sort_keys = [
+                ['foo', 'Foo item', 'asc'],
+                ['bar', 'Bar item', 'asc'],
+                ['baz', 'Baz item', 'asc']
+            ];
+            this.orderby = new Y.lp.ordering.OrderByBar({
+                sort_keys: user_supplied_sort_keys});
+            var expected = this.getIdsAndNames(user_supplied_sort_keys);
+            var actual = this.getIdsAndNames(this.orderby.get('sort_keys'));
+            ArrayAssert.itemsAreSame(expected[0], actual[0]);
+            ArrayAssert.itemsAreSame(expected[1], actual[1]);
+        },
+
+        test_rendered_items_html: function() {
+            // We should be able to get a node from the DOM via an ID
+            // created from sort keys, and the name should be used as
+            // a button display name in HTML.
+            var test_sort_keys = [
+                ['foo', 'Foo item', 'asc'],
+                ['bar', 'Bar item', 'asc']
+            ];
+            this.makeSrcNode('test-div');
+            this.orderby = new Y.lp.ordering.OrderByBar({
+                sort_keys: test_sort_keys,
+                srcNode: Y.one('#test-div'),
+                active: 'foo'
+            });
+            this.orderby.render();
+            var foo_node = Y.one('#sort-foo');
+            Assert.isNotNull(foo_node);
+            Assert.areEqual(foo_node.get('firstChild').get('text'), 'Foo item');
+            var bar_node = Y.one('#sort-bar');
+            Assert.isNotNull(bar_node);
+            Assert.areEqual(bar_node.get('firstChild').get('text'), 'Bar item');
+        },
+
+        test_render_active_sort_default: function() {
+            // Confirm that there is a default active sort class applied.
+            this.makeSrcNode('test-div');
+            this.orderby = new Y.lp.ordering.OrderByBar({
+                srcNode: Y.one('#test-div')
+            });
+            this.orderby.render();
+            var li_node = Y.one('#sort-importance');
+            Assert.isTrue(li_node.hasClass('active-sort'));
+        },
+
+        test_render_active_sort_user_supplied: function() {
+            // The active sort class is also set when "active"
+            // is supplied via config.
+            this.makeSrcNode('test-div');
+            this.orderby = new Y.lp.ordering.OrderByBar({
+                srcNode: Y.one('#test-div'),
+                active: 'status'
+            });
+            this.orderby.render();
+            var li_node = Y.one('#sort-status');
+            Assert.isTrue(li_node.hasClass('active-sort'));
+        },
+
+        test_active_sort_arrow_display_asc: function() {
+            // Buttons using "asc" order get a down arrow added to the li.
+            this.makeSrcNode('test-div');
+            this.orderby = new Y.lp.ordering.OrderByBar({
+                srcNode: Y.one('#test-div'),
+                sort_order: 'asc'
+            });
+            this.orderby.render();
+            var arrow_span = Y.one('.active-sort span');
+            var expected_text = '<span class="sprite order-ascending"></span>';
+            Assert.areEqual(expected_text, arrow_span.get('innerHTML'));
+        },
+
+        test_active_sort_arrow_display_desc: function() {
+            // Buttons using "desc" order get an up arrow added to the li.
+            this.makeSrcNode('test-div');
+            this.orderby = new Y.lp.ordering.OrderByBar({
+                srcNode: Y.one('#test-div'),
+                sort_order: 'desc'
+            });
+            this.orderby.render();
+            var arrow_span = Y.one('.active-sort span');
+            var expected_text = '<span class="sprite order-descending"></span>';
+            Assert.areEqual(expected_text, arrow_span.get('innerHTML'));
+        },
+
+        test_active_sort_click_class_change: function() {
+            // Click a node should add the active_sort class
+            // and remove that class from the previously active node.
+            this.makeSrcNode('test-div');
+            this.orderby = new Y.lp.ordering.OrderByBar({
+                srcNode: Y.one('#test-div')
+            });
+            this.orderby.render();
+            var importance_node = Y.one('#sort-importance');
+            Assert.isTrue(importance_node.hasClass('active-sort'));
+            var status_node = Y.one('#sort-status');
+            status_node.simulate('click');
+            Assert.isTrue(status_node.hasClass('active-sort'));
+        },
+
+        test_active_sort_validator: function() {
+            // This should fail because we do not allow
+            // a "active" value not found in sort_keys.
+            var test_sort_keys = [
+                ['foo', 'Foo item', 'asc'],
+                ['bar', 'Bar item', 'asc']
+            ];
+            this.orderby = new Y.lp.ordering.OrderByBar({
+                sort_keys: test_sort_keys,
+                active: 'foobarbazdonotexists'
+            });
+            this.orderby.render();
+        },
+
+        test_sort_order_validator: function() {
+            // This should fail when using a sort order
+            // other than "asc" or "desc".
+            this.orderby = new Y.lp.ordering.OrderByBar({
+                sort_order: 'foobar'
+            });
+            this.orderby.render();
+        },
+
+        test_click_current_sort_arrow_changes: function() {
+            // Clicking the currently sorted on button should change
+            // the arrow and widget state to show a sort change should
+            // happen.
+            this.makeSrcNode('test-div');
+            var test_sort_keys = [
+                ['foo', 'Foo item', 'asc'],
+                ['bar', 'Bar item', 'asc']
+            ];
+            this.orderby = new Y.lp.ordering.OrderByBar({
+                srcNode: Y.one('#test-div'),
+                sort_keys: test_sort_keys,
+                active: 'foo',
+                sort_order: 'asc'
+            });
+            this.orderby.render();
+            var foo_node = Y.one('#sort-foo');
+            var expected_starting_text =
+                '<span class="sprite order-ascending"></span>';
+            var expected_ending_text =
+                '<span class="sprite order-descending"></span>';
+            Assert.areEqual(
+                expected_starting_text, foo_node.one('span').get('innerHTML'));
+            Assert.isTrue(foo_node.one('span').hasClass('asc'));
+            foo_node.simulate('click');
+            Assert.areEqual(
+                expected_ending_text, foo_node.one('span').get('innerHTML'));
+            Assert.isTrue(foo_node.one('span').hasClass('desc'));
+        },
+
+        test_click_different_sort_arrows_change: function() {
+            // Clicking a button other than the currently sorted on button
+            // should change the arrow and widget state to show a sort
+            // change should happen.
+            this.makeSrcNode('test-div');
+            var test_sort_keys = [
+                ['foo', 'Foo item', 'asc'],
+                ['bar', 'Bar item', 'asc']
+            ];
+            this.orderby = new Y.lp.ordering.OrderByBar({
+                srcNode: Y.one('#test-div'),
+                sort_keys: test_sort_keys,
+                active: 'foo',
+                sort_order: 'asc'
+            });
+            this.orderby.render();
+            var bar_node = Y.one('#sort-bar');
+            bar_node.simulate('click');
+            var expected_arrow = '<span class="sprite order-ascending"></span>';
+            Assert.areEqual(
+                expected_arrow, bar_node.one('span').get('innerHTML'));
+            Assert.isTrue(bar_node.one('span').hasClass('asc'));
+            // Ensure the original button doesn't have sort classes.
+            Assert.isFalse(Y.one('#sort-foo').one('span').hasClass('asc'));
+            Assert.isFalse(Y.one('#sort-foo').one('span').hasClass('desc'));
+        },
+
+        test_click_different_sort_arrows_change_default_order: function() {
+            // A newly active sort button has the order as specified by
+            // the constructor parameter sort_keys.
+            this.makeSrcNode('test-div');
+            var test_sort_keys = [
+                ['foo', 'Foo item', 'asc'],
+                ['bar', 'Bar item', 'desc'],
+                ['baz', 'Baz item', 'asc']
+            ];
+            this.orderby = new Y.lp.ordering.OrderByBar({
+                srcNode: Y.one('#test-div'),
+                sort_keys: test_sort_keys,
+                active: 'foo',
+                sort_order: 'asc'
+            });
+            this.orderby.render();
+            var bar_node = Y.one('#sort-bar');
+            bar_node.simulate('click');
+            Assert.isTrue(bar_node.one('span').hasClass('desc'));
+            Assert.areEqual('desc', this.orderby.get('sort_order'));
+            var baz_node = Y.one('#sort-baz');
+            baz_node.simulate('click');
+            Assert.isTrue(baz_node.one('span').hasClass('asc'));
+            Assert.areEqual('asc', this.orderby.get('sort_order'));
+        },
+
+        test_sort_clause_default: function() {
+            // sort_clause defaults to "importance".
+            this.orderby = new Y.lp.ordering.OrderByBar();
+            this.orderby.render();
+            Assert.areEqual('importance', this.orderby.get('sort_clause'));
+        },
+
+        test_sort_event_fires_with_data: function() {
+            // A custom sort event fires from the widget to signal a
+            // sort order change should happen in the page.  The
+            // callback receives the objects sort_clause for use in
+            // a URL.
+            this.makeSrcNode('test-div');
+            var test_sort_keys = [
+                ['foo', 'Foo item', 'asc'],
+                ['bar', 'Bar item', 'asc']
+            ];
+            this.orderby = new Y.lp.ordering.OrderByBar({
+                srcNode: Y.one('#test-div'),
+                sort_keys: test_sort_keys,
+                active: 'foo',
+                sort_order: 'asc'
+            });
+            this.orderby.render();
+            var foo_node = Y.one('#sort-foo');
+            var event_fired = false;
+            Y.on('orderbybar:sort', function(e) {
+                event_fired = true;
+                // Confirm that we get the sort statement we expect, too.
+                Assert.areEqual('-foo', e);
+            });
+            foo_node.simulate('click');
+            Assert.isTrue(event_fired);
+        },
+
+        test_add_settings_slot: function() {
+            // The widget optionally can add a div for settings/config
+            // widgets to hook onto.
+            this.makeSrcNode('test-div');
+            this.orderby = new Y.lp.ordering.OrderByBar({
+                srcNode: Y.one('#test-div'),
+                config_slot: true
+            });
+            this.orderby.render();
+            var config_slot = Y.one('#test-div').one('.config-widget');
+            Assert.isNotNull(config_slot);
+        },
+
+        test_settings_slot_node_attribute: function() {
+            // The widget keeps a reference to the settings slot
+            // node if config_slot is true.
+            this.makeSrcNode('test-div');
+            this.orderby = new Y.lp.ordering.OrderByBar({
+                srcNode: Y.one('#test-div'),
+                config_slot: true
+            });
+            this.orderby.render();
+            var config_slot = Y.one('#test-div').one('.config-widget');
+            Assert.areEqual(config_slot, this.orderby.get('config_node'));
+        },
+
+        test_hide_show_sort_buttons: function() {
+            // By default, all sort buttons are shown.
+            this.orderby = new Y.lp.ordering.OrderByBar();
+            this.orderby.render();
+            Y.each(this.orderby.get('li_nodes'), function(node) {
                 Assert.isFalse(node._isHidden());
-            } else {
-                Assert.isTrue(node._isHidden());
-            }
-        });
-    }
-}));
+            });
 
-basic_test.suite = suite;
+            var visibility_rules = {
+                'bugnumber': true,
+                'bugtitle': true,
+                'status': true,
+                'importance': false,
+                'bug-heat-icons': false,
+                'package': false,
+                'milestone': false,
+                'assignee': false,
+                'bug-age': false
+            };
+            this.orderby.updateVisibility(visibility_rules);
+            Y.each(this.orderby.get('li_nodes'), function(node) {
+                sort_name = node.get('id').replace('sort-', '');
+                if (visibility_rules[sort_name] === true) {
+                    Assert.isFalse(node._isHidden());
+                } else {
+                    Assert.isTrue(node._isHidden());
+                }
+            });
+        }
+    }));
 
 }, '0.1', {'requires': ['test', 'node-event-simulate', 'lp.ordering']});

=== modified file 'lib/lp/app/javascript/overlay/tests/test_overlay.html'
--- lib/lp/app/javascript/overlay/tests/test_overlay.html	2011-08-19 14:59:06 +0000
+++ lib/lp/app/javascript/overlay/tests/test_overlay.html	2012-02-06 19:47:42 +0000
@@ -1,24 +1,47 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
-  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd";>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+  "http://www.w3.org/TR/html4/strict.dtd";>
+<!--
+Copyright 2012 Canonical Ltd.  This software is licensed under the
+GNU Affero General Public License version 3 (see the file LICENSE).
+-->
+
 <html>
   <head>
-  <title>Pretty Overlay</title>
-
-  <!-- YUI and test setup -->
-  <script type="text/javascript"
-          src="../../../../../canonical/launchpad/icing/yui/yui/yui.js">
-  </script>
-  <link rel="stylesheet" href="../../../../app/javascript/testing/test.css" />
-  <script type="text/javascript"
-          src="../../../../app/javascript/testing/testrunner.js"></script>
-
-  <!-- The module under test -->
-  <script type="text/javascript" src="../overlay.js"></script>
-
-  <!-- The test suite -->
-  <script type="text/javascript" src="test_overlay.js"></script>
-
-</head>
-<body class="yui3-skin-sam">
-</body>
+      <title>Test overlay</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/console/assets/skins/sam/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/..."></script> -->
+
+      <!-- The module under test. -->
+      <script type="text/javascript" src="../overlay.js"></script>
+
+      <!-- Any css assert for this module. -->
+      <!-- <link rel="stylesheet" href="../assets/overlay-core.css" /> -->
+
+      <!-- The test suite. -->
+      <script type="text/javascript" src="test_overlay.js"></script>
+
+    </head>
+    <body class="yui3-skin-sam">
+        <ul id="suites">
+            <!-- <li>lp.large_indicator.test</li> -->
+            <li>lp.overlay.test</li>
+        </ul>
+    </body>
 </html>

=== modified file 'lib/lp/app/javascript/overlay/tests/test_overlay.js'
--- lib/lp/app/javascript/overlay/tests/test_overlay.js	2011-08-19 14:59:06 +0000
+++ lib/lp/app/javascript/overlay/tests/test_overlay.js	2012-02-06 19:47:42 +0000
@@ -1,216 +1,238 @@
-/* Copyright (c) 2009, Canonical Ltd. All rights reserved. */
-
-YUI().use('lp.testing.runner', 'test', 'console', 'node', 'lazr.overlay',
-           'event', 'event-simulate', 'widget-stack', function(Y) {
-
-// KeyCode for escape
-var ESCAPE = 27;
-
-// Local aliases
-var Assert = Y.Assert,
-    ArrayAssert = Y.ArrayAssert;
-
-/*
- * A wrapper for the Y.Event.simulate() function.  The wrapper accepts
- * CSS selectors and Node instances instead of raw nodes.
- */
-function simulate(widget, selector, evtype, options) {
-    var rawnode = Y.Node.getDOMNode(widget.one(selector));
-    Y.Event.simulate(rawnode, evtype, options);
-}
-
-/* Helper function to clean up a dynamically added widget instance. */
-function cleanup_widget(widget) {
-    // Nuke the boundingBox, but only if we've touched the DOM.
-    if (!widget) {
-        return;
-    }
-    if (widget.get('rendered')) {
-        var bb = widget.get('boundingBox');
-        bb.get('parentNode').removeChild(bb);
-    }
-    // Kill the widget itself.
-    widget.destroy();
-}
-
-var suite = new Y.Test.Suite("LAZR Pretty Overlay Tests");
-
-suite.add(new Y.Test.Case({
-
-    name: 'pretty_overlay_basics',
-
-    setUp: function() {
-        this.overlay = null;
-    },
-
-    tearDown: function() {
-        cleanup_widget(this.overlay);
-    },
-
-    hitEscape: function() {
-        simulate(this.overlay.get('boundingBox'),
-                 '.close .close-button',
-                 'keydown', { keyCode: ESCAPE });
-    },
-
-    test_picker_can_be_instantiated: function() {
-        this.overlay = new Y.lazr.PrettyOverlay();
-        Assert.isInstanceOf(
-            Y.lazr.PrettyOverlay, this.overlay, "Overlay not instantiated.");
-    },
-
-    test_overlay_has_elements: function() {
-        this.overlay = new Y.lazr.PrettyOverlay();
-        this.overlay.render();
-        var bb = this.overlay.get('boundingBox');
-        Assert.isNotNull(
-            bb.one('.close'),
-            "Missing close button div.");
-        Assert.isNotNull(
-            bb.one('.close .close-button'),
-            "Missing close button.");
-    },
-
-    test_overlay_can_show_progressbar: function() {
-        this.overlay = new Y.lazr.PrettyOverlay({'headerContent': 'bu bu bu'});
-        var bb = this.overlay.get('boundingBox');
-        this.overlay.render();
-        Assert.isNotNull(
-            bb.one('.steps'),
-            "Progress bar is not present.");
-    },
-
-    test_overlay_can_hide_progressbar: function() {
-        this.overlay = new Y.lazr.PrettyOverlay({progressbar: false});
-        this.overlay.render();
-        var bb = this.overlay.get('boundingBox');
-        bb.set('headerContent', 'ALL HAIL DISCORDIA!');
-        Assert.isNull(
-            bb.one('.steps'),
-            "Progress bar is present when it shouldn't be.");
-    },
-
-    test_overlay_can_show_steptitle: function() {
-        this.overlay = new Y.lazr.PrettyOverlay({
-            'headerContent': 'Fnord',
-            'steptitle': 'No wife, no horse and no moustache'});
-        var bb = this.overlay.get('boundingBox');
-        this.overlay.render();
-        Assert.isNotNull(
-            bb.one('.contains-steptitle h2'),
-            "Step title is not present.");
-    },
-
-    test_overlay_can_hide_steptitle: function() {
-        this.overlay = new Y.lazr.PrettyOverlay({progressbar: false});
-        this.overlay.render();
-        var bb = this.overlay.get('boundingBox');
-        bb.set('headerContent', 'ALL HAIL DISCORDIA!');
-        Assert.isNull(
-            bb.one('.contains-steptitle h2'),
-            "Step title is present when it shouldn't be.");
-    },
-
-    test_click_cancel_hides_the_widget: function() {
-        /* Test that clicking the cancel button hides the widget. */
-        this.overlay = new Y.lazr.PrettyOverlay();
-        this.overlay.render();
-
-        simulate(this.overlay.get('boundingBox'), '.close .close-button', 'click');
-        Assert.isFalse(this.overlay.get('visible'), "The widget wasn't hidden");
-    },
-
-    test_click_cancel_fires_cancel_event: function() {
-        this.overlay = new Y.lazr.PrettyOverlay();
-        this.overlay.render();
-
-        var event_was_fired = false;
-        this.overlay.subscribe('cancel', function() {
+/* Copyright (c) 2012, Canonical Ltd. All rights reserved. */
+
+YUI.add('lp.overlay.test', function (Y) {
+
+    var tests = Y.namespace('lp.overlay.test');
+    tests.suite = new Y.Test.Suite('overlay Tests');
+
+    // KeyCode for escape
+    var ESCAPE = 27;
+
+    // Local aliases
+    var Assert = Y.Assert,
+        ArrayAssert = Y.ArrayAssert;
+
+    /*
+     * A wrapper for the Y.Event.simulate() function.  The wrapper accepts
+     * CSS selectors and Node instances instead of raw nodes.
+     */
+    function simulate(widget, selector, evtype, options) {
+        var rawnode = Y.Node.getDOMNode(widget.one(selector));
+        Y.Event.simulate(rawnode, evtype, options);
+    }
+
+    /* Helper function to clean up a dynamically added widget instance. */
+    function cleanup_widget(widget) {
+        // Nuke the boundingBox, but only if we've touched the DOM.
+        if (!widget) {
+            return;
+        }
+        if (widget.get('rendered')) {
+            var bb = widget.get('boundingBox');
+            bb.get('parentNode').removeChild(bb);
+        }
+        // Kill the widget itself.
+        widget.destroy();
+    }
+
+    tests.suite.add(new Y.Test.Case({
+        name: 'overlay_tests',
+
+        setUp: function() {
+            this.overlay = null;
+        },
+
+        tearDown: function() {
+            cleanup_widget(this.overlay);
+        },
+
+        test_library_exists: function () {
+            Y.Assert.isObject(Y.lazr.PrettyOverlay,
+                "We should be able to locate the lazr.PrettyOverlay module");
+        },
+
+        hitEscape: function() {
+            simulate(this.overlay.get('boundingBox'),
+                     '.close .close-button',
+                     'keydown', { keyCode: ESCAPE });
+        },
+
+        test_picker_can_be_instantiated: function() {
+            this.overlay = new Y.lazr.PrettyOverlay();
+            Assert.isInstanceOf(
+                Y.lazr.PrettyOverlay,
+                this.overlay,
+                "Overlay not instantiated.");
+        },
+
+        test_overlay_has_elements: function() {
+            this.overlay = new Y.lazr.PrettyOverlay();
+            this.overlay.render();
+            var bb = this.overlay.get('boundingBox');
+            Assert.isNotNull(
+                bb.one('.close'),
+                "Missing close button div.");
+            Assert.isNotNull(
+                bb.one('.close .close-button'),
+                "Missing close button.");
+        },
+
+        test_overlay_can_show_progressbar: function() {
+            this.overlay = new Y.lazr.PrettyOverlay({
+                'headerContent': 'bu bu bu'
+            });
+            var bb = this.overlay.get('boundingBox');
+            this.overlay.render();
+            Assert.isNotNull(
+                bb.one('.steps'),
+                "Progress bar is not present.");
+        },
+
+        test_overlay_can_hide_progressbar: function() {
+            this.overlay = new Y.lazr.PrettyOverlay({progressbar: false});
+            this.overlay.render();
+            var bb = this.overlay.get('boundingBox');
+            bb.set('headerContent', 'ALL HAIL DISCORDIA!');
+            Assert.isNull(
+                bb.one('.steps'),
+                "Progress bar is present when it shouldn't be.");
+        },
+
+        test_overlay_can_show_steptitle: function() {
+            this.overlay = new Y.lazr.PrettyOverlay({
+                'headerContent': 'Fnord',
+                'steptitle': 'No wife, no horse and no moustache'});
+            var bb = this.overlay.get('boundingBox');
+            this.overlay.render();
+            Assert.isNotNull(
+                bb.one('.contains-steptitle h2'),
+                "Step title is not present.");
+        },
+
+        test_overlay_can_hide_steptitle: function() {
+            this.overlay = new Y.lazr.PrettyOverlay({progressbar: false});
+            this.overlay.render();
+            var bb = this.overlay.get('boundingBox');
+            bb.set('headerContent', 'ALL HAIL DISCORDIA!');
+            Assert.isNull(
+                bb.one('.contains-steptitle h2'),
+                "Step title is present when it shouldn't be.");
+        },
+
+        test_click_cancel_hides_the_widget: function() {
+            /* Test that clicking the cancel button hides the widget. */
+            this.overlay = new Y.lazr.PrettyOverlay();
+            this.overlay.render();
+
+            simulate(this.overlay.get('boundingBox'),
+                '.close .close-button', 'click');
+            Assert.isFalse(this.overlay.get('visible'),
+                "The widget wasn't hidden");
+        },
+
+        test_click_cancel_fires_cancel_event: function() {
+            this.overlay = new Y.lazr.PrettyOverlay();
+            this.overlay.render();
+
+            var event_was_fired = false;
+            this.overlay.subscribe('cancel', function() {
+                    event_was_fired = true;
+            }, this);
+            simulate(this.overlay.get('boundingBox'),
+                '.close .close-button','click');
+            Assert.isTrue(event_was_fired, "cancel event wasn't fired");
+        },
+
+        test_stroke_escape_hides_the_widget: function() {
+            /* Test that stroking the escape button hides the widget. */
+            this.overlay = new Y.lazr.PrettyOverlay();
+            this.overlay.render();
+
+            Assert.isTrue(this.overlay.get('visible'),
+                "The widget wasn't visible");
+            this.hitEscape();
+            Assert.isFalse(this.overlay.get('visible'),
+                "The widget wasn't hidden");
+        },
+
+        test_stroke_escape_fires_cancel_event: function() {
+            this.overlay = new Y.lazr.PrettyOverlay();
+            this.overlay.render();
+
+            var event_was_fired = false;
+            this.overlay.subscribe('cancel', function() {
                 event_was_fired = true;
-        }, this);
-        simulate(this.overlay.get('boundingBox'), '.close .close-button','click');
-        Assert.isTrue(event_was_fired, "cancel event wasn't fired");
-    },
-
-    test_stroke_escape_hides_the_widget: function() {
-        /* Test that stroking the escape button hides the widget. */
-        this.overlay = new Y.lazr.PrettyOverlay();
-        this.overlay.render();
-
-        Assert.isTrue(this.overlay.get('visible'), "The widget wasn't visible");
-        this.hitEscape();
-        Assert.isFalse(this.overlay.get('visible'), "The widget wasn't hidden");
-    },
-
-    test_stroke_escape_fires_cancel_event: function() {
-        this.overlay = new Y.lazr.PrettyOverlay();
-        this.overlay.render();
-
-        var event_was_fired = false;
-        this.overlay.subscribe('cancel', function() {
-            event_was_fired = true;
-        }, this);
-        this.hitEscape();
-        Assert.isTrue(event_was_fired, "cancel event wasn't fired");
-    },
-
-    test_show_again_re_hooks_events: function() {
-        /* Test that hiding the overlay and showing it again
-         * preserves the event handlers.
-         */
-        this.overlay = new Y.lazr.PrettyOverlay();
-        this.overlay.render();
-
-        this.hitEscape();
-        Assert.isFalse(this.overlay.get('visible'), "The widget wasn't hidden");
-        this.overlay.show();
-        Assert.isTrue(this.overlay.get('visible'), "The widget wasn't shown again");
-        this.hitEscape();
-        Assert.isFalse(this.overlay.get('visible'), "The widget wasn't hidden");
-    },
-
-    test_pretty_overlay_without_header: function() {
-        this.overlay = new Y.lazr.PrettyOverlay();
-        function PrettyOverlaySubclass(config) {
-            PrettyOverlaySubclass.superclass.constructor.apply(this, arguments);
+            }, this);
+            this.hitEscape();
+            Assert.isTrue(event_was_fired, "cancel event wasn't fired");
+        },
+
+        test_show_again_re_hooks_events: function() {
+            /* Test that hiding the overlay and showing it again
+             * preserves the event handlers.
+             */
+            this.overlay = new Y.lazr.PrettyOverlay();
+            this.overlay.render();
+
+            this.hitEscape();
+            Assert.isFalse(this.overlay.get('visible'),
+                "The widget wasn't hidden");
+            this.overlay.show();
+            Assert.isTrue(this.overlay.get('visible'),
+                "The widget wasn't shown again");
+            this.hitEscape();
+            Assert.isFalse(this.overlay.get('visible'),
+                "The widget wasn't hidden");
+        },
+
+        test_pretty_overlay_without_header: function() {
+            this.overlay = new Y.lazr.PrettyOverlay();
+            function PrettyOverlaySubclass(config) {
+                PrettyOverlaySubclass.superclass.constructor.apply(
+                    this,
+                    arguments
+                );
+            }
+            PrettyOverlaySubclass.NAME = 'lazr-overlaysubclass';
+            Y.extend(PrettyOverlaySubclass, Y.lazr.PrettyOverlay);
+
+            var overlay = new PrettyOverlaySubclass({bodyContent: "Hi"});
+            // This shouldn't raise an error if the header content is not
+            // supplied and progressbar is set to `true`.
+            overlay.render();
+            cleanup_widget(overlay);
+        },
+
+        test_overlay_bodyContent_has_size_1: function() {
+            this.overlay = new Y.Overlay({
+                headerContent: 'Form for testing',
+                bodyContent: '<input type="text" name="field1" />'
+            });
+            this.overlay.render();
+            Assert.areEqual(
+                1,
+                this.overlay.get("bodyContent").size(),
+                "The bodContent should contain only one node.");
+        },
+
+        test_set_progress: function() {
+            // test that the progress bar is settable
+            this.overlay = new Y.lazr.PrettyOverlay({
+                'headerContent': 'Fnord',
+                'steptitle': 'No wife, no horse and no moustache'});
+            this.overlay.render();
+            this.overlay.set('progress', 23);
+            Assert.areEqual(
+                '23%',
+                this.overlay.get('boundingBox').
+                    one('.steps .step-on').
+                    getStyle('width')
+            );
         }
-        PrettyOverlaySubclass.NAME = 'lazr-overlaysubclass';
-        Y.extend(PrettyOverlaySubclass, Y.lazr.PrettyOverlay);
-
-        var overlay = new PrettyOverlaySubclass({bodyContent: "Hi"});
-        // This shouldn't raise an error if the header content is not
-        // supplied and progressbar is set to `true`.
-        overlay.render();
-        cleanup_widget(overlay);
-    },
-
-    test_overlay_bodyContent_has_size_1: function() {
-        this.overlay = new Y.Overlay({
-            headerContent: 'Form for testing',
-            bodyContent: '<input type="text" name="field1" />'
-        });
-        this.overlay.render();
-        Assert.areEqual(
-            1,
-            this.overlay.get("bodyContent").size(),
-            "The bodContent should contain only one node.");
-    },
-
-    test_set_progress: function() {
-        // test that the progress bar is settable
-        this.overlay = new Y.lazr.PrettyOverlay({
-            'headerContent': 'Fnord',
-            'steptitle': 'No wife, no horse and no moustache'});
-        this.overlay.render();
-        this.overlay.set('progress', 23);
-        Assert.areEqual(
-            '23%',
-            this.overlay.get('boundingBox').one('.steps .step-on').getStyle('width')
-        );
-    }
-
-}));
-
-Y.lp.testing.Runner.run(suite);
-
+
+    }));
+
+}, '0.1', {
+    'requires': ['test', 'console', 'lazr.overlay', 'node', 'event',
+        'event-simulate', 'widget-stack']
 });

=== modified file 'standard_test_template.html'
--- standard_test_template.html	2012-01-03 07:30:21 +0000
+++ standard_test_template.html	2012-02-06 19:47:42 +0000
@@ -1,31 +1,47 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+  "http://www.w3.org/TR/html4/strict.dtd";>
 <!--
 Copyright 2012 Canonical Ltd.  This software is licensed under the
 GNU Affero General Public License version 3 (see the file LICENSE).
 -->
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd";>
+
 <html>
   <head>
-    <title>Launchpad TESTLIBRARY</title>
-    <!-- YUI and test setup -->
-    <script type="text/javascript"
-            src="../../../../../canonical/launchpad/icing/yui/yui/yui.js">
-    </script>
-    <link rel="stylesheet" href="../../../../app/javascript/testing/test.css" />
-    <script type="text/javascript"
-            src="../../../../app/javascript/testing/testrunner.js"></script>
-
-    <!-- The module under test -->
-    <script type="text/javascript" src="../TESTLIBRARY.js"></script>
-
-    <!-- The test suite -->
-    <script type="text/javascript" src="test_TESTLIBRARY.js"></script>
-  </head>
-  <body class="yui-skin-sam">
-
-    <!-- The example markup required by the script to run -->
-    <div id="expected-id">
-      ...
-    </div>
-  </body>
+      <title>${LIBRARY} Tests</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/console/assets/skins/sam/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/..."></script> -->
+
+      <!-- The module under test. -->
+      <script type="text/javascript" src="../${LIBRARY}.js"></script>
+
+      <!-- Any css assert for this module. -->
+      <!-- <link rel="stylesheet" href="../assets/${LIBRARY}-core.css" /> -->
+
+      <!-- The test suite -->
+      <script type="text/javascript" src="test_${LIBRARY}.js"></script>
+
+    </head>
+    <body class="yui3-skin-sam">
+        <ul id="suites">
+            <!-- <li>lp.large_indicator.test</li> -->
+            <li>lp.${LIBRARY}.test</li>
+        </ul>
+    </body>
 </html>
-

=== modified file 'standard_test_template.js'
--- standard_test_template.js	2012-01-03 07:30:21 +0000
+++ standard_test_template.js	2012-02-06 19:47:42 +0000
@@ -1,76 +1,21 @@
 /* Copyright (c) 2012, Canonical Ltd. All rights reserved. */
-YUI({
-    base: '../../../../canonical/launchpad/icing/yui/',
-    filter: 'raw',
-    combine: false,
-    fetchCSS: false
-
-// Don't forget to add the module under test to the use() clause.
-
-      }).use('event', 'lp.client', 'node', 'test', 'widget-stack',
-             'console', function(Y) {
-
-// Local aliases
-var Assert = Y.Assert,
-    ArrayAssert = Y.ArrayAssert;
-var mynamespace = Y.lp.mynamespace;
-var suite = new Y.Test.Suite("mynamespace Tests");
-
-suite.add(new Y.Test.Case({
-    // Test the setup method.
-    name: 'setup',
-
-    _should: {
-        error: {
-            test_config_undefined: true
-            // Careful: no comma after last item or IE chokes.
-            }
-        },
-
-    setUp: function() {
-        this.tbody = Y.get('#milestone-rows');
-        },
-
-    tearDown: function() {
-        delete this.tbody;
-        mynamespace._milestone_row_uri_template = null;
-        mynamespace._tbody = null;
-        },
-
-    test_good_config: function() {
-        // Verify the config data is stored.
-        var config = {
-            milestone_row_uri_template: '/uri',
-            milestone_rows_id:  '#milestone-rows'
-            };
-        mynamespace.setup(config);
-        Y.Assert.areSame(
-            config.milestone_row_uri_template,
-            mynamespace._milestone_row_uri_template);
-        Y.Assert.areSame(this.tbody, mynamespace._tbody);
-        },
-
-    test_config_undefined: function() {
-        // Verify an error is thrown if there is no config.
-        mynamespace.setup();
+
+YUI.add('lp.${LIBRARY}.test', function (Y) {
+
+    var tests = Y.namespace('lp.${LIBRARY}.test');
+    tests.suite = new Y.Test.Suite('${LIBRARY} Tests');
+
+    tests.suite.add(new Y.Test.Case({
+        name: '${LIBRARY}_tests',
+
+        setUp: function () {},
+        tearDown: function () {},
+
+        test_library_exists: function () {
+            Y.Assert.isObject(Y.lp.${LIBRARY},
+                "We should be able to locate the lp.${LIBRARY} module");
         }
-        // Careful: no comma after last item or IE chokes.
-}));
-
-
-var handle_complete = function(data) {
-    window.status = '::::' + JSON.stringify(data);
-    };
-Y.Test.Runner.on('complete', handle_complete);
-Y.Test.Runner.add(suite);
-
-var yconsole = new Y.Console({
-    newestOnTop: false
-});
-yconsole.render('#log');
-
-Y.on('domready', function() {
-    Y.Test.Runner.run();
-});
-
-});
+
+    }));
+
+}, '0.1', {'requires': ['test', 'console', 'lp.${LIBRARY}']});