tutorbookapp/tutorbook

View on GitHub
cypress/support/commands.ts

Summary

Maintainability
A
1 hr
Test Coverage
import firebase from 'firebase/app';

import { Overrides } from 'cypress/plugins';

import 'firebase/auth';
import 'firebase/firestore';
import 'cypress-file-upload';
import '@percy/cypress';

/**
 * Add types to the existing global Cypress object.
 * @see {@link https://github.com/prescottprue/cypress-firebase/blob/master/src/attachCustomCommands.ts#L123}
 * @see {@link https://docs.cypress.io/guides/tooling/typescript-support.html#Types-for-custom-commands}
 */
declare global {
  /* eslint-disable-next-line @typescript-eslint/no-namespace */
  namespace Cypress {
    interface Chainable {
      login: (uid: string) => Chainable<null>;
      logout: () => Chainable<null>;
      setup: (overrides?: Overrides | null) => Chainable<undefined>;
      getBySel: (selector: string, args?: any) => Chainable<Element>;
      loading: (isLoading?: boolean, args?: any) => Chainable<undefined>;
    }
  }
}

const clientCredentials = {
  apiKey: Cypress.env('apiKey') as string,
  authDomain: Cypress.env('authDomain') as string,
  databaseURL: Cypress.env('databaseURL') as string,
  projectId: Cypress.env('projectId') as string,
  storageBucket: Cypress.env('storageBucket') as string,
  messagingSenderId: Cypress.env('messagingSenderId') as string,
  appId: Cypress.env('appId') as string,
  measurementId: Cypress.env('measurementId') as string,
};
if (!firebase.apps.length) firebase.initializeApp(clientCredentials);

function loginWithToken(token: string): Promise<string> {
  return new Promise<string>((resolve, reject): void => {
    firebase.auth().onIdTokenChanged((user): void => {
      if (user) resolve(user.getIdToken());
    });
    firebase.auth().signInWithCustomToken(token).catch(reject);
  });
}

function login(uid: string): Cypress.Chainable {
  if (firebase.auth().currentUser) cy.logout();
  return cy
    .task('login', uid)
    .then((token: string) => loginWithToken(token))
    .then((jwt: string) => cy.task('cookie', jwt))
    .then((cookie: string) => cy.setCookie('session', cookie));
}

function logout(): Cypress.Chainable<null> {
  return cy.wrap(
    new Promise<null>((resolve, reject): void => {
      firebase.auth().onAuthStateChanged((auth: unknown): void => {
        if (!auth) resolve(null);
      });
      firebase.auth().signOut().catch(reject);
    })
  );
}

function setup(overrides?: Overrides | null): void {
  cy.task('clear');

  if (overrides !== null) cy.task('seed', overrides);

  cy.intercept('GET', '/api/account').as('get-account');
  cy.intercept('PUT', '/api/account').as('update-account');

  cy.intercept('POST', '/api/matches').as('create-match');
  cy.intercept('GET', '/api/matches*').as('list-matches');
  cy.intercept('PUT', '/api/matches/*').as('update-match');

  cy.intercept('POST', '/api/meetings').as('create-meeting');
  cy.intercept('GET', '/api/meetings*').as('list-meetings');
  cy.intercept('PUT', '/api/meetings/*').as('update-meeting');

  cy.intercept('POST', '/api/users').as('create-user');
  cy.intercept('GET', '/api/users*', (req) => {
    delete req.headers['if-none-match'];
  }).as('list-users');
  cy.intercept('GET', '/api/users/*').as('get-user');
  cy.intercept('PUT', '/api/users/*').as('update-user');
}

function getBySel(
  selector: string,
  ...args: any
): Cypress.Chainable<JQuery<HTMLElement>> {
  return cy.get(`[data-cy=${selector}]`, ...args);
}

function loading(isLoading = true, ...args: any): void {
  if (isLoading) {
    cy.get('html', ...args).should('have.class', 'nprogress-busy');
    cy.get('#nprogress', ...args).should('exist');
  } else {
    cy.get('html', ...args).should('not.have.class', 'nprogress-busy');
    cy.get('#nprogress', ...args).should('not.exist');
  }
}

chai.Assertion.addMethod(
  'img',
  function img(src: string, w: number = 1080, q: number = 75): void {
    const element = (this._obj as JQuery<HTMLImageElement>)[0];

    // TODO: Even though I specify to Next.js to use a size of 24px, it seems to
    // re-use the already loaded 85px version of the photo. Why is that?
    const expected = `/_next/image?url=${encodeURIComponent(src)}`;
    const baseUrl = Cypress.config('baseUrl') || 'http://localhost:3000';

    this.assert(
      element.currentSrc.startsWith(`${baseUrl}${expected}`),
      'expected #{this} to start with Next.js image source #{exp}, but the source was #{act}',
      'expected #{this} not to start with Next.js image source #{exp}',
      `${baseUrl}${expected}`,
      element.currentSrc
    );
  }
);

Cypress.Commands.add('login', login);
Cypress.Commands.add('logout', logout);
Cypress.Commands.add('setup', setup);
Cypress.Commands.add('getBySel', getBySel);
Cypress.Commands.add('loading', loading);