← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Richard Harding has proposed merging lp:~rharding/launchpad/combo_yui_tests2 into lp:launchpad with lp:~rharding/launchpad/combo_yui_tests as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

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

= Summary =
Updating more of the YUI tests to be updated per the new convoy/buid dir setup.

See the first MP for the main details. I'm breaking the changes into multiple MP since this is going to be a giant diff due to touching each JS test file.

https://code.launchpad.net/~rharding/launchpad/combo_yui_tests/+merge/91478

== Tests in this batch ==
lib/lp/app/javascript/choiceedit/tests/test_choiceedit.html
lib/lp/app/javascript/confirmationoverlay/tests/test_confirmationoverlay.html
lib/lp/app/javascript/picker/tests/test_personpicker.html
lib/lp/app/javascript/picker/tests/test_picker.html
lib/lp/app/javascript/picker/tests/test_picker_patcher.html
-- 
https://code.launchpad.net/~rharding/launchpad/combo_yui_tests2/+merge/91499
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rharding/launchpad/combo_yui_tests2 into lp:launchpad.
=== modified file 'lib/lp/app/javascript/choiceedit/tests/test_choiceedit.html'
--- lib/lp/app/javascript/choiceedit/tests/test_choiceedit.html	2011-08-10 08:43:17 +0000
+++ lib/lp/app/javascript/choiceedit/tests/test_choiceedit.html	2012-02-07 16:00:29 +0000
@@ -1,28 +1,52 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.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>Status Editor</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>
-
-  <!-- Dependency -->
-  <script type="text/javascript" src="../../lazr/lazr.js"></script>
-  <script type="text/javascript" src="../../anim/anim.js"></script>
-  <script type="text/javascript" src="../../overlay/overlay.js"></script>
-  <script type="text/javascript" src="../../extras/extras.js"></script>
-
-  <!-- The module under test -->
-  <script type="text/javascript" src="../choiceedit.js"></script>
-
-  <!-- The test suite -->
-  <script type="text/javascript" src="test_choiceedit.js"></script>
-</head>
-<body class="yui3-skin-sam">
-</body>
+      <title>choiceedit 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/lazr/lazr.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/anim/anim.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/overlay/overlay.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/extras/extras.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/choice.js"></script>
+
+
+      <!-- The module under test. -->
+      <script type="text/javascript" src="../choiceedit.js"></script>
+
+      <!-- Any css assert for this module. -->
+      <!-- <link rel="stylesheet" href="../assets/choiceedit-core.css" /> -->
+
+      <!-- The test suite -->
+      <script type="text/javascript" src="test_choiceedit.js"></script>
+
+    </head>
+    <body class="yui3-skin-sam">
+        <ul id="suites">
+            <!-- <li>lp.large_indicator.test</li> -->
+            <li>lp.choiceedit.test</li>
+        </ul>
+    </body>
 </html>

