Add unit tests on Login component

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2021-04-16 14:49:43 +02:00
parent f2175c6498
commit 77200ea587
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
7 changed files with 295 additions and 53 deletions

View file

@ -28,7 +28,6 @@
"bulma-divider": "^0.2.0",
"core-js": "^3.6.4",
"date-fns": "^2.16.0",
"eslint-plugin-cypress": "^2.10.3",
"graphql": "^15.0.0",
"graphql-tag": "^2.10.3",
"intersection-observer": "^0.12.0",
@ -80,9 +79,11 @@
"@vue/test-utils": "^1.1.0",
"eslint": "^6.7.2",
"eslint-config-prettier": "^7.0.0",
"eslint-plugin-cypress": "^2.10.3",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-vue": "^7.0.0",
"eslint-plugin-vue": "^7.9.0",
"flush-promises": "^1.0.2",
"jest-junit": "^12.0.0",
"mock-apollo-client": "^0.6",
"prettier": "2.2.1",

View file

@ -158,11 +158,15 @@ const router = new Router({
router.beforeEach(authGuardIfNeeded);
router.afterEach(() => {
try {
if (router.app.$children[0]) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
router.app.$children[0].error = null;
}
} catch (e) {
console.error(e);
}
});
export default router;

View file

@ -67,7 +67,7 @@
</b-field>
<p class="control has-text-centered" v-if="!submitted">
<button class="button is-primary is-large">
<button type="submit" class="button is-primary is-large">
{{ $t("Login") }}
</button>
</p>
@ -222,14 +222,37 @@ export default class Login extends Vue {
}
saveUserData(data.login);
await this.setupClientUserAndActors(data.login);
if (this.$route.query.redirect) {
this.$router.push(this.$route.query.redirect as string);
return;
}
if (window.localStorage) {
window.localStorage.setItem("welcome-back", "yes");
}
this.$router.push({ name: RouteName.HOME });
return;
} catch (err) {
this.submitted = false;
if (err.graphQLErrors) {
err.graphQLErrors.forEach(({ message }: { message: string }) => {
this.errors.push(message);
});
} else if (err.networkError) {
this.errors.push(err.networkError.message);
}
}
}
private async setupClientUserAndActors(login: ILogin): Promise<void> {
await this.$apollo.mutate({
mutation: UPDATE_CURRENT_USER_CLIENT,
variables: {
id: data.login.user.id,
id: login.user.id,
email: this.credentials.email,
isLoggedIn: true,
role: data.login.user.role,
role: login.user.role,
},
});
try {
@ -245,22 +268,6 @@ export default class Login extends Vue {
});
}
}
if (this.$route.query.redirect) {
console.log("redirect", this.$route.query.redirect);
this.$router.push(this.$route.query.redirect as string);
return;
}
window.localStorage.setItem("welcome-back", "yes");
this.$router.push({ name: RouteName.HOME });
return;
} catch (err) {
this.submitted = false;
console.error(err);
err.graphQLErrors.forEach(({ message }: { message: string }) => {
this.errors.push(message);
});
}
}
}
</script>

View file

@ -2,23 +2,45 @@ import { config, createLocalVue, mount } from "@vue/test-utils";
import { routes } from "@/router";
import App from "@/App.vue";
import VueRouter from "vue-router";
import Home from "@/views/Home.vue";
import Buefy from "buefy";
const localVue = createLocalVue();
config.mocks.$t = (key: string): string => key;
localVue.use(VueRouter);
const router = new VueRouter({ routes });
const wrapper = mount(App, {
localVue,
router,
stubs: ["NavBar", "mobilizon-footer"],
});
localVue.use(Buefy);
describe("routing", () => {
test("Homepage", async () => {
router.push("/");
const router = new VueRouter({ routes, mode: "history" });
const wrapper = mount(App, {
localVue,
router,
stubs: {
NavBar: true,
"mobilizon-footer": true,
},
});
expect(wrapper.html()).toContain('<div id="homepage">');
});
test("About", async () => {
const router = new VueRouter({ routes, mode: "history" });
const wrapper = mount(App, {
localVue,
router,
stubs: {
NavBar: true,
"mobilizon-footer": true,
},
});
router.push("/about");
await wrapper.vm.$nextTick();
expect(wrapper.findComponent(Home).exists()).toBe(true);
await wrapper.vm.$nextTick();
expect(wrapper.vm.$route.path).toBe("/about/instance");
expect(wrapper.html()).toContain(
'<a href="/about/instance" aria-current="page"'
);
});
});

