0

I have the following situation with Angular:

Controller

function Controller () {
    // form used in template
    this.form ...
}

Template that has a form and uses this controller

Template

<div ng-controller="Controller as ctrl">
    <form name="ctrl.form">
    ...
</div>

I have to say I'm in general confused why Angular doesn't have a better way of adding the form to the controller other than automatically adding either this.form or $scope.form, depending on how you use controllers, but that's I guess another question.

The real issue I have right now is that I'm not sure how I should test this. If I just instantiate the controller in the test, then my form is undefined

$controller('Controller as ctrl')

I did find a way, by just compiling the template

$scope = $rootScope.$new();
$compile(template)($scope);

But because ng-controller in the template starts a new scope, I can't access the controller directly with $scope.ctrl, instead I'd have to do something like $scope.$$childHead.login

... and I feel it's getting too complicated. edit: not to mention that $$ indicates a 'private' property.

2 Answers 2

2

I've solved it myself, but I'm leaving it here unaccepted because I don't think the solution is very nice. If anyone knows a better way, please post.

The problem with compiling templates is that it's also not possible to insert mock services in the controller, at least I didn't figure it out. You get a controller instance on $scope, like $scope.ctrl, but that's it.

The second attempt was to locate just the form in the template, compile and add it to a controller that was instantiated separately. This worked, but not really, because the $scope for the form and controller were different and so any update to a field didn't reflect on the controller state.

The way it works in the end is to instantiate the controller with a $scope

ctrl = $controller('Controller as ctrl', {
        someDependency: mockDependency,
        $scope: $scope
    });

and then to compile the partial template (just the form) with the same $scope

var formTpl = angular.element(template).find('form');
form = $compile(formTpl)($scope);

This way, the controller ends up in $scope.ctrl, but because the form is already named name="ctrl.form" it gets inserted into $scope.ctrl.form and it's visible inside a controller.

Sign up to request clarification or add additional context in comments.

Comments

0

When using controllerAs, you can access your form in tests like so:

// 1. Create a new scope
var $scope = $rootScope.$new();

// 2. Run the controller
var Controller = $controller("Controller as ctrl", { $scope: $scope });

// 3. Compile the template against our scope
// This will add property $scope.ctrl.form (assuming that form has name="ctrl.form")
$compile(angular.element(templateHtml))($scope);

// 4. Controller.form should now also be defined
expect(Controller.form).toBeDefined();

Meanwhile mocking can be achieved by using angular's $provide:

beforeEach(angular.mock.module(function ($provide) {
    function ApiServiceMock() {
        this.getName() = function () {
            return "Fake Name";
        };
    }

    // Provide our mocked service instead of 'ApiService' 
    // when our controller's code requests to inject it
    $provide.value("ApiService", ApiServiceMock);
}));

Full example (using both - mocking & form compilation with controllerAs):

Main app code:

angular.module("my.module", [])

    .service("ApiService", function () {
        this.getName = function () {
            return "Real Name";
        };
    })

    .controller("Controller", function (ApiService) {
        var ctrl = this;
        ctrl.someProperty = ApiService.getName();
    });

HTML:

<div ng-controller="Controller as ctrl">
    <form name="ctrl.form">
        <input type="email" name="email" />
    </form>
</div>

Test:

describe("Controller", function () {
    var $scope,
        Controller;

    beforeEach(angular.mock.module("my.module"));

    beforeEach(angular.mock.module(function ($provide) {
        function ApiServiceMock() {
            this.getName() = function () {
                return "Fake Name";
            };
        }

        $provide.value("ApiService", ApiServiceMock);
    }));

    beforeEach(inject(function ($rootScope, $controller, $compile) {
        $scope = $rootScope.$new();

        Controller = $controller("Controller as ctrl", { $scope: $scope });

        // FIXME: Use your own method of retrieving template's HTML instead 'getTemplateContents' placeholder
        var html = getTemplateContents("./my.template.html"),
            formTpl = angular.element(html).find('form');

        $compile(formTpl)($scope);
    }));

    it('should contain someProperty', function () {
        expect(Controller.someProperty).toBeDefined();
    });

    it('form should contain an email field', function () {
        expect(Controller.form.email).toBeDefined();
    });
});

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.