Merge pull request #423 from gianantoniopini/develop

Unit testing environment set up and addition of some unit tests
This commit is contained in:
Amruth Pillai
2021-01-20 16:19:16 +05:30
committed by GitHub
22 changed files with 6431 additions and 4 deletions

View File

@ -8,8 +8,13 @@
"FileReader": true,
"localStorage": true
},
"extends": ["airbnb", "prettier"],
"plugins": ["prettier"],
"extends": [
"airbnb",
"plugin:jest/recommended",
"plugin:jest/style",
"prettier"
],
"plugins": ["jest", "prettier"],
"rules": {
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],

3
.gitignore vendored
View File

@ -20,6 +20,9 @@ coverage
# nyc test coverage
.nyc_output
# Coverage directory used by Jest
test-coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

View File

@ -0,0 +1,555 @@
import { waitFor } from '@testing-library/react';
import FirebaseStub, {
AuthConstants,
DatabaseConstants,
} from '../gatsby-plugin-firebase';
describe('FirebaseStub', () => {
describe('auth', () => {
afterEach(() => {
FirebaseStub.auth().dispose();
});
it('reuses existing Auth instance', () => {
const auth1 = FirebaseStub.auth();
const auth2 = FirebaseStub.auth();
expect(auth1.uuid).toBeTruthy();
expect(auth2.uuid).toBeTruthy();
expect(auth1.uuid).toEqual(auth2.uuid);
});
it('returns anonymous user 1 when signing in anonymously', async () => {
const user = await FirebaseStub.auth().signInAnonymously();
expect(user).toBeTruthy();
expect(user).toEqual(AuthConstants.anonymousUser1);
});
it('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();
});
it('onAuthStateChanged unsubscribe removes observer', () => {
const observer = () => {};
const unsubscribe = FirebaseStub.auth().onAuthStateChanged(observer);
expect(unsubscribe).toBeTruthy();
expect(FirebaseStub.auth().onAuthStateChangedObservers).toHaveLength(1);
expect(FirebaseStub.auth().onAuthStateChangedObservers[0]).toEqual(
observer,
);
unsubscribe();
expect(FirebaseStub.auth().onAuthStateChangedObservers).toHaveLength(0);
});
});
describe('database', () => {
beforeEach(() => {
FirebaseStub.database().initializeData();
});
it('reuses existing Database instance', () => {
const database1 = FirebaseStub.database();
const database2 = FirebaseStub.database();
expect(database1.uuid).toBeTruthy();
expect(database2.uuid).toBeTruthy();
expect(database1.uuid).toEqual(database2.uuid);
});
describe('ref function', () => {
it('reuses existing Reference instance', () => {
const ref1 = FirebaseStub.database().ref(
`${DatabaseConstants.resumesPath}/123`,
);
const ref2 = FirebaseStub.database().ref(
`${DatabaseConstants.resumesPath}/123`,
);
expect(ref1).toBeTruthy();
expect(ref2).toBeTruthy();
expect(ref1).toEqual(ref2);
});
it('leading slash in reference path is ignored', () => {
const path = `${DatabaseConstants.resumesPath}/123`;
const ref1 = FirebaseStub.database().ref(path);
expect(ref1).toBeTruthy();
expect(ref1.path).toEqual(path);
const ref2 = FirebaseStub.database().ref(`/${path}`);
expect(ref2).toBeTruthy();
expect(ref2).toEqual(ref1);
});
});
it('ServerValue.TIMESTAMP returns current time in milliseconds', () => {
const now = new Date().getTime();
const timestamp = FirebaseStub.database.ServerValue.TIMESTAMP;
expect(timestamp).toBeTruthy();
expect(timestamp).toBeGreaterThanOrEqual(now);
});
it('initializing data sets up resumes and users', async () => {
const resumesRef = FirebaseStub.database().ref(
DatabaseConstants.resumesPath,
);
const resumesDataSnapshot = await resumesRef.once('value');
const resumes = resumesDataSnapshot.val();
expect(resumes).toBeTruthy();
expect(Object.keys(resumes)).toHaveLength(3);
const demoStateResume1 = resumes[DatabaseConstants.demoStateResume1Id];
expect(demoStateResume1).toBeTruthy();
expect(demoStateResume1.id).toEqual(DatabaseConstants.demoStateResume1Id);
expect(demoStateResume1.user).toEqual(DatabaseConstants.user1.uid);
const demoStateResume2 = resumes[DatabaseConstants.demoStateResume2Id];
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 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);
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);
});
it('retrieves resume if it exists', async () => {
const resume = (
await FirebaseStub.database()
.ref(
`${DatabaseConstants.resumesPath}/${DatabaseConstants.demoStateResume1Id}`,
)
.once('value')
).val();
expect(resume).toBeTruthy();
expect(resume.id).toEqual(DatabaseConstants.demoStateResume1Id);
});
it('retrieves null if resume does not exist', async () => {
const resumeId = 'invalidResumeId';
const resume = (
await FirebaseStub.database()
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
.once('value')
).val();
expect(resume).toBeNull();
});
it('retrieves user if it exists', async () => {
const user = (
await FirebaseStub.database()
.ref(`${DatabaseConstants.usersPath}/${DatabaseConstants.user1.uid}`)
.once('value')
).val();
expect(user).toBeTruthy();
expect(user).toEqual(DatabaseConstants.user1);
});
it('retrieves null if user does not exist', async () => {
const userId = 'invalidUserId';
const user = (
await FirebaseStub.database()
.ref(`${DatabaseConstants.usersPath}/${userId}`)
.once('value')
).val();
expect(user).toBeNull();
});
describe('on function', () => {
describe('value event', () => {
it('triggers event with true if on the connected reference path', async () => {
let snapshotValue = null;
FirebaseStub.database()
.ref(DatabaseConstants.connectedPath)
.on('value', (snapshot) => {
snapshotValue = snapshot.val();
});
await waitFor(() =>
snapshotValue ? Promise.resolve(true) : Promise.reject(),
);
expect(snapshotValue).not.toBeNull();
expect(snapshotValue).toBe(true);
});
it('triggers event with resumes if on the resumes reference path', async () => {
const resumesDataSnapshot = await FirebaseStub.database()
.ref(DatabaseConstants.resumesPath)
.once('value');
const resumes = resumesDataSnapshot.val();
let snapshotValue = null;
FirebaseStub.database()
.ref(DatabaseConstants.resumesPath)
.on('value', (snapshot) => {
snapshotValue = snapshot.val();
});
await waitFor(() =>
snapshotValue ? Promise.resolve(true) : Promise.reject(),
);
expect(snapshotValue).not.toBeNull();
expect(snapshotValue).toEqual(resumes);
});
});
});
it('can filter resumes by user', async () => {
let snapshotValue = null;
FirebaseStub.database()
.ref(DatabaseConstants.resumesPath)
.orderByChild('user')
.equalTo(DatabaseConstants.user1.uid)
.on('value', (snapshot) => {
snapshotValue = snapshot.val();
});
await waitFor(() =>
snapshotValue ? Promise.resolve(true) : Promise.reject(),
);
expect(snapshotValue).not.toBeNull();
expect(Object.keys(snapshotValue)).toHaveLength(2);
Object.values(snapshotValue).forEach((resume) =>
expect(resume.user).toEqual(DatabaseConstants.user1.uid),
);
});
it('previously set query parameters are not kept when retrieving reference again', async () => {
let reference = null;
reference = FirebaseStub.database().ref(DatabaseConstants.resumesPath);
expect(reference).toBeTruthy();
const { uuid } = reference;
expect(reference.orderByChildPath).toHaveLength(0);
expect(reference.equalToValue).toHaveLength(0);
reference = FirebaseStub.database()
.ref(DatabaseConstants.resumesPath)
.orderByChild('user')
.equalTo('testuser1');
expect(reference).toBeTruthy();
expect(reference.uuid).toBe(uuid);
expect(reference.orderByChildPath).toBe('user');
expect(reference.equalToValue).toBe('testuser1');
reference = FirebaseStub.database().ref(DatabaseConstants.resumesPath);
expect(reference).toBeTruthy();
expect(reference.uuid).toBe(uuid);
expect(reference.orderByChildPath).toHaveLength(0);
expect(reference.equalToValue).toHaveLength(0);
});
describe('set function', () => {
it('inserts data', async () => {
const existingResume = (
await FirebaseStub.database()
.ref(
`${DatabaseConstants.resumesPath}/${DatabaseConstants.demoStateResume1Id}`,
)
.once('value')
).val();
expect(existingResume).toBeTruthy();
const newResume = JSON.parse(JSON.stringify(existingResume));
newResume.id = 'newre1';
newResume.name = `Test Resume ${newResume.id}`;
await FirebaseStub.database()
.ref(`${DatabaseConstants.resumesPath}/${newResume.id}`)
.set(newResume);
const actualResume = (
await FirebaseStub.database()
.ref(`${DatabaseConstants.resumesPath}/${newResume.id}`)
.once('value')
).val();
expect(actualResume).toBeTruthy();
expect(actualResume).toEqual(newResume);
});
it('triggers events', async () => {
let snapshotValue = null;
const callback = jest.fn((snapshot) => {
snapshotValue = snapshot.val();
});
FirebaseStub.database()
.ref(DatabaseConstants.resumesPath)
.orderByChild('user')
.equalTo(DatabaseConstants.user1.uid)
.on('value', callback);
await waitFor(() => callback.mock.calls[0][0]);
callback.mockClear();
snapshotValue = null;
const existingResume = (
await FirebaseStub.database()
.ref(
`${DatabaseConstants.resumesPath}/${DatabaseConstants.demoStateResume1Id}`,
)
.once('value')
).val();
expect(existingResume).toBeTruthy();
const newResume = JSON.parse(JSON.stringify(existingResume));
newResume.id = 'newre1';
newResume.name = `Test Resume ${newResume.id}`;
await FirebaseStub.database()
.ref(`${DatabaseConstants.resumesPath}/${newResume.id}`)
.set(newResume);
await waitFor(() => callback.mock.calls[0][0]);
expect(callback.mock.calls).toHaveLength(1);
expect(snapshotValue).not.toBeNull();
expect(Object.keys(snapshotValue)).toHaveLength(3);
expect(snapshotValue[newResume.id]).toBeTruthy();
expect(snapshotValue[newResume.id]).toEqual(newResume);
});
});
describe('update function', () => {
it('can spy on it', async () => {
const referencePath = `${DatabaseConstants.resumesPath}/123456`;
const functionSpy = jest.spyOn(
FirebaseStub.database().ref(referencePath),
'update',
);
const updateArgument = 'test value 123';
await FirebaseStub.database().ref(referencePath).update(updateArgument);
expect(functionSpy).toHaveBeenCalledTimes(1);
const functionCallArgument = functionSpy.mock.calls[0][0];
expect(functionCallArgument).toBeTruthy();
expect(functionCallArgument).toEqual(updateArgument);
});
it('updates data', async () => {
const resumeId = DatabaseConstants.demoStateResume1Id;
const existingResume = (
await FirebaseStub.database()
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
.once('value')
).val();
expect(existingResume).toBeTruthy();
const resumeName = 'Test Resume renamed';
existingResume.name = resumeName;
await FirebaseStub.database()
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
.update(existingResume);
const actualResume = (
await FirebaseStub.database()
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
.once('value')
).val();
expect(actualResume).toBeTruthy();
expect(existingResume).toEqual(actualResume);
expect(actualResume.name).toEqual(resumeName);
});
it('triggers events', async () => {
let snapshotValue = null;
const callback = jest.fn((snapshot) => {
snapshotValue = snapshot.val();
});
FirebaseStub.database()
.ref(DatabaseConstants.resumesPath)
.orderByChild('user')
.equalTo(DatabaseConstants.user1.uid)
.on('value', callback);
await waitFor(() => callback.mock.calls[0][0]);
callback.mockClear();
snapshotValue = null;
const existingResume = (
await FirebaseStub.database()
.ref(
`${DatabaseConstants.resumesPath}/${DatabaseConstants.demoStateResume1Id}`,
)
.once('value')
).val();
expect(existingResume).toBeTruthy();
existingResume.name = 'Test Resume renamed';
await FirebaseStub.database()
.ref(`${DatabaseConstants.resumesPath}/${existingResume.id}`)
.update(existingResume);
await waitFor(() => callback.mock.calls[0][0]);
expect(callback.mock.calls).toHaveLength(1);
expect(snapshotValue).not.toBeNull();
expect(Object.keys(snapshotValue)).toHaveLength(2);
expect(snapshotValue[existingResume.id]).toBeTruthy();
expect(snapshotValue[existingResume.id]).toEqual(existingResume);
});
});
describe('remove function', () => {
it('deletes data', async () => {
const resumeId = DatabaseConstants.demoStateResume1Id;
const removedResume = (
await FirebaseStub.database()
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
.once('value')
).val();
expect(removedResume).toBeTruthy();
await FirebaseStub.database()
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
.remove();
const actualResume = (
await FirebaseStub.database()
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
.once('value')
).val();
expect(actualResume).toBeNull();
});
it('triggers events', async () => {
const userUid = DatabaseConstants.user1.uid;
let valueCallbackSnapshotValue = null;
const valueCallback = jest.fn((snapshot) => {
valueCallbackSnapshotValue = snapshot.val();
});
FirebaseStub.database()
.ref(DatabaseConstants.resumesPath)
.orderByChild('user')
.equalTo(userUid)
.on('value', valueCallback);
await waitFor(() => valueCallback.mock.calls[0][0]);
valueCallback.mockClear();
valueCallbackSnapshotValue = null;
let childRemovedCallbackSnapshotValue = null;
const childRemovedCallback = jest.fn((snapshot) => {
childRemovedCallbackSnapshotValue = snapshot.val();
});
FirebaseStub.database()
.ref(DatabaseConstants.resumesPath)
.orderByChild('user')
.equalTo(userUid)
.on('child_removed', childRemovedCallback);
const removedResume = (
await FirebaseStub.database()
.ref(
`${DatabaseConstants.resumesPath}/${DatabaseConstants.demoStateResume1Id}`,
)
.once('value')
).val();
expect(removedResume).toBeTruthy();
expect(removedResume.user).toEqual(userUid);
await FirebaseStub.database()
.ref(`${DatabaseConstants.resumesPath}/${removedResume.id}`)
.remove();
await waitFor(() => childRemovedCallback.mock.calls[0][0]);
expect(childRemovedCallback.mock.calls).toHaveLength(1);
expect(childRemovedCallbackSnapshotValue).toBeTruthy();
expect(childRemovedCallbackSnapshotValue).toEqual(removedResume);
await waitFor(() => valueCallback.mock.calls[0][0]);
expect(valueCallback.mock.calls).toHaveLength(1);
expect(valueCallbackSnapshotValue).toBeTruthy();
expect(removedResume.id in valueCallbackSnapshotValue).toBe(false);
});
});
describe('off function', () => {
it('removes event callbacks', async () => {
const userUid = DatabaseConstants.user1.uid;
let valueCallbackSnapshotValue = null;
const valueCallback = jest.fn((snapshot) => {
valueCallbackSnapshotValue = snapshot.val();
});
FirebaseStub.database()
.ref(DatabaseConstants.resumesPath)
.orderByChild('user')
.equalTo(userUid)
.on('value', valueCallback);
await waitFor(() => valueCallback.mock.calls[0][0]);
valueCallback.mockClear();
valueCallbackSnapshotValue = null;
let childRemovedCallbackSnapshotValue = null;
const childRemovedCallback = jest.fn((snapshot) => {
childRemovedCallbackSnapshotValue = snapshot.val();
});
FirebaseStub.database()
.ref(DatabaseConstants.resumesPath)
.orderByChild('user')
.equalTo(userUid)
.on('child_removed', childRemovedCallback);
const removedResume = (
await FirebaseStub.database()
.ref(
`${DatabaseConstants.resumesPath}/${DatabaseConstants.demoStateResume1Id}`,
)
.once('value')
).val();
expect(removedResume).toBeTruthy();
FirebaseStub.database().ref(DatabaseConstants.resumesPath).off();
await FirebaseStub.database()
.ref(`${DatabaseConstants.resumesPath}/${removedResume.id}`)
.remove();
expect(childRemovedCallback.mock.calls).toHaveLength(0);
expect(childRemovedCallbackSnapshotValue).toBeNull();
expect(valueCallback.mock.calls).toHaveLength(0);
expect(valueCallbackSnapshotValue).toBeNull();
});
});
});
});