View file

@ -0,0 +1,183 @@
import { config, createLocalVue, mount, Wrapper } from "@vue/test-utils";
import Login from "@/views/User/Login.vue";
import Buefy from "buefy";
import {
createMockClient,
MockApolloClient,
RequestHandler,
} from "mock-apollo-client";
import VueApollo from "vue-apollo";
import buildCurrentUserResolver from "@/apollo/user";
import { InMemoryCache } from "apollo-cache-inmemory";
import { configMock } from "../../mocks/config";
import { i18n } from "@/utils/i18n";
import { CONFIG } from "@/graphql/config";
import { loginMock, loginResponseMock } from "../../mocks/auth";
import { LOGIN } from "@/graphql/auth";
import { CURRENT_USER_CLIENT } from "@/graphql/user";
import { ICurrentUser } from "@/types/current-user.model";
import flushPromises from "flush-promises";
import RouteName from "@/router/name";
const localVue = createLocalVue();
localVue.use(Buefy);
config.mocks.$t = (key: string): string => key;
const $router = { push: jest.fn() };
describe("Render login form", () => {
let wrapper: Wrapper<Vue>;
let mockClient: MockApolloClient | null;
let apolloProvider;
let requestHandlers: Record<string, RequestHandler>;
const generateWrapper = (
handlers: Record<string, unknown> = {},
customProps: Record<string, unknown> = {},
baseData: Record<string, unknown> = {},
customMocks: Record<string, unknown> = {}
) => {
const cache = new InMemoryCache({ addTypename: true });
mockClient = createMockClient({
cache,
resolvers: buildCurrentUserResolver(cache),
});
requestHandlers = {
configQueryHandler: jest.fn().mockResolvedValue(configMock),
loginMutationHandler: jest.fn().mockResolvedValue(loginResponseMock),
...handlers,
};
mockClient.setRequestHandler(CONFIG, requestHandlers.configQueryHandler);
mockClient.setRequestHandler(LOGIN, requestHandlers.loginMutationHandler);
apolloProvider = new VueApollo({
defaultClient: mockClient,
});
wrapper = mount(Login, {
localVue,
i18n,
apolloProvider,
propsData: {
...customProps,
},
mocks: {
$route: { query: {} },
$router,
...customMocks,
},
stubs: ["router-link", "router-view"],
data() {
return {
...baseData,
};
},
});
};
afterEach(() => {
wrapper.destroy();
mockClient = null;
apolloProvider = null;
$router.push.mockReset();
});
it("requires email and password to be filled", async () => {
generateWrapper();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
expect(wrapper.exists()).toBe(true);
expect(requestHandlers.configQueryHandler).toHaveBeenCalled();
expect(wrapper.vm.$apollo.queries.config).toBeTruthy();
wrapper.find('form input[type="email"]').setValue("");
wrapper.find('form input[type="password"]').setValue("");
wrapper.find("form button.button").trigger("click");
const form = wrapper.find("form");
expect(form.exists()).toBe(true);
const formElement = form.element as HTMLFormElement;
expect(formElement.checkValidity()).toBe(false);
});
it("renders and submits the login form", async () => {
generateWrapper();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
expect(wrapper.exists()).toBe(true);
expect(requestHandlers.configQueryHandler).toHaveBeenCalled();
expect(wrapper.vm.$apollo.queries.config).toBeTruthy();
wrapper.find('form input[type="email"]').setValue("some@email.tld");
wrapper.find('form input[type="password"]').setValue("somepassword");
wrapper.find("form").trigger("submit");
await wrapper.vm.$nextTick();
expect(requestHandlers.loginMutationHandler).toHaveBeenCalledWith({
...loginMock,
});
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
const currentUser = mockClient?.cache.readQuery<{
currentUser: ICurrentUser;
}>({
query: CURRENT_USER_CLIENT,
})?.currentUser;
expect(currentUser?.email).toBe("some@email.tld");
expect(currentUser?.id).toBe("1");
expect(jest.isMockFunction(wrapper.vm.$router.push)).toBe(true);
await flushPromises();
expect($router.push).toHaveBeenCalledWith({ name: RouteName.HOME });
});
it("handles a login error", async () => {
generateWrapper({
loginMutationHandler: jest.fn().mockResolvedValue({
errors: [
{
message:
'"Impossible to authenticate, either your email or password are invalid."',
},
],
}),
});
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
expect(wrapper.exists()).toBe(true);
expect(requestHandlers.configQueryHandler).toHaveBeenCalled();
expect(wrapper.vm.$apollo.queries.config).toBeTruthy();
wrapper.find('form input[type="email"]').setValue("some@email.tld");
wrapper.find('form input[type="password"]').setValue("somepassword");
wrapper.find("form").trigger("submit");
await wrapper.vm.$nextTick();
expect(requestHandlers.loginMutationHandler).toHaveBeenCalledWith({
...loginMock,
});
await flushPromises();
expect(wrapper.find("article.message.is-danger").text()).toContain(
"Impossible to authenticate, either your email or password are invalid."
);
expect($router.push).not.toHaveBeenCalled();
});
it("handles redirection after login", async () => {
generateWrapper(
{},
{},
{},
{
$route: { query: { redirect: "/about/instance" } },
}
);
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
wrapper.find('form input[type="email"]').setValue("some@email.tld");
wrapper.find('form input[type="password"]').setValue("somepassword");
wrapper.find("form").trigger("submit");
await flushPromises();
expect($router.push).toHaveBeenCalledWith("/about/instance");
});
});

