Migrate to Vue 3 and Vite

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2022-07-12 10:55:28 +02:00
parent 8f4099ee33
commit ee20e03cc2
464 changed files with 31515 additions and 32758 deletions

16
js/tests/unit/setup.ts Normal file
View File

@@ -0,0 +1,16 @@
import "./specs/mocks/matchMedia";
import { config } from "@vue/test-utils";
import { createHead } from "@vueuse/head";
import { createI18n } from "vue-i18n";
import en_US from "@/i18n/en_US.json";
const i18n = createI18n({
legacy: false,
messages: { en_US },
locale: "en_US",
});
const head = createHead();
config.global.plugins.push(head);
config.global.plugins.push(i18n);

View File

@@ -1,50 +0,0 @@
import { config, createLocalVue, mount } from "@vue/test-utils";
import { routes } from "@/router";
import App from "@/App.vue";
import VueRouter from "vue-router";
import Buefy from "buefy";
import flushPromises from "flush-promises";
import VueAnnouncer from "@vue-a11y/announcer";
import VueSkipTo from "@vue-a11y/skip-to";
const localVue = createLocalVue();
config.mocks.$t = (key: string): string => key;
localVue.use(VueRouter);
localVue.use(Buefy);
localVue.use(VueAnnouncer);
localVue.use(VueSkipTo);
describe("routing", () => {
test("Homepage", async () => {
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 flushPromises();
expect(wrapper.vm.$route.path).toBe("/about/instance");
expect(wrapper.html()).toContain(
'<a href="/about/instance" aria-current="page"'
);
});
});

View File

