ngParty / ng-metadata

Angular 2 decorators and utils for Angular 1.x

Home Page:https://hotell.gitbooks.io/ng-metadata/content/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

$rootScope reference has been destroyed by angular-mocks

aciccarello opened this issue · comments

TLDR;

Unit tests don't run ngOnChanges correctly if they are run after another test because the $rootScope reference was destroy.

Details

In my unit tests I am using the $compile service to create my component and check the resulting DOM. After each test angular-mocks destroys the $rootScope service to clean up the test (introduced in angular/angular.js#13433). However, angular does not invoke the directiveControllerFactory again so when future tests run $apply() in the _flushOnChangesQueue() function, it is calling a noop. This is evidenced by the ngOnChanges() not be called again.

When debugging this issue, I can run two copies of the same exact test and the first instance would pass and the second would fail. I will try creating an example unit test to demonstrate.

Here is a plnkr demonstrating the failed test. Tests 1 & 2 are exactly the same but test 2 fails. In the console you can see that ngOnChanges is not called a second time.

https://plnkr.co/edit/sS2eau?p=preview

Component

import { Component, Input } from 'ng-metadata/core';

@Component({
  selector: 'my-app',
  template: `<h1>Angular 1 App <small>with ng-metadata!</small></h1>
<div>{{ $ctrl.internalProp }}</div>`
})
export class AppComponent {
  @Input() prop = 'default property value';
  internalProp: string;

  ngOnChanges(changes) {
    console.log('ngOnChanges was called with: ' + angular.toJson(changes));
    this.internalProp = this.prop;
  }
}

Component spec

import './main';
import { bundle } from 'ng-metadata/core';
import { AppComponent } from './app.component';

describe('AppComponent', () => {
  // This test will work because it is run first
  it('should update the text', () => {
    console.log('test 1');
    angular.mock.module(bundle(AppComponent).name);
    let {element, $scope} = compile();
    $scope.value = 'My new value';
    $scope.$apply();
    expect(element[0].textContent).toContain('My new value');
  });

  // Same test but it fails
  it('should update the text the second time around', () => {
    console.log('test 2');
    angular.mock.module(bundle(AppComponent).name);
    let {element, $scope} = compile();
    $scope.value = 'My new value';
    $scope.$apply();
    expect(element[0].textContent).toContain('My new value');
  });
});

Any thoughts on this @Hotell?

sorry was busy with non Angular things lately. Need take a look. Sry for delay @aciccarello

Same problem here... onChanges is not call on a sequence of tests.
Any news ?

Working work around proposed by
aciccarello on ng-party slack channel:

This assumes the component is the top level element.
If you have nested components you'd have to manually call ngOnUpdate on those too ;-(

function manuallyCallOnUpdate(element: angular.IAugmentedJQuery,  changes: SimpleChanges) {
  let componentScope = <any> angular.element(element[0].children.item(0)).scope();
  componentScope.$ctrl.ngOnChanges(changes);
  componentScope.$apply();
}

I updated his example so I can pass a SimpleChanges object as parameter.

We're running into the same problem. For now we're working around it by doing $rootScope.$destroy = () => angular.noop();. Not the best solution but at least it saves us from writing specific test code to work around the issue.

@Hotell I'd like to see this fixed to improve testing but I'm afraid to touch it because I don't want to ruin production performance for a unit testing fix.

Right now, I think what needs to happen is that there needs to be a check to see if $rootScope has been destroyed. That way you don't need to take the performance hit of calling the injector every time but you can still run it in tests. Does that make sense? If so I can try to put together a PR.

I'm currious what the comment about something being made a singleton means in https://github.com/ngParty/ng-metadata/blob/master/src/core/change_detection/changes_queue.ts#L1. Is that referring to the ChangesQueue class? Trying to figure out if that would affect the implementation.

This took a lot of effort to track down, but once I got close, search terms got me to this issue. Thanks @aciccarello!

The workaround from @otijhuis fixed our tests that were failing, but caused new ones to fail (because of the way a 3rd part lib was using $rootScope). This secret sauce got our suite to pass again:

$rootScope.$destroy = () => $rootScope.$broadcast('$destroy');

Or, with our full setup (and in coffeescript):

# 1 wierd trick to set global configuration
beforeEach module 'ng', ($provide) ->
  $provide.decorator '$rootScope', ($delegate) ->
    $delegate.$destroy = -> $delegate.$broadcast('$destroy')
    $delegate

However, this feels very hacky, and makes me uncomfortable wondering what may pop up later as our code changes, including as we transition more of it to ng-metadata. (So far we have transitioned 2 components). I hope a real fix for this issue can be figured out!