← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wallyworld/launchpad/lazr-js-tests into lp:launchpad

 

Ian Booth has proposed merging lp:~wallyworld/launchpad/lazr-js-tests into lp:launchpad with lp:~wallyworld/launchpad/move-lazr-js-source as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~wallyworld/launchpad/lazr-js-tests/+merge/66987

Refactor the lazr-js javascript tests so they are run as part of the Launchpad YUI test layer.

== Implementation ==

Changes include:
- renaming lazr-js test html/js files so they are prepended by test_
- using Launchpad test harness infrastructure in the lazr js test files
- modifying build_yui_unittest_suite so that it descends into all directories under the root to look for test_* files
- ported across lazr-js combo unit test

The picker tests have been consolidated and moved to the picker directory. The picker_patcher.js has also been moved.

Also made a drive by fix for an xss issue in picker.js

== Tests ==

ian@wallyworld:~/projects/lp-branches/devel-sandbox$ bin/test -vvc --layer=YUI
Running tests at level 1
Running canonical.testing.layers.YUITestLayer tests:
  Set up canonical.testing.layers.BaseLayer in 0.050 seconds.
  Set up canonical.testing.layers.FunctionalLayer in 5.049 seconds.
  Set up canonical.testing.layers.YUITestLayer in 0.005 seconds.
  Running:
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/app/javascript/tests/test_hide_comment.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/app/javascript/tests/test_lp_names.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/app/javascript/tests/test_expander.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/app/javascript/tests/test_lp_client.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/app/javascript/tests/test_lp_collapsibles.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/app/javascript/tests/test_multicheckboxwidget.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/app/javascript/anim/tests/test_anim.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/app/javascript/inlineedit/tests/test_inline_edit.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/app/javascript/activator/tests/test_activator.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/app/javascript/picker/tests/test_picker_patcher.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/app/javascript/picker/tests/test_personpicker.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/app/javascript/picker/tests/test_picker.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/app/javascript/overlay/tests/test_overlay.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/app/javascript/autocomplete/tests/test_autocomplete.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/app/javascript/formoverlay/tests/test_formoverlay.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/app/javascript/choiceedit/tests/test_choiceedit.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/bugs/javascript/tests/test_filebug_dupfinder.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/bugs/javascript/tests/test_bug_subscription_portlet.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/bugs/javascript/tests/test_me_too.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/bugs/javascript/tests/test_subscription.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/bugs/javascript/tests/test_subscribers_list.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/bugs/javascript/tests/test_bug_notification_level.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/code/javascript/tests/test_branchdiff.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/code/javascript/tests/test_productseries-setbranch.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/registry/javascript/tests/test_structural_subscription.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/registry/javascript/tests/test_distroseries.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/registry/javascript/tests/test_milestone_table.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/registry/javascript/tests/test_distroseriesdifferences_details.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/registry/javascript/tests/test_timeline.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/soyuz/javascript/tests/test_lp_dynamic_dom_updater.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/soyuz/javascript/tests/test_archivesubscribers_index.html
 /home/ian/projects/lp-branches/devel-sandbox/lib/lp/translations/javascript/tests/test_sourcepackage_sharing_details.html
  Ran 32 tests with 0 failures and 0 errors in 56.297 seconds.

== Lint ==

There's lots of lint warnings in the existing lazr-js tests which have been copied into the lp source tree. Too many to fix in this branch and not really relevant here since the source has simply been copied across from lazr-js.
-- 
https://code.launchpad.net/~wallyworld/launchpad/lazr-js-tests/+merge/66987
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wallyworld/launchpad/lazr-js-tests into lp:launchpad.
=== renamed file 'lib/lp/app/javascript/activator/tests/activator.html' => 'lib/lp/app/javascript/activator/tests/test_activator.html'
--- lib/lp/app/javascript/activator/tests/activator.html	2011-07-07 01:58:17 +0000
+++ lib/lp/app/javascript/activator/tests/test_activator.html	2011-07-07 01:58:19 +0000
@@ -5,25 +5,22 @@
   <title>Activator</title>
 
   <!-- YUI 3.0 Setup -->
-  <script type="text/javascript" src="../../tests/lazr/config.js"></script>
-  <script type="text/javascript" src="../../yui/yui/yui.js"></script>
-  <link rel="stylesheet" href="../../yui/cssreset/reset.css"/>
-  <link rel="stylesheet" href="../../yui/cssfonts/fonts.css"/>
-  <link rel="stylesheet" href="../../yui/cssbase/base.css"/>
+  <script type="text/javascript" src="../../../../../canonical/launchpad/icing/yui/yui/yui.js"></script>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssbase/base.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/javascript/test.css" />
 
   <!-- The module under test -->
   <script type="text/javascript" src="../activator.js"></script>
   <script type="text/javascript" src="../../anim/anim.js"></script>
   <script type="text/javascript" src="../../lazr/lazr.js"></script>
-  <script type="text/javascript" src="../../tests/lazr/testing.js"></script>
 
   <!-- The test suite -->
-  <script type="text/javascript" src="activator.js"></script>
+  <script type="text/javascript" src="test_activator.js"></script>
 
-  <link rel="stylesheet" href="../../tests/lazr/assets/testlogger.css"/>
 </head>
 <body class="yui3-skin-sam">
-
 <div id="log"></div>
 </body>
 </html>

=== renamed file 'lib/lp/app/javascript/activator/tests/activator.js' => 'lib/lp/app/javascript/activator/tests/test_activator.js'
--- lib/lp/app/javascript/activator/tests/activator.js	2011-06-29 14:56:15 +0000
+++ lib/lp/app/javascript/activator/tests/test_activator.js	2011-07-07 01:58:19 +0000
@@ -1,7 +1,12 @@
 /* Copyright (c) 2009, Canonical Ltd. All rights reserved. */
 
-YUI().use('lazr.activator', 'lazr.testing.runner', 'node',
-           'event', 'event-simulate', 'console', function(Y) {
+YUI({
+    base: '../../../../../canonical/launchpad/icing/yui/',
+    filter: 'raw',
+    combine: false,
+    fetchCSS: false
+    }).use('test', 'console', 'node', 'lazr.activator',
+           'event', 'event-simulate', function(Y) {
 
 var Assert = Y.Assert;  // For easy access to isTrue(), etc.
 
@@ -77,7 +82,7 @@
         var custom_node = Y.one('#custom-animation-node');
         this.activator = new Y.lazr.activator.Activator(
             {contentBox: Y.one('#example-1'), animationNode: custom_node});
-s        Assert.areEqual(custom_node, this.activator.animation_node);
+        Assert.areEqual(custom_node, this.activator.animation_node);
     },
 
     test_unhiding_action_button: function() {
@@ -268,7 +273,20 @@
     }
 }));
 
-Y.lazr.testing.Runner.add(suite);
-Y.lazr.testing.Runner.run();
+// Lock, stock, and two smoking barrels.
+var handle_complete = function(data) {
+    window.status = '::::' + JSON.stringify(data);
+    };
+Y.Test.Runner.on('complete', handle_complete);
+Y.Test.Runner.add(suite);
+
+var yui_console = new Y.Console({
+    newestOnTop: false
+});
+yui_console.render('#log');
+
+Y.on('domready', function() {
+    Y.Test.Runner.run();
+});
 
 });

=== renamed file 'lib/lp/app/javascript/anim/tests/anim.html' => 'lib/lp/app/javascript/anim/tests/test_anim.html'
--- lib/lp/app/javascript/anim/tests/anim.html	2011-07-07 01:58:17 +0000
+++ lib/lp/app/javascript/anim/tests/test_anim.html	2011-07-07 01:58:19 +0000
@@ -5,21 +5,19 @@
   <title>Anim</title>
 
   <!-- YUI 3.0 Setup -->
-  <script type="text/javascript" src="../../tests/lazr/config.js"></script>
-  <script type="text/javascript" src="../../yui/yui/yui.js"></script>
-  <link rel="stylesheet" href="../../yui/cssreset/reset.css"/>
-  <link rel="stylesheet" href="../../yui/cssfonts/fonts.css"/>
-  <link rel="stylesheet" href="../../yui/cssbase/base.css"/>
+  <script type="text/javascript" src="../../../../../canonical/launchpad/icing/yui/yui/yui.js"></script>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssbase/base.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/javascript/test.css" />
 
   <!-- The module under test -->
   <script type="text/javascript" src="../anim.js"></script>
   <script type="text/javascript" src="../../lazr/lazr.js"></script>
-  <script type="text/javascript" src="../../tests/lazr/testing.js"></script>
 
   <!-- The test suite -->
-  <script type="text/javascript" src="anim.js"></script>
+  <script type="text/javascript" src="test_anim.js"></script>
 
-  <link rel="stylesheet" href="../../tests/lazr/assets/testlogger.css"/>
 </head>
 <body class="yui3-skin-sam">
 

=== renamed file 'lib/lp/app/javascript/anim/tests/anim.js' => 'lib/lp/app/javascript/anim/tests/test_anim.js'
--- lib/lp/app/javascript/anim/tests/anim.js	2011-06-29 14:56:15 +0000
+++ lib/lp/app/javascript/anim/tests/test_anim.js	2011-07-07 01:58:19 +0000
@@ -1,7 +1,12 @@
 /* Copyright (c) 2009, Canonical Ltd. All rights reserved. */
 
-YUI().use('lazr.anim', 'lazr.testing.runner', 'node',
-          'event', 'event-simulate', 'console', function(Y) {
+YUI({
+    base: '../../../../../canonical/launchpad/icing/yui/',
+    filter: 'raw',
+    combine: false,
+    fetchCSS: false
+    }).use('test', 'console', 'node', 'lazr.anim',
+           'event', 'event-simulate', function(Y) {
 
 var Assert = Y.Assert;  // For easy access to isTrue(), etc.
 
@@ -181,7 +186,20 @@
     }
     }));
 
