Unit testing AngularJS components with ng-model
Tags: angular, testing
Published: 2022-08-31 18:10:22

I wanted to test a component using the UI elements to avoid testing to much implementation details.

When doing that, I faced was faced with an input and did not find easily how to trigger its change events to trigger the `ng-model` change and the `ng-change` event.

Here is how I did it. (This come late given that AngularJS is deprecated. But I guess I will not be the only one lucky enough to still work with it sometime.)

&nbsp;

## The test base

For these tests, we will use the `angular-mocks` library.

We will use le `$compile` to instanciate the component. And then interact with the component by using the compiled elements.

We will compile the `input` directly to make the tests easier. We could compile a component that have inputs and retrieve them by calling the JQuery `.find(<selector>)` on the compiled element.

We will use the Jest syntax for the test declaration and assertions.

```javascript
require('angular');
require('angular-mocks');

describe('test', () => {
  let $rootScope;
  let $compile;
  let $timeout;

  beforeEach(inject((_$rootScope_, _$compile_, _$timeout_) => {
    $rootScope = _$rootScope_;
    $compile = _$compile_;
    $timeout = _$timeout_;
  }));
    
  // ...
});
```

&nbsp;

## Simple ng-model test

In this test, we will use a simple `input` without `ng-model-options`.

To update the `ng-model` update and `ng-change` update we will use the following JQuery operations:
    - `.val(<value>)` to update the value
    - `.triggerHandler('change')` to send the `change` event of the input

```javascript
it('Should trigger change ng-model value and trigger ng-change', () => {
  const scope = $rootScope.$new();
  scope.value = '';
  scope.onChange = jest.fn();

  const element = $compile('<input ng-model="value" ng-change="onChange()">')(scope);
  scope.$digest();

  element.val('test').triggerHandler('change');

  expect(scope.value).toBe('test');
  expect(scope.onChange).toHaveBeenCalled();
});
```

&nbsp;

## ng-model with update on blur

In this test, we will use a simple `input` with `ng-model-options` to update only when the input `blur` event is triggered.

The `blur` event is triggered when the field looses focus.

This may be used to avoid updating every time the value changes if we know that we will only use the value when the input will be filled.

To update the `ng-model` update and `ng-change` update we will use the following JQuery operations:
    - `.val(<value>)` to update the value
    - `.triggerHandler('change')` to send the `change` event of the input
    - `.triggerHandler('blur')` to send the `blur` event of the input

```javascript
it('Should trigger change ng-model value and trigger ng-change (update on blur)', () => {
  const scope = $rootScope.$new();
  scope.value = '';
  scope.onChange = jest.fn();

  const element = $compile('<input ng-model="value" ng-model-options="{ updateOn: \'blur\' }" ng-change="onChange()">')(scope);
  scope.$digest();

  element.val('test').triggerHandler('change').triggerHandler('blur');

  expect(scope.value).toBe('test');
  expect(scope.onChange).toHaveBeenCalled();
});
```

&nbsp;

## ng-model with debounce

In this test, we will use a simple `input` without `ng-model-options`.

The debounce option can be used to update the value only when the if it does not change for a given time.

This make it possible to keep up to date even if the input does not loose focus while avoiding getting too much events.

It can be useful for elements like search inputs where we want to show suggestions while the user types. But we do not want to do the computation on every change because it can be quite heavy.

To update the `ng-model` update and `ng-change` update we will use the following JQuery operations:
    - `.val(<value>)` to update the value
    - `.triggerHandler('change')` to send the `change` event of the input
    
Then we will use `$timeout.flush()` for the code to act like if the debounce time has passed.

```javascript
it('Should trigger change ng-model value and trigger ng-change (debounce)', () => {
  const scope = $rootScope.$new();
  scope.value = '';
  scope.onChange = jest.fn();

  const element = $compile('<input ng-model="value" ng-model-options="{ debounce: 200 }" ng-change="onChange()">')(scope);
  scope.$digest();

  element.val('test').triggerHandler('change');
  $timeout.flush();

  expect(scope.value).toBe('test');
  expect(scope.onChange).toHaveBeenCalled();
});
```