diff --git a/cloudbuild.build.yaml b/cloudbuild.build.yaml index 98d2ef5e..745d23d2 100644 --- a/cloudbuild.build.yaml +++ b/cloudbuild.build.yaml @@ -32,3 +32,4 @@ steps: entrypoint: npm dir: "functions" args: ["run", "build"] +timeout: 900s diff --git a/package.json b/package.json index ae6b462d..ae16aed8 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,9 @@ "ci": "node scripts/build.js", "release": "node scripts/release.js", "build:refs": "tsc -p common", - "build": "webpack --env=dev", - "start": "webpack --env=dev -w", - "build:development": "webpack --env=dev", + "build": "webpack", + "start": "webpack -w", "build:production": "webpack --env=prod", - "deploy:development": "npm run build:development && firebase deploy", - "deploy:production": "npm run build:production && firebase deploy", "tsc": "tsc -p common && tsc --noEmit", "test": "jest", "test:cloudbuild": "firebase emulators:exec --only firestore \"jest --coverage\"", diff --git a/src/routes/favorites/detail.tsx b/src/routes/favorites/detail.tsx index 198e8571..8bed1f40 100644 --- a/src/routes/favorites/detail.tsx +++ b/src/routes/favorites/detail.tsx @@ -28,11 +28,6 @@ const beforeEnter = (_route: FavoriteDetailRoute, params: RouteParams, s: IRootS } const onEnter = (route: FavoriteDetailRoute, _params: RouteParams, s: IRootStore) => { - const save = () => { - if (s.favorites.activeDocument && s.favorites.activeDocumentId) { - s.favorites.updateDocument(s.favorites.activeDocument, s.favorites.activeDocumentId); - } - }; const deleteAction: IViewAction = { action: () => { if (s.favorites.activeDocumentId) { @@ -51,7 +46,7 @@ const onEnter = (route: FavoriteDetailRoute, _params: RouteParams, s: IRootStore const saveAction: IViewAction = { action: () => { - save(); + s.favorites.saveFavoriteGroup(); goToFavorites(s.router); }, icon: { label: "Save", content: "save" }, @@ -66,7 +61,7 @@ const onEnter = (route: FavoriteDetailRoute, _params: RouteParams, s: IRootStore s.view.setNavigation({ action: () => { - save(); + s.favorites.saveFavoriteGroup(); goToFavorites(s.router); }, icon: { label: "Back", content: "arrow_back" } diff --git a/src/stores/favorite-store/favorite-store.test.tsx b/src/stores/favorite-store/favorite-store.test.tsx new file mode 100644 index 00000000..de46debd --- /dev/null +++ b/src/stores/favorite-store/favorite-store.test.tsx @@ -0,0 +1,318 @@ +import { Store } from "../root-store"; +import path from "path"; +import fs from "fs"; +import type firebase from "firebase"; +import { waitFor } from "@testing-library/react"; +import { initializeTestApp, loadFirestoreRules, clearFirestoreData, } from "@firebase/rules-unit-testing"; +import { useStore } from "../../contexts/store-context"; +import { IFavoriteRegistration } from "../../../common"; + +jest.mock("../../contexts/store-context"); + +const projectId = "favorite-store-test"; +const app = initializeTestApp({ + projectId, +}); + + +let divisionUserId1: string; +let divisionUserId2: string; +const divisionId1 = "div-1"; +const divisionId2 = "div-2"; + +let store: Store; +const setupAsync = async () => { + store = new Store({ + firestore: app.firestore(), + }); + + await Promise.all([ + store.auth.collection.addAsync( + { + name: "user 1", + team: "team-1", + roles: { + user: true, + }, + uid: "user-1", + divisionId: "", + recentProjects: [], + tasks: new Map(), + }, + "user-1", + ), + store.divisions.addDocuments([ + { + name: "Division 1", + createdBy: "user-1", + icon: "business", + id: divisionId1 + }, + { + name: "Division 2", + createdBy: "user-2", + icon: "house", + id: divisionId2 + }, + ]), + ]); + + const [ + generatedDivisionUserId1, + generatedDivisionUserId2, + ] = await store.user.divisionUsersCollection.addAsync([ + { + name: "Division User 1", + divisionId: divisionId1, + tasks: new Map(), + recentProjects: [] as string[], + uid: "user-1", + roles: {} + }, + { + name: "Division User 2", + divisionId: divisionId2, + tasks: new Map(), + recentProjects: [] as string[], + uid: "user-1", + roles: {} + }, + ]); + + divisionUserId1 = generatedDivisionUserId1; + divisionUserId2 = generatedDivisionUserId2; + + store.auth.setUser({ + uid: "user-1", + } as firebase.User); +}; + +beforeAll(async () => { + await loadFirestoreRules({ + projectId, + rules: fs.readFileSync(path.resolve(__dirname, "../../../firestore.rules.test"), "utf8"), + }) +}); + +beforeEach(async () => { + await setupAsync(); + (useStore as jest.Mock>).mockReturnValue(store); +}); + +afterEach(async () => { + await store.dispose(); + await clearFirestoreData({ + projectId, + }); +}); + +afterAll(() => app.delete()); + +describe("FavoriteStore", () => { + + describe("groups", () => { + describe("when there are no favorite groups", () => { + it("should return an empty array", () => { + expect(store.favorites.groups.length).toBe(0); + }); + }); + + describe("when there are favorite groups", () => { + beforeEach(async () => { + await store.favorites.addDocuments([ + { + name: "Fav group 1", + userId: "user-1", + }, + { + name: "Fav group 2", + userId: "user-1", + }, + { + name: "Fav group 3", + userId: "user-1", + }, + { + name: "Fav group 1", + userId: "user-2", + }, + { + name: "Fav group 1 - div 1", + userId: divisionUserId1, + }, + { + name: "Fav group 2 - div 1", + userId: divisionUserId1, + }, + { + name: "Fav group 1 - div 2", + userId: divisionUserId2, + }, + ]); + }); + + it("should return an array of ", async () => { + await waitFor(() => expect(store.user.divisionUser).toBeDefined()); + await waitFor(() => expect(store.favorites.groups.length).toBe(3)); + }); + + it("should return only favorite groups of the current division user", async () => { + await waitFor(() => expect(store.user.divisionUser).toBeDefined()); + + await waitFor(() => expect(store.favorites.groups.length).toBe(3)); + + store.auth.updateActiveDocument({ + divisionUserId: divisionUserId1, + divisionId: divisionId1, + }); + + await waitFor(() => expect(store.favorites.groups.length).toBe(2)); + + store.auth.updateActiveDocument({ + divisionUserId: divisionUserId2, + divisionId: divisionId2, + }); + + await waitFor(() => expect(store.favorites.groups.length).toBe(1)); + + store.auth.updateActiveDocument({ + divisionUserId: undefined, + divisionId: undefined, + }); + + await waitFor(() => expect(store.favorites.groups.length).toBe(3)); + }); + }); + }); + + describe("saveFavoriteGroup", () => { + let groupId: string; + + beforeEach( + async () => { + await waitFor(() => expect(store.user.divisionUser).toBeDefined()); + groupId = store.favorites.addFavorites( + [ + { + description: "reg 1", + userId: "user-1", + time: 3.3 + }, + ] as IFavoriteRegistration[], + { + name: "Fav group 1" + }, + ); + store.favorites.setActiveDocumentId(groupId); + + await waitFor( + () => expect( + store.favorites.activeDocument, + ).toBeDefined(), + ); + + await store.favorites.favoriteCollection.fetchAsync(); + await waitFor(() => expect( + store.favorites.favoriteCollection.docs.map((doc) => doc.data!)) + .toEqual(expect.arrayContaining([ + expect.objectContaining({ + description: "reg 1", + }) + ])) + ); + } + ); + + afterEach( + async () => { + store.favorites.deleteDocument(groupId, { useFlag: false }); + + await waitFor( + () => expect( + store.favorites.groups.length + ).toBe(0), + ); + + const registrations = await store.favorites.getFavoritesByGroupIdAsync(groupId); + + await store.favorites.favoriteCollection.deleteAsync(...registrations.map((doc) => doc.id)); + + await store.favorites.favoriteCollection.fetchAsync(); + + await waitFor( + () => expect( + store.favorites.favoriteCollection.docs.length, + ).toBe(0), + ); + }, + ); + + it("can set a new name for the favorite group", async () => { + if (store.favorites.activeDocument) { + store.favorites.activeDocument.name = "New favorite group name"; + } + + store.favorites.saveFavoriteGroup(); + + await waitFor( + () => expect( + store.favorites.groups, + ).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: "New favorite group name", + }), + ]) + ), + ); + }); + + it("can override an existing favorite group when saved with same name", async () => { + const groupId2 = store.favorites.addFavorites( + [ + { + description: "reg 2", + userId: "user-1", + time: 4, + }, + { + description: "reg 3", + userId: "user-1", + time: 2, + }, + ] as IFavoriteRegistration[], + { + name: "Favorite" + }, + ); + + await waitFor(() => expect( + store.favorites.groups.length, + ).toBe(2), + ); + + store.favorites.setActiveDocumentId(groupId2); + + const registrations = await store.favorites.getFavoritesByGroupIdAsync(groupId2); + + expect(registrations).toHaveLength(2); + + if (store.favorites.activeDocument) { + store.favorites.activeDocument.name = "Fav group 1"; + } + store.favorites.saveFavoriteGroup(); + + await waitFor(() => { + expect(store.favorites.groups).toHaveLength(1); + }); + + store.favorites.deleteDocument(groupId2); + store.favorites.favoriteCollection.deleteAsync(...registrations.map((reg) => reg.id)); + await store.favorites.favoriteCollection.fetchAsync(); + await waitFor(() => expect(store.favorites.groups).toHaveLength(0)); + await waitFor(() => { + expect(store.favorites.getFavoritesByGroupIdAsync(groupId2)).resolves.toHaveLength(0); + }); + }); + }); +}); diff --git a/src/stores/favorite-store/favorite-store.ts b/src/stores/favorite-store/favorite-store.ts index 15aa3417..07b18e81 100644 --- a/src/stores/favorite-store/favorite-store.ts +++ b/src/stores/favorite-store/favorite-store.ts @@ -63,14 +63,13 @@ export class FavoriteStore extends CrudStore !!doc.data) .reduce((data, doc) => { - if (doc.data) { - data.push({ - name: "Default", - ...doc.data!, - id: doc.id, - }); - } + data.push({ + name: "Default", + ...doc.data!, + id: doc.id, + }); return data; }, @@ -78,6 +77,30 @@ export class FavoriteStore extends CrudStore ( + group.name === this.activeDocument?.name + && group.id !== this.activeDocumentId + )); + if (existingGroup) { + // delete the existing favorite registration group + this.deleteDocument(existingGroup.id); + // delete existing favorite registrations in this group + this.getFavoritesByGroupIdAsync(existingGroup.id) + .then((favorites) => this.favoriteCollection.deleteAsync( + ...favorites.map((favorite) => favorite.id), + ), + ) + } + + this.updateDocument( + this.activeDocument, + this.activeDocumentId, + ); + } + } + public getFavoritesByGroupIdAsync(groupId: string) { this.favoriteCollection.query = collRef => collRef @@ -96,27 +119,29 @@ export class FavoriteStore extends CrudStore[], group: { name: string, id?: string }) { - const groupId = group.id - ? group.id - : this.collection.newId(); + public addFavorites( + favorites: Omit[], + group: { name: string }, + ) { + const groupId = this.collection.newId(); this.db.runTransaction(() => { if (!this.rootStore.user.divisionUser) return Promise.reject("Unauthenticated"); - return Promise.all([ - group.id - ? undefined - : this.collection.addAsync({ - name: group.name, - userId: this.rootStore.user.divisionUser.id - }, groupId), - this.favoriteCollection.addAsync( - favorites - .map(f => ({ ...f, groupId: groupId })) - ), - ]); + return Promise.all( + [ + this.collection.addAsync( + { + name: group.name, + userId: this.rootStore.user.divisionUser.id + }, + groupId, + ), + this.favoriteCollection.addAsync( + favorites.map(f => ({ ...f, groupId })) + ), + ]); }); return groupId; diff --git a/webpack.config.js b/webpack.config.js index db10c40a..7dd8670c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,9 +1,9 @@ -function buildConfig(env) { - if (env === 'dev' || env === 'prod') { - return require(`./webpack.config.${env}.js`) - } else { - console.log("Wrong webpack build parameter. Possible choices: 'dev' or 'prod'.") - } -} - -module.exports = buildConfig; +function buildConfig(env = 'dev') { + if (env === 'dev' || env === 'prod') { + return require(`./webpack.config.${env}.js`) + } else { + console.log("Wrong webpack build parameter. Possible choices: 'dev' or 'prod'.") + } +} + +module.exports = buildConfig;