=== modified file 'lib/lp/app/javascript/choiceedit/tests/test_choiceedit.js'
--- lib/lp/app/javascript/choiceedit/tests/test_choiceedit.js	2011-08-09 14:18:02 +0000
+++ lib/lp/app/javascript/choiceedit/tests/test_choiceedit.js	2012-02-07 16:00:29 +0000
@@ -1,527 +1,528 @@
-/* Copyright (c) 2008, Canonical Ltd. All rights reserved. */
-
-YUI().use('lp.testing.runner', 'test', 'console', 'node', 'lazr.choiceedit',
-           'event', 'event-simulate', 'widget-stack', function(Y) {
-
-// 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.get('rendered')) {
-        var bb = widget.get('boundingBox');
-        if (Y.Node.getDOMNode(bb)) {
-            if (bb.get('parentNode')) {
-                bb.get('parentNode').removeChild(bb);
-            }
-        }
-    }
-    // Kill the widget itself.
-    widget.destroy();
-}
-
-var suite = new Y.Test.Suite("LAZR Choice Edit Tests");
-
-function setUp() {
-    // add the in-page HTML
-    var inpage = Y.Node.create([
-        '<p id="thestatus">',
-        'Status: <span class="value">Unset</span> ',
-        '<img class="editicon" src="https://bugs.edge.launchpad.net/@@/edit";>',
-        '</p>'].join(''));
-    Y.one("body").appendChild(inpage);
-    this.config = this.make_config();
-    this.choice_edit = new Y.ChoiceSource(this.config);
-    this.choice_edit.render();
-}
-
-function tearDown() {
-    if (this.choice_edit._choice_list) {
-        cleanup_widget(this.choice_edit._choice_list);
-    }
-    var status = Y.one("document").one("#thestatus");
-    if (status) {
-        status.get("parentNode").removeChild(status);
-    }
-}
-
-suite.add(new Y.Test.Case({
-
-    name: 'choice_edit_basics',
-
-    setUp: setUp,
-
-    tearDown: tearDown,
-
-    make_config: function() {
-        return {
-            contentBox:  '#thestatus',
-            value:       'incomplete',
-            title:       'Change status to',
-            items: [
-              { name: 'New', value: 'new', style: '',
-                help: '', disabled: false },
-              { name: 'Invalid', value: 'invalid', style: '',
-                help: '', disabled: true },
-              { name: 'Incomplete', value: 'incomplete', style: '',
-                help: '', disabled: false },
-              { name: 'Fix Released', value: 'fixreleased', style: '',
-                help: '', disabled: false },
-              { name: 'Fix Committed', value: 'fixcommitted', style: '',
-                help: '', disabled: true },
-              { name: 'In Progress', value: 'inprogress', style: '',
-                help: '', disabled: false },
-              { name: 'Stalled', value: 'stalled', style: '',
-                help: '', disabled: false, source_name: 'STALLED' }
-            ]
-        };
-    },
-
-    test_can_be_instantiated: function() {
-        Assert.isInstanceOf(
-            Y.ChoiceSource, this.choice_edit, "ChoiceSource not instantiated.");
-    },
-
-    test_choicesource_overrides_value_in_page: function() {
-        var st = Y.one(document).one("#thestatus");
-        // value in page should be set to the config.items.name corresponding to
-        // config.value
-        Assert.areEqual("Incomplete", st.one(".value").get("innerHTML"),
-                        "ChoiceSource is not overriding displayed value in HTML");
-    },
-
-    test_clicking_creates_choicelist: function() {
-        simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
-        Assert.isNotNull(this.choice_edit._choice_list,
-          "ChoiceList object is not created");
-        Assert.isNotNull(Y.one(document).one(".yui3-ichoicelist"),
-          "ChoiceList HTML is not being added to the page");
-    },
-
-    test_right_clicking_doesnt_create_choicelist: function() {
-        simulate(this.choice_edit.get('boundingBox'),
-                 '.value', 'click', { button: 2 });
-        Assert.isNull(Y.one(document).one(".yui3-ichoicelist"),
-          "ChoiceList created when the right mouse button was clicked");
-    },
-
-    test_choicelist_has_correct_values: function() {
-        simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
-        var that = this;
-        Y.each(this.config.items, function(configitem) {
-            var found = false;
-            Y.each(that.choice_edit._choice_list.get("items"), function(choiceitem) {
-                if (choiceitem.name == configitem.name) {
-                    found = true;
-                }
-            });
-            Assert.isTrue(found,
-              "Item " + configitem.name + " is passed to ChoiceSource but is " +
-              "not in ChoiceList.items");
-        });
-        var choicelistcount = this.choice_edit._choice_list.get("items").length;
-        var configcount = this.config.items.length;
-        Assert.areEqual(choicelistcount, configcount,
-          "ChoiceList HTML list is a different length (" + choicelistcount +
-          ") than config items list (" + configcount + ")");
-    },
-
-    test_choicelist_html_has_correct_values: function() {
-        simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
-        var configcount = this.config.items.length;
-        var choicelist_lis = Y.one(document).all(".yui3-ichoicelist li");
-        Assert.areEqual(choicelist_lis.size(), configcount,
-          "ChoiceList HTML list is a different length (" + choicelist_lis.size() +
-          ") than config items list (" + configcount + ")");
-        // confirm that each LI matches with an item
-        var that = this;
-        choicelist_lis.each(function(li) {
-            var text = li.get("text");
-            var found = false;
-            for (var i=0; i<that.config.items.length; i++) {
-                if (that.config.items[i].name == text) {
-                    found = true;
-                    break;
-                }
-            }
-            Assert.isTrue(found, "Page LI '" + text +
-               "' did not come from a config item");
-        });
-    },
-
-    test_choicelist_html_has_disabled: function() {
-        simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
-        var configcount = this.config.items.length;
-        var choicelist_lis = Y.one(document).all(".yui3-ichoicelist li");
-        // confirm that disabled LIs are disabled
-        var that = this;
-        choicelist_lis.each(function(li) {
-            var text = li.get("text");
-            for (var i=0; i<that.config.items.length; i++) {
-                if (that.config.items[i].name == text) {
-                    if (that.config.items[i].disabled) {
-                        Assert.isNotNull(li.one("span.disabled"),
-                          "Page LI '" + text + "' was not disabled");
-                    }
-                    break;
-                }
-            }
-        });
-    },
-
-    test_choicelist_html_has_current: function() {
-        simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
-        var configcount = this.config.items.length;
-        var choicelist_lis = Y.one(document).all(".yui3-ichoicelist li");
-        // confirm that current value has an LI with current style
-        var that = this;
-        var asserted = false;
-        choicelist_lis.each(function(li) {
-            var text = li.get("text");
-            for (var i=0; i<that.config.items.length; i++) {
-                if (that.config.items[i].name == text) {
-                    if (that.config.items[i].value == that.config.value) {
-                        Assert.isNotNull(li.one("span.current"),
-                          "Page LI '" + text + "' was not marked as current");
-                        asserted = true;
-                    }
-                    break;
-                }
-            }
-        });
-        Assert.isTrue(asserted, "There was no current LI item");
-    },
-
-    test_clicking_choicelist_item_fires_signal: function() {
-        simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
-        var that = this;
-        var fired = false;
-        this.choice_edit._choice_list.on("valueChosen", function() {
-            fired = true;
-        });
-        // simulate a click on the "fix released" option, which is
-        // (a) enabled
-        // (b) not the current option
-        simulate(this.choice_edit._choice_list.get('boundingBox'),
-            'li a[href$=fixreleased]', 'click');
-        Assert.isTrue(fired, "valueChosen signal was not fired");
-    },
-
-    test_clicking_choicelist_item_does_green_flash: function() {
-        simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
-        var that = this;
-        var green_flash = Y.lp.anim.green_flash;
-        var flashed = false;
-        Y.lp.anim.green_flash = function() {
-          return {
-              run: function() {
-                  flashed = true;
-              }
-          };
-        };
-        simulate(this.choice_edit._choice_list.get('boundingBox'),
-            'li a[href$=fixreleased]', 'click');
-        Assert.isTrue(flashed, "green_flash animation was not fired");
-        Y.lp.anim.green_flash = green_flash;
-    },
-
-    test_clicking_choicelist_item_sets_page_value: function() {
-        var st = Y.one(document).one("#thestatus");
-        // The page value is set to item.name of the selected item.
-        simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
-        simulate(this.choice_edit._choice_list.get('boundingBox'),
-          'li a[href$=fixreleased]', 'click');
-        Assert.areEqual("Fix Released", st.one(".value").get("innerHTML"),
-           "Chosen choicelist item is not displayed in HTML (value is '" +
-           st.one(".value").get("innerHTML") + "')");
-    },
-
-    test_clicking_choicelist_item_sets_page_source_name: function() {
-        var st = Y.one(document).one("#thestatus");
-        // By default, the page value is set to item.name of the
-        // selected item, but this can be overridden by specifying
-        // item.source_name.
-        simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
-        var choice_list_bb = this.choice_edit._choice_list.get('boundingBox');
-        var stalled_in_list = choice_list_bb.one('li a[href$=stalled]');
-        Assert.areEqual(
-            "Stalled", stalled_in_list.get('innerHTML'),
-            "ChoiceList item not displayed correctly: " +
-                stalled_in_list.get('innerHTML'));
-        simulate(choice_list_bb, 'li a[href$=stalled]', 'click');
-        Assert.areEqual("STALLED", st.one(".value").get("innerHTML"),
-           "Chosen choicelist item is not displayed in HTML (value is '" +
-           st.one(".value").get("innerHTML") + "')");
-    }
-
-}));
-
-suite.add(new Y.Test.Case({
-
-    name: 'choice_edit_non_clickable_content',
-
-    setUp: setUp,
-
-    tearDown: tearDown,
-
-    make_config: function() {
-        return {
-            contentBox:  '#thestatus',
-            value:       'incomplete',
-            title:       'Change status to',
-            items: [
-              { name: 'New', value: 'new', style: '',
-                help: '', disabled: false },
-              { name: 'Invalid', value: 'invalid', style: '',
-                help: '', disabled: true },
-              { name: 'Incomplete', value: 'incomplete', style: '',
-                help: '', disabled: false },
-              { name: 'Fix Released', value: 'fixreleased', style: '',
-                help: '', disabled: false },
-              { name: 'Fix Committed', value: 'fixcommitted', style: '',
-                help: '', disabled: true },
-              { name: 'In Progress', value: 'inprogress', style: '',
-                help: '', disabled: false },
-              { name: 'Stalled', value: 'stalled', style: '',
-                help: '', disabled: false, source_name: 'STALLED' }
-            ],
-            clickable_content: false
-        };
-    },
-
-    test_clicking_content_doesnt_create_choicelist: function() {
-        simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
-        Assert.isUndefined(this.choice_edit._choice_list,
-          "ChoiceList object is created");
-        Assert.isNull(Y.one(document).one(".yui3-ichoicelist"),
-          "ChoiceList HTML is being added to the page");
-    },
-
-    test_clicking_icon_creates_choicelist: function() {
-        simulate(this.choice_edit.get('boundingBox'), '.editicon', 'click');
-        Assert.isNotUndefined(this.choice_edit._choice_list,
-          "ChoiceList object is not being created");
-        Assert.isNotNull(Y.one(document).one(".yui3-ichoicelist"),
-          "ChoiceList HTML is not being added to the page");
-    },
-
-}));
-
-
-/**
- * Tests what happens when config.value does not correspond to any of
- * the items in config.items.
- */
-suite.add(new Y.Test.Case({
-
-    name: 'choice_edit_value_item_mismatch',
-
-    setUp: setUp,
-
-    tearDown: tearDown,
-
-    make_config: function() {
-        return {
-            contentBox:  '#thestatus',
+/* Copyright (c) 2012, Canonical Ltd. All rights reserved. */
+
+YUI.add('lp.choiceedit.test', function (Y) {
+    var tests = Y.namespace('lp.choiceedit.test');
+    tests.suite = new Y.Test.Suite('choiceedit tests');
+
+    // 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.
+     */
+    var simulate = function (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. */
+    var cleanup_widget = function (widget) {
+        // Nuke the boundingBox, but only if we've touched the DOM.
+        if (widget.get('rendered')) {
+            var bb = widget.get('boundingBox');
+            if (Y.Node.getDOMNode(bb)) {
+                if (bb.get('parentNode')) {
+                    bb.get('parentNode').removeChild(bb);
+                }
+            }
+        }
+        // Kill the widget itself.
+        widget.destroy();
+    }
+
+    var shared_setup = function () {
+        // add the in-page HTML
+        var inpage = Y.Node.create([
+            '<p id="thestatus">',
+            'Status: <span class="value">Unset</span> ',
+            '<img class="editicon" src="https://bugs.edge.launchpad.net/@@/edit";>',
+            '</p>'].join(''));
+        Y.one("body").appendChild(inpage);
+        this.config = this.make_config();
+        this.choice_edit = new Y.ChoiceSource(this.config);
+        this.choice_edit.render();
+    }
+
+    var shared_teardown = function () {
+        if (this.choice_edit._choice_list) {
+            cleanup_widget(this.choice_edit._choice_list);
+        }
+        var status = Y.one("document").one("#thestatus");
+        if (status) {
+            status.get("parentNode").removeChild(status);
+        }
+    }
+
+    tests.suite.add(new Y.Test.Case({
+        name: 'choiceedit_tests',
+
+        setUp: shared_setup,
+        tearDown: shared_teardown,
+
+        test_library_exists: function () {
+            Y.Assert.isObject(Y.ChoiceSource,
+                "We should be able to locate the lazr.choiceedit module");
+        },
+
+        make_config: function() {
+            return {
+                contentBox:  '#thestatus',
+                value:       'incomplete',
+                title:       'Change status to',
+                items: [
+                  { name: 'New', value: 'new', style: '',
+                    help: '', disabled: false },
+                  { name: 'Invalid', value: 'invalid', style: '',
+                    help: '', disabled: true },
+                  { name: 'Incomplete', value: 'incomplete', style: '',
+                    help: '', disabled: false },
+                  { name: 'Fix Released', value: 'fixreleased', style: '',
+                    help: '', disabled: false },
+                  { name: 'Fix Committed', value: 'fixcommitted', style: '',
+                    help: '', disabled: true },
+                  { name: 'In Progress', value: 'inprogress', style: '',
+                    help: '', disabled: false },
+                  { name: 'Stalled', value: 'stalled', style: '',
+                    help: '', disabled: false, source_name: 'STALLED' }
+                ]
+            };
+        },
+
+        test_can_be_instantiated: function() {
+            Assert.isInstanceOf(
+                Y.ChoiceSource, this.choice_edit, "ChoiceSource not instantiated.");
+        },
+
+        test_choicesource_overrides_value_in_page: function() {
+            var st = Y.one(document).one("#thestatus");
+            // value in page should be set to the config.items.name corresponding to
+            // config.value
+            Assert.areEqual("Incomplete", st.one(".value").get("innerHTML"),
+                            "ChoiceSource is not overriding displayed value in HTML");
+        },
+
+        test_clicking_creates_choicelist: function() {
+            simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
+            Assert.isNotNull(this.choice_edit._choice_list,
+              "ChoiceList object is not created");
+            Assert.isNotNull(Y.one(document).one(".yui3-ichoicelist"),
+              "ChoiceList HTML is not being added to the page");
+        },
+
+        test_right_clicking_doesnt_create_choicelist: function() {
+            simulate(this.choice_edit.get('boundingBox'),
+                     '.value', 'click', { button: 2 });
+            Assert.isNull(Y.one(document).one(".yui3-ichoicelist"),
+              "ChoiceList created when the right mouse button was clicked");
+        },
+
+        test_choicelist_has_correct_values: function() {
+            simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
+            var that = this;
+            Y.each(this.config.items, function(configitem) {
+                var found = false;
+                Y.each(that.choice_edit._choice_list.get("items"), function(choiceitem) {
+                    if (choiceitem.name == configitem.name) {
+                        found = true;
+                    }
+                });
+                Assert.isTrue(found,
+                  "Item " + configitem.name + " is passed to ChoiceSource but is " +
+                  "not in ChoiceList.items");
+            });
+            var choicelistcount = this.choice_edit._choice_list.get("items").length;
+            var configcount = this.config.items.length;
+            Assert.areEqual(choicelistcount, configcount,
+              "ChoiceList HTML list is a different length (" + choicelistcount +
+              ") than config items list (" + configcount + ")");
+        },
+
+        test_choicelist_html_has_correct_values: function() {
+            simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
+            var configcount = this.config.items.length;
+            var choicelist_lis = Y.one(document).all(".yui3-ichoicelist li");
+            Assert.areEqual(choicelist_lis.size(), configcount,
+              "ChoiceList HTML list is a different length (" + choicelist_lis.size() +
+              ") than config items list (" + configcount + ")");
+            // confirm that each LI matches with an item
+            var that = this;
+            choicelist_lis.each(function(li) {
+                var text = li.get("text");
+                var found = false;
+                for (var i=0; i<that.config.items.length; i++) {
+                    if (that.config.items[i].name == text) {
+                        found = true;
+                        break;
+                    }
+                }
+                Assert.isTrue(found, "Page LI '" + text +
+                   "' did not come from a config item");
+            });
+        },
+
+        test_choicelist_html_has_disabled: function() {
+            simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
+            var configcount = this.config.items.length;
+            var choicelist_lis = Y.one(document).all(".yui3-ichoicelist li");
+            // confirm that disabled LIs are disabled
+            var that = this;
+            choicelist_lis.each(function(li) {
+                var text = li.get("text");
+                for (var i=0; i<that.config.items.length; i++) {
+                    if (that.config.items[i].name == text) {
+                        if (that.config.items[i].disabled) {
+                            Assert.isNotNull(li.one("span.disabled"),
+                              "Page LI '" + text + "' was not disabled");
+                        }
+                        break;
+                    }
+                }
+            });
+        },
+
+        test_choicelist_html_has_current: function() {
+            simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
+            var configcount = this.config.items.length;
+            var choicelist_lis = Y.one(document).all(".yui3-ichoicelist li");
+            // confirm that current value has an LI with current style
+            var that = this;
+            var asserted = false;
+            choicelist_lis.each(function(li) {
+                var text = li.get("text");
+                for (var i=0; i<that.config.items.length; i++) {
+                    if (that.config.items[i].name == text) {
+                        if (that.config.items[i].value == that.config.value) {
+                            Assert.isNotNull(li.one("span.current"),
+                              "Page LI '" + text + "' was not marked as current");
+                            asserted = true;
+                        }
+                        break;
+                    }
+                }
+            });
+            Assert.isTrue(asserted, "There was no current LI item");
+        },
+
+        test_clicking_choicelist_item_fires_signal: function() {
+            simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
+            var that = this;
+            var fired = false;
+            this.choice_edit._choice_list.on("valueChosen", function() {
+                fired = true;
+            });
+            // simulate a click on the "fix released" option, which is
+            // (a) enabled
+            // (b) not the current option
+            simulate(this.choice_edit._choice_list.get('boundingBox'),
+                'li a[href$=fixreleased]', 'click');
+            Assert.isTrue(fired, "valueChosen signal was not fired");
+        },
+
+        test_clicking_choicelist_item_does_green_flash: function() {
+            simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
+            var that = this;
+            var green_flash = Y.lp.anim.green_flash;
+            var flashed = false;
+            Y.lp.anim.green_flash = function() {
+              return {
+                  run: function() {
+                      flashed = true;
+                  }
+              };
+            };
+            simulate(this.choice_edit._choice_list.get('boundingBox'),
+                'li a[href$=fixreleased]', 'click');
+            Assert.isTrue(flashed, "green_flash animation was not fired");
+            Y.lp.anim.green_flash = green_flash;
+        },
+
+        test_clicking_choicelist_item_sets_page_value: function() {
+            var st = Y.one(document).one("#thestatus");
+            // The page value is set to item.name of the selected item.
+            simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
+            simulate(this.choice_edit._choice_list.get('boundingBox'),
+              'li a[href$=fixreleased]', 'click');
+            Assert.areEqual("Fix Released", st.one(".value").get("innerHTML"),
+               "Chosen choicelist item is not displayed in HTML (value is '" +
+               st.one(".value").get("innerHTML") + "')");
+        },
+
+        test_clicking_choicelist_item_sets_page_source_name: function() {
+            var st = Y.one(document).one("#thestatus");
+            // By default, the page value is set to item.name of the
+            // selected item, but this can be overridden by specifying
+            // item.source_name.
+            simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
+            var choice_list_bb = this.choice_edit._choice_list.get('boundingBox');
+            var stalled_in_list = choice_list_bb.one('li a[href$=stalled]');
+            Assert.areEqual(
+                "Stalled", stalled_in_list.get('innerHTML'),
+                "ChoiceList item not displayed correctly: " +
+                    stalled_in_list.get('innerHTML'));
+            simulate(choice_list_bb, 'li a[href$=stalled]', 'click');
+            Assert.areEqual("STALLED", st.one(".value").get("innerHTML"),
+               "Chosen choicelist item is not displayed in HTML (value is '" +
+               st.one(".value").get("innerHTML") + "')");
+        }
+    }));
+
+    tests.suite.add(new Y.Test.Case({
+
+        name: 'choice_edit_non_clickable_content',
+
+        setUp: shared_setup,
+        tearDown: shared_teardown,
+
+        make_config: function() {
+            return {
+                contentBox:  '#thestatus',
+                value:       'incomplete',
+                title:       'Change status to',
+                items: [
+                  { name: 'New', value: 'new', style: '',
+                    help: '', disabled: false },
+                  { name: 'Invalid', value: 'invalid', style: '',
+                    help: '', disabled: true },
+                  { name: 'Incomplete', value: 'incomplete', style: '',
+                    help: '', disabled: false },
+                  { name: 'Fix Released', value: 'fixreleased', style: '',
+                    help: '', disabled: false },
+                  { name: 'Fix Committed', value: 'fixcommitted', style: '',
+                    help: '', disabled: true },
+                  { name: 'In Progress', value: 'inprogress', style: '',
+                    help: '', disabled: false },
+                  { name: 'Stalled', value: 'stalled', style: '',
+                    help: '', disabled: false, source_name: 'STALLED' }
+                ],
+                clickable_content: false
+            };
+        },
+
+        test_clicking_content_doesnt_create_choicelist: function() {
+            simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
+            Assert.isUndefined(this.choice_edit._choice_list,
+              "ChoiceList object is created");
+            Assert.isNull(Y.one(document).one(".yui3-ichoicelist"),
+              "ChoiceList HTML is being added to the page");
+        },
+
+        test_clicking_icon_creates_choicelist: function() {
+            simulate(this.choice_edit.get('boundingBox'), '.editicon', 'click');
+            Assert.isNotUndefined(this.choice_edit._choice_list,
+              "ChoiceList object is not being created");
+            Assert.isNotNull(Y.one(document).one(".yui3-ichoicelist"),
+              "ChoiceList HTML is not being added to the page");
+        },
+
+    }));
+
+    /**
+     * Tests what happens when config.value does not correspond to any of
+     * the items in config.items.
+     */
+    tests.suite.add(new Y.Test.Case({
+
+        name: 'choice_edit_value_item_mismatch',
+
+        setUp: shared_setup,
+        tearDown: shared_teardown,
+
+        make_config: function() {
+            return {
+                contentBox:  '#thestatus',
+                value:       null,
+                title:       'Change status to',
+                items: [
+                    { name: 'New', value: 'new', style: '',
+                      help: '', disabled: false },
+                    { name: 'Invalid', value: 'invalid', style: '',
+                      help: '', disabled: true }
+                ]
+            };
+        },
+
+        /**
+         * The value displayed in the page should be left alone if
+         * config.value does not correspond to any item in config.items.
+         */
+        test_choicesource_leaves_value_in_page: function() {
+            var st = Y.one(document).one("#thestatus");
+            Assert.areEqual(
+                "Unset", st.one(".value").get("innerHTML"),
+                "ChoiceSource is overriding displayed value in HTML");
+        },
+
+        test_choicelist_html_has_current: function() {
+            simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
+            var configcount = this.config.items.length;
+            var choicelist_lis = Y.one(document).all(".yui3-ichoicelist li");
+
+            var that = this;
+            var asserted;
+            var test_li = function(li) {
+                var text = li.get("text");
+                for (var i=0; i < that.config.items.length; i++) {
+                    if (that.config.items[i].name == text) {
+                        if (that.config.items[i].value == that.choice_edit.get("value")) {
+                            Assert.isNotNull(li.one("span.current"),
+                              "Page LI '" + text + "' was not marked as current");
+                            asserted = true;
+                        }
+                        break;
+                    }
+                }
+            };
+            // When config.value does not correspond to any item in
+            // config.items, no LI in the choice list will be marked with
+            // the "current" style.
+            asserted = false;
+            choicelist_lis.each(test_li);
+            Assert.isFalse(asserted, "There was a current LI item");
+            // Once a choice is made, the current value is marked with the
+            // "current" class in the choice list.
+            simulate(this.choice_edit._choice_list.get('boundingBox'),
+                'li a[href$=new]', 'click');
+            simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
+            asserted = false;
+            choicelist_lis.refresh();
+            choicelist_lis.each(test_li);
+            Assert.isTrue(asserted, "There was no current LI item");
+        }
+
+    }));
+
+    tests.suite.add(new Y.Test.Case({
+
+        name: 'nullable_choice_edit',
+
+        setUp: function() {
+          // add the in-page HTML
+          var inpage = Y.Node.create([
+            '<p id="nullchoiceedit" style="margin-top: 25px">',
+            '  <img class="addicon" src="https://bugs.edge.launchpad.net/@@/add";>',
+            '  <span class="nulltext">Choose something</span>',
+            '  <span class="value" style="display:none" />',
+            '  <img class="editicon" style="display:none" src="https://bugs.edge.launchpad.net/@@/edit";>',
+            '</p>'].join(''));
+          Y.one("body").appendChild(inpage);
+          this.null_choice_edit = new Y.NullChoiceSource({
+            contentBox:  '#nullchoiceedit',
             value:       null,
-            title:       'Change status to',
+            title:       'Choose something',
             items: [
-                { name: 'New', value: 'new', style: '',
-                  help: '', disabled: false },
-                { name: 'Invalid', value: 'invalid', style: '',
-                  help: '', disabled: true }
+              { name: 'Chico', value: 'chico', style: '',
+                help: '', disabled: false },
+              { name: 'Harpo', value: 'harpo', style: '',
+                help: '', disabled: false },
+              { name: 'Groucho', value: 'groucho', style: '',
+                help: '', disabled: false },
+              { name: 'Gummo', value: 'gummo', style: '',
+                help: '', disabled: false },
+              { name: 'Zeppo', value: 'zeppo', style: '',
+                help: '', disabled: false },
+              { name: 'Not funny!', value: null, style: '',
+                help: '', disabled: false }
             ]
-        };
-    },
-
-    /**
-     * The value displayed in the page should be left alone if
-     * config.value does not correspond to any item in config.items.
-     */
-    test_choicesource_leaves_value_in_page: function() {
-        var st = Y.one(document).one("#thestatus");
-        Assert.areEqual(
-            "Unset", st.one(".value").get("innerHTML"),
-            "ChoiceSource is overriding displayed value in HTML");
-    },
-
-    test_choicelist_html_has_current: function() {
-        simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
-        var configcount = this.config.items.length;
-        var choicelist_lis = Y.one(document).all(".yui3-ichoicelist li");
-
-        var that = this;
-        var asserted;
-        var test_li = function(li) {
-            var text = li.get("text");
-            for (var i=0; i < that.config.items.length; i++) {
-                if (that.config.items[i].name == text) {
-                    if (that.config.items[i].value == that.choice_edit.get("value")) {
-                        Assert.isNotNull(li.one("span.current"),
-                          "Page LI '" + text + "' was not marked as current");
-                        asserted = true;
-                    }
-                    break;
+          });
+
+          this.null_choice_edit.render();
+        },
+
+        tearDown: function() {
+            if (this.null_choice_edit._choice_list) {
+                cleanup_widget(this.null_choice_edit._choice_list);
+            }
+            var nullchoiceedit = Y.one("document").one("#nullchoiceedit");
+            if (nullchoiceedit) {
+                nullchoiceedit.get("parentNode").removeChild(nullchoiceedit);
+            }
+        },
+
+        test_can_be_instantiated: function() {
+            Assert.isInstanceOf(
+                Y.NullChoiceSource, this.null_choice_edit,
+                "NullChoiceSource not instantiated.");
+        },
+
+        test_action_icon: function() {
+            var that = this;
+
+            Assert.areEqual(
+                this.null_choice_edit.get('actionicon'),
+                this.null_choice_edit.get('addicon'),
+                'Action icon is not the add icon like expected.');
+
+            Assert.areEqual(
+                this.null_choice_edit.get('addicon').getStyle('display'),
+                'inline',
+                'Add icon is not visible when it should be');
+            Assert.areEqual(
+                this.null_choice_edit.get('editicon').getStyle('display'),
+                'none',
+                "Edit icon is visible when it shouldn't be");
+
+            simulate(this.null_choice_edit.get('boundingBox'),
+                     '.value', 'click');
+            simulate(this.null_choice_edit._choice_list.get('boundingBox'),
+              'li a[href$=groucho]', 'click');
+            this.null_choice_edit._uiClearWaiting();
+
+            Assert.areEqual(
+                this.null_choice_edit.get('actionicon'),
+                this.null_choice_edit.get('editicon'),
+                'Action icon is not the add icon like expected.');
+            Assert.areEqual(
+                this.null_choice_edit.get('addicon').getStyle('display'),
+                'none',
+                "Add icon is visible when it shouldn't be");
+            Assert.areEqual(
+                this.null_choice_edit.get('editicon').getStyle('display'),
+                'inline',
+                "Edit icon is not visible when it shouldn be");
+        },
+
+        test_null_item_absent: function() {
+            Assert.areEqual(
+                this.null_choice_edit.get('value'),
+                null,
+                "Selected value isn't null");
+
+            simulate(this.null_choice_edit.get('boundingBox'),
+                     '.value', 'click');
+            var remove_action_present = false;
+            this.null_choice_edit._choice_list.get(
+                'boundingBox').all('li a').each(function(item) {
+                if (item._value == null) {
+                    remove_action_present = true;
                 }
-            }
-        };
-        // When config.value does not correspond to any item in
-        // config.items, no LI in the choice list will be marked with
-        // the "current" style.
-        asserted = false;
-        choicelist_lis.each(test_li);
-        Assert.isFalse(asserted, "There was a current LI item");
-        // Once a choice is made, the current value is marked with the
-        // "current" class in the choice list.
-        simulate(this.choice_edit._choice_list.get('boundingBox'),
-            'li a[href$=new]', 'click');
-        simulate(this.choice_edit.get('boundingBox'), '.value', 'click');
-        asserted = false;
-        choicelist_lis.refresh();
-        choicelist_lis.each(test_li);
-        Assert.isTrue(asserted, "There was no current LI item");
-    }
-
-}));
-
-suite.add(new Y.Test.Case({
-
-    name: 'nullable_choice_edit',
-
-    setUp: function() {
-      // add the in-page HTML
-      var inpage = Y.Node.create([
-        '<p id="nullchoiceedit" style="margin-top: 25px">',
-        '  <img class="addicon" src="https://bugs.edge.launchpad.net/@@/add";>',
-        '  <span class="nulltext">Choose something</span>',
-        '  <span class="value" style="display:none" />',
-        '  <img class="editicon" style="display:none" src="https://bugs.edge.launchpad.net/@@/edit";>',
-        '</p>'].join(''));
-      Y.one("body").appendChild(inpage);
-      this.null_choice_edit = new Y.NullChoiceSource({
-        contentBox:  '#nullchoiceedit',
-        value:       null,
-        title:       'Choose something',
-        items: [
-          { name: 'Chico', value: 'chico', style: '',
-            help: '', disabled: false },
-          { name: 'Harpo', value: 'harpo', style: '',
-            help: '', disabled: false },
-          { name: 'Groucho', value: 'groucho', style: '',
-            help: '', disabled: false },
-          { name: 'Gummo', value: 'gummo', style: '',
-            help: '', disabled: false },
-          { name: 'Zeppo', value: 'zeppo', style: '',
-            help: '', disabled: false },
-          { name: 'Not funny!', value: null, style: '',
-            help: '', disabled: false }
-        ]
-      });
-
-      this.null_choice_edit.render();
-    },
-
-    tearDown: function() {
-        if (this.null_choice_edit._choice_list) {
-            cleanup_widget(this.null_choice_edit._choice_list);
-        }
-        var nullchoiceedit = Y.one("document").one("#nullchoiceedit");
-        if (nullchoiceedit) {
-            nullchoiceedit.get("parentNode").removeChild(nullchoiceedit);
-        }
-    },
-
-    test_can_be_instantiated: function() {
-        Assert.isInstanceOf(
-            Y.NullChoiceSource, this.null_choice_edit,
-            "NullChoiceSource not instantiated.");
-    },
-
-    test_action_icon: function() {
-        var that = this;
-
-        Assert.areEqual(
-            this.null_choice_edit.get('actionicon'),
-            this.null_choice_edit.get('addicon'),
-            'Action icon is not the add icon like expected.');
-
-        Assert.areEqual(
-            this.null_choice_edit.get('addicon').getStyle('display'),
-            'inline',
-            'Add icon is not visible when it should be');
-        Assert.areEqual(
-            this.null_choice_edit.get('editicon').getStyle('display'),
-            'none',
-            "Edit icon is visible when it shouldn't be");
-
-        simulate(this.null_choice_edit.get('boundingBox'),
-                 '.value', 'click');
-        simulate(this.null_choice_edit._choice_list.get('boundingBox'),
-          'li a[href$=groucho]', 'click');
-        this.null_choice_edit._uiClearWaiting();
-
-        Assert.areEqual(
-            this.null_choice_edit.get('actionicon'),
-            this.null_choice_edit.get('editicon'),
-            'Action icon is not the add icon like expected.');
-        Assert.areEqual(
-            this.null_choice_edit.get('addicon').getStyle('display'),
-            'none',
-            "Add icon is visible when it shouldn't be");
-        Assert.areEqual(
-            this.null_choice_edit.get('editicon').getStyle('display'),
-            'inline',
-            "Edit icon is not visible when it shouldn be");
-    },
-
-    test_null_item_absent: function() {
-        Assert.areEqual(
-            this.null_choice_edit.get('value'),
-            null,
-            "Selected value isn't null");
-
-        simulate(this.null_choice_edit.get('boundingBox'),
-                 '.value', 'click');
-        var remove_action_present = false;
-        this.null_choice_edit._choice_list.get(
-            'boundingBox').all('li a').each(function(item) {
-            if (item._value == null) {
-                remove_action_present = true;
-            }
-        });
-        Assert.isFalse(
-            remove_action_present,
-            'Remove item is present even when the current value is null.');
-    },
-
-    test_get_input_for_null: function() {
-        this.null_choice_edit.set('value', 'groucho');
-        Assert.areEqual(
-            'groucho',
-            this.null_choice_edit.getInput(),
-            "getInput() did not return the current value");
-        // Simulate choosing a null value and check that getInput()
-        // returns the new value.
-        this.null_choice_edit.onClick({button: 1, halt: function(){}});
-        this.null_choice_edit._choice_list.fire('valueChosen', null);
-        Assert.areEqual(
-            null,
-            this.null_choice_edit.getInput(),
-            "getInput() did not return the current (null) value");
-    }
-}));
-
-Y.lp.testing.Runner.run(suite);
-
+            });
+            Assert.isFalse(
+                remove_action_present,
+                'Remove item is present even when the current value is null.');
+        },
+
+        test_get_input_for_null: function() {
+            this.null_choice_edit.set('value', 'groucho');
+            Assert.areEqual(
+                'groucho',
+                this.null_choice_edit.getInput(),
+                "getInput() did not return the current value");
+            // Simulate choosing a null value and check that getInput()
+            // returns the new value.
+            this.null_choice_edit.onClick({button: 1, halt: function(){}});
+            this.null_choice_edit._choice_list.fire('valueChosen', null);
+            Assert.areEqual(
+                null,
+                this.null_choice_edit.getInput(),
+                "getInput() did not return the current (null) value");
+        }
+    }));
+
+
+
+}, '0.1', {
+    'requires': ['test', 'console', 'lp.choiceedit', 'node', 'event',
+        'event-simulate', 'widget-stack', 'lp.app.choice']
 });

=== modified file 'lib/lp/app/javascript/confirmationoverlay/tests/test_confirmationoverlay.html'
--- lib/lp/app/javascript/confirmationoverlay/tests/test_confirmationoverlay.html	2011-08-30 15:06:11 +0000
+++ lib/lp/app/javascript/confirmationoverlay/tests/test_confirmationoverlay.html	2012-02-07 16:00:29 +0000
@@ -1,39 +1,60 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.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>Confirmation 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>
-
-  <!-- dependent modules from lazr-->
-  <script type="text/javascript" src="../../lazr/lazr.js"></script>
-  <script type="text/javascript" src="../../overlay/overlay.js"></script>
-  <script type="text/javascript" src="../../formoverlay/formoverlay.js"></script>
-
-  <!-- The module under test -->
-  <script type="text/javascript" src="../confirmationoverlay.js"></script>
-
-  <!-- The test suite -->
-  <script type="text/javascript" src="test_confirmationoverlay.js"></script>
-
-</head>
-<body class="yui3-skin-sam">
-  <div id="placeholder" style="display:none;">
-  </div>
-
-  <script type="text/x-template" id="form-template">
-    <span id="test">content</span>
-    <form name="my-form">
-      <input type="checkbox" name="checkbox" value="checkbox" />
-      <input id="submit" type="submit" name="submit_name" value="submit_value"/>
-    </form>
-  </script>
-
-</body>
+      <title>Indicator confirmationoverlay</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/lazr/lazr.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/overlay/overlay.js"></script>
+      <script type="text/javascript" src="../../../../../../build/js/lp/app/formoverlay/formoverlay.js"></script>
+
+      <!-- The module under test. -->
+      <script type="text/javascript" src="../confirmationoverlay.js"></script>
+
+      <!-- Any css assert for this module. -->
+      <!-- <link rel="stylesheet" href="../assets/confirmationoverlay-core.css" /> -->
+
+      <!-- The test suite. -->
+      <script type="text/javascript" src="test_confirmationoverlay.js"></script>
+
+    </head>
+    <body class="yui3-skin-sam">
+        <ul id="suites">
+            <!-- <li>lp.large_indicator.test</li> -->
+            <li>lp.app.confirmationoverlay.test</li>
+        </ul>
+
+        <div id="placeholder" style="display:none;">
+        </div>
+
+        <script type="text/x-template" id="form-template">
+            <span id="test">content</span>
+            <form name="my-form">
+                <input type="checkbox" name="checkbox" value="checkbox" />
+                <input id="submit" type="submit" name="submit_name" value="submit_value"/>
+            </form>
+        </script>
+    </body>
 </html>

=== modified file 'lib/lp/app/javascript/confirmationoverlay/tests/test_confirmationoverlay.js'
--- lib/lp/app/javascript/confirmationoverlay/tests/test_confirmationoverlay.js	2011-09-13 10:58:15 +0000
+++ lib/lp/app/javascript/confirmationoverlay/tests/test_confirmationoverlay.js	2012-02-07 16:00:29 +0000
@@ -1,226 +1,229 @@
-/* Copyright (c) 2011, Canonical Ltd. All rights reserved. */
-
-YUI().use('lp.testing.runner', 'test', 'dump', 'console', 'node',
-          'lp.app.confirmationoverlay', 'event', 'event-simulate',
-          'node-event-simulate', function(Y) {
-
-var suite = new Y.Test.Suite("Confirmation Overlay Tests");
-
-var form_html = Y.one('#form-template').getContent();
-
-suite.add(new Y.Test.Case({
-
-    name: 'confirmation_overlay_basics',
-
-    setUp: function() {
-        Y.one("#placeholder")
-            .empty()
-            .append(Y.Node.create(form_html));
-        this.button = Y.one('#submit');
-        this.overlay = new Y.lp.app.confirmationoverlay.ConfirmationOverlay(
-            {button: this.button});
-    },
-
-    tearDown: function() {
-        this.overlay.destroy();
-    },
-
-    test_button_set: function() {
-        Y.ObjectAssert.areEqual(this.button, this.overlay.get('button'));
-    },
-
-    test_form_set: function() {
-        var form = Y.one("#placeholder").one('form');
-        Y.ObjectAssert.areEqual(form, this.overlay.get('submit_form'));
-    },
-
-    test_not_visible_by_default: function() {
-        Y.Assert.isFalse(this.overlay.get('visible'));
-    },
-
-    test_shown_when_button_clicked: function() {
-        this.button.simulate('click');
-        Y.Assert.isTrue(this.overlay.get('visible'));
-    },
-
-    test_hidden_field_added_on_ok: function() {
-        // When 'ok' (i.e. confirmation) is clicked, the Confirmation Overlay
-        // adds an additional field to the form to simulate the click on the
-        // right button.
-        this.button.simulate('click');
-
-        this.overlay.form_node.one('.ok-btn').simulate('click');
-        var hidden_input = this.overlay.get(
-            'submit_form').one('input.hidden-input');
-        var real_input = this.overlay.get('submit_form').one('input#submit');
-
-        Y.Assert.areEqual(
-            real_input.get('name'),
-            hidden_input.get('name'));
-        Y.Assert.areEqual(
-            real_input.get('value'),
-            hidden_input.get('value'));
-     },
-
-    test_call_submit_on_ok: function() {
-        // When 'ok' (i.e. confirmation) is clicked, the Confirmation Overlay
-        // submits the form.
-        // (Since we don't use YUI to make the request, we have to patch the
-        // form object to test it's submission (and prevent the form to be
-        // actually submitted.)
-        this.button.simulate('click');
-
-        var mockForm = Y.Mock();
-        Y.Mock.expect(mockForm, {
-            method: "submit"
-        });
-        Y.Mock.expect(mockForm, {
-            method: "append",
-            args: [Y.Mock.Value.Object]
-        });
-        this.overlay.set('submit_form', mockForm);
-        this.overlay.form_node.one('.ok-btn').simulate('click');
-
-        Y.Mock.verify(mockForm);
-    }
-
-}));
-
-suite.add(new Y.Test.Case({
-
-    name: 'confirmation_overlay_content_functions',
-
-    setUp: function() {
-        Y.one("#placeholder")
-            .empty()
-            .append(Y.Node.create(form_html));
-        this.button = Y.one('#submit');
-        this.getTestContent = function() {
-            return Y.one('span#test').get('text');
-        };
-        this.isTestNotEmpty = function() {
-            return Y.one('span#test').get('text') !== '';
-        };
-        this.overlay = null;
-     },
-
-    tearDown: function() {
-        // Each test is responsible for creating it's own overlay
-        // but the cleanup is done in a centralized fashion.
-        if (this.overlay !== null) {
-            this.overlay.destroy();
-        }
-    },
-
-    test_form_content_fn: function() {
-        this.overlay = new Y.lp.app.confirmationoverlay.ConfirmationOverlay({
-            button: this.button,
-            form_content_fn: this.getTestContent
-        });
-
-        Y.one('span#test').set('innerHTML', 'random content');
-        Y.Assert.areEqual('', this.overlay.get('form_content'));
-        this.button.simulate('click');
-        Y.Assert.areEqual('random content', this.overlay.get('form_content'));
-    },
-
-    test_header_content_fn: function() {
-        this.overlay = new Y.lp.app.confirmationoverlay.ConfirmationOverlay({
-            button: this.button,
-            header_content_fn: this.getTestContent
-        });
-
-        Y.one('span#test').set('innerHTML', 'random content');
-        Y.Assert.areEqual('', this.overlay.get('form_header'));
-        this.button.simulate('click');
-        Y.Assert.areEqual(
-            'random content',
-            this.overlay.get('headerContent').get('text').join(''));
-    },
-
-    test_do_not_display_fn: function() {
-        // The parameter display_confirmation_fn can be used
-        // to prevent the Confirmation Overlay from popping up.
-        this.overlay = new Y.lp.app.confirmationoverlay.ConfirmationOverlay({
-            button: this.button,
-            display_confirmation_fn: this.isTestNotEmpty
-        });
-
-        // Hack the form to prevent real submission.
-        Y.one('form').on('submit', function(e) {
-            e.preventDefault();
-        });
-
-        Y.one('span#test').set('innerHTML', '');
-        Y.Assert.isFalse(this.overlay.get('visible'));
-        this.button.simulate('click');
-
-        // The Overlay was not displayed.
-        Y.Assert.isFalse(this.overlay.get('visible'));
-    },
-
-    test_callback_called: function() {
-        // If submit_fn is passed to the constructor, call this function
-        // when the 'ok' is clicked instead of submitting the form.
-        var called = false;
-        var callback = function() {
-            called = true;
-        };
-        // Hack the form to record form submission.
-        var form_submitted = false;
-        Y.one('form').on('submit', function(e) {
-            form_submitted = true;
-            e.preventDefault();
-        });
-
-        this.overlay = new Y.lp.app.confirmationoverlay.ConfirmationOverlay({
-            button: this.button,
-            submit_fn: callback
-        });
-        this.button.simulate('click');
-        Y.Assert.isTrue(this.overlay.get('visible'));
-        this.overlay.form_node.one('.ok-btn').simulate('click');
-        Y.Assert.isFalse(this.overlay.get('visible'));
-        // The callback has been called.
-        Y.Assert.isTrue(called);
-        // The form has not been submitted.
-        Y.Assert.isFalse(form_submitted);
-    }
-
-}));
-
-suite.add(new Y.Test.Case({
-
-    name: 'confirmation_overlay_buttonless',
-
-    tearDown: function() {
-        if (this.overlay !== null) {
-            this.overlay.destroy();
-        }
-    },
-
-    test_callback_called: function() {
-        // A ConfirmationOverlay can be constructed without passing a button.
-        // The creator is responsible for calling show() manually.
-        var called = false;
-        var callback = function() {
-            called = true;
-        };
-
-        this.overlay = new Y.lp.app.confirmationoverlay.ConfirmationOverlay({
-            submit_fn: callback
-        });
-        Y.Assert.isFalse(this.overlay.get('visible'));
-        this.overlay.show();
-        Y.Assert.isTrue(this.overlay.get('visible'));
-        this.overlay.form_node.one('.ok-btn').simulate('click');
-        Y.Assert.isFalse(this.overlay.get('visible'));
-        // The callback has been called.
-        Y.Assert.isTrue(called);
-    }
-
-}));
-
-Y.lp.testing.Runner.run(suite);
-
+/* Copyright (c) 2012, Canonical Ltd. All rights reserved. */
+
+YUI.add('lp.app.confirmationoverlay.test', function (Y) {
+
+    var tests = Y.namespace('lp.app.confirmationoverlay.test');
+    tests.suite = new Y.Test.Suite('app.confirmationoverlay Tests');
+
+    var form_html = Y.one('#form-template').getContent();
+
+    tests.suite.add(new Y.Test.Case({
+        name: 'app.confirmationoverlay_tests',
+
+        setUp: function() {
+            Y.one("#placeholder")
+                .empty()
+                .append(Y.Node.create(form_html));
+            this.button = Y.one('#submit');
+            this.overlay = new Y.lp.app.confirmationoverlay.ConfirmationOverlay(
+                {button: this.button});
+        },
+
+        tearDown: function() {
+            this.overlay.destroy();
+        },
+
+        test_library_exists: function () {
+            Y.Assert.isObject(Y.lp.app.confirmationoverlay,
+                "We should be able to locate the lp.app.confirmationoverlay module");
+        },
+
+        test_button_set: function() {
+            Y.ObjectAssert.areEqual(this.button, this.overlay.get('button'));
+        },
+
+        test_form_set: function() {
+            var form = Y.one("#placeholder").one('form');
+            Y.ObjectAssert.areEqual(form, this.overlay.get('submit_form'));
+        },
+
+        test_not_visible_by_default: function() {
+            Y.Assert.isFalse(this.overlay.get('visible'));
+        },
+
+        test_shown_when_button_clicked: function() {
+            this.button.simulate('click');
+            Y.Assert.isTrue(this.overlay.get('visible'));
+        },
+
+        test_hidden_field_added_on_ok: function() {
+            // When 'ok' (i.e. confirmation) is clicked, the Confirmation Overlay
+            // adds an additional field to the form to simulate the click on the
+            // right button.
+            this.button.simulate('click');
+
+            this.overlay.form_node.one('.ok-btn').simulate('click');
+            var hidden_input = this.overlay.get(
+                'submit_form').one('input.hidden-input');
+            var real_input = this.overlay.get('submit_form').one('input#submit');
+
+            Y.Assert.areEqual(
+                real_input.get('name'),
+                hidden_input.get('name'));
+            Y.Assert.areEqual(
+                real_input.get('value'),
+                hidden_input.get('value'));
+         },
+
+        test_call_submit_on_ok: function() {
+            // When 'ok' (i.e. confirmation) is clicked, the Confirmation Overlay
+            // submits the form.
+            // (Since we don't use YUI to make the request, we have to patch the
+            // form object to test it's submission (and prevent the form to be
+            // actually submitted.)
+            this.button.simulate('click');
+
+            var mockForm = Y.Mock();
+            Y.Mock.expect(mockForm, {
+                method: "submit"
+            });
+            Y.Mock.expect(mockForm, {
+                method: "append",
+                args: [Y.Mock.Value.Object]
+            });
+            this.overlay.set('submit_form', mockForm);
+            this.overlay.form_node.one('.ok-btn').simulate('click');
+
+            Y.Mock.verify(mockForm);
+        }
+    }));
+
+    tests.suite.add(new Y.Test.Case({
+
+        name: 'confirmation_overlay_content_functions',
+
+        setUp: function() {
+            Y.one("#placeholder")
+                .empty()
+                .append(Y.Node.create(form_html));
+            this.button = Y.one('#submit');
+            this.getTestContent = function() {
+                return Y.one('span#test').get('text');
+            };
+            this.isTestNotEmpty = function() {
+                return Y.one('span#test').get('text') !== '';
+            };
+            this.overlay = null;
+         },
+
+        tearDown: function() {
+            // Each test is responsible for creating it's own overlay
+            // but the cleanup is done in a centralized fashion.
+            if (this.overlay !== null) {
+                this.overlay.destroy();
+            }
+        },
+
+        test_form_content_fn: function() {
+            this.overlay = new Y.lp.app.confirmationoverlay.ConfirmationOverlay({
+                button: this.button,
+                form_content_fn: this.getTestContent
+            });
+
+            Y.one('span#test').set('innerHTML', 'random content');
+            Y.Assert.areEqual('', this.overlay.get('form_content'));
+            this.button.simulate('click');
+            Y.Assert.areEqual('random content', this.overlay.get('form_content'));
+        },
+
+        test_header_content_fn: function() {
+            this.overlay = new Y.lp.app.confirmationoverlay.ConfirmationOverlay({
+                button: this.button,
+                header_content_fn: this.getTestContent
+            });
+
+            Y.one('span#test').set('innerHTML', 'random content');
+            Y.Assert.areEqual('', this.overlay.get('form_header'));
+            this.button.simulate('click');
+            Y.Assert.areEqual(
+                'random content',
+                this.overlay.get('headerContent').get('text').join(''));
+        },
+
+        test_do_not_display_fn: function() {
+            // The parameter display_confirmation_fn can be used
+            // to prevent the Confirmation Overlay from popping up.
+            this.overlay = new Y.lp.app.confirmationoverlay.ConfirmationOverlay({
+                button: this.button,
+                display_confirmation_fn: this.isTestNotEmpty
+            });
+
+            // Hack the form to prevent real submission.
+            Y.one('form').on('submit', function(e) {
+                e.preventDefault();
+            });
+
+            Y.one('span#test').set('innerHTML', '');
+            Y.Assert.isFalse(this.overlay.get('visible'));
+            this.button.simulate('click');
+
+            // The Overlay was not displayed.
+            Y.Assert.isFalse(this.overlay.get('visible'));
+        },
+
+        test_callback_called: function() {
+            // If submit_fn is passed to the constructor, call this function
+            // when the 'ok' is clicked instead of submitting the form.
+            var called = false;
+            var callback = function() {
+                called = true;
+            };
+            // Hack the form to record form submission.
+            var form_submitted = false;
+            Y.one('form').on('submit', function(e) {
+                form_submitted = true;
+                e.preventDefault();
+            });
+
+            this.overlay = new Y.lp.app.confirmationoverlay.ConfirmationOverlay({
+                button: this.button,
+                submit_fn: callback
+            });
+            this.button.simulate('click');
+            Y.Assert.isTrue(this.overlay.get('visible'));
+            this.overlay.form_node.one('.ok-btn').simulate('click');
+            Y.Assert.isFalse(this.overlay.get('visible'));
+            // The callback has been called.
+            Y.Assert.isTrue(called);
+            // The form has not been submitted.
+            Y.Assert.isFalse(form_submitted);
+        }
+
+    }));
+
+    tests.suite.add(new Y.Test.Case({
+
+        name: 'confirmation_overlay_buttonless',
+
+        tearDown: function() {
+            if (this.overlay !== null) {
+                this.overlay.destroy();
+            }
+        },
+
+        test_callback_called: function() {
+            // A ConfirmationOverlay can be constructed without passing a button.
+            // The creator is responsible for calling show() manually.
+            var called = false;
+            var callback = function() {
+                called = true;
+            };
+
+            this.overlay = new Y.lp.app.confirmationoverlay.ConfirmationOverlay({
+                submit_fn: callback
+            });
+            Y.Assert.isFalse(this.overlay.get('visible'));
+            this.overlay.show();
+            Y.Assert.isTrue(this.overlay.get('visible'));
+            this.overlay.form_node.one('.ok-btn').simulate('click');
+            Y.Assert.isFalse(this.overlay.get('visible'));
+            // The callback has been called.
+            Y.Assert.isTrue(called);
+        }
+
+    }));
+
+}, '0.1', {
+    'requires': ['test', 'console', 'lp.app.confirmationoverlay',
+        'dump', 'node', 'event', 'event-simulate', 'node-event-simulate']
 });

=== modified file 'lib/lp/app/javascript/picker/tests/test_personpicker.html'
--- lib/lp/app/javascript/picker/tests/test_personpicker.html	2011-08-10 13:33:26 +0000
+++ lib/lp/app/javascript/picker/tests/test_personpicker.html	2012-02-07 16:00:29 +0000
@@ -1,50 +1,85 @@
+<!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>Launchpad PersonPicker</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>
-
-    <!-- Some required dependencies -->
-    <script type="text/javascript" src="../../client.js"></script>
-    <script type="text/javascript" src="../../lp.js"></script>
-    <script type="text/javascript" src="../../activator/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="../../overlay/overlay.js"></script>
-    <script type="text/javascript" src="../../effects/effects.js"></script>
-    <script type="text/javascript" src="../../extras/extras.js"></script>
-    <script type="text/javascript" src="../../expander.js"></script>
-    <script type="text/javascript" src="../picker.js"></script>
-    <script type="text/javascript" src="../picker_patcher.js"></script>
-
-    <!-- The module under test -->
-    <script type="text/javascript" src="../person_picker.js"></script>
-
-    <!-- The test suite -->
-    <script type="text/javascript" src="test_personpicker.js"></script>
-  </head>
-<body class="yui3-skin-sam">
-  <div class="yui3-widget yui3-activator yui3-activator-focused">
-      <span id="picker_id" class="yui3-activator-content yui3-activator-success">
-        <span id="pickertest">
-          <span>
-            A picker widget test
-            <button id="edit-pickertest-btn"
-                    class="lazr-btn yui3-activator-act yui3-activator-hidden">
-              Edit
-            </button>
-          </span>
-          <span class="yui3-activator-data-box">
-          </span>
-          <span class="yui3-activator-message-box yui3-activator-hidden"></span>
-        </span>
-      </span>
-  </div>
-</body>
+      <title>test person_picker</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/client.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/lp.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/activator/activator.js"></script>
+      <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/overlay/overlay.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/effects/effect.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/expander.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/picker/picker.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/picker/picker_patcher.js"></script>
+
+
+      <!-- The module under test. -->
+      <script type="text/javascript" src="../person_picker.js"></script>
+
+      <!-- Any css assert for this module. -->
+      <!-- <link rel="stylesheet" href="../assets/person_picker-core.css" /> -->
+
+      <!-- The test suite. -->
+      <script type="text/javascript" src="test_personpicker.js"></script>
+
+    </head>
+    <body class="yui3-skin-sam">
+        <ul id="suites">
+            <!-- <li>lp.large_indicator.test</li> -->
+            <li>lp.person_picker.test</li>
+        </ul>
+
+        <div class="yui3-widget yui3-activator yui3-activator-focused">
+            <span id="picker_id" class="yui3-activator-content yui3-activator-success">
+              <span id="pickertest">
+                <span>
+                  A picker widget test
+                  <button id="edit-pickertest-btn"
+                          class="lazr-btn yui3-activator-act yui3-activator-hidden">
+                    Edit
+                  </button>
+                </span>
+                <span class="yui3-activator-data-box">
+                </span>
+                <span class="yui3-activator-message-box yui3-activator-hidden"></span>
+              </span>
+            </span>
+        </div>
+
+    </body>
 </html>

=== modified file 'lib/lp/app/javascript/picker/tests/test_personpicker.js'
--- lib/lp/app/javascript/picker/tests/test_personpicker.js	2011-09-19 00:35:49 +0000
+++ lib/lp/app/javascript/picker/tests/test_personpicker.js	2012-02-07 16:00:29 +0000
@@ -6,473 +6,477 @@
            'lazr.picker', 'lazr.person-picker', 'lp.app.picker',
            'node-event-simulate', function(Y) {
 
-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();
-    var data_box = Y.one('#picker_id .yui3-activator-data-box');
-    var link = data_box.one('a');
-    if (link) {
-        link.get('parentNode').removeChild(link);
-    }
-}
-
-/*
- * 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);
-}
-
-var suite = new Y.Test.Suite("PersonPicker Tests");
-
-/*
- * Test cases for person picker functionality.
- */
-var commonPersonPickerTests = {
-
-    name: 'common_person_picker',
-
-    setUp: function() {
-        this.ME = '/~me';
-        window.LP = {
-                links: {me: this.ME},
-            cache: {}
-        };
-        this.vocabulary = [
-            {
-                "value": "me",
-                "metadata": "person",
-                "title": "Me",
-                "css": "sprite-person",
-                "description": "me@xxxxxxxxxxx",
-                "api_uri": "/~me"
-            },
-            {
-                "value": "someteam",
-                "metadata": "team",
-                "title": "Some Team",
-                "css": "sprite-team",
-                "description": "someone@xxxxxxxxxxx",
-                "api_uri": "/~someteam"
-            }
-        ];
-
-        // We patch Launchpad client to return some fake data for the patch
-        // operation.
-        Y.lp.client.Launchpad = function() {};
-        Y.lp.client.Launchpad.prototype.patch =
-            function(uri, representation, config, headers) {
-                // our setup assumes success, so we just do the success
-                // callback.
-                var entry_repr = {
-                  'test_link': representation.test_link,
-                  'lp_html': {
-                      'test_link':
-                          '<a href="' + representation.test_link +
-                              '">Content</a>'}
-                };
-                var result = new Y.lp.client.Entry(
-                    null, entry_repr, "a_self_link");
-                config.on.success(result);
-            };
-    },
-
-    tearDown: function() {
-        cleanup_widget(this.picker);
-        delete window.LP;
-    },
-
-    _picker_params: function(
-        show_assign_me_button, show_remove_button,
-        selected_value, selected_value_metadata) {
-        return {
-            "show_assign_me_button": show_assign_me_button,
-            "show_remove_button": show_remove_button,
-            "selected_value": selected_value,
-            "selected_value_metadata": selected_value_metadata
-        };
-    },
-
-    _check_button_state: function(btn_class, is_visible) {
-        var assign_me_button = Y.one(btn_class);
-        Assert.isNotNull(assign_me_button);
-        if (is_visible) {
-            Assert.isFalse(
-                assign_me_button.hasClass('yui3-picker-hidden'),
-                btn_class + " should be visible but is hidden");
-        } else {
-            Assert.isTrue(
-                assign_me_button.hasClass('yui3-picker-hidden'),
-                btn_class + " should be hidden but is visible");
-        }
-    },
-
-    _check_assign_me_button_state: function(is_visible) {
-        this._check_button_state('.yui-picker-assign-me-button', is_visible);
-    },
-
-    _check_remove_button_state: function(is_visible) {
-        this._check_button_state('.yui-picker-remove-button', is_visible);
-    },
-
-    test_min_search_chars: function() {
-        // The minimum search term is 2 characters.
-        this.create_picker(this._picker_params(true, true));
-        Assert.areEqual(2, this.picker.get('min_search_chars'));
-    },
-
-    test_search_field_focus: function () {
-        // The search field has focus when the picker is shown.
-        this.create_picker(this._picker_params(true, true));
-        this.picker.render();
-        this.picker.hide();
-
-        var got_focus = false;
-        this.picker._search_input.on('focus', function(e) {
-            got_focus = true;
-        });
-        this.picker.show();
-        Y.Assert.isTrue(got_focus, "search input did not get focus.");
-    },
-
-    test_buttons_save: function () {
-        // The assign/remove links save the correct values.
-        this.create_picker(this._picker_params(true, true));
-        this.picker.render();
-        this.picker.show();
-
-        // Patch the picker so the assign_me and remove methods can be
-        // tested.
-        var data = null;
-        this.picker.on('save', function (result) {
-            data = result.value;
-        });
-        var remove = Y.one('.yui-picker-remove-button');
-        remove.simulate('click');
-        Y.Assert.areEqual(null, data);
-
-        var assign_me = Y.one('.yui-picker-assign-me-button');
-        assign_me.simulate('click');
-        Y.Assert.areEqual('me', data);
-    },
-
-    test_picker_assign_me_button_text: function() {
-        // The assign me button text is correct.
-        this.create_picker(this._picker_params(true, true));
-        this.picker.render();
-        var assign_me_button = Y.one('.yui-picker-assign-me-button');
-        Assert.areEqual('Assign Moi', assign_me_button.get('innerHTML'));
-    },
-
-    test_picker_assign_me_button_not_shown_when_not_logged_in: function() {
-        // The assign me button is hidden when the user is not logged-in.
-        delete window.LP.links.me;  // Log-out.
-        this.create_picker(this._picker_params(true, true));
-        this.picker.render();
-        var assign_me_button = Y.one('.yui-picker-assign-me-button');
-        Assert.isNull(assign_me_button);
-    },
-
-    test_picker_remove_person_button_text: function() {
-        // The remove button text is correct.
-        this.create_picker(this._picker_params(true, true, "fred", "person"));
-        this.picker.render();
-        var remove_button = Y.one('.yui-picker-remove-button');
-        Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
-    },
-
-    test_picker_remove_team_button_text: function() {
-        // The remove button text is correct.
-        this.create_picker(this._picker_params(true, true, "cats", "team"));
-        this.picker.render();
-        var remove_button = Y.one('.yui-picker-remove-button');
-        Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
-    },
-
-    test_picker_has_assign_me_button: function() {
-        // The assign me button is shown.
-        this.create_picker(this._picker_params(true, true));
-        this.picker.render();
-        this._check_assign_me_button_state(true);
-    },
-
-    test_picker_no_assign_me_button_unless_configured: function() {
-        // The assign me button is only rendered if show_assign_me_button
-        // config setting is true.
-        this.create_picker(this._picker_params(false, true));
-        this.picker.render();
-        Assert.isNull(Y.one('.yui-picker-assign-me-button'));
-    },
-
-    test_picker_no_assign_me_button_if_value_is_me: function() {
-        // The assign me button is not shown if the picker is created for a
-        // field where the value is "me".
-        this.create_picker(this._picker_params(true, true, "me"), this.ME);
-        this.picker.render();
-        this._check_assign_me_button_state(false);
-    },
-
-    test_picker_no_remove_button_if_null_value: function() {
-        // The remove button is not shown if the picker is created for a field
-        // which has a null value.
-        this.create_picker(this._picker_params(true, true));
-        this.picker.render();
-        this._check_remove_button_state(false);
-    },
-
-    test_picker_has_remove_button_if_value: function() {
-        // The remove button is shown if the picker is created for a field
-        // which has a value.
-        this.create_picker(this._picker_params(true, true, "me"), this.ME);
-        this.picker.render();
-        this._check_remove_button_state(true);
-    },
-
-    test_picker_no_remove_button_unless_configured: function() {
-        // The remove button is only rendered if show_remove_button setting is
-        // true.
-        this.create_picker(this._picker_params(true, false, "me"), this.ME);
-        this.picker.render();
-        Assert.isNull(Y.one('.yui-picker-remove-button'));
-    }
-};
-
-/*
- * Test cases for person picker functionality when created using
- * addPickerPatcher.
- */
-var pickerPatcherPersonPickerTests = {
-
-    name: 'picker_patcher_person_picker',
-
-    create_picker: function(params, field_value) {
-        if (field_value !== undefined) {
-            var data_box = Y.one('#picker_id .yui3-activator-data-box');
-            data_box.appendChild(Y.Node.create('<a>Content</a>'));
-            data_box.one('a').set('href', field_value);
-        }
-
-        var config = {
-            "picker_type": "person",
-            "step_title": "Choose someone",
-            "header": "Pick Someone",
-            "validate_callback": null,
-            "show_search_box": true,
-            "show_assign_me_button": params.show_assign_me_button,
-            "show_remove_button": params.show_remove_button,
-            "selected_value": params.selected_value,
-            "selected_value_metadata": params.selected_value_metadata,
-            "assign_me_text": "Assign Moi",
-            "remove_person_text": "Remove someone",
-            "remove_team_text": "Remove some team"
-            };
-        this.picker = Y.lp.app.picker.addPickerPatcher(
-                this.vocabulary,
-                "foo/bar",
-                "test_link",
-                "picker_id",
-                config);
-    },
-
-    test_picker_assign_me_button_hide_on_save: function() {
-        // The assign me button is shown initially but hidden if the picker
-        // saves a value equal to 'me'.
-        this.create_picker(this._picker_params(true, true));
-        this._check_assign_me_button_state(true);
-        this.picker.set('results', this.vocabulary);
-        this.picker.render();
-        simulate(
-            this.picker.get('boundingBox').one('.yui3-picker-results'),
-                'li:nth-child(1)', 'click');
-        this._check_assign_me_button_state(false);
-    },
-
-    test_picker_remove_button_clicked: function() {
-        // The remove button is hidden once a picker value has been removed.
-        // And the assign me button is shown.
-        this.create_picker(this._picker_params(true, true, "me"), this.ME);
-        this.picker.render();
-        this._check_assign_me_button_state(false);
-        var remove = Y.one('.yui-picker-remove-button');
-        remove.simulate('click');
-        this._check_remove_button_state(false);
-        this._check_assign_me_button_state(true);
-    },
-
-    test_picker_assign_me_button_clicked: function() {
-        // The assign me button is hidden once it is clicked.
-        // And the remove button is shown.
-        this.create_picker(this._picker_params(true, true));
-        this.picker.render();
-        var assign_me = Y.one('.yui-picker-assign-me-button');
-        assign_me.simulate('click');
-        this._check_remove_button_state(true);
-        this._check_assign_me_button_state(false);
-    },
-
-    test_picker_assign_me_updates_remove_text: function() {
-        // When Assign me is used, the Remove button text is updated from
-        // the team removal text to the person removal text.
-        this.create_picker(this._picker_params(true, true, "cats", "team"));
-        this.picker.render();
-        var remove_button = Y.one('.yui-picker-remove-button');
-        Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
-        var assign_me = Y.one('.yui-picker-assign-me-button');
-        assign_me.simulate('click');
-        Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
-    },
-
-    test_picker_save_updates_remove_text: function() {
-        // When save is called, the Remove button text is updated according to
-        // the newly saved value.
-        this.create_picker(this._picker_params(true, true, "me"), this.ME);
-        var remove_button = Y.one('.yui-picker-remove-button');
-        Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
-        this.picker.set('results', this.vocabulary);
-        this.picker.render();
-        simulate(
-            this.picker.get('boundingBox').one('.yui3-picker-results'),
-                'li:nth-child(2)', 'click');
-        Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
-    }
-};
-
-/*
- * Test cases for person picker functionality when created using
- * addPickerPatcher.
- */
-var createDirectPersonPickerTests = {
-
-    name: 'create_direct_person_picker',
-
-    tearDown: function() {
-        cleanup_widget(this.picker);
-        this.search_input.remove();
-        delete window.LP;
-    },
-
-    create_picker: function(params, field_value) {
-        var associated_field_id;
-        this.search_input = Y.Node.create(
-                '<input id="field_initval" value="foo"/>');
-        Y.one(document.body).appendChild(this.search_input);
-        associated_field_id = 'field_initval';
-        var text_field = Y.one('#field_initval');
-        if (field_value !== undefined) {
-            text_field.set('text', field_value);
-        }
-        var config = {
-            "picker_type": "person",
-            "associated_field_id": associated_field_id,
-            "show_assign_me_button": params.show_assign_me_button,
-            "show_remove_button": params.show_remove_button,
-            "selected_value": params.selected_value,
-            "selected_value_metadata": params.selected_value_metadata,
-            "assign_me_text": "Assign Moi",
-            "remove_person_text": "Remove someone",
-            "remove_team_text": "Remove some team"
-            };
-        this.picker = Y.lp.app.picker.create(
-                            this.vocabulary, config, associated_field_id);
-    },
-
-    test_picker_assign_me_button_hide_on_save: function() {
-        // The assign me button is shown initially but hidden if the picker
-        // saves a value equal to 'me'.
-        this.create_picker(this._picker_params(true, true));
-        this._check_assign_me_button_state(true);
-        this.picker.set('results', this.vocabulary);
-        this.picker.render();
-        simulate(
-            this.picker.get('boundingBox').one('.yui3-picker-results'),
-                'li:nth-child(1)', 'click');
-        this._check_assign_me_button_state(false);
-    },
-
-    test_picker_remove_button_clicked: function() {
-        // The remove button is hidden once a picker value has been removed.
-        // And the assign me button is shown.
-        this.create_picker(this._picker_params(true, true, "me"), this.ME);
-        this.picker.render();
-        this._check_assign_me_button_state(false);
-        var remove = Y.one('.yui-picker-remove-button');
-        remove.simulate('click');
-        this._check_remove_button_state(false);
-        this._check_assign_me_button_state(true);
-    },
-
-    test_picker_assign_me_button_clicked: function() {
-        // The assign me button is hidden once it is clicked.
-        // And the remove button is shown.
-        this.create_picker(this._picker_params(true, true));
-        this.picker.render();
-        var assign_me = Y.one('.yui-picker-assign-me-button');
-        assign_me.simulate('click');
-        this._check_remove_button_state(true);
-        this._check_assign_me_button_state(false);
-    },
-
-    test_picker_assign_me_updates_remove_text: function() {
-        // When Assign me is used, the Remove button text is updated from
-        // the team removal text to the person removal text.
-        this.create_picker(this._picker_params(true, true, "cats", "team"));
-        this.picker.render();
-        var remove_button = Y.one('.yui-picker-remove-button');
-        Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
-        var assign_me = Y.one('.yui-picker-assign-me-button');
-        assign_me.simulate('click');
-        Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
-    },
-
-    test_picker_save_updates_remove_text: function() {
-        // When save is called, the Remove button text is updated according to
-        // the newly saved value.
-        this.create_picker(this._picker_params(true, true, "me"), this.ME);
-        var remove_button = Y.one('.yui-picker-remove-button');
-        Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
-        this.picker.set('results', this.vocabulary);
-        this.picker.render();
-        simulate(
-            this.picker.get('boundingBox').one('.yui3-picker-results'),
-                'li:nth-child(2)', 'click');
-        Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
-    }
-};
-
-suite.add(new Y.Test.Case(
-  Y.merge(
-      commonPersonPickerTests,
-      pickerPatcherPersonPickerTests)
-));
-
-suite.add(new Y.Test.Case(
-  Y.merge(
-      commonPersonPickerTests,
-      createDirectPersonPickerTests)
-));
-
-// Hook for the test runner to get test results.
-var handle_complete = function(data) {
-    window.status = '::::' + JSON.stringify(data);
-};
-Y.Test.Runner.on('complete', handle_complete);
-Y.Test.Runner.add(suite);
-
-var console = new Y.Console({newestOnTop: false});
-console.render('#log');
-
-Y.on('domready', function() {
-    Y.Test.Runner.run();
-});
+    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();
+        var data_box = Y.one('#picker_id .yui3-activator-data-box');
+        var link = data_box.one('a');
+        if (link) {
+            link.get('parentNode').removeChild(link);
+        }
+    }
+
+    /*
+     * 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);
+    }
+
+    var suite = new Y.Test.Suite("PersonPicker Tests");
+
+    /*
+     * Test cases for person picker functionality.
+     */
+    var commonPersonPickerTests = {
+
+        name: 'common_person_picker',
+
+        setUp: function() {
+            this.ME = '/~me';
+            window.LP = {
+                    links: {me: this.ME},
+                cache: {}
+            };
+            this.vocabulary = [
+                {
+                    "value": "me",
+                    "metadata": "person",
+                    "title": "Me",
+                    "css": "sprite-person",
+                    "description": "me@xxxxxxxxxxx",
+                    "api_uri": "/~me"
+                },
+                {
+                    "value": "someteam",
+                    "metadata": "team",
+                    "title": "Some Team",
+                    "css": "sprite-team",
+                    "description": "someone@xxxxxxxxxxx",
+                    "api_uri": "/~someteam"
+                }
+            ];
+
+            // We patch Launchpad client to return some fake data for the patch
+            // operation.
+            Y.lp.client.Launchpad = function() {};
+            Y.lp.client.Launchpad.prototype.patch =
+                function(uri, representation, config, headers) {
+                    // our setup assumes success, so we just do the success
+                    // callback.
+                    var entry_repr = {
+                      'test_link': representation.test_link,
+                      'lp_html': {
+                          'test_link':
+                              '<a href="' + representation.test_link +
+                                  '">Content</a>'}
+                    };
+                    var result = new Y.lp.client.Entry(
+                        null, entry_repr, "a_self_link");
+                    config.on.success(result);
+                };
+        },
+
+        tearDown: function() {
+            cleanup_widget(this.picker);
+            delete window.LP;
+        },
+
+        _picker_params: function(
+            show_assign_me_button, show_remove_button,
+            selected_value, selected_value_metadata) {
+            return {
+                "show_assign_me_button": show_assign_me_button,
+                "show_remove_button": show_remove_button,
+                "selected_value": selected_value,
+                "selected_value_metadata": selected_value_metadata
+            };
+        },
+
+        _check_button_state: function(btn_class, is_visible) {
+            var assign_me_button = Y.one(btn_class);
+            Assert.isNotNull(assign_me_button);
+            if (is_visible) {
+                Assert.isFalse(
+                    assign_me_button.hasClass('yui3-picker-hidden'),
+                    btn_class + " should be visible but is hidden");
+            } else {
+                Assert.isTrue(
+                    assign_me_button.hasClass('yui3-picker-hidden'),
+                    btn_class + " should be hidden but is visible");
+            }
+        },
+
+        _check_assign_me_button_state: function(is_visible) {
+            this._check_button_state('.yui-picker-assign-me-button',
+                is_visible);
+        },
+
+        _check_remove_button_state: function(is_visible) {
+            this._check_button_state('.yui-picker-remove-button', is_visible);
+        },
+
+        test_min_search_chars: function() {
+            // The minimum search term is 2 characters.
+            this.create_picker(this._picker_params(true, true));
+            Assert.areEqual(2, this.picker.get('min_search_chars'));
+        },
+
+        test_search_field_focus: function () {
+            // The search field has focus when the picker is shown.
+            this.create_picker(this._picker_params(true, true));
+            this.picker.render();
+            this.picker.hide();
+
+            var got_focus = false;
+            this.picker._search_input.on('focus', function(e) {
+                got_focus = true;
+            });
+            this.picker.show();
+            Y.Assert.isTrue(got_focus, "search input did not get focus.");
+        },
+
+        test_buttons_save: function () {
+            // The assign/remove links save the correct values.
+            this.create_picker(this._picker_params(true, true));
+            this.picker.render();
+            this.picker.show();
+
+            // Patch the picker so the assign_me and remove methods can be
+            // tested.
+            var data = null;
+            this.picker.on('save', function (result) {
+                data = result.value;
+            });
+            var remove = Y.one('.yui-picker-remove-button');
+            remove.simulate('click');
+            Y.Assert.areEqual(null, data);
+
+            var assign_me = Y.one('.yui-picker-assign-me-button');
+            assign_me.simulate('click');
+            Y.Assert.areEqual('me', data);
+        },
+
+        test_picker_assign_me_button_text: function() {
+            // The assign me button text is correct.
+            this.create_picker(this._picker_params(true, true));
+            this.picker.render();
+            var assign_me_button = Y.one('.yui-picker-assign-me-button');
+            Assert.areEqual('Assign Moi', assign_me_button.get('innerHTML'));
+        },
+
+        test_picker_assign_me_button_not_shown_when_not_logged_in: function() {
+            // The assign me button is hidden when the user is not logged-in.
+            delete window.LP.links.me;  // Log-out.
+            this.create_picker(this._picker_params(true, true));
+            this.picker.render();
+            var assign_me_button = Y.one('.yui-picker-assign-me-button');
+            Assert.isNull(assign_me_button);
+        },
+
+        test_picker_remove_person_button_text: function() {
+            // The remove button text is correct.
+            this.create_picker(this._picker_params(true,
+                true,
+                "fred",
+                "person"));
+            this.picker.render();
+            var remove_button = Y.one('.yui-picker-remove-button');
+            Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
+        },
+
+        test_picker_remove_team_button_text: function() {
+            // The remove button text is correct.
+            this.create_picker(this._picker_params(true, true, "cats", "team"));
+            this.picker.render();
+            var remove_button = Y.one('.yui-picker-remove-button');
+            Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
+        },
+
+        test_picker_has_assign_me_button: function() {
+            // The assign me button is shown.
+            this.create_picker(this._picker_params(true, true));
+            this.picker.render();
+            this._check_assign_me_button_state(true);
+        },
+
+        test_picker_no_assign_me_button_unless_configured: function() {
+            // The assign me button is only rendered if show_assign_me_button
+            // config setting is true.
+            this.create_picker(this._picker_params(false, true));
+            this.picker.render();
+            Assert.isNull(Y.one('.yui-picker-assign-me-button'));
+        },
+
+        test_picker_no_assign_me_button_if_value_is_me: function() {
+            // The assign me button is not shown if the picker is created for a
+            // field where the value is "me".
+            this.create_picker(this._picker_params(true, true, "me"), this.ME);
+            this.picker.render();
+            this._check_assign_me_button_state(false);
+        },
+
+        test_picker_no_remove_button_if_null_value: function() {
+            // The remove button is not shown if the picker is created for a
+            // field which has a null value.
+            this.create_picker(this._picker_params(true, true));
+            this.picker.render();
+            this._check_remove_button_state(false);
+        },
+
+        test_picker_has_remove_button_if_value: function() {
+            // The remove button is shown if the picker is created for a field
+            // which has a value.
+            this.create_picker(this._picker_params(true, true, "me"), this.ME);
+            this.picker.render();
+            this._check_remove_button_state(true);
+        },
+
+        test_picker_no_remove_button_unless_configured: function() {
+            // The remove button is only rendered if show_remove_button
+            // setting is true.
+            this.create_picker(this._picker_params(true, false, "me"), this.ME);
+            this.picker.render();
+            Assert.isNull(Y.one('.yui-picker-remove-button'));
+        }
+    };
+
+    /*
+     * Test cases for person picker functionality when created using
+     * addPickerPatcher.
+     */
+    var pickerPatcherPersonPickerTests = {
+
+        name: 'picker_patcher_person_picker',
+
+        create_picker: function(params, field_value) {
+            if (field_value !== undefined) {
+                var data_box = Y.one('#picker_id .yui3-activator-data-box');
+                data_box.appendChild(Y.Node.create('<a>Content</a>'));
+                data_box.one('a').set('href', field_value);
+            }
+
+            var config = {
+                "picker_type": "person",
+                "step_title": "Choose someone",
+                "header": "Pick Someone",
+                "validate_callback": null,
+                "show_search_box": true,
+                "show_assign_me_button": params.show_assign_me_button,
+                "show_remove_button": params.show_remove_button,
+                "selected_value": params.selected_value,
+                "selected_value_metadata": params.selected_value_metadata,
+                "assign_me_text": "Assign Moi",
+                "remove_person_text": "Remove someone",
+                "remove_team_text": "Remove some team"
+                };
+            this.picker = Y.lp.app.picker.addPickerPatcher(
+                    this.vocabulary,
+                    "foo/bar",
+                    "test_link",
+                    "picker_id",
+                    config);
+        },
+
+        test_picker_assign_me_button_hide_on_save: function() {
+            // The assign me button is shown initially but hidden if the picker
+            // saves a value equal to 'me'.
+            this.create_picker(this._picker_params(true, true));
+            this._check_assign_me_button_state(true);
+            this.picker.set('results', this.vocabulary);
+            this.picker.render();
+            simulate(
+                this.picker.get('boundingBox').one('.yui3-picker-results'),
+                    'li:nth-child(1)', 'click');
+            this._check_assign_me_button_state(false);
+        },
+
+        test_picker_remove_button_clicked: function() {
+            // The remove button is hidden once a picker value has been removed.
+            // And the assign me button is shown.
+            this.create_picker(this._picker_params(true, true, "me"), this.ME);
+            this.picker.render();
+            this._check_assign_me_button_state(false);
+            var remove = Y.one('.yui-picker-remove-button');
+            remove.simulate('click');
+            this._check_remove_button_state(false);
+            this._check_assign_me_button_state(true);
+        },
+
+        test_picker_assign_me_button_clicked: function() {
+            // The assign me button is hidden once it is clicked.
+            // And the remove button is shown.
+            this.create_picker(this._picker_params(true, true));
+            this.picker.render();
+            var assign_me = Y.one('.yui-picker-assign-me-button');
+            assign_me.simulate('click');
+            this._check_remove_button_state(true);
+            this._check_assign_me_button_state(false);
+        },
+
+        test_picker_assign_me_updates_remove_text: function() {
+            // When Assign me is used, the Remove button text is updated from
+            // the team removal text to the person removal text.
+            this.create_picker(this._picker_params(true, true, "cats", "team"));
+            this.picker.render();
+            var remove_button = Y.one('.yui-picker-remove-button');
+            Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
+            var assign_me = Y.one('.yui-picker-assign-me-button');
+            assign_me.simulate('click');
+            Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
+        },
+
+        test_picker_save_updates_remove_text: function() {
+            // When save is called, the Remove button text is updated
+            // according to the newly saved value.
+            this.create_picker(this._picker_params(true, true, "me"), this.ME);
+            var remove_button = Y.one('.yui-picker-remove-button');
+            Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
+            this.picker.set('results', this.vocabulary);
+            this.picker.render();
+            simulate(
+                this.picker.get('boundingBox').one('.yui3-picker-results'),
+                    'li:nth-child(2)', 'click');
+            Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
+        }
+    };
+
+    /*
+     * Test cases for person picker functionality when created using
+     * addPickerPatcher.
+     */
+    var createDirectPersonPickerTests = {
+
+        name: 'create_direct_person_picker',
+
+        tearDown: function() {
+            cleanup_widget(this.picker);
+            this.search_input.remove();
+            delete window.LP;
+        },
+
+        create_picker: function(params, field_value) {
+            var associated_field_id;
+            this.search_input = Y.Node.create(
+                    '<input id="field_initval" value="foo"/>');
+            Y.one(document.body).appendChild(this.search_input);
+            associated_field_id = 'field_initval';
+            var text_field = Y.one('#field_initval');
+            if (field_value !== undefined) {
+                text_field.set('text', field_value);
+            }
+            var config = {
+                "picker_type": "person",
+                "associated_field_id": associated_field_id,
+                "show_assign_me_button": params.show_assign_me_button,
+                "show_remove_button": params.show_remove_button,
+                "selected_value": params.selected_value,
+                "selected_value_metadata": params.selected_value_metadata,
+                "assign_me_text": "Assign Moi",
+                "remove_person_text": "Remove someone",
+                "remove_team_text": "Remove some team"
+                };
+            this.picker = Y.lp.app.picker.create(
+                                this.vocabulary, config, associated_field_id);
+        },
+
+        test_picker_assign_me_button_hide_on_save: function() {
+            // The assign me button is shown initially but hidden if the picker
+            // saves a value equal to 'me'.
+            this.create_picker(this._picker_params(true, true));
+            this._check_assign_me_button_state(true);
+            this.picker.set('results', this.vocabulary);
+            this.picker.render();
+            simulate(
+                this.picker.get('boundingBox').one('.yui3-picker-results'),
+                    'li:nth-child(1)', 'click');
+            this._check_assign_me_button_state(false);
+        },
+
+        test_picker_remove_button_clicked: function() {
+            // The remove button is hidden once a picker value has been removed.
+            // And the assign me button is shown.
+            this.create_picker(this._picker_params(true, true, "me"), this.ME);
+            this.picker.render();
+            this._check_assign_me_button_state(false);
+            var remove = Y.one('.yui-picker-remove-button');
+            remove.simulate('click');
+            this._check_remove_button_state(false);
+            this._check_assign_me_button_state(true);
+        },
+
+        test_picker_assign_me_button_clicked: function() {
+            // The assign me button is hidden once it is clicked.
+            // And the remove button is shown.
+            this.create_picker(this._picker_params(true, true));
+            this.picker.render();
+            var assign_me = Y.one('.yui-picker-assign-me-button');
+            assign_me.simulate('click');
+            this._check_remove_button_state(true);
+            this._check_assign_me_button_state(false);
+        },
+
+        test_picker_assign_me_updates_remove_text: function() {
+            // When Assign me is used, the Remove button text is updated from
+            // the team removal text to the person removal text.
+            this.create_picker(this._picker_params(true, true, "cats", "team"));
+            this.picker.render();
+            var remove_button = Y.one('.yui-picker-remove-button');
+            Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
+            var assign_me = Y.one('.yui-picker-assign-me-button');
+            assign_me.simulate('click');
+            Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
+        },
+
+        test_picker_save_updates_remove_text: function() {
+            // When save is called, the Remove button text is updated
+            // according to the newly saved value.
+            this.create_picker(this._picker_params(true, true, "me"), this.ME);
+            var remove_button = Y.one('.yui-picker-remove-button');
+            Assert.areEqual('Remove someone', remove_button.get('innerHTML'));
+            this.picker.set('results', this.vocabulary);
+            this.picker.render();
+            simulate(
+                this.picker.get('boundingBox').one('.yui3-picker-results'),
+                    'li:nth-child(2)', 'click');
+            Assert.areEqual('Remove some team', remove_button.get('innerHTML'));
+        }
+    };
+
+    suite.add(new Y.Test.Case(
+      Y.merge(
+          commonPersonPickerTests,
+          pickerPatcherPersonPickerTests)
+    ));
+
+    suite.add(new Y.Test.Case(
+      Y.merge(
+          commonPersonPickerTests,
+          createDirectPersonPickerTests)
+    ));
+
+    // Hook for the test runner to get test results.
+    var handle_complete = function(data) {
+        window.status = '::::' + JSON.stringify(data);
+    };
+    Y.Test.Runner.on('complete', handle_complete);
+    Y.Test.Runner.add(suite);
+
+    var console = new Y.Console({newestOnTop: false});
+    console.render('#log');
+
+    Y.on('domready', function() {
+        Y.Test.Runner.run();
+    });
 
 });

=== modified file 'lib/lp/app/javascript/picker/tests/test_picker.html'
--- lib/lp/app/javascript/picker/tests/test_picker.html	2011-08-10 08:43:17 +0000
+++ lib/lp/app/javascript/picker/tests/test_picker.html	2012-02-07 16:00:29 +0000
@@ -1,29 +1,108 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
-  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd";>
-<html>
-  <head>
-  <title>Picker</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/overlay.js"></script>
-  <script type="text/javascript" src="../picker.js"></script>
-  <script type="text/javascript" src="../../expander.js"></script>
-  <script type="text/javascript" src="../../anim/anim.js"></script>
-  <script type="text/javascript" src="../../effects/effects.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_picker.js"></script>
-</head>
-<body class="yui3-skin-sam">
-</body>
+<!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 ${LIBRARY}</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>
+<!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>test picker</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>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/picker/picker.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/expander.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/anim/anim.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/effects/effects.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="../picker.js"></script>
+
+      <!-- Any css assert for this module. -->
+      <!-- <link rel="stylesheet" href="../assets/picker-core.css" /> -->
+
+      <!-- The test suite. -->
+      <script type="text/javascript" src="test_picker.js"></script>
+
+    </head>
+    <body class="yui3-skin-sam">
+        <ul id="suites">
+            <!-- <li>lp.large_indicator.test</li> -->
+            <li>lp.picker.test</li>
+        </ul>
+    </body>
 </html>

=== modified file 'lib/lp/app/javascript/picker/tests/test_picker.js'
--- lib/lp/app/javascript/picker/tests/test_picker.js	2011-09-16 16:12:22 +0000
+++ lib/lp/app/javascript/picker/tests/test_picker.js	2012-02-07 16:00:29 +0000
@@ -1,1419 +1,1448 @@
-/* Copyright (c) 2009, Canonical Ltd. All rights reserved. */
-
-YUI().use('lp.testing.runner', 'test', 'console', 'node', 'lazr.picker',
-           'event', 'node-event-simulate', 'dump', function(Y) {
-
-// Local aliases
-var Assert = Y.Assert,
-    ArrayAssert = Y.ArrayAssert;
-
-var module = Y.lazr.picker;
-
-/*
- * 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.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 Picker Tests");
-
-suite.add(new Y.Test.Case({
-
-    name: 'picker_basics',
-
-    setUp: function() {
-        this.picker = new Y.lazr.picker.Picker({
-            "selected_value": 'foo',
-            "selected_value_metadata": 'foobar'
-        });
-    },
-
-    tearDown: function() {
-        cleanup_widget(this.picker);
-    },
-
-    test_picker_can_be_instantiated: function() {
-        Assert.isInstanceOf(
-            Y.lazr.picker.Picker, this.picker,
-            "Picker failed to be instantiated");
-    },
-
-    test_picker_initialisation: function() {
-        Assert.areEqual('foo', this.picker.get('selected_value'));
-        Assert.areEqual('foobar', this.picker.get('selected_value_metadata'));
-    },
-
-    test_picker_is_stackable: function() {
-        // We should probably define an Assert.hasExtension.
-        Assert.areSame(
-            Y.WidgetStack.prototype.sizeShim, this.picker.sizeShim,
-            "Picker should be stackable.");
-        Assert.areSame(
-            Y.WidgetPositionAlign.prototype.align, this.picker.align,
-            "Picker should be positionable.");
-    },
-
-    test_picker_has_elements: function () {
-        /**
-         * Test that renderUI() adds search box, an error container and a
-         * results container to the widget.
-         * */
-        this.picker.render();
-
-        var bb = this.picker.get('boundingBox');
-        Assert.isNotNull(
-            bb.one('.yui3-picker-search'),
-            "Missing search box.");
-        Assert.isNotNull(
-            bb.one('.lazr-search.lazr-btn'),
-            "Missing search button.");
-        Assert.isNotNull(
-            bb.one('.yui3-picker-results'),
-            "Missing search results.");
-        Assert.isNotNull(
-            bb.one('.yui3-picker-error'), "Missing error box.");
-    },
-
-    test_set_results_updates_display: function () {
-        this.picker.render();
-        var image_url = '../../lazr/assets/skins/sam/search.png';
-        this.picker.set('results', [
-            {
-                image: image_url,
-                css: 'yui3-blah-blue',
-                value: 'jschmo',
-                title: 'Joe Schmo',
-                description: 'joe@xxxxxxxxxxx'
-            }
-        ]);
-        var bb = this.picker.get('boundingBox');
-        var li = bb.one('.yui3-picker-results li');
-        Assert.isNotNull(li, "Results not found");
-        Assert.isTrue(li.hasClass('yui3-blah-blue'), "Missing class name.");
-        Assert.isNotNull(li.one('img'), "Missing image.");
-        Assert.areEqual(
-            image_url, li.one('img').getAttribute('src'),
-            "Unexpected image url");
-        var title_el = li.one('.yui3-picker-result-title');
-        Assert.isNotNull(title_el, "Missing title element");
-        Assert.areEqual(
-            'Joe Schmo', title_el.get('text'), 'Unexpected title value.');
-        var description_el = li.one('.yui3-picker-result-description');
-        Assert.isNotNull(description_el, "Missing description element.");
-        Assert.areEqual(
-            'joe@xxxxxxxxxxx', description_el.get('text'),
-            'Unexpected description value.');
-    },
-
-    test_alternate_title_text: function () {
-        this.picker.render();
-        this.picker.set('results', [
-            {
-                css: 'yui3-blah-blue',
-                value: 'jschmo',
-                title: 'Joe Schmo',
-                description: 'joe@xxxxxxxxxxx',
-                alt_title: 'Another Joe'
-            }
-        ]);
-        var bb = this.picker.get('boundingBox');
-        var li = bb.one('.yui3-picker-results li');
-        var title_el = li.one('.yui3-picker-result-title');
-        Assert.isNotNull(title_el, "Missing title element");
-        Assert.areEqual('A', title_el.get('tagName'), 'Tab key is broken.');
-        Assert.isTrue(title_el.hasClass('js-action'));
-        Assert.areEqual(
-            'Joe Schmo\u00a0(Another Joe)', title_el.get('text'),
-            'Unexpected title value.');
-    },
-
-    test_title_links: function () {
-        this.picker.render();
-        this.picker.set('results', [
-            {
-                css: 'yui3-blah-blue',
-                value: 'jschmo',
-                title: 'Joe Schmo',
-                description: 'joe@xxxxxxxxxxx',
-                alt_title: 'Joe Again <foo></foo>',
-                title_link: 'http://somewhere.com',
-                alt_title_link: 'http://somewhereelse.com',
-                link_css: 'cool-style',
-                details: ['Member since 2007']
-            }
-        ]);
-
-        function check_link(picker, link_selector, title, href) {
-            var bb = picker.get('boundingBox');
-            var link_clicked = false;
-            var link_node = bb.one(link_selector);
-
-            Assert.areEqual(title, link_node.get('text'));
-            Assert.areEqual(href, link_node.get('href'));
-
-            Y.on('click', function(e) {
-                link_clicked = true;
-            }, link_node);
-            simulate(bb, link_selector, 'click');
-            Assert.isTrue(link_clicked,
-                link_selector + ' link was not clicked');
-        }
-        check_link(
-            this.picker, 'a.cool-style:nth-child(1)', 'Joe Schmo',
-            'http://somewhere.com/');
-        check_link(
-            this.picker, 'a.cool-style:last-child', 'View details',
-            'http://somewhereelse.com/');
-        var alt_text_node = this.picker.get('boundingBox')
-            .one('.yui3-picker-result-title span');
-        Assert.areEqual('Joe Again <foo></foo>', alt_text_node.get('text'));
-    },
-
-    test_details: function () {
-        // The details of the li is the content node of the expander.
-        this.picker.render();
-        this.picker.set('results', [
-            {
-                css: 'yui3-blah-blue',
-                value: 'jschmo',
-                title: 'Joe Schmo',
-                description: 'joe@xxxxxxxxxxx',
-                details: ['joe on irc.freenode.net', 'Member since 2007'],
-                alt_title_link: '/~jschmo'
-            }
-        ]);
-        var bb = this.picker.get('boundingBox');
-        var li = bb.one('.yui3-picker-results li');
-        var expander_action = li.expander.icon_node.get('firstChild');
-        Assert.areEqual(
-            'A', expander_action.get('tagName'), 'Tab key is broken.');
-        var details = li.expander.content_node;
-        Assert.areEqual(
-            'joe on irc.freenode.net<br>Member since 2007',
-            details.one('div').getContent());
-        Assert.areEqual(
-            'Select Joe Schmo', details.one('ul li:first-child').get('text'));
-        Assert.areEqual(
-            'View details', details.one('ul li:last-child').get('text'));
-    },
-
-    test_details_escaping: function () {
-        // The content of details is escaped.
-        this.picker.render();
-        this.picker.set('results', [
-            {
-                css: 'yui3-blah-blue',
-                value: 'jschmo',
-                title: 'Joe <Schmo>',
-                description: 'joe@xxxxxxxxxxx',
-                details: ['<joe> on irc.freenode.net', 'f<nor>d maintainer'],
-                alt_title_link: '/~jschmo'
-            }
-        ]);
-        var bb = this.picker.get('boundingBox');
-        var li = bb.one('.yui3-picker-results li');
-        var details = li.expander.content_node;
-        Assert.areEqual(
-            '&lt;joe&gt; on irc.freenode.net<br>f&lt;nor&gt;d maintainer',
-            details.one('div').getContent());
-        Assert.areEqual(
-            'Select Joe &lt;Schmo&gt;',
-            details.one('ul li:first-child a').getContent('text'));
-    },
-
-    test_expander_only_one_open: function() {
-        // Only one expanded details entry should be open at any time.
-        this.picker.render();
-        this.picker.set('results', [
-            {
-                value: 'jschmo',
-                title: 'Joe Schmo',
-                details: ['detail 1', 'detail 2']
-            },
-            {
-                value: 'jsmith',
-                title: 'Joe Smith',
-                details: ['detail 1', 'detail 2']
-            }
-        ]);
-        var bb = this.picker.get('boundingBox');
-        var first_entry = bb.one('.yui3-picker-results li');
-        var first_expander = first_entry.expander;
-        var second_entry = bb.one('.yui3-picker-results li.yui3-lazr-odd');
-        var second_expander = second_entry.expander;
-        first_expander.icon_node.simulate('click');
-        Y.Assert.isTrue(first_expander.isExpanded());
-        Y.Assert.isFalse(second_expander.isExpanded());
-
-        // Open the other expander and check that the first one has closed.
-        second_expander.icon_node.simulate('click');
-        Y.Assert.isFalse(first_expander.isExpanded());
-        Y.Assert.isTrue(second_expander.isExpanded());
-    },
-
-    test_expander_multiple_pickers: function() {
-        // Expanders for one picker should not interfere with those for
-        // another picker. ie if Picker A expander is opened, any open
-        // expanders on Picker B should remain open.
-        var results = [
-            {
-                value: 'jschmo',
-                title: 'Joe Schmo',
-                details: ['detail 1', 'detail 2']
-            },
-            {
-                value: 'jsmith',
-                title: 'Joe Smith',
-                details: ['detail 1', 'detail 2']
-            }
-        ];
-        this.picker.render();
-        this.picker.set('results', results);
-
-        var another_picker = new Y.lazr.picker.Picker();
-        another_picker.render();
-        another_picker.set('results', results);
-
-        var bb = this.picker.get('boundingBox');
-        var picker_entry = bb.one('.yui3-picker-results li');
-        var picker_expander = picker_entry.expander;
-        picker_expander.icon_node.simulate('click');
-
-        bb = another_picker.get('boundingBox');
-        var another_picker_entry = bb.one('.yui3-picker-results li');
-        var another_picker_expander = another_picker_entry.expander;
-        another_picker_expander.icon_node.simulate('click');
-
-        Y.Assert.isTrue(picker_expander.isExpanded());
-        Y.Assert.isTrue(another_picker_expander.isExpanded());
-        cleanup_widget(another_picker);
-    },
-
-    test_details_save_link: function () {
-        // The select link the li's details saves the selection.
-        this.picker.render();
-        this.picker.set('results', [
-            {
-                css: 'yui3-blah-blue',
-                value: 'jschmo',
-                title: 'Joe Schmo',
-                description: 'joe@xxxxxxxxxxx',
-                alt_title_link: 'http://somewhereelse.com',
-                link_css: 'cool-style',
-                details: ['Member since 2007']
-            }
-        ]);
-        var bb = this.picker.get('boundingBox');
-        var link_node = bb.one('a.save');
-        Assert.areEqual('Select Joe Schmo', link_node.get('text'));
-        Assert.isTrue(link_node.get('href').indexOf(window.location) === 0);
-        var selected_value = null;
-        this.picker.subscribe('save', function(e) {
-            selected_value = e.details[0].value;
-        }, this);
-        simulate(bb, 'a.save', 'click');
-        Assert.areEqual('jschmo', selected_value);
-    },
-
-    test_title_badges: function () {
-        this.picker.render();
-        var badge_info = [
-            {
-                url: '../../lazr/assets/skins/sam/search.png',
-                label: 'product 1',
-                role: 'driver'},
-            {   url: '../../lazr/assets/skins/sam/spinner.png',
-                label: 'product 2',
-                role: 'maintainer'},
-            {   url: '../../lazr/assets/skins/sam/spinner.png',
-                label: 'product 2',
-                role: 'driver'
-            }];
-        this.picker.set('results', [
-            {
-                badges: badge_info,
-                css: 'yui3-blah-blue',
-                value: 'jschmo',
-                title: 'Joe Schmo',
-                description: 'joe@xxxxxxxxxxx'
-            }
-        ]);
-        var bb = this.picker.get('boundingBox');
-        var li = bb.one('.yui3-picker-results li');
-        var i;
-        for (i=0; i<badge_info.length; i++) {
-            var img_node = li.one(
-                'div.badge img.badge:nth-child(' + (i + 1) + ')');
-            // Check that duplicate badge urls are not displayed.
-            if (i===2) {
-                Assert.isNull(img_node);
-                break;
-            }
-            Assert.areEqual(
-                badge_info[i].url, img_node.getAttribute('src'),
-                'Unexpected badge url');
-            var badge_text = badge_info[i].label + ' ' + badge_info[i].role;
-            Assert.areEqual(
-                badge_text, img_node.get('alt'),
-                'Unexpected badge alt text');
-        }
-    },
-
-    test_details_badges: function () {
-        // The affiliation details are rendered correctly in the content node
-        // of the expander.
-        this.picker.render();
-        var badge_info = [
-            {
-                url: '../../lazr/assets/skins/sam/spinner.png',
-                label: 'product 1',
-                role: 'maintainer'},
-            {
-                url: '../../lazr/assets/skins/sam/search.png',
-                label: 'product 2',
-                role: 'driver'}];
-        this.picker.set('results', [
-            {
-                badges: badge_info,
-                css: 'yui3-blah-blue',
-                value: 'jschmo',
-                title: 'Joe Schmo',
-                description: 'joe@xxxxxxxxxxx'
-            }
-        ]);
-        var bb = this.picker.get('boundingBox');
-        var li = bb.one('.yui3-picker-results li');
-        var details = li.expander.content_node;
-        var affiliation_header = details.one('div.affiliation:nth-child(1)');
-        Assert.areEqual('Affiliation', affiliation_header.get('text'));
-        var badge_img = affiliation_header.one('img');
-        Assert.areEqual('product 1 maintainer', badge_img.get('alt'));
-        Assert.areEqual(
-            '../../lazr/assets/skins/sam/spinner.png',
-            badge_img.getAttribute('src'));
-        affiliation_header = details.one('div.affiliation:nth-child(3)');
-        badge_img = affiliation_header.one('img');
-        Assert.areEqual('product 2 driver', badge_img.get('alt'));
-        Assert.areEqual(
-            '../../lazr/assets/skins/sam/search.png',
-            badge_img.getAttribute('src'));
-        var affiliation_text = details.one(
-            'div.affiliation-text:nth-child(2)');
-        Assert.areEqual(
-            'product 1 maintainer', affiliation_text.get('innerHTML'));
-        affiliation_text = details.one('div.affiliation-text:nth-child(4)');
-        Assert.areEqual(
-            'product 2 driver', affiliation_text.get('innerHTML'));
-    },
-
-    test_results_display_escaped: function () {
-        this.picker.render();
-        this.picker.set('results', [
-            {
-                image: '<script>throw "back";</script>',
-                css: 'yui3-blah-blue',
-                value: '<script>throw "wobbly";</script>',
-                title: '<script>throw "toys out of pram";</script>',
-                description: '<script>throw "up";</script>'
-            }
-        ]);
-        var bb = this.picker.get('boundingBox');
-        var li = bb.one('.yui3-picker-results li');
-        var image_el = li.one('img');
-        Assert.areEqual(
-            '<script>throw "back";</script>', image_el.getAttribute('src'),
-            "Unexpected image url");
-        var title_el = li.one('.yui3-picker-result-title');
-        Assert.areEqual(
-            '&lt;script&gt;throw "toys out of pram";&lt;/script&gt;',
-            title_el.get('innerHTML'), 'Unexpected title value.');
-        var description_el = li.one('.yui3-picker-result-description');
-        Assert.areEqual(
-            '&lt;script&gt;throw "up";&lt;/script&gt;',
-            description_el.get('innerHTML'), 'Unexpected description value.');
-    },
-
-    test_results_updates_display_with_missing_data: function () {
-        this.picker.render();
-        var image_url = '../../lazr/assets/skins/sam/search.png';
-        this.picker.set('results', [
-            { value: 'jschmo', title: 'Joe Schmo' }
-        ]);
-        var bb = this.picker.get('boundingBox');
-        var li = bb.one('.yui3-picker-results li');
-        Assert.isNotNull(li, "Results not found.");
-        Assert.areEqual(Y.lazr.ui.CSS_EVEN, li.getAttribute('class'));
-        Assert.isNull(li.one('img'), "Unexpected image.");
-        var description_el = li.one('.yui3-picker-result-description.');
-        Assert.isNull(description_el, "Unexpected description element.");
-    },
-
-    test_render_displays_initial_results: function () {
-        this.picker.set('results', [
-                {'title': 'Title 1'},
-                {'title': 'Title 2'}
-            ]);
-        this.picker.render();
-        var bb = this.picker.get('boundingBox');
-        var results = bb.all('.yui3-picker-results li');
-        Assert.isNotNull(results, "Results not found.");
-        Assert.areEqual(2, results.size());
-    },
-
-    test_resetting_results_removes_previous_results: function () {
-        this.picker.render();
-        var bb = this.picker.get('boundingBox');
-
-        // First time setting the results.
-        this.picker.set('results', [
-                {'title': 'Title 1'},
-                {'title': 'Title 2'}
-            ]);
-        var results = bb.all('.yui3-picker-results li');
-        Assert.isNotNull(results, "Results not found.");
-        Assert.areEqual(2, results.size());
-
-        // Second time setting the results.
-        this.picker.set('results', [
-                {'title': 'Title 1'}
-            ]);
-        results = bb.all('.yui3-picker-results li');
-        Assert.isNotNull(results, "Results not found");
-        Assert.areEqual(1, results.size());
-    },
-
-    test_updateResultsDisplay_adds_even_odd_class: function () {
-        this.picker.set('results', [
-                {'title': 'Title 1'},
-                {'title': 'Title 2'},
-                {'title': 'Title 1'},
-                {'title': 'Title 2'}
-            ]);
-        this.picker.render();
-        var bb = this.picker.get('boundingBox');
-        var results = bb.all('.yui3-picker-results li');
-        Assert.isNotNull(results, "Results not found.");
-        ArrayAssert.itemsAreEqual(
-            [true, false, true, false], results.hasClass(Y.lazr.ui.CSS_EVEN));
-        ArrayAssert.itemsAreEqual(
-            [false, true, false, true], results.hasClass(Y.lazr.ui.CSS_ODD));
-    },
-
-    test_clicking_search_button_fires_search_event: function () {
-        this.picker.render();
-
-        var bb = this.picker.get('boundingBox');
-        var input = bb.one('.yui3-picker-search');
-        input.set('value', 'a search');
-        var event_has_fired = false;
-        this.picker.subscribe('search', function(e) {
-                event_has_fired = true;
-                Assert.areEqual(
-                    'a search', e.details[0],
-                    'Search event is missing the search string.');
-        }, this);
-        simulate(
-            this.picker.get('boundingBox'), '.lazr-search.lazr-btn', 'click');
-        Assert.isTrue(event_has_fired, "search event wasn't fired");
-    },
-
-    test_set_search_mode_disables_search_button: function () {
-        this.picker.render();
-
-        var bb = this.picker.get('boundingBox');
-        this.picker.set('search_mode', true);
-        Assert.isTrue(
-            bb.one('.lazr-search.lazr-btn').get('disabled'),
-            "Search button wasn't disabled.");
-        this.picker.set('search_mode', false);
-        Assert.isFalse(
-            bb.one('.lazr-search.lazr-btn').get('disabled'),
-            "Search button wasn't re-enabled.");
-    },
-
-    test_hitting_enter_in_search_input_fires_search_event: function () {
-        this.picker.render();
-
-        var bb = this.picker.get('boundingBox');
-        var input = bb.one('.yui3-picker-search');
-        input.set('value', 'a search');
-        var event_has_fired = false;
-        this.picker.subscribe('search', function() {
-            event_has_fired = true;
-        }, this);
-        simulate(
-            this.picker.get('boundingBox'), '.yui3-picker-search', 'keydown',
-            {keyCode: 13});
-        Assert.isTrue(event_has_fired, "search event wasn't fired");
-    },
-
-    test_search_event_sets_the_in_search_mode: function () {
-        this.picker.render();
-
-        Assert.isFalse(
-            this.picker.get('search_mode'),
-            "Widget shouldn't be in search mode.");
-        this.picker.fire('search');
-        Assert.isTrue(
-            this.picker.get('search_mode'),
-            "Widget should be in search mode.");
-    },
-
-    test_search_event_resets_the_current_batch: function () {
-        this.picker.render();
-        this.picker.set('batches', [
-            {value: 'batch1', name: 'Batch 1'},
-            {value: 'batch2', name: 'Batch 2'}
-            ]);
-        this.picker.set('selected_batch', 1);
-        Assert.areEqual(1, this.picker.get('selected_batch'));
-        this.picker._search_input.set('value', 'bar');
-        simulate(
-            this.picker.get('boundingBox'), '.lazr-search.lazr-btn', 'click');
-
-        // An initial search resets the batch.
-        Assert.areEqual(0, this.picker.get('selected_batch'));
-
-        // Batch is reset if search term has changed.
-        this.picker.set('search_mode', false);
-        this.picker.set('selected_batch', 1);
-        this.picker._search_input.set('value', 'foo');
-        simulate(
-            this.picker.get('boundingBox'), '.lazr-search.lazr-btn', 'click');
-        Assert.areEqual(0, this.picker.get('selected_batch'));
-
-        // Batch is not reset if search term hasn't changed.
-        this.picker.set('search_mode', false);
-        this.picker.set('selected_batch', 1);
-        simulate(
-            this.picker.get('boundingBox'), '.lazr-search.lazr-btn', 'click');
-        Assert.areEqual(1, this.picker.get('selected_batch'));
-    },
-
-    test_setting_search_mode: function () {
-        // Setting search_mode adds a CSS class and disables the search box.
-
-        this.picker.render();
-
-        this.picker.set('search_mode', true);
-        var bb = this.picker.get('boundingBox');
-        Assert.isTrue(
-            bb.one('.yui3-picker-search').get('disabled'),
-            "Search box should be disabled.");
-        Assert.isTrue(
-            bb.hasClass('yui3-picker-search-mode'),
-            'Missing CSS class on widget.');
-    },
-
-    test_unsetting_search_mode: function () {
-
-        this.picker.render();
-
-        this.picker.set('search_mode', true);
-        this.picker.set('search_mode', false);
-        var bb = this.picker.get('boundingBox');
-        Assert.isFalse(
-            bb.one('.yui3-picker-search').get('disabled'),
-            "Search input should be enabled.");
-        Assert.isFalse(
-            bb.hasClass('yui3-picker-search-mode'),
-            'CSS class should be removed from the widget.');
-    },
-
-    test_set_results_remove_search_mode: function () {
-        this.picker.render();
-
-        this.picker.set('search_mode', true);
-        this.picker.set('results', []);
-
-        Assert.isFalse(
-            this.picker.get('search_mode'),
-            "Widget should be out of search_mode.");
-    },
-
-    test_set_error: function () {
-        // Setting the error property displays the string in the
-        // error box and puts an in-error CSS class on the widget.
-        this.picker.render();
-
-        var error_msg = 'Sorry an <error> occured.';
-        this.picker.set('error', error_msg);
-
-        var bb = this.picker.get('boundingBox');
-        Assert.areEqual(
-            error_msg, bb.one('.yui3-picker-error').get('text'),
-            "Error message wasn't displayed.");
-        Assert.isTrue(
-            bb.hasClass('yui3-picker-error-mode'),
-            "Missing error-mode class.");
-    },
-
-    test_set_error_null_clears_ui: function () {
-        this.picker.render();
-
-        this.picker.set('error', 'Sorry an error occured.');
-        this.picker.set('error', null);
-        var bb = this.picker.get('boundingBox');
-        Assert.areEqual('', bb.one('.yui3-picker-error').get('text'),
-            "Error message wasn't cleared.");
-        Assert.isFalse(
-            bb.hasClass('yui3-picker-error-mode'),
-            "error-mode class should be removed.");
-    },
-
-    test_small_search_sets_error: function () {
-        this.picker.render();
-        this.picker.set('min_search_chars', 4);
-        var bb = this.picker.get('boundingBox');
-        var input = bb.one('.yui3-picker-search');
-        input.set('value', ' 1 3 '); // 3 characters after trim.
-        simulate(
-            this.picker.get('boundingBox'), '.lazr-search.lazr-btn', 'click');
-        Assert.areEqual(
-            "Please enter at least 4 characters.",
-            this.picker.get('error'),
-            "Error message wasn't displayed.");
-    },
-
-    test_click_on_result_fire_save_event: function () {
-        this.picker.set('results', [
-            {'title': 'Object 1', value: 'first'},
-            {'title': 'Object 2', value: 'second'}
-        ]);
-
-        this.picker.render();
-
-        var event_has_fired = false;
-        this.picker.subscribe('save', function(e) {
-            event_has_fired = true;
-            Assert.areEqual(
-                'first', e.details[0].value,
-                "The event value of the clicked li is wrong.");
-            Assert.areEqual(
-                'Object 1', e.details[0].title,
-                "The event title of the clicked li is wrong.");
-        }, this);
-        simulate(
-            this.picker.get('boundingBox'),
-                '.yui3-picker-results li', 'click');
-        Assert.isTrue(event_has_fired, "save event wasn't fired.");
-    },
-
-    test_cancel_event_hides_widget: function () {
-        this.picker.render();
-
-        this.picker.fire('cancel', 'bogus');
-        Assert.isFalse(
-            this.picker.get('visible'), "The widget should be hidden.");
-    },
-
-    test_cancel_event_resets_search_mode: function () {
-        this.picker.render();
-        this.picker.set('search_mode', true);
-        Assert.isTrue(this.picker.get('search_mode'));
-        this.picker.fire('cancel', 'bogus');
-        Assert.isFalse(this.picker.get('search_mode'));
-    },
-
-    test_save_event_hides_widget: function () {
-        this.picker.render();
-
-        this.picker.fire('save', 'bogus');
-        Assert.isFalse(
-            this.picker.get('visible'), "The widget should be hidden.");
-    },
-
-    test_save_event_clears_widget_by_default: function () {
-        this.picker.render();
-
-        this.picker._search_input.set('value', 'foo');
-        this.picker.fire('save', 'bogus');
-        Assert.areEqual(
-            '', this.picker._search_input.get('value'),
-            "The widget hasn't been cleared");
-    },
-
-    test_save_does_not_clear_widget_when_clear_on_save_is_false: function () {
-        picker = new Y.lazr.picker.Picker({clear_on_save: false});
-        picker.render();
-
-        picker._search_input.set('value', 'foo');
-        picker.fire('save', 'bogus');
-        Assert.areEqual(
-            'foo', picker._search_input.get('value'),
-            "The widget has been cleared but it should not");
-    },
-
-    test_cancel_event_does_not_clear_widget_by_default: function () {
-        this.picker.render();
-
-        this.picker._search_input.set('value', 'foo');
-        this.picker.fire('cancel', 'bogus');
-        Assert.areEqual(
-            'foo', this.picker._search_input.get('value'),
-            "The widget has been cleared but it should not");
-    },
-
-    test_cancel_event_clears_widget_when_clear_on_cancel_true: function () {
-        picker = new Y.lazr.picker.Picker({clear_on_cancel: true});
-        picker.render();
-
-        picker._search_input.set('value', 'foo');
-        picker.fire('cancel', 'bogus');
-        Assert.areEqual(
-            '', picker._search_input.get('value'),
-            "The widget hasn't been cleared");
-    },
-
-    test_search_clears_any_eror: function () {
-        this.picker.render();
-        this.picker.set('error', "An error");
-
-        this.picker.fire('search');
-
-        Assert.isNull(
-            this.picker.get('error'), 'Error should be cleared.');
-
-    },
-
-    test_no_search_result_msg: function () {
-        this.picker.render();
-
-        this.picker.set(
-            'no_results_search_message', "Your query '{query}' sucked.");
-        var bb = this.picker.get('boundingBox');
-        bb.one('.yui3-picker-search').set('value', 'my <search> string');
-        this.picker.set('results', []);
-
-        var search_results = bb.one('.yui3-picker-results');
-        Assert.areEqual(
-            "Your query 'my <search> string' sucked.",
-            search_results.get('text'),
-            "Empty results message wasn't displayed.");
-        Assert.isTrue(
-            search_results.hasClass('yui3-picker-no-results'),
-            "Missing no-results CSS class.");
-    },
-
-    test_search_results_clear_no_results_css: function () {
-        this.picker.render();
-
-        var bb = this.picker.get('boundingBox');
-        bb.one('.yui3-picker-search').set('value', 'my search string');
-        this.picker.set('results', []);
-
-        this.picker.set('results', [{title: 'Title 1'}, {title: 'Title 2'}]);
-        Assert.isFalse(
-            bb.one('.yui3-picker-results').hasClass('yui3-picker-no-results'),
-            "The no-results CSS class should have been removed.");
-    },
-
-    test_setting_search_slot_updates_ui: function () {
-        this.picker.render();
-        var filler = '<span>hello</span>';
-        this.picker.set('search_slot', Y.Node.create(filler));
-        var bb = this.picker.get('boundingBox');
-        var div = bb.one('.yui3-picker-search-slot');
-
-        Assert.isNotNull(div, 'Container for form extras not found.');
-        Assert.areEqual(filler, div.get('innerHTML'));
-    },
-
-    test_setting_footer_slot_updates_ui: function () {
-        this.picker.render();
-        var filler = '<span>foobar</span>';
-        this.picker.set('footer_slot', Y.Node.create(filler));
-        var bb = this.picker.get('boundingBox');
-        var div = bb.one('.yui3-picker-footer-slot');
-
-        Assert.isNotNull(div, 'Container for form extras not found.');
-        Assert.areEqual(filler, div.get('innerHTML'));
-    },
-
-    test_setting_batches_updates_ui: function () {
-        this.picker.render();
-        this.picker.set('batches', [
-            {value: 'new', name: 'New'},
-            {value: 'assigned', name: 'Assigned'}
-            ]);
-        var bb = this.picker.get('boundingBox');
-        Assert.isNotNull(
-            bb.one('.yui3-picker-batches span'),
-            "Container for batches not found.");
-        var batches = bb.all('.yui3-picker-batches span');
-        Assert.isNotNull(batches, "Batches not found");
-        Assert.areEqual(2, batches.size());
-        ArrayAssert.itemsAreEqual(
-            ['New', 'Assigned'],
-            batches.get('text'),
-            "Batches don't contain batch names.");
-        ArrayAssert.itemsAreEqual(
-            [true, false],
-            batches.hasClass('yui3-picker-selected-batch'),
-            "Selected batches missing CSS class.");
-
-        Assert.isNotNull(
-            bb.one('.yui3-picker-batches .lazr-prev'),
-            "There should be a previous button.");
-        Assert.isNotNull(
-            bb.one('.yui3-picker-batches .lazr-next'),
-            "There should be a next button.");
-    },
-
-    test_simplified_batching_interface: function () {
-        this.picker.render();
-        this.picker.set('batch_count', 4);
-        this.picker.set('results', [
-            { value: 'aardvark', title: 'Aardvarks' },
-            { value: 'bats', title: 'Bats' },
-            { value: 'cats', title: 'Cats' },
-            { value: 'dogs', title: 'Dogs' },
-            { value: 'emus', title: 'Emus' },
-            { value: 'frogs', title: 'Frogs' },
-            { value: 'gerbils', title: 'Gerbils' }
-        ]);
-        var bb = this.picker.get('boundingBox');
-        Assert.isNotNull(
-            bb.one('.yui3-picker-batches span'),
-            "Container for batches not found.");
-        var batches = bb.all('.yui3-picker-batches span');
-        Assert.isNotNull(batches, "Batches not found");
-        Assert.areEqual(4, batches.size());
-        ArrayAssert.itemsAreEqual(
-            ['1', '2', '3', '4'],
-            batches.get('text'),
-            "Batches don't contain batch names.");
-        ArrayAssert.itemsAreEqual(
-            [true, false, false, false],
-            batches.hasClass('yui3-picker-selected-batch'),
-            "Selected batches missing CSS class.");
-
-        Assert.isNotNull(
-            bb.one('.yui3-picker-batches .lazr-prev'),
-            "There should be a previous button.");
-        Assert.isNotNull(
-            bb.one('.yui3-picker-batches .lazr-next'),
-            "There should be a next button.");
-    },
-
-    test_clicking_a_batch_item_fires_search_event: function () {
-        this.picker.set('current_search_string', 'search');
-        this.picker.set('batches', [
-            {value: 'item1', name: 'Item 1'},
-            {value: 'item2', name: 'Item 2'}
-            ]);
-        this.picker.render();
-
-        var bb = this.picker.get('boundingBox');
-        var event_has_fired = false;
-        this.picker.subscribe('search', function(e) {
-            event_has_fired = true;
-            ArrayAssert.itemsAreEqual(
-                ['search', 'item1'], e.details,
-                "Search event details should contain search" +
-                "string and selected batch");
-        }, this);
-        simulate(
-            this.picker.get('boundingBox'),
-            '.yui3-picker-batches span', 'click');
-        Assert.isTrue(event_has_fired, "search event wasn't fired.");
-    },
-
-    test_clicking_a_batch_item_sets_selected_batch: function () {
-        this.picker.set('current_search_string', 'search');
-        this.picker.set('batches', [
-            {value: 'item1', name: 'Item 1'},
-            {value: 'item2', name: 'Item 2'}
-            ]);
-        this.picker.render();
-
-        var bb = this.picker.get('boundingBox');
-        Assert.areEqual(
-            0, this.picker.get('selected_batch'),
-            "First batch should be selected.");
-        simulate(
-            this.picker.get('boundingBox'),
-            '.yui3-picker-batches span:nth-last-child(2)', 'click');
-        Assert.areEqual(
-            1, this.picker.get('selected_batch'),
-            "selected_batch should have been updated.");
-    },
-
-    test_set_selected_batch_updates_css: function () {
-        this.picker.render();
-        this.picker.set('batches', [
-            {value: 'item1', name: '1'},
-            {value: 'item2', name: '2'}
-            ]);
-        Assert.areEqual(
-            0, this.picker.get('selected_batch'),
-            "Expected first batch to be selected by default.");
-        this.picker.set('selected_batch', 1);
-
-        var bb = this.picker.get('boundingBox');
-        var batches = bb.all('.yui3-picker-batches span');
-        ArrayAssert.itemsAreEqual(
-            [false, true],
-            batches.hasClass('yui3-picker-selected-batch'),
-            "Selected batch missing CSS class.");
-    },
-
-    test_set_selected_batch_validator: function () {
-        this.picker.render();
-        this.picker.set('batches', [
-            {value: 'item1', name: '1'},
-            {value: 'item2', name: '2'}
-            ]);
-
-        this.picker.set('selected_batch', -1);
-        Assert.areEqual(
-            0, this.picker.get('selected_batch'),
-            "Negative index shouldn't update selected_batch.");
-
-        this.picker.set('selected_batch', 3);
-        Assert.areEqual(
-            0, this.picker.get('selected_batch'),
-            "Index greather than last batch item shouldn't " +
-            "update selected_batch.");
-
-        this.picker.set('selected_batch', 'one');
-        Assert.areEqual(
-            0, this.picker.get('selected_batch'),
-            "Non-integere shouldn't update selected_batch.");
-    },
-
-    test_prev_button_is_disabled_only_on_first_batch: function () {
-        this.picker.set('batches', [
-            {value: 'item1', name: '1'},
-            {value: 'item2', name: '2'}
-            ]);
-        this.picker.render();
-
-        var bb = this.picker.get('boundingBox');
-        Assert.isTrue(
-            bb.one('.lazr-prev').get('disabled'),
-            "Previous button should be disabled on first batch.");
-
-        this.picker.set('selected_batch', 1);
-        Assert.isFalse(
-            bb.one('.lazr-prev').get('disabled'),
-            "Previous button shouldn't be disabled on last batch.");
-    },
-
-    test_next_button_is_disabled_only_on_last_batch: function () {
-        this.picker.set('batches', [
-            {value: 'item1', name: '1'},
-            {value: 'item2', name: '2'}
-            ]);
-        this.picker.render();
-
-        var bb = this.picker.get('boundingBox');
-        Assert.isFalse(
-            bb.one('.lazr-next').get('disabled'),
-            "Next button shouldn't be disabled on first batch.");
-
-        this.picker.set('selected_batch', 1);
-        Assert.isTrue(
-            bb.one('.lazr-next').get('disabled'),
-            "Previous button should be disabled on last batch.");
-    },
-
-    test_click_on_next_button_selects_next_batch: function () {
-        this.picker.set('batches', [
-            {value: 'item1', name: '1'},
-            {value: 'item2', name: '2'},
-            {value: 'item3', name: '3'}
-            ]);
-        this.picker.set('selected_batch', 1);
-        this.picker.render();
-
-        var bb = this.picker.get('boundingBox');
-        simulate(
-            this.picker.get('boundingBox'), '.lazr-next.lazr-btn', 'click');
-        Assert.areEqual(
-            2, this.picker.get('selected_batch'),
-            "Next batch should have been selected.");
-    },
-
-    test_click_on_next_button_fires_search_event: function () {
-        this.picker.set('current_search_string', 'search');
-        this.picker.set('batches', [
-            {value: 'item1', name: 'Item 1'},
-            {value: 'item2', name: 'Item 2'}
-            ]);
-        this.picker.render();
-
-        var event_has_fired = false;
-        this.picker.subscribe('search', function(e) {
-            event_has_fired = true;
-            ArrayAssert.itemsAreEqual(
-                ['search', 'item2'], e.details,
-                "Search event details should contain search" +
-                "string and selected batch");
-        }, this);
-        simulate(
-            this.picker.get('boundingBox'),
-            '.yui3-picker-batches .lazr-next', 'click');
-        Assert.isTrue(event_has_fired, "search event wasn't fired.");
-    },
-
-    test_click_on_prev_button_selects_prev_batch: function () {
-        this.picker.set('batches', [
-            {value: 'item1', name: '1'},
-            {value: 'item2', name: '2'},
-            {value: 'item3', name: '3'}
-            ]);
-        this.picker.set('selected_batch', 1);
-        this.picker.render();
-
-        var bb = this.picker.get('boundingBox');
-        simulate(
-            this.picker.get('boundingBox'), '.lazr-prev.lazr-btn', 'click');
-        Assert.areEqual(
-            0, this.picker.get('selected_batch'),
-            "Previous batch should have been selected.");
-    },
-
-    test_click_on_prev_button_fires_search_event: function () {
-        this.picker.set('current_search_string', 'search');
-        this.picker.set('batches', [
-            {value: 'item1', name: 'Item 1'},
-            {value: 'item2', name: 'Item 2'}
-            ]);
-        this.picker.set('selected_batch', 1);
-        this.picker.render();
-
-        var event_has_fired = false;
-        this.picker.subscribe('search', function(e) {
-            event_has_fired = true;
-            ArrayAssert.itemsAreEqual(
-                ['search', 'item1'], e.details,
-                "Search event details should contain search" +
-                "string and selected batch");
-        }, this);
-        simulate(
-            this.picker.get('boundingBox'),
-            '.yui3-picker-batches .lazr-prev', 'click');
-        Assert.isTrue(event_has_fired, "search event wasn't fired.");
-    },
-
-    test_buttons_are_displayed_only_if_there_are_batches: function () {
-        this.picker.render();
-
-        var bb = this.picker.get('boundingBox');
-        Assert.isNull(
-            bb.one('.yui3-picker-batches .lazr-prev'),
-            "There should be no previous button.");
-        Assert.isNull(
-            bb.one('.yui3-picker-batches .lazr-next'),
-            "There should be no next button.");
-    },
-
-    test_text_input_on_footer_can_be_focused: function () {
-        this.picker.render();
-        this.picker.set('footer_slot', Y.Node.create(
-            '<input class="extra-input" name="extra_input" type="text" />'));
-        var extra_input = this.picker.get('boundingBox').one('.extra-input');
-        var got_focus = false;
-        extra_input.on('focus', function(e) {
-            got_focus = true;
-        });
-        extra_input.focus();
-        Assert.isTrue(got_focus, "focus didn't go to the extra input.");
-    },
-
-    test_overlay_progress_value: function () {
-        // Setting the progress attribute controls the overlay's
-        // green progress bar.
-        this.picker.render();
-        Assert.areEqual(
-            50,
-            this.picker.get('progress'),
-            "The picker should start out with progress at 50%.");
-
-        this.picker.set('results', [
-            {
-                value: 'jschmo',
-                title: 'Joe Schmo',
-                description: 'joe@xxxxxxxxxxx'
-            }
-        ]);
-        Assert.areEqual(
-            100,
-            this.picker.get('progress'),
-            "The picker progress should be 100% with results.");
-
-        this.picker.set('results', []);
-        Assert.areEqual(
-            50,
-            this.picker.get('progress'),
-            "The picker progress should be 50% without results.");
-    },
-
-    test_exiting_search_mode_focus_search_box: function () {
-        this.picker.render();
-        this.picker.set('search_mode', true);
-
-        var bb = this.picker.get('boundingBox');
-        var search_input = bb.one('.yui3-picker-search');
-        var got_focus = false;
-        search_input.on('focus', function(e) {
-            got_focus = true;
-        });
-        this.picker.set('search_mode', false);
-        Assert.isTrue(got_focus, "focus didn't go to the search input.");
-    }
-}));
-
-suite.add(new Y.Test.Case({
-
-    name: 'picker_with_filter',
-
-    setUp: function() {
-        this.picker = new Y.lazr.picker.Picker({
-            "selected_value": 'foo',
-            "selected_value_metadata": 'foobar',
-            "filter_options": [
-                {'name': 'ALL',
-                 'title': 'All',
-                 'description': 'Display all'},
-                {'name': 'PROJECT',
-                 'title': 'Product',
-                 'description': 'Display products'}
-            ]
-        });
-    },
-
-    tearDown: function() {
-        cleanup_widget(this.picker);
-    },
-
-    test_picker_has_elements: function () {
-         // Test renderUI() adds filter container container to the widget.
-        this.picker.render();
-
-        var bb = this.picker.get('boundingBox');
-        Assert.isNotNull(
-            bb.one('.yui3-picker-filter'),
-            "Missing filter box.");
-    },
-
-    _check_filter: function (filter_data) {
-        // Check the expected filter links are rendered with the correct data.
-        var filter_div = this.picker.get('boundingBox')
-            .one('.yui3-picker-filter');
-        var i;
-        for (i=0; i<filter_data.length; i++) {
-        var link = filter_div.one('a:nth-child(' + (i + 1) + ')');
-            Assert.isTrue(link.hasClass(filter_data[i].css));
-            Assert.areEqual(link.get('text'), filter_data[i].title);
-            Assert.areEqual(link.get('title'), filter_data[i].description);
-        }
-    },
-
-    test_no_results_does_not_render_filter: function () {
-        // Rendering empty results doesn't render the filter and clears it
-        // if it is already visible.
-        this.picker.render();
-        this.picker._search_input.set('value', 'Joe');
-        var filter_div = this.picker.get('boundingBox')
-            .one('.yui3-picker-filter');
-        // Make the filter visible by rendering some results.
-        this.picker.set('results', [
-            {
-                value: 'jschmo',
-                title: 'Joe Schmo',
-                description: 'joe@xxxxxxxxxxx'
-            }
-        ]);
-        Assert.areNotEqual('', filter_div.get('innerHTML'));
-        // Reset and render empty results.
-        this.picker.set('current_filter_value', null);
-        this.picker.set('results', []);
-        Assert.areEqual('', filter_div.get('innerHTML'));
-    },
-
-    test_set_results_renders_filter: function () {
-        // Rendering results also renders the filter elements.
-        this.picker.render();
-        this.picker._search_input.set('value', 'Joe');
-        this.picker.set('results', [
-            {
-                value: 'jschmo',
-                title: 'Joe Schmo',
-                description: 'joe@xxxxxxxxxxx'
-            }
-        ]);
-        var filter_div = this.picker.get('boundingBox')
-            .one('.yui3-picker-filter');
-        Assert.isNotNull(filter_div, "Filter not found");
-        Assert.areEqual('Showing All matches for "Joe".' +
-                        'Filter by:\u00A0All,\u00A0or\u00A0Product',
-            filter_div.get('textContent'));
-        this._check_filter([
-            {css: 'invalid-link', title: 'All', description: 'Display all'},
-            {css: 'js-action', title: 'Product',
-                description: 'Display products'}
-        ]);
-    },
-
-    test_filter_search: function () {
-        // When a filter link is clicked a new search is performed and the
-        // filter is updated.
-        this.picker.render();
-        this.picker._search_input.set('value', 'Joe');
-        this.picker.set('results', [
-            {
-                value: 'jschmo',
-                title: 'Joe Schmo',
-                description: 'joe@xxxxxxxxxxx'
-            }
-        ]);
-        var search_ok = false;
-        var picker = this.picker;
-        this.picker.subscribe('search', function(e) {
-            var search_string = e.details[0];
-            var filter_name = e.details[3];
-            Assert.areEqual('Joe', search_string);
-            Assert.areEqual('PROJECT', filter_name);
-            Assert.areEqual('Product', picker.get('current_filter_value'));
-            search_ok = true;
-            picker.set('results', [
-                {
-                    value: 'jschmo',
-                    title: 'Joe Schmo',
-                    description: 'joe@xxxxxxxxxxx'
-                }
-            ]);
-        });
-        var filter_div = this.picker.get('boundingBox')
-            .one('.yui3-picker-filter');
-        var filter_link = filter_div.one('a:nth-child(2)');
-        filter_link.simulate('click');
-        Assert.isTrue(search_ok);
-        this._check_filter([
-            {css: 'js-action', title: 'All', description: 'Display all'},
-            {css: 'invalid-link', title: 'Product',
-                description: 'Display products'}
-        ]);
-    },
-
-    test_filter_search_no_results: function () {
-        // When a filter link is clicked and no results are returned, the
-        // filter is still visible.
-        this.picker.render();
-        this.picker._search_input.set('value', 'Joe');
-        this.picker.set('results', [
-            {
-                value: 'jschmo',
-                title: 'Joe Schmo',
-                description: 'joe@xxxxxxxxxxx'
-            }
-        ]);
-        var search_ok = false;
-        var picker = this.picker;
-        this.picker.subscribe('search', function(e) {
-            var search_string = e.details[0];
-            var filter_name = e.details[3];
-            Assert.areEqual('Joe', search_string);
-            Assert.areEqual('PROJECT', filter_name);
-            Assert.areEqual('Product', picker.get('current_filter_value'));
-            search_ok = true;
-            picker.set('results', []);
-        });
-        var filter_div = this.picker.get('boundingBox')
-            .one('.yui3-picker-filter');
-        var filter_link = filter_div.one('a:nth-child(2)');
-        filter_link.simulate('click');
-        Assert.isTrue(search_ok);
-        this._check_filter([
-            {css: 'js-action', title: 'All', description: 'Display all'},
-            {css: 'invalid-link', title: 'Product',
-                description: 'Display products'}
-        ]);
-    },
-
-    test_search_resets_filter: function () {
-        // When a new search is performed the current filter is reset.
-        this.picker.render();
-        var picker = this.picker;
-        var search_ok = false;
-        this.picker.set('current_filter_value', 'Product');
-        this.picker._search_input.set('value', 'Joe');
-        this.picker.subscribe('search', function(e) {
-            var search_string = e.details[0];
-            var filter_name = e.details[3];
-            Assert.areEqual('Joe', search_string);
-            Assert.isFalse(Y.Lang.isValue(filter_name));
-            Assert.isFalse(
-                Y.Lang.isValue(picker.get('current_filter_value')));
-            search_ok = true;
-        });
-        this.picker._search_button.simulate('click');
-        Assert.isTrue(search_ok);
-    }
-}));
-
-suite.add(new Y.Test.Case({
-
-    name: 'picker_text_field_plugin',
-
-    setUp: function() {
-        this.search_input = Y.Node.create(
-                '<input id="field.initval" value="foo"/>');
-        Y.one(document.body).appendChild(this.search_input);
-        this.picker = new Y.lazr.picker.Picker({
-            associated_field_id: 'field.initval'
-        });
-    },
-
-    tearDown: function() {
-        cleanup_widget(this.picker);
-        this.search_input.remove();
-    },
-
-    test_TextFieldPickerPlugin_initial_value: function () {
-        this.picker.render();
-        this.picker.show();
-        Assert.areEqual('foo', this.picker._search_input.get('value'));
-    },
-
-    test_TextFieldPickerPlugin_selected_item_is_saved: function () {
-        this.picker.set('results', [{
-            'title': 'Object 1', value: 'first', metadata: 'new_metadata'}]);
-        this.picker.render();
-        var got_focus = false;
-        this.search_input.on('focus', function(e) {
-            got_focus = true;
-        });
-        simulate(
-            this.picker.get('boundingBox'),
-                '.yui3-picker-results li', 'click');
-        Assert.areEqual(
-            'first', Y.one('[id="field.initval"]').get("value"));
-        Assert.areEqual(
-            'new_metadata', this.picker.get('selected_value_metadata'));
-        Assert.areEqual(
-            'first', this.picker.get('selected_value'));
-        Assert.isTrue(got_focus, "focus didn't go to the search input.");
-    }
-
-}));
-
-Y.lp.testing.Runner.run(suite);
-
+/* Copyright (c) 2012, Canonical Ltd. All rights reserved. */
+
+YUI.add('lp.picker.test', function (Y) {
+    var tests = Y.namespace('lp.picker.test');
+    tests.suite = new Y.Test.Suite('picker Tests');
+
+    // 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.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: 'picker_tests',
+
+        setUp: function() {
+            this.picker = new Y.lazr.picker.Picker({
+                "selected_value": 'foo',
+                "selected_value_metadata": 'foobar'
+            });
+        },
+
+        tearDown: function() {
+            cleanup_widget(this.picker);
+        },
+
+        test_library_exists: function () {
+            Y.Assert.isObject(Y.lazr.picker,
+                "We should be able to locate the lazr.picker module");
+        },
+
+        test_picker_can_be_instantiated: function() {
+            Assert.isInstanceOf(
+                Y.lazr.picker.Picker, this.picker,
+                "Picker failed to be instantiated");
+        },
+
+        test_picker_initialisation: function() {
+            Assert.areEqual('foo', this.picker.get('selected_value'));
+            Assert.areEqual('foobar',
+                this.picker.get('selected_value_metadata'));
+        },
+
+        test_picker_is_stackable: function() {
+            // We should probably define an Assert.hasExtension.
+            Assert.areSame(
+                Y.WidgetStack.prototype.sizeShim, this.picker.sizeShim,
+                "Picker should be stackable.");
+            Assert.areSame(
+                Y.WidgetPositionAlign.prototype.align, this.picker.align,
+                "Picker should be positionable.");
+        },
+
+        test_picker_has_elements: function () {
+            /**
+             * Test that renderUI() adds search box, an error container and a
+             * results container to the widget.
+             * */
+            this.picker.render();
+
+            var bb = this.picker.get('boundingBox');
+            Assert.isNotNull(
+                bb.one('.yui3-picker-search'),
+                "Missing search box.");
+            Assert.isNotNull(
+                bb.one('.lazr-search.lazr-btn'),
+                "Missing search button.");
+            Assert.isNotNull(
+                bb.one('.yui3-picker-results'),
+                "Missing search results.");
+            Assert.isNotNull(
+                bb.one('.yui3-picker-error'), "Missing error box.");
+        },
+
+        test_set_results_updates_display: function () {
+            this.picker.render();
+            var image_url = '../../lazr/assets/skins/sam/search.png';
+            this.picker.set('results', [
+                {
+                    image: image_url,
+                    css: 'yui3-blah-blue',
+                    value: 'jschmo',
+                    title: 'Joe Schmo',
+                    description: 'joe@xxxxxxxxxxx'
+                }
+            ]);
+            var bb = this.picker.get('boundingBox');
+            var li = bb.one('.yui3-picker-results li');
+            Assert.isNotNull(li, "Results not found");
+            Assert.isTrue(li.hasClass('yui3-blah-blue'), "Missing class name.");
+            Assert.isNotNull(li.one('img'), "Missing image.");
+            Assert.areEqual(
+                image_url, li.one('img').getAttribute('src'),
+                "Unexpected image url");
+            var title_el = li.one('.yui3-picker-result-title');
+            Assert.isNotNull(title_el, "Missing title element");
+            Assert.areEqual(
+                'Joe Schmo', title_el.get('text'), 'Unexpected title value.');
+            var description_el = li.one('.yui3-picker-result-description');
+            Assert.isNotNull(description_el, "Missing description element.");
+            Assert.areEqual(
+                'joe@xxxxxxxxxxx', description_el.get('text'),
+                'Unexpected description value.');
+        },
+
+        test_alternate_title_text: function () {
+            this.picker.render();
+            this.picker.set('results', [
+                {
+                    css: 'yui3-blah-blue',
+                    value: 'jschmo',
+                    title: 'Joe Schmo',
+                    description: 'joe@xxxxxxxxxxx',
+                    alt_title: 'Another Joe'
+                }
+            ]);
+            var bb = this.picker.get('boundingBox');
+            var li = bb.one('.yui3-picker-results li');
+            var title_el = li.one('.yui3-picker-result-title');
+            Assert.isNotNull(title_el, "Missing title element");
+            Assert.areEqual('A', title_el.get('tagName'), 'Tab key is broken.');
+            Assert.isTrue(title_el.hasClass('js-action'));
+            Assert.areEqual(
+                'Joe Schmo\u00a0(Another Joe)', title_el.get('text'),
+                'Unexpected title value.');
+        },
+
+        test_title_links: function () {
+            this.picker.render();
+            this.picker.set('results', [
+                {
+                    css: 'yui3-blah-blue',
+                    value: 'jschmo',
+                    title: 'Joe Schmo',
+                    description: 'joe@xxxxxxxxxxx',
+                    alt_title: 'Joe Again <foo></foo>',
+                    title_link: 'http://somewhere.com',
+                    alt_title_link: 'http://somewhereelse.com',
+                    link_css: 'cool-style',
+                    details: ['Member since 2007']
+                }
+            ]);
+
+            function check_link(picker, link_selector, title, href) {
+                var bb = picker.get('boundingBox');
+                var link_clicked = false;
+                var link_node = bb.one(link_selector);
+
+                Assert.areEqual(title, link_node.get('text'));
+                Assert.areEqual(href, link_node.get('href'));
+
+                Y.on('click', function(e) {
+                    link_clicked = true;
+                }, link_node);
+                simulate(bb, link_selector, 'click');
+                Assert.isTrue(link_clicked,
+                    link_selector + ' link was not clicked');
+            }
+            check_link(
+                this.picker, 'a.cool-style:nth-child(1)', 'Joe Schmo',
+                'http://somewhere.com/');
+            check_link(
+                this.picker, 'a.cool-style:last-child', 'View details',
+                'http://somewhereelse.com/');
+            var alt_text_node = this.picker.get('boundingBox')
+                .one('.yui3-picker-result-title span');
+            Assert.areEqual('Joe Again <foo></foo>', alt_text_node.get('text'));
+        },
+
+        test_details: function () {
+            // The details of the li is the content node of the expander.
+            this.picker.render();
+            this.picker.set('results', [
+                {
+                    css: 'yui3-blah-blue',
+                    value: 'jschmo',
+                    title: 'Joe Schmo',
+                    description: 'joe@xxxxxxxxxxx',
+                    details: ['joe on irc.freenode.net', 'Member since 2007'],
+                    alt_title_link: '/~jschmo'
+                }
+            ]);
+            var bb = this.picker.get('boundingBox');
+            var li = bb.one('.yui3-picker-results li');
+            var expander_action = li.expander.icon_node.get('firstChild');
+            Assert.areEqual(
+                'A', expander_action.get('tagName'), 'Tab key is broken.');
+            var details = li.expander.content_node;
+            Assert.areEqual(
+                'joe on irc.freenode.net<br>Member since 2007',
+                details.one('div').getContent());
+            Assert.areEqual(
+                'Select Joe Schmo',
+                details.one('ul li:first-child').get('text'));
+            Assert.areEqual(
+                'View details', details.one('ul li:last-child').get('text'));
+        },
+
+        test_details_escaping: function () {
+            // The content of details is escaped.
+            this.picker.render();
+            this.picker.set('results', [
+                {
+                    css: 'yui3-blah-blue',
+                    value: 'jschmo',
+                    title: 'Joe <Schmo>',
+                    description: 'joe@xxxxxxxxxxx',
+                    details: ['<joe> on irc.freenode.net',
+                        'f<nor>d maintainer'],
+                    alt_title_link: '/~jschmo'
+                }
+            ]);
+            var bb = this.picker.get('boundingBox');
+            var li = bb.one('.yui3-picker-results li');
+            var details = li.expander.content_node;
+            Assert.areEqual(
+                '&lt;joe&gt; on irc.freenode.net<br>f&lt;nor&gt;d maintainer',
+                details.one('div').getContent());
+            Assert.areEqual(
+                'Select Joe &lt;Schmo&gt;',
+                details.one('ul li:first-child a').getContent('text'));
+        },
+
+        test_expander_only_one_open: function() {
+            // Only one expanded details entry should be open at any time.
+            this.picker.render();
+            this.picker.set('results', [
+                {
+                    value: 'jschmo',
+                    title: 'Joe Schmo',
+                    details: ['detail 1', 'detail 2']
+                },
+                {
+                    value: 'jsmith',
+                    title: 'Joe Smith',
+                    details: ['detail 1', 'detail 2']
+                }
+            ]);
+            var bb = this.picker.get('boundingBox');
+            var first_entry = bb.one('.yui3-picker-results li');
+            var first_expander = first_entry.expander;
+            var second_entry = bb.one('.yui3-picker-results li.yui3-lazr-odd');
+            var second_expander = second_entry.expander;
+            first_expander.icon_node.simulate('click');
+            Y.Assert.isTrue(first_expander.isExpanded());
+            Y.Assert.isFalse(second_expander.isExpanded());
+
+            // Open the other expander and check that the first one has closed.
+            second_expander.icon_node.simulate('click');
+            Y.Assert.isFalse(first_expander.isExpanded());
+            Y.Assert.isTrue(second_expander.isExpanded());
+        },
+
+        test_expander_multiple_pickers: function() {
+            // Expanders for one picker should not interfere with those for
+            // another picker. ie if Picker A expander is opened, any open
+            // expanders on Picker B should remain open.
+            var results = [
+                {
+                    value: 'jschmo',
+                    title: 'Joe Schmo',
+                    details: ['detail 1', 'detail 2']
+                },
+                {
+                    value: 'jsmith',
+                    title: 'Joe Smith',
+                    details: ['detail 1', 'detail 2']
+                }
+            ];
+            this.picker.render();
+            this.picker.set('results', results);
+
+            var another_picker = new Y.lazr.picker.Picker();
+            another_picker.render();
+            another_picker.set('results', results);
+
+            var bb = this.picker.get('boundingBox');
+            var picker_entry = bb.one('.yui3-picker-results li');
+            var picker_expander = picker_entry.expander;
+            picker_expander.icon_node.simulate('click');
+
+            bb = another_picker.get('boundingBox');
+            var another_picker_entry = bb.one('.yui3-picker-results li');
+            var another_picker_expander = another_picker_entry.expander;
+            another_picker_expander.icon_node.simulate('click');
+
+            Y.Assert.isTrue(picker_expander.isExpanded());
+            Y.Assert.isTrue(another_picker_expander.isExpanded());
+            cleanup_widget(another_picker);
+        },
+
+        test_details_save_link: function () {
+            // The select link the li's details saves the selection.
+            this.picker.render();
+            this.picker.set('results', [
+                {
+                    css: 'yui3-blah-blue',
+                    value: 'jschmo',
+                    title: 'Joe Schmo',
+                    description: 'joe@xxxxxxxxxxx',
+                    alt_title_link: 'http://somewhereelse.com',
+                    link_css: 'cool-style',
+                    details: ['Member since 2007']
+                }
+            ]);
+            var bb = this.picker.get('boundingBox');
+            var link_node = bb.one('a.save');
+            Assert.areEqual('Select Joe Schmo', link_node.get('text'));
+            Assert.isTrue(link_node.get('href').indexOf(window.location) === 0);
+            var selected_value = null;
+            this.picker.subscribe('save', function(e) {
+                selected_value = e.details[0].value;
+            }, this);
+            simulate(bb, 'a.save', 'click');
+            Assert.areEqual('jschmo', selected_value);
+        },
+
+        test_title_badges: function () {
+            this.picker.render();
+            var badge_info = [
+                {
+                    url: '../../lazr/assets/skins/sam/search.png',
+                    label: 'product 1',
+                    role: 'driver'},
+                {   url: '../../lazr/assets/skins/sam/spinner.png',
+                    label: 'product 2',
+                    role: 'maintainer'},
+                {   url: '../../lazr/assets/skins/sam/spinner.png',
+                    label: 'product 2',
+                    role: 'driver'
+                }];
+            this.picker.set('results', [
+                {
+                    badges: badge_info,
+                    css: 'yui3-blah-blue',
+                    value: 'jschmo',
+                    title: 'Joe Schmo',
+                    description: 'joe@xxxxxxxxxxx'
+                }
+            ]);
+            var bb = this.picker.get('boundingBox');
+            var li = bb.one('.yui3-picker-results li');
+            var i;
+            for (i=0; i<badge_info.length; i++) {
+                var img_node = li.one(
+                    'div.badge img.badge:nth-child(' + (i + 1) + ')');
+                // Check that duplicate badge urls are not displayed.
+                if (i===2) {
+                    Assert.isNull(img_node);
+                    break;
+                }
+                Assert.areEqual(
+                    badge_info[i].url, img_node.getAttribute('src'),
+                    'Unexpected badge url');
+                var badge_text = badge_info[i].label + ' ' + badge_info[i].role;
+                Assert.areEqual(
+                    badge_text, img_node.get('alt'),
+                    'Unexpected badge alt text');
+            }
+        },
+
+        test_details_badges: function () {
+            // The affiliation details are rendered correctly in the content
+            // node of the expander.
+            this.picker.render();
+            var badge_info = [
+                {
+                    url: '../../lazr/assets/skins/sam/spinner.png',
+                    label: 'product 1',
+                    role: 'maintainer'},
+                {
+                    url: '../../lazr/assets/skins/sam/search.png',
+                    label: 'product 2',
+                    role: 'driver'}];
+            this.picker.set('results', [
+                {
+                    badges: badge_info,
+                    css: 'yui3-blah-blue',
+                    value: 'jschmo',
+                    title: 'Joe Schmo',
+                    description: 'joe@xxxxxxxxxxx'
+                }
+            ]);
+            var bb = this.picker.get('boundingBox');
+            var li = bb.one('.yui3-picker-results li');
+            var details = li.expander.content_node;
+            var affiliation_header =
+                details.one('div.affiliation:nth-child(1)');
+            Assert.areEqual('Affiliation', affiliation_header.get('text'));
+            var badge_img = affiliation_header.one('img');
+            Assert.areEqual('product 1 maintainer', badge_img.get('alt'));
+            Assert.areEqual(
+                '../../lazr/assets/skins/sam/spinner.png',
+                badge_img.getAttribute('src'));
+            affiliation_header = details.one('div.affiliation:nth-child(3)');
+            badge_img = affiliation_header.one('img');
+            Assert.areEqual('product 2 driver', badge_img.get('alt'));
+            Assert.areEqual(
+                '../../lazr/assets/skins/sam/search.png',
+                badge_img.getAttribute('src'));
+            var affiliation_text = details.one(
+                'div.affiliation-text:nth-child(2)');
+            Assert.areEqual(
+                'product 1 maintainer', affiliation_text.get('innerHTML'));
+            affiliation_text = details.one('div.affiliation-text:nth-child(4)');
+            Assert.areEqual(
+                'product 2 driver', affiliation_text.get('innerHTML'));
+        },
+
+        test_results_display_escaped: function () {
+            this.picker.render();
+            this.picker.set('results', [
+                {
+                    image: '<script>throw "back";</script>',
+                    css: 'yui3-blah-blue',
+                    value: '<script>throw "wobbly";</script>',
+                    title: '<script>throw "toys out of pram";</script>',
+                    description: '<script>throw "up";</script>'
+                }
+            ]);
+            var bb = this.picker.get('boundingBox');
+            var li = bb.one('.yui3-picker-results li');
+            var image_el = li.one('img');
+            Assert.areEqual(
+                '<script>throw "back";</script>', image_el.getAttribute('src'),
+                "Unexpected image url");
+            var title_el = li.one('.yui3-picker-result-title');
+            Assert.areEqual(
+                '&lt;script&gt;throw "toys out of pram";&lt;/script&gt;',
+                title_el.get('innerHTML'), 'Unexpected title value.');
+            var description_el = li.one('.yui3-picker-result-description');
+            Assert.areEqual(
+                '&lt;script&gt;throw "up";&lt;/script&gt;',
+                description_el.get('innerHTML'),
+                'Unexpected description value.');
+        },
+
+        test_results_updates_display_with_missing_data: function () {
+            this.picker.render();
+            var image_url = '../../lazr/assets/skins/sam/search.png';
+            this.picker.set('results', [
+                { value: 'jschmo', title: 'Joe Schmo' }
+            ]);
+            var bb = this.picker.get('boundingBox');
+            var li = bb.one('.yui3-picker-results li');
+            Assert.isNotNull(li, "Results not found.");
+            Assert.areEqual(Y.lazr.ui.CSS_EVEN, li.getAttribute('class'));
+            Assert.isNull(li.one('img'), "Unexpected image.");
+            var description_el = li.one('.yui3-picker-result-description.');
+            Assert.isNull(description_el, "Unexpected description element.");
+        },
+
+        test_render_displays_initial_results: function () {
+            this.picker.set('results', [
+                    {'title': 'Title 1'},
+                    {'title': 'Title 2'}
+                ]);
+            this.picker.render();
+            var bb = this.picker.get('boundingBox');
+            var results = bb.all('.yui3-picker-results li');
+            Assert.isNotNull(results, "Results not found.");
+            Assert.areEqual(2, results.size());
+        },
+
+        test_resetting_results_removes_previous_results: function () {
+            this.picker.render();
+            var bb = this.picker.get('boundingBox');
+
+            // First time setting the results.
+            this.picker.set('results', [
+                    {'title': 'Title 1'},
+                    {'title': 'Title 2'}
+                ]);
+            var results = bb.all('.yui3-picker-results li');
+            Assert.isNotNull(results, "Results not found.");
+            Assert.areEqual(2, results.size());
+
+            // Second time setting the results.
+            this.picker.set('results', [
+                    {'title': 'Title 1'}
+                ]);
+            results = bb.all('.yui3-picker-results li');
+            Assert.isNotNull(results, "Results not found");
+            Assert.areEqual(1, results.size());
+        },
+
+        test_updateResultsDisplay_adds_even_odd_class: function () {
+            this.picker.set('results', [
+                    {'title': 'Title 1'},
+                    {'title': 'Title 2'},
+                    {'title': 'Title 1'},
+                    {'title': 'Title 2'}
+                ]);
+            this.picker.render();
+            var bb = this.picker.get('boundingBox');
+            var results = bb.all('.yui3-picker-results li');
+            Assert.isNotNull(results, "Results not found.");
+            ArrayAssert.itemsAreEqual(
+                [true, false, true, false],
+                results.hasClass(Y.lazr.ui.CSS_EVEN));
+            ArrayAssert.itemsAreEqual(
+                [false, true, false, true],
+                results.hasClass(Y.lazr.ui.CSS_ODD));
+        },
+
+        test_clicking_search_button_fires_search_event: function () {
+            this.picker.render();
+
+            var bb = this.picker.get('boundingBox');
+            var input = bb.one('.yui3-picker-search');
+            input.set('value', 'a search');
+            var event_has_fired = false;
+            this.picker.subscribe('search', function(e) {
+                    event_has_fired = true;
+                    Assert.areEqual(
+                        'a search', e.details[0],
+                        'Search event is missing the search string.');
+            }, this);
+            simulate(
+                this.picker.get('boundingBox'),
+                '.lazr-search.lazr-btn',
+                'click');
+            Assert.isTrue(event_has_fired, "search event wasn't fired");
+        },
+
+        test_set_search_mode_disables_search_button: function () {
+            this.picker.render();
+
+            var bb = this.picker.get('boundingBox');
+            this.picker.set('search_mode', true);
+            Assert.isTrue(
+                bb.one('.lazr-search.lazr-btn').get('disabled'),
+                "Search button wasn't disabled.");
+            this.picker.set('search_mode', false);
+            Assert.isFalse(
+                bb.one('.lazr-search.lazr-btn').get('disabled'),
+                "Search button wasn't re-enabled.");
+        },
+
+        test_hitting_enter_in_search_input_fires_search_event: function () {
+            this.picker.render();
+
+            var bb = this.picker.get('boundingBox');
+            var input = bb.one('.yui3-picker-search');
+            input.set('value', 'a search');
+            var event_has_fired = false;
+            this.picker.subscribe('search', function() {
+                event_has_fired = true;
+            }, this);
+            simulate(
+                this.picker.get('boundingBox'),
+                '.yui3-picker-search',
+                'keydown',
+                {keyCode: 13});
+            Assert.isTrue(event_has_fired, "search event wasn't fired");
+        },
+
+        test_search_event_sets_the_in_search_mode: function () {
+            this.picker.render();
+
+            Assert.isFalse(
+                this.picker.get('search_mode'),
+                "Widget shouldn't be in search mode.");
+            this.picker.fire('search');
+            Assert.isTrue(
+                this.picker.get('search_mode'),
+                "Widget should be in search mode.");
+        },
+
+        test_search_event_resets_the_current_batch: function () {
+            this.picker.render();
+            this.picker.set('batches', [
+                {value: 'batch1', name: 'Batch 1'},
+                {value: 'batch2', name: 'Batch 2'}
+                ]);
+            this.picker.set('selected_batch', 1);
+            Assert.areEqual(1, this.picker.get('selected_batch'));
+            this.picker._search_input.set('value', 'bar');
+            simulate(
+                this.picker.get('boundingBox'),
+                '.lazr-search.lazr-btn',
+                'click');
+
+            // An initial search resets the batch.
+            Assert.areEqual(0, this.picker.get('selected_batch'));
+
+            // Batch is reset if search term has changed.
+            this.picker.set('search_mode', false);
+            this.picker.set('selected_batch', 1);
+            this.picker._search_input.set('value', 'foo');
+            simulate(
+                this.picker.get('boundingBox'),
+                '.lazr-search.lazr-btn',
+                'click');
+            Assert.areEqual(0, this.picker.get('selected_batch'));
+
+            // Batch is not reset if search term hasn't changed.
+            this.picker.set('search_mode', false);
+            this.picker.set('selected_batch', 1);
+            simulate(
+                this.picker.get('boundingBox'),
+                '.lazr-search.lazr-btn',
+                'click');
+            Assert.areEqual(1, this.picker.get('selected_batch'));
+        },
+
+        test_setting_search_mode: function () {
+            // Setting search_mode adds a CSS class and disables the search box.
+
+            this.picker.render();
+
+            this.picker.set('search_mode', true);
+            var bb = this.picker.get('boundingBox');
+            Assert.isTrue(
+                bb.one('.yui3-picker-search').get('disabled'),
+                "Search box should be disabled.");
+            Assert.isTrue(
+                bb.hasClass('yui3-picker-search-mode'),
+                'Missing CSS class on widget.');
+        },
+
+        test_unsetting_search_mode: function () {
+
+            this.picker.render();
+
+            this.picker.set('search_mode', true);
+            this.picker.set('search_mode', false);
+            var bb = this.picker.get('boundingBox');
+            Assert.isFalse(
+                bb.one('.yui3-picker-search').get('disabled'),
+                "Search input should be enabled.");
+            Assert.isFalse(
+                bb.hasClass('yui3-picker-search-mode'),
+                'CSS class should be removed from the widget.');
+        },
+
+        test_set_results_remove_search_mode: function () {
+            this.picker.render();
+
+            this.picker.set('search_mode', true);
+            this.picker.set('results', []);
+
+            Assert.isFalse(
+                this.picker.get('search_mode'),
+                "Widget should be out of search_mode.");
+        },
+
+        test_set_error: function () {
+            // Setting the error property displays the string in the
+            // error box and puts an in-error CSS class on the widget.
+            this.picker.render();
+
+            var error_msg = 'Sorry an <error> occured.';
+            this.picker.set('error', error_msg);
+
+            var bb = this.picker.get('boundingBox');
+            Assert.areEqual(
+                error_msg, bb.one('.yui3-picker-error').get('text'),
+                "Error message wasn't displayed.");
+            Assert.isTrue(
+                bb.hasClass('yui3-picker-error-mode'),
+                "Missing error-mode class.");
+        },
+
+        test_set_error_null_clears_ui: function () {
+            this.picker.render();
+
+            this.picker.set('error', 'Sorry an error occured.');
+            this.picker.set('error', null);
+            var bb = this.picker.get('boundingBox');
+            Assert.areEqual('', bb.one('.yui3-picker-error').get('text'),
+                "Error message wasn't cleared.");
+            Assert.isFalse(
+                bb.hasClass('yui3-picker-error-mode'),
+                "error-mode class should be removed.");
+        },
+
+        test_small_search_sets_error: function () {
+            this.picker.render();
+            this.picker.set('min_search_chars', 4);
+            var bb = this.picker.get('boundingBox');
+            var input = bb.one('.yui3-picker-search');
+            input.set('value', ' 1 3 '); // 3 characters after trim.
+            simulate(
+                this.picker.get('boundingBox'),
+                '.lazr-search.lazr-btn',
+                'click');
+            Assert.areEqual(
+                "Please enter at least 4 characters.",
+                this.picker.get('error'),
+                "Error message wasn't displayed.");
+        },
+
+        test_click_on_result_fire_save_event: function () {
+            this.picker.set('results', [
+                {'title': 'Object 1', value: 'first'},
+                {'title': 'Object 2', value: 'second'}
+            ]);
+
+            this.picker.render();
+
+            var event_has_fired = false;
+            this.picker.subscribe('save', function(e) {
+                event_has_fired = true;
+                Assert.areEqual(
+                    'first', e.details[0].value,
+                    "The event value of the clicked li is wrong.");
+                Assert.areEqual(
+                    'Object 1', e.details[0].title,
+                    "The event title of the clicked li is wrong.");
+            }, this);
+            simulate(
+                this.picker.get('boundingBox'),
+                    '.yui3-picker-results li', 'click');
+            Assert.isTrue(event_has_fired, "save event wasn't fired.");
+        },
+
+        test_cancel_event_hides_widget: function () {
+            this.picker.render();
+
+            this.picker.fire('cancel', 'bogus');
+            Assert.isFalse(
+                this.picker.get('visible'), "The widget should be hidden.");
+        },
+
+        test_cancel_event_resets_search_mode: function () {
+            this.picker.render();
+            this.picker.set('search_mode', true);
+            Assert.isTrue(this.picker.get('search_mode'));
+            this.picker.fire('cancel', 'bogus');
+            Assert.isFalse(this.picker.get('search_mode'));
+        },
+
+        test_save_event_hides_widget: function () {
+            this.picker.render();
+
+            this.picker.fire('save', 'bogus');
+            Assert.isFalse(
+                this.picker.get('visible'), "The widget should be hidden.");
+        },
+
+        test_save_event_clears_widget_by_default: function () {
+            this.picker.render();
+
+            this.picker._search_input.set('value', 'foo');
+            this.picker.fire('save', 'bogus');
+            Assert.areEqual(
+                '', this.picker._search_input.get('value'),
+                "The widget hasn't been cleared");
+        },
+
+        test_save_does_not_clear_widget_when_clear_on_save_is_false: function () {
+            picker = new Y.lazr.picker.Picker({clear_on_save: false});
+            picker.render();
+
+            picker._search_input.set('value', 'foo');
+            picker.fire('save', 'bogus');
+            Assert.areEqual(
+                'foo', picker._search_input.get('value'),
+                "The widget has been cleared but it should not");
+        },
+
+        test_cancel_event_does_not_clear_widget_by_default: function () {
+            this.picker.render();
+
+            this.picker._search_input.set('value', 'foo');
+            this.picker.fire('cancel', 'bogus');
+            Assert.areEqual(
+                'foo', this.picker._search_input.get('value'),
+                "The widget has been cleared but it should not");
+        },
+
+        test_cancel_event_clears_widget_when_clear_on_cancel_true: function () {
+            picker = new Y.lazr.picker.Picker({clear_on_cancel: true});
+            picker.render();
+
+            picker._search_input.set('value', 'foo');
+            picker.fire('cancel', 'bogus');
+            Assert.areEqual(
+                '', picker._search_input.get('value'),
+                "The widget hasn't been cleared");
+        },
+
+        test_search_clears_any_eror: function () {
+            this.picker.render();
+            this.picker.set('error', "An error");
+
+            this.picker.fire('search');
+
+            Assert.isNull(
+                this.picker.get('error'), 'Error should be cleared.');
+
+        },
+
+        test_no_search_result_msg: function () {
+            this.picker.render();
+
+            this.picker.set(
+                'no_results_search_message', "Your query '{query}' sucked.");
+            var bb = this.picker.get('boundingBox');
+            bb.one('.yui3-picker-search').set('value', 'my <search> string');
+            this.picker.set('results', []);
+
+            var search_results = bb.one('.yui3-picker-results');
+            Assert.areEqual(
+                "Your query 'my <search> string' sucked.",
+                search_results.get('text'),
+                "Empty results message wasn't displayed.");
+            Assert.isTrue(
+                search_results.hasClass('yui3-picker-no-results'),
+                "Missing no-results CSS class.");
+        },
+
+        test_search_results_clear_no_results_css: function () {
+            this.picker.render();
+
+            var bb = this.picker.get('boundingBox');
+            bb.one('.yui3-picker-search').set('value', 'my search string');
+            this.picker.set('results', []);
+
+            this.picker.set('results',
+                [{title: 'Title 1'},
+                {title: 'Title 2'}]);
+            Assert.isFalse(
+                bb.one('.yui3-picker-results').
+                    hasClass('yui3-picker-no-results'),
+                "The no-results CSS class should have been removed.");
+        },
+
+        test_setting_search_slot_updates_ui: function () {
+            this.picker.render();
+            var filler = '<span>hello</span>';
+            this.picker.set('search_slot', Y.Node.create(filler));
+            var bb = this.picker.get('boundingBox');
+            var div = bb.one('.yui3-picker-search-slot');
+
+            Assert.isNotNull(div, 'Container for form extras not found.');
+            Assert.areEqual(filler, div.get('innerHTML'));
+        },
+
+        test_setting_footer_slot_updates_ui: function () {
+            this.picker.render();
+            var filler = '<span>foobar</span>';
+            this.picker.set('footer_slot', Y.Node.create(filler));
+            var bb = this.picker.get('boundingBox');
+            var div = bb.one('.yui3-picker-footer-slot');
+
+            Assert.isNotNull(div, 'Container for form extras not found.');
+            Assert.areEqual(filler, div.get('innerHTML'));
+        },
+
+        test_setting_batches_updates_ui: function () {
+            this.picker.render();
+            this.picker.set('batches', [
+                {value: 'new', name: 'New'},
+                {value: 'assigned', name: 'Assigned'}
+                ]);
+            var bb = this.picker.get('boundingBox');
+            Assert.isNotNull(
+                bb.one('.yui3-picker-batches span'),
+                "Container for batches not found.");
+            var batches = bb.all('.yui3-picker-batches span');
+            Assert.isNotNull(batches, "Batches not found");
+            Assert.areEqual(2, batches.size());
+            ArrayAssert.itemsAreEqual(
+                ['New', 'Assigned'],
+                batches.get('text'),
+                "Batches don't contain batch names.");
+            ArrayAssert.itemsAreEqual(
+                [true, false],
+                batches.hasClass('yui3-picker-selected-batch'),
+                "Selected batches missing CSS class.");
+
+            Assert.isNotNull(
+                bb.one('.yui3-picker-batches .lazr-prev'),
+                "There should be a previous button.");
+            Assert.isNotNull(
+                bb.one('.yui3-picker-batches .lazr-next'),
+                "There should be a next button.");
+        },
+
+        test_simplified_batching_interface: function () {
+            this.picker.render();
+            this.picker.set('batch_count', 4);
+            this.picker.set('results', [
+                { value: 'aardvark', title: 'Aardvarks' },
+                { value: 'bats', title: 'Bats' },
+                { value: 'cats', title: 'Cats' },
+                { value: 'dogs', title: 'Dogs' },
+                { value: 'emus', title: 'Emus' },
+                { value: 'frogs', title: 'Frogs' },
+                { value: 'gerbils', title: 'Gerbils' }
+            ]);
+            var bb = this.picker.get('boundingBox');
+            Assert.isNotNull(
+                bb.one('.yui3-picker-batches span'),
+                "Container for batches not found.");
+            var batches = bb.all('.yui3-picker-batches span');
+            Assert.isNotNull(batches, "Batches not found");
+            Assert.areEqual(4, batches.size());
+            ArrayAssert.itemsAreEqual(
+                ['1', '2', '3', '4'],
+                batches.get('text'),
+                "Batches don't contain batch names.");
+            ArrayAssert.itemsAreEqual(
+                [true, false, false, false],
+                batches.hasClass('yui3-picker-selected-batch'),
+                "Selected batches missing CSS class.");
+
+            Assert.isNotNull(
+                bb.one('.yui3-picker-batches .lazr-prev'),
+                "There should be a previous button.");
+            Assert.isNotNull(
+                bb.one('.yui3-picker-batches .lazr-next'),
+                "There should be a next button.");
+        },
+
+        test_clicking_a_batch_item_fires_search_event: function () {
+            this.picker.set('current_search_string', 'search');
+            this.picker.set('batches', [
+                {value: 'item1', name: 'Item 1'},
+                {value: 'item2', name: 'Item 2'}
+                ]);
+            this.picker.render();
+
+            var bb = this.picker.get('boundingBox');
+            var event_has_fired = false;
+            this.picker.subscribe('search', function(e) {
+                event_has_fired = true;
+                ArrayAssert.itemsAreEqual(
+                    ['search', 'item1'], e.details,
+                    "Search event details should contain search" +
+                    "string and selected batch");
+            }, this);
+            simulate(
+                this.picker.get('boundingBox'),
+                '.yui3-picker-batches span', 'click');
+            Assert.isTrue(event_has_fired, "search event wasn't fired.");
+        },
+
+        test_clicking_a_batch_item_sets_selected_batch: function () {
+            this.picker.set('current_search_string', 'search');
+            this.picker.set('batches', [
+                {value: 'item1', name: 'Item 1'},
+                {value: 'item2', name: 'Item 2'}
+                ]);
+            this.picker.render();
+
+            var bb = this.picker.get('boundingBox');
+            Assert.areEqual(
+                0, this.picker.get('selected_batch'),
+                "First batch should be selected.");
+            simulate(
+                this.picker.get('boundingBox'),
+                '.yui3-picker-batches span:nth-last-child(2)', 'click');
+            Assert.areEqual(
+                1, this.picker.get('selected_batch'),
+                "selected_batch should have been updated.");
+        },
+
+        test_set_selected_batch_updates_css: function () {
+            this.picker.render();
+            this.picker.set('batches', [
+                {value: 'item1', name: '1'},
+                {value: 'item2', name: '2'}
+                ]);
+            Assert.areEqual(
+                0, this.picker.get('selected_batch'),
+                "Expected first batch to be selected by default.");
+            this.picker.set('selected_batch', 1);
+
+            var bb = this.picker.get('boundingBox');
+            var batches = bb.all('.yui3-picker-batches span');
+            ArrayAssert.itemsAreEqual(
+                [false, true],
+                batches.hasClass('yui3-picker-selected-batch'),
+                "Selected batch missing CSS class.");
+        },
+
+        test_set_selected_batch_validator: function () {
+            this.picker.render();
+            this.picker.set('batches', [
+                {value: 'item1', name: '1'},
+                {value: 'item2', name: '2'}
+                ]);
+
+            this.picker.set('selected_batch', -1);
+            Assert.areEqual(
+                0, this.picker.get('selected_batch'),
+                "Negative index shouldn't update selected_batch.");
+
+            this.picker.set('selected_batch', 3);
+            Assert.areEqual(
+                0, this.picker.get('selected_batch'),
+                "Index greather than last batch item shouldn't " +
+                "update selected_batch.");
+
+            this.picker.set('selected_batch', 'one');
+            Assert.areEqual(
+                0, this.picker.get('selected_batch'),
+                "Non-integere shouldn't update selected_batch.");
+        },
+
+        test_prev_button_is_disabled_only_on_first_batch: function () {
+            this.picker.set('batches', [
+                {value: 'item1', name: '1'},
+                {value: 'item2', name: '2'}
+                ]);
+            this.picker.render();
+
+            var bb = this.picker.get('boundingBox');
+            Assert.isTrue(
+                bb.one('.lazr-prev').get('disabled'),
+                "Previous button should be disabled on first batch.");
+
+            this.picker.set('selected_batch', 1);
+            Assert.isFalse(
+                bb.one('.lazr-prev').get('disabled'),
+                "Previous button shouldn't be disabled on last batch.");
+        },
+
+        test_next_button_is_disabled_only_on_last_batch: function () {
+            this.picker.set('batches', [
+                {value: 'item1', name: '1'},
+                {value: 'item2', name: '2'}
+                ]);
+            this.picker.render();
+
+            var bb = this.picker.get('boundingBox');
+            Assert.isFalse(
+                bb.one('.lazr-next').get('disabled'),
+                "Next button shouldn't be disabled on first batch.");
+
+            this.picker.set('selected_batch', 1);
+            Assert.isTrue(
+                bb.one('.lazr-next').get('disabled'),
+                "Previous button should be disabled on last batch.");
+        },
+
+        test_click_on_next_button_selects_next_batch: function () {
+            this.picker.set('batches', [
+                {value: 'item1', name: '1'},
+                {value: 'item2', name: '2'},
+                {value: 'item3', name: '3'}
+                ]);
+            this.picker.set('selected_batch', 1);
+            this.picker.render();
+
+            var bb = this.picker.get('boundingBox');
+            simulate(
+                this.picker.get('boundingBox'), '.lazr-next.lazr-btn', 'click');
+            Assert.areEqual(
+                2, this.picker.get('selected_batch'),
+                "Next batch should have been selected.");
+        },
+
+        test_click_on_next_button_fires_search_event: function () {
+            this.picker.set('current_search_string', 'search');
+            this.picker.set('batches', [
+                {value: 'item1', name: 'Item 1'},
+                {value: 'item2', name: 'Item 2'}
+                ]);
+            this.picker.render();
+
+            var event_has_fired = false;
+            this.picker.subscribe('search', function(e) {
+                event_has_fired = true;
+                ArrayAssert.itemsAreEqual(
+                    ['search', 'item2'], e.details,
+                    "Search event details should contain search" +
+                    "string and selected batch");
+            }, this);
+            simulate(
+                this.picker.get('boundingBox'),
+                '.yui3-picker-batches .lazr-next', 'click');
+            Assert.isTrue(event_has_fired, "search event wasn't fired.");
+        },
+
+        test_click_on_prev_button_selects_prev_batch: function () {
+            this.picker.set('batches', [
+                {value: 'item1', name: '1'},
+                {value: 'item2', name: '2'},
+                {value: 'item3', name: '3'}
+                ]);
+            this.picker.set('selected_batch', 1);
+            this.picker.render();
+
+            var bb = this.picker.get('boundingBox');
+            simulate(
+                this.picker.get('boundingBox'), '.lazr-prev.lazr-btn', 'click');
+            Assert.areEqual(
+                0, this.picker.get('selected_batch'),
+                "Previous batch should have been selected.");
+        },
+
+        test_click_on_prev_button_fires_search_event: function () {
+            this.picker.set('current_search_string', 'search');
+            this.picker.set('batches', [
+                {value: 'item1', name: 'Item 1'},
+                {value: 'item2', name: 'Item 2'}
+                ]);
+            this.picker.set('selected_batch', 1);
+            this.picker.render();
+
+            var event_has_fired = false;
+            this.picker.subscribe('search', function(e) {
+                event_has_fired = true;
+                ArrayAssert.itemsAreEqual(
+                    ['search', 'item1'], e.details,
+                    "Search event details should contain search" +
+                    "string and selected batch");
+            }, this);
+            simulate(
+                this.picker.get('boundingBox'),
+                '.yui3-picker-batches .lazr-prev', 'click');
+            Assert.isTrue(event_has_fired, "search event wasn't fired.");
+        },
+
+        test_buttons_are_displayed_only_if_there_are_batches: function () {
+            this.picker.render();
+
+            var bb = this.picker.get('boundingBox');
+            Assert.isNull(
+                bb.one('.yui3-picker-batches .lazr-prev'),
+                "There should be no previous button.");
+            Assert.isNull(
+                bb.one('.yui3-picker-batches .lazr-next'),
+                "There should be no next button.");
+        },
+
+        test_text_input_on_footer_can_be_focused: function () {
+            this.picker.render();
+            this.picker.set('footer_slot', Y.Node.create(
+                '<input class="extra-input" name="extra_input" type="text" />'));
+            var extra_input =
+                this.picker.get('boundingBox').one('.extra-input');
+            var got_focus = false;
+            extra_input.on('focus', function(e) {
+                got_focus = true;
+            });
+            extra_input.focus();
+            Assert.isTrue(got_focus, "focus didn't go to the extra input.");
+        },
+
+        test_overlay_progress_value: function () {
+            // Setting the progress attribute controls the overlay's
+            // green progress bar.
+            this.picker.render();
+            Assert.areEqual(
+                50,
+                this.picker.get('progress'),
+                "The picker should start out with progress at 50%.");
+
+            this.picker.set('results', [
+                {
+                    value: 'jschmo',
+                    title: 'Joe Schmo',
+                    description: 'joe@xxxxxxxxxxx'
+                }
+            ]);
+            Assert.areEqual(
+                100,
+                this.picker.get('progress'),
+                "The picker progress should be 100% with results.");
+
+            this.picker.set('results', []);
+            Assert.areEqual(
+                50,
+                this.picker.get('progress'),
+                "The picker progress should be 50% without results.");
+        },
+
+        test_exiting_search_mode_focus_search_box: function () {
+            this.picker.render();
+            this.picker.set('search_mode', true);
+
+            var bb = this.picker.get('boundingBox');
+            var search_input = bb.one('.yui3-picker-search');
+            var got_focus = false;
+            search_input.on('focus', function(e) {
+                got_focus = true;
+            });
+            this.picker.set('search_mode', false);
+            Assert.isTrue(got_focus, "focus didn't go to the search input.");
+        }
+    }));
+
+    tests.suite.add(new Y.Test.Case({
+
+        name: 'picker_with_filter',
+
+        setUp: function() {
+            this.picker = new Y.lazr.picker.Picker({
+                "selected_value": 'foo',
+                "selected_value_metadata": 'foobar',
+                "filter_options": [
+                    {'name': 'ALL',
+                     'title': 'All',
+                     'description': 'Display all'},
+                    {'name': 'PROJECT',
+                     'title': 'Product',
+                     'description': 'Display products'}
+                ]
+            });
+        },
+
+        tearDown: function() {
+            cleanup_widget(this.picker);
+        },
+
+        test_picker_has_elements: function () {
+             // Test renderUI() adds filter container container to the widget.
+            this.picker.render();
+
+            var bb = this.picker.get('boundingBox');
+            Assert.isNotNull(
+                bb.one('.yui3-picker-filter'),
+                "Missing filter box.");
+        },
+
+        _check_filter: function (filter_data) {
+            // Check the expected filter links are rendered with the correct
+            // data.
+            var filter_div = this.picker.get('boundingBox')
+                .one('.yui3-picker-filter');
+            var i;
+            for (i=0; i<filter_data.length; i++) {
+            var link = filter_div.one('a:nth-child(' + (i + 1) + ')');
+                Assert.isTrue(link.hasClass(filter_data[i].css));
+                Assert.areEqual(link.get('text'), filter_data[i].title);
+                Assert.areEqual(link.get('title'), filter_data[i].description);
+            }
+        },
+
+        test_no_results_does_not_render_filter: function () {
+            // Rendering empty results doesn't render the filter and clears it
+            // if it is already visible.
+            this.picker.render();
+            this.picker._search_input.set('value', 'Joe');
+            var filter_div = this.picker.get('boundingBox')
+                .one('.yui3-picker-filter');
+            // Make the filter visible by rendering some results.
+            this.picker.set('results', [
+                {
+                    value: 'jschmo',
+                    title: 'Joe Schmo',
+                    description: 'joe@xxxxxxxxxxx'
+                }
+            ]);
+            Assert.areNotEqual('', filter_div.get('innerHTML'));
+            // Reset and render empty results.
+            this.picker.set('current_filter_value', null);
+            this.picker.set('results', []);
+            Assert.areEqual('', filter_div.get('innerHTML'));
+        },
+
+        test_set_results_renders_filter: function () {
+            // Rendering results also renders the filter elements.
+            this.picker.render();
+            this.picker._search_input.set('value', 'Joe');
+            this.picker.set('results', [
+                {
+                    value: 'jschmo',
+                    title: 'Joe Schmo',
+                    description: 'joe@xxxxxxxxxxx'
+                }
+            ]);
+            var filter_div = this.picker.get('boundingBox')
+                .one('.yui3-picker-filter');
+            Assert.isNotNull(filter_div, "Filter not found");
+            Assert.areEqual('Showing All matches for "Joe".' +
+                            'Filter by:\u00A0All,\u00A0or\u00A0Product',
+                filter_div.get('textContent'));
+            this._check_filter([
+                {css: 'invalid-link', title: 'All', description: 'Display all'},
+                {css: 'js-action', title: 'Product',
+                    description: 'Display products'}
+            ]);
+        },
+
+        test_filter_search: function () {
+            // When a filter link is clicked a new search is performed and the
+            // filter is updated.
+            this.picker.render();
+            this.picker._search_input.set('value', 'Joe');
+            this.picker.set('results', [
+                {
+                    value: 'jschmo',
+                    title: 'Joe Schmo',
+                    description: 'joe@xxxxxxxxxxx'
+                }
+            ]);
+            var search_ok = false;
+            var picker = this.picker;
+            this.picker.subscribe('search', function(e) {
+                var search_string = e.details[0];
+                var filter_name = e.details[3];
+                Assert.areEqual('Joe', search_string);
+                Assert.areEqual('PROJECT', filter_name);
+                Assert.areEqual('Product', picker.get('current_filter_value'));
+                search_ok = true;
+                picker.set('results', [
+                    {
+                        value: 'jschmo',
+                        title: 'Joe Schmo',
+                        description: 'joe@xxxxxxxxxxx'
+                    }
+                ]);
+            });
+            var filter_div = this.picker.get('boundingBox')
+                .one('.yui3-picker-filter');
+            var filter_link = filter_div.one('a:nth-child(2)');
+            filter_link.simulate('click');
+            Assert.isTrue(search_ok);
+            this._check_filter([
+                {css: 'js-action', title: 'All', description: 'Display all'},
+                {css: 'invalid-link', title: 'Product',
+                    description: 'Display products'}
+            ]);
+        },
+
+        test_filter_search_no_results: function () {
+            // When a filter link is clicked and no results are returned, the
+            // filter is still visible.
+            this.picker.render();
+            this.picker._search_input.set('value', 'Joe');
+            this.picker.set('results', [
+                {
+                    value: 'jschmo',
+                    title: 'Joe Schmo',
+                    description: 'joe@xxxxxxxxxxx'
+                }
+            ]);
+            var search_ok = false;
+            var picker = this.picker;
+            this.picker.subscribe('search', function(e) {
+                var search_string = e.details[0];
+                var filter_name = e.details[3];
+                Assert.areEqual('Joe', search_string);
+                Assert.areEqual('PROJECT', filter_name);
+                Assert.areEqual('Product', picker.get('current_filter_value'));
+                search_ok = true;
+                picker.set('results', []);
+            });
+            var filter_div = this.picker.get('boundingBox')
+                .one('.yui3-picker-filter');
+            var filter_link = filter_div.one('a:nth-child(2)');
+            filter_link.simulate('click');
+            Assert.isTrue(search_ok);
+            this._check_filter([
+                {css: 'js-action', title: 'All', description: 'Display all'},
+                {css: 'invalid-link', title: 'Product',
+                    description: 'Display products'}
+            ]);
+        },
+
+        test_search_resets_filter: function () {
+            // When a new search is performed the current filter is reset.
+            this.picker.render();
+            var picker = this.picker;
+            var search_ok = false;
+            this.picker.set('current_filter_value', 'Product');
+            this.picker._search_input.set('value', 'Joe');
+            this.picker.subscribe('search', function(e) {
+                var search_string = e.details[0];
+                var filter_name = e.details[3];
+                Assert.areEqual('Joe', search_string);
+                Assert.isFalse(Y.Lang.isValue(filter_name));
+                Assert.isFalse(
+                    Y.Lang.isValue(picker.get('current_filter_value')));
+                search_ok = true;
+            });
+            this.picker._search_button.simulate('click');
+            Assert.isTrue(search_ok);
+        }
+    }));
+
+    tests.suite.add(new Y.Test.Case({
+
+        name: 'picker_text_field_plugin',
+
+        setUp: function() {
+            this.search_input = Y.Node.create(
+                    '<input id="field.initval" value="foo"/>');
+            Y.one(document.body).appendChild(this.search_input);
+            this.picker = new Y.lazr.picker.Picker({
+                associated_field_id: 'field.initval'
+            });
+        },
+
+        tearDown: function() {
+            cleanup_widget(this.picker);
+            this.search_input.remove();
+        },
+
+        test_TextFieldPickerPlugin_initial_value: function () {
+            this.picker.render();
+            this.picker.show();
+            Assert.areEqual('foo', this.picker._search_input.get('value'));
+        },
+
+        test_TextFieldPickerPlugin_selected_item_is_saved: function () {
+            this.picker.set('results', [{
+                title: 'Object 1',
+                value: 'first',
+                metadata: 'new_metadata'}]
+            );
+            this.picker.render();
+            var got_focus = false;
+            this.search_input.on('focus', function(e) {
+                got_focus = true;
+            });
+            simulate(
+                this.picker.get('boundingBox'),
+                    '.yui3-picker-results li', 'click');
+            Assert.areEqual(
+                'first', Y.one('[id="field.initval"]').get("value"));
+            Assert.areEqual(
+                'new_metadata', this.picker.get('selected_value_metadata'));
+            Assert.areEqual(
+                'first', this.picker.get('selected_value'));
+            Assert.isTrue(got_focus, "focus didn't go to the search input.");
+        }
+
+    }));
+
+}, '0.1', {
+    'requires': ['test', 'console', 'lazr.picker', 'event',
+        'node-event-simulate', 'dump']
 });

