diff --git a/__mocks__/@reach/router.js b/__mocks__/@reach/router.js new file mode 100644 index 000000000..75fb2af6e --- /dev/null +++ b/__mocks__/@reach/router.js @@ -0,0 +1,17 @@ +import { delay } from '../../src/utils/index'; + +const ReachRouter = jest.requireActual('@reach/router'); + +const defaultDelayInMilliseconds = 100; + +// eslint-disable-next-line no-unused-vars +const navigate = async (to, options) => { + await delay(defaultDelayInMilliseconds); + + return Promise.resolve(); +}; + +module.exports = { + ...ReachRouter, + navigate: jest.fn(navigate), +}; diff --git a/__mocks__/__tests__/gatsby-plugin-firebase/auth.signInAnonymously.test.js b/__mocks__/__tests__/gatsby-plugin-firebase/auth.signInAnonymously.test.js new file mode 100644 index 000000000..d159d44ea --- /dev/null +++ b/__mocks__/__tests__/gatsby-plugin-firebase/auth.signInAnonymously.test.js @@ -0,0 +1,35 @@ +import FirebaseStub, { AuthConstants } from '../../gatsby-plugin-firebase'; + +test('sets current user to anonymous user 1', async () => { + await FirebaseStub.auth().signInAnonymously(); + + const { currentUser } = FirebaseStub.auth(); + expect(currentUser).toBeTruthy(); + expect(currentUser.uid).toEqual(AuthConstants.anonymousUser1.uid); +}); + +test('calls onAuthStateChanged observer with anonymous user 1', async () => { + let user = null; + let error = null; + FirebaseStub.auth().onAuthStateChanged( + (_user) => { + user = _user; + }, + (_error) => { + error = _error; + }, + ); + + await FirebaseStub.auth().signInAnonymously(); + + expect(user).toBeTruthy(); + expect(user.uid).toEqual(AuthConstants.anonymousUser1.uid); + expect(error).toBeNull(); +}); + +test('returns anonymous user 1', async () => { + const user = await FirebaseStub.auth().signInAnonymously(); + + expect(user).toBeTruthy(); + expect(user.uid).toEqual(AuthConstants.anonymousUser1.uid); +}); diff --git a/__mocks__/__tests__/gatsby-plugin-firebase/auth.signInWithPopup.test.js b/__mocks__/__tests__/gatsby-plugin-firebase/auth.signInWithPopup.test.js new file mode 100644 index 000000000..e20cd1cab --- /dev/null +++ b/__mocks__/__tests__/gatsby-plugin-firebase/auth.signInWithPopup.test.js @@ -0,0 +1,54 @@ +import FirebaseStub, { AuthConstants } from '../../gatsby-plugin-firebase'; + +describe('with Google auth provider', () => { + test('sets current user to Google user 3', async () => { + await FirebaseStub.auth().signInWithPopup( + new FirebaseStub.auth.GoogleAuthProvider(), + ); + + const { currentUser } = FirebaseStub.auth(); + expect(currentUser).toBeTruthy(); + expect(currentUser.uid).toEqual(AuthConstants.googleUser3.uid); + }); + + test('sets current user provider data', async () => { + const provider = new FirebaseStub.auth.GoogleAuthProvider(); + await FirebaseStub.auth().signInWithPopup(provider); + + const { currentUser } = FirebaseStub.auth(); + expect(currentUser).toBeTruthy(); + expect(currentUser.providerData).toBeTruthy(); + expect(currentUser.providerData).toHaveLength(1); + expect(currentUser.providerData[0].providerId).toEqual(provider.providerId); + }); + + test('calls onAuthStateChanged observer with Google user 3', async () => { + let user = null; + let error = null; + FirebaseStub.auth().onAuthStateChanged( + (_user) => { + user = _user; + }, + (_error) => { + error = _error; + }, + ); + + await FirebaseStub.auth().signInWithPopup( + new FirebaseStub.auth.GoogleAuthProvider(), + ); + + expect(user).toBeTruthy(); + expect(user.uid).toEqual(AuthConstants.googleUser3.uid); + expect(error).toBeNull(); + }); + + test('returns Google user 3', async () => { + const user = await FirebaseStub.auth().signInWithPopup( + new FirebaseStub.auth.GoogleAuthProvider(), + ); + + expect(user).toBeTruthy(); + expect(user.uid).toEqual(AuthConstants.googleUser3.uid); + }); +}); diff --git a/__mocks__/__tests__/gatsby-plugin-firebase/auth.signOut.test.js b/__mocks__/__tests__/gatsby-plugin-firebase/auth.signOut.test.js new file mode 100644 index 000000000..06bd830bc --- /dev/null +++ b/__mocks__/__tests__/gatsby-plugin-firebase/auth.signOut.test.js @@ -0,0 +1,29 @@ +import FirebaseStub from '../../gatsby-plugin-firebase'; + +test('sets current user to null', async () => { + await FirebaseStub.auth().signInAnonymously(); + + await FirebaseStub.auth().signOut(); + + const { currentUser } = FirebaseStub.auth(); + expect(currentUser).toBeNull(); +}); + +test('calls onAuthStateChanged observer with null', async () => { + let user = null; + let error = null; + FirebaseStub.auth().onAuthStateChanged( + (_user) => { + user = _user; + }, + (_error) => { + error = _error; + }, + ); + await FirebaseStub.auth().signInAnonymously(); + + await FirebaseStub.auth().signOut(); + + expect(user).toBeNull(); + expect(error).toBeNull(); +}); diff --git a/__mocks__/__tests__/gatsby-plugin-firebase/auth.test.js b/__mocks__/__tests__/gatsby-plugin-firebase/auth.test.js index d70a5c1da..2ffd0aa3b 100644 --- a/__mocks__/__tests__/gatsby-plugin-firebase/auth.test.js +++ b/__mocks__/__tests__/gatsby-plugin-firebase/auth.test.js @@ -1,4 +1,4 @@ -import FirebaseStub, { AuthConstants } from '../../gatsby-plugin-firebase'; +import FirebaseStub from '../../gatsby-plugin-firebase'; test('reuses existing Auth instance', () => { const auth1 = FirebaseStub.auth(); @@ -9,32 +9,6 @@ test('reuses existing Auth instance', () => { expect(auth1.uuid).toEqual(auth2.uuid); }); -test('returns anonymous user 1 when signing in anonymously', async () => { - const user = await FirebaseStub.auth().signInAnonymously(); - - expect(user).toBeTruthy(); - expect(user).toEqual(AuthConstants.anonymousUser1); -}); - -test('calls onAuthStateChanged observer with anonymous user 1 when signing in anonymously', async () => { - let user = null; - let error = null; - FirebaseStub.auth().onAuthStateChanged( - (_user) => { - user = _user; - }, - (_error) => { - error = _error; - }, - ); - - await FirebaseStub.auth().signInAnonymously(); - - expect(user).toBeTruthy(); - expect(user).toEqual(AuthConstants.anonymousUser1); - expect(error).toBeNull(); -}); - test('onAuthStateChanged unsubscribe removes observer', () => { const observer = () => {}; const unsubscribe = FirebaseStub.auth().onAuthStateChanged(observer); @@ -49,3 +23,13 @@ test('onAuthStateChanged unsubscribe removes observer', () => { FirebaseStub.auth().onAuthStateChangedObservers.indexOf(observer), ).not.toBeGreaterThanOrEqual(0); }); + +test('current user delete calls signOut', async () => { + const mockSignOut = jest.spyOn(FirebaseStub.auth(), 'signOut'); + await FirebaseStub.auth().signInAnonymously(); + const { currentUser } = FirebaseStub.auth(); + + await currentUser.delete(); + + expect(mockSignOut).toHaveBeenCalledTimes(1); +}); diff --git a/__mocks__/__tests__/gatsby-plugin-firebase/database.test.js b/__mocks__/__tests__/gatsby-plugin-firebase/database.test.js index 8a97c0a94..e0f219af1 100644 --- a/__mocks__/__tests__/gatsby-plugin-firebase/database.test.js +++ b/__mocks__/__tests__/gatsby-plugin-firebase/database.test.js @@ -24,7 +24,7 @@ test('initializing data sets up resumes and users', async () => { const resumesDataSnapshot = await resumesRef.once('value'); const resumes = resumesDataSnapshot.val(); expect(resumes).toBeTruthy(); - expect(Object.keys(resumes)).toHaveLength(3); + expect(Object.keys(resumes)).toHaveLength(5); const demoStateResume1 = resumes[DatabaseConstants.demoStateResume1Id]; expect(demoStateResume1).toBeTruthy(); expect(demoStateResume1.id).toEqual(DatabaseConstants.demoStateResume1Id); @@ -33,20 +33,35 @@ test('initializing data sets up resumes and users', async () => { expect(demoStateResume2).toBeTruthy(); expect(demoStateResume2.id).toEqual(DatabaseConstants.demoStateResume2Id); expect(demoStateResume2.user).toEqual(DatabaseConstants.user2.uid); - const initialStateResume = resumes[DatabaseConstants.initialStateResumeId]; - expect(initialStateResume).toBeTruthy(); - expect(initialStateResume.id).toEqual(DatabaseConstants.initialStateResumeId); - expect(initialStateResume.user).toEqual(DatabaseConstants.user1.uid); + const initialStateResume1 = resumes[DatabaseConstants.initialStateResume1Id]; + expect(initialStateResume1).toBeTruthy(); + expect(initialStateResume1.id).toEqual( + DatabaseConstants.initialStateResume1Id, + ); + expect(initialStateResume1.user).toEqual(DatabaseConstants.user1.uid); + const demoStateResume3 = resumes[DatabaseConstants.demoStateResume3Id]; + expect(demoStateResume3).toBeTruthy(); + expect(demoStateResume3.id).toEqual(DatabaseConstants.demoStateResume3Id); + expect(demoStateResume3.user).toEqual(DatabaseConstants.user3.uid); + const initialStateResume2 = resumes[DatabaseConstants.initialStateResume2Id]; + expect(initialStateResume2).toBeTruthy(); + expect(initialStateResume2.id).toEqual( + DatabaseConstants.initialStateResume2Id, + ); + expect(initialStateResume2.user).toEqual(DatabaseConstants.user3.uid); const usersRef = FirebaseStub.database().ref(DatabaseConstants.usersPath); const usersDataSnapshot = await usersRef.once('value'); const users = usersDataSnapshot.val(); expect(users).toBeTruthy(); - expect(Object.keys(users)).toHaveLength(2); + expect(Object.keys(users)).toHaveLength(3); const anonymousUser1 = users[DatabaseConstants.user1.uid]; expect(anonymousUser1).toBeTruthy(); expect(anonymousUser1).toEqual(DatabaseConstants.user1); const anonymousUser2 = users[DatabaseConstants.user2.uid]; expect(anonymousUser2).toBeTruthy(); expect(anonymousUser2).toEqual(DatabaseConstants.user2); + const googleUser3 = users[DatabaseConstants.user3.uid]; + expect(googleUser3).toBeTruthy(); + expect(googleUser3).toEqual(DatabaseConstants.user3); }); diff --git a/__mocks__/__tests__/gatsby-plugin-firebase/functions.test.js b/__mocks__/__tests__/gatsby-plugin-firebase/functions.test.js new file mode 100644 index 000000000..6cf12ab4c --- /dev/null +++ b/__mocks__/__tests__/gatsby-plugin-firebase/functions.test.js @@ -0,0 +1,19 @@ +import FirebaseStub from '../../gatsby-plugin-firebase'; + +test('reuses existing Functions instance', () => { + const functions1 = FirebaseStub.functions(); + const functions2 = FirebaseStub.functions(); + + expect(functions1.uuid).toBeTruthy(); + expect(functions2.uuid).toBeTruthy(); + expect(functions1.uuid).toEqual(functions2.uuid); +}); + +test('deleteUser function returns true', async () => { + const deleteUser = FirebaseStub.functions().httpsCallable('deleteUser'); + + const result = await deleteUser(); + + expect(result).toBeTruthy(); + expect(result.data).toEqual(true); +}); diff --git a/__mocks__/gatsby-plugin-firebase.js b/__mocks__/gatsby-plugin-firebase.js index ea5af4ab7..34e1317b1 100644 --- a/__mocks__/gatsby-plugin-firebase.js +++ b/__mocks__/gatsby-plugin-firebase.js @@ -2,6 +2,9 @@ import Auth from './gatsby-plugin-firebase/auth/auth'; import AuthConstants from './gatsby-plugin-firebase/constants/auth'; import Database from './gatsby-plugin-firebase/database/database'; import DatabaseConstants from './gatsby-plugin-firebase/constants/database'; +import Functions from './gatsby-plugin-firebase/functions/functions'; +import FunctionsConstants from './gatsby-plugin-firebase/constants/functions'; +import GoogleAuthProvider from './gatsby-plugin-firebase/auth/googleAuthProvider'; class FirebaseStub { static auth() { @@ -11,8 +14,14 @@ class FirebaseStub { static database() { return Database.instance; } + + static functions() { + return Functions.instance; + } } +FirebaseStub.auth.GoogleAuthProvider = GoogleAuthProvider; + FirebaseStub.database.ServerValue = {}; Object.defineProperty(FirebaseStub.database.ServerValue, 'TIMESTAMP', { get() { @@ -21,4 +30,4 @@ Object.defineProperty(FirebaseStub.database.ServerValue, 'TIMESTAMP', { }); export default FirebaseStub; -export { AuthConstants, DatabaseConstants }; +export { AuthConstants, DatabaseConstants, FunctionsConstants }; diff --git a/__mocks__/gatsby-plugin-firebase/auth/auth.js b/__mocks__/gatsby-plugin-firebase/auth/auth.js index 4d443bbec..93273ffae 100644 --- a/__mocks__/gatsby-plugin-firebase/auth/auth.js +++ b/__mocks__/gatsby-plugin-firebase/auth/auth.js @@ -2,7 +2,10 @@ import { v4 as uuidv4 } from 'uuid'; import { delay } from '../../../src/utils/index'; +import AuthProvider from './authProvider'; import Constants from '../constants/auth'; +import GoogleAuthProvider from './googleAuthProvider'; +import User from './user'; const singleton = Symbol(''); const singletonEnforcer = Symbol(''); @@ -14,6 +17,7 @@ class Auth { } this._uuid = uuidv4(); + this._currentUser = null; this._onAuthStateChangedObservers = []; } @@ -25,6 +29,10 @@ class Auth { return this[singleton]; } + get currentUser() { + return this._currentUser; + } + get uuid() { return this._uuid; } @@ -37,19 +45,81 @@ class Auth { this.onAuthStateChangedObservers.push(observer); return () => { - this._onAuthStateChangedObservers = - this.onAuthStateChangedObservers.filter((obs) => obs !== observer); + this._onAuthStateChangedObservers = this.onAuthStateChangedObservers.filter( + (obs) => obs !== observer, + ); }; } async signInAnonymously() { const user = Constants.anonymousUser1; + this._currentUser = new User( + user.displayName, + user.email, + user.providerId, + user.uid, + user.isAnonymous, + this.signOut.bind(this), + ); + await delay(Constants.defaultDelayInMilliseconds); - this.onAuthStateChangedObservers.forEach((observer) => observer(user)); + this.onAuthStateChangedObservers.forEach((observer) => + observer(this._currentUser), + ); - return user; + return this._currentUser; + } + + /** + * Authenticates with popup. + * + * @param {AuthProvider} provider The provider to authenticate. + */ + async signInWithPopup(provider) { + if (!provider) { + throw new Error('provider must be provided.'); + } else if (!(provider instanceof AuthProvider)) { + throw new Error('provider should be an AuthProvider.'); + } + + if (!(provider instanceof GoogleAuthProvider)) { + throw new Error( + `${provider.constructor.name} is currently not supported.`, + ); + } + + const user = Constants.googleUser3; + + this._currentUser = new User( + user.displayName, + user.email, + user.providerId, + user.uid, + user.isAnonymous, + this.signOut.bind(this), + ); + + await delay(Constants.defaultDelayInMilliseconds); + + this.onAuthStateChangedObservers.forEach((observer) => + observer(this._currentUser), + ); + + return this._currentUser; + } + + async signOut() { + if (this._currentUser === null) { + return; + } + + this._currentUser = null; + + await delay(Constants.defaultDelayInMilliseconds); + + this.onAuthStateChangedObservers.forEach((observer) => observer(null)); } } diff --git a/__mocks__/gatsby-plugin-firebase/auth/authProvider.js b/__mocks__/gatsby-plugin-firebase/auth/authProvider.js new file mode 100644 index 000000000..e9b77a6cf --- /dev/null +++ b/__mocks__/gatsby-plugin-firebase/auth/authProvider.js @@ -0,0 +1,23 @@ +/* eslint-disable no-underscore-dangle */ +class AuthProvider { + /** + * Creates a new auth provider. + * + * @param {string} providerId Provider ID. + */ + constructor(providerId) { + if (!providerId) { + throw new Error('providerId must be provided.'); + } else if (typeof providerId !== 'string') { + throw new Error('providerId should be a string.'); + } else { + this._providerId = providerId; + } + } + + get providerId() { + return this._providerId; + } +} + +export default AuthProvider; diff --git a/__mocks__/gatsby-plugin-firebase/auth/googleAuthProvider.js b/__mocks__/gatsby-plugin-firebase/auth/googleAuthProvider.js new file mode 100644 index 000000000..f98ea3c66 --- /dev/null +++ b/__mocks__/gatsby-plugin-firebase/auth/googleAuthProvider.js @@ -0,0 +1,10 @@ +import AuthProvider from './authProvider'; +import Constants from '../constants/auth'; + +class GoogleAuthProvider extends AuthProvider { + constructor() { + super(Constants.googleAuthProviderId); + } +} + +export default GoogleAuthProvider; diff --git a/__mocks__/gatsby-plugin-firebase/auth/user.js b/__mocks__/gatsby-plugin-firebase/auth/user.js new file mode 100644 index 000000000..05843cb47 --- /dev/null +++ b/__mocks__/gatsby-plugin-firebase/auth/user.js @@ -0,0 +1,67 @@ +/* eslint-disable no-underscore-dangle */ +import Constants from '../constants/auth'; +// eslint-disable-next-line no-unused-vars +import AuthProvider from './authProvider'; +import UserInfo from './userInfo'; +import { delay } from '../../../src/utils/index'; + +class User extends UserInfo { + /** + * Creates a new user. + * + * @param {string|null} displayName Display name. + * @param {string|null} email Email. + * @param {string} providerId Auth provider ID. + * @param {string} uid The user's unique ID. + * @param {boolean} isAnonymous Is anonymous. + * @param {function():Promise} deleteUser Delete user callback. + */ + constructor(displayName, email, providerId, uid, isAnonymous, deleteUser) { + super(displayName, email, providerId, uid); + + if (!deleteUser) { + throw new Error('deleteUser must be provided.'); + } else if (typeof deleteUser !== 'function') { + throw new Error('deleteUser should be a function.'); + } else { + this._deleteUser = deleteUser; + } + + this._isAnonymous = isAnonymous; + + this._providerData = []; + if (!isAnonymous) { + this._providerData.push( + new UserInfo(displayName, email, providerId, uid), + ); + } + } + + get isAnonymous() { + return this._isAnonymous; + } + + get providerData() { + return this._providerData; + } + + async delete() { + await delay(Constants.defaultDelayInMilliseconds); + + await this._deleteUser(); + } + + /** + * Reauthenticates the user with popup. + * + * @param {AuthProvider} provider The provider to authenticate. + */ + // eslint-disable-next-line no-unused-vars + async reauthenticateWithPopup(provider) { + await delay(Constants.defaultDelayInMilliseconds); + + return this; + } +} + +export default User; diff --git a/__mocks__/gatsby-plugin-firebase/auth/userInfo.js b/__mocks__/gatsby-plugin-firebase/auth/userInfo.js new file mode 100644 index 000000000..c648698de --- /dev/null +++ b/__mocks__/gatsby-plugin-firebase/auth/userInfo.js @@ -0,0 +1,47 @@ +/* eslint-disable no-underscore-dangle */ +class UserInfo { + /** + * Creates a new user profile information. + * + * @param {string|null} displayName Display name. + * @param {string|null} email Email. + * @param {string} providerId Auth provider ID. + * @param {string} uid The user's unique ID. + */ + constructor(displayName, email, providerId, uid) { + if (!uid) { + throw new Error('uid must be provided.'); + } else if (typeof uid !== 'string') { + throw new Error('uid should be a string.'); + } else { + this._uid = uid; + } + + if (typeof providerId !== 'string') { + throw new Error('providerId should be a string.'); + } else { + this._providerId = providerId; + } + + this._displayName = displayName; + this._email = email; + } + + get displayName() { + return this._displayName; + } + + get email() { + return this._email; + } + + get providerId() { + return this._providerId; + } + + get uid() { + return this._uid; + } +} + +export default UserInfo; diff --git a/__mocks__/gatsby-plugin-firebase/constants/auth.js b/__mocks__/gatsby-plugin-firebase/constants/auth.js index 13f8fcc40..072d04a7d 100644 --- a/__mocks__/gatsby-plugin-firebase/constants/auth.js +++ b/__mocks__/gatsby-plugin-firebase/constants/auth.js @@ -1,22 +1,36 @@ +const googleAuthProviderId = 'google.com'; + const anonymousUser1 = { displayName: 'Anonymous User 1', email: 'anonymous1@noemail.com', isAnonymous: true, - name: 'Anonymous 1', - uid: 'anonym123', + providerId: '', + uid: 'anonym1', }; const anonymousUser2 = { displayName: 'Anonymous User 2', email: 'anonymous2@noemail.com', isAnonymous: true, - name: 'Anonymous 2', - uid: 'anonym456', + providerId: '', + uid: 'anonym2', +}; + +const googleUser3 = { + displayName: 'Google User 3', + email: 'google3@noemail.com', + isAnonymous: false, + providerId: googleAuthProviderId, + uid: 'google3', }; const defaultDelayInMilliseconds = 100; class Auth { + static get googleAuthProviderId() { + return googleAuthProviderId; + } + static get anonymousUser1() { return anonymousUser1; } @@ -25,6 +39,10 @@ class Auth { return anonymousUser2; } + static get googleUser3() { + return googleUser3; + } + static get defaultDelayInMilliseconds() { return defaultDelayInMilliseconds; } diff --git a/__mocks__/gatsby-plugin-firebase/constants/database.js b/__mocks__/gatsby-plugin-firebase/constants/database.js index 62a5459e0..e4417175b 100644 --- a/__mocks__/gatsby-plugin-firebase/constants/database.js +++ b/__mocks__/gatsby-plugin-firebase/constants/database.js @@ -9,7 +9,9 @@ const connectedPath = '.info/connected'; const demoStateResume1Id = 'demo_1'; const demoStateResume2Id = 'demo_2'; -const initialStateResumeId = 'initst'; +const demoStateResume3Id = 'demo_3'; +const initialStateResume1Id = 'init_1'; +const initialStateResume2Id = 'init_2'; const user1 = { uid: AuthConstants.anonymousUser1.uid, @@ -19,6 +21,10 @@ const user2 = { uid: AuthConstants.anonymousUser2.uid, isAnonymous: AuthConstants.anonymousUser2.isAnonymous, }; +const user3 = { + uid: AuthConstants.googleUser3.uid, + isAnonymous: AuthConstants.googleUser3.isAnonymous, +}; const defaultDelayInMilliseconds = 100; @@ -51,8 +57,16 @@ class Database { return demoStateResume2Id; } - static get initialStateResumeId() { - return initialStateResumeId; + static get demoStateResume3Id() { + return demoStateResume3Id; + } + + static get initialStateResume1Id() { + return initialStateResume1Id; + } + + static get initialStateResume2Id() { + return initialStateResume2Id; } static get user1() { @@ -63,6 +77,10 @@ class Database { return user2; } + static get user3() { + return user3; + } + static get defaultDelayInMilliseconds() { return defaultDelayInMilliseconds; } diff --git a/__mocks__/gatsby-plugin-firebase/constants/functions.js b/__mocks__/gatsby-plugin-firebase/constants/functions.js new file mode 100644 index 000000000..6e238cdc5 --- /dev/null +++ b/__mocks__/gatsby-plugin-firebase/constants/functions.js @@ -0,0 +1,15 @@ +const deleteUserFunctionName = 'deleteUser'; + +const defaultDelayInMilliseconds = 100; + +class Functions { + static get deleteUserFunctionName() { + return deleteUserFunctionName; + } + + static get defaultDelayInMilliseconds() { + return defaultDelayInMilliseconds; + } +} + +export default Functions; diff --git a/__mocks__/gatsby-plugin-firebase/database/database.js b/__mocks__/gatsby-plugin-firebase/database/database.js index 081cc43a7..644ba16ea 100644 --- a/__mocks__/gatsby-plugin-firebase/database/database.js +++ b/__mocks__/gatsby-plugin-firebase/database/database.js @@ -101,9 +101,9 @@ class Database { initializeData() { const resumes = {}; + const date = new Date('December 15, 2020 11:20:25'); const demoStateResume1 = readFile('../../../src/data/demoState.json'); - const date = new Date('December 15, 2020 11:20:25'); demoStateResume1.updatedAt = date.valueOf(); date.setMonth(date.getMonth() - 2); demoStateResume1.createdAt = date.valueOf(); @@ -114,11 +114,24 @@ class Database { demoStateResume2.user = DatabaseConstants.user2.uid; resumes[DatabaseConstants.demoStateResume2Id] = demoStateResume2; - const initialStateResume = readFile('../../../src/data/initialState.json'); - initialStateResume.updatedAt = date.valueOf(); - initialStateResume.createdAt = date.valueOf(); - initialStateResume.user = DatabaseConstants.user1.uid; - resumes[DatabaseConstants.initialStateResumeId] = initialStateResume; + const initialStateResume1 = readFile('../../../src/data/initialState.json'); + initialStateResume1.updatedAt = date.valueOf(); + initialStateResume1.createdAt = date.valueOf(); + initialStateResume1.user = DatabaseConstants.user1.uid; + resumes[DatabaseConstants.initialStateResume1Id] = initialStateResume1; + + const demoStateResume3 = readFile('../../../src/data/demoState.json'); + demoStateResume3.updatedAt = date.valueOf(); + date.setMonth(date.getMonth() - 2); + demoStateResume3.createdAt = date.valueOf(); + demoStateResume3.user = DatabaseConstants.user3.uid; + resumes[DatabaseConstants.demoStateResume3Id] = demoStateResume3; + + const initialStateResume2 = readFile('../../../src/data/initialState.json'); + initialStateResume2.updatedAt = date.valueOf(); + initialStateResume2.createdAt = date.valueOf(); + initialStateResume2.user = DatabaseConstants.user3.uid; + resumes[DatabaseConstants.initialStateResume2Id] = initialStateResume2; Object.keys(resumes).forEach((key) => { const resume = resumes[key]; @@ -131,6 +144,7 @@ class Database { const users = {}; users[DatabaseConstants.user1.uid] = DatabaseConstants.user1; users[DatabaseConstants.user2.uid] = DatabaseConstants.user2; + users[DatabaseConstants.user3.uid] = DatabaseConstants.user3; this._data[DatabaseConstants.usersPath] = users; } diff --git a/__mocks__/gatsby-plugin-firebase/functions/functions.js b/__mocks__/gatsby-plugin-firebase/functions/functions.js new file mode 100644 index 000000000..3a4d6b1dd --- /dev/null +++ b/__mocks__/gatsby-plugin-firebase/functions/functions.js @@ -0,0 +1,54 @@ +/* eslint-disable no-underscore-dangle */ +import { v4 as uuidv4 } from 'uuid'; + +import FunctionsConstants from '../constants/functions'; +import HttpsCallableResult from './httpsCallableResult'; +import { delay } from '../../../src/utils/index'; + +const singleton = Symbol(''); +const singletonEnforcer = Symbol(''); + +const deleteUser = async () => { + await delay(FunctionsConstants.defaultDelayInMilliseconds); + + return new HttpsCallableResult(true); +}; + +class Functions { + constructor(enforcer) { + if (enforcer !== singletonEnforcer) { + throw new Error('Cannot construct singleton'); + } + + this._uuid = uuidv4(); + + this._httpsCallables = {}; + this._httpsCallables[ + FunctionsConstants.deleteUserFunctionName + ] = deleteUser; + } + + static get instance() { + if (!this[singleton]) { + this[singleton] = new Functions(singletonEnforcer); + } + + return this[singleton]; + } + + get uuid() { + return this._uuid; + } + + httpsCallable(name) { + if (!name) { + throw new Error('name must be provided.'); + } else if (typeof name !== 'string') { + throw new Error('name should be a string.'); + } + + return this._httpsCallables[name]; + } +} + +export default Functions; diff --git a/__mocks__/gatsby-plugin-firebase/functions/httpsCallableResult.js b/__mocks__/gatsby-plugin-firebase/functions/httpsCallableResult.js new file mode 100644 index 000000000..d35ba4eee --- /dev/null +++ b/__mocks__/gatsby-plugin-firebase/functions/httpsCallableResult.js @@ -0,0 +1,19 @@ +/* eslint-disable no-underscore-dangle */ +import { v4 as uuidv4 } from 'uuid'; + +class HttpsCallableResult { + constructor(data) { + this._uuid = uuidv4(); + this._data = data; + } + + get data() { + return this._data; + } + + get uuid() { + return this._uuid; + } +} + +export default HttpsCallableResult; diff --git a/__mocks__/gatsby.js b/__mocks__/gatsby.js index bac68ffb9..996f2e576 100644 --- a/__mocks__/gatsby.js +++ b/__mocks__/gatsby.js @@ -4,24 +4,18 @@ import { delay } from '../src/utils/index'; const Gatsby = jest.requireActual('gatsby'); -const fluidImageShapes = [ - { - aspectRatio: 2, - src: 'test_image.jpg', - srcSet: 'some srcSet', - srcSetWebp: 'some srcSetWebp', - sizes: '(max-width: 600px) 100vw, 600px', - base64: 'string_of_base64', +const imageData = { + images: { + fallback: { + src: `image_src.jpg`, + srcSet: `image_src_set.jpg 1x`, + }, }, - { - aspectRatio: 3, - src: 'test_image_2.jpg', - srcSet: 'some other srcSet', - srcSetWebp: 'some other srcSetWebp', - sizes: '(max-width: 400px) 100vw, 400px', - base64: 'string_of_base64', - }, -]; + layout: `fixed`, + width: 1, + height: 2, +}; +const childImageSharp = { gatsbyImageData: imageData }; const useStaticQuery = () => ({ site: { @@ -33,39 +27,25 @@ const useStaticQuery = () => ({ }, }, file: { - childImageSharp: { - fluid: fluidImageShapes[0], - }, + childImageSharp, }, onyx: { - childImageSharp: { - fluid: fluidImageShapes[0], - }, + childImageSharp, }, pikachu: { - childImageSharp: { - fluid: fluidImageShapes[1], - }, + childImageSharp, }, gengar: { - childImageSharp: { - fluid: fluidImageShapes[0], - }, + childImageSharp, }, castform: { - childImageSharp: { - fluid: fluidImageShapes[1], - }, + childImageSharp, }, glalie: { - childImageSharp: { - fluid: fluidImageShapes[0], - }, + childImageSharp, }, celebi: { - childImageSharp: { - fluid: fluidImageShapes[1], - }, + childImageSharp, }, }); diff --git a/jest-preprocess.js b/jest-preprocess.js index c5af69317..52a941801 100644 --- a/jest-preprocess.js +++ b/jest-preprocess.js @@ -2,4 +2,4 @@ const babelOptions = { presets: ['babel-preset-gatsby'], }; -module.exports = require('babel-jest').createTransformer(babelOptions); +module.exports = require('babel-jest').default.createTransformer(babelOptions); diff --git a/jest.config.js b/jest.config.js index 2d83acea7..11a69f511 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,5 @@ +const esModules = ['gatsby', 'nanoevents'].join('|'); + module.exports = { testRegex: '/*.test.js$', collectCoverage: true, @@ -26,11 +28,12 @@ module.exports = { '.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': `/__mocks__/file-mock.js`, }, testPathIgnorePatterns: [`node_modules`, `\\.cache`], - transformIgnorePatterns: [`node_modules/(?!(gatsby)/)`], + transformIgnorePatterns: [`node_modules/(?!${esModules})`], globals: { __PATH_PREFIX__: ``, }, testURL: `http://localhost`, setupFiles: [`/loadershim.js`], setupFilesAfterEnv: [`/jest.setup.js`], + testEnvironment: 'jsdom', }; diff --git a/src/contexts/UserContext.js b/src/contexts/UserContext.js index 314ba58d3..bf507e0c2 100644 --- a/src/contexts/UserContext.js +++ b/src/contexts/UserContext.js @@ -66,8 +66,8 @@ const UserProvider = ({ children }) => { } }; - const logout = () => { - firebase.auth().signOut(); + const logout = async () => { + await firebase.auth().signOut(); localStorage.removeItem('user'); setUser(null); navigate('/'); @@ -123,7 +123,7 @@ const UserProvider = ({ children }) => { } catch (error) { toast.error(error.message); } finally { - logout(); + await logout(); toast( "It's sad to see you go, but we respect your privacy. All your data has been deleted successfully. Hope to see you again soon!", ); diff --git a/src/pages/app/__tests__/builder.dataPersistence.test.js b/src/pages/app/__tests__/builder.dataPersistence.test.js index 978e34ddf..caad478b3 100644 --- a/src/pages/app/__tests__/builder.dataPersistence.test.js +++ b/src/pages/app/__tests__/builder.dataPersistence.test.js @@ -7,13 +7,14 @@ import { setupAndWait, } from './helpers/builder'; -const testTimeoutInMilliseconds = 20000; +const testTimeoutInMilliseconds = 30000; jest.setTimeout(testTimeoutInMilliseconds); test('when input value is changed, updates database', async () => { const resumeId = DatabaseConstants.demoStateResume1Id; const { mockDatabaseUpdateFunction } = await setupAndWait( resumeId, + false, true, true, ); diff --git a/src/pages/app/__tests__/builder.rendering.test.js b/src/pages/app/__tests__/builder.rendering.test.js index 950296522..3a5e82bdb 100644 --- a/src/pages/app/__tests__/builder.rendering.test.js +++ b/src/pages/app/__tests__/builder.rendering.test.js @@ -20,6 +20,7 @@ jest.setTimeout(testTimeoutInMilliseconds); test('renders first and last name', async () => { const { resume } = await setupAndWait( DatabaseConstants.demoStateResume1Id, + false, true, true, ); @@ -53,7 +54,7 @@ test('renders loading screen', async () => { }); test('if resume is in initial state, renders load demo data notification', async () => { - await setup(DatabaseConstants.initialStateResumeId); + await setup(DatabaseConstants.initialStateResume1Id); const notification = await screen.findByRole('alert'); expect( diff --git a/src/pages/app/__tests__/builder.settings.deleteAccount.test.js b/src/pages/app/__tests__/builder.settings.deleteAccount.test.js new file mode 100644 index 000000000..72bc54fe9 --- /dev/null +++ b/src/pages/app/__tests__/builder.settings.deleteAccount.test.js @@ -0,0 +1,284 @@ +import { navigate as mockNavigateFunction } from '@reach/router'; +import { toast } from 'react-toastify'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; + +import FirebaseStub, { + AuthConstants, + DatabaseConstants, + FunctionsConstants, +} from 'gatsby-plugin-firebase'; + +import { setupAndWait } from './helpers/builder'; +import { delay } from '../../../utils/index'; + +const testTimeoutInMilliseconds = 60000; +jest.setTimeout(testTimeoutInMilliseconds); + +async function setup(signInWithGoogle, failReauthentication) { + const resumeId = signInWithGoogle + ? DatabaseConstants.demoStateResume3Id + : DatabaseConstants.demoStateResume1Id; + + await setupAndWait(resumeId, signInWithGoogle, true, true); + + let mockFirebaseCurrentUserReauthenticateWithPopupErrorMessage; + let mockFirebaseCurrentUserReauthenticateWithPopup; + if (failReauthentication) { + mockFirebaseCurrentUserReauthenticateWithPopupErrorMessage = + 'Error occurred while reauthenticating.'; + mockFirebaseCurrentUserReauthenticateWithPopup = jest.fn(async () => { + await delay(AuthConstants.defaultDelayInMilliseconds); + throw new Error( + mockFirebaseCurrentUserReauthenticateWithPopupErrorMessage, + ); + }); + FirebaseStub.auth().currentUser.reauthenticateWithPopup = mockFirebaseCurrentUserReauthenticateWithPopup; + } else { + mockFirebaseCurrentUserReauthenticateWithPopupErrorMessage = null; + mockFirebaseCurrentUserReauthenticateWithPopup = jest.spyOn( + FirebaseStub.auth().currentUser, + 'reauthenticateWithPopup', + ); + } + + const mockFirebaseDeleteUserCloudFunction = jest.fn(async () => { + await delay(FunctionsConstants.defaultDelayInMilliseconds); + }); + const mockFirebaseFunctionsHttpsCallable = jest.fn((name) => + name === FunctionsConstants.deleteUserFunctionName + ? mockFirebaseDeleteUserCloudFunction + : undefined, + ); + FirebaseStub.functions().httpsCallable = mockFirebaseFunctionsHttpsCallable; + + const mockFirebaseCurrentUserDelete = jest.spyOn( + FirebaseStub.auth().currentUser, + 'delete', + ); + + const mockFirebaseAuthSignOutErrorMessage = + 'Error occurred while signing out.'; + const mockFirebaseAuthSignOut = jest.fn(async () => { + await delay(AuthConstants.defaultDelayInMilliseconds); + throw new Error(mockFirebaseAuthSignOutErrorMessage); + }); + FirebaseStub.auth().signOut = mockFirebaseAuthSignOut; + + // eslint-disable-next-line no-unused-vars + const mockToastError = jest.fn((content, options) => {}); + toast.error = mockToastError; + + const deleteAccountButtonText = /Delete Account/i; + const areYouSureButtonText = /Are you sure?/i; + const deleteAccountButton = screen.getByRole('button', { + name: deleteAccountButtonText, + }); + const waitForDeleteAccountButtonTextToChangeTo = async (text) => { + await waitFor(() => expect(deleteAccountButton).toHaveTextContent(text), { + timeout: 30000, + }); + }; + + return { + deleteAccountButton, + areYouSureButtonText, + deleteAccountButtonText, + waitForDeleteAccountButtonTextToChangeTo, + mockFirebaseCurrentUserReauthenticateWithPopup, + mockFirebaseCurrentUserReauthenticateWithPopupErrorMessage, + mockFirebaseDeleteUserCloudFunction, + mockFirebaseCurrentUserDelete, + mockFirebaseAuthSignOut, + mockFirebaseAuthSignOutErrorMessage, + mockToastError, + }; +} + +test('prompts for confirmation', async () => { + const { + deleteAccountButton, + areYouSureButtonText, + waitForDeleteAccountButtonTextToChangeTo, + mockFirebaseDeleteUserCloudFunction, + mockFirebaseCurrentUserDelete, + mockFirebaseAuthSignOut, + mockToastError, + } = await setup(false, false); + + fireEvent.click(deleteAccountButton); + + await expect( + waitForDeleteAccountButtonTextToChangeTo(areYouSureButtonText), + ).resolves.toBeUndefined(); + expect(mockFirebaseDeleteUserCloudFunction).not.toHaveBeenCalled(); + expect(mockFirebaseCurrentUserDelete).not.toHaveBeenCalled(); + expect(mockFirebaseAuthSignOut).not.toHaveBeenCalled(); + expect(mockNavigateFunction).not.toHaveBeenCalled(); + expect(mockToastError).not.toHaveBeenCalled(); +}); + +test('calls Firebase delete user cloud function', async () => { + const { + deleteAccountButton, + deleteAccountButtonText, + mockFirebaseDeleteUserCloudFunction, + waitForDeleteAccountButtonTextToChangeTo, + } = await setup(false, false); + + fireEvent.click(deleteAccountButton); + fireEvent.click(deleteAccountButton); + + await waitForDeleteAccountButtonTextToChangeTo(deleteAccountButtonText); + + await waitFor(() => + expect(mockFirebaseDeleteUserCloudFunction).toHaveBeenCalledTimes(1), + ); +}); + +test('calls Firebase current user delete', async () => { + const { + deleteAccountButton, + deleteAccountButtonText, + mockFirebaseCurrentUserDelete, + waitForDeleteAccountButtonTextToChangeTo, + } = await setup(false, false); + + fireEvent.click(deleteAccountButton); + fireEvent.click(deleteAccountButton); + + await waitForDeleteAccountButtonTextToChangeTo(deleteAccountButtonText); + + await waitFor(() => + expect(mockFirebaseCurrentUserDelete).toHaveBeenCalledTimes(1), + ); +}); + +test('calls Firebase auth sign out', async () => { + const { + deleteAccountButton, + deleteAccountButtonText, + mockFirebaseAuthSignOut, + waitForDeleteAccountButtonTextToChangeTo, + } = await setup(false, false); + + fireEvent.click(deleteAccountButton); + fireEvent.click(deleteAccountButton); + + await waitForDeleteAccountButtonTextToChangeTo(deleteAccountButtonText); + + await waitFor(() => expect(mockFirebaseAuthSignOut).toHaveBeenCalledTimes(1)); +}); + +describe('if an error occurs while signing out the current user', () => { + test('does not navigate away', async () => { + const { + deleteAccountButton, + deleteAccountButtonText, + waitForDeleteAccountButtonTextToChangeTo, + } = await setup(false, false); + + fireEvent.click(deleteAccountButton); + fireEvent.click(deleteAccountButton); + + await waitForDeleteAccountButtonTextToChangeTo(deleteAccountButtonText); + + expect(mockNavigateFunction).not.toHaveBeenCalled(); + }); + + test('displays error notifications', async () => { + const { + deleteAccountButton, + deleteAccountButtonText, + mockFirebaseAuthSignOutErrorMessage, + waitForDeleteAccountButtonTextToChangeTo, + mockToastError, + } = await setup(false, false); + + fireEvent.click(deleteAccountButton); + fireEvent.click(deleteAccountButton); + + await waitForDeleteAccountButtonTextToChangeTo(deleteAccountButtonText); + + expect(mockToastError).toHaveBeenCalledTimes(2); + expect(mockToastError).toHaveBeenNthCalledWith( + 1, + mockFirebaseAuthSignOutErrorMessage, + ); + expect(mockToastError).toHaveBeenNthCalledWith( + 2, + 'An error occurred deleting your account.', + ); + }); +}); + +describe('if the current user is signed in with Google', () => { + test('reauthenticates the user', async () => { + const googleAuthProvider = new FirebaseStub.auth.GoogleAuthProvider(); + const { + deleteAccountButton, + deleteAccountButtonText, + mockFirebaseCurrentUserReauthenticateWithPopup, + waitForDeleteAccountButtonTextToChangeTo, + } = await setup(true, false); + + fireEvent.click(deleteAccountButton); + fireEvent.click(deleteAccountButton); + + await waitForDeleteAccountButtonTextToChangeTo(deleteAccountButtonText); + + expect( + mockFirebaseCurrentUserReauthenticateWithPopup, + ).toHaveBeenCalledTimes(1); + expect(mockFirebaseCurrentUserReauthenticateWithPopup).toHaveBeenCalledWith( + googleAuthProvider, + ); + }); + + describe('and reauthentication fails', () => { + test('does not proceed further', async () => { + const { + deleteAccountButton, + deleteAccountButtonText, + waitForDeleteAccountButtonTextToChangeTo, + mockFirebaseDeleteUserCloudFunction, + mockFirebaseCurrentUserDelete, + mockFirebaseAuthSignOut, + } = await setup(true, true); + + fireEvent.click(deleteAccountButton); + fireEvent.click(deleteAccountButton); + + await waitForDeleteAccountButtonTextToChangeTo(deleteAccountButtonText); + + expect(mockFirebaseDeleteUserCloudFunction).not.toHaveBeenCalled(); + expect(mockFirebaseCurrentUserDelete).not.toHaveBeenCalled(); + expect(mockFirebaseAuthSignOut).not.toHaveBeenCalled(); + expect(mockNavigateFunction).not.toHaveBeenCalled(); + }); + + test('displays error notifications', async () => { + const { + deleteAccountButton, + deleteAccountButtonText, + waitForDeleteAccountButtonTextToChangeTo, + mockFirebaseCurrentUserReauthenticateWithPopupErrorMessage, + mockToastError, + } = await setup(true, true); + + fireEvent.click(deleteAccountButton); + fireEvent.click(deleteAccountButton); + + await waitForDeleteAccountButtonTextToChangeTo(deleteAccountButtonText); + + expect(mockToastError).toHaveBeenCalledTimes(2); + expect(mockToastError).toHaveBeenNthCalledWith( + 1, + mockFirebaseCurrentUserReauthenticateWithPopupErrorMessage, + ); + expect(mockToastError).toHaveBeenNthCalledWith( + 2, + 'An error occurred deleting your account.', + ); + }); + }); +}); diff --git a/src/pages/app/__tests__/builder.settings.test.js b/src/pages/app/__tests__/builder.settings.test.js index c17514a65..1b4c99893 100644 --- a/src/pages/app/__tests__/builder.settings.test.js +++ b/src/pages/app/__tests__/builder.settings.test.js @@ -16,6 +16,7 @@ test('allows to change the language', async () => { const resumeId = DatabaseConstants.demoStateResume1Id; const { mockDatabaseUpdateFunction } = await setupAndWait( resumeId, + false, true, true, ); diff --git a/src/pages/app/__tests__/builder.skills.test.js b/src/pages/app/__tests__/builder.skills.test.js index fc44dfb1b..3207af8ab 100644 --- a/src/pages/app/__tests__/builder.skills.test.js +++ b/src/pages/app/__tests__/builder.skills.test.js @@ -19,6 +19,7 @@ test('allows to drag & drop', async () => { const resumeId = DatabaseConstants.demoStateResume1Id; const { resume, mockDatabaseUpdateFunction } = await setupAndWait( resumeId, + false, true, true, ); diff --git a/src/pages/app/__tests__/dashboard.deleteResume.test.js b/src/pages/app/__tests__/dashboard.deleteResume.test.js index fe9e1801b..a4d22d798 100644 --- a/src/pages/app/__tests__/dashboard.deleteResume.test.js +++ b/src/pages/app/__tests__/dashboard.deleteResume.test.js @@ -44,7 +44,7 @@ async function setup() { ); const resumeToDeleteId = resumeToDelete.id; const [undeletedResume] = Object.values(userResumes).filter( - (resume) => resume.id === DatabaseConstants.initialStateResumeId, + (resume) => resume.id === DatabaseConstants.initialStateResume1Id, ); const mockDatabaseRemoveFunction = jest.spyOn( diff --git a/src/pages/app/__tests__/helpers/builder.js b/src/pages/app/__tests__/helpers/builder.js index b2a9f9fa0..439206e4c 100644 --- a/src/pages/app/__tests__/helpers/builder.js +++ b/src/pages/app/__tests__/helpers/builder.js @@ -80,6 +80,7 @@ const dragAndDropListItem = (listItemElement, direction) => { // eslint-disable-next-line no-underscore-dangle async function _setup( resumeId, + signInWithGoogle, waitForLoadingScreenToDisappear, waitForDatabaseUpdateToHaveCompleted, ) { @@ -96,7 +97,12 @@ async function _setup( 'update', ); - FirebaseStub.auth().signInAnonymously(); + if (signInWithGoogle) { + const provider = new FirebaseStub.auth.GoogleAuthProvider(); + FirebaseStub.auth().signInWithPopup(provider); + } else { + FirebaseStub.auth().signInAnonymously(); + } render( @@ -132,17 +138,19 @@ async function _setup( } async function setup(resumeId) { - const returnValue = await _setup(resumeId, false, false); + const returnValue = await _setup(resumeId, false, false, false); return returnValue; } async function setupAndWait( resumeId, + signInWithGoogle, waitForLoadingScreenToDisappear, waitForDatabaseUpdateToHaveCompleted, ) { const returnValue = await _setup( resumeId, + signInWithGoogle, waitForLoadingScreenToDisappear, waitForDatabaseUpdateToHaveCompleted, ); diff --git a/src/templates/__tests__/Castform.test.js b/src/templates/__tests__/Castform.test.js index 1c2228bac..44dc620ed 100644 --- a/src/templates/__tests__/Castform.test.js +++ b/src/templates/__tests__/Castform.test.js @@ -21,7 +21,7 @@ async function setup(resumeId) { } test('renders correctly', async () => { - const resume = await setup(DatabaseConstants.initialStateResumeId); + const resume = await setup(DatabaseConstants.initialStateResume1Id); const { container } = render(); @@ -30,7 +30,7 @@ test('renders correctly', async () => { }); test('date of birth is not shown if not provided', async () => { - const resume = await setup(DatabaseConstants.initialStateResumeId); + const resume = await setup(DatabaseConstants.initialStateResume1Id); render(); @@ -38,7 +38,7 @@ test('date of birth is not shown if not provided', async () => { }); test('date of birth is shown if provided', async () => { - const resume = await setup(DatabaseConstants.initialStateResumeId); + const resume = await setup(DatabaseConstants.initialStateResume1Id); const birthDate = new Date(1990, 0, 20); const birthDateFormatted = '20 January 1990'; diff --git a/src/utils/index.js b/src/utils/index.js index d1f63ac28..65f8a0e21 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -137,6 +137,5 @@ export const b64toBlob = (b64Data, contentType = '', sliceSize = 512) => { return blob; }; -export const delay = async (milliseconds) => { - await new Promise((resolve) => setTimeout(resolve, milliseconds)); -}; +export const delay = (milliseconds) => + new Promise((resolve) => setTimeout(resolve, milliseconds));