← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~henninge/launchpad/bug-824435-failure-reporting-2 into lp:launchpad

 

Henning Eggers has proposed merging lp:~henninge/launchpad/bug-824435-failure-reporting-2 into lp:launchpad with lp:~henninge/launchpad/bug-824435-failure-reporting as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~henninge/launchpad/bug-824435-failure-reporting-2/+merge/72546

= Summary =

Since the Thunderdome we had two Javascript modules to mock Y.io in
tests: lazr.testing.mockio and lp.testing.iorecorder. They both were
pretty similar in their approach so merging the two was not only
necessary but also fairly easy. The new module is lp.testing.mockio
and contains the best of both previous modules.

I was surprised to find that these were only used in so few places but
that made transitioning the old code to the new mockio a quick task,
too.

I noticed this because I had started to extend IORecorder's
functionality for the orginial fixing of the error reporting in the
requestbuild_overlay code. So this branch is still part of that
series.

== Proposed fix ==

Merge iorecorder.js code into mockio.js and call it lp.testing.mockio.

Add tests for the new module, the old code was completely untested.

Also clean up the interface and provide some helpers to make
instrumenting the code under test less intrusive.

== Pre-implementation notes ==

I talked to Aaron and Deryck about this but the need for this merge
was really too blatantly obvious.

== Implementation details ==

There is a test in test_picker_patcher.js that was improved (i.e.
shortened) a great deal because the new MockIo learned from IORecorder
how to handle multiple requests.

The mockio.js file looks as if it is completely replace and although
that is almost true, I also added some indentation which changed
most lines in the file.

Looking at test_success_helper__status_override and its evil twin
test_failure_helper__status_override I think it would be better to
raise an exception when the status code does is not correct but I
don't know how to test for assertions being raised.

While iorecorder passed the "io" function to the instrumented function
under test, mockio passes a object that has an "io" method. I picked
the latter approach because it mirrors "Y.io" more closely and does
away with the need to Y.bind the io function.

I exposed MockHttpResponse in the module, like iorecorder did because
there is a test that uses just that.

Any thing else that needs explainin, just ask. ;-)

== Tests ==

firefox lib/lp/app/javascript/formoverlay/tests/test_formoverlay.html
firefox lib/lp/app/javascript/picker/tests/test_picker_patcher.html
firefox lib/lp/app/javascript/testing/tests/test_mockio.html
firefox lib/lp/app/javascript/tests/test_lp_client.html
firefox lib/lp/code/javascript/tests/test_requestbuild_overlay.html

== Demo and Q/A ==

No QA, only test code touched.

= Launchpad lint =

As the diff is already big enough, I will not fix the remaining lint
in requestbuild_overlay.js. That file will be worked on in the
follow-up branch anyway.

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/app/javascript/tests/test_lp_client.js
  lib/lp/code/javascript/tests/test_requestbuild_overlay.html
  lib/lp/app/javascript/client.js
  lib/lp/app/javascript/tests/test_lp_client.html
  lib/lp/app/javascript/formoverlay/tests/test_formoverlay.js
  lib/lp/app/javascript/testing/tests/test_mockio.js
  lib/lp/code/javascript/requestbuild_overlay.js
  lib/lp/app/javascript/testing/tests/test_mockio.html
  lib/lp/code/javascript/tests/test_requestbuild_overlay.js
  lib/lp/app/javascript/picker/tests/test_picker_patcher.js
  lib/lp/app/javascript/testing/mockio.js
 
./lib/lp/code/javascript/requestbuild_overlay.js
      40: Expected '===' and instead saw '=='.
      45: Expected '!==' and instead saw '!='.
      70: Expected '===' and instead saw '=='.
      72: ['name'] is better written in dot notation.
     111: Expected '===' and instead saw '=='.
     112: Expected '===' and instead saw '=='.
     113: Expected '{' and instead saw 'header'.
     116: Expected '{' and instead saw 'header'.
     119: Expected '!==' and instead saw '!='.
     121: Expected '===' and instead saw '=='.
     224: Expected '!==' and instead saw '!='.
     225: Expected '{' and instead saw 'error_msg'.
     236: Use the array literal notation [].
     258: Expected '{' and instead saw 'return'.
     272: Expected ';' and instead saw 'if'.
     273: Expected '===' and instead saw '=='.
     287: Expected '{' and instead saw 'return'.
     321: ['builds'] is better written in dot notation.
     323: ['already_pending'] is better written in dot notation.
     325: ['errors'] is better written in dot notation.
     328: Expected '!==' and instead saw '!='.
     340: Expected '!==' and instead saw '!='.
     341: Expected '!==' and instead saw '!='.
     342: Expected '{' and instead saw 'error_header_text'.
     344: Expected '{' and instead saw 'error_header_text'.
     347: Move 'var' declarations to the top of the function.
     347: Stopping.  (67% scanned).
       0: JSLINT had a fatal error.
     161: Line has trailing whitespace.
