Why doesn't async angular unit-test find DOM element?
Asked Answered
D

2

14

I've got a failing asynchronous angular component DOM test but it's synchronous equivalent fails and I don't understand why.

Here's the jasmine test:

describe('Availability Component', () => {

    let fixture: ComponentFixture<AvailabilityComponent>;

    const absenceService = jasmine.createSpyObj('AbsenceService', ['findAbsences']);
    absenceService.findAbsences.and.returnValue(of([{}]));

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            declarations: [AvailabilityComponent],
            imports: [CalendarModule.forRoot()],
            providers: [{provide: AbsenceService, useValue: absenceService}]
        }).compileComponents();
    }));

    beforeEach(() => {
        fixture = TestBed.createComponent(AvailabilityComponent);
    });

    const printAbsenceReasons = function () {
        console.log(fixture.debugElement.queryAll(By.css('.calendarAbsence'))
        .map(e => e.nativeElement.textContent)
        .join(','));
    };

    it('should synchronously find absences in calendar view', () => {
        fixture.detectChanges();

        console.log('synchronous test');
        printAbsenceReasons();
    });

    it('should  asynchronously find absences in calendar view', fakeAsync(() => {
        fixture.detectChanges();
        tick();
        fixture.detectChanges();
        tick();

        console.log('asynchronous test');
        printAbsenceReasons();
    }));
});

Which creates the correct output in the synchronous case but incorrect in the asynchronous case:

LOG: 'processing absences'
HeadlessChrome 0.0.0 (Mac OS X 10.13.4): Executed 0 of 51 SUCCESS (0 secs / 0 secs)
LOG: 'synchronous test'
HeadlessChrome 0.0.0 (Mac OS X 10.13.4): Executed 0 of 51 SUCCESS (0 secs / 0 secs)
LOG: 'A,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,'
HeadlessChrome 0.0.0 (Mac OS X 10.13.4): Executed 0 of 51 SUCCESS (0 secs / 0 secs)
ERROR: 'Spec 'Availability Component should synchronously find absences in calendar view' has no expectations.'
HeadlessChrome 0.0.0 (Mac OS X 10.13.4): Executed 0 of 51 SUCCESS (0 secs / 0 secs)
LOG: 'processing absences'
HeadlessChrome 0.0.0 (Mac OS X 10.13.4): Executed 1 of 51 SUCCESS (0 secs / 0 secs)
LOG: 'asynchronous test'
HeadlessChrome 0.0.0 (Mac OS X 10.13.4): Executed 1 of 51 SUCCESS (0 secs / 0 secs)
LOG: ''
HeadlessChrome 0.0.0 (Mac OS X 10.13.4): Executed 1 of 51 SUCCESS (0 secs / 0 secs)
ERROR: 'Spec 'Availability Component should  asynchronously find absences in calendar view' has no expectations.'

I'm not sure if this has something to do with the angular-calendar component I'm using or if it's a more basic problem with my test code.

For reference here is my component, template and service code:

@Component({
    selector: 'app-availability',
    templateUrl: './availability.component.html',
    styleUrls: ['availability.component.scss']
})
export class AvailabilityComponent implements OnInit {
    viewDate: Date = new Date();
    absenceEvents: CalendarEvent[] = [];

    constructor(private absenceService: AbsenceService) {
    }

    ngOnInit() {
        this.getAbsences();
    }

    getAbsences() {
        this.absenceService.findAbsences()
        .subscribe(ignored => {
            console.log('processing absences');
            this.absenceEvents = [{
                start: new Date(2018, 3, 29), title: 'A'
            }];
        });
    }

    getAbsence(events: CalendarEvent[]) {
        return events[0] ? events[0].title : '';
    }
}

template code:

<div>
    <div>
        <mwl-calendar-month-view
            [viewDate]="viewDate"
            [events]="absenceEvents"
            [cellTemplate]="availabilityCellTemplate">
        </mwl-calendar-month-view>
    </div>
    <ng-template #availabilityCellTemplate let-day="day">
        <div class="calendarAbsence">{{ getAbsence(day.events) }}</div>
    </ng-template>
</div>  

service code:

@Injectable()
export class AbsenceService {

    private url = environment.APP_SHIFT_SERVICE_BASE_URL + '/api/absences';

    constructor(private http: HttpClient) {
    }

    findAbsences(): Observable<Absence[]> {
        console.error('Actual findAbsences() called');
        return this.http.get<Absence[]>(this.url);
    }
}
Doctrine answered 9/5, 2018 at 7:9 Comment(0)
D
3

Unfortunately this seems to have something to do with the fakeAsync zone in particular and not asynchronous tests in particular.

I've managed to get a working asynchronous test to work with async instead of fakeAsync:

it('should asynchronously find absences in calendar view', async(() => {
        fixture.detectChanges();

        fixture.whenStable().then(() => {
            console.log('asynchronous test');
            printAbsenceReasons(fixture);

            expect(getAbsenceElements(fixture).length).toEqual(35);
        });
    });
}));

I've debugged both the failing fakeAsync and the successful synchronous tests. They both call a method called getMonthView in a library "calendar-utils" by the same author as "angular-calendar": https://github.com/mattlewis92/calendar-utils/blob/master/src/calendar-utils.ts

In this method both the Date parameters and also the other Date computations itself seem to go very wrong.

I can only assume currently that it's related to this known issue in zone.js. I'm on Angular 5.2, but I assume its still related. In any case I'm sure that my test code is essentially correct, but the problem lies elsewhere.

Doctrine answered 4/6, 2018 at 6:40 Comment(0)
R
1

I'm pretty sure that the reason this is not working is because of how you have defined your spy. It is defined outside of a beforeEach block.

The way you have it now, your spy is created once, when the fixture starts up. And jasmine works by removing all old spies at the end of every test. So, by the second test, your spy no longer exists.

I would bet that if you switched your test order, then you would see the async test working, but the sync one not.

To fix, simply move this into a beforeEach block:

const absenceService = jasmine.createSpyObj('AbsenceService', ['findAbsences']);
absenceService.findAbsences.and.returnValue(of([{}]));
Resentful answered 28/5, 2018 at 18:16 Comment(1)
That makes a lot of sense, but when I comment the synchronous test I still see the same result of '' for the asynchronous test. Surprisingly an async instead of fakeAsync test actually works: async(() => { fixture.detectChanges(); fixture.whenStable().then(() => printAbsenceReasons())Doctrine

© 2022 - 2024 — McMap. All rights reserved.