=== modified file 'lib/lp/app/javascript/picker/tests/test_picker_patcher.html'
--- lib/lp/app/javascript/picker/tests/test_picker_patcher.html	2011-08-10 08:43:17 +0000
+++ lib/lp/app/javascript/picker/tests/test_picker_patcher.html	2012-02-07 16:00:29 +0000
@@ -1,52 +1,83 @@
-<!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>Launchpad Picker</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>
-
-  <!-- Some required dependencies -->
-  <script type="text/javascript" src="../../activator/activator.js"></script>
-  <script type="text/javascript" src="../../overlay/overlay.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="../../client.js"></script>
-  <script type="text/javascript" src="../../extras/extras.js"></script>
-  <script type="text/javascript" src="../picker_patcher.js"></script>
-
-  <!-- The module under test -->
-  <script type="text/javascript" src="../picker.js"></script>
-  <script type="text/javascript" src="../person_picker.js"></script>
-
-  <!-- Testing helpers -->
-  <script type="text/javascript" src="../../testing/mockio.js"></script>
-
-  <!-- The test suite -->
-  <script type="text/javascript" src="test_picker_patcher.js"></script>
-</head>
-<body class="yui3-skin-sam">
-  <div class="yui3-widget yui3-activator yui3-activator-focused">
-      <span id="picker_id" class="yui3-activator-content yui3-activator-success">
-        <span id="pickertest">
-          <span>
-            A picker widget test
-            <button id="edit-pickertest-btn"
-                    class="lazr-btn yui3-activator-act yui3-activator-hidden">
-              Edit
-            </button>
-          </span>
-          <span class="yui3-activator-data-box">
-          </span>
-          <span class="yui3-activator-message-box yui3-activator-hidden"></span>
-        </span>
-      </span>
-  </div>
-</body>
+      <title>Indicator picker_patcher</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/activator/activator.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/overlay/overlay.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/extras/extras.js"></script>
+      <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/client.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/lazr/lazr.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/picker/picker.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/picker/person_picker.js"></script>
+      <script type="text/javascript"
+          src="../../../../../../build/js/lp/app/testing/mockio.js"></script>
+
+      <!-- The module under test. -->
+      <script type="text/javascript" src="../picker_patcher.js"></script>
+
+      <!-- Any css assert for this module. -->
+      <!-- <link rel="stylesheet" href="../assets/picker_patcher-core.css" /> -->
+
+      <!-- The test suite. -->
+      <script type="text/javascript" src="test_picker_patcher.js"></script>
+
+    </head>
+    <body class="yui3-skin-sam">
+        <ul id="suites">
+            <!-- <li>lp.large_indicator.test</li> -->
+            <li>lp.app.picker.test</li>
+        </ul>
+
+        <div class="yui3-widget yui3-activator yui3-activator-focused">
+            <span id="picker_id" class="yui3-activator-content yui3-activator-success">
+              <span id="pickertest">
+                <span>
+                  A picker widget test
+                  <button id="edit-pickertest-btn"
+                          class="lazr-btn yui3-activator-act yui3-activator-hidden">
+                    Edit
+                  </button>
+                </span>
+                <span class="yui3-activator-data-box">
+                </span>
+                <span class="yui3-activator-message-box yui3-activator-hidden"></span>
+              </span>
+            </span>
+        </div>
+    </body>
 </html>