-Y.lazr.testing.Runner.add(suite);
-Y.lazr.testing.Runner.run();
+// Lock, stock, and two smoking barrels.
+var handle_complete = function(data) {
+    window.status = '::::' + JSON.stringify(data);
+    };
+Y.Test.Runner.on('complete', handle_complete);
+Y.Test.Runner.add(suite);
+
+var yui_console = new Y.Console({
+    newestOnTop: false
+});
+yui_console.render('#log');
+
+Y.on('domready', function() {
+    Y.Test.Runner.run();
+});
 
 });

=== renamed file 'lib/lp/app/javascript/autocomplete/tests/index.html' => 'lib/lp/app/javascript/autocomplete/tests/test_autocomplete.html'
--- lib/lp/app/javascript/autocomplete/tests/index.html	2011-07-07 01:58:17 +0000
+++ lib/lp/app/javascript/autocomplete/tests/test_autocomplete.html	2011-07-07 01:58:19 +0000
@@ -5,20 +5,17 @@
   <title>autocomplete unit tests</title>
 
   <!-- YUI 3.0 Setup -->
-  <script type="text/javascript" src="../../tests/lazr/config.js"></script>
-  <script type="text/javascript" src="../../yui/yui/yui.js"></script>
-  <link rel="stylesheet" href="../../yui/cssreset/reset.css"/>
-  <link rel="stylesheet" href="../../yui/cssfonts/fonts.css"/>
-  <link rel="stylesheet" href="../../yui/cssbase/base.css"/>
-
-  <link rel="stylesheet" href="../../tests/lazr/assets/testlogger.css"/>
-  <script type="text/javascript" src="../../tests/lazr/testing.js"></script>
+  <script type="text/javascript" src="../../../../../canonical/launchpad/icing/yui/yui/yui.js"></script>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssbase/base.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/javascript/test.css" />
 
   <!-- The module under test -->
   <script type="text/javascript" src="../autocomplete.js"></script>
 
   <!-- The test suite -->
-  <script type="text/javascript" src="autocomplete.js"></script>
+  <script type="text/javascript" src="test_autocomplete.js"></script>
 
 </head>
 <body class="yui3-skin-sam">

=== renamed file 'lib/lp/app/javascript/autocomplete/tests/autocomplete.js' => 'lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js'
--- lib/lp/app/javascript/autocomplete/tests/autocomplete.js	2011-06-29 14:56:15 +0000
+++ lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js	2011-07-07 01:58:19 +0000
@@ -1,7 +1,12 @@
 /* Copyright (c) 2009, Canonical Ltd. All rights reserved. */
 
-YUI().use('lazr.autocomplete', 'lazr.testing.runner',
-          'node', 'event', 'console', function(Y) {
+YUI({
+    base: '../../../../../canonical/launchpad/icing/yui/',
+    filter: 'raw',
+    combine: false,
+    fetchCSS: false
+    }).use('test', 'console', 'node', 'lazr.autocomplete',
+           'event', 'event-simulate', function(Y) {
 
 /*****************************
  *
@@ -564,7 +569,20 @@
     }
 }));
 
-Y.lazr.testing.Runner.add(suite);
-Y.lazr.testing.Runner.run();
+// Lock, stock, and two smoking barrels.
+var handle_complete = function(data) {
+    window.status = '::::' + JSON.stringify(data);
+    };
+Y.Test.Runner.on('complete', handle_complete);
+Y.Test.Runner.add(suite);
+
+var yui_console = new Y.Console({
+    newestOnTop: false
+});
+yui_console.render('#log');
+
+Y.on('domready', function() {
+    Y.Test.Runner.run();
+});
 
 });

=== renamed file 'lib/lp/app/javascript/choiceedit/tests/choiceedit.html' => 'lib/lp/app/javascript/choiceedit/tests/test_choiceedit.html'
--- lib/lp/app/javascript/choiceedit/tests/choiceedit.html	2011-07-07 01:58:17 +0000
+++ lib/lp/app/javascript/choiceedit/tests/test_choiceedit.html	2011-07-07 01:58:19 +0000
@@ -4,23 +4,22 @@
   <title>Status Editor</title>
 
   <!-- YUI 3.0 Setup -->
-  <script type="text/javascript" src="../../tests/lazr/config.js"></script>
-  <script type="text/javascript" src="../../yui/yui/yui.js"></script>
-  <link rel="stylesheet" href="../../yui/cssreset/reset.css"/>
-  <link rel="stylesheet" href="../../yui/cssfonts/fonts.css"/>
-  <link rel="stylesheet" href="../../yui/cssbase/base.css"/>
+  <script type="text/javascript" src="../../../../../canonical/launchpad/icing/yui/yui/yui.js"></script>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssbase/base.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/javascript/test.css" />
 
   <!-- Dependency -->
   <script type="text/javascript" src="../../lazr/lazr.js"></script>
   <script type="text/javascript" src="../../anim/anim.js"></script>
-  <script type="text/javascript" src="../../tests/lazr/testing.js"></script>
   <script type="text/javascript" src="../../overlay/overlay.js"></script>
 
   <!-- The module under test -->
   <script type="text/javascript" src="../choiceedit.js"></script>
 
   <!-- The test suite -->
-  <script type="text/javascript" src="choiceedit.js"></script>
+  <script type="text/javascript" src="test_choiceedit.js"></script>
 </head>
 <body class="yui3-skin-sam">
 

=== renamed file 'lib/lp/app/javascript/choiceedit/tests/choiceedit.js' => 'lib/lp/app/javascript/choiceedit/tests/test_choiceedit.js'
--- lib/lp/app/javascript/choiceedit/tests/choiceedit.js	2011-06-29 14:56:15 +0000
+++ lib/lp/app/javascript/choiceedit/tests/test_choiceedit.js	2011-07-07 01:58:19 +0000
@@ -1,7 +1,12 @@
 /* Copyright (c) 2008, Canonical Ltd. All rights reserved. */
 
-YUI().use('lazr.choiceedit', 'lazr.testing.runner', 'node',
-          'event', 'event-simulate', 'widget-stack', 'console', function(Y) {
+YUI({
+    base: '../../../../../canonical/launchpad/icing/yui/',
+    filter: 'raw',
+    combine: false,
+    fetchCSS: false
+    }).use('test', 'console', 'node', 'lazr.choiceedit',
+           'event', 'event-simulate', 'widget-stack', function(Y) {
 
 // Local aliases
 var Assert = Y.Assert,
@@ -522,7 +527,20 @@
     }
 }));
 
-Y.lazr.testing.Runner.add(suite);
-Y.lazr.testing.Runner.run();
+// Lock, stock, and two smoking barrels.
+var handle_complete = function(data) {
+    window.status = '::::' + JSON.stringify(data);
+    };
+Y.Test.Runner.on('complete', handle_complete);
+Y.Test.Runner.add(suite);
+
+var yui_console = new Y.Console({
+    newestOnTop: false
+});
+yui_console.render('#log');
+
+Y.on('domready', function() {
+    Y.Test.Runner.run();
+});
 
 });

=== renamed file 'lib/lp/app/javascript/formoverlay/tests/formoverlay.html' => 'lib/lp/app/javascript/formoverlay/tests/test_formoverlay.html'
--- lib/lp/app/javascript/formoverlay/tests/formoverlay.html	2011-07-07 01:58:17 +0000
+++ lib/lp/app/javascript/formoverlay/tests/test_formoverlay.html	2011-07-07 01:58:19 +0000
@@ -4,26 +4,24 @@
   <title>Form Overlay</title>
 
   <!-- YUI 3.0 Setup -->
-  <script type="text/javascript" src="../../tests/lazr/config.js"></script>
-  <script type="text/javascript" src="../../yui/yui/yui.js"></script>
-  <link rel="stylesheet" href="../../yui/cssreset/reset.css"/>
-  <link rel="stylesheet" href="../../yui/cssfonts/fonts.css"/>
-  <link rel="stylesheet" href="../../yui/cssbase/base.css"/>
-  <link rel="stylesheet" href="../../tests/lazr/assets/testlogger.css"/>
+  <script type="text/javascript" src="../../../../../canonical/launchpad/icing/yui/yui/yui.js"></script>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssbase/base.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/javascript/test.css" />
 
   <!-- 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="../../tests/lazr/testing.js"></script>
 
   <!-- The module under test -->
   <script type="text/javascript" src="../formoverlay.js"></script>
 
   <!-- Testing helpers -->
-  <script type="text/javascript" src="../../tests/lazr/mockio.js"></script>
+  <script type="text/javascript" src="../../testing/mockio.js"></script>
 
   <!-- The test suite -->
-  <script type="text/javascript" src="formoverlay.js"></script>
+  <script type="text/javascript" src="test_formoverlay.js"></script>
 
 </head>
 <body class="yui3-skin-sam">

=== renamed file 'lib/lp/app/javascript/formoverlay/tests/formoverlay.js' => 'lib/lp/app/javascript/formoverlay/tests/test_formoverlay.js'
--- lib/lp/app/javascript/formoverlay/tests/formoverlay.js	2011-06-29 14:56:15 +0000
+++ lib/lp/app/javascript/formoverlay/tests/test_formoverlay.js	2011-07-07 01:58:19 +0000
@@ -1,8 +1,12 @@
 /* Copyright (c) 2008, Canonical Ltd. All rights reserved. */
 
-YUI().use('lazr.formoverlay', 'lazr.testing.runner',
-          'lazr.testing.mockio', 'node', 'event', 'event-simulate',
-          'dump', 'console', function(Y) {
+YUI({
+    base: '../../../../../canonical/launchpad/icing/yui/',
+    filter: 'raw',
+    combine: false,
+    fetchCSS: false
+    }).use('test', 'dump', 'console', 'node', 'lazr.formoverlay',
+           'event', 'event-simulate', 'lazr.testing.mockio', function(Y) {
 
 var Assert = Y.Assert;  // For easy access to isTrue(), etc.
 
@@ -498,7 +502,20 @@
     }
 }));
 
-Y.lazr.testing.Runner.add(suite);
-Y.lazr.testing.Runner.run();
+// Lock, stock, and two smoking barrels.
+var handle_complete = function(data) {
+    window.status = '::::' + JSON.stringify(data);
+    };
+Y.Test.Runner.on('complete', handle_complete);
+Y.Test.Runner.add(suite);
+
+var yui_console = new Y.Console({
+    newestOnTop: false
+});
+yui_console.render('#log');
+
+Y.on('domready', function() {
+    Y.Test.Runner.run();
+});
 
 });

