brisket/brisket

View on GitHub
spec/client/client/ClientRenderingWorkflowSpec.js

Summary

Maintainability
F
2 wks
Test Coverage
"use strict";

describe("ClientRenderingWorkflow", function() {
    var ClientRenderingWorkflow = require("../../../lib/client/ClientRenderingWorkflow");
    var ClientRenderer = require("../../../lib/client/ClientRenderer");
    var ClientRequest = require("../../../lib/client/ClientRequest");
    var ClientResponse = require("../../../lib/client/ClientResponse");
    var Layout = require("../../../lib/viewing/Layout");
    var View = require("../../../lib/viewing/View");
    var mockWindow = require("../../mock/mockWindow");
    var Errors = require("../../../lib/errors/Errors");
    var Promise = require("bluebird");

    var originalHandler;
    var expectedView;
    var expectedView2;
    var fakeRouter;
    var handlerReturns;
    var ExampleLayout;
    var PageNotFoundView;
    var ErrorView;
    var onRouteStart;
    var onRouteComplete;
    var secondHandler;
    var firstReturns;
    var secondReturns;
    var bothReturn;
    var windough;
    var mockClientRequest;
    var mockClientResponse;
    var error;

    beforeEach(function() {
        ExampleLayout = Layout.extend({
            name: "example"
        });

        expectedView = new View();

        PageNotFoundView = View.extend({
            name: "page_not_found"
        });
        ErrorView = View.extend({
            name: "unhandled_error"
        });

        onRouteStart = jasmine.createSpy("on route start");
        onRouteComplete = jasmine.createSpy("on route complete");

        fakeRouter = {
            layout: ExampleLayout,
            errorViewMapping: {
                404: PageNotFoundView,
                500: ErrorView
            },
            otherMethod: jasmine.createSpy("other method"),
            onRouteStart: onRouteStart,
            onRouteComplete: onRouteComplete,
            close: jasmine.createSpy("close")
        };

        spyOn(Errors, "notify");

        spyOn(ClientRenderer, "render").and.returnValue("page was rendered");

        windough = mockWindow();

        mockClientResponse = new ClientResponse(windough);

        var originalClientRequestFrom = ClientRequest.from;

        spyOn(ClientRequest, "from").and.callFake(function() {
            mockClientRequest = originalClientRequestFrom.apply(null, arguments);

            spyOn(mockClientRequest, "off").and.callThrough();

            return mockClientRequest;
        });
        spyOn(ClientResponse, "from").and.returnValue(mockClientResponse);

        spyOn(Layout.prototype, "close");
        spyOn(Layout.prototype, "backToNormal");
    });

    afterEach(function() {
        ClientRenderingWorkflow.reset();
    });

    it("executes layout commands AFTER route handlers", function(done) {
        var layoutCommandWasExecuted = false;

        fakeRouter.layout = Layout.extend({

            testLayoutCommandWasExecuted: function() {
                layoutCommandWasExecuted = true;
            }

        });

        originalHandler = function(layout) {
            layout.testLayoutCommandWasExecuted();

            expect(layoutCommandWasExecuted).toBe(false);

            return expectedView;
        };

        callAugmentedRouterHandler()
            .finally(function() {
                expect(layoutCommandWasExecuted).toBe(true);
            })
            .finally(done);
    });

    it("passes request as option to layout", function(done) {
        fakeRouter.layout = Layout.extend({
            initialize: function(options) {
                expect(options.request.host).toEqual("example.com:8080");
            }
        });

        callAugmentedRouterHandler()
            .catch(failTest)
            .finally(done);
    });

    describe("exposing environmentConfig to layout for rendering", function() {

        beforeEach(function() {
            ClientRenderingWorkflow.setEnvironmentConfig({
                "made": "in client rendering spec"
            });
        });

        it("[deprecated] ensures layout has environmentConfig before it is passed to route handlers", function(done) {
            fakeRouter.layout = Layout.extend({

                testEnvironmentConfig: function() {
                    expect(this.environmentConfig).toEqual({
                        "made": "in client rendering spec"
                    });
                }

            });

            originalHandler = function(layout) {
                layout.testEnvironmentConfig();

                return expectedView;
            };

            callAugmentedRouterHandler()
                .catch(failTest)
                .finally(done);
        });

        it("ensures layout's model has environmentConfig", function(done) {
            fakeRouter.layout = Layout.extend({

                initialize: function() {
                    expect(this.model.get("environmentConfig")).toEqual({
                        "made": "in client rendering spec"
                    });
                }

            });

            originalHandler = function(layout) {
                layout.testEnvironmentConfig();

                return expectedView;
            };

            callAugmentedRouterHandler()
                .catch(failTest)
                .finally(done);
        });
    });

    describe("when route has finished", function() {
        var layoutCommandWasExecuted;
        var whenRouteFinished;

        beforeEach(function() {
            layoutCommandWasExecuted = false;

            fakeRouter.layout = Layout.extend({

                testLayoutCommandWasExecuted: function() {
                    layoutCommandWasExecuted = true;
                }

            });

            originalHandler = function(layout) {
                expectedView.on("event", function() {
                    layout.testLayoutCommandWasExecuted();
                });

                return expectedView;
            };

            whenRouteFinished = callAugmentedRouterHandler();
        });

        it("executes layout commands from route handler immediately", function(done) {
            whenRouteFinished
                .catch(failTest)
                .finally(function() {
                    expect(layoutCommandWasExecuted).toBe(false);

                    expectedView.trigger("event");

                    expect(layoutCommandWasExecuted).toBe(true);
                })
                .finally(done);
        });

        it("unbinds request onComplete handlers", function(done) {
            whenRouteFinished
                .catch(failTest)
                .finally(function() {
                    expect(mockClientRequest.off).toHaveBeenCalled();
                })
                .finally(done);
        });

    });

    describe("whenever handler is called", function() {

        beforeEach(function() {
            originalHandler = jasmine.createSpy("whenever handler is called");
        });

        it("calls original handler with params, layout, brisketRequest, and brisketResponse", function(done) {
            callAugmentedRouterHandlerWith(originalHandler, "param1", "param2")
                .then(function() {
                    expect(originalHandler).toHaveBeenCalledWith(
                        "param1",
                        "param2",
                        jasmine.any(Function),
                        mockClientRequest,
                        mockClientResponse
                    );
                })
                .catch(failTest)
                .finally(done);
        });

    });

    describe("when original handler uses 'this'", function() {

        beforeEach(function() {
            originalHandler = function() {
                this.otherMethod();
            };
        });

        it("ensures original handler's scope is bound to router", function(done) {
            callAugmentedRouterHandler()
                .then(function() {
                    expect(fakeRouter.otherMethod).toHaveBeenCalled();
                })
                .catch(failTest)
                .finally(done);
        });

    });

    describe("when original handler redirects", function() {
        var restOfCodeInTheHandler;

        beforeEach(function() {
            restOfCodeInTheHandler = jasmine.createSpy("rest of code in the handler");

            originalHandler = function(layout, request, response) {
                response.redirect("go/somewhere");
                restOfCodeInTheHandler();
                return expectedView;
            };

            handlerReturns = callAugmentedRouterHandler();
        });

        it("does NOT execute the rest of code in the handler", function(done) {
            handlerReturns
                .finally(function() {
                    expect(restOfCodeInTheHandler).not.toHaveBeenCalled();
                })
                .finally(done);
        });

        it("does NOT render a View", function(done) {
            handlerReturns
                .finally(function() {
                    expect(ClientRenderer.render).not.toHaveBeenCalled();
                })
                .finally(done);
        });

        itUnbindsRequestOnCompleteHandlers();
        itCleansUpRouter();
    });

    describe("setting response headers", function() {

        it("does NOT throw an error when 'response.set' is called", function(done) {
            originalHandler = function(layout, request, response) {
                function callingResponseSet() {
                    response.set("Cache-control", "public, max-age=3600");
                }

                expect(callingResponseSet).not.toThrow();

                return expectedView;
            };

            callAugmentedRouterHandler()
                .catch(failTest)
                .finally(done);
        });

    });

    describe("when original handler does NOT return a View NOR promise of View", function() {

        beforeEach(function() {
            originalHandler = function() {
                return null;
            };

            handlerReturns = callAugmentedRouterHandler();
        });

        it("does NOT render without View", function(done) {
            handlerReturns
                .catch(failTest)
                .finally(function() {
                    expectNotToRender(null);
                })
                .finally(done);
        });

        it("renders error view", function(done) {
            handlerReturns
                .then(function() {
                    expectRenderFor(jasmine.any(ErrorView));
                })
                .catch(failTest)
                .finally(done);
        });

    });

    describe("when original handler returns View", function() {

        beforeEach(function() {
            originalHandler = function() {
                return expectedView;
            };

            handlerReturns = callAugmentedRouterHandler();
        });

        it("renders page", function(done) {
            handlerReturns
                .then(function() {
                    expectRenderFor(expectedView);
                })
                .catch(failTest)
                .finally(done);
        });

        it("returns promise of rendered page", function(done) {
            handlerReturns
                .then(function(html) {
                    expect(html).toBe("page was rendered");
                })
                .catch(failTest)
                .finally(done);
        });

        itUnbindsRequestOnCompleteHandlers();
        itCleansUpRouter();
    });

    describe("when original handler returns promise of View", function() {

        beforeEach(function() {
            originalHandler = function() {
                return Promise.resolve(expectedView);
            };

            handlerReturns = callAugmentedRouterHandler();
        });

        it("render page", function(done) {
            handlerReturns
                .then(function() {
                    expectRenderFor(expectedView);
                })
                .catch(failTest)
                .finally(done);

        });

        it("returns promise of rendered page", function(done) {
            handlerReturns
                .then(function(html) {
                    expect(html).toBe("page was rendered");
                })
                .catch(failTest)
                .finally(done);
        });

        itUnbindsRequestOnCompleteHandlers();
        itCleansUpRouter();
    });

    describe("when original handler returns a rejected promise", function() {

        beforeEach(function() {
            error = "original handler returns a rejected promise";

            originalHandler = function() {
                return Promise.reject(error);
            };

            handlerReturns = callAugmentedRouterHandler();
        });

        it("logs the error to console", function(done) {
            handlerReturns
                .then(function() {
                    expect(Errors.notify).toHaveBeenCalledWith(
                        "original handler returns a rejected promise",
                        mockClientRequest
                    );
                })
                .catch(failTest)
                .finally(done);
        });

        itUnbindsRequestOnCompleteHandlers();
        itResetsLayout();
        itCleansUpRouter();
        itDoesNotRethrowError();
    });

    describe("when original handler returns with a 404", function() {

        beforeEach(function() {
            error = {
                status: 404
            };

            originalHandler = function() {
                return Promise.reject(error);
            };

            handlerReturns = callAugmentedRouterHandler();
        });

        it("renders 404 view", function(done) {
            handlerReturns
                .then(function() {
                    expectRenderFor(jasmine.any(PageNotFoundView));
                })
                .catch(failTest)
                .finally(done);
        });

        it("logs the jqxhr to console", function(done) {
            handlerReturns
                .then(function() {
                    expect(Errors.notify.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({
                        status: 404
                    }));
                })
                .catch(failTest)
                .finally(done);
        });

        itUnbindsRequestOnCompleteHandlers();
        itResetsLayout();
        itCleansUpRouter();
        itDoesNotRethrowError();
    });

    describe("when original handler returns with a 500", function() {

        beforeEach(function() {
            error = {
                status: 500
            };

            originalHandler = function() {
                return Promise.reject(error);
            };

            handlerReturns = callAugmentedRouterHandler();
        });

        it("renders error view", function(done) {
            handlerReturns
                .then(function() {
                    expectRenderFor(jasmine.any(ErrorView));
                })
                .catch(failTest)
                .finally(done);
        });

        it("logs the jqxhr to console", function(done) {
            handlerReturns
                .then(function() {
                    expect(Errors.notify.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({
                        status: 500
                    }));
                })
                .catch(failTest)
                .finally(done);
        });

        itUnbindsRequestOnCompleteHandlers();
        itResetsLayout();
        itCleansUpRouter();
        itDoesNotRethrowError();
    });

    describe("when original handler returns error (not 500 or 404)", function() {

        beforeEach(function() {
            error = {
                status: 503
            };

            originalHandler = function() {
                return Promise.reject(error);
            };

            handlerReturns = callAugmentedRouterHandler();
        });

        it("renders error view", function(done) {
            handlerReturns
                .then(function() {
                    expectRenderFor(jasmine.any(ErrorView));
                })
                .catch(failTest)
                .finally(done);
        });

        it("logs the jqxhr to console", function(done) {
            handlerReturns
                .then(function() {
                    expect(Errors.notify.calls.mostRecent().args[0]).toEqual(jasmine.objectContaining({
                        status: 503
                    }));
                })
                .catch(failTest)
                .finally(done);
        });

        itUnbindsRequestOnCompleteHandlers();
        itResetsLayout();
        itCleansUpRouter();
        itDoesNotRethrowError();
    });

    describe("when original handler has an uncaught error", function() {

        beforeEach(function() {
            error = new Error("original handler has uncaught error");

            originalHandler = function() {
                throw error;
            };

            handlerReturns = callAugmentedRouterHandler();
        });

        it("renders error view", function(done) {
            handlerReturns
                .then(function() {
                    expectRenderFor(jasmine.any(ErrorView));
                })
                .catch(failTest)
                .finally(done);
        });

        it("logs the jqxhr to console", function(done) {
            handlerReturns
                .then(function() {
                    expect(Errors.notify).toHaveBeenCalledWith(
                        error,
                        mockClientRequest
                    );
                })
                .catch(failTest)
                .finally(done);
        });

        itUnbindsRequestOnCompleteHandlers();
        itResetsLayout();
        itCleansUpRouter();
        itDoesNotRethrowError();
    });

    describe("the first client side render", function() {

        beforeEach(function() {
            originalHandler = function() {
                return expectedView;
            };

            spyOn(Layout.prototype, "fetchData");
            handlerReturns = callAugmentedRouterHandler();
        });

        it("fetches layout data", function(done) {
            handlerReturns
                .then(function() {
                    expect(Layout.prototype.fetchData).toHaveBeenCalled();
                })
                .catch(failTest)
                .finally(done);
        });

        itCleansUpRouter();
    });

    describe("all client side renders except for the first", function() {
        var firstHandlerReturns;
        var CurrentLayout;

        beforeEach(function() {
            originalHandler = jasmine.createSpy("first client side render");
            CurrentLayout = Layout.extend({
                name: "currentLayout"
            });
            spyOn(CurrentLayout.prototype, "fetchData");

            fakeRouter.layout = CurrentLayout;
            firstHandlerReturns = callAugmentedRouterHandler();
        });

        describe("when second request wants to render with current layout that was used in first request", function() {

            beforeEach(function(done) {
                expectedView2 = new View();
                handlerReturns = callAugmentedRouterHandler(function() {
                    return expectedView2;
                });

                firstHandlerReturns
                    .then(function() {
                        expect(CurrentLayout.prototype.fetchData.calls.count()).toBe(1);
                    })
                    .catch(failTest)
                    .finally(done);

                bothReturn = Promise.all([firstHandlerReturns, handlerReturns]);
            });

            it("does NOT fetch layout data for second request", function(done) {
                bothReturn
                    .then(function() {
                        expect(CurrentLayout.prototype.fetchData.calls.count()).toBe(1);
                    })
                    .catch(failTest)
                    .finally(done);
            });

            itDoesNotCleanUpLayout();

            itCleansUpBothRouters();
        });

        describe("when second request renders with different layout than the layout for first request", function() {
            var DifferentLayout;

            beforeEach(function(done) {
                DifferentLayout = Layout.extend({
                    name: "differentLayout"
                });
                fakeRouter.layout = DifferentLayout;
                expectedView2 = new View();
                windough.location.pathname = "/second/route";

                firstHandlerReturns
                    .catch(failTest)
                    .finally(done);
            });

            describe("when should redirect on new layout", function() {

                beforeEach(function() {
                    expect(windough.location.replace).not.toHaveBeenCalled();

                    ClientRenderingWorkflow.setEnvironmentConfig({
                        "brisket:layoutRedirect": true
                    });
                });

                it("redirects to new route", function() {
                    callAugmentedRouterHandler(function() {
                        return expectedView2;
                    });

                    expect(windough.location.replace).toHaveBeenCalledWith("/second/route");
                });

            });

            describe("[deprecated] when should NOT redirect on new layout", function() {

                beforeEach(function() {
                    ClientRenderingWorkflow.setEnvironmentConfig({
                        "brisket:layoutRedirect": false
                    });
                });

                it("renders an error", function() {
                    callAugmentedRouterHandler(function() {
                        return expectedView2;
                    });

                    expect(ClientRenderer.render).toHaveBeenCalledWith(
                        jasmine.any(Layout),
                        jasmine.any(ErrorView),
                        jasmine.any(Number)
                    );

                    expect(Errors.notify).toHaveBeenCalledWith(
                        jasmine.any(Error),
                        mockClientRequest
                    );
                });

            });

        });

    });

    describe("when original handler returns cancelled request", function() {

        describe("when readyState is 0", function() {

            beforeEach(function() {
                originalHandler = function() {
                    return Promise.reject({
                        readyState: 0
                    });
                };
            });

            it("should NOT render", function(done) {
                callAugmentedRouterHandler()
                    .then(function() {
                        expect(ClientRenderer.render).not.toHaveBeenCalled();
                    })
                    .catch(failTest)
                    .finally(done);
            });

            it("does NOT clean up layout", function(done) {
                callAugmentedRouterHandler()
                    .then(function() {
                        expect(Layout.prototype.close).not.toHaveBeenCalled();
                    })
                    .catch(failTest)
                    .finally(done);
            });

            it("cleans up router", function(done) {
                callAugmentedRouterHandler()
                    .then(function() {
                        expect(fakeRouter.close).toHaveBeenCalled();
                    })
                    .catch(failTest)
                    .finally(done);
            });

        });

        describe("when statusCode is 0", function() {

            beforeEach(function() {
                originalHandler = function() {
                    return Promise.reject({
                        status: 0
                    });
                };
            });

            it("should NOT render when", function(done) {
                callAugmentedRouterHandler()
                    .then(function() {
                        expect(ClientRenderer.render).not.toHaveBeenCalled();
                    })
                    .catch(failTest)
                    .finally(done);
            });

            it("does NOT clean up layout", function(done) {
                callAugmentedRouterHandler()
                    .then(function() {
                        expect(Layout.prototype.close).not.toHaveBeenCalled();
                        done();
                    });
            });

            it("cleans up router", function(done) {
                callAugmentedRouterHandler()
                    .then(function() {
                        expect(fakeRouter.close).toHaveBeenCalled();
                    })
                    .catch(failTest)
                    .finally(done);
            });

        });

    });

    describe("when the first render request takes longer to return than the second", function() {
        var commandFromOriginalHandler;
        var commandFromSecondHandler;
        var firstRouteOnComplete;
        var secondRouteOnComplete;

        beforeEach(function() {
            expectedView2 = new View();
            expectedView2.id = "view2";

            commandFromOriginalHandler = jasmine.createSpy("commands from original handler");
            commandFromSecondHandler = jasmine.createSpy("commands from second handler");
            firstRouteOnComplete = jasmine.createSpy("first route on complete");
            secondRouteOnComplete = jasmine.createSpy("second route on complete");

            fakeRouter.layout = Layout.extend({
                commandFromOriginalHandler: commandFromOriginalHandler,
                commandFromSecondHandler: commandFromSecondHandler
            });
        });

        describe("when both handlers are resolved", function() {

            beforeEach(function() {
                originalHandler = function(layout, request) {
                    layout.commandFromOriginalHandler();
                    request.onComplete(firstRouteOnComplete);

                    return Promise.resolve(expectedView).timeout(10);
                };

                secondHandler = function(layout, request) {
                    layout.commandFromSecondHandler();
                    request.onComplete(secondRouteOnComplete);

                    return Promise.resolve(expectedView2).timeout(5);
                };

                runBothHandlers();
            });

            it("does NOT render the first request", function(done) {
                bothReturn
                    .then(function() {
                        expect(ClientRenderer.render).not.toHaveBeenCalledWith(
                            jasmine.any(Layout),
                            expectedView,
                            jasmine.any(Number)
                        );
                    })
                    .catch(failTest)
                    .finally(done);
            });

            it("does NOT run layout commands for first request", function(done) {
                bothReturn
                    .then(function() {
                        expect(commandFromOriginalHandler).not.toHaveBeenCalled();
                    })
                    .catch(failTest)
                    .finally(done);
            });

            it("does NOT run request.onComplete handler for first request", function(done) {
                bothReturn
                    .then(function() {
                        expect(firstRouteOnComplete).not.toHaveBeenCalled();
                    })
                    .catch(failTest)
                    .finally(done);
            });

            it("renders the latest request", function(done) {
                bothReturn
                    .then(function() {
                        expect(ClientRenderer.render).toHaveBeenCalledWith(
                            jasmine.any(Layout),
                            expectedView2,
                            2
                        );
                    })
                    .catch(failTest)
                    .finally(done);
            });

            it("runs layout commands for latest request", function(done) {
                bothReturn
                    .then(function() {
                        expect(commandFromSecondHandler).toHaveBeenCalled();
                    })
                    .catch(failTest)
                    .finally(done);
            });

            it("runs request.onComplete handler for second request", function(done) {
                bothReturn
                    .then(function() {
                        expect(secondRouteOnComplete).toHaveBeenCalled();
                    })
                    .catch(failTest)
                    .finally(done);
            });

            firstRouteDoesNotCleanUpLayout();

            itCleansUpBothRouters();
        });

        describe("when first handler results in error view", function() {

            beforeEach(function() {
                originalHandler = function() {
                    return Promise.reject({
                        status: 404
                    }).timeout(10);
                };

                secondHandler = function() {
                    return Promise.resolve(expectedView2).timeout(5);
                };

                runBothHandlers();
            });

            it("does NOT render the first request", function(done) {
                bothReturn
                    .then(function() {
                        expect(ClientRenderer.render).not.toHaveBeenCalledWith(
                            jasmine.any(Layout),
                            jasmine.any(PageNotFoundView),
                            jasmine.any(Number)
                        );
                    })
                    .catch(failTest)
                    .finally(done);
            });

            it("renders the latest request", function(done) {
                bothReturn
                    .then(function() {
                        expect(ClientRenderer.render).toHaveBeenCalledWith(
                            jasmine.any(Layout),
                            expectedView2,
                            2
                        );
                    })
                    .catch(failTest)
                    .finally(done);
            });

            firstRouteDoesNotCleanUpLayout();

            itCleansUpBothRouters();
        });

        describe("when second handler results in error view", function() {

            beforeEach(function() {
                originalHandler = function() {
                    return Promise.resolve(expectedView).timeout(10);
                };

                secondHandler = function() {
                    return Promise.reject({
                        status: 404
                    }).timeout(5);
                };

                runBothHandlers();
            });

            it("does NOT render the first request", function(done) {
                bothReturn
                    .then(function() {
                        expect(ClientRenderer.render).not.toHaveBeenCalledWith(
                            jasmine.any(Layout),
                            expectedView,
                            jasmine.any(Number)
                        );
                    })
                    .catch(failTest)
                    .finally(done);
            });

            it("renders the latest request", function(done) {
                bothReturn
                    .then(function() {
                        expect(ClientRenderer.render).toHaveBeenCalledWith(
                            jasmine.any(Layout),
                            jasmine.any(PageNotFoundView),
                            2
                        );
                    })
                    .catch(failTest)
                    .finally(done);
            });

            secondRouteDoesNotCleanUpLayout();

            itCleansUpBothRouters();
        });

        describe("when both handlers result in error view", function() {

            beforeEach(function() {
                originalHandler = function() {
                    return Promise.reject({
                        status: 500
                    }).timeout(10);
                };

                secondHandler = function() {
                    return Promise.reject({
                        status: 404
                    }).timeout(5);
                };

                runBothHandlers();
            });

            it("does NOT render the first request", function(done) {
                bothReturn
                    .then(function() {
                        expect(ClientRenderer.render).not.toHaveBeenCalledWith(
                            jasmine.any(Layout),
                            jasmine.any(ErrorView),
                            jasmine.any(Number)
                        );
                    })
                    .catch(failTest)
                    .finally(done);
            });

            it("renders the latest request", function(done) {
                bothReturn
                    .then(function() {
                        expect(ClientRenderer.render).toHaveBeenCalledWith(
                            jasmine.any(Layout),
                            jasmine.any(PageNotFoundView),
                            2
                        );
                    })
                    .catch(failTest)
                    .finally(done);

            });

            firstRouteDoesNotCleanUpLayout();
            secondRouteDoesNotCleanUpLayout();

            itCleansUpBothRouters();
        });

    });

    describe("route start and end hooks", function() {

        beforeEach(function() {
            originalHandler = function() {
                return Promise.resolve(expectedView).timeout(5);
            };
        });

        describe("when route starts", function() {

            it("fires the on route start callback", function(done) {
                handlerReturns = callAugmentedRouterHandler();

                handlerReturns
                    .catch(failTest)
                    .finally(function() {
                        expect(onRouteStart).toHaveBeenCalledWith(
                            jasmine.any(Function),
                            mockClientRequest,
                            mockClientResponse
                        );
                    })
                    .finally(done);
            });

        });

        describe("when route ends", function() {

            it("fires the on route end handler from the router", function(done) {
                handlerReturns = callAugmentedRouterHandler();

                expect(onRouteComplete).not.toHaveBeenCalled();

                handlerReturns
                    .then(function() {
                        expect(onRouteComplete).toHaveBeenCalledWith(
                            jasmine.any(Function),
                            mockClientRequest,
                            mockClientResponse
                        );
                    })
                    .catch(failTest)
                    .finally(done);
            });

        });

        describe("when the first request takes longer than the second", function() {

            beforeEach(function() {
                secondHandler = function() {
                    return Promise.resolve(expectedView).timeout(2);
                };

                runBothHandlers();
            });

            it("only fires onRouteComplete for second request", function(done) {
                bothReturn
                    .then(function() {
                        expect(onRouteComplete).toHaveBeenCalledWith(
                            jasmine.any(Function),
                            mockClientRequest,
                            mockClientResponse
                        );
                        expect(onRouteComplete.calls.count()).toBe(1);
                    })
                    .catch(failTest)
                    .finally(done);
            });

        });

    });

    describe("when onRouteComplete callback errors", function() {

        beforeEach(function() {
            error = new Error("onRouteComplete callback error");

            onRouteComplete.and.callFake(function() {
                throw error;
            });

            handlerReturns = callAugmentedRouterHandler();
        });

        itNotifiesAboutError();
        itRethrowsError();
    });

    describe("when request.onComplete callback errors", function() {

        beforeEach(function() {
            error = new Error("request.onComplete callback error");

            originalHandler = function(layout, request) {
                request.onComplete(function() {
                    throw error;
                });

                return expectedView;
            };

            handlerReturns = callAugmentedRouterHandler();
        });

        itNotifiesAboutError();
        itRethrowsError();
    });

    describe("when router errors on close", function() {

        beforeEach(function() {
            error = new Error("router close error");

            fakeRouter.close.and.callFake(function() {
                throw error;
            });

            handlerReturns = callAugmentedRouterHandler();
        });

        itNotifiesAboutError();
        itRethrowsError();
    });

    describe("when router doesn't have errorViewMapping and there is an error", function() {

        beforeEach(function() {
            fakeRouter.errorViewMapping = null;

            originalHandler = function() {
                return Promise.reject(error);
            };

            handlerReturns = callAugmentedRouterHandler();
        });

        itNotifiesAboutError();
        itRethrowsError();
    });

    function itNotifiesAboutError() {
        it("notifies about error", function(done) {
            handlerReturns
                .then(failTest)
                .catch(function() {
                    expect(Errors.notify).toHaveBeenCalledWith(
                        error,
                        mockClientRequest
                    );
                })
                .finally(done);
        });
    }

    function itDoesNotRethrowError() {

        it("does NOT rethrow error", function(done) {
            handlerReturns
                .then(function() {
                    expect(true).toBe(true);
                })
                .catch(failTest)
                .finally(done);
        });
    }

    function itRethrowsError() {
        it("rethrows error", function(done) {
            handlerReturns
                .then(failTest)
                .catch(function(e) {
                    expect(e).toBe(error);
                })
                .finally(done);
        });
    }

    function itCleansUpRouter() {
        it("cleans up router", function(done) {
            handlerReturns
                .then(function() {
                    expect(fakeRouter.close).toHaveBeenCalled();
                })
                .catch(failTest)
                .finally(done);
        });
    }

    function itCleansUpBothRouters() {
        it("cleans up router for both requests", function(done) {
            bothReturn
                .then(function() {
                    expect(fakeRouter.close.calls.count()).toBe(2);
                })
                .catch(failTest)
                .finally(done);
        });
    }

    function itDoesNotCleanUpLayout() {
        it("does NOT clean up layout", function(done) {
            handlerReturns
                .then(function() {
                    expect(Layout.prototype.close).not.toHaveBeenCalled();
                })
                .catch(failTest)
                .finally(done);
        });
    }

    function firstRouteDoesNotCleanUpLayout() {
        it("does NOT clean up layout for first route", function(done) {
            firstReturns
                .then(function() {
                    expect(Layout.prototype.close).not.toHaveBeenCalled();
                })
                .catch(failTest)
                .finally(done);
        });
    }

    function secondRouteDoesNotCleanUpLayout() {
        it("does NOT clean up layout for second route", function(done) {
            secondReturns
                .then(function() {
                    expect(Layout.prototype.close).not.toHaveBeenCalled();
                })
                .catch(failTest)
                .finally(done);
        });
    }

    function itUnbindsRequestOnCompleteHandlers() {
        it("unbinds request.onComplete handlers", function(done) {
            handlerReturns
                .catch(failTest)
                .finally(function() {
                    expect(mockClientRequest.off).toHaveBeenCalled();
                })
                .finally(done);
        });
    }

    function itResetsLayout() {
        it("resets layout", function(done) {
            handlerReturns
                .then(function() {
                    expect(Layout.prototype.backToNormal).toHaveBeenCalled();
                })
                .catch(failTest)
                .finally(done);
        });
    }

    function runBothHandlers() {
        firstReturns = callAugmentedRouterHandler(originalHandler);
        secondReturns = callAugmentedRouterHandler(secondHandler);

        bothReturn = Promise.all([firstReturns, secondReturns]);
    }

    function expectRenderFor(view) {
        expect(ClientRenderer.render).toHaveBeenCalledWith(
            jasmine.any(Layout),
            view,
            1
        );
    }

    function expectNotToRender(view) {
        expect(ClientRenderer.render).not.toHaveBeenCalledWith(
            jasmine.any(Layout),
            view,
            1
        );
    }

    function callAugmentedRouterHandler(requestedHandler) {
        var handler = requestedHandler || originalHandler;
        return callAugmentedRouterHandlerWith(handler);
    }

    function callAugmentedRouterHandlerWith(handler) {
        var args = Array.prototype.slice.call(arguments, 1);

        return ClientRenderingWorkflow.execute(fakeRouter, handler, args.concat(null), windough);
    }

    function failTest(reason) {
        expect(reason).toBeUndefined();
    }

});

// ----------------------------------------------------------------------------
// Copyright (C) 2018 Bloomberg Finance L.P.
//
// Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// ----------------------------- END-OF-FILE ----------------------------------