0

I am writing tests for my angular web application that contains a page with a firebase UI element. There are two tests, one that ensures the page loads and one that ensures the firebaseUI component loads correctly:

authentication.component.spec.ts

/*eslint-env jasmine*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { AuthenticationComponent } from './authentication.component';
import { FirebaseService } from '../services/firebase.service';

describe('AuthenticationComponent_Logged_Out', () => {
  let component: AuthenticationComponent;
  let fixture: ComponentFixture<AuthenticationComponent>;
  let service;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ AuthenticationComponent ],
      schemas: [ NO_ERRORS_SCHEMA ]
    }).compileComponents();
    service = TestBed.inject(FirebaseService);
    fixture = TestBed.createComponent(AuthenticationComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should render auth ui', () => {
    const compiled = fixture.nativeElement;
    expect(compiled.querySelector("#firebaseui_auth_container")).toBeTruthy();
  });

  afterEach(async () => {
    service.ui.reset();
  });
});

and with a template file of:

<script src="https://www.gstatic.com/firebasejs/ui/4.8.0/firebase-ui-auth.js"></script>
<link type="text/css" rel="stylesheet" href="https://www.gstatic.com/firebasejs/ui/4.8.0/firebase-ui-auth.css" />

<div id="authentication-wrapper">
    <h1>Please sign in below to access your quotes!</h1>
    <div id="firebaseui_auth_container"></div>
    <div id="loader">Loading...</div>
</div>

and a class of:

import { Component } from '@angular/core';
import { FirebaseService } from '../services/firebase.service';

@Component({
  selector: 'app-authentication',
  templateUrl: './authentication.component.html',
  styleUrls: ['./authentication.component.css']
})
export class AuthenticationComponent {

  constructor (private fbService: FirebaseService) {
    sessionStorage.removeItem('displayed_random');
    // If user logged in, redirect to feed
    if (fbService.currentUser) {
      window.location.href = "/feed";
    } else {
      this.fbService.instantiateUi();
    }
  }
}

The service that actually loads the firebase ui is:

firebase.service.ts

import { Injectable } from '@angular/core';
import firebase from "firebase/app";
import * as firebaseui from "firebaseui";
import { config } from './config';
import 'firebase/database';
import 'firebase/auth';
firebase.initializeApp(config);

@Injectable({
  providedIn: 'root'
})
export class FirebaseService {
  currentUser: string;
  auth = firebase.auth();
  ui = firebaseui.auth.AuthUI.getInstance() || new firebaseui.auth.AuthUI(this.auth);

  constructor () {
    const username = sessionStorage.getItem('username');
    if (username) {
      this.currentUser = username;
    }
  }

  signoutUser () {
    this.auth.signOut();
    this.currentUser = undefined;
    if (sessionStorage.getItem('username')) {
      sessionStorage.removeItem('username');
    }
  }

  getRef (path) {
    return firebase.database().ref(path);
  }

  instantiateUi () {
    this.ui.start("#firebaseui_auth_container", {
      callbacks: {
        signInSuccessWithAuthResult: (authResult) => {
          // Save username in storage
          sessionStorage.setItem('username', authResult.user.displayName);
          return true;
        },
        uiShown: () => {
          // The widget is rendered, hide the loader.
          document.getElementById('loader').style.display = 'none';
        }
      },
      // Will use popup for IDP Providers sign-in flow instead of the default, redirect.
      signInFlow: 'popup',
      signInSuccessUrl: 'feed',
      signInOptions: [
        {
          provider: firebase.auth.GoogleAuthProvider.PROVIDER_ID,
          customParameters: {
            prompt: 'select_account' // Forces account selection even when only one account is available.
          }
        },
        firebase.auth.EmailAuthProvider.PROVIDER_ID
      ]
    });
  }
}

Now, running the application in a normal dev server environment (i.e. ng serve), the UI is created as expected. However, in the test, it does not get generated properly for some reason and the UI fails to be created. enter image description here

enter image description here

What should I be doing to allow the test framework to find the firebaseui auth container? I have obviously tried to inject the service multiple times and reset the UI after each test but with no joy. I'm guessing that it's to do with compiled being undefined for some reason, which is also strange as that logic works on my other tests.

shad0w
  • 15
  • 6

1 Answers1

0

Based on your provided files, we can make a few approaches to test your component in this case. I will mock up your FirebaseService because we should trust that this external dependency is working well.

We should write unite test to lock the code, and if something else on our source code change will break the tests.

Tests scope

We should write minimal around four tests to lock àuthentication.component.ts

  • it should create
  • it should be empty the displayed_random value from sessionStorage
  • execute location href when currentUser is truthy.
  • call fbService function when currentUser is falsy.
constructor (private fbService: FirebaseService) {
    sessionStorage.removeItem('displayed_random');
    // If user logged in, redirect to feed
    if (fbService.currentUser) {     
      window.location.href = "/feed";
    } else {
      this.fbService.instantiateUi();
    }
  }

when the current user is true

Inside your authentication.component.spec.ts

describe('AuthenticationComponent with current user', () => {
beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ AuthenticationComponent ],
      providers: [
        {
          provide: FirebaseService,
          useValue: {
             currentUser: true,
             instantiateUi: () => null
          }
        }
      ],
      schemas: [ NO_ERRORS_SCHEMA ]
    }).compileComponents();
    service = TestBed.inject(FirebaseService);
    fixture = TestBed.createComponent(AuthenticationComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });
it('should create', () => {
  expect(component).toBeTruthy();
});
it('should be session storage displayed_random empty on start', () => {
  expect(sessionStorage.getItem('displayed_random').toBeFalsy();
});
it('should change href value to feed', () => {
   expect(window.location.href).toBe('/feed');
});

when the current user is false

Inside your authentication.component.spec.ts

describe('AuthenticationComponent without current user', () => {
beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ AuthenticationComponent ],
      providers: [
        {
          provide: FirebaseService,
          useValue: {
             currentUser: false,
             instantiateUi: () => null
          }
        }
      ],
      schemas: [ NO_ERRORS_SCHEMA ]
    }).compileComponents();
    service = TestBed.inject(FirebaseService);
    fixture = TestBed.createComponent(AuthenticationComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });
it('should create', () => {
  expect(component).toBeTruthy();
});
it('should call instantiateUI when no current user', () => {
   const instantiateUiSpy = spyOn(service, 'instantiateUi');
   expect(instantiateUiSpy).toHaveBeenCalled();
});
Luis Reinoso
  • 748
  • 4
  • 13
  • No joy sadly. `Error: Expected spy instantiateUi to have been called.` is the error in the log. I omitted the `'should change href value to feed` test as that one is handled by a seperate test In addition, where are you getting `fbService` service from? Is that meant to be the `service` injection variable above? – shad0w May 12 '21 at 14:34
  • Also, including two `beforeEach` in that manner causes my Karma view to crash for some reason (it doesn't produce an output view of the tests and instead just shows `NOT FOUND`), so I have scaled back to only include the logged out spec – shad0w May 12 '21 at 15:00
  • the idea is to wrap tests inside two describe on the same file. (it mean two test suits). There aren't two beforeEach in the same test suit – Luis Reinoso May 12 '21 at 16:04
  • about fbService data: The service should be tested in its own service tests. In this way, I'm doing a mock for fbService for test `authentication.component.ts` – Luis Reinoso May 12 '21 at 16:06
  • My apologies, the `fbService` should be renamed to `service.` Because you are using `service = TestBed.inject(FirebaseService);` And it has to used inside here `const instantiateUiSpy = spyOn(fbService, 'instantiateUi');` so this become `const instantiateUiSpy = spyOn(service 'instantiateUi');` I will update the answer – Luis Reinoso May 12 '21 at 16:10