=== renamed file 'lib/lp/app/javascript/inlineedit/tests/index.html' => 'lib/lp/app/javascript/inlineedit/tests/test_inline_edit.html'
--- lib/lp/app/javascript/inlineedit/tests/index.html	2011-07-07 01:58:17 +0000
+++ lib/lp/app/javascript/inlineedit/tests/test_inline_edit.html	2011-07-07 01:58:19 +0000
@@ -5,21 +5,19 @@
   <title>Inline Edit</title>
 
   <!-- YUI 3.0 Setup -->
-  <script type="text/javascript" src="../../tests/lazr/config.js"></script>
-  <script type="text/javascript" src="../../yui/yui/yui.js"></script>
-  <link rel="stylesheet" href="../../yui/cssreset/reset.css"/>
-  <link rel="stylesheet" href="../../yui/cssfonts/fonts.css"/>
-  <link rel="stylesheet" href="../../yui/cssbase/base.css"/>
-  <link rel="stylesheet" href="../../tests/lazr/assets/testlogger.css"/>
+  <script type="text/javascript" src="../../../../../canonical/launchpad/icing/yui/yui/yui.js"></script>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssbase/base.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/javascript/test.css" />
 
   <!-- The module under test -->
   <script type="text/javascript" src="../editor.js"></script>
   <script type="text/javascript" src="../../anim/anim.js"></script>
   <script type="text/javascript" src="../../lazr/lazr.js"></script>
-  <script type="text/javascript" src="../../tests/lazr/testing.js"></script>
 
   <!-- The test suite -->
-  <script type="text/javascript" src="inline_edit.js"></script>
+  <script type="text/javascript" src="test_inline_edit.js"></script>
 
 </head>
 <body class="yui3-skin-sam">

=== renamed file 'lib/lp/app/javascript/inlineedit/tests/inline_edit.js' => 'lib/lp/app/javascript/inlineedit/tests/test_inline_edit.js'
--- lib/lp/app/javascript/inlineedit/tests/inline_edit.js	2011-06-29 14:56:15 +0000
+++ lib/lp/app/javascript/inlineedit/tests/test_inline_edit.js	2011-07-07 01:58:19 +0000
@@ -1,8 +1,14 @@
 /* Copyright (c) 2009, Canonical Ltd. All rights reserved. */
 
-YUI().use('lazr.editor', 'lazr.testing.runner', 'node',
-          'event', 'event-simulate', 'console', 'plugin', function(Y) {
-var SAMPLE_HTML = "                                                                           \
+YUI({
+    base: '../../../../../canonical/launchpad/icing/yui/',
+    filter: 'raw',
+    combine: false,
+    fetchCSS: false
+    }).use('test', 'console', 'node', 'lazr.editor',
+           'event', 'event-simulate', 'plugin', function(Y) {
+
+        var SAMPLE_HTML = "                                                                           \
  <h1>Single-line editing</h1>                                                                 \
   <div id='editable_single_text'>                                                             \
     <span id='single_text' class='yui3-editable_text-text'>Some editable inline text.</span>  \
@@ -1232,19 +1238,30 @@
         Assert.isTrue(editor.get('in_error'), "Editor should be in error");
         // Both the submit and cancel buttons should be visible.
         Assert.areEqual(
-            'inline',
+            'inline-block',
             editor.get('submit_button').getStyle('display'),
             "Submit should be set to display:inline");
         Assert.areEqual(
-            'inline',
+            'inline-block',
             editor.get('cancel_button').getStyle('display'),
             "Cancel should be set to display:inline");
     }
 }));
 
-
-
-Y.lazr.testing.Runner.add(suite);
-Y.lazr.testing.Runner.run();
+// Lock, stock, and two smoking barrels.
+var handle_complete = function(data) {
+    window.status = '::::' + JSON.stringify(data);
+    };
+Y.Test.Runner.on('complete', handle_complete);
+Y.Test.Runner.add(suite);
+
+var yui_console = new Y.Console({
+    newestOnTop: false
+});
+yui_console.render('#log');
+
+Y.on('domready', function() {
+    Y.Test.Runner.run();
+});
 
 });

=== renamed file 'lib/lp/app/javascript/overlay/tests/overlay.html' => 'lib/lp/app/javascript/overlay/tests/test_overlay.html'
--- lib/lp/app/javascript/overlay/tests/overlay.html	2011-07-07 01:58:17 +0000
+++ lib/lp/app/javascript/overlay/tests/test_overlay.html	2011-07-07 01:58:19 +0000
@@ -5,19 +5,18 @@
   <title>Pretty Overlay</title>
 
   <!-- YUI 3.0 Setup -->
-  <script type="text/javascript" src="../../tests/lazr/config.js"></script>
-  <script type="text/javascript" src="../../yui/yui/yui.js"></script>
-  <link rel="stylesheet" href="../../yui/cssreset/reset.css"/>
-  <link rel="stylesheet" href="../../yui/cssfonts/fonts.css"/>
-  <link rel="stylesheet" href="../../yui/cssbase/base.css"/>
-  <link rel="stylesheet" href="../../tests/lazr/assets/testlogger.css"/>
+  <!-- YUI 3.0 Setup -->
+  <script type="text/javascript" src="../../../../../canonical/launchpad/icing/yui/yui/yui.js"></script>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssbase/base.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/javascript/test.css" />
 
   <!-- The module under test -->
   <script type="text/javascript" src="../overlay.js"></script>
-  <script type="text/javascript" src="../../tests/lazr/testing.js"></script>
 
   <!-- The test suite -->
-  <script type="text/javascript" src="overlay.js"></script>
+  <script type="text/javascript" src="test_overlay.js"></script>
 
 </head>
 <body class="yui3-skin-sam">

=== renamed file 'lib/lp/app/javascript/overlay/tests/overlay.js' => 'lib/lp/app/javascript/overlay/tests/test_overlay.js'
--- lib/lp/app/javascript/overlay/tests/overlay.js	2011-06-29 14:56:15 +0000
+++ lib/lp/app/javascript/overlay/tests/test_overlay.js	2011-07-07 01:58:19 +0000
@@ -1,7 +1,12 @@
 /* Copyright (c) 2009, Canonical Ltd. All rights reserved. */
 
-YUI().use('lazr.overlay', 'lazr.testing.runner', 'node',
-          'event', 'event-simulate', 'widget-stack', 'console', function(Y) {
+YUI({
+    base: '../../../../../canonical/launchpad/icing/yui/',
+    filter: 'raw',
+    combine: false,
+    fetchCSS: false
+    }).use('test', 'console', 'node', 'lazr.overlay',
+           'event', 'event-simulate', 'widget-stack', function(Y) {
 
 // KeyCode for escape
 var ESCAPE = 27;
@@ -211,7 +216,20 @@
 
 }));
 
-Y.lazr.testing.Runner.add(suite);
-Y.lazr.testing.Runner.run();
+// Lock, stock, and two smoking barrels.
+var handle_complete = function(data) {
+    window.status = '::::' + JSON.stringify(data);
+    };
+Y.Test.Runner.on('complete', handle_complete);
+Y.Test.Runner.add(suite);
+
+var yui_console = new Y.Console({
+    newestOnTop: false
+});
+yui_console.render('#log');
+
+Y.on('domready', function() {
+    Y.Test.Runner.run();
+});
 
 });

=== modified file 'lib/lp/app/javascript/picker/picker.js'
--- lib/lp/app/javascript/picker/picker.js	2011-07-05 04:01:35 +0000
+++ lib/lp/app/javascript/picker/picker.js	2011-07-07 01:58:19 +0000
@@ -372,7 +372,9 @@
                 }, alt_link);
             }
             li_title.appendChild('&nbsp;(');