2
__mocks__/file-mock.js Normal file
View File

@ -0,0 +1,2 @@
const mockFile = 'test-file-stub';
export default mockFile;

View File

@ -0,0 +1,24 @@
import Auth from './gatsby-plugin-firebase/auth/auth';
import Database from './gatsby-plugin-firebase/database/database';
import AuthConstants from './gatsby-plugin-firebase/constants/auth';
import DatabaseConstants from './gatsby-plugin-firebase/constants/database';
class FirebaseStub {
static auth() {
return Auth.instance;
}
static database() {
return Database.instance;
}
}
FirebaseStub.database.ServerValue = {};
Object.defineProperty(FirebaseStub.database.ServerValue, 'TIMESTAMP', {
get() {
return new Date().getTime();
},
});
export default FirebaseStub;
export { AuthConstants, DatabaseConstants };

View File

@ -0,0 +1,58 @@
/* eslint-disable no-underscore-dangle */
import { v4 as uuidv4 } from 'uuid';
import Constants from '../constants/auth';
const singleton = Symbol('');
const singletonEnforcer = Symbol('');
class Auth {
constructor(enforcer) {
if (enforcer !== singletonEnforcer) {
throw new Error('Cannot construct singleton');
}
this._uuid = uuidv4();
this._onAuthStateChangedObservers = [];
}
static get instance() {
if (!this[singleton]) {
this[singleton] = new Auth(singletonEnforcer);
}
return this[singleton];
}
get uuid() {
return this._uuid;
}
get onAuthStateChangedObservers() {
return this._onAuthStateChangedObservers;
}
dispose() {
this._onAuthStateChangedObservers = [];
}
onAuthStateChanged(observer) {
this.onAuthStateChangedObservers.push(observer);
return () => {
this._onAuthStateChangedObservers = this.onAuthStateChangedObservers.filter(
(obs) => obs !== observer,
);
};
}
async signInAnonymously() {
const user = Constants.anonymousUser1;
this.onAuthStateChangedObservers.forEach((observer) => observer(user));
return Promise.resolve(user);
}
}
export default Auth;