View file

@ -0,0 +1,20 @@
export const loginMock = {
email: "some@email.tld",
password: "somepassword",
};
export const loginResponseMock = {
data: {
login: {
__typename: "Login",
accessToken: "some access token",
refreshToken: "some refresh token",
user: {
__typename: "User",
email: "some@email.tld",
id: "1",
role: "ADMINISTRATOR",
},
},
},
};

View file

@ -4448,9 +4448,9 @@ date-fns@^1.27.2:
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
date-fns@^2.16.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.20.3.tgz#5a28718edb95a80db96187b25340959867d36bc8"
integrity sha512-BbiJSlfmr1Fnfi1OHY8arklKdwtZ9n3NkjCeK8G9gtEe0ZSUwJuwHc6gYBl0uoC0Oa5RdpJV1gBBdXcZi8Efdw==
version "2.21.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.21.1.tgz#679a4ccaa584c0706ea70b3fa92262ac3009d2b0"
integrity sha512-m1WR0xGiC6j6jNFAyW4Nvh4WxAi4JF4w9jRJwSI8nBmNcyZXPcP9VUQG+6gHQXAmqaGEKDKhOqAtENDC941UkA==
de-indent@^1.0.2:
version "1.0.2"
@ -5081,13 +5081,13 @@ eslint-plugin-import@^2.20.2:
tsconfig-paths "^3.9.0"
eslint-plugin-prettier@^3.1.3:
version "3.3.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.1.tgz#7079cfa2497078905011e6f82e8dd8453d1371b7"
integrity sha512-Rq3jkcFY8RYeQLgk2cCwuc0P7SEFwDravPhsJZOQ5N4YI4DSg50NyqJ/9gdZHzQlHf8MvafSesbNJCcP/FF6pQ==
version "3.4.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.0.tgz#cdbad3bf1dbd2b177e9825737fe63b476a08f0c7"
integrity sha512-UDK6rJT6INSfcOo545jiaOwB701uAIt2/dR7WnFQoGCVl1/EMqdANBmwUaqqQ45aXprsTGzSa39LI1PyuRBxxw==
dependencies:
prettier-linter-helpers "^1.0.0"
eslint-plugin-vue@^7.0.0:
eslint-plugin-vue@^7.9.0:
version "7.9.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-7.9.0.tgz#f8e83a2a908f4c43fc8304f5401d4ff671f3d560"
integrity sha512-2Q0qQp5+5h+pZvJKCbG1/jCRUYrdgAz5BYKGyTlp2NU8mx09u3Hp7PsH6d5qef6ojuPoCXMnrbbDxeoplihrSw==
@ -5825,6 +5825,11 @@ flow-static-land@0.2.8:
resolved "https://registry.yarnpkg.com/flow-static-land/-/flow-static-land-0.2.8.tgz#49617e531396928bae6eb5d8ba32e7071637e5b9"
integrity sha512-pOZFExu2rbscCgcEo7nL7FNhBubMi18dn1Un4lm8LOmQkYhgsHLsrBGMWmuJXRWcYMrOC7I/bPsiqqVjdD3K1g==
flush-promises@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/flush-promises/-/flush-promises-1.0.2.tgz#4948fd58f15281fed79cbafc86293d5bb09b2ced"
integrity sha512-G0sYfLQERwKz4+4iOZYQEZVpOt9zQrlItIxQAAYAWpfby3gbHrx0osCHz5RLl/XoXevXk0xoN4hDFky/VV9TrA==
flush-write-stream@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"