-            li_title.appendChild(document.createTextNode(data.alt_title));
+            var alt_title_node = Y.Node.create('<span></span>')
+                .set('text', data.alt_title);
+            li_title.appendChild(alt_title_node);
             li_title.appendChild(')');
             if (alt_link !== null) {
                 li_title.appendChild(alt_link);

=== renamed file 'lib/lp/app/javascript/picker_patcher.js' => 'lib/lp/app/javascript/picker/picker_patcher.js'
=== renamed file 'lib/lp/app/javascript/tests/test_personpicker.html' => 'lib/lp/app/javascript/picker/tests/test_personpicker.html'
--- lib/lp/app/javascript/tests/test_personpicker.html	2011-07-07 01:58:17 +0000
+++ lib/lp/app/javascript/picker/tests/test_personpicker.html	2011-07-07 01:58:19 +0000
@@ -2,28 +2,28 @@
   <head>
     <title>Launchpad PersonPicker</title>
     <!-- YUI 3.0 Setup -->
-    <script type="text/javascript" src="../../../../canonical/launchpad/icing/yui/yui/yui.js"></script>
-    <link rel="stylesheet"
-      href="../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/>
-    <link rel="stylesheet"
-      href="../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/>
-    <link rel="stylesheet"
-      href="../../../../canonical/launchpad/icing/yui/cssbase/base.css"/>
-    <link rel="stylesheet"
-      href="../../../../canonical/launchpad/javascript/test.css" />
+    <script type="text/javascript" src="../../../../../canonical/launchpad/icing/yui/yui/yui.js"></script>
+    <link rel="stylesheet"
+      href="../../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/>
+    <link rel="stylesheet"
+      href="../../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/>
+    <link rel="stylesheet"
+      href="../../../../../canonical/launchpad/icing/yui/cssbase/base.css"/>
+    <link rel="stylesheet"
+      href="../../../../../canonical/launchpad/javascript/test.css" />
 
     <!-- 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="../picker/picker.js"></script>
+    <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="../picker.js"></script>
     <script type="text/javascript" src="../picker_patcher.js"></script>
 
     <!-- The module under test -->
-    <script type="text/javascript" src="../picker/person_picker.js"></script>
+    <script type="text/javascript" src="../person_picker.js"></script>
 
     <!-- The test suite -->
     <script type="text/javascript" src="test_personpicker.js"></script>

=== renamed file 'lib/lp/app/javascript/tests/test_personpicker.js' => 'lib/lp/app/javascript/picker/tests/test_personpicker.js'
--- lib/lp/app/javascript/tests/test_personpicker.js	2011-07-06 03:20:46 +0000
+++ lib/lp/app/javascript/picker/tests/test_personpicker.js	2011-07-07 01:58:19 +0000
@@ -3,7 +3,7 @@
  */
 
 YUI({
-    base: '../../../../canonical/launchpad/icing/yui/',
+    base: '../../../../../canonical/launchpad/icing/yui/',
     filter: 'raw', combine: false,
     fetchCSS: false
     }).use('test', 'console', 'plugin',

=== renamed file 'lib/lp/app/javascript/picker/tests/picker.html' => 'lib/lp/app/javascript/picker/tests/test_picker.html'
--- lib/lp/app/javascript/picker/tests/picker.html	2011-07-07 01:58:17 +0000
+++ lib/lp/app/javascript/picker/tests/test_picker.html	2011-07-07 01:58:19 +0000
@@ -5,22 +5,20 @@
   <title>Picker</title>
 
   <!-- YUI 3.0 Setup -->
-  <script type="text/javascript" src="../../tests/lazr/config.js"></script>
-  <script type="text/javascript" src="../../yui/yui/yui.js"></script>
-  <link rel="stylesheet" href="../../yui/cssreset/reset.css"/>
-  <link rel="stylesheet" href="../../yui/cssfonts/fonts.css"/>
-  <link rel="stylesheet" href="../../yui/cssbase/base.css"/>
-  <link rel="stylesheet" href="../../tests/lazr/assets/testlogger.css"/>
+  <script type="text/javascript" src="../../../../../canonical/launchpad/icing/yui/yui/yui.js"></script>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssbase/base.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/javascript/test.css" />
 
   <!-- 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="../../anim/anim.js"></script>
   <script type="text/javascript" src="../../lazr/lazr.js"></script>
-  <script type="text/javascript" src="../../tests/lazr/testing.js"></script>
 
   <!-- The test suite -->
-  <script type="text/javascript" src="picker.js"></script>
+  <script type="text/javascript" src="test_picker.js"></script>
 </head>
 <body class="yui3-skin-sam">
   <div id="log"></div>

=== renamed file 'lib/lp/app/javascript/picker/tests/picker.js' => 'lib/lp/app/javascript/picker/tests/test_picker.js'
--- lib/lp/app/javascript/picker/tests/picker.js	2011-07-05 03:17:02 +0000
+++ lib/lp/app/javascript/picker/tests/test_picker.js	2011-07-07 01:58:19 +0000
@@ -1,8 +1,12 @@
 /* Copyright (c) 2009, Canonical Ltd. All rights reserved. */
 
-YUI().use('lazr.picker', 'lazr.testing.runner', 'node',
-          'event', 'event-focus', 'event-simulate', 'console', 'dump',
-          function(Y) {
+YUI({
+    base: '../../../../../canonical/launchpad/icing/yui/',
+    filter: 'raw',
+    combine: false,
+    fetchCSS: false
+    }).use('test', 'console', 'dump', 'node', 'lazr.picker',
+           'event', 'event-simulate', function(Y) {
 
 // Local aliases
 var Assert = Y.Assert,
@@ -160,11 +164,14 @@
                 link_selector + ' link was not clicked');
         }
         check_link(
-            this.picker, '.cool-style:nth-child(1)', 'Joe Schmo',
+            this.picker, 'a.cool-style:nth-child(1)', 'Joe Schmo',
             'http://somewhere.com/');
         check_link(
-            this.picker, '.cool-style:nth-child(2)', 'Joe Again <foo></foo>',
+            this.picker, 'a.cool-style:nth-child(3)', ' 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_title_badges: function () {
@@ -954,7 +961,20 @@
 
 }));
 
-Y.lazr.testing.Runner.add(suite);
-Y.lazr.testing.Runner.run();
+// Lock, stock, and two smoking barrels.
+var handle_complete = function(data) {
+    window.status = '::::' + JSON.stringify(data);
+    };
+Y.Test.Runner.on('complete', handle_complete);
+Y.Test.Runner.add(suite);
+
+var yui_console = new Y.Console({
+    newestOnTop: false
+});
+yui_console.render('#log');
+
+Y.on('domready', function() {
+    Y.Test.Runner.run();
+});
 
 });

=== renamed file 'lib/lp/app/javascript/tests/test_picker.html' => 'lib/lp/app/javascript/picker/tests/test_picker_patcher.html'
--- lib/lp/app/javascript/tests/test_picker.html	2011-07-07 01:58:17 +0000
+++ lib/lp/app/javascript/picker/tests/test_picker_patcher.html	2011-07-07 01:58:19 +0000
@@ -5,26 +5,26 @@
   <title>Launchpad Picker</title>
 
   <!-- YUI 3.0 Setup -->
-  <script type="text/javascript" src="../../../../canonical/launchpad/icing/yui/yui/yui.js"></script>
-  <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/>
-  <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/>
-  <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssbase/base.css"/>
-  <link rel="stylesheet" href="../../../../canonical/launchpad/javascript/test.css" />
+  <script type="text/javascript" src="../../../../../canonical/launchpad/icing/yui/yui/yui.js"></script>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/icing/yui/cssbase/base.css"/>
+  <link rel="stylesheet" href="../../../../../canonical/launchpad/javascript/test.css" />
 
   <!-- 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="../../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="../picker_patcher.js"></script>
 
   <!-- The module under test -->
-  <script type="text/javascript" src="../picker/picker.js"></script>
-  <script type="text/javascript" src="../picker/person_picker.js"></script>
+  <script type="text/javascript" src="../picker.js"></script>
+  <script type="text/javascript" src="../person_picker.js"></script>
 
   <!-- The test suite -->
-  <script type="text/javascript" src="test_picker.js"></script>
+  <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">

=== renamed file 'lib/lp/app/javascript/tests/test_picker.js' => 'lib/lp/app/javascript/picker/tests/test_picker_patcher.js'
--- lib/lp/app/javascript/tests/test_picker.js	2011-07-06 03:20:46 +0000
+++ lib/lp/app/javascript/picker/tests/test_picker_patcher.js	2011-07-07 01:58:19 +0000
@@ -1,7 +1,7 @@
 /* Copyright (c) 2011, Canonical Ltd. All rights reserved. */
 
 YUI({
-    base: '../../../../canonical/launchpad/icing/yui/',
+    base: '../../../../../canonical/launchpad/icing/yui/',
     filter: 'raw',
     combine: false,
     fetchCSS: false

=== added directory 'lib/lp/app/javascript/testing'
=== added file 'lib/lp/app/javascript/testing/mockio.js'
--- lib/lp/app/javascript/testing/mockio.js	1970-01-01 00:00:00 +0000
+++ lib/lp/app/javascript/testing/mockio.js	2011-07-07 01:58:19 +0000
@@ -0,0 +1,69 @@
+/* Copyright (c) 2009, Canonical Ltd. All rights reserved. */
+
+YUI.add('lazr.testing.mockio', function(Y) {
+/**
+ * A utility module for use in YUI unit-tests with a helper for mocking Y.io.
+ *
+ * @module lazr.testing
+ */
+var MockIo = function() {
+    this.uri = null;
+    this.cfg = null;
+};
+
+/* Save the Y.io() arguments. */
+MockIo.prototype.io = function(uri, cfg) {
+    this.uri = uri;
+    this.cfg = cfg;
+    return this;  // Usually this isn't used, except for logging.
+};
+
+/* Simulate the Xhr request/response cycle. */
+MockIo.prototype.simulateXhr = function(response, is_failure) {
+    var cfg = this.cfg;
+    var context = cfg.context || this;
+    var args = cfg.arguments;
+    var tId = 'mockTId';
+    if (!response) {
+        response = {};
+    }
+
+    // See the Y.io utility documentation for the signatures.
+    if (cfg.on.start) {
+        cfg.on.start.call(context, tId, args);
+    }
+    if (cfg.on.complete) {
+        cfg.on.complete.call(context, tId, response, args);
+    }
+    if (cfg.on.success && !is_failure) {
+        cfg.on.success.call(context, tId, response, args);
+    }
+    if (cfg.on.failure && is_failure) {
+        cfg.on.failure.call(context, tId, response, args);
+    }
+};
+
+/* Make a successful XHR response object. */
+MockIo.makeXhrSuccessResponse = function(responseText) {
+    var text = responseText || "";
+    return {
+        status: 200,
+        statusText: "OK",
+        responseText: text
+    };
+};
+
+/* Make a failed XHR response object. */
+MockIo.makeXhrFailureResponse = function(responseText) {
+    var text = responseText || "";
+    return {
+        status: 500,
+        statusText: "Internal Server Error",
+        responseText: text
+    };
+};
+
+Y.namespace("lazr.testing");
+Y.lazr.testing.MockIo = MockIo;
+
+}, '0.1', {});

=== removed directory 'lib/lp/app/javascript/tests/lazr'
=== removed directory 'lib/lp/app/javascript/tests/lazr/assets'
=== removed file 'lib/lp/app/javascript/tests/lazr/assets/testlogger.css'
--- lib/lp/app/javascript/tests/lazr/assets/testlogger.css	2011-06-29 14:56:15 +0000
+++ lib/lp/app/javascript/tests/lazr/assets/testlogger.css	1970-01-01 00:00:00 +0000
@@ -1,24 +0,0 @@
-/* Taken and customized from testlogger.css */
-/*#log .yui3-console-content { width: 44em }*/
-/*#log .yui3-console .yui3-console-bd { height: 30em }*/
-#log .yui3-console .yui3-console-controls { display: none; }
-#log .yui3-console .yui3-console-hd { display: none; }
-#log .yui3-console .yui3-console-ft { position: absolute; top: 0; }
-
-#log .yui3-console-entry-src { display: none; }
-
-#log .yui3-console-entry-pass .yui3-console-entry-cat {
-  background-color: green;
-  font-weight: bold;
-  color: white;
-}
-#log .yui3-console-entry-fail .yui3-console-entry-cat {
-  background-color: red;
-  font-weight: bold;
-  color: white;
-}
-#log .yui3-console-entry-ignore .yui3-console-entry-cat {
-  background-color: #666;
-  font-weight: bold;
-  color: white;
-}