View File

@ -0,0 +1,27 @@
const anonymousUser1 = {
displayName: 'Anonymous User 1',
email: 'anonymous1@noemail.com',
isAnonymous: true,
name: 'Anonymous 1',
uid: 'anonym123',
};
const anonymousUser2 = {
displayName: 'Anonymous User 2',
email: 'anonymous2@noemail.com',
isAnonymous: true,
name: 'Anonymous 2',
uid: 'anonym456',
};
class Auth {
static get anonymousUser1() {
return anonymousUser1;
}
static get anonymousUser2() {
return anonymousUser2;
}
}
export default Auth;

View File

@ -0,0 +1,65 @@
import AuthConstants from './auth';
const valueEventType = 'value';
const childRemovedEventType = 'child_removed';
const resumesPath = 'resumes';
const usersPath = 'users';
const connectedPath = '.info/connected';
const demoStateResume1Id = 'demo_1';
const demoStateResume2Id = 'demo_2';
const initialStateResumeId = 'initst';
const user1 = {
uid: AuthConstants.anonymousUser1.uid,
isAnonymous: AuthConstants.anonymousUser1.isAnonymous,
};
const user2 = {
uid: AuthConstants.anonymousUser2.uid,
isAnonymous: AuthConstants.anonymousUser2.isAnonymous,
};
class Database {
static get valueEventType() {
return valueEventType;
}
static get childRemovedEventType() {
return childRemovedEventType;
}
static get resumesPath() {
return resumesPath;
}
static get usersPath() {
return usersPath;
}
static get connectedPath() {
return connectedPath;
}
static get demoStateResume1Id() {
return demoStateResume1Id;
}
static get demoStateResume2Id() {
return demoStateResume2Id;
}
static get initialStateResumeId() {
return initialStateResumeId;
}
static get user1() {
return user1;
}
static get user2() {
return user2;
}
}
export default Database;