-- 
https://code.launchpad.net/~henninge/launchpad/bug-824435-failure-reporting-2/+merge/72546
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~henninge/launchpad/bug-824435-failure-reporting-2 into lp:launchpad.
=== modified file 'lib/lp/app/javascript/client.js'
--- lib/lp/app/javascript/client.js	2011-07-15 18:03:04 +0000
+++ lib/lp/app/javascript/client.js	2011-08-23 10:50:30 +0000
@@ -200,11 +200,8 @@
         on: on,
         'arguments': [entry.lp_client, url, old_on_success, false]
     };
-    var io = config.io;
-    if (!Y.Lang.isValue(io)) {
-        io = Y.io;
-    }
-    io(url, y_config);
+    var io_provider = Y.lp.testing.mockio.io_provider_config(config);
+    io_provider.io(url, y_config);
 };
 
 
@@ -1131,4 +1128,6 @@
 Y.namespace('lp.client.plugins');
 Y.lp.client.plugins.PATCHPlugin = PATCHPlugin;
 
-  }, "0.1", {"requires": ["plugin", "dump", "lazr.editor", "lp.client"]});
+  }, "0.1", {"requires": [
+                  "plugin", "dump", "lazr.editor", "lp.testing.mockio",
+                  "lp.client"]});

=== modified file 'lib/lp/app/javascript/formoverlay/tests/test_formoverlay.js'
--- lib/lp/app/javascript/formoverlay/tests/test_formoverlay.js	2011-08-19 15:28:07 +0000
+++ lib/lp/app/javascript/formoverlay/tests/test_formoverlay.js	2011-08-23 10:50:30 +0000
@@ -1,8 +1,8 @@
-/* Copyright (c) 2008, Canonical Ltd. All rights reserved. */
+/* Copyright (c) 2008-2011, Canonical Ltd. All rights reserved. */
 
 YUI().use('lp.testing.runner', 'test', 'dump', 'console', 'node',
           'lazr.formoverlay', 'event', 'event-simulate',
-          'lazr.testing.mockio', function(Y) {
+          'lp.testing.mockio', function(Y) {
 
 var Assert = Y.Assert;  // For easy access to isTrue(), etc.
 
@@ -442,16 +442,14 @@
         var form_overlay = make_form_overlay({
             headerContent: 'Form for testing'
             });
-        var mock_io = new Y.lazr.testing.MockIo();
+        var mock_io = new Y.lp.testing.mockio.MockIo();
         form_overlay.loadFormContentAndRender(
             'http://example.com/form', mock_io);
 
         // loadFormContentAndRender calls .io() to issue an XHR. Simulate a
         // successful response, to make sure that the form content gets
         // set and rendered.
-        var response = Y.lazr.testing.MockIo.makeXhrSuccessResponse(
-            external_form_content);
-        mock_io.simulateXhr(response, false);
+        mock_io.success({responseText: external_form_content});
 
         Assert.areEqual(
             external_form_content, form_overlay.get('form_content'),
@@ -472,18 +470,17 @@
         var form_overlay = make_form_overlay({
             headerContent: 'Form for testing'
             });
-        var mock_io = new Y.lazr.testing.MockIo();
+        var mock_io = new Y.lp.testing.mockio.MockIo();
         form_overlay.loadFormContentAndRender(
             'http://example.com/form', mock_io);
 
         // loadFormContentAndRender calls .io() to issue an XHR. Simulate a
         // failed response, to make sure that the error message gets set
         // and rendered.
-        var response = Y.lazr.testing.MockIo.makeXhrFailureResponse(
-            'failure');
-        mock_io.simulateXhr(response, true);
+        mock_io.failure();
 
-        var error_message = "Sorry, an error occurred while loading the form.";
+        var error_message = "Sorry, an error occurred " +
+                            "while loading the form.";
         Assert.areEqual(
             error_message, form_overlay.get('form_content'),
             "Failure to set form content.");
@@ -505,7 +502,7 @@
             headerContent: 'Form for testing',
             form_submit_callback: submit_callback
             });
-        var mock_io = new Y.lazr.testing.MockIo();
+        var mock_io = new Y.lp.testing.mockio.MockIo();
         form_overlay.loadFormContentAndRender(
             'http://example.com/form', mock_io);
 
@@ -513,9 +510,7 @@
         // successful response, to make sure that the submit button get
         // hooked up to the form_submit_call.
         var external_form_content = '<div id="loaded-content"></div>';
-        var response = Y.lazr.testing.MockIo.makeXhrSuccessResponse(
-            external_form_content);
-        mock_io.simulateXhr(response, false);
+        mock_io.success({responseText: external_form_content});
         simulate(
             form_overlay.form_node,
             "input[type=submit]",

=== modified file 'lib/lp/app/javascript/picker/tests/test_picker_patcher.js'
--- lib/lp/app/javascript/picker/tests/test_picker_patcher.js	2011-08-22 12:13:22 +0000
+++ lib/lp/app/javascript/picker/tests/test_picker_patcher.js	2011-08-23 10:50:30 +0000
@@ -3,7 +3,7 @@
 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',
-        'lazr.testing.mockio',
+        'lp.testing.mockio',
         function(Y) {
 
 var Assert = Y.Assert;
@@ -364,7 +364,7 @@
     },
 
     create_picker: function() {
-        this.mock_io = new Y.lazr.testing.MockIo();
+        this.mock_io = new Y.lp.testing.mockio.MockIo();
         this.picker = Y.lp.app.picker.addPickerPatcher(
             "Foo",
             "foo/bar",
@@ -373,24 +373,17 @@
             {yio: this.mock_io});
     },
 
-    make_error_response: function(status, oops) {
-        if (oops === undefined) {
-            oops = null;
-        }
-        return {
-            status: status,
-            getResponseHeader: function(header) {
-                if (header === 'X-Lazr-OopsId') {
-                    return oops;
-                }
-            }
-        };
+    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.simulateXhr(this.make_error_response(500, 'OOPS'), true);
+        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. " +
@@ -401,7 +394,8 @@
     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.simulateXhr(this.make_error_response(503, 'OOPS'), true);
+        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)",
@@ -411,7 +405,7 @@
     test_other_error: function() {
         // Any other type of error just displays a generic failure
         // message, with no OOPS ID.
-        this.mock_io.simulateXhr(this.make_error_response(400), true);
+        this.mock_io.failure({status: 400});
         Assert.areEqual(
             "Sorry, something went wrong with your search.",
             this.picker.get('error'));
@@ -451,25 +445,20 @@
         // 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.lazr.testing.MockIo();
+        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);
-        // We have to stash away the mock IO's on-success handler because
-        // it'll get clobbered by the second searc if we don't.
-        automated_success = mock_io.cfg.on.success;
         // Then the user initiates their own search.
         picker.fire('search', 'test');
-        // Now to get ahold of the user-initiated search's on-success.
-        user_success = mock_io.cfg.on.success;
-        // Now, if the automated search returns...
-        mock_io.cfg.on.success = automated_success;
-        mock_io.simulateXhr(this.make_response(200, null, '{"entries": 1}'));
+        // 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'));
-        // But if the user's search returns, the results are kept.
-        mock_io.cfg.on.success = user_success;
-        mock_io.simulateXhr(this.make_response(200, null, '{"entries": 2}'));
+        // Respond to the user request.
+        mock_io.requests[1].respond({responseText: '{"entries": 2}'});
         Assert.areEqual(2, picker.get('results'));
         cleanup_widget(picker);
     },
@@ -478,11 +467,11 @@
         // 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.lazr.testing.MockIo();
+        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.simulateXhr(this.make_response(500, 'OOPS'), true);
+        mock_io.failure();
         Assert.areEqual(null, picker.get('error'));
         cleanup_widget(picker);
     }

=== removed file 'lib/lp/app/javascript/testing/iorecorder.js'
--- lib/lp/app/javascript/testing/iorecorder.js	2011-08-19 19:32:11 +0000
+++ lib/lp/app/javascript/testing/iorecorder.js	1970-01-01 00:00:00 +0000
@@ -1,60 +0,0 @@
-/* Copyright (c) 2011, Canonical Ltd. All rights reserved. */
-
-YUI.add('lp.testing.iorecorder', function(Y) {
-    var namespace = Y.namespace("lp.testing.iorecorder");
-
-    function MockHttpResponse (status) {
-        this.status = status;
-        this.responseText = '[]';
-        this.responseHeaders = {};
-    }
-
-    MockHttpResponse.prototype = {
-        setResponseHeader: function (header, value) {
-            this.responseHeaders[header] = value;
-        },
-
-        getResponseHeader: function(header) {
-            return this.responseHeaders[header];
-        }
-    };
-    namespace.MockHttpResponse = MockHttpResponse;
-
-
-    function IORecorderRequest(url, config){
-        this.url = url;
-        this.config = config;
-        this.response = null;
-    }
-
-
-    IORecorderRequest.prototype.respond = function(status, value, headers){
-        this.response = new MockHttpResponse(status);
-        this.response.setResponseHeader(
-            'Content-Type', headers['Content-Type']);
-        this.response.responseText = value;
-        var callback;
-        if (status === 200) {
-            callback = this.config.on.success;
-        } else {
-            callback = this.config.on.failure;
-        }
-        callback(null, this.response, this.config['arguments']);
-    };
-
-
-    IORecorderRequest.prototype.success = function(value, headers){
-        this.respond(200, value, headers);
-    };
-
-
-    function IORecorder(){
-        this.requests = [];
-    }
-
-
-    IORecorder.prototype.do_io = function(url, config){
-        this.requests.push(new IORecorderRequest(url, config));
-    };
-    namespace.IORecorder = IORecorder;
-}, '0.1', {});

=== modified file 'lib/lp/app/javascript/testing/mockio.js'
--- lib/lp/app/javascript/testing/mockio.js	2011-07-06 05:13:30 +0000
+++ lib/lp/app/javascript/testing/mockio.js	2011-08-23 10:50:30 +0000
@@ -1,69 +1,149 @@
-/* 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;
+/* Copyright (c) 2009-2011, Canonical Ltd. All rights reserved. */
+
+YUI.add('lp.testing.mockio', function(Y) {
+    /**
+     * A utility module for use in YUI unit-tests with a helper for
+     * mocking Y.io.
+     *
+     * @module lp.testing.mockio
+     */
+    var namespace =  Y.namespace("lp.testing.mockio");
+
+    var MockHttpResponse = function(config) {
+        if (config === undefined) {
+            config = {};
+        }
+        if (config.status !== undefined) {
+            this.status = config.status;
+        } else {
+            this.status = 200;
+        }
+        if (config.statusText !== undefined) {
+            this.statusText = config.statusText;
+        } else {
+            if (this.isFailure()) {
+                this.statusText = "Internal Server Error";
+            } else {
+                this.statusText = "OK";
+            }
+        }
+        if (config.responseText !== undefined) {
+            this.responseText = config.responseText;
+        } else {
+            this.responseText = '[]';
+        }
+        if (config.responseHeaders !== undefined) {
+            this.responseHeaders = config.responseHeaders;
+        } else {
+            this.responseHeaders = {};
+        }
+    };
+
+    MockHttpResponse.prototype = {
+        isFailure: function () {
+            return this.status >= 400;
+        },
+
+        setResponseHeader: function (header, value) {
+            this.responseHeaders[header] = value;
+        },
+
+        getResponseHeader: function(header) {
+            return this.responseHeaders[header];
+        }
+    };
+    namespace.MockHttpResponse = MockHttpResponse;
+
+    function MockHttpRequest(url, config){
+        this.url = url;
+        this.config = config;
+        this.response = null;
+    }
+
+    /* Simulate the Xhr request/response cycle. */
+    MockHttpRequest.prototype.respond = function(response_config) {
+        var context = this.config.context || Y,
+            args = this.config['arguments'] || [],
+            tId = 'mockTId',
+            response = this.response = new MockHttpResponse(response_config);
+
+        // See the Y.io utility documentation for the signatures.
+        if (this.config.on.start !== undefined) {
+            this.config.on.start.call(context, tId, args);
+        }
+        if (this.config.on.complete !== undefined) {
+            this.config.on.complete.call(context, tId, response, args);
+        }
+        if (this.config.on.success !== undefined && !response.isFailure()) {
+            this.config.on.success.call(context, tId, response, args);
+        }
+        if (this.config.on.failure !== undefined && response.isFailure()) {
+            this.config.on.failure.call(context, tId, response, args);
+        }
+    };
+
+    namespace.MockHttpRequest = MockHttpRequest;
+
+    var MockIo = function() {
+        this.requests = [];
+        this.last_request = null;
+    };
+
+    /* Save the Y.io() arguments. */
+    MockIo.prototype.io = function(url, config) {
+        this.last_request = new MockHttpRequest(url, config);
+        this.requests.push(this.last_request);
+        return this;  // Usually this isn't used, except for logging.
+    };
+
+    /* Call respond method on last_request. */
+    MockIo.prototype.respond = function(response_config) {
+        this.last_request.respond(response_config);
+    };
+
+    /* Call respond method with successful values. */
+    MockIo.prototype.success = function(config) {
+        if (config === undefined) {
+            config = {};
+        }
+        if (config.status === undefined || config.status >= 400) {
+            config.status = 200;
+        }
+        config.statusText = 'OK';
+        this.respond(config);
+    };
+
+    /* Call respond method with failed values. */
+    MockIo.prototype.failure = function(config) {
+        if (config === undefined) {
+            config = {};
+        }
+        if (config.status === undefined || config.status < 400) {
+            config.status = 500;
+        }
+        config.statusText = 'Internal Server Error';
+        this.respond(config);
+    };
+
+    namespace.MockIo = MockIo;
+
+    /* Helper to select the io_provider. */
+    namespace.io_provider = function(mockio) {
+        if (mockio === undefined) {
+            return Y;
+        }
+        return mockio;
+    };
+
+    /* Helper to select the io_provider from a config. */
+    namespace.io_provider_config = function(config, key) {
+        if (key === undefined) {
+            key = 'io_provider';
+        }
+        if (config === undefined || config[key] === undefined) {
+            return Y;
+        }
+        return config[key];
+    };
 
 }, '0.1', {});

=== added directory 'lib/lp/app/javascript/testing/tests'
=== added file 'lib/lp/app/javascript/testing/tests/test_mockio.html'
--- lib/lp/app/javascript/testing/tests/test_mockio.html	1970-01-01 00:00:00 +0000
+++ lib/lp/app/javascript/testing/tests/test_mockio.html	2011-08-23 10:50:30 +0000
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd";>
+<html>
+  <head>
+  <title>Launchpad lp.testing.mockio module</title>
+
+  <!-- YUI and test setup -->
+  <script type="text/javascript"
+          src="../../../../../canonical/launchpad/icing/yui/yui/yui.js">
+  </script>
+  <link rel="stylesheet" href="../test.css" />
+  <script type="text/javascript" src="../testrunner.js"></script>
+
+  <!-- The module under test -->
+  <script type="text/javascript" src="../mockio.js"></script>
+
+  <!-- The test suite -->
+  <script type="text/javascript" src="test_mockio.js"></script>
+</head>
+<body class="yui3-skin-sam">
+</body>
+</html>

=== added file 'lib/lp/app/javascript/testing/tests/test_mockio.js'
--- lib/lp/app/javascript/testing/tests/test_mockio.js	1970-01-01 00:00:00 +0000
+++ lib/lp/app/javascript/testing/tests/test_mockio.js	2011-08-23 10:50:30 +0000
@@ -0,0 +1,242 @@
+/* Copyright (c) 2011, Canonical Ltd. All rights reserved. */
+
+YUI().use('test', 'console', 'node-event-simulate',
+          'lp.testing.mockio', 'lp.testing.runner', function(Y) {
+
+var suite = new Y.Test.Suite("lp.testing.mockio Tests");
+
+var module = Y.lp.testing.mockio;
+
+var make_call_recorder = function() {
+    var recorder;
+    recorder = function() {
+        recorder.call_count += 1;
+        recorder.args = arguments;
+    };
+    recorder.call_count = 0;
+    recorder.args = null;
+    return recorder;
+};
+
+suite.add(new Y.Test.Case({
+    name: "lp.testing.mokio.MockIo",
+
+    test_url: "https://launchpad.dev/test/url";,
+
+    setUp: function() {
+        // Initialize call_count on recorders.
+        this.test_config = {
+            on: {
+                start: make_call_recorder(),
+                complete: make_call_recorder(),
+                success: make_call_recorder(),
+                failure: make_call_recorder()
+            },
+            context:  {marker: "context"},
+            'arguments': ["arguments"]
+        };
+    },
+
+    _make_mockio: function() {
+        var mockio = new module.MockIo();
+        mockio.io(this.test_url, this.test_config);
+        return mockio;
+    },
+
+    test_respond_success: function() {
+        // The success handler is called on success.
+        var mockio = this._make_mockio();
+        mockio.respond({status: 200});
+        Y.Assert.areEqual(1, this.test_config.on.start.call_count);
+        Y.Assert.areEqual(1, this.test_config.on.complete.call_count);
+        Y.Assert.areEqual(1, this.test_config.on.success.call_count);
+        Y.Assert.areEqual(0, this.test_config.on.failure.call_count);
+    },
+
+    test_respond_failure: function() {
+        // The failure handler is called on failure.
+        var mockio = this._make_mockio();
+        mockio.respond({status: 500});
+        Y.Assert.areEqual(1, this.test_config.on.start.call_count);
+        Y.Assert.areEqual(1, this.test_config.on.complete.call_count);
+        Y.Assert.areEqual(0, this.test_config.on.success.call_count);
+        Y.Assert.areEqual(1, this.test_config.on.failure.call_count);
+    },
+
+    test_multiple_requests: function() {
+        // Multiple requests are stored.
+        var mockio = new module.MockIo();
+        mockio.io(this.test_url, this.test_config);
+        mockio.io(this.test_url, this.test_config);
+        Y.Assert.areEqual(2, mockio.requests.length);
+    },
+
+    test_last_request: function() {
+        // The last request is available through last_request.
+        var mockio = new module.MockIo();
+        mockio.io("Request 1", this.test_config);
+        mockio.io("Request 2", this.test_config);
+        Y.Assert.areEqual("Request 2", mockio.last_request.url);
+    },
+
+    test_status: function() {
+        // The status is passed to the handler.
+        var mockio = this._make_mockio();
+        var expected_status = 503;
+        mockio.respond({status: expected_status});
+        Y.Assert.areEqual(
+            expected_status, this.test_config.on.failure.args[1].status);
+    },
+
+    test_statusText: function() {
+        // The statusText is passed to the handler.
+        var mockio = this._make_mockio();
+        var expected_status_text = "All is well";
+        mockio.respond({statusText: expected_status_text});
+        Y.Assert.areEqual(
+            expected_status_text,
+            this.test_config.on.success.args[1].statusText);
+    },
+
+    test_responseText: function() {
+        // The responseText is passed to the handler.
+        var mockio = this._make_mockio();
+        var expected_response_text = "myresponse";
+        mockio.respond({responseText: expected_response_text});
+        Y.Assert.areEqual(
+            expected_response_text,
+            this.test_config.on.success.args[1].responseText);
+    },
+
+    test_responseHeader: function() {
+        // A response header is passed to the handler.
+        var mockio = this._make_mockio();
+        var response = new Y.lp.testing.mockio.MockHttpResponse();
+        var expected_header_key = "X-My-Header",
+            expected_header_val = "MyHeaderValue",
+            response_headers = {};
+        response.setResponseHeader(expected_header_key,  expected_header_val);
+        mockio.respond(response);
+        var headers = this.test_config.on.success.args[1].responseHeaders;
+        Y.Assert.areEqual(expected_header_val, headers[expected_header_key]);
+    },
+
+    test_success_helper: function() {
+        // The success helper creates a successful response.
+        var mockio = this._make_mockio(),
+            response_text = "Success!";
+        mockio.success({responseText: response_text});
+        Y.Assert.areEqual(1, this.test_config.on.success.call_count);
+        Y.Assert.areEqual(0, this.test_config.on.failure.call_count);
+        Y.Assert.areEqual(
+            response_text, mockio.last_request.response.responseText);
+    },
+
+    test_success_helper__own_status: function() {
+        // The failure can define its own non-4xx or non-5xx status.
+        var mockio = this._make_mockio(),
+            status = 302;
+        mockio.success({status: status});
+        Y.Assert.areEqual(1, this.test_config.on.success.call_count);
+        Y.Assert.areEqual(0, this.test_config.on.failure.call_count);
+        Y.Assert.areEqual(status, mockio.last_request.response.status);
+    },
+
+    test_success_helper__status_override: function() {
+        // A status that is 4xx or 5xx is overridden to be 200.
+        // This is to guard against foot shooting.
+        var mockio = this._make_mockio(),
+            own_status = 500,
+            real_status = 200;
+        mockio.success({status: own_status});
+        Y.Assert.areEqual(1, this.test_config.on.success.call_count);
+        Y.Assert.areEqual(0, this.test_config.on.failure.call_count);
+        Y.Assert.areEqual(real_status, mockio.last_request.response.status);
+    },
+
+    test_failure_helper: function() {
+        // The failure helper creates a failed response.
+        var mockio = this._make_mockio(),
+            response_text = "Failure!";
+        mockio.failure({responseText: response_text});
+        Y.Assert.areEqual(0, this.test_config.on.success.call_count);
+        Y.Assert.areEqual(1, this.test_config.on.failure.call_count);
+        Y.Assert.areEqual(
+            response_text, mockio.last_request.response.responseText);
+    },
+
+    test_failure_helper__own_status: function() {
+        // The failure can define its own 4xx or 5xx status.
+        var mockio = this._make_mockio(),
+            status = 404;
+        mockio.failure({status: status});
+        Y.Assert.areEqual(0, this.test_config.on.success.call_count);
+        Y.Assert.areEqual(1, this.test_config.on.failure.call_count);
+        Y.Assert.areEqual(status, mockio.last_request.response.status);
+    },
+
+    test_failure_helper__status_override: function() {
+        // A status that is not 4xx or 5xx is overridden to be 500.
+        // This is to guard against foot shooting.
+        var mockio = this._make_mockio(),
+            own_status = 200,
+            real_status = 500;
+        mockio.failure({status: own_status});
+        Y.Assert.areEqual(0, this.test_config.on.success.call_count);
+        Y.Assert.areEqual(1, this.test_config.on.failure.call_count);
+        Y.Assert.areEqual(real_status, mockio.last_request.response.status);
+    }
+}));
+
+suite.add(new Y.Test.Case({
+    name: "lp.testing.mokio.io_provider",
+
+    setUp: function() {
+    },
+
+    test_io_provider__default: function() {
+        var mockio,
+            io_provider = module.io_provider(mockio);
+        Y.Assert.areSame(Y, io_provider);
+    },
+
+    test_io_provider__mockio: function() {
+        // If mockio is provided, it is picked as the io_provider.
+        var mockio = new module.MockIo(),
+            io_provider = module.io_provider(mockio);
+        Y.Assert.areSame(mockio, io_provider);
+    },
+
+    test_io_provider_config__default: function() {
+        // If no io_provider is configured, Y is the io_provider.
+        var io_provider = module.io_provider_config({});
+        Y.Assert.areSame(Y, io_provider);
+    },
+
+    test_io_provider_config__default_undefined: function() {
+        // If no configuration is provided, Y is the io_provider.
+        var io_provider = module.io_provider_config();
+        Y.Assert.areSame(Y, io_provider);
+    },
+
+    test_io_provider_config__mockio: function() {
+        // If io_provider is configured,  it is picked as the io_provider.
+        var mockio = new module.MockIo(),
+            io_provider = module.io_provider_config(
+                {io_provider: mockio});
+        Y.Assert.areSame(mockio, io_provider);
+    },
+
+    test_io_provider_config__different_key: function() {
+        // The io_provider can be stored with a different key.
+        var mockio = new module.MockIo(),
+            io_provider = module.io_provider_config(
+                {my_io: mockio}, 'my_io');
+        Y.Assert.areSame(mockio, io_provider);
+    }
+}));
+
+
+Y.lp.testing.Runner.run(suite);
+
+});

=== modified file 'lib/lp/app/javascript/tests/test_lp_client.html'
--- lib/lp/app/javascript/tests/test_lp_client.html	2011-07-15 20:01:48 +0000
+++ lib/lp/app/javascript/tests/test_lp_client.html	2011-08-23 10:50:30 +0000
@@ -11,7 +11,7 @@
   <script type="text/javascript"
           src="../../../app/javascript/testing/testrunner.js"></script>
   <script type="text/javascript"
-          src="../../../app/javascript/testing/iorecorder.js"></script>
+          src="../../../app/javascript/testing/mockio.js"></script>
 
   <!-- The module under test -->
   <script type="text/javascript" src="../lp.js"></script>

=== modified file 'lib/lp/app/javascript/tests/test_lp_client.js'
--- lib/lp/app/javascript/tests/test_lp_client.js	2011-07-28 15:25:04 +0000
+++ lib/lp/app/javascript/tests/test_lp_client.js	2011-08-23 10:50:30 +0000
@@ -1,6 +1,6 @@
 /* Copyright (c) 2011, Canonical Ltd. All rights reserved. */
 
-YUI().use('lp.testing.runner', 'lp.testing.iorecorder', 'test', 'console',
+YUI().use('lp.testing.runner', 'lp.testing.mockio', 'test', 'console',
           'lp.client', 'escape', function(Y) {
 
 var Assert = Y.Assert;  // For easy access to isTrue(), etc.
@@ -74,11 +74,11 @@
         Assert.areEqual(expected, actual);
     },
     test_load_model: function(){
-        var recorder = new Y.lp.testing.iorecorder.IORecorder();
-        Assert.areEqual(0, recorder.requests.length);
+        var mockio = new Y.lp.testing.mockio.MockIo();
+        Assert.areEqual(0, mockio.requests.length);
         var mylist = [];
         var config = {
-            io: Y.bind(recorder.do_io, recorder),
+            io_provider: mockio,
             on: {
                 success: Y.bind(mylist.push, mylist)
             }
@@ -87,11 +87,13 @@
         var client = new Y.lp.client.Launchpad();
         var context = new Y.lp.client.Entry(client, entry_repr, null);
         Y.lp.client.load_model(context, '+myview', config);
-        var request = recorder.requests[0];
-        Assert.areEqual('/context/+myview/++model++', request.url);
-        request.success(
-            '{"boolean": true, "entry": {"resource_type_link": "foo"}}',
-            {'Content-Type': 'application/json'});
+        Assert.areEqual(
+            '/context/+myview/++model++', mockio.last_request.url);
+        mockio.success({
+            responseText:
+                '{"boolean": true, "entry": {"resource_type_link": "foo"}}',
+            responseHeaders: {'Content-Type': 'application/json'}
+        });
         var result = mylist[0];
         Assert.areSame(true, result.boolean);
         Assert.isInstanceOf(Y.lp.client.Entry, result.entry);
@@ -125,8 +127,8 @@
         Y.Assert.areNotSame(foo, bar);
     },
     test_wrap_resource_creates_mapping: function() {
-        // wrap_resource creates new mappings, rather than reusing the existing
-        // one.
+        // wrap_resource creates new mappings, rather than reusing the
+        // existing one.
         var foo = {a: 'b'};
         var bar = new Y.lp.client.Launchpad().wrap_resource(null, foo);
         Y.Assert.areNotSame(foo, bar);
@@ -294,7 +296,7 @@
     setUp: function() {
         this.client = new Y.lp.client.Launchpad();
         this.args=[this.client, null, this._on_success, false];
-        this.response = new Y.lp.testing.iorecorder.MockHttpResponse();
+        this.response = new Y.lp.testing.mockio.MockHttpResponse();
         this.response.setResponseHeader('Content-Type', 'application/json');
     },
 

=== modified file 'lib/lp/code/javascript/requestbuild_overlay.js'
--- lib/lp/code/javascript/requestbuild_overlay.js	2011-08-19 20:43:47 +0000
+++ lib/lp/code/javascript/requestbuild_overlay.js	2011-08-23 10:50:30 +0000
@@ -209,11 +209,8 @@
             },
             data: qs
         };
-        var io = Y.io;
-        if ( config !== undefined && Y.Lang.isValue(config.io)) {
-            io = config.io;
-        }
-        io(submit_url, y_config);
+        var io_provider = Y.lp.testing.mockio.io_provider_config(config);
+        io_provider.io(submit_url, y_config);
     });
 
     // Wire up the processing hooks
@@ -511,5 +508,5 @@
 }
 }, "0.1", {"requires": [
     "dom", "node", "escape", "io-base", "lp.anim", "lazr.formoverlay",
-    "lp.client"
+    "lp.client", "lp.testing.mockio"
     ]});