=== removed file 'lib/lp/app/javascript/tests/lazr/config.js'
--- lib/lp/app/javascript/tests/lazr/config.js	2011-06-29 14:56:15 +0000
+++ lib/lp/app/javascript/tests/lazr/config.js	1970-01-01 00:00:00 +0000
@@ -1,6 +0,0 @@
-var YUI_config = {
-    base: '../../yui/',
-    filter: 'debug',
-    combine: false,
-    fetchCSS: false
-};
\ No newline at end of file

=== removed file 'lib/lp/app/javascript/tests/lazr/jsUnitMockTimeout.js'
--- lib/lp/app/javascript/tests/lazr/jsUnitMockTimeout.js	2011-06-29 14:56:15 +0000
+++ lib/lp/app/javascript/tests/lazr/jsUnitMockTimeout.js	1970-01-01 00:00:00 +0000
@@ -1,120 +0,0 @@
-/*
-    Version: MPL 1.1/GPL 2.0/LGPL 2.1
-
-    The contents of this file are subject to the Mozilla Public License Version
-    1.1 (the "License"); you may not use this file except in compliance with
-    the License. You may obtain a copy of the License at
-    http://www.mozilla.org/MPL/
-
-    Software distributed under the License is distributed on an "AS IS" basis,
-    WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-    for the specific language governing rights and limitations under the
-    License.
-
-    The Original Code is Edward Hieatt code.
-
-    The Initial Developer of the Original Code is
-    Edward Hieatt, edward@xxxxxxxxxx.
-    Portions created by the Initial Developer are Copyright (C) 2003
-    the Initial Developer. All Rights Reserved.
-
-    Author Edward Hieatt, edward@xxxxxxxxxx
-
-    Alternatively, the contents of this file may be used under the terms of
-    either the GNU General Public License Version 2 or later (the "GPL"), or
-    the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-    in which case the provisions of the GPL or the LGPL are applicable instead
-    of those above. If you wish to allow use of your version of this file only
-    under the terms of either the GPL or the LGPL, and not to allow others to
-    use your version of this file under the terms of the MPL, indicate your
-    decision by deleting the provisions above and replace them with the notice
-    and other provisions required by the LGPL or the GPL. If you do not delete
-    the provisions above, a recipient may use your version of this file under
-    the terms of any one of the MPL, the GPL or the LGPL.
-*/
-
-// Mock setTimeout, clearTimeout
-// Contributed by Pivotal Computer Systems, www.pivotalsf.com
-//
-// Copied from the JsUnit 2.2alpha1 release, made in Mar 24 2006
-// (http://www.jsunit.net/)
-
-var Clock = {
-    timeoutsMade: 0,
-    scheduledFunctions: {},
-    nowMillis: 0,
-    reset: function() {
-        this.scheduledFunctions = {};
-        this.nowMillis = 0;
-        this.timeoutsMade = 0;
-    },
-    tick: function(millis) {
-        var oldMillis = this.nowMillis;
-        var newMillis = oldMillis + millis;
-        this.runFunctionsWithinRange(oldMillis, newMillis);
-        this.nowMillis = newMillis;
-    },
-    runFunctionsWithinRange: function(oldMillis, nowMillis) {
-        var scheduledFunc;
-        var funcsToRun = [];
-        for (var timeoutKey in this.scheduledFunctions) {
-            scheduledFunc = this.scheduledFunctions[timeoutKey];
-            if (scheduledFunc != undefined &&
-                scheduledFunc.runAtMillis >= oldMillis &&
-                scheduledFunc.runAtMillis <= nowMillis) {
-                funcsToRun.push(scheduledFunc);
-                this.scheduledFunctions[timeoutKey] = undefined;
-            }
-        }
-
-        if (funcsToRun.length > 0) {
-            funcsToRun.sort(function(a, b) {
-                return a.runAtMillis - b.runAtMillis;
-            });
-            for (var i = 0; i < funcsToRun.length; ++i) {
-                try {
-                    this.nowMillis = funcsToRun[i].runAtMillis;
-                    funcsToRun[i].funcToCall();
-                    if (funcsToRun[i].recurring) {
-                        Clock.scheduleFunction(funcsToRun[i].timeoutKey,
-                                funcsToRun[i].funcToCall,
-                                funcsToRun[i].millis,
-                                true);
-                    }
-                } catch(e) {
-                    console.log(e);
-                }
-            }
-            this.runFunctionsWithinRange(oldMillis, nowMillis);
-        }
-    },
-    scheduleFunction: function(timeoutKey, funcToCall, millis, recurring) {
-        Clock.scheduledFunctions[timeoutKey] = {
-            runAtMillis: Clock.nowMillis + millis,
-            funcToCall: funcToCall,
-            recurring: recurring,
-            timeoutKey: timeoutKey,
-            millis: millis
-        };
-    }
-};
-
-function setTimeout(funcToCall, millis) {
-    Clock.timeoutsMade = Clock.timeoutsMade + 1;
-    Clock.scheduleFunction(Clock.timeoutsMade, funcToCall, millis, false);
-    return Clock.timeoutsMade;
-}
-
-function setInterval(funcToCall, millis) {
-    Clock.timeoutsMade = Clock.timeoutsMade + 1;
-    Clock.scheduleFunction(Clock.timeoutsMade, funcToCall, millis, true);
-    return Clock.timeoutsMade;
-}
-
-function clearTimeout(timeoutKey) {
-    Clock.scheduledFunctions[timeoutKey] = undefined;
-}
-
-function clearInterval(timeoutKey) {
-    Clock.scheduledFunctions[timeoutKey] = undefined;
-}

=== removed file 'lib/lp/app/javascript/tests/lazr/mockio.js'
--- lib/lp/app/javascript/tests/lazr/mockio.js	2011-06-29 14:56:15 +0000
+++ lib/lp/app/javascript/tests/lazr/mockio.js	1970-01-01 00:00:00 +0000
@@ -1,69 +0,0 @@
-/* Copyright (c) 2009, Canonical Ltd. All rights reserved. */
-
-YUI.add('lazr.testing.mockio', function(Y) {
-/**
- * A utility module for use in YUI unit-tests with a helper for mocking Y.io.
- *
- * @module lazr.testing
- */
-var MockIo = function() {
-    this.uri = null;
-    this.cfg = null;
-};
-
-/* Save the Y.io() arguments. */
-MockIo.prototype.io = function(uri, cfg) {
-    this.uri = uri;
-    this.cfg = cfg;
-    return this;  // Usually this isn't used, except for logging.
-};
-
-/* Simulate the Xhr request/response cycle. */
-MockIo.prototype.simulateXhr = function(response, is_failure) {
-    var cfg = this.cfg;
-    var context = cfg.context || this;
-    var args = cfg.arguments;
-    var tId = 'mockTId';
-    if (!response) {
-        response = {};
-    }
-
-    // See the Y.io utility documentation for the signatures.
-    if (cfg.on.start) {
-        cfg.on.start.call(context, tId, args);
-    }
-    if (cfg.on.complete) {
-        cfg.on.complete.call(context, tId, response, args);
-    }
-    if (cfg.on.success && !is_failure) {
-        cfg.on.success.call(context, tId, response, args);
-    }
-    if (cfg.on.failure && is_failure) {
-        cfg.on.failure.call(context, tId, response, args);
-    }
-};
-
-/* Make a successful XHR response object. */
-MockIo.makeXhrSuccessResponse = function(responseText) {
-    var text = responseText || "";
-    return {
-        status: 200,
-        statusText: "OK",
-        responseText: text
-    };
-};
-
-/* Make a failed XHR response object. */
-MockIo.makeXhrFailureResponse = function(responseText) {
-    var text = responseText || "";
-    return {
-        status: 500,
-        statusText: "Internal Server Error",
-        responseText: text
-    };
-};
-
-Y.namespace("lazr.testing");
-Y.lazr.testing.MockIo = MockIo;
-
-}, '0.1', {});