View File

@ -0,0 +1,24 @@
/* eslint-disable no-underscore-dangle */
class DataSnapshot {
constructor(getData, value = undefined) {
if (!getData) {
throw new Error('getData must be provided.');
} else if (typeof getData !== 'function') {
throw new Error('getData should be a function.');
}
this._getData = getData;
this._value = value;
}
get value() {
return this._value;
}
val() {
return typeof this.value !== 'undefined' ? this.value : this._getData();
}
}
export default DataSnapshot;

View File

@ -0,0 +1,156 @@
/* eslint-disable no-underscore-dangle */
import path from 'path';
import fs from 'fs';
import { v4 as uuidv4 } from 'uuid';
import DatabaseConstants from '../constants/database';
import Reference from './reference';
const singleton = Symbol('');
const singletonEnforcer = Symbol('');
const readFile = (fileRelativePath) => {
const fileAbsolutePath = path.resolve(__dirname, fileRelativePath);
const fileBuffer = fs.readFileSync(fileAbsolutePath);
const fileData = JSON.parse(fileBuffer);
return fileData;
};
class Database {
constructor(enforcer) {
if (enforcer !== singletonEnforcer) {
throw new Error('Cannot construct singleton');
}
this._uuid = uuidv4();
this._data = {};
this._references = {};
}
static get instance() {
if (!this[singleton]) {
this[singleton] = new Database(singletonEnforcer);
}
return this[singleton];
}
get uuid() {
return this._uuid;
}
_getData(dataPath) {
if (!dataPath) {
throw new Error('dataPath must be provided.');
}
const dataPathElements = dataPath.split('/');
if (!(dataPathElements[0] in this._data)) {
return null;
}
if (dataPathElements.length === 1) {
return this._data[dataPathElements[0]];
}
if (dataPathElements.length === 2) {
if (!(dataPathElements[1] in this._data[dataPathElements[0]])) {
return null;
}
return this._data[dataPathElements[0]][dataPathElements[1]];
}
return null;
}
_getReference(referencePath) {
return referencePath in this._references
? this._references[referencePath]
: null;
}
_setData(dataPath, value) {
if (!dataPath) {
throw new Error('dataPath must be provided.');
}
if (typeof value === 'undefined') {
throw new Error('value is undefined.');
}
const dataPathElements = dataPath.split('/');
if (dataPathElements.length !== 2) {
return;
}
if (!(dataPathElements[0] in this._data)) {
return;
}
if (!dataPathElements[1]) {
return;
}
if (value === null) {
delete this._data[dataPathElements[0]][dataPathElements[1]];
} else {
this._data[dataPathElements[0]][dataPathElements[1]] = value;
}
}
initializeData() {
const resumes = {};
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();
demoStateResume1.user = DatabaseConstants.user1.uid;
resumes[DatabaseConstants.demoStateResume1Id] = demoStateResume1;
const demoStateResume2 = JSON.parse(JSON.stringify(demoStateResume1));
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;
Object.keys(resumes).forEach((key) => {
const resume = resumes[key];
resume.id = key;
resume.name = `Test Resume ${key}`;
});
this._data[DatabaseConstants.resumesPath] = resumes;
const users = {};
users[DatabaseConstants.user1.uid] = DatabaseConstants.user1;
users[DatabaseConstants.user2.uid] = DatabaseConstants.user2;
this._data[DatabaseConstants.usersPath] = users;
}
ref(referencePath) {
const newRef = new Reference(
referencePath,
(dataPath) => this._getData(dataPath),
(dataPath, value) => this._setData(dataPath, value),
(refPath) => this._getReference(refPath),
);
const existingRef = this._getReference(newRef.path);
if (existingRef) {
existingRef.initializeQueryParameters();
return existingRef;
}
this._references[newRef.path] = newRef;
return newRef;
}
}
export default Database;

