49

I've tried following the format of the ng-directive-testing repo for a directive I've written. The directive basically renders an overlay when the user clicks on an element. Here's the directive (simplified):

mod.directive('uiCopyLinkDialog', function(){
    return {
        restrict: 'A',
        link: function(scope, element, attrs) {
            var $elm = angular.element(element);
            element.bind('click', function(event) {
                $elm.addClass('test');
            });
        }
    };
});

The test I'm writing looks like this:

describe('pre-compiled link', function () {

    beforeEach(mocks.inject(function($compile, $rootScope) {
        scope = $rootScope;
        element = angular.element('<span class="foo" ui-copy-link-dialog="url"></span>');
        $compile(element)(scope);
        scope.$digest();
    }));

    it("should change the class when clicked", function () {
        element.click(); // this returns "'undefined' is not a function"
        element[0].click(); // so does this
        $(elm).click(); // this uses jquery and doesn't *seem* to fail
        waits(500); // hack to see if it was a race condition
        expect(elm.className).toContain('test'); // always fails
    });

});

You can see in the test that I try several ways to trigger the click() event on the link, with most of them giving an undefined error.

Can anyone tell me what I'm doing wrong here? Reading the examples this sounds like it's the correct syntax but my test runner (Karma via Grunt) doesn't want to play ball.

Noremac
  • 3,445
  • 5
  • 34
  • 62
Matt Andrews
  • 2,868
  • 3
  • 31
  • 53
  • You may want to try giving it an ID, and selecting based on the Id: angular.element('span[id="theSpan"]').click(); Additionally since I see you mention dialog... is your overlay using a jQuery dialog? if so; you'll be blocked from triggering a click event on anything outside that dialog while the dialog is opened. A solution is to trigger an event once the dialog closes, then run a click event. – Jeff Dalley Jun 22 '13 at 06:21
  • 1
    Thanks: it looks like this is actually a bug with running these tests in PhantomJS -- when I run tests in Chrome it correctly fires the `click()` events. Weird! – Matt Andrews Jun 24 '13 at 14:32

6 Answers6

57

You can use triggerHandler, part of JQLite.

I used this to trigger a click event on a directive...

element = angular.element("<div myDirective-on='click'></div>");
compiled = $compile(element)($rootScope);
compiled.triggerHandler('click');

Full example available on this blog post: http://sravi-kiran.blogspot.co.nz/2013/12/TriggeringEventsInAngularJsDirectiveTests.html

Kildareflare
  • 4,590
  • 5
  • 51
  • 65
  • Interesting, your plunker undeniably works. I have a similar problem [here](http://stackoverflow.com/questions/26675196/trigger-click-event-on-an-angularjs-directive-in-mocha-test-suite) but this solution doesn't solve it quite. Perhaps you can spot the problem? – miphe Nov 03 '14 at 13:40
  • Great! But I didn't need the `$compile` step. – Darwin Tech Feb 05 '15 at 20:05
  • This is my preferred solution too, and it works great with PhantomJS. In my case I was triggering an element – Dinis Cruz Aug 09 '15 at 11:38
18

So this turned out to be a problem with PhantomJS: some events that act on elements don't seem to fire when the elements aren't actually on a document anywhere, but just in memory (that's my theory, anyway). To work around this I had to use this function to trigger click events on elements:

define(function () {
    return {
        click: function (el) {
            var ev = document.createEvent("MouseEvent");
            ev.initMouseEvent(
                "click",
                true /* bubble */, true /* cancelable */,
                window, null,
                0, 0, 0, 0, /* coordinates */
                false, false, false, false, /* modifier keys */
                0 /*left*/, null
            );
            el.dispatchEvent(ev);
        }
    };
});

This worked, although other things were harder: I also wanted to write a test that ensures a given form input has focus, but getting this value was almost impossible using PhantomJS since I guess the browser can't make something focused if it has no onscreen representation. Anyone needing this could have a look at CasperJS which offers a simple API for some of these requirements.

Matt Andrews
  • 2,868
  • 3
  • 31
  • 53
4

I've had trouble with this also. It seems as though click() doesn't seem to work at all for me with PhantomJS for any element I compile. It always returns undefined.

Though not really as good as an actual click, you could access the directive's function in ng-click to simulate a click through it's isolate scope:

var element = $compile('<a ng-click="myfunc()">Click me</a>')(scope);
var isolateScope = element.isolateScope();
isolateScope.myfunc();
scope.$digest();

/* check that things changed ... */
antimatter
  • 3,240
  • 2
  • 23
  • 34
4

So my solution to this was to actually append the element to the body. Since the root problem is that phantomJs doesn't fire events for elements in memory, it seemed simpler to append each element so that the events work for real.

afterEach(function(){
    $('body').empty();
});

it('should do something when clicked', function(){
    element = $compile('<div my-directive></div>')($scope);
    $('body').append(element);

    // fire all the watches, so the scope expressions will be evaluated
    $rootScope.$digest();

    $(element).find('.my-input').click();
});
frodo2975
  • 10,340
  • 3
  • 34
  • 41
  • I tried this approach but I can't get it to work. Would be so kind and have a look at this bin: http://jsbin.com/womored/edit?js,output – Gerome Bochmann Jan 25 '16 at 15:30
  • You can also use `angular.element('body')` instead of `$('body')`. If you don't use jQuery, inject `$document` and use `$document.find('body')` instead. – Gerome Bochmann Jan 25 '16 at 15:31
  • Hmm, I took a look, but the jsbin looks ok to me, except it seemed to be erroring out on a missed comma. Have you tried including jquery and using its click method instead? There could be a difference. Also, if you can't get it to work, you can use angular's triggerHandler method to manually fire the listeners for an element. – frodo2975 Jan 26 '16 at 18:49
0

You can use angular-test-runner library and test will look like:

const testRunner = require('angular-test-runner');

describe('directive', () => {
    let app;
    const {expectElement, click} = testRunner.actions;

    beforeEach(() => {
        app = testRunner.app(['mod']);
    });

    it("should add class when clicked", function () {
        const html = app.runHtml('<span class="foo" ui-copy-link-dialog="url"></span>');

        html.perform(
            click.in('.foo')
        );

        html.verify(
            expectElement('.foo').toHaveClass('test')
        );
    });

});
wprzechodzen
  • 619
  • 4
  • 13
-1

How about including angular-scenario and then using browserTrigger(element, 'click')?

jmazin
  • 1,021
  • 9
  • 11