=== removed file 'lib/lp/app/javascript/tests/lazr/testing.js'
--- lib/lp/app/javascript/tests/lazr/testing.js	2011-06-29 14:56:15 +0000
+++ lib/lp/app/javascript/tests/lazr/testing.js	1970-01-01 00:00:00 +0000
@@ -1,131 +0,0 @@
-/* Copyright (c) 2009, Canonical Ltd. All rights reserved. */
-
-YUI.add("lazr.testing.runner", function(Y) {
-
-/**
- * Testing utilities.
- *
- * @module lazr.testing
- * @namespace lazr
- */
-
-Runner = Y.namespace("lazr.testing.Runner");
-
-Runner.add = function(suite) {
-    if ((typeof jstestdriver === "undefined")) {
-        // If we are not running under JsTestDriver, then
-        // register the suite with Y.Test.Runner and run it.
-        Y.Test.Runner.add(suite);
-    } else {
-        // If ``jstestdriver`` is defined, that means we are
-        // running under JsTestDriver, so instead register each
-        // test case from the suite as a separate TestCase() with
-        // JsTestDriver.
-        var tests = [];
-
-        Y.each(suite.items, function(testCase, idx) {
-            var suiteName = suite.name;
-            var testCaseName = testCase.name;
-
-            var clone = {};
-            for (var prop in testCase){
-                // Clone everything that is not a test method.
-                if (prop.indexOf("test") === -1){
-                    clone[prop] = testCase[prop];
-                }
-            }
-
-            // Now for each test method, create a JsTestDriver
-            // TestCase that wraps a single YUI TestSuite, that wraps
-            // a clone of the original TestCase but with only the
-            // single test method that we are interested in.
-            Y.each(testCase, function(property, name) {
-                if (name.indexOf("test") === 0 &&
-                    Y.Lang.isFunction(property)){
-                    tests.push({"suiteName": suiteName,
-                                "caseName": testCaseName,
-                                "case": clone,
-                                "methodName": name,
-                                "method": property});
-                }
-            });
-        });
-
-        Y.each(tests, function(testObject, i) {
-            testObject = tests[i];
-
-            var fakeTestCase = {
-                "setUp": Y.bind(function(testObject){
-                    var testSuite = new Y.Test.Suite(testObject.suiteName);
-                    var testCase = new Y.Test.Case(testObject['case']);
-                    testCase[testObject.methodName] = testObject.method;
-                    testSuite.add(testCase);
-                    Y.Test.Runner.clear();
-                    Y.Test.Runner.add(testSuite);
-                }, this, testObject),
-                "tearDown": function(){
-                    Y.Test.Runner.clear();
-                }
-            };
-
-            fakeTestCase[testObject.methodName] = Y.bind(function (testObject) {
-                var results = [];
-
-                var onComplete = function (methodName, results, e) {
-                    Y.Test.Runner.unsubscribe("testcasecomplete");
-                    results.push(e.results[methodName]);
-                };
-
-                Y.Test.Runner.subscribe(
-                    "testcasecomplete",
-                    Y.bind(onComplete, this, testObject.methodName, results),
-                    Y.Test.Runner);
-
-                Clock.reset();
-                Y.Test.Runner.run();
-                var i = 100;
-                while (i--) {
-                    if (!Y.Test.Runner.isRunning()){
-                        break;
-                    }
-                    Clock.tick(100);
-                }
-
-                var result = results.pop();
-                if (result === undefined) {
-                    fail("Test did not finish after 100 iterations.");
-                } else {
-                    if (result.result == "fail") {
-                        fail(result.message);
-                    }
-                }
-
-            }, this, testObject);
-
-            // JSLint will complain if the constructur is used without `new`
-            // and if the result of `new` is not used. The TestCase class is
-            // defined globally by jstestdriver and automatically registers
-            // itself, so it is not necessary to return this object.
-            var ignored = new TestCase(
-                testObject.caseName + "." + testObject.methodName,
-                fakeTestCase);
-        });
-    }
-};
-
-Runner.run = function(suite) {
-    Y.on("domready", function() {
-        if ((typeof jstestdriver === "undefined")) {
-            // If we are not running under JsTestDriver, then run all
-            // the registered test suites with Y.Test.Runner.
-            var yconsole = new Y.Console({
-                newestOnTop: false,
-                useBrowserConsole: true
-            });
-            yconsole.render("#log");
-            Y.Test.Runner.run();
-        }
-    });
-};
-
-}, "0.1", {"requires": ["oop", "test", "console"]});

=== modified file 'lib/lp/app/tests/test_yuitests.py'
--- lib/lp/app/tests/test_yuitests.py	2011-06-09 15:37:18 +0000
+++ lib/lp/app/tests/test_yuitests.py	2011-07-07 01:58:19 +0000
@@ -20,5 +20,5 @@
 
 
 def test_suite():
-    app_testing_path = 'lp/app/javascript/tests'
+    app_testing_path = 'lp/app/javascript'
     return build_yui_unittest_suite(app_testing_path, AppYUIUnitTestCase)

=== modified file 'lib/lp/bugs/javascript/tests/test_subscribers_list.html'
--- lib/lp/bugs/javascript/tests/test_subscribers_list.html	2011-07-07 01:58:17 +0000
+++ lib/lp/bugs/javascript/tests/test_subscribers_list.html	2011-07-07 01:58:19 +0000
@@ -33,7 +33,7 @@
     <script type="text/javascript"
       src="../../../app/javascript/lazr/lazr.js"></script>
     <script type="text/javascript"
-      src="../../../app/javascript/picker_patcher.js"></script>
+      src="../../../app/javascript/picker/picker_patcher.js"></script>
     <script type="text/javascript"
       src="../../../app/javascript/picker/picker.js"></script>
     <script type="text/javascript"

=== modified file 'lib/lp/bugs/tests/test_yuitests.py'
--- lib/lp/bugs/tests/test_yuitests.py	2011-06-09 15:37:18 +0000
+++ lib/lp/bugs/tests/test_yuitests.py	2011-07-07 01:58:19 +0000
@@ -20,5 +20,5 @@
 
 
 def test_suite():
-    app_testing_path = 'lp/bugs/javascript/tests'
+    app_testing_path = 'lp/bugs/javascript'
     return build_yui_unittest_suite(app_testing_path, BugsYUIUnitTestCase)

=== modified file 'lib/lp/code/tests/test_yuitests.py'
--- lib/lp/code/tests/test_yuitests.py	2011-06-09 15:37:18 +0000
+++ lib/lp/code/tests/test_yuitests.py	2011-07-07 01:58:19 +0000
@@ -20,5 +20,5 @@
 
 
 def test_suite():
-    app_testing_path = 'lp/code/javascript/tests'
+    app_testing_path = 'lp/code/javascript'
     return build_yui_unittest_suite(app_testing_path, CodeYUIUnitTestCase)

=== modified file 'lib/lp/registry/javascript/tests/test_distroseries.html'
--- lib/lp/registry/javascript/tests/test_distroseries.html	2011-07-07 01:58:17 +0000
+++ lib/lp/registry/javascript/tests/test_distroseries.html	2011-07-07 01:58:19 +0000
@@ -23,7 +23,7 @@
     <script type="text/javascript" src="../../../app/javascript/overlay/overlay.js"></script>
     <script type="text/javascript" src="../../../app/javascript/picker/picker.js"></script>
     <script type="text/javascript" src="../../../app/javascript/picker/person_picker.js"></script>
-    <script type="text/javascript" src="../../../app/javascript/picker_patcher.js"></script>
+    <script type="text/javascript" src="../../../app/javascript/picker/picker_patcher.js"></script>
     <!-- The module under test -->
     <script type="text/javascript" src="../distroseries.js"></script>
     <!-- The test suite -->

=== modified file 'lib/lp/registry/tests/test_yuitests.py'
--- lib/lp/registry/tests/test_yuitests.py	2011-06-09 15:37:18 +0000
+++ lib/lp/registry/tests/test_yuitests.py	2011-07-07 01:58:19 +0000
@@ -20,5 +20,5 @@
 
 
 def test_suite():
-    app_testing_path = 'lp/registry/javascript/tests'
+    app_testing_path = 'lp/registry/javascript'
     return build_yui_unittest_suite(app_testing_path, RegistryYUIUnitTestCase)

=== modified file 'lib/lp/scripts/utilities/js/jsbuild.py'
--- lib/lp/scripts/utilities/js/jsbuild.py	2011-07-07 01:58:17 +0000
+++ lib/lp/scripts/utilities/js/jsbuild.py	2011-07-07 01:58:19 +0000
@@ -1,8 +1,10 @@
 """build.py - Minifies and creates the JS build directory."""
 
 __metaclass__ = type
-__all__ = []
-
+__all__ = [
+    'CSSComboFile',
+    'JSComboFile',
+    ]
 
 import optparse
 import os