@@ -1,12 +1,10 @@
import { config, createLocalVue, shallowMount, Wrapper } from "@vue/test-utils";
import { config, shallowMount, VueWrapper } from "@vue/test-utils";
import CommentTree from "@/components/Comment/CommentTree.vue";
import Buefy from "buefy";
import {
createMockClient,
MockApolloClient,
RequestHandler,
} from "mock-apollo-client";
import VueApollo from "@vue/apollo-option";
import {
COMMENTS_THREADS_WITH_REPLIES,
CREATE_COMMENT_FROM_EVENT,
@@ -21,10 +19,20 @@ import {
} from "../../mocks/event";
import flushPromises from "flush-promises";
import { defaultResolvers } from "../../common";
const localVue = createLocalVue();
localVue.use(Buefy);
localVue.use(VueApollo);
config.mocks.$t = (key: string): string => key;
import { afterEach, describe, vi, it, expect, beforeEach } from "vitest";
import { DefaultApolloClient } from "@vue/apollo-composable";
import Oruga from "@oruga-ui/oruga-next";
import { notifierPlugin } from "@/plugins/notifier";
import { InMemoryCache } from "@apollo/client/cache";
import { createRouter, createWebHistory, Router } from "vue-router";
import { routes } from "@/router";
import { dialogPlugin } from "@/plugins/dialog";
config.global.plugins.push(Oruga);
config.global.plugins.push(notifierPlugin);
config.global.plugins.push(dialogPlugin);
let router: Router;
const eventData = {
id: "1",
@@ -34,21 +42,23 @@ const eventData = {
},
};
describe("CommentTree", () => {
let wrapper: Wrapper<Vue>;
let wrapper: VueWrapper;
let mockClient: MockApolloClient | null;
let apolloProvider;
let requestHandlers: Record<string, RequestHandler>;
const generateWrapper = (handlers = {}, baseData = {}) => {
const generateWrapper = (handlers = {}, extraProps = {}) => {
const cache = new InMemoryCache({ addTypename: true });
mockClient = createMockClient({
cache,
resolvers: defaultResolvers,
});
requestHandlers = {
eventCommentThreadsQueryHandler: jest
eventCommentThreadsQueryHandler: vi
.fn()
.mockResolvedValue(eventCommentThreadsMock),
createCommentForEventMutationHandler: jest
createCommentForEventMutationHandler: vi
.fn()
.mockResolvedValue(newCommentForEventResponse),
...handlers,
@@ -62,36 +72,39 @@ describe("CommentTree", () => {
CREATE_COMMENT_FROM_EVENT,
requestHandlers.createCommentForEventMutationHandler
);
apolloProvider = new VueApollo({
defaultClient: mockClient,
});
wrapper = shallowMount(CommentTree, {
localVue,
apolloProvider,
propsData: {
props: {
event: { ...eventData },
...extraProps,
},
stubs: ["editor"],
data() {
return {
...baseData,
};
global: {
provide: {
[DefaultApolloClient]: mockClient,
},
plugins: [router],
},
});
};
beforeEach(async () => {
router = createRouter({
history: createWebHistory(),
routes: routes,
});
// await router.isReady();
});
afterEach(() => {
mockClient = null;
requestHandlers = {};
apolloProvider = null;
wrapper.destroy();
wrapper.unmount();
});
it("renders a loading comment tree", async () => {
generateWrapper();
expect(wrapper.find(".loading").text()).toBe("Loading comments…");
expect(wrapper.find("p.text-center").text()).toBe("Loading comments…");
expect(wrapper.html()).toMatchSnapshot();
});
@@ -99,15 +112,14 @@ describe("CommentTree", () => {
it("renders a comment tree with comments", async () => {
generateWrapper();
await flushPromises();
expect(wrapper.exists()).toBe(true);
expect(
requestHandlers.eventCommentThreadsQueryHandler
).toHaveBeenCalledWith({ eventUUID: eventData.uuid });
expect(wrapper.vm.$apollo.queries.comments).toBeTruthy();
expect(wrapper.find(".loading").exists()).toBe(false);
expect(wrapper.findAll(".comment-list .root-comment").length).toBe(2);
await flushPromises();
expect(wrapper.find("p.text-center").exists()).toBe(false);
expect(wrapper.findAllComponents("comment-stub").length).toBe(2);
expect(wrapper.html()).toMatchSnapshot();
});
@@ -117,18 +129,20 @@ describe("CommentTree", () => {
{
newComment: {
text: newCommentForEventMock.text,
isAnnouncement: false,
},
}
);
await flushPromises();
expect(wrapper.find("form.new-comment").isVisible()).toBe(true);
expect(wrapper.findAll(".comment-list .root-comment").length).toBe(2);
expect(wrapper.find("form").isVisible()).toBe(true);
expect(wrapper.findAllComponents("comment-stub").length).toBe(2);
wrapper.getComponent({ ref: "commenteditor" });
wrapper.find("form.new-comment").trigger("submit");
wrapper.find("form").trigger("submit");
await wrapper.vm.$nextTick();
await flushPromises();
expect(
requestHandlers.createCommentForEventMutationHandler
).toHaveBeenCalledWith({
@@ -152,7 +166,7 @@ describe("CommentTree", () => {
it("renders an empty comment tree", async () => {
generateWrapper({
eventCommentThreadsQueryHandler: jest
eventCommentThreadsQueryHandler: vi
.fn()
.mockResolvedValue(eventNoCommentThreadsMock),
});
@@ -162,9 +176,7 @@ describe("CommentTree", () => {
requestHandlers.eventCommentThreadsQueryHandler
).toHaveBeenCalledWith({ eventUUID: eventData.uuid });
expect(wrapper.findComponent({ name: "EmptyContent" }).text()).toBe(
"No comments yet"
);
expect(wrapper.findComponent({ name: "EmptyContent" }).exists());
expect(wrapper.html()).toMatchSnapshot();
});
});

View File

@@ -1,73 +1,67 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1
exports[`CommentTree renders a comment tree with comments 1`] = `
<div>
<form class="new-comment">
<!---->
<article class="media">
<figure class="media-left">
<identity-picker-wrapper-stub value="[object Object]"></identity-picker-wrapper-stub>
exports[`CommentTree > renders a comment tree with comments 1`] = `
"<div data-v-1d76124d=\\"\\">
<form class=\\"\\" data-v-1d76124d=\\"\\">
<!--v-if-->
<article class=\\"flex flex-wrap items-start gap-2\\" data-v-1d76124d=\\"\\">
<figure class=\\"\\" data-v-1d76124d=\\"\\">
<identity-picker-wrapper-stub modelvalue=\\"[object Object]\\" inline=\\"false\\" masked=\\"false\\" data-v-1d76124d=\\"\\"></identity-picker-wrapper-stub>
</figure>
<div class="media-content">
<div class="field">
<div class="field">
<p class="control">
<editor-stub mode="comment" aria-label="Comment body" value=""></editor-stub>
</p>
<!---->
<div class=\\"flex-1\\" data-v-1d76124d=\\"\\">
<div class=\\"flex flex-col gap-2\\" data-v-1d76124d=\\"\\">
<div class=\\"editor-wrapper\\" data-v-1d76124d=\\"\\">
<editor-stub currentactor=\\"[object Object]\\" mode=\\"comment\\" modelvalue=\\"\\" aria-label=\\"Comment body\\" data-v-1d76124d=\\"\\"></editor-stub>
<!--v-if-->
</div>
<!---->
<!--v-if-->
</div>
</div>
<div class="send-comment">
<b-button-stub type="is-primary" iconleft="send" nativetype="submit" tag="button" class="comment-button-submit">Send</b-button-stub>
<div class=\\"\\" data-v-1d76124d=\\"\\">
<o-button-stub variant=\\"primary\\" iconleft=\\"send\\" rounded=\\"false\\" outlined=\\"false\\" expanded=\\"false\\" inverted=\\"false\\" nativetype=\\"submit\\" tag=\\"button\\" disabled=\\"false\\" iconboth=\\"false\\" data-v-1d76124d=\\"\\"></o-button-stub>
</div>
</article>
</form>
<transition-group-stub tag="div" name="comment-empty-list">
<transition-group-stub tag="ul" name="comment-list" class="comment-list">
<comment-stub comment="[object Object]" event="[object Object]" class="root-comment"></comment-stub>
<comment-stub comment="[object Object]" event="[object Object]" class="root-comment"></comment-stub>
<transition-group-stub data-v-1d76124d=\\"\\">
<transition-group-stub data-v-1d76124d=\\"\\">
<comment-stub comment=\\"[object Object]\\" event=\\"[object Object]\\" currentactor=\\"[object Object]\\" rootcomment=\\"true\\" class=\\"root-comment\\" data-v-1d76124d=\\"\\"></comment-stub>
<comment-stub comment=\\"[object Object]\\" event=\\"[object Object]\\" currentactor=\\"[object Object]\\" rootcomment=\\"true\\" class=\\"root-comment\\" data-v-1d76124d=\\"\\"></comment-stub>
</transition-group-stub>
</transition-group-stub>
</div>
</div>"
`;
exports[`CommentTree renders a loading comment tree 1`] = `
<div>
<!---->
<p class="loading has-text-centered">
Loading comments…
</p>
</div>
exports[`CommentTree > renders a loading comment tree 1`] = `
"<div data-v-1d76124d=\\"\\">
<!--v-if-->
<p class=\\"text-center\\" data-v-1d76124d=\\"\\">Loading comments…</p>
</div>"
`;
exports[`CommentTree renders an empty comment tree 1`] = `
<div>
<form class="new-comment">
<!---->
<article class="media">
<figure class="media-left">
<identity-picker-wrapper-stub value="[object Object]"></identity-picker-wrapper-stub>
exports[`CommentTree > renders an empty comment tree 1`] = `
"<div data-v-1d76124d=\\"\\">
<form class=\\"\\" data-v-1d76124d=\\"\\">
<!--v-if-->
<article class=\\"flex flex-wrap items-start gap-2\\" data-v-1d76124d=\\"\\">
<figure class=\\"\\" data-v-1d76124d=\\"\\">
<identity-picker-wrapper-stub modelvalue=\\"[object Object]\\" inline=\\"false\\" masked=\\"false\\" data-v-1d76124d=\\"\\"></identity-picker-wrapper-stub>
</figure>
<div class="media-content">
<div class="field">
<div class="field">
<p class="control">
<editor-stub mode="comment" aria-label="Comment body" value=""></editor-stub>
</p>
<!---->
<div class=\\"flex-1\\" data-v-1d76124d=\\"\\">
<div class=\\"flex flex-col gap-2\\" data-v-1d76124d=\\"\\">
<div class=\\"editor-wrapper\\" data-v-1d76124d=\\"\\">
<editor-stub currentactor=\\"[object Object]\\" mode=\\"comment\\" modelvalue=\\"\\" aria-label=\\"Comment body\\" data-v-1d76124d=\\"\\"></editor-stub>
<!--v-if-->
</div>
<!---->
<!--v-if-->
</div>
</div>
<div class="send-comment">
<b-button-stub type="is-primary" iconleft="send" nativetype="submit" tag="button" class="comment-button-submit">Send</b-button-stub>
<div class=\\"\\" data-v-1d76124d=\\"\\">
<o-button-stub variant=\\"primary\\" iconleft=\\"send\\" rounded=\\"false\\" outlined=\\"false\\" expanded=\\"false\\" inverted=\\"false\\" nativetype=\\"submit\\" tag=\\"button\\" disabled=\\"false\\" iconboth=\\"false\\" data-v-1d76124d=\\"\\"></o-button-stub>
</div>
</article>
</form>
<transition-group-stub tag="div" name="comment-empty-list">
<empty-content-stub icon="comment" descriptionclasses="" inline="true"><span>No comments yet</span></empty-content-stub>
<transition-group-stub data-v-1d76124d=\\"\\">
<empty-content-stub icon=\\"comment\\" descriptionclasses=\\"\\" inline=\\"true\\" center=\\"false\\" data-v-1d76124d=\\"\\"></empty-content-stub>
</transition-group-stub>
</div>
</div>"
`;

View File

@@ -1,15 +1,23 @@
import { config, createLocalVue, mount } from "@vue/test-utils";
import { config, mount } from "@vue/test-utils";
import GroupSection from "@/components/Group/GroupSection.vue";
import Buefy from "buefy";
import VueRouter, { Location } from "vue-router";
import RouteName from "@/router/name";
import { routes } from "@/router";
import { describe, it, expect, beforeEach } from "vitest";
import { createRouter, createWebHistory, Router } from "vue-router";
import Oruga from "@oruga-ui/oruga-next";
const localVue = createLocalVue();
localVue.use(Buefy);
config.mocks.$t = (key: string): string => key;
localVue.use(VueRouter);
const router = new VueRouter({ routes, mode: "history" });
config.global.plugins.push(Oruga);
let router: Router;
beforeEach(async () => {
router = createRouter({
history: createWebHistory(),
routes: routes,
});
// await router.isReady();
});
const groupPreferredUsername = "my_group";
const groupDomain = "remotedomain.net";
@@ -22,7 +30,7 @@ type Props = {
title?: string;
icon?: string;
privateSection?: boolean;
route?: Location;
route?: { name: string; params: { preferredUsername: string } };
};
const baseProps: Props = {
@@ -38,47 +46,42 @@ const baseProps: Props = {
const generateWrapper = (customProps: Props = {}) => {
return mount(GroupSection, {
localVue,
router,
propsData: { ...baseProps, ...customProps },
props: { ...baseProps, ...customProps },
slots: {
default: `<div>${defaultSlotText}</div>`,
create: `<router-link :to="{
name: 'POST_CREATE',
params: { preferredUsername: '${groupUsername}' },
}"
class="button is-primary"
>{{ $t("${createSlotButtonText}") }}</router-link
>`,
create: `<router-link :to="{
name: 'POST_CREATE',
params: {
preferredUsername: '${groupUsername}'
}
}"
class="btn-primary">${createSlotButtonText}</router-link>`,
},
global: {
plugins: [router],
},
});
};
describe("GroupSection", () => {
it("renders group section with basic informations", () => {
const wrapper = generateWrapper({});
const wrapper = generateWrapper();
expect(
wrapper
.find(".group-section-title h2 span.icon i")
.classes(`mdi-${baseProps.icon}`)
).toBe(true);
expect(wrapper.find("i.mdi").classes(`mdi-${baseProps.icon}`)).toBe(true);
expect(wrapper.find(".group-section-title h2 span:last-child").text()).toBe(
baseProps.title
expect(wrapper.find("h2").text()).toBe(baseProps.title);
expect(wrapper.find("a").attributes("href")).toBe(`/@${groupUsername}/p`);
// expect(wrapper.find(".group-section-title").classes("privateSection")).toBe(
// true
// );
expect(wrapper.find("section > div.flex-1").text()).toBe(defaultSlotText);
expect(wrapper.find(".flex.justify-end.p-2 a").text()).toBe(
createSlotButtonText
);
expect(wrapper.find(".group-section-title a").attributes("href")).toBe(
`/@${groupUsername}/p`
);
expect(wrapper.find(".group-section-title").classes("privateSection")).toBe(
true
);
expect(wrapper.find(".main-slot div").text()).toBe(defaultSlotText);
expect(wrapper.find(".create-slot a").text()).toBe(createSlotButtonText);
expect(wrapper.find(".create-slot a").attributes("href")).toBe(
expect(wrapper.find(".flex.justify-end.p-2 a").attributes("href")).toBe(
`/@${groupUsername}/p/new`
);
expect(wrapper.html()).toMatchSnapshot();
@@ -87,9 +90,9 @@ describe("GroupSection", () => {
it("renders public group section", () => {
const wrapper = generateWrapper({ privateSection: false });
expect(wrapper.find(".group-section-title").classes("privateSection")).toBe(
false
);
// expect(wrapper.find(".group-section-title").classes("privateSection")).toBe(
// false
// );
expect(wrapper.html()).toMatchSnapshot();
});
});

View File

@@ -1,25 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1
exports[`GroupSection renders group section with basic informations 1`] = `
<section>
<div class="group-section-title privateSection">
<h2><span class="icon"><i class="mdi mdi-bullhorn mdi-24px"></i></span> <span>My group section</span></h2> <a href="/@my_group@remotedomain.net/p" class="">View all</a>
exports[`GroupSection > renders group section with basic informations 1`] = `
"<section class=\\"flex flex-col mb-3 border-2 border-mbz-purple\\">
<div class=\\"flex items-stretch py-3 px-1 bg-yellow-1 text-violet-title\\">
<div class=\\"flex flex-1 gap-1\\"><span class=\\"o-icon\\"><i class=\\"mdi mdi-bullhorn 36\\"></i></span>
<h2 class=\\"text-2xl font-medium mt-0\\">My group section</h2>
</div><a href=\\"/@my_group@remotedomain.net/p\\" class=\\"self-center\\">View all</a>
</div>
<div class="main-slot">
<div class=\\"flex-1\\">
<div>A list of elements</div>
</div>
<div class="create-slot"><a href="/@my_group@remotedomain.net/p/new" class="button is-primary">+ Create a post</a></div>
</section>
<div class=\\"flex justify-end p-2\\"><a href=\\"/@my_group@remotedomain.net/p/new\\" class=\\"btn-primary\\">+ Create a post</a></div>
</section>"
`;
exports[`GroupSection renders public group section 1`] = `
<section>
<div class="group-section-title">
<h2><span class="icon"><i class="mdi mdi-bullhorn mdi-24px"></i></span> <span>My group section</span></h2> <a href="/@my_group@remotedomain.net/p" class="">View all</a>
exports[`GroupSection > renders public group section 1`] = `
"<section class=\\"flex flex-col mb-3 border-2 border-yellow-1\\">
<div class=\\"flex items-stretch py-3 px-1 bg-yellow-1 text-violet-title\\">
<div class=\\"flex flex-1 gap-1\\"><span class=\\"o-icon\\"><i class=\\"mdi mdi-bullhorn 36\\"></i></span>
<h2 class=\\"text-2xl font-medium mt-0\\">My group section</h2>
</div><a href=\\"/@my_group@remotedomain.net/p\\" class=\\"self-center\\">View all</a>
</div>
<div class="main-slot">
<div class=\\"flex-1\\">
<div>A list of elements</div>
</div>
<div class="create-slot"><a href="/@my_group@remotedomain.net/p/new" class="button is-primary">+ Create a post</a></div>
</section>
<div class=\\"flex justify-end p-2\\"><a href=\\"/@my_group@remotedomain.net/p/new\\" class=\\"btn-primary\\">+ Create a post</a></div>
</section>"
`;

View File

@@ -1,26 +1,26 @@
import { config, createLocalVue, mount, Wrapper } from "@vue/test-utils";
import { config, mount, VueWrapper } from "@vue/test-utils";
import ParticipationSection from "@/components/Participation/ParticipationSection.vue";
import Buefy from "buefy";
import VueRouter from "vue-router";
import { createRouter, createWebHistory, Router } from "vue-router";
import { routes } from "@/router";
import { CommentModeration, EventJoinOptions } from "@/types/enums";
import {
createMockClient,
MockApolloClient,
RequestHandler,
} from "mock-apollo-client";
import { CONFIG } from "@/graphql/config";
import VueApollo from "@vue/apollo-option";
import { configMock } from "../../mocks/config";
import { InMemoryCache } from "@apollo/client/cache";
import { defaultResolvers } from "../../common";
import { beforeEach, describe, expect, vi, it } from "vitest";
import Oruga from "@oruga-ui/oruga-next";
import FloatingVue from "floating-vue";
const localVue = createLocalVue();
localVue.use(Buefy);
localVue.use(VueRouter);
const router = new VueRouter({ routes, mode: "history" });
config.mocks.$t = (key: string): string => key;
config.global.plugins.push(Oruga);
config.global.plugins.push(FloatingVue);
let router: Router;
beforeEach(async () => {
router = createRouter({
history: createWebHistory(),
routes: routes,
});
// await router.isReady();
});
const eventData = {
id: "1",
uuid: "e37910ea-fd5a-4756-7634-00971f3f4107",
@@ -32,49 +32,31 @@ const eventData = {
};
describe("ParticipationSection", () => {
let wrapper: Wrapper<Vue>;
let mockClient: MockApolloClient;
let apolloProvider;
let requestHandlers: Record<string, RequestHandler>;
let wrapper: VueWrapper;
const generateWrapper = (
handlers: Record<string, unknown> = {},
customProps: Record<string, unknown> = {},
baseData: Record<string, unknown> = {}
) => {
const cache = new InMemoryCache({ addTypename: false });
mockClient = createMockClient({
cache,
resolvers: defaultResolvers,
});
requestHandlers = {
configQueryHandler: jest.fn().mockResolvedValue(configMock),
...handlers,
};
mockClient.setRequestHandler(CONFIG, requestHandlers.configQueryHandler);
apolloProvider = new VueApollo({
defaultClient: mockClient,
});
wrapper = mount(ParticipationSection, {
localVue,
router,
apolloProvider,
stubs: {
ParticipationButton: true,
},
propsData: {
props: {
participation: null,
event: eventData,
anonymousParticipation: null,
currentActor: { id: "5" },
identities: [],
anonymousParticipationConfig: {
allowed: true,
},
...customProps,
},
data() {
return {
...baseData,
};
global: {
plugins: [router],
},
});
};
@@ -84,8 +66,6 @@ describe("ParticipationSection", () => {
await wrapper.vm.$nextTick();
expect(wrapper.exists()).toBe(true);
expect(requestHandlers.configQueryHandler).toHaveBeenCalled();
expect(wrapper.vm.$apollo.queries.config).toBeTruthy();
expect(wrapper.find(".event-participation").exists()).toBeTruthy();
@@ -101,14 +81,14 @@ describe("ParticipationSection", () => {
});
it("renders the participation section with existing confimed anonymous participation", async () => {
generateWrapper({}, { anonymousParticipation: true });
generateWrapper({ anonymousParticipation: true });
expect(wrapper.find(".event-participation > small").text()).toContain(
"You are participating in this event anonymously"
);
const cancelAnonymousParticipationButton = wrapper.find(
".event-participation > button.button.is-text"
".event-participation > button.o-btn--text"
);
expect(cancelAnonymousParticipationButton.text()).toBe(
"Cancel anonymous participation"
@@ -127,20 +107,17 @@ describe("ParticipationSection", () => {
});
it("renders the participation section with existing confimed anonymous participation but event moderation", async () => {
generateWrapper(
{},
{
anonymousParticipation: true,
event: { ...eventData, joinOptions: EventJoinOptions.RESTRICTED },
}
);
generateWrapper({
anonymousParticipation: true,
event: { ...eventData, joinOptions: EventJoinOptions.RESTRICTED },
});
expect(wrapper.find(".event-participation > small").text()).toContain(
"You are participating in this event anonymously"
);
const cancelAnonymousParticipationButton = wrapper.find(
".event-participation > button.button.is-text"
".event-participation > button.o-btn--text"
);
expect(cancelAnonymousParticipationButton.text()).toBe(
"Cancel anonymous participation"
@@ -153,7 +130,7 @@ describe("ParticipationSection", () => {
ref: "anonymous-participation-modal",
});
expect(modal.isVisible()).toBeTruthy();
expect(modal.find("article.notification.is-primary").text()).toBe(
expect(modal.find(".o-notification--primary").text()).toBe(
"As the event organizer has chosen to manually validate participation requests, your participation will be really confirmed only once you receive an email stating it's being accepted."
);
@@ -163,7 +140,7 @@ describe("ParticipationSection", () => {
});
it("renders the participation section with existing unconfirmed anonymous participation", async () => {
generateWrapper({}, { anonymousParticipation: false });
generateWrapper({ anonymousParticipation: false });
expect(wrapper.find(".event-participation > small").text()).toContain(
"You are participating in this event anonymously but didn't confirm participation"
@@ -171,19 +148,16 @@ describe("ParticipationSection", () => {
});
it("renders the participation section but the event is already passed", async () => {
generateWrapper(
{},
{
event: {
...eventData,
beginsOn: "2020-12-02T10:52:56Z",
endsOn: "2020-12-03T10:52:56Z",
},
}
);
generateWrapper({
event: {
...eventData,
beginsOn: "2020-12-02T10:52:56Z",
endsOn: "2020-12-03T10:52:56Z",
},
});
expect(wrapper.find(".event-participation").exists()).toBeFalsy();
expect(wrapper.find("button.button.is-primary").text()).toBe(
expect(wrapper.find("button.o-btn--primary").text()).toBe(
"Event already passed"
);
});

View File

@@ -1,7 +1,5 @@
import { config, createLocalVue, mount, Wrapper } from "@vue/test-utils";
import { config, mount, VueWrapper } from "@vue/test-utils";
import ParticipationWithoutAccount from "@/components/Participation/ParticipationWithoutAccount.vue";
import Buefy from "buefy";
import VueRouter from "vue-router";
import { routes } from "@/router";
import {
CommentModeration,
@@ -13,26 +11,34 @@ import {
MockApolloClient,
RequestHandler,
} from "mock-apollo-client";
import { CONFIG } from "@/graphql/config";
import VueApollo from "@vue/apollo-option";
import { ANONYMOUS_ACTOR_ID } from "@/graphql/config";
import { FETCH_EVENT_BASIC, JOIN_EVENT } from "@/graphql/event";
import { IEvent } from "@/types/event.model";
import { i18n } from "@/utils/i18n";
import { configMock } from "../../mocks/config";
import { anonymousActorIdMock } from "../../mocks/config";
import {
fetchEventBasicMock,
joinEventMock,
joinEventResponseMock,
} from "../../mocks/event";
import { InMemoryCache } from "@apollo/client/cache";
import { defaultResolvers } from "../../common";
import flushPromises from "flush-promises";
import { vi, describe, expect, it, beforeEach, afterEach } from "vitest";
import { DefaultApolloClient } from "@vue/apollo-composable";
import { Router, createRouter, createWebHistory } from "vue-router";
import Oruga from "@oruga-ui/oruga-next";
import { cache } from "@/apollo/memory";
const localVue = createLocalVue();
localVue.use(Buefy);
localVue.use(VueRouter);
const router = new VueRouter({ routes, mode: "history" });
config.mocks.$t = (key: string): string => key;
config.global.plugins.push(Oruga);
let router: Router;
beforeEach(async () => {
router = createRouter({
history: createWebHistory(),
routes: routes,
});
// await router.isReady();
});
const eventData = {
id: "1",
@@ -56,31 +62,38 @@ const eventData = {
};
describe("ParticipationWithoutAccount", () => {
let wrapper: Wrapper<Vue>;
let mockClient: MockApolloClient;
let apolloProvider;
let wrapper: VueWrapper;
let mockClient: MockApolloClient | null;
let requestHandlers: Record<string, RequestHandler>;
afterEach(() => {
wrapper?.unmount();
cache.reset();
mockClient = null;
});
const generateWrapper = (
handlers: Record<string, unknown> = {},
customProps: Record<string, unknown> = {},
baseData: Record<string, unknown> = {}
customProps: Record<string, unknown> = {}
) => {
const cache = new InMemoryCache({ addTypename: false });
mockClient = createMockClient({
cache,
resolvers: defaultResolvers,
});
requestHandlers = {
configQueryHandler: jest.fn().mockResolvedValue(configMock),
fetchEventQueryHandler: jest.fn().mockResolvedValue(fetchEventBasicMock),
joinEventMutationHandler: jest
anonymousActorIdQueryHandler: vi
.fn()
.mockResolvedValue(anonymousActorIdMock),
fetchEventQueryHandler: vi.fn().mockResolvedValue(fetchEventBasicMock),
joinEventMutationHandler: vi
.fn()
.mockResolvedValue(joinEventResponseMock),
...handlers,
};
mockClient.setRequestHandler(CONFIG, requestHandlers.configQueryHandler);
mockClient.setRequestHandler(
ANONYMOUS_ACTOR_ID,
requestHandlers.anonymousActorIdQueryHandler
);
mockClient.setRequestHandler(
FETCH_EVENT_BASIC,
requestHandlers.fetchEventQueryHandler
@@ -90,43 +103,33 @@ describe("ParticipationWithoutAccount", () => {
requestHandlers.joinEventMutationHandler
);
apolloProvider = new VueApollo({
defaultClient: mockClient,
});
wrapper = mount(ParticipationWithoutAccount, {
localVue,
router,
i18n,
apolloProvider,
propsData: {
props: {
uuid: eventData.uuid,
...customProps,
},
data() {
return {
...baseData,
};
global: {
provide: {
[DefaultApolloClient]: mockClient,
},
plugins: [router],
},
});
};
it("renders the participation without account view with minimal data", 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();
expect(requestHandlers.anonymousActorIdQueryHandler).toHaveBeenCalled();
expect(requestHandlers.fetchEventQueryHandler).toHaveBeenCalledWith({
uuid: eventData.uuid,
});
expect(wrapper.vm.$apollo.queries.event).toBeTruthy();
await flushPromises();
expect(wrapper.find(".hero-body .container").isVisible()).toBeTruthy();
expect(wrapper.find("article.message.is-info").text()).toBe(
expect(wrapper.find(".container").isVisible()).toBeTruthy();
expect(wrapper.find(".o-notification--info").text()).toBe(
"Your email will only be used to confirm that you're a real person and send you eventual updates for this event. It will NOT be transmitted to other instances or to the event organizer."
);
@@ -134,13 +137,13 @@ describe("ParticipationWithoutAccount", () => {
wrapper.find("textarea").setValue("a message long enough");
wrapper.find("form").trigger("submit");
await wrapper.vm.$nextTick();
await flushPromises();
expect(requestHandlers.joinEventMutationHandler).toHaveBeenCalledWith({
...joinEventMock,
});
const cachedData = mockClient.cache.readQuery<{ event: IEvent }>({
const cachedData = mockClient?.cache.readQuery<{ event: IEvent }>({
query: FETCH_EVENT_BASIC,
variables: {
uuid: eventData.uuid,
@@ -161,10 +164,10 @@ describe("ParticipationWithoutAccount", () => {
expect(wrapper.find("h1.title").text()).toBe(
"Request for participation confirmation sent"
);
// TextEncoder is not in js-dom
expect(
wrapper.find("article.message.is-warning .media-content").text()
).toBe("Unable to save your participation in this browser.");
// TextEncoder ~is~ was not in js-dom?
// expect(wrapper.find(".o-notification--error").text()).toBe(
// "Unable to save your participation in this browser."
// );
expect(wrapper.find("span.details").text()).toBe(
"Your participation will be validated once you click the confirmation link into the email."
@@ -174,7 +177,7 @@ describe("ParticipationWithoutAccount", () => {
it("renders the warning if the event participation is restricted", async () => {
generateWrapper({
fetchEventQueryHandler: jest.fn().mockResolvedValue({
fetchEventQueryHandler: vi.fn().mockResolvedValue({
data: {
event: {
...fetchEventBasicMock.data.event,
@@ -182,7 +185,7 @@ describe("ParticipationWithoutAccount", () => {
},
},
}),
joinEventMutationHandler: jest.fn().mockResolvedValue({
joinEventMutationHandler: vi.fn().mockResolvedValue({
data: {
joinEvent: {
...joinEventResponseMock.data.joinEvent,
@@ -192,17 +195,18 @@ describe("ParticipationWithoutAccount", () => {
}),
});
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await flushPromises();
expect(wrapper.vm.$data.event.joinOptions).toBe(
EventJoinOptions.RESTRICTED
);
// expect(wrapper.vm.$data.event.joinOptions).toBe(
// EventJoinOptions.RESTRICTED
// );
expect(wrapper.find(".hero-body .container").text()).toContain(
expect(wrapper.findAll("section.container form > p")[1].text()).toContain(
"The event organizer manually approves participations. Since you've chosen to participate without an account, please explain why you want to participate to this event."
);
expect(wrapper.find(".hero-body .container").text()).not.toContain(
expect(
wrapper.findAll("section.container form > p")[1].text()
).not.toContain(
"If you want, you may send a message to the event organizer here."
);
@@ -210,13 +214,13 @@ describe("ParticipationWithoutAccount", () => {
wrapper.find("textarea").setValue("a message long enough");
wrapper.find("form").trigger("submit");
await wrapper.vm.$nextTick();
await flushPromises();
expect(requestHandlers.joinEventMutationHandler).toHaveBeenCalledWith({
...joinEventMock,
});
const cachedData = mockClient.cache.readQuery<{ event: IEvent }>({
const cachedData = mockClient?.cache.readQuery<{ event: IEvent }>({
query: FETCH_EVENT_BASIC,
variables: {
uuid: eventData.uuid,
@@ -242,30 +246,28 @@ describe("ParticipationWithoutAccount", () => {
it("handles being already a participant", async () => {
generateWrapper({
joinEventMutationHandler: jest
joinEventMutationHandler: vi
.fn()
.mockRejectedValue(
new Error("You are already a participant of this event")
),
});
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await flushPromises();
wrapper.find('input[type="email"]').setValue("some@email.tld");
wrapper.find("textarea").setValue("a message long enough");
wrapper.find("form").trigger("submit");
await wrapper.vm.$nextTick();
await flushPromises();
expect(requestHandlers.joinEventMutationHandler).toHaveBeenCalledWith({
...joinEventMock,
});
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await flushPromises();
expect(wrapper.find("form").exists()).toBeTruthy();
expect(
wrapper.find("article.message.is-danger .media-content").text()
).toContain("You are already a participant of this event");
expect(wrapper.find(".o-notification--danger").text()).toContain(
"You are already a participant of this event"
);
expect(wrapper.html()).toMatchSnapshot();
});
});

View File

@@ -1,130 +1,83 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1
exports[`ParticipationWithoutAccount handles being already a participant 1`] = `
<section class="container section hero is-fullheight">
<div class="hero-body">
<div class="container">
<form>
<p>
This Mobilizon instance and this event organizer allows anonymous participations, but requires validation through email confirmation.
</p>
<transition-stub name="fade">
<article class="message is-info">
<!---->
<section class="message-body">
<div class="media">
<!---->
<div class="media-content">
Your email will only be used to confirm that you're a real person and send you eventual updates for this event. It will NOT be transmitted to other instances or to the event organizer.
</div>
</div>
</section>
<!---->
</article>
</transition-stub>
<transition-stub name="fade">
<article class="message is-danger">
<!---->
<section class="message-body">
<div class="media">
<!---->
<div class="media-content">You are already a participant of this event</div>
</div>
</section>
<!---->
</article>
</transition-stub>
<div class="field"><label class="label">Email address</label>
<div class="control is-clearfix"><input type="email" autocomplete="on" placeholder="Your email" required="required" class="input">
<!---->
<!---->
<!---->
exports[`ParticipationWithoutAccount > handles being already a participant 1`] = `
"<section class=\\"container mx-auto\\">
<div class=\\"\\">
<form>
<p>This Mobilizon instance and this event organizer allows anonymous participations, but requires validation through email confirmation.</p>
<transition-stub>
<article class=\\"o-notification o-notification--info\\">
<!--v-if-->
<!--v-if-->
<div class=\\"o-notification__wrapper\\">
<!--v-if-->
<div class=\\"o-notification__content\\">Your email will only be used to confirm that you're a real person and send you eventual updates for this event. It will NOT be transmitted to other instances or to the event organizer.</div>
</div>
<!---->
</div>
<p>
If you want, you may send a message to the event organizer here.
</p>
<div class="field"><label class="label">Message</label>
<div class="control is-clearfix"><textarea minlength="10" class="textarea"></textarea>
<!---->
<!---->
<!---->
</article>
</transition-stub>
<transition-stub>
<article class=\\"o-notification o-notification--danger\\">
<!--v-if-->
<!--v-if-->
<div class=\\"o-notification__wrapper\\">
<!--v-if-->
<div class=\\"o-notification__content\\">You are already a participant of this event</div>
</div>
<!---->
</article>
</transition-stub>
<div class=\\"o-field o-field--filled\\"><label for=\\"anonymousParticipationEmail\\" class=\\"o-field__label\\">Email address</label>
<div class=\\"o-ctrl-input\\"><input id=\\"anonymousParticipationEmail\\" placeholder=\\"Your email\\" required=\\"\\" class=\\"o-input\\" type=\\"email\\" autocomplete=\\"off\\">
<!--v-if-->
<!--v-if-->
<!--v-if-->
</div>
<div class="field">
<!----><label class="b-checkbox checkbox"><input type="checkbox" autocomplete="on" true-value="true" value="false"><span class="check"></span><span class="control-label"><b>Remember my participation in this browser</b> <p>
Will allow to display and manage your participation status on the event page when using this device. Uncheck if you're using a public device.
</p></span></label>
<!---->
</div> <button type="submit" class="button is-primary">
<!----><span>Send email</span>
<!---->
</button>
<div class="has-text-centered"><a class="button is-text">
<!----><span>Back to previous page</span>
<!---->
</a></div>
</form>
</div>
<!--v-if-->
</div>
<p>If you want, you may send a message to the event organizer here.</p>
<div class=\\"o-field o-field--filled\\"><label for=\\"anonymousParticipationMessage\\" class=\\"o-field__label\\">Message</label>
<div class=\\"o-ctrl-input\\"><textarea id=\\"anonymousParticipationMessage\\" minlength=\\"10\\" class=\\"o-input o-input__textarea\\"></textarea>
<!--v-if-->
<!--v-if-->
<!--v-if-->
</div>
<!--v-if-->
</div>
<div class=\\"o-field\\">
<!--v-if--><label class=\\"o-chk o-chk--checked\\"><input type=\\"checkbox\\" class=\\"o-chk__check o-chk__check--checked\\" true-value=\\"true\\" false-value=\\"false\\" value=\\"false\\"><span class=\\"o-chk__label\\"><b>Remember my participation in this browser</b><p>Will allow to display and manage your participation status on the event page when using this device. Uncheck if you're using a public device.</p></span></label>
<!--v-if-->
</div>
<div class=\\"flex gap-2 my-2\\"><button type=\\"submit\\" class=\\"o-btn o-btn--primary o-btn--disabled\\" disabled=\\"\\"><span class=\\"o-btn__wrapper\\"><!--v-if--><span class=\\"o-btn__label\\">Send email</span>
<!--v-if--></span>
</button><button type=\\"button\\" class=\\"o-btn o-btn--text\\"><span class=\\"o-btn__wrapper\\"><!--v-if--><span class=\\"o-btn__label\\">Back to previous page</span>
<!--v-if--></span>
</button></div>
</form>
</div>
</section>
</section>"
`;
exports[`ParticipationWithoutAccount renders the participation without account view with minimal data 1`] = `
<section class="container section hero is-fullheight">
<div class="hero-body">
<div class="container">
<div>
<h1 class="title">
Request for participation confirmation sent
</h1>
<p class="content"><span>Check your inbox (and your junk mail folder).</span> <span class="details">Your participation will be validated once you click the confirmation link into the email.</span></p>
<transition-stub name="fade">
<article class="message is-warning">
<!---->
<section class="message-body">
<div class="media">
<!---->
<div class="media-content">Unable to save your participation in this browser.</div>
</div>
</section>
<!---->
</article>
</transition-stub>
<p class="content"><span>You may now close this window, or <a href="/events/f37910ea-fd5a-4756-9679-00971f3f4106" class="">return to the event's page</a>.</span></p>
</div>
exports[`ParticipationWithoutAccount > renders the participation without account view with minimal data 1`] = `
"<section class=\\"container mx-auto\\">
<div class=\\"\\">
<div>
<h1 class=\\"title\\">Request for participation confirmation sent</h1>
<p class=\\"prose dark:prose-invert\\"><span>Check your inbox (and your junk mail folder).</span><span class=\\"details\\">Your participation will be validated once you click the confirmation link into the email.</span></p>
<!--v-if-->
<p class=\\"prose dark:prose-invert\\">You may now close this window, or <a href=\\"/events/f37910ea-fd5a-4756-9679-00971f3f4106\\" class=\\"\\">return to the event's page</a>.</p>
</div>
</div>
</section>
</section>"
`;
exports[`ParticipationWithoutAccount renders the warning if the event participation is restricted 1`] = `
<section class="container section hero is-fullheight">
<div class="hero-body">
<div class="container">
<div>
<h1 class="title">
Request for participation confirmation sent
</h1>
<p class="content"><span>Check your inbox (and your junk mail folder).</span> <span class="details">
Your participation will be validated once you click the confirmation link into the email, and after the organizer manually validates your participation. </span></p>
<transition-stub name="fade">
<article class="message is-warning">
<!---->
<section class="message-body">
<div class="media">
<!---->
<div class="media-content">Unable to save your participation in this browser.</div>
</div>
</section>
<!---->
</article>
</transition-stub>
<p class="content"><span>You may now close this window, or <a href="/events/f37910ea-fd5a-4756-9679-00971f3f4106" class="">return to the event's page</a>.</span></p>
</div>
exports[`ParticipationWithoutAccount > renders the warning if the event participation is restricted 1`] = `
"<section class=\\"container mx-auto\\">
<div class=\\"\\">
<div>
<h1 class=\\"title\\">Request for participation confirmation sent</h1>
<p class=\\"prose dark:prose-invert\\"><span>Check your inbox (and your junk mail folder).</span><span class=\\"details\\">Your participation will be validated once you click the confirmation link into the email, and after the organizer manually validates your participation.</span></p>
<!--v-if-->
<p class=\\"prose dark:prose-invert\\">You may now close this window, or <a href=\\"/events/f37910ea-fd5a-4756-9679-00971f3f4106\\" class=\\"\\">return to the event's page</a>.</p>
</div>
</div>
</section>
</section>"
`;

View File

@@ -1,20 +1,20 @@
import { config, createLocalVue, mount } from "@vue/test-utils";
import { config, mount } from "@vue/test-utils";
import PostListItem from "@/components/Post/PostListItem.vue";
import Buefy from "buefy";
import VueRouter from "vue-router";
import { routes } from "@/router";
import { vi, beforeEach, describe, it, expect } from "vitest";
import { enUS } from "date-fns/locale";
import { formatDateTimeString } from "@/filters/datetime";
import { i18n } from "@/utils/i18n";
import { routes } from "@/router";
import { createRouter, createWebHistory, Router } from "vue-router";
const localVue = createLocalVue();
localVue.use(Buefy);
localVue.use(VueRouter);
localVue.use((vue) => {
vue.prototype.$dateFnsLocale = enUS;
let router: Router;
beforeEach(async () => {
router = createRouter({
history: createWebHistory(),
routes: routes,
});
// await router.isReady();
});
const router = new VueRouter({ routes, mode: "history" });
config.mocks.$t = (key: string): string => key;
const postData = {
id: "1",
@@ -31,15 +31,15 @@ const generateWrapper = (
customProps: Record<string, unknown> = {}
) => {
return mount(PostListItem, {
localVue,
router,
i18n,
propsData: {
props: {
post: { ...postData, ...customPostData },
...customProps,
},
filters: {
formatDateTimeString,
global: {
provide: {
dateFnsLocale: enUS,
},
plugins: [router],
},
});
};
@@ -50,15 +50,15 @@ describe("PostListItem", () => {
expect(wrapper.html()).toMatchSnapshot();
expect(
wrapper.find("a.post-minimalist-card-wrapper").attributes("href")
).toBe(`/p/${postData.slug}`);
expect(wrapper.find("a.block.bg-white").attributes("href")).toBe(
`/p/${postData.slug}`
);
expect(wrapper.find(".post-minimalist-title").text()).toBe(postData.title);
expect(wrapper.find("h3").text()).toBe(postData.title);
expect(wrapper.find(".post-publication-date").text()).toBe("Dec 2, 2020");
expect(wrapper.find("p.flex.gap-2").text()).toBe("Dec 2, 2020");
expect(wrapper.find(".post-publisher").exists()).toBeFalsy();
expect(wrapper.find("p.flex.gap-1").exists()).toBeFalsy();
});
it("renders post list item with tags", () => {
@@ -68,9 +68,11 @@ describe("PostListItem", () => {
expect(wrapper.html()).toMatchSnapshot();
expect(wrapper.find(".tags").text()).toContain("A tag");
expect(wrapper.find("div.flex.flex-wrap.gap-y-0.gap-x-2").text()).toContain(
"A tag"
);
expect(wrapper.find(".post-publisher").exists()).toBeFalsy();
expect(wrapper.find("p.flex.gap-1").exists()).toBeFalsy();
});
it("renders post list item with publisher name", () => {
@@ -81,7 +83,7 @@ describe("PostListItem", () => {
expect(wrapper.html()).toMatchSnapshot();
expect(wrapper.find(".post-publisher").exists()).toBeTruthy();
expect(wrapper.find(".post-publisher").text()).toContain("An author");
expect(wrapper.find("p.flex.gap-1").exists()).toBeTruthy();
expect(wrapper.find("p.flex.gap-1").text()).toContain("An author");
});
});

View File

@@ -1,45 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1
exports[`PostListItem renders post list item with basic informations 1`] = `
<a href="/p/my-blog-post-some-uuid" class="post-minimalist-card-wrapper" dir="auto">
<!---->
<div class="title-info-wrapper has-text-grey-dark px-1">
<h3 lang="en" class="post-minimalist-title">
My Blog Post
</h3>
<p class="post-publication-date"><span class="icon"><i class="mdi mdi-clock mdi-24px"></i></span> <span dir="auto" class="has-text-grey-dark">Dec 2, 2020</span></p>
<!---->
<!---->
exports[`PostListItem > renders post list item with basic informations 1`] = `
"<a href=\\"/p/my-blog-post-some-uuid\\" class=\\"block md:flex bg-white dark:bg-violet-2 dark:text-white dark:hover:text-white rounded-lg shadow-md\\" dir=\\"auto\\" data-v-3b2c1ec0=\\"\\">
<!--v-if-->
<div class=\\"flex flex-col gap-1 bg-inherit p-2 rounded-lg flex-1\\" data-v-3b2c1ec0=\\"\\">
<h3 class=\\"text-xl color-violet-3 line-clamp-3 mb-2 font-bold\\" lang=\\"en\\" data-v-3b2c1ec0=\\"\\">My Blog Post</h3>
<p class=\\"flex gap-2\\" data-v-3b2c1ec0=\\"\\"><span aria-hidden=\\"true\\" class=\\"material-design-icon clock-icon\\" role=\\"img\\" data-v-3b2c1ec0=\\"\\"><svg fill=\\"currentColor\\" class=\\"material-design-icon__svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\"><path d=\\"M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M16.2,16.2L11,13V7H12.5V12.2L17,14.9L16.2,16.2Z\\"><!--v-if--></path></svg></span><span dir=\\"auto\\" class=\\"\\" data-v-3b2c1ec0=\\"\\">Dec 2, 2020</span></p>
<!--v-if-->
<!--v-if-->
</div>
</a>
</a>"
`;
exports[`PostListItem renders post list item with publisher name 1`] = `
<a href="/p/my-blog-post-some-uuid" class="post-minimalist-card-wrapper" dir="auto">
<!---->
<div class="title-info-wrapper has-text-grey-dark px-1">
<h3 lang="en" class="post-minimalist-title">
My Blog Post
</h3>
<p class="post-publication-date"><span class="icon"><i class="mdi mdi-clock mdi-24px"></i></span> <span dir="auto" class="has-text-grey-dark">Dec 2, 2020</span></p>
<!---->
<p class="post-publisher has-text-grey-dark"><span class="icon"><i class="mdi mdi-account-edit mdi-24px"></i></span> <span>Published by <b class="has-text-weight-medium">An author</b></span></p>
exports[`PostListItem > renders post list item with publisher name 1`] = `
"<a href=\\"/p/my-blog-post-some-uuid\\" class=\\"block md:flex bg-white dark:bg-violet-2 dark:text-white dark:hover:text-white rounded-lg shadow-md\\" dir=\\"auto\\" data-v-3b2c1ec0=\\"\\">
<!--v-if-->
<div class=\\"flex flex-col gap-1 bg-inherit p-2 rounded-lg flex-1\\" data-v-3b2c1ec0=\\"\\">
<h3 class=\\"text-xl color-violet-3 line-clamp-3 mb-2 font-bold\\" lang=\\"en\\" data-v-3b2c1ec0=\\"\\">My Blog Post</h3>
<p class=\\"flex gap-2\\" data-v-3b2c1ec0=\\"\\"><span aria-hidden=\\"true\\" class=\\"material-design-icon clock-icon\\" role=\\"img\\" data-v-3b2c1ec0=\\"\\"><svg fill=\\"currentColor\\" class=\\"material-design-icon__svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\"><path d=\\"M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M16.2,16.2L11,13V7H12.5V12.2L17,14.9L16.2,16.2Z\\"><!--v-if--></path></svg></span><span dir=\\"auto\\" class=\\"\\" data-v-3b2c1ec0=\\"\\">Dec 2, 2020</span></p>
<!--v-if-->
<p class=\\"flex gap-1\\" data-v-3b2c1ec0=\\"\\"><span aria-hidden=\\"true\\" class=\\"material-design-icon account-edit-icon\\" role=\\"img\\" data-v-3b2c1ec0=\\"\\"><svg fill=\\"currentColor\\" class=\\"material-design-icon__svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\"><path d=\\"M21.7,13.35L20.7,14.35L18.65,12.3L19.65,11.3C19.86,11.09 20.21,11.09 20.42,11.3L21.7,12.58C21.91,12.79 21.91,13.14 21.7,13.35M12,18.94L18.06,12.88L20.11,14.93L14.06,21H12V18.94M12,14C7.58,14 4,15.79 4,18V20H10V18.11L14,14.11C13.34,14.03 12.67,14 12,14M12,4A4,4 0 0,0 8,8A4,4 0 0,0 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4Z\\"><!--v-if--></path></svg></span>Published by <b class=\\"\\" data-v-3b2c1ec0=\\"\\">An author</b></p>
</div>
</a>
</a>"
`;
exports[`PostListItem renders post list item with tags 1`] = `
<a href="/p/my-blog-post-some-uuid" class="post-minimalist-card-wrapper" dir="auto">
<!---->
<div class="title-info-wrapper has-text-grey-dark px-1">
<h3 lang="en" class="post-minimalist-title">
My Blog Post
</h3>
<p class="post-publication-date"><span class="icon"><i class="mdi mdi-clock mdi-24px"></i></span> <span dir="auto" class="has-text-grey-dark">Dec 2, 2020</span></p>
<div class="tags" style="display: inline;"><span class="icon"><i class="mdi mdi-tag mdi-24px"></i></span> <span class="tag"><!----><span class="">A tag</span>
<!----></span>
</div>
<!---->
exports[`PostListItem > renders post list item with tags 1`] = `
"<a href=\\"/p/my-blog-post-some-uuid\\" class=\\"block md:flex bg-white dark:bg-violet-2 dark:text-white dark:hover:text-white rounded-lg shadow-md\\" dir=\\"auto\\" data-v-3b2c1ec0=\\"\\">
<!--v-if-->
<div class=\\"flex flex-col gap-1 bg-inherit p-2 rounded-lg flex-1\\" data-v-3b2c1ec0=\\"\\">
<h3 class=\\"text-xl color-violet-3 line-clamp-3 mb-2 font-bold\\" lang=\\"en\\" data-v-3b2c1ec0=\\"\\">My Blog Post</h3>
<p class=\\"flex gap-2\\" data-v-3b2c1ec0=\\"\\"><span aria-hidden=\\"true\\" class=\\"material-design-icon clock-icon\\" role=\\"img\\" data-v-3b2c1ec0=\\"\\"><svg fill=\\"currentColor\\" class=\\"material-design-icon__svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\"><path d=\\"M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M16.2,16.2L11,13V7H12.5V12.2L17,14.9L16.2,16.2Z\\"><!--v-if--></path></svg></span><span dir=\\"auto\\" class=\\"\\" data-v-3b2c1ec0=\\"\\">Dec 2, 2020</span></p>
<div class=\\"flex flex-wrap gap-y-0 gap-x-2\\" data-v-3b2c1ec0=\\"\\"><span aria-hidden=\\"true\\" class=\\"material-design-icon tag-icon\\" role=\\"img\\" data-v-3b2c1ec0=\\"\\"><svg fill=\\"currentColor\\" class=\\"material-design-icon__svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\"><path d=\\"M5.5,7A1.5,1.5 0 0,1 4,5.5A1.5,1.5 0 0,1 5.5,4A1.5,1.5 0 0,1 7,5.5A1.5,1.5 0 0,1 5.5,7M21.41,11.58L12.41,2.58C12.05,2.22 11.55,2 11,2H4C2.89,2 2,2.89 2,4V11C2,11.55 2.22,12.05 2.59,12.41L11.58,21.41C11.95,21.77 12.45,22 13,22C13.55,22 14.05,21.77 14.41,21.41L21.41,14.41C21.78,14.05 22,13.55 22,13C22,12.44 21.77,11.94 21.41,11.58Z\\"><!--v-if--></path></svg></span><span class=\\"rounded-md my-1 truncate text-sm text-violet-title capitalize px-2 py-1 bg-purple-3 dark:text-violet-3\\" data-v-bb7ceecc=\\"\\" data-v-3b2c1ec0=\\"\\">A tag</span></div>
<!--v-if-->
</div>
</a>
</a>"
`;

View File

@@ -1,18 +1,16 @@
import { config, createLocalVue, mount } from "@vue/test-utils";
import { config, mount } from "@vue/test-utils";
import ReportCard from "@/components/Report/ReportCard.vue";
import Buefy from "buefy";
import { ActorType } from "@/types/enums";
const localVue = createLocalVue();
localVue.use(Buefy);
config.mocks.$t = (key: string): string => key;
import { describe, expect, it } from "vitest";
import { createI18n } from "vue-i18n";
import en from "@/i18n/en_US.json";
const reportData = {
id: "1",
content: "My content",
insertedAt: "2020-12-02T09:01:20.873Z",
reporter: {
preferredUsername: "author",
preferredUsername: "John Snow",
domain: null,
name: "Reporter of Things",
type: ActorType.PERSON,
@@ -26,7 +24,6 @@ const reportData = {
const generateWrapper = (customReportData: Record<string, unknown> = {}) => {
return mount(ReportCard, {
localVue,
propsData: {
report: { ...reportData, ...customReportData },
},
@@ -37,16 +34,16 @@ describe("ReportCard", () => {
it("renders report card with basic informations", () => {
const wrapper = generateWrapper();
expect(wrapper.find(".media-content .title").text()).toBe(
expect(wrapper.find(".flex.gap-1 div p:first-child").text()).toBe(
reportData.reported.name
);
expect(wrapper.find(".media-content .subtitle").text()).toBe(
expect(wrapper.find(".flex.gap-1 div p:nth-child(2)").text()).toBe(
`@${reportData.reported.preferredUsername}`
);
expect(wrapper.find(".is-one-quarter-desktop span").text()).toBe(
`Reported by {reporter}`
expect(wrapper.find(".reported_by div:first-child").text()).toBe(
`Reported by John Snow`
);
});
@@ -55,8 +52,8 @@ describe("ReportCard", () => {
reporter: { domain: "somewhere.else", type: ActorType.APPLICATION },
});
expect(wrapper.find(".is-one-quarter-desktop span").text()).toBe(
"Reported by someone on {domain}"
expect(wrapper.find(".reported_by div:first-child").text()).toBe(
"Reported by someone on somewhere.else"
);
});
});

View File

@@ -1,19 +1,18 @@
import { config, createLocalVue, mount } from "@vue/test-utils";
import { config, mount } from "@vue/test-utils";
import ReportModal from "@/components/Report/ReportModal.vue";
import Buefy from "buefy";
import { vi, beforeEach, describe, it, expect } from "vitest";
const localVue = createLocalVue();
localVue.use(Buefy);
config.mocks.$t = (key: string): string => key;
import Oruga from "@oruga-ui/oruga-next";
config.global.plugins.push(Oruga);
const propsData = {
onConfirm: jest.fn(),
onConfirm: vi.fn(),
};
const generateWrapper = (customPropsData: Record<string, unknown> = {}) => {
return mount(ReportModal, {
localVue,
propsData: {
props: {
...propsData,
...customPropsData,
},
@@ -21,24 +20,22 @@ const generateWrapper = (customPropsData: Record<string, unknown> = {}) => {
};
beforeEach(() => {
jest.resetAllMocks();
vi.resetAllMocks();
});
describe("ReportModal", () => {
it("renders report modal with basic informations and submits it", async () => {
const wrapper = generateWrapper();
expect(wrapper.find(".modal-card-head").exists()).toBe(false);
expect(wrapper.find("header").exists()).toBe(false);
expect(wrapper.find(".media-content").text()).not.toContain(
expect(wrapper.find("section").text()).not.toContain(
"The content came from another server. Transfer an anonymous copy of the report?"
);
expect(
wrapper.find("footer.modal-card-foot button:first-child").text()
).toBe("Cancel");
expect(wrapper.find("footer button:first-child").text()).toBe("Cancel");
const submit = wrapper.find("footer.modal-card-foot button.is-primary");
const submit = wrapper.find("footer button.o-btn--primary");
expect(submit.text()).toBe("Send the report");
@@ -47,7 +44,7 @@ describe("ReportModal", () => {
submit.trigger("click");
await localVue.nextTick();
await wrapper.vm.$nextTick();
expect(wrapper.emitted().close).toBeTruthy();
expect(wrapper.vm.$props.onConfirm).toHaveBeenCalledTimes(1);
@@ -55,6 +52,7 @@ describe("ReportModal", () => {
"some comment with my report",
false
);
expect(wrapper.html()).toMatchSnapshot();
});
it("renders report modal and shows an inline comment if it's provided", async () => {
@@ -78,13 +76,13 @@ describe("ReportModal", () => {
it("renders report modal with with a remote content", async () => {
const wrapper = generateWrapper({ outsideDomain: "somewhere.else" });
expect(wrapper.find(".media-content").text()).toContain(
expect(wrapper.find(".control p").text()).toContain(
"The content came from another server. Transfer an anonymous copy of the report?"
);
const submit = wrapper.find("footer.modal-card-foot button.is-primary");
const submit = wrapper.find("footer button.o-btn--primary");
submit.trigger("click");
await localVue.nextTick();
await wrapper.vm.$nextTick();
expect(wrapper.vm.$props.onConfirm).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$props.onConfirm).toHaveBeenCalledWith("", false);
@@ -93,16 +91,16 @@ describe("ReportModal", () => {
it("renders report modal with with a remote content and accept to forward", async () => {
const wrapper = generateWrapper({ outsideDomain: "somewhere.else" });
expect(wrapper.find(".media-content").text()).toContain(
expect(wrapper.find(".control p").text()).toContain(
"The content came from another server. Transfer an anonymous copy of the report?"
);
const switchButton = wrapper.find('input[type="checkbox"]');
switchButton.setChecked();
switchButton.setValue(true);
const submit = wrapper.find("footer.modal-card-foot button.is-primary");
const submit = wrapper.find("footer button.o-btn--primary");
submit.trigger("click");
await localVue.nextTick();
await wrapper.vm.$nextTick();
expect(wrapper.vm.$props.onConfirm).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$props.onConfirm).toHaveBeenCalledWith("", true);
@@ -115,16 +113,10 @@ describe("ReportModal", () => {
confirmText: "report!",
});
expect(wrapper.find(".modal-card-head .modal-card-title").text()).toBe(
"want to report something?"
);
expect(wrapper.find("header h2").text()).toBe("want to report something?");
expect(
wrapper.find("footer.modal-card-foot button:first-child").text()
).toBe("nah");
expect(wrapper.find("footer button:first-child").text()).toBe("nah");
expect(
wrapper.find("footer.modal-card-foot button.is-primary").text()
).toBe("report!");
expect(wrapper.find("footer button.o-btn--primary").text()).toBe("report!");
});
});

View File

@@ -0,0 +1,29 @@
// Vitest Snapshot v1
exports[`ReportModal > renders report modal with basic informations and submits it 1`] = `
"<div class=\\"p-2\\" data-v-8c6db6e4=\\"\\">
<!--v-if-->
<section data-v-8c6db6e4=\\"\\">
<div class=\\"flex gap-1 flex-row mb-3\\" data-v-8c6db6e4=\\"\\"><span class=\\"o-icon o-icon--warning hidden md:block flex-1\\" data-v-8c6db6e4=\\"\\"><i class=\\"mdi mdi-alert 48\\"></i></span>
<p data-v-8c6db6e4=\\"\\">The report will be sent to the moderators of your instance. You can explain why you report this content below.</p>
</div>
<div class=\\"\\" data-v-8c6db6e4=\\"\\">
<!--v-if-->
<div class=\\"o-field o-field--filled\\" data-v-8c6db6e4=\\"\\"><label for=\\"additonal-comments\\" class=\\"o-field__label\\">Additional comments</label>
<div class=\\"o-ctrl-input\\" data-v-8c6db6e4=\\"\\"><textarea id=\\"additonal-comments\\" class=\\"o-input o-input__textarea\\"></textarea>
<!--v-if-->
<!--v-if-->
<!--v-if-->
</div>
<!--v-if-->
</div>
<!--v-if-->
</div>
</section>
<footer class=\\"flex gap-2 py-3\\" data-v-8c6db6e4=\\"\\"><button type=\\"button\\" class=\\"o-btn\\" data-v-8c6db6e4=\\"\\"><span class=\\"o-btn__wrapper\\"><!--v-if--><span class=\\"o-btn__label\\">Cancel</span>
<!--v-if--></span>
</button><button type=\\"button\\" class=\\"o-btn o-btn--primary\\" data-v-8c6db6e4=\\"\\"><span class=\\"o-btn__wrapper\\"><!--v-if--><span class=\\"o-btn__label\\">Send the report</span>
<!--v-if--></span>
</button></footer>
</div>"
`;

View File

@@ -1,17 +1,23 @@
import { config, createLocalVue, mount } from "@vue/test-utils";
const useRouterMock = vi.fn(() => ({
push: () => {},
}));
import { config, 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-option";
import { resetPasswordResponseMock } from "../../mocks/auth";
import RouteName from "@/router/name";
import flushPromises from "flush-promises";
import { describe, expect, it, vi } from "vitest";
import { DefaultApolloClient } from "@vue/apollo-composable";
import Oruga from "@oruga-ui/oruga-next";
const localVue = createLocalVue();
localVue.use(Buefy);
config.mocks.$t = (key: string): string => key;
const $router = { push: jest.fn() };
config.global.plugins.push(Oruga);
vi.mock("vue-router/dist/vue-router.mjs", () => ({
useRouter: useRouterMock,
}));
let requestHandlers: Record<string, RequestHandler>;
@@ -22,7 +28,7 @@ const generateWrapper = (
const mockClient = createMockClient();
requestHandlers = {
resetPasswordMutationHandler: jest
resetPasswordMutationHandler: vi
.fn()
.mockResolvedValue(resetPasswordResponseMock),
...customRequestHandlers,
@@ -33,21 +39,19 @@ const generateWrapper = (
requestHandlers.resetPasswordMutationHandler
);
const apolloProvider = new VueApollo({
defaultClient: mockClient,
});
return mount(PasswordReset, {
localVue,
mocks: {
$route: { query: {} },
$router,
...customMocks,
},
apolloProvider,
propsData: {
props: {
token: "some-token",
},
global: {
stubs: ["router-link", "router-view"],
mocks: {
...customMocks,
},
provide: {
[DefaultApolloClient]: mockClient,
},
},
});
};
@@ -60,12 +64,14 @@ describe("Reset page", () => {
it("shows error if token is invalid", async () => {
const wrapper = generateWrapper({
resetPasswordMutationHandler: jest.fn().mockResolvedValue({
resetPasswordMutationHandler: vi.fn().mockResolvedValue({
errors: [{ message: "The token you provided is invalid." }],
}),
});
wrapper.findAll('input[type="password"').setValue("my password");
wrapper
.findAll('input[type="password"')
.forEach((inputField) => inputField.setValue("my password"));
wrapper.find("form").trigger("submit");
await wrapper.vm.$nextTick();
@@ -78,27 +84,30 @@ describe("Reset page", () => {
await flushPromises();
expect(wrapper.find("article.message.is-danger").text()).toContain(
expect(wrapper.find(".o-notification--danger").text()).toContain(
"The token you provided is invalid"
);
expect(wrapper.html()).toMatchSnapshot();
});
it("redirects to homepage if token is valid", async () => {
const push = vi.fn(); // needs to write this code before render()
useRouterMock.mockImplementationOnce(() => ({
push,
}));
const wrapper = generateWrapper();
wrapper.findAll('input[type="password"').setValue("my password");
wrapper.find("form").trigger("submit");
await wrapper.vm.$nextTick();
wrapper
.findAll('input[type="password"')
.forEach((inputField) => inputField.setValue("my password"));
await wrapper.find("form").trigger("submit");
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 });
expect(push).toHaveBeenCalledWith({ name: RouteName.HOME });
});
});

View File

@@ -1,75 +1,55 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1
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>
exports[`Reset page > renders correctly 1`] = `
"<section class=\\"container mx-auto\\">
<h1 class=\\"\\">Password reset</h1>
<form>
<div class=\\"o-field\\"><label class=\\"o-field__label\\">Password</label>
<div class=\\"o-ctrl-input\\"><input aria-required=\\"true\\" required=\\"\\" minlength=\\"6\\" class=\\"o-input o-input-iconspace-right\\" type=\\"password\\" autocomplete=\\"off\\">
<!--v-if--><span class=\\"o-icon o-icon--clickable o-input__icon-right\\"><i class=\\"mdi mdi-eye mdi-24px\\"></i></span>
<!--v-if-->
</div>
<!--v-if-->
</div>
</div>
</section>
<div class=\\"o-field\\"><label class=\\"o-field__label\\">Password (confirmation)</label>
<div class=\\"o-ctrl-input\\"><input aria-required=\\"true\\" required=\\"\\" minlength=\\"6\\" class=\\"o-input o-input-iconspace-right\\" type=\\"password\\" autocomplete=\\"off\\">
<!--v-if--><span class=\\"o-icon o-icon--clickable o-input__icon-right\\"><i class=\\"mdi mdi-eye mdi-24px\\"></i></span>
<!--v-if-->
</div>
<!--v-if-->
</div><button class=\\"button is-primary\\">Reset my password</button>
</form>
</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>
exports[`Reset page > shows error if token is invalid 1`] = `
"<section class=\\"container mx-auto\\">
<h1 class=\\"\\">Password reset</h1>
<transition-stub title=\\"Error\\">
<article class=\\"o-notification o-notification--danger\\">
<!--v-if-->
<!--v-if-->
<div class=\\"o-notification__wrapper\\">
<!--v-if-->
<div class=\\"o-notification__content\\">The token you provided is invalid.</div>
</div>
</article>
</transition-stub>
<form>
<div class=\\"o-field o-field--filled\\"><label class=\\"o-field__label\\">Password</label>
<div class=\\"o-ctrl-input\\"><input aria-required=\\"true\\" required=\\"\\" minlength=\\"6\\" class=\\"o-input o-input-iconspace-right\\" type=\\"password\\" autocomplete=\\"off\\">
<!--v-if--><span class=\\"o-icon o-icon--clickable o-input__icon-right\\"><i class=\\"mdi mdi-eye mdi-24px\\"></i></span>
<!--v-if-->
</div>
<!--v-if-->
</div>
</div>
</section>
<div class=\\"o-field o-field--filled\\"><label class=\\"o-field__label\\">Password (confirmation)</label>
<div class=\\"o-ctrl-input\\"><input aria-required=\\"true\\" required=\\"\\" minlength=\\"6\\" class=\\"o-input o-input-iconspace-right\\" type=\\"password\\" autocomplete=\\"off\\">
<!--v-if--><span class=\\"o-icon o-icon--clickable o-input__icon-right\\"><i class=\\"mdi mdi-eye mdi-24px\\"></i></span>
<!--v-if-->
</div>
<!--v-if-->
</div><button class=\\"button is-primary\\">Reset my password</button>
</form>
</section>"
`;

View File

@@ -1,86 +1,83 @@
import { config, createLocalVue, mount, Wrapper } from "@vue/test-utils";
import Login from "@/views/User/Login.vue";
import Buefy from "buefy";
const useRouterMock = vi.fn(() => ({
push: () => {},
replace: () => {},
}));
const useRouteMock = vi.fn(() => {});
import { config, mount, VueWrapper } from "@vue/test-utils";
import Login from "@/views/User/LoginView.vue";
import {
createMockClient,
MockApolloClient,
RequestHandler,
} from "mock-apollo-client";
import VueApollo from "@vue/apollo-option";
import buildCurrentUserResolver from "@/apollo/user";
import { configMock } from "../../mocks/config";
import { i18n } from "@/utils/i18n";
import { CONFIG } from "@/graphql/config";
import { loginMock as loginConfigMock } from "../../mocks/config";
import { LOGIN_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";
import { InMemoryCache } from "@apollo/client/cache";
import { afterEach, describe, expect, it, vi } from "vitest";
import { DefaultApolloClient } from "@vue/apollo-composable";
import Oruga from "@oruga-ui/oruga-next";
import { cache } from "@/apollo/memory";
const localVue = createLocalVue();
localVue.use(Buefy);
config.mocks.$t = (key: string): string => key;
const $router = { push: jest.fn(), replace: jest.fn() };
vi.mock("vue-router/dist/vue-router.mjs", () => ({
useRouter: useRouterMock,
useRoute: useRouteMock,
}));
config.global.plugins.push(Oruga);
describe("Render login form", () => {
let wrapper: Wrapper<Vue>;
let wrapper: VueWrapper;
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: false });
mockClient = createMockClient({
cache,
resolvers: buildCurrentUserResolver(cache),
});
requestHandlers = {
configQueryHandler: jest.fn().mockResolvedValue(configMock),
loginMutationHandler: jest.fn().mockResolvedValue(loginResponseMock),
configQueryHandler: vi.fn().mockResolvedValue(loginConfigMock),
loginMutationHandler: vi.fn().mockResolvedValue(loginResponseMock),
...handlers,
};
mockClient.setRequestHandler(CONFIG, requestHandlers.configQueryHandler);
mockClient.setRequestHandler(
LOGIN_CONFIG,
requestHandlers.configQueryHandler
);
mockClient.setRequestHandler(LOGIN, requestHandlers.loginMutationHandler);
apolloProvider = new VueApollo({
defaultClient: mockClient,
});
wrapper = mount(Login, {
localVue,
i18n,
apolloProvider,
propsData: {
props: {
...customProps,
},
mocks: {
$route: { query: {} },
$router,
...customMocks,
},
stubs: ["router-link", "router-view"],
data() {
return {
...baseData,
};
global: {
provide: {
[DefaultApolloClient]: mockClient,
},
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper?.unmount();
cache.reset();
mockClient = null;
apolloProvider = null;
$router.push.mockReset();
});
it("requires email and password to be filled", async () => {
@@ -90,10 +87,9 @@ describe("Render login form", () => {
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");
wrapper.find('form button[type="submit"]').trigger("click");
const form = wrapper.find("form");
expect(form.exists()).toBe(true);
const formElement = form.element as HTMLFormElement;
@@ -101,13 +97,18 @@ describe("Render login form", () => {
});
it("renders and submits the login form", async () => {
const replace = vi.fn(); // needs to write this code before render()
const push = vi.fn();
useRouterMock.mockImplementationOnce(() => ({
replace,
push,
}));
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");
@@ -125,14 +126,19 @@ describe("Render login form", () => {
await flushPromises();
expect(currentUser?.email).toBe("some@email.tld");
expect(currentUser?.id).toBe("1");
expect(jest.isMockFunction(wrapper.vm.$router.replace)).toBe(true);
await flushPromises();
expect($router.replace).toHaveBeenCalledWith({ name: RouteName.HOME });
expect(replace).toHaveBeenCalledWith({ name: RouteName.HOME });
});
it("handles a login error", async () => {
const replace = vi.fn(); // needs to write this code before render()
const push = vi.fn();
useRouterMock.mockImplementationOnce(() => ({
push,
replace,
}));
generateWrapper({
loginMutationHandler: jest.fn().mockResolvedValue({
loginMutationHandler: vi.fn().mockResolvedValue({
errors: [
{
message:
@@ -147,7 +153,6 @@ describe("Render login form", () => {
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");
@@ -156,21 +161,23 @@ describe("Render login form", () => {
...loginMock,
});
await flushPromises();
expect(wrapper.find("article.message.is-danger").text()).toContain(
expect(wrapper.find(".o-notification--danger").text()).toContain(
"Impossible to authenticate, either your email or password are invalid."
);
expect($router.push).not.toHaveBeenCalled();
expect(push).not.toHaveBeenCalled();
});
it("handles redirection after login", async () => {
generateWrapper(
{},
{},
{},
{
$route: { query: { redirect: "/about/instance" } },
}
);
const replace = vi.fn(); // needs to write this code before render()
const push = vi.fn();
useRouterMock.mockImplementationOnce(() => ({
replace,
push,
}));
useRouteMock.mockImplementationOnce(() => ({
query: { redirect: "/about/instance" },
}));
generateWrapper();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
@@ -179,6 +186,6 @@ describe("Render login form", () => {
wrapper.find('form input[type="password"]').setValue("somepassword");
wrapper.find("form").trigger("submit");
await flushPromises();
expect($router.push).toHaveBeenCalledWith("/about/instance");
expect(push).toHaveBeenCalledWith("/about/instance");
});
});

View File

@@ -1,3 +1,194 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1
exports[`App component renders a Vue component 1`] = `<b-navbar-stub type="is-secondary" wrapperclass="container" closeonclick="true" mobileburger="true" id="navbar"><template></template> <template></template> <template></template></b-navbar-stub>`;
exports[`App component > renders a Vue component 1`] = `
"<nav class=\\"bg-white border-gray-200 px-2 sm:px-4 py-2.5 dark:bg-gray-900\\" data-v-4295d220=\\"\\">
<div class=\\"container mx-auto flex flex-wrap justify-between items-center mx-auto\\" data-v-4295d220=\\"\\">
<router-link to=\\"[object Object]\\" class=\\"flex items-center\\" data-v-4295d220=\\"\\">
<logo-stub invert=\\"false\\" class=\\"w-40\\" data-v-4295d220=\\"\\"></logo-stub>
</router-link>
<!--v-if--><button type=\\"button\\" class=\\"inline-flex items-center p-2 ml-1 text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600\\" aria-controls=\\"mobile-menu-2\\" aria-expanded=\\"false\\" data-v-4295d220=\\"\\"><span class=\\"sr-only\\" data-v-4295d220=\\"\\">Open main menu</span><svg class=\\"w-6 h-6\\" aria-hidden=\\"true\\" fill=\\"currentColor\\" viewBox=\\"0 0 20 20\\" xmlns=\\"http://www.w3.org/2000/svg\\" data-v-4295d220=\\"\\">
<path fill-rule=\\"evenodd\\" d=\\"M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z\\" clip-rule=\\"evenodd\\" data-v-4295d220=\\"\\"></path>
</svg></button>
<div class=\\"justify-between items-center w-full md:flex md:w-auto md:order-1 hidden\\" id=\\"mobile-menu-2\\" data-v-4295d220=\\"\\">
<ul class=\\"flex flex-col md:flex-row md:space-x-8 mt-2 md:mt-0 md:text-sm md:font-medium\\" data-v-4295d220=\\"\\">
<li data-v-4295d220=\\"\\">
<router-link to=\\"[object Object]\\" class=\\"block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700\\" data-v-4295d220=\\"\\">Login</router-link>
</li>
<li data-v-4295d220=\\"\\">
<router-link to=\\"[object Object]\\" class=\\"block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700\\" data-v-4295d220=\\"\\">Register</router-link>
</li>
</ul>
</div>
</div>
</nav>
<!-- <o-navbar
id=\\"navbar\\"
type=\\"is-secondary\\"
wrapper-class=\\"container mx-auto\\"
v-model:active=\\"mobileNavbarActive\\"
>
<template #brand>
<o-navbar-item
tag=\\"router-link\\"
:to=\\"{ name: RouteName.HOME }\\"
:aria-label=\\"$t('Home')\\"
>
<logo />
</o-navbar-item>
</template>
<template #start>
<o-navbar-item tag=\\"router-link\\" :to=\\"{ name: RouteName.SEARCH }\\">{{
$t(\\"Explore\\")
}}</o-navbar-item>
<o-navbar-item
v-if=\\"currentActor.id && currentUser?.isLoggedIn\\"
tag=\\"router-link\\"
:to=\\"{ name: RouteName.MY_EVENTS }\\"
>{{ $t(\\"My events\\") }}</o-navbar-item
>
<o-navbar-item
tag=\\"router-link\\"
:to=\\"{ name: RouteName.MY_GROUPS }\\"
v-if=\\"
config &&
config.features.groups &&
currentActor.id &&
currentUser?.isLoggedIn
\\"
>{{ $t(\\"My groups\\") }}</o-navbar-item
>
<o-navbar-item
tag=\\"span\\"
v-if=\\"
config &&
config.features.eventCreation &&
currentActor.id &&
currentUser?.isLoggedIn
\\"
>
<o-button
v-if=\\"!hideCreateEventsButton\\"
tag=\\"router-link\\"
:to=\\"{ name: RouteName.CREATE_EVENT }\\"
variant=\\"primary\\"
>{{ $t(\\"Create\\") }}</o-button
>
</o-navbar-item>
</template>
<template #end>
<o-navbar-item tag=\\"div\\">
<search-field @navbar-search=\\"mobileNavbarActive = false\\" />
</o-navbar-item>
<o-navbar-dropdown
v-if=\\"currentActor.id && currentUser?.isLoggedIn\\"
right
collapsible
ref=\\"user-dropdown\\"
tabindex=\\"0\\"
tag=\\"span\\"
@keyup.enter=\\"toggleMenu\\"
>
<template #label v-if=\\"currentActor\\">
<div class=\\"identity-wrapper\\">
<div>
<figure class=\\"image is-32x32\\" v-if=\\"currentActor.avatar\\">
<img
class=\\"is-rounded\\"
alt=\\"avatarUrl\\"
:src=\\"currentActor.avatar.url\\"
/>
</figure>
<o-icon v-else icon=\\"account-circle\\" />
</div>
<div class=\\"media-content is-hidden-desktop\\">
<span>{{ displayName(currentActor) }}</span>
<span class=\\"has-text-grey-dark\\" v-if=\\"currentActor.name\\"
>@{{ currentActor.preferredUsername }}</span
>
</div>
</div>
</template>
No identities dropdown if no identities
<span v-if=\\"identities.length <= 1\\"></span>
<o-navbar-item
tag=\\"span\\"
v-for=\\"identity in identities\\"
v-else
:active=\\"identity.id === currentActor.id\\"
:key=\\"identity.id\\"
tabindex=\\"0\\"
@click=\\"setIdentity({
preferredUsername: identity.preferredUsername,
})\\"
@keyup.enter=\\"setIdentity({
preferredUsername: identity.preferredUsername,
})\\"
>
<span>
<div class=\\"media-left\\">
<figure class=\\"image is-32x32\\" v-if=\\"identity.avatar\\">
<img
class=\\"is-rounded\\"
loading=\\"lazy\\"
:src=\\"identity.avatar.url\\"
alt
/>
</figure>
<o-icon v-else size=\\"is-medium\\" icon=\\"account-circle\\" />
</div>
<div class=\\"media-content\\">
<span>{{ displayName(identity) }}</span>
<span class=\\"has-text-grey-dark\\" v-if=\\"identity.name\\"
>@{{ identity.preferredUsername }}</span
>
</div>
</span>
<hr class=\\"navbar-divider\\" role=\\"presentation\\" />
</o-navbar-item>
<o-navbar-item
tag=\\"router-link\\"
:to=\\"{ name: RouteName.UPDATE_IDENTITY }\\"
>{{ $t(\\"My account\\") }}</o-navbar-item
>
<o-navbar-item
v-if=\\"currentUser.role === ICurrentUserRole.ADMINISTRATOR\\"
tag=\\"router-link\\"
:to=\\"{ name: RouteName.ADMIN_DASHBOARD }\\"
>{{ $t(\\"Administration\\") }}</o-navbar-item
>
<o-navbar-item
tag=\\"span\\"
tabindex=\\"0\\"
@click=\\"logout\\"
@keyup.enter=\\"logout\\"
>
<span>{{ $t(\\"Log out\\") }}</span>
</o-navbar-item>
</o-navbar-dropdown>
<o-navbar-item v-else tag=\\"div\\">
<div class=\\"buttons\\">
<router-link
class=\\"button is-primary\\"
v-if=\\"config && config.registrationsOpen\\"
:to=\\"{ name: RouteName.REGISTER }\\"
>
<strong>{{ $t(\\"Sign up\\") }}</strong>
</router-link>
<router-link
class=\\"button is-light\\"
:to=\\"{ name: RouteName.LOGIN }\\"
>{{ $t(\\"Log in\\") }}</router-link
>
</div>
</o-navbar-item>
</template>
</o-navbar> -->"
`;

View File

@@ -1,30 +1,29 @@
import { shallowMount, createLocalVue, Wrapper, config } from "@vue/test-utils";
const useRouterMock = vi.fn(() => ({
push: () => {},
}));
import { shallowMount, VueWrapper } from "@vue/test-utils";
import NavBar from "@/components/NavBar.vue";
import {
createMockClient,
MockApolloClient,
RequestHandler,
} from "mock-apollo-client";
import VueApollo from "@vue/apollo-option";
import { CONFIG } from "@/graphql/config";
import { USER_SETTINGS } from "@/graphql/user";
import buildCurrentUserResolver from "@/apollo/user";
import Buefy from "buefy";
import { configMock } from "../mocks/config";
import { InMemoryCache } from "@apollo/client/cache";
import { describe, it, vi, expect, afterEach } from "vitest";
import { DefaultApolloClient } from "@vue/apollo-composable";
const localVue = createLocalVue();
localVue.use(VueApollo);
localVue.use(Buefy);
config.mocks.$t = (key: string): string => key;
vi.mock("vue-router/dist/vue-router.mjs", () => ({
useRouter: useRouterMock,
}));
describe("App component", () => {
let wrapper: Wrapper<Vue>;
let wrapper: VueWrapper;
let mockClient: MockApolloClient | null;
let apolloProvider;
let requestHandlers: Record<string, RequestHandler>;
const createComponent = (handlers = {}, baseData = {}) => {
const createComponent = (handlers = {}) => {
const cache = new InMemoryCache({ addTypename: false });
mockClient = createMockClient({
@@ -32,50 +31,34 @@ describe("App component", () => {
resolvers: buildCurrentUserResolver(cache),
});
requestHandlers = {
configQueryHandler: jest.fn().mockResolvedValue(configMock),
loggedUserQueryHandler: jest.fn().mockResolvedValue(null),
...handlers,
};
mockClient.setRequestHandler(CONFIG, requestHandlers.configQueryHandler);
mockClient.setRequestHandler(
USER_SETTINGS,
requestHandlers.loggedUserQueryHandler
);
apolloProvider = new VueApollo({
defaultClient: mockClient,
});
requestHandlers = { ...handlers };
wrapper = shallowMount(NavBar, {
localVue,
apolloProvider,
stubs: ["router-link", "router-view"],
data() {
return {
...baseData,
};
// stubs: ["router-link", "router-view", "o-dropdown", "o-dropdown-item"],
global: {
provide: {
[DefaultApolloClient]: mockClient,
},
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper?.unmount();
mockClient = null;
apolloProvider = null;
});
it("renders a Vue component", async () => {
const push = vi.fn();
useRouterMock.mockImplementationOnce(() => ({
push,
}));
createComponent();
await wrapper.vm.$nextTick();
expect(wrapper.exists()).toBe(true);
expect(requestHandlers.configQueryHandler).toHaveBeenCalled();
expect(wrapper.vm.$apollo.queries.config).toBeTruthy();
expect(wrapper.html()).toMatchSnapshot();
expect(wrapper.findComponent({ name: "b-navbar" }).exists()).toBeTruthy();
// expect(wrapper.findComponent({ name: "b-navbar" }).exists()).toBeTruthy();
});
});

View File

@@ -1,5 +1,6 @@
import { mount } from "@vue/test-utils";
import Tag from "@/components/Tag.vue";
import { it, expect } from "vitest";
const tagContent = "My tag";
@@ -15,5 +16,5 @@ it("renders a Vue component", () => {
const wrapper = createComponent();
expect(wrapper.exists()).toBe(true);
expect(wrapper.find("span.tag span").text()).toEqual(tagContent);
expect(wrapper.find("span").text()).toEqual(tagContent);
});

View File

@@ -127,3 +127,28 @@ export const configMock = {
},
},
};
export const loginMock = {
data: {
config: {
__typename: "Config",
auth: {
__typename: "Auth",
oauthProviders: [],
},
registrationsOpen: true,
},
},
};
export const anonymousActorIdMock = {
data: {
config: {
__typename: "Config",
anonymous: {
__typename: "Anonymous",
actorId: "1",
},
},
},
};

View File

@@ -15,11 +15,7 @@ export const fetchEventBasicMock = {
__typename: "ParticipantStats",
notApproved: 0,
notConfirmed: 0,
rejected: 0,
participant: 0,
creator: 1,
moderator: 0,
administrator: 0,
going: 1,
},
},
@@ -61,7 +57,7 @@ export const joinEventMock = {
email: "some@email.tld",
message: "a message long enough",
locale: "en_US",
timezone: "UTC",
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
};
export const eventNoCommentThreadsMock = {
@@ -152,8 +148,9 @@ export const eventCommentThreadsMock = {
export const newCommentForEventMock = {
eventId: "1",
text: "my new comment",
inReplyToCommentId: null,
inReplyToCommentId: undefined,
isAnnouncement: false,
originCommentId: undefined,
};
export const newCommentForEventResponse: DataMock = {

View File

@@ -0,0 +1,26 @@
import { vi } from "vitest";
window.matchMedia = vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(), // deprecated
removeListener: vi.fn(), // deprecated
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
}));
// Object.defineProperty(window, "matchMedia", {
// writable: true,
// value: vi.fn().mockImplementation((query) => ({
// matches: false,
// media: query,
// onchange: null,
// addListener: vi.fn(), // deprecated
// removeListener: vi.fn(), // deprecated
// addEventListener: vi.fn(),
// removeEventListener: vi.fn(),
// dispatchEvent: vi.fn(),
// })),
// });