diff --git a/__mocks__/svgMock.js b/__mocks__/svgMock.js new file mode 100644 index 000000000..b1c6ea436 --- /dev/null +++ b/__mocks__/svgMock.js @@ -0,0 +1 @@ +export default {} diff --git a/assets/scss/styles.scss b/assets/scss/styles.scss index 81eb63d13..5c7759242 100644 --- a/assets/scss/styles.scss +++ b/assets/scss/styles.scss @@ -293,6 +293,7 @@ hr { button { @apply justify-start; + @apply text-left; } } diff --git a/components/firebase/__tests__/feeds.spec.js b/components/firebase/__tests__/feeds.spec.js new file mode 100644 index 000000000..09d53786e --- /dev/null +++ b/components/firebase/__tests__/feeds.spec.js @@ -0,0 +1,133 @@ +import feeds from "../feeds" +import { shallowMount } from "@vue/test-utils" + +jest.mock("~/helpers/fb", () => { + return { + __esModule: true, + fb: { + currentFeeds: [ + { + id: "test1", + label: "First", + message: "First Message", + }, + { + id: "test2", + label: "Second", + }, + { + id: "test3", + message: "Third Message", + }, + { + id: "test4", + }, + ], + deleteFeed: jest.fn(() => Promise.resolve()), + }, + } +}) + +const { fb } = require("~/helpers/fb") + +const factory = () => + shallowMount(feeds, { + mocks: { + $t: (text) => text, + $toast: { + error: jest.fn(), + }, + }, + }) + +beforeEach(() => { + fb.deleteFeed.mockClear() +}) + +describe("feeds", () => { + test("mounts properly when proper components are given", () => { + const wrapper = factory() + + expect(wrapper).toBeTruthy() + }) + + test("renders all the current feeds", () => { + const wrapper = factory() + + expect(wrapper.findAll("div[data-test='list-item']").wrappers).toHaveLength(4) + }) + + test("feeds with no label displays the 'no_label' message", () => { + const wrapper = factory() + + expect( + wrapper + .findAll("label[data-test='list-label']") + .wrappers.map((e) => e.text()) + .filter((text) => text == "no_label") + ).toHaveLength(2) + }) + + test("feeds with no message displays the 'empty' message", () => { + const wrapper = factory() + + expect( + wrapper + .findAll("li[data-test='list-message']") + .wrappers.map((e) => e.text()) + .filter((text) => text == "empty") + ).toHaveLength(2) + }) + + test("labels in the list are proper", () => { + const wrapper = factory() + + expect(wrapper.findAll("label[data-test='list-label']").wrappers.map((e) => e.text())).toEqual([ + "First", + "Second", + "no_label", + "no_label", + ]) + }) + + test("messages in the list are proper", () => { + const wrapper = factory() + + expect(wrapper.findAll("li[data-test='list-message']").wrappers.map((e) => e.text())).toEqual([ + "First Message", + "empty", + "Third Message", + "empty", + ]) + }) + + test("clicking on the delete button deletes the feed", async () => { + const wrapper = factory() + + const deleteButton = wrapper.find("button") + + await deleteButton.trigger("click") + + expect(fb.deleteFeed).toHaveBeenCalledTimes(1) + }) + + test("correct feed is passed to from the list for deletion", async () => { + const wrapper = factory() + + const deleteButton = wrapper.find("button") + + await deleteButton.trigger("click") + + expect(fb.deleteFeed).toHaveBeenCalledWith("test1") + }) + + test("renders the 'empty' label if no elements in the current feeds", () => { + jest.spyOn(fb, "currentFeeds", "get").mockReturnValueOnce([]) + + const wrapper = factory() + + expect(wrapper.findAll("li").wrappers).toHaveLength(1) + + expect(wrapper.find("li").text()).toEqual("empty") + }) +}) diff --git a/components/firebase/__tests__/inputform.spec.js b/components/firebase/__tests__/inputform.spec.js new file mode 100644 index 000000000..f5d42bbd5 --- /dev/null +++ b/components/firebase/__tests__/inputform.spec.js @@ -0,0 +1,93 @@ +import inputform from "../inputform" +import { shallowMount } from "@vue/test-utils" + +jest.mock("~/helpers/fb", () => { + return { + __esModule: true, + fb: { + writeFeeds: jest.fn(() => Promise.resolve()), + }, + } +}) + +const { fb } = require("~/helpers/fb") + +const factory = () => + shallowMount(inputform, { + mocks: { + $t: (text) => text, + }, + }) + +beforeEach(() => { + fb.writeFeeds.mockClear() +}) + +describe("inputform", () => { + test("mounts properly", () => { + const wrapper = factory() + + expect(wrapper).toBeTruthy() + }) + test("calls writeFeeds when submitted properly", async () => { + const wrapper = factory() + + const addButton = wrapper.find("button") + const [messageInput, labelInput] = wrapper.findAll("input").wrappers + + await messageInput.setValue("test message") + await labelInput.setValue("test label") + + await addButton.trigger("click") + + expect(fb.writeFeeds).toHaveBeenCalledTimes(1) + }) + test("doesn't call writeFeeds when submitted without a data", async () => { + const wrapper = factory() + + const addButton = wrapper.find("button") + + await addButton.trigger("click") + + expect(fb.writeFeeds).not.toHaveBeenCalled() + }) + test("doesn't call writeFeeds when message or label is null", async () => { + const wrapper = factory() + + const addButton = wrapper.find("button") + const [messageInput, labelInput] = wrapper.findAll("input").wrappers + + await messageInput.setValue(null) + await labelInput.setValue(null) + + await addButton.trigger("click") + + expect(fb.writeFeeds).not.toHaveBeenCalled() + }) + test("doesn't call writeFeeds when message or label is empty", async () => { + const wrapper = factory() + + const addButton = wrapper.find("button") + const [messageInput, labelInput] = wrapper.findAll("input").wrappers + + await messageInput.setValue("") + await labelInput.setValue("") + + await addButton.trigger("click") + + expect(fb.writeFeeds).not.toHaveBeenCalled() + }) + test("calls writeFeeds with correct values", async () => { + const wrapper = factory() + + const addButton = wrapper.find("button") + const [messageInput, labelInput] = wrapper.findAll("input").wrappers + + await messageInput.setValue("test message") + await labelInput.setValue("test label") + + await addButton.trigger("click") + + expect(fb.writeFeeds).toHaveBeenCalledWith("test message", "test label") + }) +}) diff --git a/components/firebase/__tests__/logout.spec.js b/components/firebase/__tests__/logout.spec.js new file mode 100644 index 000000000..b13256249 --- /dev/null +++ b/components/firebase/__tests__/logout.spec.js @@ -0,0 +1,66 @@ +import logout from "../logout" +import { shallowMount, createLocalVue } from "@vue/test-utils" + +jest.mock("~/helpers/fb", () => { + return { + __esModule: true, + fb: { + signOutUser: jest.fn(() => Promise.resolve()), + }, + } +}) + +const { fb } = require("~/helpers/fb") + +const $toast = { + info: jest.fn(), + show: jest.fn(), +} + +const localVue = createLocalVue() + +localVue.directive("close-popover", {}) + +const factory = () => + shallowMount(logout, { + mocks: { + $t: (text) => text, + $toast, + }, + localVue, + }) + +beforeEach(() => { + fb.signOutUser.mockClear() + $toast.info.mockClear() + $toast.show.mockClear() +}) + +describe("logout", () => { + test("mounts properly", () => { + const wrapper = factory() + + expect(wrapper).toBeTruthy() + }) + + test("clicking the logout button fires the logout firebase function", async () => { + const wrapper = factory() + + const button = wrapper.find("button") + + await button.trigger("click") + + expect(fb.signOutUser).toHaveBeenCalledTimes(1) + }) + + test("failed signout request fires a error toast", async () => { + fb.signOutUser.mockImplementationOnce(() => Promise.reject("test reject")) + + const wrapper = factory() + const button = wrapper.find("button") + await button.trigger("click") + + expect($toast.show).toHaveBeenCalledTimes(1) + expect($toast.show).toHaveBeenCalledWith("test reject", expect.anything()) + }) +}) diff --git a/components/firebase/feeds.vue b/components/firebase/feeds.vue index 4100fd867..81c13e4c5 100644 --- a/components/firebase/feeds.vue +++ b/components/firebase/feeds.vue @@ -5,9 +5,9 @@ :key="feed.id" class="flex-col py-2 border-b border-dashed border-brdColor" > -
+
  • -
  • @@ -16,7 +16,7 @@
    -
  • +
  • @@ -54,8 +54,8 @@ export default { } }, methods: { - deleteFeed(feed) { - fb.deleteFeed(feed.id) + async deleteFeed(feed) { + await fb.deleteFeed(feed.id) this.$toast.error(this.$t("deleted"), { icon: "delete", }) diff --git a/components/firebase/login.vue b/components/firebase/login.vue index e4e41e430..a2962d8eb 100644 --- a/components/firebase/login.vue +++ b/components/firebase/login.vue @@ -39,205 +39,144 @@ export default { icon: "vpn_key", }) }, - signInWithGoogle() { - const provider = new firebase.auth.GoogleAuthProvider() - const self = this - firebase - .auth() - .signInWithPopup(provider) - .then(({ additionalUserInfo }) => { - if (additionalUserInfo.isNewUser) { - self.$toast.info(`${self.$t("turn_on")} ${self.$t("sync")}`, { - icon: "sync", - duration: null, - closeOnSwipe: false, - action: { - text: self.$t("yes"), - onClick: (e, toastObject) => { - fb.writeSettings("syncHistory", true) - fb.writeSettings("syncCollections", true) - fb.writeSettings("syncEnvironments", true) - self.$router.push({ path: "/settings" }) - toastObject.remove() - }, + async signInWithGoogle() { + try { + const { additionUserInfo } = await fb.signInUserWithGoogle() + + if (additionalUserInfo.isNewUser) { + this.$toast.info(`${this.$t("turn_on")} ${this.$t("sync")}`, { + icon: "sync", + duration: null, + closeOnSwipe: false, + action: { + text: this.$t("yes"), + onClick: (e, toastObject) => { + fb.writeSettings("syncHistory", true) + fb.writeSettings("syncCollections", true) + fb.writeSettings("syncEnvironments", true) + this.$router.push({ path: "/settings" }) + toastObject.remove() }, - }) - } - self.showLoginSuccess() - }) - .catch((err) => { - // An error happened. - if (err.code === "auth/account-exists-with-different-credential") { - // Step 2. - // User's email already exists. - // The pending Google credential. - const pendingCred = err.credential - // The provider account's email address. - const email = err.email - // Get sign-in methods for this email. - firebase - .auth() - .fetchSignInMethodsForEmail(email) - .then((methods) => { - // Step 3. - // If the user has several sign-in methods, - // the first method in the list will be the "recommended" method to use. - if (methods[0] === "password") { - // Asks the user their password. - // In real scenario, you should handle this asynchronously. - const password = promptUserForPassword() // TODO: implement promptUserForPassword. - auth - .signInWithEmailAndPassword(email, password) - .then(( - user // Step 4a. - ) => user.linkWithCredential(pendingCred)) - .then(() => { - // Google account successfully linked to the existing Firebase user. - self.showLoginSuccess() - }) - return - } + }, + }) + } - self.$toast.info(`${self.$t("login_with")}`, { - icon: "vpn_key", - duration: null, - closeOnSwipe: false, - action: { - text: self.$t("yes"), - onClick: (e, toastObject) => { - // All the other cases are external providers. - // Construct provider object for that provider. - // TODO: implement getProviderForProviderId. - const provider = new firebase.auth.GithubAuthProvider() - // At this point, you should let the user know that they already has an account - // but with a different provider, and let them validate the fact they want to - // sign in with this provider. - // Sign in to provider. Note: browsers usually block popup triggered asynchronously, - // so in real scenario you should ask the user to click on a "continue" button - // that will trigger the signInWithPopup. - firebase - .auth() - .signInWithPopup(provider) - .then(({ user }) => { - // Remember that the user may have signed in with an account that has a different email - // address than the first one. This can happen as Firebase doesn't control the provider's - // sign in flow and the user is free to login using whichever account they own. - // Step 4b. - // Link to Google credential. - // As we have access to the pending credential, we can directly call the link method. - user.linkAndRetrieveDataWithCredential(pendingCred).then((usercred) => { - // Google account successfully linked to the existing Firebase user. - self.showLoginSuccess() - }) - }) + this.showLoginSuccess() + } catch (err) { + // An error happened. + if (err.code === "auth/account-exists-with-different-credential") { + // Step 2. + // User's email already exists. + // The pending Google credential. + const pendingCred = err.credential + // The provider account's email address. + const email = err.email + // Get sign-in methods for this email. + const methods = await fb.getSignInMethodsForEmail(email) - toastObject.remove() - }, - }, - }) - }) + // Step 3. + // If the user has several sign-in methods, + // the first method in the list will be the "recommended" method to use. + if (methods[0] === "password") { + // Asks the user their password. + // In real scenario, you should handle this asynchronously. + const password = promptUserForPassword() // TODO: implement promptUserForPassword. + const user = await fb.signInWithEmailAndPassword(email, password) + + await user.linkWithCredential(pendingCred) + this.showLoginSuccess() + + return } - }) + + this.$toast.info(`${this.$t("login_with")}`, { + icon: "vpn_key", + duration: null, + closeOnSwipe: false, + action: { + text: this.$t("yes"), + onClick: async (e, toastObject) => { + const user = await fb.signInWithGithub() + await user.linkAndRetrieveDataWithCredential(pendingCred) + + this.showLoginSuccess() + + toastObject.remove() + }, + }, + }) + } + } }, - signInWithGithub() { - const provider = new firebase.auth.GithubAuthProvider() - const self = this - firebase - .auth() - .signInWithPopup(provider) - .then(({ additionalUserInfo }) => { - if (additionalUserInfo.isNewUser) { - self.$toast.info(`${self.$t("turn_on")} ${self.$t("sync")}`, { - icon: "sync", - duration: null, - closeOnSwipe: false, - action: { - text: self.$t("yes"), - onClick: (e, toastObject) => { - fb.writeSettings("syncHistory", true) - fb.writeSettings("syncCollections", true) - fb.writeSettings("syncEnvironments", true) - self.$router.push({ path: "/settings" }) - toastObject.remove() - }, + async signInWithGithub() { + try { + const { additionalUserInfo } = await fb.signInUserWithGithub() + + if (additionalUserInfo.isNewUser) { + this.$toast.info(`${this.$t("turn_on")} ${this.$t("sync")}`, { + icon: "sync", + duration: null, + closeOnSwipe: false, + action: { + text: this.$t("yes"), + onClick: (e, toastObject) => { + fb.writeSettings("syncHistory", true) + fb.writeSettings("syncCollections", true) + fb.writeSettings("syncEnvironments", true) + this.$router.push({ path: "/settings" }) + toastObject.remove() }, - }) - } - self.showLoginSuccess() - }) - .catch((err) => { - // An error happened. - if (err.code === "auth/account-exists-with-different-credential") { - // Step 2. - // User's email already exists. - // The pending Google credential. - const pendingCred = err.credential - // The provider account's email address. - const email = err.email - // Get sign-in methods for this email. - firebase - .auth() - .fetchSignInMethodsForEmail(email) - .then((methods) => { - // Step 3. - // If the user has several sign-in methods, - // the first method in the list will be the "recommended" method to use. - if (methods[0] === "password") { - // Asks the user their password. - // In real scenario, you should handle this asynchronously. - const password = promptUserForPassword() // TODO: implement promptUserForPassword. - firebase - .auth() - .signInWithEmailAndPassword(email, password) - .then(( - user // Step 4a. - ) => user.linkWithCredential(pendingCred)) - .then(() => { - // Google account successfully linked to the existing Firebase user. - self.showLoginSuccess() - }) - return - } + }, + }) + } - self.$toast.info(`${self.$t("login_with")}`, { - icon: "vpn_key", - duration: null, - closeOnSwipe: false, - action: { - text: self.$t("yes"), - onClick: (e, toastObject) => { - // All the other cases are external providers. - // Construct provider object for that provider. - // TODO: implement getProviderForProviderId. - const provider = new firebase.auth.GoogleAuthProvider() - // At this point, you should let the user know that they already has an account - // but with a different provider, and let them validate the fact they want to - // sign in with this provider. - // Sign in to provider. Note: browsers usually block popup triggered asynchronously, - // so in real scenario you should ask the user to click on a "continue" button - // that will trigger the signInWithPopup. - firebase - .auth() - .signInWithPopup(provider) - .then(({ user }) => { - // Remember that the user may have signed in with an account that has a different email - // address than the first one. This can happen as Firebase doesn't control the provider's - // sign in flow and the user is free to login using whichever account they own. - // Step 4b. - // Link to Google credential. - // As we have access to the pending credential, we can directly call the link method. - user.linkAndRetrieveDataWithCredential(pendingCred).then((usercred) => { - self.showLoginSuccess() - }) - }) + this.showLoginSuccess() + } catch (err) { + // An error happened. + if (err.code === "auth/account-exists-with-different-credential") { + // Step 2. + // User's email already exists. + // The pending Google credential. + const pendingCred = err.credential + // The provider account's email address. + const email = err.email + // Get sign-in methods for this email. + const methods = await fb.getSignInMethodsForEmail(email) - toastObject.remove() - }, - }, - }) - }) + // Step 3. + // If the user has several sign-in methods, + // the first method in the list will be the "recommended" method to use. + if (methods[0] === "password") { + // Asks the user their password. + // In real scenario, you should handle this asynchronously. + const password = promptUserForPassword() // TODO: implement promptUserForPassword. + + const user = await fb.signInWithEmailAndPassword(email, password) + await user.linkWithCredential(pendingCred) + + this.showLoginSuccess() + + return } - }) + + this.$toast.info(`${this.$t("login_with")}`, { + icon: "vpn_key", + duration: null, + closeOnSwipe: false, + action: { + text: this.$t("yes"), + onClick: async (e, toastObject) => { + const { user } = await fb.signInUserWithGoogle() + await user.linkAndRetrieveDataWithCredential(pendingCred) + + this.showLoginSuccess() + + toastObject.remove() + }, + }, + }) + } + } }, }, } diff --git a/components/firebase/logout.vue b/components/firebase/logout.vue index 3f3d4d54d..c230e9d63 100644 --- a/components/firebase/logout.vue +++ b/components/firebase/logout.vue @@ -8,7 +8,6 @@