=== added directory 'lib/lp/scripts/utilities/js/tests'
=== added file 'lib/lp/scripts/utilities/js/tests/__init__.py'
=== added file 'lib/lp/scripts/utilities/js/tests/test_combo.py'
--- lib/lp/scripts/utilities/js/tests/test_combo.py	1970-01-01 00:00:00 +0000
+++ lib/lp/scripts/utilities/js/tests/test_combo.py	2011-07-07 01:58:19 +0000
@@ -0,0 +1,561 @@
+import os
+import shutil
+import tempfile
+
+from paste.fixture import TestApp
+
+from lp.scripts.utilities.js.combo import parse_url, combine_files, combo_app
+from lp.testing import TestCase
+
+
+class ComboTestBase(TestCase):
+
+    def setUp(self):
+        self.__cleanup_paths = []
+        self.addCleanup(self.__cleanup)
+        super(ComboTestBase, self).setUp()
+
+    def __cleanup(self):
+        for path in self.__cleanup_paths:
+            if os.path.isfile(path):
+                os.unlink(path)
+            elif os.path.isdir(path):
+                shutil.rmtree(path)
+
+    def makeFile(self, content=None, suffix="", prefix="tmp", basename=None,
+                 dirname=None, path=None):
+        """Create a temporary file and return the path to it.
+
+        @param content: Initial content for the file.
+        @param suffix: Suffix to be given to the file's basename.
+        @param prefix: Prefix to be given to the file's basename.
+        @param basename: Full basename for the file.
+        @param dirname: Put file inside this directory.
+
+        The file is removed after the test runs.
+        """
+        if path is not None:
+            self.__cleanup_paths.append(path)
+        elif basename is not None:
+            if dirname is None:
+                dirname = tempfile.mkdtemp()
+                self.__cleanup_paths.append(dirname)
+            path = os.path.join(dirname, basename)
+        else:
+            fd, path = tempfile.mkstemp(suffix, prefix, dirname)
+            self.__cleanup_paths.append(path)
+            os.close(fd)
+            if content is None:
+                os.unlink(path)
+        if content is not None:
+            file = open(path, "w")
+            file.write(content)
+            file.close()
+        return path
+
+    def makeDir(self, suffix="", prefix="tmp", dirname=None, path=None):
+        """Create a temporary directory and return the path to it.
+
+        @param suffix: Suffix to be given to the file's basename.
+        @param prefix: Prefix to be given to the file's basename.
+        @param dirname: Put directory inside this parent directory.
+
+        The directory is removed after the test runs.
+        """
+        if path is not None:
+            os.makedirs(path)
+        else:
+            path = tempfile.mkdtemp(suffix, prefix, dirname)
+        self.__cleanup_paths.append(path)
+        return path
+
+    def makeSampleFile(self, root, fname, content):
+        full = os.path.join(root, fname)
+        parent = os.path.dirname(full)
+        if not os.path.exists(parent):
+            os.makedirs(parent)
+        return self.makeFile(content=content, path=full)
+
+
+class TestCombo(ComboTestBase):
+
+    def test_parse_url_keeps_order(self):
+        """Parsing a combo loader URL returns an ordered list of filenames."""
+        self.assertEquals(
+            parse_url(("http://yui.yahooapis.com/combo?";
+                       "3.0.0/build/yui/yui-min.js&"
+                       "3.0.0/build/oop/oop-min.js&"
+                       "3.0.0/build/event-custom/event-custom-min.js&")),
+            ("3.0.0/build/yui/yui-min.js",
+             "3.0.0/build/oop/oop-min.js",
+             "3.0.0/build/event-custom/event-custom-min.js"))
+
+    def test_combine_files_includes_filename(self):
+        """Combining files should include their relative filename at the top."""
+        test_dir = self.makeDir()
+
+        files = [
+            self.makeSampleFile(
+                test_dir,
+                os.path.join("yui", "yui-min.js"),
+                "** yui-min **"),
+            self.makeSampleFile(
+                test_dir,
+                os.path.join("oop", "oop-min.js"),
+                "** oop-min **"),
+            self.makeSampleFile(
+                test_dir,
+                os.path.join("event-custom", "event-custom-min.js"),
+                "** event-custom-min **"),
+            ]
+
+        expected = "\n".join(("// yui/yui-min.js",
+                              "** yui-min **",
+                              "// oop/oop-min.js",
+                              "** oop-min **",
+                              "// event-custom/event-custom-min.js",
+                              "** event-custom-min **"))
+        self.assertEquals(
+            "".join(combine_files(["yui/yui-min.js",
+                                   "oop/oop-min.js",
+                                   "event-custom/event-custom-min.js"],
+                                  root=test_dir)).strip(),
+            expected)
+
+    def test_combine_css_minifies_and_makes_relative(self):
+        """
+        Combining CSS files minifies and makes URLs in CSS
+        declarations relative to the target path.
+        """
+        test_dir = self.makeDir()
+
+        files = [
+            self.makeSampleFile(
+                test_dir,
+                os.path.join("widget", "assets", "skins", "sam", "widget.css"),
+                """\
+                /* widget skin */
+                .yui-widget {
+                   background: url("img/bg.png");
+                }
+                """),
+            self.makeSampleFile(
+                test_dir,
+                os.path.join("editor", "assets", "skins", "sam", "editor.css"),
+                """\
+                /* editor skin */
+                .yui-editor {
+                   background: url("img/bg.png");
+                }
+                """),
+            ]
+
+        expected = "\n".join(
+            ("/* widget/assets/skins/sam/widget.css */",
+             ".yui-widget{background:url(widget/assets/skins/sam/img/bg.png)}",
+             "/* editor/assets/skins/sam/editor.css */",
+             ".yui-editor{background:url(editor/assets/skins/sam/img/bg.png)}",
+             ))
+        self.assertEquals(
+            "".join(combine_files(["widget/assets/skins/sam/widget.css",
+                                   "editor/assets/skins/sam/editor.css"],
+                                  root=test_dir)).strip(),
+            expected)
+
+    def test_combine_css_leaves_absolute_urls_untouched(self):
+        """
+        Combining CSS files does not touch absolute URLs in
+        declarations.
+        """
+        test_dir = self.makeDir()
+
+        files = [
+            self.makeSampleFile(
+                test_dir,
+                os.path.join("widget", "assets", "skins", "sam", "widget.css"),
+                """\
+                /* widget skin */
+                .yui-widget {
+                   background: url("/static/img/bg.png");
+                }
+                """),
+            self.makeSampleFile(
+                test_dir,
+                os.path.join("editor", "assets", "skins", "sam", "editor.css"),
+                """\
+                /* editor skin */
+                .yui-editor {
+                   background: url("http://foo/static/img/bg.png";);
+                }
+                """),
+            ]
+
+        expected = "\n".join(
+            ("/* widget/assets/skins/sam/widget.css */",
+             ".yui-widget{background:url(/static/img/bg.png)}",
+             "/* editor/assets/skins/sam/editor.css */",
+             ".yui-editor{background:url(http://foo/static/img/bg.png)}",
+             ))
+        self.assertEquals(
+            "".join(combine_files(["widget/assets/skins/sam/widget.css",
+                                   "editor/assets/skins/sam/editor.css"],
+                                  root=test_dir)).strip(),
+            expected)
+
+    def test_combine_css_leaves_data_uris_untouched(self):
+        """
+        Combining CSS files does not touch data uris in
+        declarations.
+        """
+        test_dir = self.makeDir()
+
+        files = [
+            self.makeSampleFile(
+                test_dir,
+                os.path.join("widget", "assets", "skins", "sam", "widget.css"),
+                """\
+                /* widget skin */
+                .yui-widget {
+                background: url("data:image/gif;base64,base64-data");
+                }
+                """),
+            self.makeSampleFile(
+                test_dir,
+                os.path.join("editor", "assets", "skins", "sam", "editor.css"),
+                """\
+                /* editor skin */
+                .yui-editor {
+                   background: url(data:image/gif;base64,base64-data);
+                }
+                """),
+            ]
+
+        expected = "\n".join(
+            ('/* widget/assets/skins/sam/widget.css */',
+             '.yui-widget{background:url("data:image/gif;base64,base64-data")}',
+             '/* editor/assets/skins/sam/editor.css */',
+             '.yui-editor{background:url("data:image/gif;base64,base64-data")}',
+             ))
+        self.assertEquals(
+            "".join(combine_files(["widget/assets/skins/sam/widget.css",
+                                   "editor/assets/skins/sam/editor.css"],
+                                  root=test_dir)).strip(),
+            expected)
+
+    def test_combine_css_disable_minify(self):
+        """
+        It is possible to disable CSS minification altogether, while
+        keeping the URL rewriting behavior.
+        """
+        test_dir = self.makeDir()
+
+        files = [
+            self.makeSampleFile(
+                test_dir,
+                os.path.join("widget", "assets", "skins", "sam", "widget.css"),
+                "\n".join(
+                    ('/* widget skin */',
+                     '.yui-widget {',
+                     '   background: url("img/bg.png");',
+                     '}'))
+                ),
+            self.makeSampleFile(
+                test_dir,
+                os.path.join("editor", "assets", "skins", "sam", "editor.css"),
+                "\n".join(('/* editor skin */',
+                           '.yui-editor {',
+                           '   background: url("img/bg.png");',
+                           '}'))
+                ),
+            ]
+
+        expected = "\n".join(
+            ("/* widget/assets/skins/sam/widget.css */",
+             "/* widget skin */",
+             ".yui-widget {",
+             "   background: url(widget/assets/skins/sam/img/bg.png);",
+             "}",
+             "/* editor/assets/skins/sam/editor.css */",
+             "/* editor skin */",
+             ".yui-editor {",
+             "   background: url(editor/assets/skins/sam/img/bg.png);",
+             "}",
+             ))
+        self.assertEquals(
+            "".join(combine_files(["widget/assets/skins/sam/widget.css",
+                                   "editor/assets/skins/sam/editor.css"],
+                                  root=test_dir, minify_css=False)).strip(),
+            expected)
+
+    def test_combine_css_disable_rewrite_url(self):
+        """
+        It is possible to disable the rewriting of urls in the CSS
+        file.
+        """
+        test_dir = self.makeDir()
+
+        files = [
+            self.makeSampleFile(
+                test_dir,
+                os.path.join("widget", "assets", "skins", "sam", "widget.css"),
+                """\
+                /* widget skin */
+                .yui-widget {
+                   background: url("img/bg.png");
+                }
+                """),
+            self.makeSampleFile(
+                test_dir,
+                os.path.join("editor", "assets", "skins", "sam", "editor.css"),
+                """\
+                /* editor skin */
+                .yui-editor {
+                   background: url("img/bg.png");
+                }
+                """),
+            ]
+
+        expected = "\n".join(
+            ("/* widget/assets/skins/sam/widget.css */",
+             ".yui-widget{background:url(img/bg.png)}",
+             "/* editor/assets/skins/sam/editor.css */",
+             ".yui-editor{background:url(img/bg.png)}",
+             ))
+        self.assertEquals(
+            "".join(combine_files(["widget/assets/skins/sam/widget.css",
+                                   "editor/assets/skins/sam/editor.css"],
+                                  root=test_dir, rewrite_urls=False)).strip(),
+            expected)
+
+    def test_combine_css_disable_rewrite_url_and_minify(self):
+        """
+        It is possible to disable both the rewriting of urls in the
+        CSS file and minification, in which case the files are
+        returned unchanged.
+        """
+        test_dir = self.makeDir()
+
+        files = [
+            self.makeSampleFile(
+                test_dir,
+                os.path.join("widget", "assets", "skins", "sam", "widget.css"),
+                "\n".join(
+                    ('/* widget skin */',
+                     '.yui-widget {',
+                     '   background: url("img/bg.png");',
+                     '}'))
+                ),
+            self.makeSampleFile(
+                test_dir,
+                os.path.join("editor", "assets", "skins", "sam", "editor.css"),
+                "\n".join(('/* editor skin */',
+                           '.yui-editor {',
+                           '   background: url("img/bg.png");',
+                           '}'))
+                ),
+            ]
+
+        expected = "\n".join(
+            ('/* widget/assets/skins/sam/widget.css */',
+             '/* widget skin */',
+             '.yui-widget {',
+             '   background: url("img/bg.png");',
+             '}',
+             '/* editor/assets/skins/sam/editor.css */',
+             '/* editor skin */',
+             '.yui-editor {',
+             '   background: url("img/bg.png");',
+             '}',
+             ))
+        self.assertEquals(
+            "".join(combine_files(["widget/assets/skins/sam/widget.css",
+                                   "editor/assets/skins/sam/editor.css"],
+                                  root=test_dir,
+                                  minify_css=False,
+                                  rewrite_urls=False)).strip(),
+            expected)
+
+    def test_combine_css_adds_custom_prefix(self):
+        """
+        Combining CSS files minifies and makes URLs in CSS
+        declarations relative to the target path. It's also possible
+        to specify an additional prefix for the rewritten URLs.
+        """
+        test_dir = self.makeDir()
+
+        files = [
+            self.makeSampleFile(
+                test_dir,
+                os.path.join("widget", "assets", "skins", "sam", "widget.css"),
+                """\
+                /* widget skin */
+                .yui-widget {
+                   background: url("img/bg.png");
+                }
+                """),
+            self.makeSampleFile(
+                test_dir,
+                os.path.join("editor", "assets", "skins", "sam", "editor.css"),
+                """\
+                /* editor skin */
+                .yui-editor {
+                   background: url("img/bg.png");
+                }
+                """),
+            ]
+
+        expected = "\n".join(
+            ("/* widget/assets/skins/sam/widget.css */",
+             ".yui-widget{background:url(" +
+             "/static/widget/assets/skins/sam/img/bg.png)}",
+             "/* editor/assets/skins/sam/editor.css */",
+             ".yui-editor{background:url(" +
+             "/static/editor/assets/skins/sam/img/bg.png)}",
+             ))
+        self.assertEquals(
+            "".join(combine_files(["widget/assets/skins/sam/widget.css",
+                                   "editor/assets/skins/sam/editor.css"],
+                                  root=test_dir,
+                                  resource_prefix="/static/")).strip(),
+            expected)
+
+    def test_missing_file_is_ignored(self):
+        """If a missing file is requested we should still combine the others."""
+        test_dir = self.makeDir()
+
+        files = [
+            self.makeSampleFile(
+                test_dir,
+                os.path.join("yui", "yui-min.js"),
+                "** yui-min **"),
+            self.makeSampleFile(
+                test_dir,
+                os.path.join("event-custom", "event-custom-min.js"),
+                "** event-custom-min **"),
+            ]
+
+        expected = "\n".join(("// yui/yui-min.js",
+                              "** yui-min **",
+                              "// oop/oop-min.js",
+                              "// [missing]",
+                              "// event-custom/event-custom-min.js",
+                              "** event-custom-min **"))
+        self.assertEquals(
+            "".join(combine_files(["yui/yui-min.js",
+                                   "oop/oop-min.js",
+                                   "event-custom/event-custom-min.js"],
+                                  root=test_dir)).strip(),
+            expected)
+
+    def test_no_parent_hack(self):
+        """If someone tries to hack going up the root, he'll get a miss."""
+        test_dir = self.makeDir()
+        files = [
+            self.makeSampleFile(
+                test_dir,
+                os.path.join("oop", "oop-min.js"),
+                "** oop-min **"),
+            ]
+
+        root = os.path.join(test_dir, "root", "lazr")
+        os.makedirs(root)
+
+        hack = "../../oop/oop-min.js"
+        self.assertTrue(os.path.exists(os.path.join(root, hack)))
+
+        expected = "\n".join(("// ../../oop/oop-min.js",
+                              "// [missing]"))
+        self.assertEquals(
+            "".join(combine_files([hack], root=root)).strip(),
+            expected)
+
+    def test_rewrite_url_normalizes_parent_references(self):
+        """URL references in CSS files get normalized for parent dirs."""
+        test_dir = self.makeDir()
+        files = [
+            self.makeSampleFile(
+                test_dir,
+                os.path.join("yui", "base", "base.css"),
+                ".foo{background-image:url(../../img.png)}"),
+            ]
+
+        expected = "\n".join(("/* yui/base/base.css */",
+                              ".foo{background-image:url(img.png)}"))
+        self.assertEquals(
+            "".join(combine_files(files, root=test_dir)).strip(),
+            expected)
+
+
+    def test_no_absolute_path_hack(self):
+        """If someone tries to fetch an absolute file, he'll get nothing."""
+        test_dir = self.makeDir()
+
+        hack = "/etc/passwd"
+        self.assertTrue(os.path.exists("/etc/passwd"))
+
+        expected = ""
+        self.assertEquals(
+            "".join(combine_files([hack], root=test_dir)).strip(),
+            expected)
+
+
+class TestWSGICombo(ComboTestBase):
+
+    def setUp(self):
+        super(TestWSGICombo, self).setUp()
+        self.root = self.makeDir()
+        self.app = TestApp(combo_app(self.root))
+
+    def test_combo_app_sets_content_type_for_js(self):
+        """The WSGI App should set a proper Content-Type for Javascript."""
+        files = [
+            self.makeSampleFile(
+                self.root,
+                os.path.join("yui", "yui-min.js"),
+                "** yui-min **"),
+            self.makeSampleFile(
+                self.root,
+                os.path.join("oop", "oop-min.js"),
+                "** oop-min **"),
+            self.makeSampleFile(
+                self.root,
+                os.path.join("event-custom", "event-custom-min.js"),
+                "** event-custom-min **"),
+            ]
+
+        expected = "\n".join(("// yui/yui-min.js",
+                              "** yui-min **",
+                              "// oop/oop-min.js",
+                              "** oop-min **",
+                              "// event-custom/event-custom-min.js",
+                              "** event-custom-min **"))
+
+        res = self.app.get("/?" + "&".join(
+            ["yui/yui-min.js",
+             "oop/oop-min.js",
+             "event-custom/event-custom-min.js"]), status=200)
+        self.assertEquals(res.headers, [("Content-Type", "text/javascript")])
+        self.assertEquals(res.body.strip(), expected)
+
+    def test_combo_app_sets_content_type_for_css(self):
+        """The WSGI App should set a proper Content-Type for CSS."""
+        files = [
+            self.makeSampleFile(
+                self.root,
+                os.path.join("widget", "skin", "sam", "widget.css"),
+                "/* widget-skin-sam */"),
+            ]
+
+        expected = "/* widget/skin/sam/widget.css */"
+
+        res = self.app.get("/?" + "&".join(
+            ["widget/skin/sam/widget.css"]), status=200)
+        self.assertEquals(res.headers, [("Content-Type", "text/css")])
+        self.assertEquals(res.body.strip(), expected)
+
+    def test_no_filename_gives_404(self):
+        """If no filename is included, a 404 should be returned."""
+        res = self.app.get("/", status=404)
+        self.assertEquals(res.headers, [("Content-Type", "text/plain")])
+        self.assertEquals(res.body, "Not Found")
+