=== modified file 'lib/lp/app/javascript/picker/tests/test_picker_patcher.js'
--- lib/lp/app/javascript/picker/tests/test_picker_patcher.js	2012-02-01 23:40:45 +0000
+++ lib/lp/app/javascript/picker/tests/test_picker_patcher.js	2012-02-07 16:00:29 +0000
@@ -1,636 +1,635 @@
-/* Copyright (c) 2011, Canonical Ltd. All rights reserved. */
-
-YUI().use('lp.testing.runner', 'test', 'console', 'node', 'lp', 'lp.client',
-        'event-focus', 'event-simulate', 'lazr.picker', 'lazr.person-picker',
-        'lp.app.picker', 'node-event-simulate', 'escape', 'event',
-        'lp.testing.mockio',
-        function(Y) {
-
-var Assert = Y.Assert;
-
-/*
- * 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.get('rendered')) {
-        var bb = widget.get('boundingBox');
-        bb.get('parentNode').removeChild(bb);
-    }
-    // Kill the widget itself.
-    widget.destroy();
-    var data_box = Y.one('#picker_id .yui3-activator-data-box');
-    var link = data_box.one('a');
-    if (link) {
-        link.get('parentNode').removeChild(link);
-    }
-}
-
-var suite = new Y.Test.Suite("Launchpad Picker Tests");
-
-/*
-* Test cases for a picker with a yesno validation callback.
-*/
-suite.add(new Y.Test.Case({
-
-    name: 'picker_yesyno_validation',
-
-    setUp: function() {
-        this.vocabulary = [
-            {"value": "fred", "title": "Fred", "css": "sprite-person",
-                "description": "fred@xxxxxxxxxxx", "api_uri": "~/fred",
-                "metadata": "person"},
-            {"value": "frieda", "title": "Frieda", "css": "sprite-team",
-                "description": "frieda@xxxxxxxxxxx", "api_uri": "~/frieda",
-                "metadata": "team"}
-        ];
-        this.picker = null;
-        this.text_input = null;
-        this.select_menu = null;
-        this.saved_picker_value = null;
-        this.validation_namespace =
-            Y.namespace('lp.app.picker.validation');
-    },
-
-    tearDown: function() {
-        if (this.select_menu !== null) {
-            Y.one('body').removeChild(this.select_menu);
-            }
-        if (this.text_input !== null) {
-            Y.one('body').removeChild(this.text_input);
-            }
-        if (this.picker !== null) {
+/* Copyright (c) 2012, Canonical Ltd. All rights reserved. */
+
+YUI.add('lp.app.picker.test', function (Y) {
+
+    var tests = Y.namespace('lp.app.picker.test');
+    var Assert = Y.Assert;
+    tests.suite = new Y.Test.Suite('picker patcher Tests');
+
+    /*
+     * 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.get('rendered')) {
+            var bb = widget.get('boundingBox');
+            bb.get('parentNode').removeChild(bb);
+        }
+        // Kill the widget itself.
+        widget.destroy();
+        var data_box = Y.one('#picker_id .yui3-activator-data-box');
+        var link = data_box.one('a');
+        if (link) {
+            link.get('parentNode').removeChild(link);
+        }
+    }
+
+    tests.suite.add(new Y.Test.Case({
+        name: 'picker_yesyno_validation',
+
+        setUp: function() {
+            this.vocabulary = [
+                {"value": "fred", "title": "Fred", "css": "sprite-person",
+                    "description": "fred@xxxxxxxxxxx", "api_uri": "~/fred",
+                    "metadata": "person"},
+                {"value": "frieda", "title": "Frieda", "css": "sprite-team",
+                    "description": "frieda@xxxxxxxxxxx", "api_uri": "~/frieda",
+                    "metadata": "team"}
+            ];
+            this.picker = null;
+            this.text_input = null;
+            this.select_menu = null;
+            this.saved_picker_value = null;
+            this.validation_namespace =
+                Y.namespace('lp.app.picker.validation');
+        },
+
+        tearDown: function() {
+            if (this.select_menu !== null) {
+                Y.one('body').removeChild(this.select_menu);
+                }
+            if (this.text_input !== null) {
+                Y.one('body').removeChild(this.text_input);
+                }
+            if (this.picker !== null) {
+                cleanup_widget(this.picker);
+                }
+            this.validation_namespace.show_picker_id = undefined;
+        },
+
+        test_library_exists: function () {
+            Y.Assert.isObject(Y.lp.app.picker,
+                "We should be able to locate the lp.app.picker module");
+        },
+
+        create_picker: function(validate_callback, extra_config) {
+            var config = {
+                    "step_title": "Choose someone",
+                    "header": "Pick Someone",
+                    "null_display_value": "No one",
+                    "show_widget_id": "show_picker_id",
+                    "validate_callback": validate_callback
+            };
+            if (extra_config !== undefined) {
+               config = Y.merge(extra_config, config);
+            }
+            this.picker = Y.lp.app.picker.addPickerPatcher(
+                this.vocabulary,
+                "foo/bar",
+                "test_link",
+                "picker_id",
+                config);
+            var self = this;
+            this.picker.subscribe('save', function(e) {
+                self.saved_picker_value =
+                     e.details[Y.lazr.picker.Picker.SAVE_RESULT].api_uri;
+            });
+        },
+
+        create_picker_direct: function(associated_field) {
+            this.picker = Y.lp.app.picker.create(
+                this.vocabulary,
+                undefined,
+                associated_field);
+            var self = this;
+            this.picker.subscribe('save', function(e) {
+                 self.saved_picker_value =
+                     e.details[Y.lazr.picker.Picker.SAVE_RESULT].api_uri;
+            });
+        },
+
+        test_picker_can_be_instantiated: function() {
+            // The picker can be instantiated.
+            this.create_picker();
+            Assert.isInstanceOf(
+                Y.lazr.picker.Picker, this.picker,
+                "Picker failed to be instantiated");
+        },
+
+        // A validation callback stub. Instead of going to the server to see if
+        // a picker value requires confirmation, we compare it to a known value.
+        yesno_validate_callback: function(expected_value) {
+            return function(picker, value, save_fn, cancel_fn) {
+                Assert.areEqual(
+                        expected_value, value.api_uri, "unexpected picker value");
+                if (value === null) {
+                    return true;
+                }
+                var requires_confirmation = value.api_uri !== "~/fred";
+                if (requires_confirmation) {
+                    var yesno_content = "<p>Confirm selection</p>";
+                    Y.lp.app.picker.yesno_save_confirmation(
+                            picker, yesno_content, "Yes", "No",
+                            save_fn, cancel_fn);
+                } else {
+                    save_fn();
+                }
+            };
+        },
+
+        test_no_confirmation_required: function() {
+            // The picker saves the selected value if no confirmation
+            // is required.
+            this.create_picker(this.yesno_validate_callback("~/fred"));
+            this.picker.set('results', this.vocabulary);
+            this.picker.render();
+
+            simulate(
+                this.picker.get('boundingBox').one('.yui3-picker-results'),
+                    'li:nth-child(1)', 'click');
+            Assert.areEqual('~/fred', this.saved_picker_value);
+        },
+
+        test_TextFieldPickerPlugin_selected_item_is_saved: function () {
+            // The picker saves the selected value to its associated
+            // textfield if one is defined.
+            this.text_input = Y.Node.create(
+                    '<input id="field.testfield" value="foo" />');
+            Y.one(document.body).appendChild(this.text_input);
+            this.create_picker_direct('field.testfield');
+            this.picker.set('results', this.vocabulary);
+            this.picker.render();
+            var got_focus = false;
+            this.text_input.on('focus', function(e) {
+                got_focus = true;
+            });
+            simulate(
+                this.picker.get('boundingBox').one('.yui3-picker-results'),
+                    'li:nth-child(2)', 'click');
+            Assert.areEqual(
+                'frieda', Y.one('[id="field.testfield"]').get("value"));
+            Assert.areEqual('team', this.picker.get('selected_value_metadata'));
+            Assert.isTrue(got_focus, "focus didn't go to the search input.");
+        },
+
+        test_navigation_renders_after_results: function () {
+            // We modify the base picker to show the batch navigation below the
+            // picker results.
+            this.create_picker();
+            this.picker.set('results', this.vocabulary);
+            var results_box = this.picker._results_box;
+            var batch_box = this.picker._batches_box;
+            Assert.areEqual(results_box.next(), batch_box);
+        },
+
+        test_extra_no_results_message: function () {
+            // If "extra_no_results_message" is defined, it is rendered in the
+            // footer slot when there are no results.
+            this.create_picker(
+                undefined, {'extra_no_results_message': 'message'});
+            this.picker.set('results', []);
+            var footer_slot = this.picker.get('footer_slot');
+            Assert.areEqual('message', footer_slot.get('text'));
+        },
+
+        test_footer_node_preserved_without_extra_no_results_message: function () {
+            // If "extra_no_results_message" is not defined, the footer slot node
+            // should be preserved.
+            this.create_picker();
+            var footer_node = Y.Node.create("<span>foobar</span>");
+            this.picker.set('footer_slot', footer_node);
+            this.picker.set('results', []);
+            var footer_slot = this.picker.get('footer_slot');
+            Assert.areEqual('foobar', footer_slot.get('text'));
+        },
+
+        test_confirmation_yes: function() {
+            // The picker saves the selected value if the user answers
+            // "Yes" to a confirmation request.
+            // The validator is specified using the picker's config object.
+            this.create_picker(this.yesno_validate_callback("~/frieda"));
+            this.picker.set('results', this.vocabulary);
+            this.picker.render();
+
+            simulate(
+                this.picker.get('boundingBox').one('.yui3-picker-results'),
+                    'li:nth-child(2)', 'click');
+            var yesno = this.picker.get('contentBox').one('.extra-form-buttons');
+
+            simulate(
+                    yesno, 'button:nth-child(1)', 'click');
+            Assert.areEqual('~/frieda', this.saved_picker_value);
+        },
+
+        test_confirmation_yes_namespace_validator: function() {
+            // The picker saves the selected value if the user answers
+            // "Yes" to a confirmation request.
+            // The validator is specified using the validation namespace.
+            this.validation_namespace.show_picker_id =
+                [this.yesno_validate_callback("~/frieda")];
+
+            this.create_picker();
+            this.picker.set('results', this.vocabulary);
+            this.picker.render();
+
+            simulate(
+                this.picker.get('boundingBox').one('.yui3-picker-results'),
+                    'li:nth-child(2)', 'click');
+            var yesno = this.picker.get('contentBox').one('.extra-form-buttons');
+
+            simulate(
+                    yesno, 'button:nth-child(1)', 'click');
+            Assert.areEqual('~/frieda', this.saved_picker_value);
+        },
+
+        test_confirmation_no: function() {
+            // The picker doesn't save the selected value if the user answers
+            // "No" to a confirmation request.
+            this.create_picker(this.yesno_validate_callback("~/frieda"));
+            this.picker.set('results', this.vocabulary);
+            this.picker.render();
+
+            simulate(
+                this.picker.get('boundingBox').one('.yui3-picker-results'),
+                    'li:nth-child(2)', 'click');
+            var yesno = this.picker.get('contentBox').one('.extra-form-buttons');
+            simulate(
+                yesno, 'button:nth-child(2)', 'click');
+            Assert.isNull(this.saved_picker_value);
+        },
+
+        // Helper function for multiple (2) validators.
+        choose_all_yes: function() {
+            simulate(
+                this.picker.get('boundingBox').one('.yui3-picker-results'),
+                    'li:nth-child(2)', 'click');
+            var yesno = this.picker.get('contentBox').one('.extra-form-buttons');
+            // Click the Yes button.
+            simulate(yesno, 'button:nth-child(1)', 'click');
+            yesno = this.picker.get('contentBox').one('.extra-form-buttons');
+            // Click the Yes button.
+            simulate(yesno, 'button:nth-child(1)', 'click');
+            Assert.areEqual('~/frieda', this.saved_picker_value);
+        },
+
+        test_multi_validators_config_and_ns_all_yes: function() {
+            // Set up a picker with 2 validators, one via the config and one via
+            // the namespace.
+
+            // The picker saves the selected value if the user answers
+            // "Yes" to all confirmation request.
+            this.validation_namespace.show_picker_id =
+                this.yesno_validate_callback("~/frieda");
+            this.create_picker(this.yesno_validate_callback("~/frieda"));
+            this.picker.set('results', this.vocabulary);
+            this.picker.render();
+            this.choose_all_yes();
+        },
+
+        test_multi_validators_via_ns_all_yes: function() {
+            // Set up a picker with 2 validators, both configured via the
+            // namespace.
+
+            // The picker saves the selected value if the user answers
+            // "Yes" to all confirmation request.
+            this.validation_namespace.show_picker_id = [
+                this.yesno_validate_callback("~/frieda"),
+                this.yesno_validate_callback("~/frieda")
+                ];
+            this.create_picker();
+            this.picker.set('results', this.vocabulary);
+            this.picker.render();
+            this.choose_all_yes();
+        },
+
+        test_multi_validators_via_config_all_yes: function() {
+            // Set up a picker with 2 validators, both configured via the picker
+            // config.
+
+            // The picker saves the selected value if the user answers
+            // "Yes" to all confirmation request.
+            var validators = [
+                this.yesno_validate_callback("~/frieda"),
+                this.yesno_validate_callback("~/frieda")
+                ];
+            this.create_picker(validators);
+            this.picker.set('results', this.vocabulary);
+            this.picker.render();
+            this.choose_all_yes();
+        },
+
+        test_chained_validators_one_no: function() {
+            // The picker doesn't save the selected value if the user answers
+            // "No" to any one of the confirmation requests.
+            this.validation_namespace.show_picker_id = [
+                this.yesno_validate_callback("~/frieda"),
+                this.yesno_validate_callback("~/frieda")];
+
+            this.create_picker();
+            this.picker.set('results', this.vocabulary);
+            this.picker.render();
+
+            simulate(
+                this.picker.get('boundingBox').one('.yui3-picker-results'),
+                    'li:nth-child(2)', 'click');
+            var yesno = this.picker.get('contentBox').one('.extra-form-buttons');
+            // Click the Yes button.
+            simulate(yesno, 'button:nth-child(1)', 'click');
+            yesno = this.picker.get('contentBox').one('.extra-form-buttons');
+            // Click the No button.
+            simulate(yesno, 'button:nth-child(2)', 'click');
+            Assert.isNull(this.saved_picker_value);
+        },
+
+        test_connect_select_menu: function() {
+            // connect_select_menu() connects the select menu's onchange event to
+            // copy the selected value to the text input field.
+            this.text_input = Y.Node.create(
+                    '<input id="field.testfield" value="foo" />');
+            var node = Y.one(document.body).appendChild(this.text_input);
+            this.select_menu = Y.Node.create(
+                '<select id="field.testfield-suggestions"> ' +
+                '    <option value="">Did you mean...</option>' +
+                '    <option value="fnord">Fnord Snarf (fnord)</option>' +
+                '</select>');
+            Y.one('body').appendChild(this.select_menu);
+            var select_menu = Y.DOM.byId('field.testfield-suggestions');
+            var text_input = Y.DOM.byId('field.testfield');
+            Y.lp.app.picker.connect_select_menu(select_menu, text_input);
+            select_menu.selectedIndex = 1;
+            Y.Event.simulate(select_menu, 'change');
+            Assert.areEqual(
+                'fnord', text_input.value,
+                "Select menu's onchange handler failed.");
+        },
+
+        test_privacy_warning: function () {
+            //tests that the specific public_private warning version of yesno
+            //works.
+            var overridden_privacy_callback = function (
+                picker, value, save_fn, cancel_fn) {
+                window.LP = { cache: {} };
+                LP.cache.context = { private: false };
+                Y.lp.client.Launchpad = function() {};
+                Y.lp.client.Launchpad.prototype.get = function(uri, config) {
+                    var person = {
+                        get: function (key) {
+                            return true;
+                        }
+                    };
+                    config.on.success(person);
+                };
+
+                Y.lp.app.picker.public_private_warning(
+                    picker, value, save_fn, cancel_fn);
+            };
+
+            this.create_picker(overridden_privacy_callback);
+            this.picker.set('results', this.vocabulary);
+            this.picker.render();
+
+            simulate(
+                this.picker.get('boundingBox').one('.yui3-picker-results'),
+                    'li:nth-child(2)', 'click');
+            // expected_text is a little weird since breaks between p tags and
+            // buttons are lost.
+            var expected_text = 'This action will reveal this team\'s name to ' +
+                'the public.ContinueChoose Again';
+            var text = Y.one(".validation-node").get('text');
+            Assert.areEqual(expected_text, text);
+        }
+
+    }));
+
+    /*
+     * Test cases for a picker with a large vocabulary.
+     */
+    tests.suite.add(new Y.Test.Case({
+
+        name: 'picker_large_vocabulary',
+
+        setUp: function() {
+            var i;
+            this.vocabulary = new Array(121);
+            for (i = 0; i < 121; i++) {
+                this.vocabulary[i] = {
+                    "value": "value-" + i,
+                    "title": "title-" + i,
+                    "css": "sprite-person",
+                    "description": "description-" + i,
+                    "api_uri": "~/fred-" + i};
+            }
+        },
+
+        tearDown: function() {
             cleanup_widget(this.picker);
-            }
-        this.validation_namespace.show_picker_id = undefined;
-    },
+        },
 
-    create_picker: function(validate_callback, extra_config) {
-        var config = {
+        create_picker: function(show_search_box, extra_config) {
+            var config = {
                 "step_title": "Choose someone",
                 "header": "Pick Someone",
-                "null_display_value": "No one",
-                "show_widget_id": "show_picker_id",
-                "validate_callback": validate_callback
-        };
-        if (extra_config !== undefined) {
-           config = Y.merge(extra_config, config);
-        }
-        this.picker = Y.lp.app.picker.addPickerPatcher(
-            this.vocabulary,
-            "foo/bar",
-            "test_link",
-            "picker_id",
-            config);
-        var self = this;
-        this.picker.subscribe('save', function(e) {
-            self.saved_picker_value =
-                 e.details[Y.lazr.picker.Picker.SAVE_RESULT].api_uri;
-        });
-    },
-
-    create_picker_direct: function(associated_field) {
-        this.picker = Y.lp.app.picker.create(
-            this.vocabulary,
-            undefined,
-            associated_field);
-        var self = this;
-        this.picker.subscribe('save', function(e) {
-             self.saved_picker_value =
-                 e.details[Y.lazr.picker.Picker.SAVE_RESULT].api_uri;
-        });
-    },
-
-    test_picker_can_be_instantiated: function() {
-        // The picker can be instantiated.
-        this.create_picker();
-        Assert.isInstanceOf(
-            Y.lazr.picker.Picker, this.picker,
-            "Picker failed to be instantiated");
-    },
-
-    // A validation callback stub. Instead of going to the server to see if
-    // a picker value requires confirmation, we compare it to a known value.
-    yesno_validate_callback: function(expected_value) {
-        return function(picker, value, save_fn, cancel_fn) {
-            Assert.areEqual(
-                    expected_value, value.api_uri, "unexpected picker value");
-            if (value === null) {
-                return true;
-            }
-            var requires_confirmation = value.api_uri !== "~/fred";
-            if (requires_confirmation) {
-                var yesno_content = "<p>Confirm selection</p>";
-                Y.lp.app.picker.yesno_save_confirmation(
-                        picker, yesno_content, "Yes", "No",
-                        save_fn, cancel_fn);
-            } else {
-                save_fn();
-            }
-        };
-    },
-
-    test_no_confirmation_required: function() {
-        // The picker saves the selected value if no confirmation
-        // is required.
-        this.create_picker(this.yesno_validate_callback("~/fred"));
-        this.picker.set('results', this.vocabulary);
-        this.picker.render();
-
-        simulate(
-            this.picker.get('boundingBox').one('.yui3-picker-results'),
-                'li:nth-child(1)', 'click');
-        Assert.areEqual('~/fred', this.saved_picker_value);
-    },
-
-    test_TextFieldPickerPlugin_selected_item_is_saved: function () {
-        // The picker saves the selected value to its associated
-        // textfield if one is defined.
-        this.text_input = Y.Node.create(
-                '<input id="field.testfield" value="foo" />');
-        Y.one(document.body).appendChild(this.text_input);
-        this.create_picker_direct('field.testfield');
-        this.picker.set('results', this.vocabulary);
-        this.picker.render();
-        var got_focus = false;
-        this.text_input.on('focus', function(e) {
-            got_focus = true;
-        });
-        simulate(
-            this.picker.get('boundingBox').one('.yui3-picker-results'),
-                'li:nth-child(2)', 'click');
-        Assert.areEqual(
-            'frieda', Y.one('[id="field.testfield"]').get("value"));
-        Assert.areEqual('team', this.picker.get('selected_value_metadata'));
-        Assert.isTrue(got_focus, "focus didn't go to the search input.");
-    },
-
-    test_navigation_renders_after_results: function () {
-        // We modify the base picker to show the batch navigation below the
-        // picker results.
-        this.create_picker();
-        this.picker.set('results', this.vocabulary);
-        var results_box = this.picker._results_box;
-        var batch_box = this.picker._batches_box;
-        Assert.areEqual(results_box.next(), batch_box);
-    },
-
-    test_extra_no_results_message: function () {
-        // If "extra_no_results_message" is defined, it is rendered in the
-        // footer slot when there are no results.
-        this.create_picker(
-            undefined, {'extra_no_results_message': 'message'});
-        this.picker.set('results', []);
-        var footer_slot = this.picker.get('footer_slot');
-        Assert.areEqual('message', footer_slot.get('text'));
-    },
-
-    test_footer_node_preserved_without_extra_no_results_message: function () {
-        // If "extra_no_results_message" is not defined, the footer slot node
-        // should be preserved.
-        this.create_picker();
-        var footer_node = Y.Node.create("<span>foobar</span>");
-        this.picker.set('footer_slot', footer_node);
-        this.picker.set('results', []);
-        var footer_slot = this.picker.get('footer_slot');
-        Assert.areEqual('foobar', footer_slot.get('text'));
-    },
-
-    test_confirmation_yes: function() {
-        // The picker saves the selected value if the user answers
-        // "Yes" to a confirmation request.
-        // The validator is specified using the picker's config object.
-        this.create_picker(this.yesno_validate_callback("~/frieda"));
-        this.picker.set('results', this.vocabulary);
-        this.picker.render();
-
-        simulate(
-            this.picker.get('boundingBox').one('.yui3-picker-results'),
-                'li:nth-child(2)', 'click');
-        var yesno = this.picker.get('contentBox').one('.extra-form-buttons');
-
-        simulate(
-                yesno, 'button:nth-child(1)', 'click');
-        Assert.areEqual('~/frieda', this.saved_picker_value);
-    },
-
-    test_confirmation_yes_namespace_validator: function() {
-        // The picker saves the selected value if the user answers
-        // "Yes" to a confirmation request.
-        // The validator is specified using the validation namespace.
-        this.validation_namespace.show_picker_id =
-            [this.yesno_validate_callback("~/frieda")];
-
-        this.create_picker();
-        this.picker.set('results', this.vocabulary);
-        this.picker.render();
-
-        simulate(
-            this.picker.get('boundingBox').one('.yui3-picker-results'),
-                'li:nth-child(2)', 'click');
-        var yesno = this.picker.get('contentBox').one('.extra-form-buttons');
-
-        simulate(
-                yesno, 'button:nth-child(1)', 'click');
-        Assert.areEqual('~/frieda', this.saved_picker_value);
-    },
-
-    test_confirmation_no: function() {
-        // The picker doesn't save the selected value if the user answers
-        // "No" to a confirmation request.
-        this.create_picker(this.yesno_validate_callback("~/frieda"));
-        this.picker.set('results', this.vocabulary);
-        this.picker.render();
-
-        simulate(
-            this.picker.get('boundingBox').one('.yui3-picker-results'),
-                'li:nth-child(2)', 'click');
-        var yesno = this.picker.get('contentBox').one('.extra-form-buttons');
-        simulate(
-            yesno, 'button:nth-child(2)', 'click');
-        Assert.isNull(this.saved_picker_value);
-    },
-
-    // Helper function for multiple (2) validators.
-    choose_all_yes: function() {
-        simulate(
-            this.picker.get('boundingBox').one('.yui3-picker-results'),
-                'li:nth-child(2)', 'click');
-        var yesno = this.picker.get('contentBox').one('.extra-form-buttons');
-        // Click the Yes button.
-        simulate(yesno, 'button:nth-child(1)', 'click');
-        yesno = this.picker.get('contentBox').one('.extra-form-buttons');
-        // Click the Yes button.
-        simulate(yesno, 'button:nth-child(1)', 'click');
-        Assert.areEqual('~/frieda', this.saved_picker_value);
-    },
-
-    test_multi_validators_config_and_ns_all_yes: function() {
-        // Set up a picker with 2 validators, one via the config and one via
-        // the namespace.
-
-        // The picker saves the selected value if the user answers
-        // "Yes" to all confirmation request.
-        this.validation_namespace.show_picker_id =
-            this.yesno_validate_callback("~/frieda");
-        this.create_picker(this.yesno_validate_callback("~/frieda"));
-        this.picker.set('results', this.vocabulary);
-        this.picker.render();
-        this.choose_all_yes();
-    },
-
-    test_multi_validators_via_ns_all_yes: function() {
-        // Set up a picker with 2 validators, both configured via the
-        // namespace.
-
-        // The picker saves the selected value if the user answers
-        // "Yes" to all confirmation request.
-        this.validation_namespace.show_picker_id = [
-            this.yesno_validate_callback("~/frieda"),
-            this.yesno_validate_callback("~/frieda")
-            ];
-        this.create_picker();
-        this.picker.set('results', this.vocabulary);
-        this.picker.render();
-        this.choose_all_yes();
-    },
-
-    test_multi_validators_via_config_all_yes: function() {
-        // Set up a picker with 2 validators, both configured via the picker
-        // config.
-
-        // The picker saves the selected value if the user answers
-        // "Yes" to all confirmation request.
-        var validators = [
-            this.yesno_validate_callback("~/frieda"),
-            this.yesno_validate_callback("~/frieda")
-            ];
-        this.create_picker(validators);
-        this.picker.set('results', this.vocabulary);
-        this.picker.render();
-        this.choose_all_yes();
-    },
-
-    test_chained_validators_one_no: function() {
-        // The picker doesn't save the selected value if the user answers
-        // "No" to any one of the confirmation requests.
-        this.validation_namespace.show_picker_id = [
-            this.yesno_validate_callback("~/frieda"),
-            this.yesno_validate_callback("~/frieda")];
-
-        this.create_picker();
-        this.picker.set('results', this.vocabulary);
-        this.picker.render();
-
-        simulate(
-            this.picker.get('boundingBox').one('.yui3-picker-results'),
-                'li:nth-child(2)', 'click');
-        var yesno = this.picker.get('contentBox').one('.extra-form-buttons');
-        // Click the Yes button.
-        simulate(yesno, 'button:nth-child(1)', 'click');
-        yesno = this.picker.get('contentBox').one('.extra-form-buttons');
-        // Click the No button.
-        simulate(yesno, 'button:nth-child(2)', 'click');
-        Assert.isNull(this.saved_picker_value);
-    },
-
-    test_connect_select_menu: function() {
-        // connect_select_menu() connects the select menu's onchange event to
-        // copy the selected value to the text input field.
-        this.text_input = Y.Node.create(
-                '<input id="field.testfield" value="foo" />');
-        var node = Y.one(document.body).appendChild(this.text_input);
-        this.select_menu = Y.Node.create(
-            '<select id="field.testfield-suggestions"> ' +
-            '    <option value="">Did you mean...</option>' +
-            '    <option value="fnord">Fnord Snarf (fnord)</option>' +
-            '</select>');
-        Y.one('body').appendChild(this.select_menu);
-        var select_menu = Y.DOM.byId('field.testfield-suggestions');
-        var text_input = Y.DOM.byId('field.testfield');
-        Y.lp.app.picker.connect_select_menu(select_menu, text_input);
-        select_menu.selectedIndex = 1;
-        Y.Event.simulate(select_menu, 'change');
-        Assert.areEqual(
-            'fnord', text_input.value,
-            "Select menu's onchange handler failed.");
-    },
-
-    test_privacy_warning: function () {
-        //tests that the specific public_private warning version of yesno
-        //works.
-        var overridden_privacy_callback = function (
-            picker, value, save_fn, cancel_fn) {
-            window.LP = { cache: {} };
-            LP.cache.context = { private: false };
-            Y.lp.client.Launchpad = function() {};
-            Y.lp.client.Launchpad.prototype.get = function(uri, config) {
-                var person = {
-                    get: function (key) {
-                        return true;
+                "validate_callback": null
+                };
+            if (show_search_box !== undefined) {
+                config.show_search_box = show_search_box;
+            }
+            if (extra_config !== undefined) {
+               config = Y.merge(extra_config, config);
+            }
+            this.picker = Y.lp.app.picker.addPickerPatcher(
+                    this.vocabulary,
+                    "foo/bar",
+                    "test_link",
+                    "picker_id",
+                    config);
+        },
+
+        test_filter_options_initialisation: function() {
+            // Filter options are correctly used to set up the picker.
+            this.picker = Y.lp.app.picker.create(
+                this.vocabulary, undefined, undefined, ['a', 'b', 'c']);
+            Y.ArrayAssert.itemsAreEqual(
+                ['a', 'b', 'c'], this.picker.get('filter_options'));
+        },
+
+        test_picker_displays_empty_list: function() {
+            // With too many results, the results will be empty.
+            this.create_picker(true);
+            this.picker.render();
+            this.picker.set('min_search_chars', 0);
+            this.picker.fire('search', '');
+            var result_text = this.picker.get('contentBox')
+                .one('.yui3-picker-results').get('text');
+            Assert.areEqual('', result_text);
+        },
+
+        test_picker_displays_warning: function() {
+            // With a search box the picker will refuse to display more than
+            // 120 values.
+            this.create_picker(true);
+            this.picker.set('min_search_chars', 0);
+            this.picker.fire('search', '');
+            Assert.areEqual(
+                'Too many matches. Please try to narrow your search.',
+                this.picker.get('error'));
+        },
+
+        test_picker_displays_warning_by_default: function() {
+            // If show_search_box is not supplied in config, it defaults to true.
+            // Thus the picker will refuse to display more than 120 values.
+            this.create_picker();
+            this.picker.set('min_search_chars', 0);
+            this.picker.fire('search', '');
+            Assert.areEqual(
+                'Too many matches. Please try to narrow your search.',
+                this.picker.get('error'));
+        },
+
+        test_picker_no_warning: function() {
+            // Without a search box the picker will also display more than
+            // 120 values.
+            this.create_picker(false);
+            this.picker.set('min_search_chars', 0);
+            this.picker.fire('search', '');
+            Assert.areEqual(null, this.picker.get('error'));
+        },
+
+        test_vocab_filter_config: function () {
+            // The vocab filter config is correctly used to create the picker.
+            var filters = [{name: 'ALL', title: 'All', description: 'All'}];
+            this.create_picker(undefined,  {'vocabulary_filters': filters});
+            var filter_options = this.picker.get('filter_options');
+            Assert.areEqual(filters, filter_options);
+        }
+    }));
+
+    tests.suite.add(new Y.Test.Case({
+
+        name: 'picker_error_handling',
+
+        setUp: function() {
+            this.create_picker();
+            this.picker.fire('search', 'foo');
+        },
+
+        tearDown: function() {
+            cleanup_widget(this.picker);
+        },
+
+        create_picker: function() {
+            this.mock_io = new Y.lp.testing.mockio.MockIo();
+            this.picker = Y.lp.app.picker.addPickerPatcher(
+                "Foo",
+                "foo/bar",
+                "test_link",
+                "picker_id",
+                {yio: this.mock_io});
+        },
+
+        get_oops_headers: function(oops) {
+            var headers = {};
+            headers['X-Lazr-OopsId'] = oops;
+            return headers;
+        },
+
+        test_oops: function() {
+            // A 500 (ISE) with an OOPS ID informs the user that we've
+            // logged it, and gives them the OOPS ID.
+            this.mock_io.failure(
+                {responseHeaders: this.get_oops_headers('OOPS')});
+            Assert.areEqual(
+                "Sorry, something went wrong with your search. We've recorded " +
+                "what happened, and we'll fix it as soon as possible. " +
+                "(Error ID: OOPS)",
+                this.picker.get('error'));
+        },
+
+        test_timeout: function() {
+            // A 503 (timeout) or 502/504 (proxy error) informs the user
+            // that they should retry, and gives them the OOPS ID.
+            this.mock_io.failure(
+                {status: 503, responseHeaders: this.get_oops_headers('OOPS')});
+            Assert.areEqual(
+                "Sorry, something went wrong with your search. Trying again " +
+                "in a couple of minutes might work. (Error ID: OOPS)",
+                this.picker.get('error'));
+        },
+
+        test_other_error: function() {
+            // Any other type of error just displays a generic failure
+            // message, with no OOPS ID.
+            this.mock_io.failure({status: 400});
+            Assert.areEqual(
+                "Sorry, something went wrong with your search.",
+                this.picker.get('error'));
+        }
+    }));
+
+    tests.suite.add(new Y.Test.Case({
+
+        name: 'picker_automated_search',
+
+        create_picker: function(yio) {
+            var config = {yio: yio};
+            return Y.lp.app.picker.addPickerPatcher(
+                "Foo",
+                "foo/bar",
+                "test_link",
+                "picker_id",
+                config);
+        },
+
+        make_response: function(status, oops, responseText) {
+            if (oops === undefined) {
+                oops = null;
+            }
+            return {
+                status: status,
+                responseText: responseText,
+                getResponseHeader: function(header) {
+                    if (header === 'X-Lazr-OopsId') {
+                        return oops;
                     }
-                };
-                config.on.success(person);
-            };
-
-            Y.lp.app.picker.public_private_warning(
-                picker, value, save_fn, cancel_fn);
-        };
-
-        this.create_picker(overridden_privacy_callback);
-        this.picker.set('results', this.vocabulary);
-        this.picker.render();
-
-        simulate(
-            this.picker.get('boundingBox').one('.yui3-picker-results'),
-                'li:nth-child(2)', 'click');
-        // expected_text is a little weird since breaks between p tags and
-        // buttons are lost.
-        var expected_text = 'This action will reveal this team\'s name to ' +
-            'the public.ContinueChoose Again';
-        var text = Y.one(".validation-node").get('text');
-        Assert.areEqual(expected_text, text);
-    }
-}));
-
-/*
- * Test cases for a picker with a large vocabulary.
- */
-suite.add(new Y.Test.Case({
-
-    name: 'picker_large_vocabulary',
-
-    setUp: function() {
-        var i;
-        this.vocabulary = new Array(121);
-        for (i = 0; i < 121; i++) {
-            this.vocabulary[i] = {
-                "value": "value-" + i,
-                "title": "title-" + i,
-                "css": "sprite-person",
-                "description": "description-" + i,
-                "api_uri": "~/fred-" + i};
-        }
-    },
-
-    tearDown: function() {
-        cleanup_widget(this.picker);
-    },
-
-    create_picker: function(show_search_box, extra_config) {
-        var config = {
-            "step_title": "Choose someone",
-            "header": "Pick Someone",
-            "validate_callback": null
-            };
-        if (show_search_box !== undefined) {
-            config.show_search_box = show_search_box;
-        }
-        if (extra_config !== undefined) {
-           config = Y.merge(extra_config, config);
-        }
-        this.picker = Y.lp.app.picker.addPickerPatcher(
-                this.vocabulary,
-                "foo/bar",
-                "test_link",
-                "picker_id",
-                config);
-    },
-
-    test_filter_options_initialisation: function() {
-        // Filter options are correctly used to set up the picker.
-        this.picker = Y.lp.app.picker.create(
-            this.vocabulary, undefined, undefined, ['a', 'b', 'c']);
-        Y.ArrayAssert.itemsAreEqual(
-            ['a', 'b', 'c'], this.picker.get('filter_options'));
-    },
-
-    test_picker_displays_empty_list: function() {
-        // With too many results, the results will be empty.
-        this.create_picker(true);
-        this.picker.render();
-        this.picker.set('min_search_chars', 0);
-        this.picker.fire('search', '');
-        var result_text = this.picker.get('contentBox')
-            .one('.yui3-picker-results').get('text');
-        Assert.areEqual('', result_text);
-    },
-
-    test_picker_displays_warning: function() {
-        // With a search box the picker will refuse to display more than
-        // 120 values.
-        this.create_picker(true);
-        this.picker.set('min_search_chars', 0);
-        this.picker.fire('search', '');
-        Assert.areEqual(
-            'Too many matches. Please try to narrow your search.',
-            this.picker.get('error'));
-    },
-
-    test_picker_displays_warning_by_default: function() {
-        // If show_search_box is not supplied in config, it defaults to true.
-        // Thus the picker will refuse to display more than 120 values.
-        this.create_picker();
-        this.picker.set('min_search_chars', 0);
-        this.picker.fire('search', '');
-        Assert.areEqual(
-            'Too many matches. Please try to narrow your search.',
-            this.picker.get('error'));
-    },
-
-    test_picker_no_warning: function() {
-        // Without a search box the picker will also display more than
-        // 120 values.
-        this.create_picker(false);
-        this.picker.set('min_search_chars', 0);
-        this.picker.fire('search', '');
-        Assert.areEqual(null, this.picker.get('error'));
-    },
-
-    test_vocab_filter_config: function () {
-        // The vocab filter config is correctly used to create the picker.
-        var filters = [{name: 'ALL', title: 'All', description: 'All'}];
-        this.create_picker(undefined,  {'vocabulary_filters': filters});
-        var filter_options = this.picker.get('filter_options');
-        Assert.areEqual(filters, filter_options);
-    }
-}));
-
-suite.add(new Y.Test.Case({
-
-    name: 'picker_error_handling',
-
-    setUp: function() {
-        this.create_picker();
-        this.picker.fire('search', 'foo');
-    },
-
-    tearDown: function() {
-        cleanup_widget(this.picker);
-    },
-
-    create_picker: function() {
-        this.mock_io = new Y.lp.testing.mockio.MockIo();
-        this.picker = Y.lp.app.picker.addPickerPatcher(
-            "Foo",
-            "foo/bar",
-            "test_link",
-            "picker_id",
-            {yio: this.mock_io});
-    },
-
-    get_oops_headers: function(oops) {
-        var headers = {};
-        headers['X-Lazr-OopsId'] = oops;
-        return headers;
-    },
-
-    test_oops: function() {
-        // A 500 (ISE) with an OOPS ID informs the user that we've
-        // logged it, and gives them the OOPS ID.
-        this.mock_io.failure(
-            {responseHeaders: this.get_oops_headers('OOPS')});
-        Assert.areEqual(
-            "Sorry, something went wrong with your search. We've recorded " +
-            "what happened, and we'll fix it as soon as possible. " +
-            "(Error ID: OOPS)",
-            this.picker.get('error'));
-    },
-
-    test_timeout: function() {
-        // A 503 (timeout) or 502/504 (proxy error) informs the user
-        // that they should retry, and gives them the OOPS ID.
-        this.mock_io.failure(
-            {status: 503, responseHeaders: this.get_oops_headers('OOPS')});
-        Assert.areEqual(
-            "Sorry, something went wrong with your search. Trying again " +
-            "in a couple of minutes might work. (Error ID: OOPS)",
-            this.picker.get('error'));
-    },
-
-    test_other_error: function() {
-        // Any other type of error just displays a generic failure
-        // message, with no OOPS ID.
-        this.mock_io.failure({status: 400});
-        Assert.areEqual(
-            "Sorry, something went wrong with your search.",
-            this.picker.get('error'));
-    }
-}));
-
-suite.add(new Y.Test.Case({
-
-    name: 'picker_automated_search',
-
-    create_picker: function(yio) {
-        var config = {yio: yio};
-        return Y.lp.app.picker.addPickerPatcher(
-            "Foo",
-            "foo/bar",
-            "test_link",
-            "picker_id",
-            config);
-    },
-
-    make_response: function(status, oops, responseText) {
-        if (oops === undefined) {
-            oops = null;
-        }
-        return {
-            status: status,
-            responseText: responseText,
-            getResponseHeader: function(header) {
-                if (header === 'X-Lazr-OopsId') {
-                    return oops;
                 }
-            }
-        };
-    },
-
-    test_automated_search_results_ignored_if_user_has_searched: function() {
-        // If an automated search (like loading branch suggestions) returns
-        // results and the user has submitted a search, then the results of
-        // the automated search are ignored so as not to confuse the user.
-        var mock_io = new Y.lp.testing.mockio.MockIo();
-        var picker = this.create_picker(mock_io);
-        // First an automated search is run.
-        picker.fire('search', 'guess', undefined, true);
-        // Then the user initiates their own search.
-        picker.fire('search', 'test');
-        // Two requests have been sent out.
-        Y.Assert.areEqual(2, mock_io.requests.length);
-        // Respond to the automated request.
-        mock_io.requests[0].respond({responseText: '{"entries": 1}'});
-        // ... the results are ignored.
-        Assert.areNotEqual(1, picker.get('results'));
-        // Respond to the user request.
-        mock_io.requests[1].respond({responseText: '{"entries": 2}'});
-        Assert.areEqual(2, picker.get('results'));
-        cleanup_widget(picker);
-    },
-
-    test_automated_search_error_ignored_if_user_has_searched: function() {
-        // If an automated search (like loading branch suggestions) returns an
-        // error and the user has submitted a search, then the error from the
-        // automated search is ignored so as not to confuse the user.
-        var mock_io = new Y.lp.testing.mockio.MockIo();
-        var picker = this.create_picker(mock_io);
-        picker.fire('search', 'test');
-        picker.fire('search', 'guess', undefined, true);
-        mock_io.failure();
-        Assert.areEqual(null, picker.get('error'));
-        cleanup_widget(picker);
-    }
-
-}));
-
-Y.lp.testing.Runner.run(suite);
-
-});
+            };
+        },
+
+        test_automated_search_results_ignored_if_user_has_searched: function() {
+            // If an automated search (like loading branch suggestions) returns
+            // results and the user has submitted a search, then the results of
+            // the automated search are ignored so as not to confuse the user.
+            var mock_io = new Y.lp.testing.mockio.MockIo();
+            var picker = this.create_picker(mock_io);
+            // First an automated search is run.
+            picker.fire('search', 'guess', undefined, true);
+            // Then the user initiates their own search.
+            picker.fire('search', 'test');
+            // Two requests have been sent out.
+            Y.Assert.areEqual(2, mock_io.requests.length);
+            // Respond to the automated request.
+            mock_io.requests[0].respond({responseText: '{"entries": 1}'});
+            // ... the results are ignored.
+            Assert.areNotEqual(1, picker.get('results'));
+            // Respond to the user request.
+            mock_io.requests[1].respond({responseText: '{"entries": 2}'});
+            Assert.areEqual(2, picker.get('results'));
+            cleanup_widget(picker);
+        },
+
+        test_automated_search_error_ignored_if_user_has_searched: function() {
+            // If an automated search (like loading branch suggestions) returns an
+            // error and the user has submitted a search, then the error from the
+            // automated search is ignored so as not to confuse the user.
+            var mock_io = new Y.lp.testing.mockio.MockIo();
+            var picker = this.create_picker(mock_io);
+            picker.fire('search', 'test');
+            picker.fire('search', 'guess', undefined, true);
+            mock_io.failure();
+            Assert.areEqual(null, picker.get('error'));
+            cleanup_widget(picker);
+        }
+
+    }));
+
+}, '0.1', {'requires': ['test', 'console', 'lp.pickert', 'node', 'lp', 'lp.client',
+        'event-focus', 'event-simulate', 'lazr.picker', 'lazr.person-picker',
+        'lp.app.picker', 'node-event-simulate', 'escape', 'event',
+        'lp.testing.mockio',]});