Cherry picking refactored fb.js from #879 by @AndrewBastin (#1286)

* Cherry picking refactored fb.js from #879 by @AndrewBastin

* Fixed a minor UI glitch in History section

* Removed logout success toast testcase

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
This commit is contained in:
Liyas Thomas
2020-10-17 14:53:19 +05:30
committed by GitHub
parent b6b3cbcb9a
commit 22a3bba6ab
13 changed files with 2408 additions and 473 deletions

View File

@@ -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")
})
})

View File

@@ -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")
})
})

View File

@@ -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())
})
})

View File

@@ -5,9 +5,9 @@
:key="feed.id"
class="flex-col py-2 border-b border-dashed border-brdColor"
>
<div class="show-on-large-screen">
<div data-test="list-item" class="show-on-large-screen">
<li class="info">
<label>
<label data-test="list-label">
{{ feed.label || $t("no_label") }}
</label>
</li>
@@ -16,7 +16,7 @@
</button>
</div>
<div class="show-on-large-screen">
<li class="info clamb-3">
<li data-test="list-message" class="info clamb-3">
<label>{{ feed.message || $t("empty") }}</label>
</li>
</div>
@@ -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",
})

View File

@@ -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()
},
},
})
}
}
},
},
}

View File

@@ -8,7 +8,6 @@
</template>
<script>
import firebase from "firebase/app"
import { fb } from "~/helpers/fb"
import exitToAppIcon from "~/static/icons/exit_to_app-24px.svg?inline"
@@ -20,20 +19,18 @@ export default {
}
},
methods: {
logout() {
fb.currentUser = null
const self = this
firebase
.auth()
.signOut()
.catch((err) => {
self.$toast.show(err.message || err, {
icon: "error",
})
async logout() {
try {
await fb.signOutUser()
this.$toast.info(this.$t("logged_out"), {
icon: "vpn_key",
})
self.$toast.info(this.$t("logged_out"), {
icon: "vpn_key",
})
} catch (err) {
this.$toast.show(err.message || err, {
icon: "error",
})
}
},
},
}