View File

@ -0,0 +1,213 @@
/* eslint-disable no-underscore-dangle */
import { v4 as uuidv4 } from 'uuid';
import { debounce } from 'lodash';
import DatabaseConstants from '../constants/database';
import DataSnapshot from './dataSnapshot';
const parsePath = (path) => {
if (!path) {
throw new Error('path must be provided.');
} else if (typeof path !== 'string') {
throw new Error('path should be a string.');
} else {
let parsedPath = path.trim();
if (parsedPath[0] === '/') {
parsedPath = parsedPath.substring(1);
}
return parsedPath;
}
};
class Reference {
constructor(path, getDatabaseData, setDatabaseData, getReference) {
this._path = parsePath(path);
this._uuid = uuidv4();
if (this.path === DatabaseConstants.connectedPath) {
this._dataSnapshot = new DataSnapshot(() => {}, true);
} else {
this._dataSnapshot = new DataSnapshot(() => this._getData());
}
if (!getDatabaseData) {
throw new Error('getDatabaseData must be provided.');
} else if (typeof getDatabaseData !== 'function') {
throw new Error('getDatabaseData should be a function.');
}
this._getDatabaseData = getDatabaseData;
if (!setDatabaseData) {
throw new Error('setDatabaseData must be provided.');
} else if (typeof getDatabaseData !== 'function') {
throw new Error('setDatabaseData should be a function.');
}
this._setDatabaseData = setDatabaseData;
if (!getReference) {
throw new Error('getReference must be provided.');
} else if (typeof getDatabaseData !== 'function') {
throw new Error('getReference should be a function.');
}
this._getReference = getReference;
this._eventCallbacks = {};
this.initializeQueryParameters();
}
get path() {
return this._path;
}
get uuid() {
return this._uuid;
}
get eventCallbacks() {
return this._eventCallbacks;
}
get orderByChildPath() {
return this._orderByChildPath;
}
get equalToValue() {
return this._equalToValue;
}
_getData() {
const databaseData = this._getDatabaseData(this.path);
if (!databaseData) {
return null;
}
if (this.orderByChildPath && this.equalToValue) {
return Object.fromEntries(
Object.entries(databaseData).filter(
([, value]) => value[this.orderByChildPath] === this.equalToValue,
),
);
}
return databaseData;
}
_getParent() {
const pathElements = this.path.split('/');
let parent = null;
if (pathElements.length === 2) {
parent = this._getReference(pathElements[0]);
}
return parent;
}
_handleDataUpdate(value) {
if (typeof value === 'undefined') {
throw new Error('value must be provided.');
}
const currentData = this._getData();
const parentReference = this._getParent();
this._setDatabaseData(this.path, value);
if (value === null) {
if (parentReference) {
parentReference.triggerEventCallback(
DatabaseConstants.childRemovedEventType,
currentData,
);
}
} else {
this.triggerEventCallback(DatabaseConstants.valueEventType);
}
if (parentReference) {
parentReference.triggerEventCallback(DatabaseConstants.valueEventType);
}
}
triggerEventCallback(eventType, snapshotValue = undefined) {
if (!(eventType in this.eventCallbacks)) {
return;
}
const snapshot =
this.path === DatabaseConstants.connectedPath
? this._dataSnapshot
: new DataSnapshot(() => this._getData(), snapshotValue);
const debouncedEventCallback = debounce(
this.eventCallbacks[eventType],
100,
);
debouncedEventCallback(snapshot);
}
equalTo(value) {
this._equalToValue = value;
return this;
}
initializeQueryParameters() {
this._orderByChildPath = '';
this._equalToValue = '';
}
off() {
this._eventCallbacks = {};
}
on(eventType, callback) {
this.eventCallbacks[eventType] = callback;
if (eventType === DatabaseConstants.valueEventType) {
this.triggerEventCallback(eventType);
}
}
async once(eventType) {
if (!eventType) {
throw new Error('eventType must be provided.');
} else if (typeof eventType !== 'string') {
throw new Error('eventType should be a string.');
}
return Promise.resolve(this._dataSnapshot);
}
orderByChild(path) {
this._orderByChildPath = path;
return this;
}
async update(value) {
this._handleDataUpdate(value);
return Promise.resolve(true);
}
async remove() {
this._handleDataUpdate(null);
return Promise.resolve(true);
}
async set(value) {
this._handleDataUpdate(value);
return Promise.resolve(true);
}
}
export default Reference;