=== modified file 'lib/lp/soyuz/tests/test_yuitests.py'
--- lib/lp/soyuz/tests/test_yuitests.py	2011-06-09 15:37:18 +0000
+++ lib/lp/soyuz/tests/test_yuitests.py	2011-07-07 01:58:19 +0000
@@ -20,5 +20,5 @@
 
 
 def test_suite():
-    app_testing_path = 'lp/soyuz/javascript/tests'
+    app_testing_path = 'lp/soyuz/javascript'
     return build_yui_unittest_suite(app_testing_path, SoyuzYUIUnitTestCase)

=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py	2011-06-27 23:06:19 +0000
+++ lib/lp/testing/__init__.py	2011-07-07 01:58:19 +0000
@@ -920,16 +920,25 @@
 def build_yui_unittest_suite(app_testing_path, yui_test_class):
     suite = unittest.TestSuite()
     testing_path = os.path.join(config.root, 'lib', app_testing_path)
-    unit_test_names = [
-        file_name for file_name in os.listdir(testing_path)
-        if file_name.startswith('test_') and file_name.endswith('.html')]
-    for unit_test_name in unit_test_names:
-        test_path = os.path.join(app_testing_path, unit_test_name)
+    unit_test_names = _harvest_yui_test_files(testing_path)
+    for unit_test_path in unit_test_names:
         test_case = yui_test_class()
-        test_case.initialize(test_path)
+        test_case.initialize(unit_test_path)
         suite.addTest(test_case)
     return suite
 
+def _harvest_yui_test_files(file_path):
+    file_names = []
+    dirs = []
+    for file_name in os.listdir(file_path):
+        full_name = os.path.join(file_path, file_name)
+        if file_name.startswith('test_') and file_name.endswith('.html'):
+            file_names.append(full_name)
+        elif os.path.isdir(full_name):
+            dirs.append(full_name)
+    for dir_name in dirs:
+        file_names.extend(_harvest_yui_test_files(dir_name))
+    return file_names
 
 class ZopeTestInSubProcess:
     """Run tests in a sub-process, respecting Zope idiosyncrasies.

=== modified file 'lib/lp/translations/tests/test_yuitests.py'
--- lib/lp/translations/tests/test_yuitests.py	2011-06-09 15:37:18 +0000
+++ lib/lp/translations/tests/test_yuitests.py	2011-07-07 01:58:19 +0000
@@ -20,7 +20,7 @@
 
 
 def test_suite():
-    app_testing_path = 'lp/translations/javascript/tests'
+    app_testing_path = 'lp/translations/javascript'
     return build_yui_unittest_suite(
             app_testing_path,
             TranslationsYUIUnitTestCase)