=== modified file 'lib/lp/code/javascript/tests/test_requestbuild_overlay.html'
--- lib/lp/code/javascript/tests/test_requestbuild_overlay.html	2011-08-19 19:21:10 +0000
+++ lib/lp/code/javascript/tests/test_requestbuild_overlay.html	2011-08-23 10:50:30 +0000
@@ -18,7 +18,7 @@
   <script type="text/javascript"
           src="../../../app/javascript/testing/testrunner.js"></script>
   <script type="text/javascript"
-          src="../../../app/javascript/testing/iorecorder.js"></script>
+          src="../../../app/javascript/testing/mockio.js"></script>
 
  <script type="text/javascript"
           src="../../../app/javascript/client.js"></script>

=== modified file 'lib/lp/code/javascript/tests/test_requestbuild_overlay.js'
--- lib/lp/code/javascript/tests/test_requestbuild_overlay.js	2011-08-19 20:43:47 +0000
+++ lib/lp/code/javascript/tests/test_requestbuild_overlay.js	2011-08-23 10:50:30 +0000
@@ -1,7 +1,7 @@
 /* Copyright (c) 2011, Canonical Ltd. All rights reserved. */
 
 YUI().use('test', 'console', 'node-event-simulate',
-          'lp.testing.iorecorder', 'lp.testing.runner',
+          'lp.testing.mockio', 'lp.testing.runner',
           'lp.code.requestbuild_overlay', function(Y) {
 
 var suite = new Y.Test.Suite("lp.code.requestbuild_overlay Tests");
@@ -17,7 +17,7 @@
     name: "lp.code.requestbuild_overlay",
 
     setUp: function() {
-        LP.cache['context'] = {
+        LP.cache.context = {
             web_link: "http://code.launchpad.dev/~foobar/myrecipe"};
         // Prepare testbed.
         var testbed = Y.one("#testbed").set('innerHTML', '');
@@ -34,21 +34,22 @@
     },
 
     _makeRequest: function() {
-        var recorder = new Y.lp.testing.iorecorder.IORecorder();
+        var mockio = new Y.lp.testing.mockio.MockIo();
         var build_now_link = Y.one('#request-daily-build');
         build_now_link.removeClass('unseen');
-        module.connect_requestdailybuild(
-            {io: Y.bind(recorder.do_io, recorder)});
+        module.connect_requestdailybuild({io_provider: mockio});
         build_now_link.simulate('click');
-        
-        Y.Assert.areSame(1, recorder.requests.length);
-        return recorder.requests[0];
+
+        Y.Assert.areSame(1, mockio.requests.length);
+        return mockio;
     },
-    
+
     test_requestbuild_success: function() {
-        var request = this._makeRequest();
-        request.success(
-            builds_target_markup,  {'Content-Type': 'application/xhtml'});
+        var mockio = this._makeRequest();
+        mockio.success({
+            responseText: builds_target_markup,
+            responseHeaders: {'Content-Type': 'application/xhtml'}
+        });
 
         // The markup has been inserted.
         Y.Assert.areSame(
@@ -68,8 +69,8 @@
 
 
     _testRequestbuildFailure: function(status, expected_message) {
-        var request = this._makeRequest();
-        request.respond(status, '',  {});
+        var mockio = this._makeRequest();
+        mockio.respond({status: status});
 
         // No build targets.
         Y.Assert.areSame(


Follow ups