80
__mocks__/gatsby.js Normal file
View File

@ -0,0 +1,80 @@
import React from 'react';
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',
},
{
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',
},
];
const useStaticQuery = () => ({
site: {
siteMetadata: {
title: 'Test title',
description: 'Test description',
author: 'Test author',
siteUrl: 'https://testsiteurl/',
},
},
file: {
childImageSharp: {
fluid: fluidImageShapes[0],
},
},
onyx: {
childImageSharp: {
fluid: fluidImageShapes[0],
},
},
pikachu: {
childImageSharp: {
fluid: fluidImageShapes[1],
},
},
gengar: {
childImageSharp: {
fluid: fluidImageShapes[0],
},
},
castform: {
childImageSharp: {
fluid: fluidImageShapes[1],
},
},
glalie: {
childImageSharp: {
fluid: fluidImageShapes[0],
},
},
celebi: {
childImageSharp: {
fluid: fluidImageShapes[1],
},
},
});
module.exports = {
...Gatsby,
graphql: jest.fn(),
Link: jest.fn().mockImplementation(({ to, ...rest }) =>
React.createElement('a', {
...rest,
href: to,
}),
),
useStaticQuery,
};

5
jest-preprocess.js Normal file
View File

@ -0,0 +1,5 @@
const babelOptions = {
presets: ['babel-preset-gatsby'],
};
module.exports = require('babel-jest').createTransformer(babelOptions);

36
jest.config.js Normal file
View File

