1

This is working when I run the UI in the browser, but I'm always getting null for the 'd' in my validateAsync method that needs to call the done method to get it back to the save method. I can't figure out how to use the andCallFake (required to spyOn the unique name test), but also get it to return the (jQuery) deferred to call done on. Hopefully this code will give you enough context to see what I'm trying to accomplish.

    validateAsync = function () {
        var d,
            isValid = true,
            isUnique = false;
            // validate that name and description are given
            if (layout.Name() === '') {
                toastr.warning('Layout name is required', 'Layout');
                isValid = false;
            }
             // validate that there are no other layouts of the same type with the same name
            d = uiDataService.GetIsLayoutNameUniqueAsync(layout.LayoutId(), layout.Name(), layout.LayoutTypeId())
                .done(function (isUniqueResult) {
                    isUnique = isUniqueResult.toLowerCase() === "true";
                    if (!isUnique) {
                        toastr.warning('Layout name ' + layout.Name() + ' must be unique. There is already a layout with this name.', 'Layout');
                    }
                    // this is always undefined in my Jasmine tests
                    d.done(isValid && isUnique);
                })
                .fail(function (response) {
                    mstar.AjaxService.CommonFailHandling(response.responseText);
                });
            return d;
    },
    save = function () {
        validateAsync()
            .done(function (isValidResult) {
                var isValid = isValidResult.toLowerCase() === "true";
                if (!isValid) {
                    return;
                }
                 // show a toastr notification on fail or success
                dataContext.SaveChanges(layout, uiDataService)
                    .done(function (layoutIdFromSave) {
                        toastr.success('The layout was saved. Refreshing...');
                    })
                    .fail(function () {
                        toastr.error('There was an error saving the layout.');
                    })
                    .always(function () {
                        // toastr.info('finished');
                    });
            })
            .fail(function () {
                throw new Error('There was an error validating before save');
            });
    };      

    // in uiDataService
     getIsLayoutNameUniqueAsync = function (layoutId, layoutName, layoutTypeId) {
        return ajaxService.AjaxGetJsonAsync(webServiceUrl + "GetIsLayoutNameUnique?layoutId=" + layoutId + "&layoutName=" + escape(layoutName) + "&layoutTypeId=" + layoutTypeId);
    },
    // in ajaxService
 ajaxGetJsonAsync = function (url, cache) {
            return $.ajax({
                type: "GET",
                url: url,
                dataType: "json",
                accepts: {
                    json: "application/json"
                },
                cache: cache === undefined ? false : cache
        });
    },
// in a beforeEach
var getIsLayoutNameUniquePromiseSpy = spyOn(mstar.dataService.UiDataService, "GetIsLayoutNameUniqueAsync")
    .andCallFake(function () {
        spyObj.called = true;
        // http://stackoverflow.com/questions/13148356/how-to-properly-unit-test-jquerys-ajax-promises-using-jasmine-and-or-sinon
        var d = $.Deferred();
        d.resolve('true');
        return d.promise();
    });
// and a test
it("should show a toastr", function () {
    // Act
    vm.GetLayout().Name('Test');
    vm.GetLayout().Description('Test');
    vm.Save();
    // Assert
    expect(toastr.success).toHaveBeenCalledWith('The layout was saved. Refreshing...');
});
AlignedDev
  • 8,102
  • 9
  • 56
  • 91
  • You are passing a boolean to `.done()`, where it expects a function. – Beetroot-Beetroot Apr 08 '13 at 20:22
  • @Beetroot-Beetroot the done() there is supposed to fire off the .done in the save method. $.Deferred() has a .resolve method, but I don't think the $.Deferred().promise() has a .resolve method. – AlignedDev Apr 08 '13 at 20:32
  • That's correct, only a Deferred has state-changing methods, whereas a Promise derived from a Deferred is a "consumer" object that (like the Deferred itself) can respond to state changes. But please don't get hung up on this point because everything in `validateAsync()` and `save()` is on the consumer side - changes of state are managed inside the `uiDataService.GetIsLayoutNameUniqueAsync()` method. I'm not going to attempt to comment on the Jasmine other than to say it seems completely nonsensical to write and debug code to debug other code that is of the same order of complexity or simpler. – Beetroot-Beetroot Apr 09 '13 at 00:14
  • Have you read my answer? – Beetroot-Beetroot Apr 09 '13 at 00:15
  • Thanks for your responses, I was working on something else and just got back to this. The reason I'm using Jasmine is to have Behavior Driven Development Specification tests that ensure that what I want to happens, keeps happening later in development. – AlignedDev Apr 09 '13 at 12:58
  • Aligned, who knows, I may need Jasmine one day and I will gratefully recall your explanation. Meanwhile, I'll keep my head well buried in the sand where it normally is :) – Beetroot-Beetroot Apr 09 '13 at 16:30

1 Answers1

2

Aligned, I don't know a lot about Jasmine but taking the code on its own merits, it's a lot easier to see what's going on if it's stripped right down to the bare bones.

Greatly simplified, validateAsync() is currently structured as follows :

validateAsync = function () {
    ...
    var d = fn_that_returns_a_promise().done(function() {
        ...
        d.done(boolean);
    }).fail(function() {
        ...
    });
    return d;
};

which can't be right, because .done() doesn't accept a boolean argument and, whereas I can't say it's definitely wrong, d.done() is not really appropriate inside a d.done() handler (though maybe in different circumstances).

I suggest you want to employ .then() to filter the success case (thus passing on a new promise resolved with your boolean value), while retaining .fail() for the failure case; giving a structure as follows :

validateAsync = function () {
    ...
    return uiDataService.GetIsLayoutNameUniqueAsync(...).then(function(...) {
        ...
        return isValid && isUnique;
    }).fail(function(...) {
        ...
    });
};

Thus, save() can be as follows :

save = function() {
    validateAsync().done(function(isValid) {
        //validation success
        if(!isValid) return;
        ...
    }.fail(function() {
        //validation failure
        ...
    });
};

Now all you have to do is "join up the dots" (ie. reinsert your own statements etc) and hope I haven't made any mistakes.

Beetroot-Beetroot
  • 18,022
  • 3
  • 37
  • 44
  • 1
    This was the way to go. I didn't understand that you could return from a then and was thinking I had to resolve the returned promise from the fn_that_returns_a_promise method. Thank you for your help! – AlignedDev Apr 09 '13 at 13:06
  • Interestingly, I think I'm right in saying that in `validateAsync()`, the two methods `.then()` and `.fail()` are commutative - in other words, they can be chained in either order; `.then().fail()` or `.fail().then()`. This is not a generality, just in this case. – Beetroot-Beetroot Apr 09 '13 at 16:37