Fix Vue unit tests

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2021-06-04 20:24:43 +02:00
parent f100fce0da
commit 51106841ab
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
18 changed files with 283 additions and 66 deletions

View file

@ -99,9 +99,7 @@ import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
@Component({ @Component({
apollo: { apollo: {
currentActor: { currentActor: CURRENT_ACTOR_CLIENT,
query: CURRENT_ACTOR_CLIENT,
},
comments: { comments: {
query: COMMENTS_THREADS_WITH_REPLIES, query: COMMENTS_THREADS_WITH_REPLIES,
variables() { variables() {
@ -152,7 +150,6 @@ export default class CommentTree extends Vue {
} }
async createCommentForEvent(comment: IComment): Promise<void> { async createCommentForEvent(comment: IComment): Promise<void> {
console.log("creating comment", comment);
this.emptyCommentError = ["", "<p></p>"].includes(comment.text); this.emptyCommentError = ["", "<p></p>"].includes(comment.text);
if (this.emptyCommentError) return; if (this.emptyCommentError) return;
try { try {

View file

@ -217,25 +217,24 @@ export default class ParticipationWithoutAccount extends Vue {
); );
return; return;
} }
const { event } = cachedData; const participantStats = { ...cachedData.event.participantStats };
if (event === null) {
console.error(
"Cannot update event participant cache, because of null value."
);
return;
}
if (updateData.joinEvent.role === ParticipantRole.NOT_CONFIRMED) { if (updateData.joinEvent.role === ParticipantRole.NOT_CONFIRMED) {
event.participantStats.notConfirmed += 1; participantStats.notConfirmed += 1;
} else { } else {
event.participantStats.going += 1; participantStats.going += 1;
event.participantStats.participant += 1; participantStats.participant += 1;
} }
store.writeQuery({ store.writeQuery({
query: FETCH_EVENT_BASIC, query: FETCH_EVENT_BASIC,
variables: { uuid: this.event.uuid }, variables: { uuid: this.event.uuid },
data: { event }, data: {
event: {
...cachedData.event,
participantStats,
},
},
}); });
}, },
}); });

View file

@ -93,9 +93,9 @@ export default class PasswordReset extends Vue {
} }
saveUserData(data.resetPassword); saveUserData(data.resetPassword);
await this.$router.push({ name: RouteName.HOME }); this.$router.push({ name: RouteName.HOME });
return;
} catch (err) { } catch (err) {
console.error(err);
err.graphQLErrors.forEach(({ message }: { message: any }) => { err.graphQLErrors.forEach(({ message }: { message: any }) => {
this.errors.push(message); this.errors.push(message);
}); });

View file

@ -136,6 +136,7 @@ const errorLink = onError(
const fullLink = authMiddleware.concat(errorLink).concat(link); const fullLink = authMiddleware.concat(errorLink).concat(link);
const cache = new InMemoryCache({ const cache = new InMemoryCache({
addTypename: true,
typePolicies, typePolicies,
possibleTypes, possibleTypes,
dataIdFromObject: (object: any) => { dataIdFromObject: (object: any) => {

View file

@ -0,0 +1,19 @@
import { ICurrentUserRole } from "@/types/enums";
export const defaultResolvers = {
Query: {
currentUser: (): Record<string, any> => ({
email: "user@mail.com",
id: "2",
role: ICurrentUserRole.USER,
isLoggedIn: true,
__typename: "CurrentUser",
}),
currentActor: (): Record<string, any> => ({
id: "67",
preferredUsername: "someone",
name: "Personne",
__typename: "CurrentActor",
}),
},
};

View file

@ -6,9 +6,11 @@ import {
MockApolloClient, MockApolloClient,
RequestHandler, RequestHandler,
} from "mock-apollo-client"; } from "mock-apollo-client";
import buildCurrentUserResolver from "@/apollo/user";
import VueApollo from "vue-apollo"; import VueApollo from "vue-apollo";
import { COMMENTS_THREADS, CREATE_COMMENT_FROM_EVENT } from "@/graphql/comment"; import {
COMMENTS_THREADS_WITH_REPLIES,
CREATE_COMMENT_FROM_EVENT,
} from "@/graphql/comment";
import { CommentModeration } from "@/types/enums"; import { CommentModeration } from "@/types/enums";
import { IEvent } from "@/types/event.model"; import { IEvent } from "@/types/event.model";
import { import {
@ -16,8 +18,9 @@ import {
newCommentForEventMock, newCommentForEventMock,
newCommentForEventResponse, newCommentForEventResponse,
} from "../../mocks/event"; } from "../../mocks/event";
import { InMemoryCache } from "@apollo/client/cache"; import flushPromises from "flush-promises";
import { InMemoryCache } from "@apollo/client/core";
import { defaultResolvers } from "../../common";
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Buefy); localVue.use(Buefy);
localVue.use(VueApollo); localVue.use(VueApollo);
@ -35,13 +38,12 @@ describe("CommentTree", () => {
let mockClient: MockApolloClient; let mockClient: MockApolloClient;
let apolloProvider; let apolloProvider;
let requestHandlers: Record<string, RequestHandler>; let requestHandlers: Record<string, RequestHandler>;
const cache = new InMemoryCache({ addTypename: false });
const generateWrapper = (handlers = {}, baseData = {}) => { const generateWrapper = (handlers = {}, baseData = {}) => {
const cache = new InMemoryCache({ addTypename: true });
mockClient = createMockClient({ mockClient = createMockClient({
cache, cache,
resolvers: buildCurrentUserResolver(cache), resolvers: defaultResolvers,
}); });
requestHandlers = { requestHandlers = {
@ -55,14 +57,13 @@ describe("CommentTree", () => {
}; };
mockClient.setRequestHandler( mockClient.setRequestHandler(
COMMENTS_THREADS, COMMENTS_THREADS_WITH_REPLIES,
requestHandlers.eventCommentThreadsQueryHandler requestHandlers.eventCommentThreadsQueryHandler
); );
mockClient.setRequestHandler( mockClient.setRequestHandler(
CREATE_COMMENT_FROM_EVENT, CREATE_COMMENT_FROM_EVENT,
requestHandlers.createCommentForEventMutationHandler requestHandlers.createCommentForEventMutationHandler
); );
apolloProvider = new VueApollo({ apolloProvider = new VueApollo({
defaultClient: mockClient, defaultClient: mockClient,
}); });
@ -76,16 +77,13 @@ describe("CommentTree", () => {
stubs: ["editor"], stubs: ["editor"],
data() { data() {
return { return {
currentActor: {
id: "76",
},
...baseData, ...baseData,
}; };
}, },
}); });
}; };
it("renders a comment tree", async () => { it("renders an empty comment tree", async () => {
generateWrapper(); generateWrapper();
expect(wrapper.exists()).toBe(true); expect(wrapper.exists()).toBe(true);
@ -98,7 +96,7 @@ describe("CommentTree", () => {
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });
it("renders a comment tree", async () => { it("renders a comment tree with comments", async () => {
generateWrapper(); generateWrapper();
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
@ -124,14 +122,7 @@ describe("CommentTree", () => {
} }
); );
wrapper.setData({ await flushPromises();
currentActor: {
id: "67",
},
});
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick(); // because of the <transition>
expect(wrapper.find("form.new-comment").isVisible()).toBe(true); expect(wrapper.find("form.new-comment").isVisible()).toBe(true);
expect(wrapper.findAll(".comment-list .root-comment").length).toBe(2); expect(wrapper.findAll(".comment-list .root-comment").length).toBe(2);
@ -147,7 +138,7 @@ describe("CommentTree", () => {
if (mockClient) { if (mockClient) {
const cachedData = mockClient.cache.readQuery<{ event: IEvent }>({ const cachedData = mockClient.cache.readQuery<{ event: IEvent }>({
query: COMMENTS_THREADS, query: COMMENTS_THREADS_WITH_REPLIES,
variables: { variables: {
eventUUID: eventData.uuid, eventUUID: eventData.uuid,
}, },

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CommentTree renders a comment tree 1`] = ` exports[`CommentTree renders a comment tree with comments 1`] = `
<div> <div>
<!----> <!---->
<transition-group-stub name="comment-empty-list" mode="out-in"> <transition-group-stub name="comment-empty-list" mode="out-in">
@ -13,7 +13,7 @@ exports[`CommentTree renders a comment tree 1`] = `
</div> </div>
`; `;
exports[`CommentTree renders a comment tree 2`] = ` exports[`CommentTree renders an empty comment tree 1`] = `
<div> <div>
<!----> <!---->
<transition-group-stub name="comment-empty-list" mode="out-in"> <transition-group-stub name="comment-empty-list" mode="out-in">

View file

@ -9,11 +9,11 @@ import {
MockApolloClient, MockApolloClient,
RequestHandler, RequestHandler,
} from "mock-apollo-client"; } from "mock-apollo-client";
import buildCurrentUserResolver from "@/apollo/user";
import { CONFIG } from "@/graphql/config"; import { CONFIG } from "@/graphql/config";
import VueApollo from "vue-apollo"; import VueApollo from "vue-apollo";
import { configMock } from "../../mocks/config"; import { configMock } from "../../mocks/config";
import { InMemoryCache } from "@apollo/client/cache"; import { InMemoryCache } from "@apollo/client/cache";
import { defaultResolvers } from "../../common";
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Buefy); localVue.use(Buefy);
@ -42,11 +42,11 @@ describe("ParticipationSection", () => {
customProps: Record<string, unknown> = {}, customProps: Record<string, unknown> = {},
baseData: Record<string, unknown> = {} baseData: Record<string, unknown> = {}
) => { ) => {
const cache = new InMemoryCache({ addTypename: true }); const cache = new InMemoryCache({ addTypename: false });
mockClient = createMockClient({ mockClient = createMockClient({
cache, cache,
resolvers: buildCurrentUserResolver(cache), resolvers: defaultResolvers,
}); });
requestHandlers = { requestHandlers = {
configQueryHandler: jest.fn().mockResolvedValue(configMock), configQueryHandler: jest.fn().mockResolvedValue(configMock),
@ -62,6 +62,9 @@ describe("ParticipationSection", () => {
localVue, localVue,
router, router,
apolloProvider, apolloProvider,
stubs: {
ParticipationButton: true,
},
propsData: { propsData: {
participation: null, participation: null,
event: eventData, event: eventData,
@ -70,9 +73,6 @@ describe("ParticipationSection", () => {
}, },
data() { data() {
return { return {
currentActor: {
id: "76",
},
...baseData, ...baseData,
}; };
}, },
@ -89,14 +89,15 @@ describe("ParticipationSection", () => {
expect(wrapper.find(".event-participation").exists()).toBeTruthy(); expect(wrapper.find(".event-participation").exists()).toBeTruthy();
const participationButton = wrapper.find( // TODO: Move to participation button test
".event-participation .participation-button a.button.is-large.is-primary" // const participationButton = wrapper.find(
); // ".event-participation .participation-button a.button.is-large.is-primary"
expect(participationButton.attributes("href")).toBe( // );
`/events/${eventData.uuid}/participate/with-account` // expect(participationButton.attributes("href")).toBe(
); // `/events/${eventData.uuid}/participate/with-account`
// );
expect(participationButton.text()).toBe("Participate"); // expect(participationButton.text()).toBe("Participate");
}); });
it("renders the participation section with existing confimed anonymous participation", async () => { it("renders the participation section with existing confimed anonymous participation", async () => {

View file

@ -13,7 +13,6 @@ import {
MockApolloClient, MockApolloClient,
RequestHandler, RequestHandler,
} from "mock-apollo-client"; } from "mock-apollo-client";
import buildCurrentUserResolver from "@/apollo/user";
import { CONFIG } from "@/graphql/config"; import { CONFIG } from "@/graphql/config";
import VueApollo from "vue-apollo"; import VueApollo from "vue-apollo";
import { FETCH_EVENT_BASIC, JOIN_EVENT } from "@/graphql/event"; import { FETCH_EVENT_BASIC, JOIN_EVENT } from "@/graphql/event";
@ -26,6 +25,8 @@ import {
joinEventResponseMock, joinEventResponseMock,
} from "../../mocks/event"; } from "../../mocks/event";
import { InMemoryCache } from "@apollo/client/cache"; import { InMemoryCache } from "@apollo/client/cache";
import { defaultResolvers } from "../../common";
import flushPromises from "flush-promises";
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Buefy); localVue.use(Buefy);
@ -65,11 +66,11 @@ describe("ParticipationWithoutAccount", () => {
customProps: Record<string, unknown> = {}, customProps: Record<string, unknown> = {},
baseData: Record<string, unknown> = {} baseData: Record<string, unknown> = {}
) => { ) => {
const cache = new InMemoryCache({ addTypename: true }); const cache = new InMemoryCache({ addTypename: false });
mockClient = createMockClient({ mockClient = createMockClient({
cache, cache,
resolvers: buildCurrentUserResolver(cache), resolvers: defaultResolvers,
}); });
requestHandlers = { requestHandlers = {
configQueryHandler: jest.fn().mockResolvedValue(configMock), configQueryHandler: jest.fn().mockResolvedValue(configMock),
@ -155,11 +156,7 @@ describe("ParticipationWithoutAccount", () => {
eventData.participantStats.participant + 1 eventData.participantStats.participant + 1
); );
} }
// lots of things to await await flushPromises();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
expect(wrapper.find("form").exists()).toBeFalsy(); expect(wrapper.find("form").exists()).toBeFalsy();
expect(wrapper.find("h1.title").text()).toBe( expect(wrapper.find("h1.title").text()).toBe(
"Request for participation confirmation sent" "Request for participation confirmation sent"

View file

@ -3,10 +3,14 @@ import PostListItem from "@/components/Post/PostListItem.vue";
import Buefy from "buefy"; import Buefy from "buefy";
import VueRouter from "vue-router"; import VueRouter from "vue-router";
import { routes } from "@/router"; import { routes } from "@/router";
import { enUS } from "date-fns/locale";
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Buefy); localVue.use(Buefy);
localVue.use(VueRouter); localVue.use(VueRouter);
localVue.use((vue) => {
vue.prototype.$dateFnsLocale = enUS;
});
const router = new VueRouter({ routes, mode: "history" }); const router = new VueRouter({ routes, mode: "history" });
config.mocks.$t = (key: string): string => key; config.mocks.$t = (key: string): string => key;

View file

@ -98,7 +98,7 @@ describe("ReportModal", () => {
); );
const switchButton = wrapper.find('input[type="checkbox"]'); const switchButton = wrapper.find('input[type="checkbox"]');
switchButton.trigger("click"); switchButton.setChecked();
const submit = wrapper.find("footer.modal-card-foot button.is-primary"); const submit = wrapper.find("footer.modal-card-foot button.is-primary");
submit.trigger("click"); submit.trigger("click");

View file

@ -0,0 +1,105 @@
import { config, createLocalVue, mount } from "@vue/test-utils";
import PasswordReset from "@/views/User/PasswordReset.vue";
import Buefy from "buefy";
import { createMockClient, RequestHandler } from "mock-apollo-client";
import { RESET_PASSWORD } from "@/graphql/auth";
import VueApollo from "vue-apollo";
import { resetPasswordResponseMock } from "../../mocks/auth";
import RouteName from "@/router/name";
import flushPromises from "flush-promises";
const localVue = createLocalVue();
localVue.use(Buefy);
config.mocks.$t = (key: string): string => key;
const $router = { push: jest.fn() };
let requestHandlers: Record<string, RequestHandler>;
const generateWrapper = (
customRequestHandlers: Record<string, RequestHandler> = {},
customMocks = {}
) => {
const mockClient = createMockClient();
requestHandlers = {
resetPasswordMutationHandler: jest
.fn()
.mockResolvedValue(resetPasswordResponseMock),
...customRequestHandlers,
};
mockClient.setRequestHandler(
RESET_PASSWORD,
requestHandlers.resetPasswordMutationHandler
);
const apolloProvider = new VueApollo({
defaultClient: mockClient,
});
return mount(PasswordReset, {
localVue,
mocks: {
$route: { query: {} },
$router,
...customMocks,
},
apolloProvider,
propsData: {
token: "some-token",
},
});
};
describe("Reset page", () => {
it("renders correctly", () => {
const wrapper = generateWrapper();
expect(wrapper.findAll('input[type="password"').length).toBe(2);
expect(wrapper.html()).toMatchSnapshot();
});
it("shows error if token is invalid", async () => {
const wrapper = generateWrapper({
resetPasswordMutationHandler: jest.fn().mockResolvedValue({
errors: [{ message: "The token you provided is invalid." }],
}),
});
wrapper.findAll('input[type="password"').setValue("my password");
wrapper.find("form").trigger("submit");
await wrapper.vm.$nextTick();
expect(requestHandlers.resetPasswordMutationHandler).toBeCalledTimes(1);
expect(requestHandlers.resetPasswordMutationHandler).toBeCalledWith({
password: "my password",
token: "some-token",
});
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
expect(wrapper.find("article.message.is-danger").text()).toContain(
"The token you provided is invalid"
);
expect(wrapper.html()).toMatchSnapshot();
});
it("redirects to homepage if token is valid", async () => {
const wrapper = generateWrapper();
wrapper.findAll('input[type="password"').setValue("my password");
wrapper.find("form").trigger("submit");
await wrapper.vm.$nextTick();
expect(requestHandlers.resetPasswordMutationHandler).toBeCalledTimes(1);
expect(requestHandlers.resetPasswordMutationHandler).toBeCalledWith({
password: "my password",
token: "some-token",
});
expect(jest.isMockFunction(wrapper.vm.$router.push)).toBe(true);
await flushPromises();
expect($router.push).toHaveBeenCalledWith({ name: RouteName.HOME });
});
});

View file

@ -0,0 +1,74 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Reset page renders correctly 1`] = `
<section class="section container">
<div class="columns is-mobile is-centered">
<div class="column is-half-desktop">
<h1 class="title">
Password reset
</h1>
<form>
<div class="field"><label class="label">Password</label>
<div class="control has-icons-right is-clearfix"><input type="password" autocomplete="on" aria-required="true" required="required" minlength="6" class="input">
<!----><span class="icon is-right has-text-primary is-clickable"><i class="mdi mdi-eye mdi-24px"></i></span>
<!---->
</div>
<!---->
</div>
<div class="field"><label class="label">Password (confirmation)</label>
<div class="control has-icons-right is-clearfix"><input type="password" autocomplete="on" aria-required="true" required="required" minlength="6" class="input">
<!----><span class="icon is-right has-text-primary is-clickable"><i class="mdi mdi-eye mdi-24px"></i></span>
<!---->
</div>
<!---->
</div> <button class="button is-primary">
Reset my password
</button>
</form>
</div>
</div>
</section>
`;
exports[`Reset page shows error if token is invalid 1`] = `
<section class="section container">
<div class="columns is-mobile is-centered">
<div class="column is-half-desktop">
<h1 class="title">
Password reset
</h1>
<transition-stub name="fade">
<article class="message is-danger">
<header class="message-header">
<p>Error</p><button type="button" class="delete"></button>
</header>
<section class="message-body">
<div class="media">
<!---->
<div class="media-content">The token you provided is invalid.</div>
</div>
</section>
</article>
</transition-stub>
<form>
<div class="field"><label class="label">Password</label>
<div class="control has-icons-right is-clearfix"><input type="password" autocomplete="on" aria-required="true" required="required" minlength="6" class="input">
<!----><span class="icon is-right has-text-primary is-clickable"><i class="mdi mdi-eye mdi-24px"></i></span>
<!---->
</div>
<!---->
</div>
<div class="field"><label class="label">Password (confirmation)</label>
<div class="control has-icons-right is-clearfix"><input type="password" autocomplete="on" aria-required="true" required="required" minlength="6" class="input">
<!----><span class="icon is-right has-text-primary is-clickable"><i class="mdi mdi-eye mdi-24px"></i></span>
<!---->
</div>
<!---->
</div> <button class="button is-primary">
Reset my password
</button>
</form>
</div>
</div>
</section>
`;

View file

@ -36,7 +36,7 @@ describe("Render login form", () => {
baseData: Record<string, unknown> = {}, baseData: Record<string, unknown> = {},
customMocks: Record<string, unknown> = {} customMocks: Record<string, unknown> = {}
) => { ) => {
const cache = new InMemoryCache({ addTypename: true }); const cache = new InMemoryCache({ addTypename: false });
mockClient = createMockClient({ mockClient = createMockClient({
cache, cache,

View file

@ -25,7 +25,7 @@ describe("App component", () => {
let requestHandlers: Record<string, RequestHandler>; let requestHandlers: Record<string, RequestHandler>;
const createComponent = (handlers = {}, baseData = {}) => { const createComponent = (handlers = {}, baseData = {}) => {
const cache = new InMemoryCache({ addTypename: true }); const cache = new InMemoryCache({ addTypename: false });
mockClient = createMockClient({ mockClient = createMockClient({
cache, cache,

View file

@ -18,3 +18,17 @@ export const loginResponseMock = {
}, },
}, },
}; };
export const resetPasswordResponseMock = {
data: {
resetPassword: {
__typename: "Login",
accessToken: "some access token",
refreshToken: "some refresh token",
user: {
__typename: "User",
id: "1",
},
},
},
};

View file

@ -113,6 +113,11 @@ export const configMock = {
__typename: "InstanceFeeds", __typename: "InstanceFeeds",
enabled: false, enabled: false,
}, },
webPush: {
__typename: "WebPush",
enabled: true,
publicKey: "",
},
}, },
}, },
}; };

View file

@ -80,6 +80,9 @@ export const eventCommentThreadsMock = {
visibility: "PUBLIC", visibility: "PUBLIC",
totalReplies: 5, totalReplies: 5,
updatedAt: "2020-12-03T09:02:00Z", updatedAt: "2020-12-03T09:02:00Z",
inReplyToComment: null,
originComment: null,
replies: [],
actor: { actor: {
__typename: "Person", __typename: "Person",
avatar: { avatar: {
@ -94,6 +97,7 @@ export const eventCommentThreadsMock = {
summary: "I am the senate", summary: "I am the senate",
}, },
deletedAt: null, deletedAt: null,
isAnnouncement: false,
}, },
{ {
__typename: "Comment", __typename: "Comment",
@ -105,6 +109,9 @@ export const eventCommentThreadsMock = {
visibility: "PUBLIC", visibility: "PUBLIC",
totalReplies: 0, totalReplies: 0,
updatedAt: "2020-12-03T11:02:00Z", updatedAt: "2020-12-03T11:02:00Z",
inReplyToComment: null,
originComment: null,
replies: [],
actor: { actor: {
__typename: "Person", __typename: "Person",
avatar: { avatar: {
@ -119,6 +126,7 @@ export const eventCommentThreadsMock = {
summary: "I am the senate", summary: "I am the senate",
}, },
deletedAt: null, deletedAt: null,
isAnnouncement: false,
}, },
], ],
}, },
@ -129,6 +137,7 @@ export const newCommentForEventMock = {
eventId: "1", eventId: "1",
text: "my new comment", text: "my new comment",
inReplyToCommentId: null, inReplyToCommentId: null,
isAnnouncement: false,
}; };
export const newCommentForEventResponse: DataMock = { export const newCommentForEventResponse: DataMock = {
@ -160,6 +169,7 @@ export const newCommentForEventResponse: DataMock = {
summary: "I am the senate", summary: "I am the senate",
}, },
deletedAt: null, deletedAt: null,
isAnnouncement: false,
}, },
}, },
}; };