@ -0,0 +1,36 @@
module.exports = {
testRegex: '/*.test.js$',
collectCoverage: true,
collectCoverageFrom: [
'**/*.{js,jsx}',
'!\\.cache/**',
'!node_modules/**',
'!public/**',
'!test-coverage/**',
],
coverageDirectory: 'test-coverage',
coverageThreshold: {
global: {
branches: 0,
functions: 0,
lines: 0,
statements: 0,
},
},
verbose: true,
transform: {
'^.+\\.jsx?$': `<rootDir>/jest-preprocess.js`,
},
moduleNameMapper: {
'.+\\.(css|styl|less|sass|scss)$': `identity-obj-proxy`,
'.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': `<rootDir>/__mocks__/file-mock.js`,
},
testPathIgnorePatterns: [`node_modules`, `\\.cache`],
transformIgnorePatterns: [`node_modules/(?!(gatsby)/)`],
globals: {
__PATH_PREFIX__: ``,
},
testURL: `http://localhost`,
setupFiles: [`<rootDir>/loadershim.js`],
setupFilesAfterEnv: [`<rootDir>/jest.setup.js`],
};

1
jest.setup.js Normal file
View File

@ -0,0 +1 @@
import '@testing-library/jest-dom/extend-expect';

3
loadershim.js Normal file
View File

@ -0,0 +1,3 @@
global.___loader = {
enqueue: jest.fn(),
};

4884
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,13 +7,14 @@
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"prebuild": "npm run test",
"build": "gatsby build",
"develop": "gatsby develop",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
"start": "npm run develop",
"serve": "gatsby serve",
"clean": "gatsby clean",
"test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1"
"test": "jest"
},
"dependencies": {
"@material-ui/core": "^4.11.2",
@ -61,14 +62,21 @@
"yup": "^0.32.8"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.11.8",
"@testing-library/react": "^11.2.3",
"babel-jest": "^26.6.3",
"babel-preset-gatsby": "^0.10.0",
"eslint": "^7.17.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-prettier": "^7.1.0",
"eslint-loader": "^4.0.2",
"eslint-plugin-jest": "^24.1.3",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.22.0",
"gatsby-plugin-eslint": "^2.0.8",
"identity-obj-proxy": "^3.0.0",
"jest": "^26.6.3",
"prettier": "2.2.1",
"stylelint": "^13.8.0",
"stylelint-config-standard": "^20.0.0",

View File

@ -128,4 +128,7 @@ export default DatabaseContext;
const memoizedProvider = memo(DatabaseProvider);
export { memoizedProvider as DatabaseProvider };
export {
memoizedProvider as DatabaseProvider,
DEBOUNCE_WAIT_TIME as DebounceWaitTime,
};

View File

@ -0,0 +1,147 @@
import React from 'react';
import {
act,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import FirebaseStub, { DatabaseConstants } from 'gatsby-plugin-firebase';
import { SettingsProvider } from '../../../contexts/SettingsContext';
import { ModalProvider } from '../../../contexts/ModalContext';
import { UserProvider } from '../../../contexts/UserContext';
import {
DatabaseProvider,
DebounceWaitTime,
} from '../../../contexts/DatabaseContext';
import { ResumeProvider } from '../../../contexts/ResumeContext';
import { StorageProvider } from '../../../contexts/StorageContext';
import Builder from '../builder';
describe('Builder', () => {
let resumeId = null;
let resume = null;
let mockUpdateFunction = null;
beforeEach(async () => {
FirebaseStub.database().initializeData();
resumeId = DatabaseConstants.demoStateResume1Id;
resume = (
await FirebaseStub.database()
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
.once('value')
).val();
mockUpdateFunction = jest.spyOn(
FirebaseStub.database().ref(
`${DatabaseConstants.resumesPath}/${resumeId}`,
),
'update',
);
render(
<SettingsProvider>
<ModalProvider>
<UserProvider>
<DatabaseProvider>
<ResumeProvider>
<StorageProvider>
<Builder id={resume.id} />
</StorageProvider>
</ResumeProvider>
</DatabaseProvider>
</UserProvider>
</ModalProvider>
</SettingsProvider>,
);
await act(async () => {
await FirebaseStub.auth().signInAnonymously();
});
await waitFor(() => mockUpdateFunction.mock.calls[0][0], {
timeout: DebounceWaitTime,
});
mockUpdateFunction.mockClear();
});
describe('renders', () => {
it('first and last name', async () => {
expect(
screen.getByLabelText(new RegExp('first name', 'i')),
).toHaveDisplayValue(resume.profile.firstName);
expect(
screen.getByLabelText(new RegExp('last name', 'i')),
).toHaveDisplayValue(resume.profile.lastName);
expect(
screen.getAllByText(new RegExp(resume.profile.firstName, 'i')).length,
).toBeTruthy();
expect(
screen.getAllByText(new RegExp(resume.profile.lastName, 'i')).length,
).toBeTruthy();
});
});
describe('updates data', () => {
it('when input value is changed', async () => {
const input = screen.getByLabelText(new RegExp('address line 1', 'i'));
const newInputValue = 'test street 123';
const now = new Date().getTime();
fireEvent.change(input, { target: { value: newInputValue } });
expect(input.value).toBe(newInputValue);
await waitFor(() => mockUpdateFunction.mock.calls[0][0], {
timeout: DebounceWaitTime,
});
expect(mockUpdateFunction).toHaveBeenCalledTimes(1);
const mockUpdateFunctionCallArgument =
mockUpdateFunction.mock.calls[0][0];
expect(mockUpdateFunctionCallArgument.id).toBe(resume.id);
expect(mockUpdateFunctionCallArgument.profile.address.line1).toBe(
newInputValue,
);
expect(mockUpdateFunctionCallArgument.updatedAt).toBeGreaterThanOrEqual(
now,
);
});
});
describe('settings', () => {
it('allow to change the language', async () => {
const languageSelectElement = screen.getByLabelText('Language');
const newLanguage = 'it';
const now = new Date().getTime();
fireEvent.change(languageSelectElement, {
target: { value: newLanguage },
});
expect(languageSelectElement).toHaveValue(newLanguage);
expect(
screen.queryByLabelText(new RegExp('date of birth', 'i')),
).toBeNull();
expect(
screen.getByLabelText(new RegExp('data di nascita', 'i')),
).toBeInTheDocument();
await waitFor(() => mockUpdateFunction.mock.calls[0][0], {
timeout: DebounceWaitTime,
});
expect(mockUpdateFunction).toHaveBeenCalledTimes(1);
const mockUpdateFunctionCallArgument =
mockUpdateFunction.mock.calls[0][0];
expect(mockUpdateFunctionCallArgument.id).toBe(resume.id);
expect(mockUpdateFunctionCallArgument.metadata.language).toBe(
newLanguage,
);
expect(mockUpdateFunctionCallArgument.updatedAt).toBeGreaterThanOrEqual(
now,
);
});
});
});

View File

@ -0,0 +1,76 @@
import React from 'react';
import { act, render, screen, waitFor } from '@testing-library/react';
import FirebaseStub, { DatabaseConstants } from 'gatsby-plugin-firebase';
import '../../../i18n/index';
import '../../../utils/dayjs';
import { SettingsProvider } from '../../../contexts/SettingsContext';
import { ModalProvider } from '../../../contexts/ModalContext';
import { UserProvider } from '../../../contexts/UserContext';
import { DatabaseProvider } from '../../../contexts/DatabaseContext';
import { ResumeProvider } from '../../../contexts/ResumeContext';
import { StorageProvider } from '../../../contexts/StorageContext';
import Dashboard from '../dashboard';
describe('Dashboard', () => {
let resumes = null;
const user = DatabaseConstants.user1;
beforeEach(async () => {
FirebaseStub.database().initializeData();
resumes = (
await FirebaseStub.database()
.ref(DatabaseConstants.resumesPath)
.orderByChild('user')
.equalTo(user.uid)
.once('value')
).val();
render(
<SettingsProvider>
<ModalProvider>
<UserProvider>
<DatabaseProvider>
<ResumeProvider>
<StorageProvider>
<Dashboard user={user} />
</StorageProvider>
</ResumeProvider>
</DatabaseProvider>
</UserProvider>
</ModalProvider>
</SettingsProvider>,
);
await act(async () => {
await FirebaseStub.auth().signInAnonymously();
});
await waitFor(() => screen.getByText('Create Resume'));
});
describe('renders', () => {
it('document title', async () => {
expect(document.title).toEqual('Dashboard | Reactive Resume');
});
it('create resume', async () => {
expect(screen.getByText('Create Resume')).toBeInTheDocument();
});
it('preview of user resumes', async () => {
expect(Object.keys(resumes)).toHaveLength(2);
expect(Object.values(resumes)[0].user).toEqual(user.uid);
expect(
screen.getByText(Object.values(resumes)[0].name),
).toBeInTheDocument();
expect(Object.values(resumes)[1].user).toEqual(user.uid);
expect(
screen.getByText(Object.values(resumes)[1].name),
).toBeInTheDocument();
});
});
});

View File

@ -0,0 +1,52 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import FirebaseStub, { DatabaseConstants } from 'gatsby-plugin-firebase';
import '../../i18n/index';
import Castform from '../Castform';
describe('Castform', () => {
let resume = {};
beforeEach(async () => {
FirebaseStub.database().initializeData();
const resumeId = DatabaseConstants.initialStateResumeId;
resume = (
await FirebaseStub.database()
.ref(`${DatabaseConstants.resumesPath}/${resumeId}`)
.once('value')
).val();
});
it('renders correctly', () => {
const { container } = render(<Castform data={resume} />);
expect(container).toBeTruthy();
expect(container).toBeInTheDocument();
});
describe('date of birth', () => {
const birthDateLabelMatcher = /Date of birth/i;
it('is not shown if not provided', () => {
render(<Castform data={resume} />);
expect(screen.queryByText(birthDateLabelMatcher)).toBeNull();
});
it('is shown if provided', () => {
const birthDate = new Date(1990, 0, 20);
const birthDateFormatted = '20 January 1990';
resume.profile.birthDate = birthDate;
render(<Castform data={resume} />);
expect(screen.getByText(birthDateLabelMatcher)).toBeTruthy();
expect(screen.getByText(birthDateLabelMatcher)).toBeInTheDocument();
expect(screen.getByText(birthDateFormatted)).toBeTruthy();
expect(screen.getByText(birthDateFormatted)).toBeInTheDocument();
});
});
});