build: switch from yarn to npm to manage js dependencies and move js contents to root

yarn v1 is being deprecated and starts to have some issues

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2023-11-14 17:24:42 +01:00
parent 32055122c3
commit 2e72f6faf4
595 changed files with 12078 additions and 7843 deletions

172
tests/e2e/login.spec.ts Normal file
View File

@@ -0,0 +1,172 @@
import { test, expect } from "@playwright/test";
test("Login has everything we need", async ({ page }) => {
await page.goto("/login");
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Login/);
const forgotPasswordLink = page.locator("a", {
hasText: "Forgot your password?",
});
const reAskInstructionsLink = page.locator("a", {
hasText: "Didn't receive the instructions?",
});
const registerLink = page.locator("a > span > span", {
hasText: "Create an account",
});
await expect(forgotPasswordLink).toBeVisible();
await expect(reAskInstructionsLink).toBeVisible();
await expect(registerLink).toBeVisible();
await expect(page.locator("form .field label").first()).toHaveText("Email");
await expect(page.locator("form .field label").nth(1)).toHaveText("Password");
await forgotPasswordLink.click();
await page.waitForURL("/password-reset/send");
expect(page.url()).toContain("/password-reset/send");
await page.goBack();
await reAskInstructionsLink.click();
await page.waitForURL("/resend-instructions");
expect(page.url()).toContain("/resend-instructions");
await page.goBack();
await registerLink.click();
await page.waitForURL("/register/user?default_email=&default_password=");
expect(page.url()).toContain("/register/user");
await page.goBack();
});
test("Login rejects unknown users properly", async ({ page }) => {
await page.goto("/login");
await page.locator("#email").fill("hello@me.com");
await page.locator("#password").fill("some password");
const loginButton = page.locator("form button", { hasText: "Login" });
await expect(loginButton).toHaveAttribute("type", "submit");
await loginButton.click();
await expect(page.locator(".notification-danger")).toHaveText(
"User not found"
);
});
test("Tries to login with valid credentials", async ({ page, context }) => {
await page.goto("/login");
await page.locator("#email").fill("user@email.com");
await page.locator("#password").fill("some password");
const loginButton = page.locator("form button", { hasText: "Login" });
await expect(loginButton).toHaveAttribute("type", "submit");
await loginButton.click();
await page.waitForURL("/");
expect(new URL(page.url()).pathname).toBe("/");
const localStorage = (
await context.storageState()
).origins[0].localStorage.reduce((acc: Record<string, string>, elem) => {
acc[elem.name] = elem.value;
return acc;
}, {});
expect(localStorage["auth-user-role"]).toBe("USER");
expect(localStorage["auth-access-token"]).toBeDefined();
expect(localStorage["auth-refresh-token"]).toBeDefined();
expect(localStorage["auth-user-email"]).toBe("user@email.com");
// Changes each run
// expect(localStorage["auth-user-id"]).toBe("3");
// Doesn't work in Chrome for some reason
// expect(localStorage['auth-user-actor-id']).toBe('2');
});
test("Tries to login with valid credentials but unconfirmed account", async ({
page,
}) => {
await page.goto("/login");
await page.locator("#email").fill("unconfirmed@email.com");
await page.locator("#password").fill("some password");
await page.keyboard.press("Enter");
await expect(page.locator(".notification-danger")).toHaveText(
"User not found"
);
});
test("Tries to login with valid credentials, confirmed account but no profile", async ({
page,
}) => {
await page.goto("/login");
await page.locator("#email").fill("confirmed@email.com");
await page.locator("#password").fill("some password");
await page.keyboard.press("Enter");
await page.waitForURL("/register/profile/confirmed@email.com/true");
expect(page.url()).toContain("/register/profile/confirmed@email.com/true");
await expect(page.locator("p.prose").first()).toHaveText(
"Now, create your first profile:"
);
const displayNameField = page.locator("form > .field").first();
await expect(displayNameField.locator("label")).toHaveText(
"Displayed nickname"
);
const displayNameInput = displayNameField.locator("input");
await displayNameInput.fill("Duplicate");
const usernameField = page.locator("form > .field").nth(1);
await expect(usernameField.locator("label")).toHaveText("Username");
const usernameFieldInput = usernameField.locator("input");
await usernameFieldInput.fill("test_user");
const descriptionField = page.locator("form > .field").nth(2);
await expect(descriptionField.locator("label")).toHaveText("Short bio");
await descriptionField
.locator("textarea")
.fill("This shouln't work because it's using a dupublicated username");
const submitButton = page.locator('button[type="submit"]', {
hasText: "Create my profile",
});
await submitButton.click();
await expect(page.locator("p.field-message-danger")).toHaveText(
"This username is already taken."
);
await displayNameInput.fill("");
await displayNameInput.fill("Not");
await usernameFieldInput.fill("");
await usernameFieldInput.fill("test_user_2");
await submitButton.click();
// cy.get("form .field input").first(0).clear().type("test_user_2");
// cy.get("form .field input").eq(1).type("Not");
// cy.get("form .field textarea").clear().type("This will now work");
// cy.get("form").submit();
// cy.get(".navbar-link span.icon i").should(
// "have.class",
// "mdi-account-circle"
// );
await page.waitForURL("/");
expect(page.url()).toContain("/");
await expect(page.locator(".notification-info")).toHaveText(
"Welcome to Mobilizon, Not!"
);
await expect(
page.locator("button#user-menu-button span:not(.sr-only)")
).toHaveClass("material-design-icon account-circle-icon");
});

9
tests/unit/.eslintrc.js Normal file
View File

@@ -0,0 +1,9 @@
module.exports = {
env: {
mocha: true,
},
globals: {
expect: true,
sinon: true,
},
};

16
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

@@ -0,0 +1,20 @@
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",
avatar: null,
__typename: "CurrentActor",
}),
},
};

View File

@@ -0,0 +1,182 @@
import { config, shallowMount, VueWrapper } from "@vue/test-utils";
import CommentTree from "@/components/Comment/CommentTree.vue";
import {
createMockClient,
MockApolloClient,
RequestHandler,
} from "mock-apollo-client";
import {
COMMENTS_THREADS_WITH_REPLIES,
CREATE_COMMENT_FROM_EVENT,
} from "@/graphql/comment";
import { CommentModeration } from "@/types/enums";
import { IEvent } from "@/types/event.model";
import {
eventCommentThreadsMock,
eventNoCommentThreadsMock,
newCommentForEventMock,
newCommentForEventResponse,
} from "../../mocks/event";
import flushPromises from "flush-promises";
import { defaultResolvers } from "../../common";
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",
uuid: "e37910ea-fd5a-4756-7634-00971f3f4107",
options: {
commentModeration: CommentModeration.ALLOW_ALL,
},
};
describe("CommentTree", () => {
let wrapper: VueWrapper;
let mockClient: MockApolloClient | null;
let requestHandlers: Record<string, RequestHandler>;
const generateWrapper = (handlers = {}, extraProps = {}) => {
const cache = new InMemoryCache({ addTypename: true });
mockClient = createMockClient({
cache,
resolvers: defaultResolvers,
});
requestHandlers = {
eventCommentThreadsQueryHandler: vi
.fn()
.mockResolvedValue(eventCommentThreadsMock),
createCommentForEventMutationHandler: vi
.fn()
.mockResolvedValue(newCommentForEventResponse),
...handlers,
};
mockClient.setRequestHandler(
COMMENTS_THREADS_WITH_REPLIES,
requestHandlers.eventCommentThreadsQueryHandler
);
mockClient.setRequestHandler(
CREATE_COMMENT_FROM_EVENT,
requestHandlers.createCommentForEventMutationHandler
);
wrapper = shallowMount(CommentTree, {
props: {
event: { ...eventData },
...extraProps,
},
global: {
provide: {
[DefaultApolloClient]: mockClient,
},
plugins: [router],
},
});
};
beforeEach(async () => {
router = createRouter({
history: createWebHistory(),
routes: routes,
});
// await router.isReady();
});
afterEach(() => {
mockClient = null;
requestHandlers = {};
wrapper.unmount();
});
it("renders a loading comment tree", async () => {
generateWrapper();
expect(wrapper.find("p.text-center").text()).toBe("Loading comments…");
expect(wrapper.html()).toMatchSnapshot();
});
it("renders a comment tree with comments", async () => {
generateWrapper();
expect(wrapper.exists()).toBe(true);
expect(
requestHandlers.eventCommentThreadsQueryHandler
).toHaveBeenCalledWith({ eventUUID: eventData.uuid });
await flushPromises();
expect(wrapper.find("p.text-center").exists()).toBe(false);
expect(wrapper.findAllComponents("comment-stub").length).toBe(2);
expect(wrapper.html()).toMatchSnapshot();
});
it("renders the form if we can comment", async () => {
generateWrapper(
{},
{
newComment: {
text: newCommentForEventMock.text,
isAnnouncement: false,
},
}
);
await flushPromises();
expect(wrapper.find("form").isVisible()).toBe(true);
expect(wrapper.findAllComponents("comment-stub").length).toBe(2);
wrapper.getComponent({ ref: "commenteditor" });
wrapper.find("form").trigger("submit");
await flushPromises();
expect(
requestHandlers.createCommentForEventMutationHandler
).toHaveBeenCalledWith({
...newCommentForEventMock,
});
if (mockClient) {
const cachedData = mockClient.cache.readQuery<{ event: IEvent }>({
query: COMMENTS_THREADS_WITH_REPLIES,
variables: {
eventUUID: eventData.uuid,
},
});
if (cachedData) {
const { event } = cachedData;
expect(event.comments).toHaveLength(3);
}
}
});
it("renders an empty comment tree", async () => {
generateWrapper({
eventCommentThreadsQueryHandler: vi
.fn()
.mockResolvedValue(eventNoCommentThreadsMock),
});
await flushPromises();
expect(
requestHandlers.eventCommentThreadsQueryHandler
).toHaveBeenCalledWith({ eventUUID: eventData.uuid });
expect(wrapper.findComponent({ name: "EmptyContent" }).exists());
expect(wrapper.html()).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,67 @@
// Vitest Snapshot v1
exports[`CommentTree > renders a comment tree with comments 1`] = `
"<div data-v-5d0380ab=\\"\\">
<form class=\\"\\" data-v-5d0380ab=\\"\\">
<!--v-if-->
<article class=\\"flex flex-wrap items-start gap-2\\" data-v-5d0380ab=\\"\\">
<figure class=\\"\\" data-v-5d0380ab=\\"\\">
<identity-picker-wrapper-stub modelvalue=\\"[object Object]\\" inline=\\"false\\" masked=\\"false\\" data-v-5d0380ab=\\"\\"></identity-picker-wrapper-stub>
</figure>
<div class=\\"flex-1\\" data-v-5d0380ab=\\"\\">
<div class=\\"flex flex-col gap-2\\" data-v-5d0380ab=\\"\\">
<div class=\\"editor-wrapper\\" data-v-5d0380ab=\\"\\">
<editor-stub currentactor=\\"[object Object]\\" mode=\\"comment\\" modelvalue=\\"\\" aria-label=\\"Comment body\\" data-v-5d0380ab=\\"\\"></editor-stub>
<!--v-if-->
</div>
<!--v-if-->
</div>
</div>
<div class=\\"\\" data-v-5d0380ab=\\"\\">
<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-5d0380ab=\\"\\"></o-button-stub>
</div>
</article>
</form>
<transition-group-stub data-v-5d0380ab=\\"\\">
<transition-group-stub data-v-5d0380ab=\\"\\">
<comment-stub comment=\\"[object Object]\\" event=\\"[object Object]\\" currentactor=\\"[object Object]\\" rootcomment=\\"true\\" class=\\"root-comment\\" data-v-5d0380ab=\\"\\"></comment-stub>
<comment-stub comment=\\"[object Object]\\" event=\\"[object Object]\\" currentactor=\\"[object Object]\\" rootcomment=\\"true\\" class=\\"root-comment\\" data-v-5d0380ab=\\"\\"></comment-stub>
</transition-group-stub>
</transition-group-stub>
</div>"
`;
exports[`CommentTree > renders a loading comment tree 1`] = `
"<div data-v-5d0380ab=\\"\\">
<!--v-if-->
<p class=\\"text-center\\" data-v-5d0380ab=\\"\\">Loading comments…</p>
</div>"
`;
exports[`CommentTree > renders an empty comment tree 1`] = `
"<div data-v-5d0380ab=\\"\\">
<form class=\\"\\" data-v-5d0380ab=\\"\\">
<!--v-if-->
<article class=\\"flex flex-wrap items-start gap-2\\" data-v-5d0380ab=\\"\\">
<figure class=\\"\\" data-v-5d0380ab=\\"\\">
<identity-picker-wrapper-stub modelvalue=\\"[object Object]\\" inline=\\"false\\" masked=\\"false\\" data-v-5d0380ab=\\"\\"></identity-picker-wrapper-stub>
</figure>
<div class=\\"flex-1\\" data-v-5d0380ab=\\"\\">
<div class=\\"flex flex-col gap-2\\" data-v-5d0380ab=\\"\\">
<div class=\\"editor-wrapper\\" data-v-5d0380ab=\\"\\">
<editor-stub currentactor=\\"[object Object]\\" mode=\\"comment\\" modelvalue=\\"\\" aria-label=\\"Comment body\\" data-v-5d0380ab=\\"\\"></editor-stub>
<!--v-if-->
</div>
<!--v-if-->
</div>
</div>
<div class=\\"\\" data-v-5d0380ab=\\"\\">
<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-5d0380ab=\\"\\"></o-button-stub>
</div>
</article>
</form>
<transition-group-stub data-v-5d0380ab=\\"\\">
<empty-content-stub icon=\\"comment\\" descriptionclasses=\\"\\" inline=\\"true\\" center=\\"false\\" data-v-5d0380ab=\\"\\"></empty-content-stub>
</transition-group-stub>
</div>"
`;

View File

@@ -0,0 +1,98 @@
import { config, mount } from "@vue/test-utils";
import GroupSection from "@/components/Group/GroupSection.vue";
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";
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";
const groupUsername = `${groupPreferredUsername}@${groupDomain}`;
const defaultSlotText = "A list of elements";
const createSlotButtonText = "+ Create a post";
type Props = {
title?: string;
icon?: string;
privateSection?: boolean;
route?: { name: string; params: { preferredUsername: string } };
};
const baseProps: Props = {
title: "My group section",
icon: "bullhorn",
route: {
name: RouteName.POSTS,
params: {
preferredUsername: groupUsername,
},
},
};
const generateWrapper = (customProps: Props = {}) => {
return mount(GroupSection, {
props: { ...baseProps, ...customProps },
slots: {
default: `<div>${defaultSlotText}</div>`,
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();
expect(wrapper.find("i.mdi").classes(`mdi-${baseProps.icon}`)).toBe(true);
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(".flex.justify-end.p-2 a").attributes("href")).toBe(
`/@${groupUsername}/p/new`
);
expect(wrapper.html()).toMatchSnapshot();
});
it("renders public group section", () => {
const wrapper = generateWrapper({ privateSection: false });
// expect(wrapper.find(".group-section-title").classes("privateSection")).toBe(
// false
// );
expect(wrapper.html()).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,29 @@
// Vitest Snapshot v1
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=\\"flex-1\\">
<div>A list of elements</div>
</div>
<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 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=\\"flex-1\\">
<div>A list of elements</div>
</div>
<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

@@ -0,0 +1,158 @@
import { config, mount, VueWrapper } from "@vue/test-utils";
import ParticipationSection from "@/components/Participation/ParticipationSection.vue";
import { createRouter, createWebHistory, Router } from "vue-router";
import { routes } from "@/router";
import { CommentModeration, EventJoinOptions } from "@/types/enums";
import { beforeEach, describe, expect, it } from "vitest";
import Oruga from "@oruga-ui/oruga-next";
import FloatingVue from "floating-vue";
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",
options: {
commentModeration: CommentModeration.ALLOW_ALL,
},
beginsOn: new Date("2089-12-04T09:21:25Z"),
endsOn: new Date("2089-12-04T11:21:25Z"),
};
describe("ParticipationSection", () => {
let wrapper: VueWrapper;
const generateWrapper = (customProps: Record<string, unknown> = {}) => {
wrapper = mount(ParticipationSection, {
stubs: {
ParticipationButton: true,
},
props: {
participation: null,
event: eventData,
anonymousParticipation: null,
currentActor: { id: "5" },
identities: [],
anonymousParticipationConfig: {
allowed: true,
},
...customProps,
},
global: {
plugins: [router],
},
});
};
it("renders the participation section with minimal data", async () => {
generateWrapper();
await wrapper.vm.$nextTick();
expect(wrapper.exists()).toBe(true);
expect(wrapper.find(".event-participation").exists()).toBeTruthy();
// TODO: Move to participation button test
// 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.text()).toBe("Participate");
});
it("renders the participation section with existing confimed anonymous participation", async () => {
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.o-btn--text"
);
expect(cancelAnonymousParticipationButton.text()).toBe(
"Cancel anonymous participation"
);
wrapper.find(".event-participation small span").trigger("click");
expect(
wrapper
.findComponent({ ref: "anonymous-participation-modal" })
.isVisible()
).toBeTruthy();
cancelAnonymousParticipationButton.trigger("click");
await wrapper.vm.$nextTick();
expect(wrapper.emitted("cancel-anonymous-participation")).toBeTruthy();
});
it("renders the participation section with existing confimed anonymous participation but event moderation", async () => {
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.o-btn--text"
);
expect(cancelAnonymousParticipationButton.text()).toBe(
"Cancel anonymous participation"
);
wrapper.find(".event-participation small span").trigger("click");
await wrapper.vm.$nextTick();
const modal = wrapper.findComponent({
ref: "anonymous-participation-modal",
});
expect(modal.isVisible()).toBeTruthy();
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."
);
cancelAnonymousParticipationButton.trigger("click");
await wrapper.vm.$nextTick();
expect(wrapper.emitted("cancel-anonymous-participation")).toBeTruthy();
});
it("renders the participation section with existing unconfirmed anonymous participation", async () => {
generateWrapper({ anonymousParticipation: false });
expect(wrapper.find(".event-participation > small").text()).toContain(
"You are participating in this event anonymously but didn't confirm participation"
);
});
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",
},
});
expect(wrapper.find(".event-participation").exists()).toBeFalsy();
expect(wrapper.find("button.o-btn--primary").text()).toBe(
"Event already passed"
);
});
});

View File

@@ -0,0 +1,273 @@
import { config, mount, VueWrapper } from "@vue/test-utils";
import ParticipationWithoutAccount from "@/components/Participation/ParticipationWithoutAccount.vue";
import { routes } from "@/router";
import {
CommentModeration,
EventJoinOptions,
ParticipantRole,
} from "@/types/enums";
import {
createMockClient,
MockApolloClient,
RequestHandler,
} from "mock-apollo-client";
import { ANONYMOUS_ACTOR_ID } from "@/graphql/config";
import { FETCH_EVENT_BASIC, JOIN_EVENT } from "@/graphql/event";
import { IEvent } from "@/types/event.model";
import { anonymousActorIdMock } from "../../mocks/config";
import {
fetchEventBasicMock,
joinEventMock,
joinEventResponseMock,
} from "../../mocks/event";
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";
config.global.plugins.push(Oruga);
let router: Router;
beforeEach(async () => {
router = createRouter({
history: createWebHistory(),
routes: routes,
});
// await router.isReady();
});
const eventData = {
id: "1",
uuid: "f37910ea-fd5a-4756-9679-00971f3f4106",
options: {
commentModeration: CommentModeration.ALLOW_ALL,
},
joinOptions: EventJoinOptions.FREE,
beginsOn: new Date("2089-12-04T09:21:25Z"),
endsOn: new Date("2089-12-04T11:21:25Z"),
participantStats: {
notApproved: 0,
notConfirmed: 0,
rejected: 0,
participant: 0,
creator: 1,
moderator: 0,
administrator: 0,
going: 1,
},
};
describe("ParticipationWithoutAccount", () => {
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> = {}
) => {
mockClient = createMockClient({
cache,
resolvers: defaultResolvers,
});
requestHandlers = {
anonymousActorIdQueryHandler: vi
.fn()
.mockResolvedValue(anonymousActorIdMock),
fetchEventQueryHandler: vi.fn().mockResolvedValue(fetchEventBasicMock),
joinEventMutationHandler: vi
.fn()
.mockResolvedValue(joinEventResponseMock),
...handlers,
};
mockClient.setRequestHandler(
ANONYMOUS_ACTOR_ID,
requestHandlers.anonymousActorIdQueryHandler
);
mockClient.setRequestHandler(
FETCH_EVENT_BASIC,
requestHandlers.fetchEventQueryHandler
);
mockClient.setRequestHandler(
JOIN_EVENT,
requestHandlers.joinEventMutationHandler
);
wrapper = mount(ParticipationWithoutAccount, {
props: {
uuid: eventData.uuid,
...customProps,
},
global: {
provide: {
[DefaultApolloClient]: mockClient,
},
plugins: [router],
},
});
};
it("renders the participation without account view with minimal data", async () => {
generateWrapper();
expect(wrapper.exists()).toBe(true);
expect(requestHandlers.anonymousActorIdQueryHandler).toHaveBeenCalled();
expect(requestHandlers.fetchEventQueryHandler).toHaveBeenCalledWith({
uuid: eventData.uuid,
});
await flushPromises();
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."
);
wrapper.find('input[type="email"]').setValue("some@email.tld");
wrapper.find("textarea").setValue("a message long enough");
wrapper.find("form").trigger("submit");
await flushPromises();
expect(requestHandlers.joinEventMutationHandler).toHaveBeenCalledWith({
...joinEventMock,
});
const cachedData = mockClient?.cache.readQuery<{ event: IEvent }>({
query: FETCH_EVENT_BASIC,
variables: {
uuid: eventData.uuid,
},
});
if (cachedData) {
const { event } = cachedData;
expect(event.participantStats.going).toBe(
eventData.participantStats.going + 1
);
expect(event.participantStats.participant).toBe(
eventData.participantStats.participant + 1
);
}
await flushPromises();
expect(wrapper.find("form").exists()).toBeFalsy();
expect(wrapper.find("h1.title").text()).toBe(
"Request for participation confirmation sent"
);
// 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."
);
expect(wrapper.html()).toMatchSnapshot();
});
it("renders the warning if the event participation is restricted", async () => {
generateWrapper({
fetchEventQueryHandler: vi.fn().mockResolvedValue({
data: {
event: {
...fetchEventBasicMock.data.event,
joinOptions: EventJoinOptions.RESTRICTED,
},
},
}),
joinEventMutationHandler: vi.fn().mockResolvedValue({
data: {
joinEvent: {
...joinEventResponseMock.data.joinEvent,
role: ParticipantRole.NOT_CONFIRMED,
},
},
}),
});
await flushPromises();
// expect(wrapper.vm.$data.event.joinOptions).toBe(
// EventJoinOptions.RESTRICTED
// );
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.findAll("section.container form > p")[1].text()
).not.toContain(
"If you want, you may send a message to the event organizer here."
);
wrapper.find('input[type="email"]').setValue("some@email.tld");
wrapper.find("textarea").setValue("a message long enough");
wrapper.find("form").trigger("submit");
await flushPromises();
expect(requestHandlers.joinEventMutationHandler).toHaveBeenCalledWith({
...joinEventMock,
});
const cachedData = mockClient?.cache.readQuery<{ event: IEvent }>({
query: FETCH_EVENT_BASIC,
variables: {
uuid: eventData.uuid,
},
});
if (cachedData) {
const { event } = cachedData;
expect(event.participantStats.notConfirmed).toBe(
eventData.participantStats.notConfirmed + 1
);
}
await flushPromises();
expect(wrapper.find("form").exists()).toBeFalsy();
expect(wrapper.find("h1.title").text()).toBe(
"Request for participation confirmation sent"
);
expect(wrapper.find("span.details").text()).toBe(
"Your participation will be validated once you click the confirmation link into the email, and after the organizer manually validates your participation."
);
expect(wrapper.html()).toMatchSnapshot();
});
it("handles being already a participant", async () => {
generateWrapper({
joinEventMutationHandler: vi
.fn()
.mockRejectedValue(
new Error("You are already a participant of this event")
),
});
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 flushPromises();
expect(requestHandlers.joinEventMutationHandler).toHaveBeenCalledWith({
...joinEventMock,
});
await flushPromises();
expect(wrapper.find("form").exists()).toBeTruthy();
expect(wrapper.find(".o-notification--danger").text()).toContain(
"You are already a participant of this event"
);
expect(wrapper.html()).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,83 @@
// Vitest Snapshot v1
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>
</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>
<!--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>"
`;
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>"
`;
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>"
`;

View File

@@ -0,0 +1,89 @@
import { mount } from "@vue/test-utils";
import PostListItem from "@/components/Post/PostListItem.vue";
import { beforeEach, describe, it, expect } from "vitest";
import { enUS } from "date-fns/locale";
import { routes } from "@/router";
import { createRouter, createWebHistory, Router } from "vue-router";
let router: Router;
beforeEach(async () => {
router = createRouter({
history: createWebHistory(),
routes: routes,
});
// await router.isReady();
});
const postData = {
id: "1",
slug: "my-blog-post-some-uuid",
title: "My Blog Post",
body: "My content",
insertedAt: "2020-12-02T09:01:20.873Z",
tags: [],
language: "en",
};
const generateWrapper = (
customPostData: Record<string, unknown> = {},
customProps: Record<string, unknown> = {}
) => {
return mount(PostListItem, {
props: {
post: { ...postData, ...customPostData },
...customProps,
},
global: {
provide: {
dateFnsLocale: enUS,
},
plugins: [router],
},
});
};
describe("PostListItem", () => {
it("renders post list item with basic informations", () => {
const wrapper = generateWrapper();
expect(wrapper.html()).toMatchSnapshot();
expect(wrapper.find("a.block.bg-white").attributes("href")).toBe(
`/p/${postData.slug}`
);
expect(wrapper.find("h3").text()).toBe(postData.title);
expect(wrapper.find("p.flex.gap-2").text()).toBe("Dec 2, 2020");
expect(wrapper.find("p.flex.gap-1").exists()).toBeFalsy();
});
it("renders post list item with tags", () => {
const wrapper = generateWrapper({
tags: [{ slug: "a-tag", title: "A tag" }],
});
expect(wrapper.html()).toMatchSnapshot();
expect(wrapper.find("div.flex.flex-wrap.gap-y-0.gap-x-2").text()).toContain(
"A tag"
);
expect(wrapper.find("p.flex.gap-1").exists()).toBeFalsy();
});
it("renders post list item with publisher name", () => {
const wrapper = generateWrapper(
{ author: { name: "An author" } },
{ isCurrentActorMember: true }
);
expect(wrapper.html()).toMatchSnapshot();
expect(wrapper.find("p.flex.gap-1").exists()).toBeTruthy();
expect(wrapper.find("p.flex.gap-1").text()).toContain("An author");
});
});

View File

@@ -0,0 +1,37 @@
// Vitest Snapshot v1
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-6ca7cc69=\\"\\">
<!--v-if-->
<div class=\\"flex flex-col gap-1 bg-inherit p-2 rounded-lg flex-1\\" data-v-6ca7cc69=\\"\\">
<h3 class=\\"text-xl color-violet-3 line-clamp-3 mb-2 font-bold\\" lang=\\"en\\" data-v-6ca7cc69=\\"\\">My Blog Post</h3>
<p class=\\"flex gap-2\\" data-v-6ca7cc69=\\"\\"><span aria-hidden=\\"true\\" class=\\"material-design-icon clock-icon\\" role=\\"img\\" data-v-6ca7cc69=\\"\\"><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-6ca7cc69=\\"\\">Dec 2, 2020</span></p>
<!--v-if-->
<!--v-if-->
</div>
</a>"
`;
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-6ca7cc69=\\"\\">
<!--v-if-->
<div class=\\"flex flex-col gap-1 bg-inherit p-2 rounded-lg flex-1\\" data-v-6ca7cc69=\\"\\">
<h3 class=\\"text-xl color-violet-3 line-clamp-3 mb-2 font-bold\\" lang=\\"en\\" data-v-6ca7cc69=\\"\\">My Blog Post</h3>
<p class=\\"flex gap-2\\" data-v-6ca7cc69=\\"\\"><span aria-hidden=\\"true\\" class=\\"material-design-icon clock-icon\\" role=\\"img\\" data-v-6ca7cc69=\\"\\"><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-6ca7cc69=\\"\\">Dec 2, 2020</span></p>
<!--v-if-->
<p class=\\"flex gap-1\\" data-v-6ca7cc69=\\"\\"><span aria-hidden=\\"true\\" class=\\"material-design-icon account-edit-icon\\" role=\\"img\\" data-v-6ca7cc69=\\"\\"><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-6ca7cc69=\\"\\">An author</b></p>
</div>
</a>"
`;
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-6ca7cc69=\\"\\">
<!--v-if-->
<div class=\\"flex flex-col gap-1 bg-inherit p-2 rounded-lg flex-1\\" data-v-6ca7cc69=\\"\\">
<h3 class=\\"text-xl color-violet-3 line-clamp-3 mb-2 font-bold\\" lang=\\"en\\" data-v-6ca7cc69=\\"\\">My Blog Post</h3>
<p class=\\"flex gap-2\\" data-v-6ca7cc69=\\"\\"><span aria-hidden=\\"true\\" class=\\"material-design-icon clock-icon\\" role=\\"img\\" data-v-6ca7cc69=\\"\\"><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-6ca7cc69=\\"\\">Dec 2, 2020</span></p>
<div class=\\"flex flex-wrap gap-y-0 gap-x-2\\" data-v-6ca7cc69=\\"\\"><span aria-hidden=\\"true\\" class=\\"material-design-icon tag-icon\\" role=\\"img\\" data-v-6ca7cc69=\\"\\"><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 truncate text-sm text-violet-title px-2 py-1 bg-purple-3 dark:text-violet-3\\" data-v-6955ca87=\\"\\" data-v-6ca7cc69=\\"\\">A tag</span></div>
<!--v-if-->
</div>
</a>"
`;

View File

@@ -0,0 +1,57 @@
import { mount } from "@vue/test-utils";
import ReportCard from "@/components/Report/ReportCard.vue";
import { ActorType } from "@/types/enums";
import { describe, expect, it } from "vitest";
const reportData = {
id: "1",
content: "My content",
insertedAt: "2020-12-02T09:01:20.873Z",
reporter: {
preferredUsername: "John Snow",
domain: null,
name: "Reporter of Things",
type: ActorType.PERSON,
},
reported: {
preferredUsername: "my-awesome-group",
domain: null,
name: "My Awesome Group",
},
};
const generateWrapper = (customReportData: Record<string, unknown> = {}) => {
return mount(ReportCard, {
propsData: {
report: { ...reportData, ...customReportData },
},
});
};
describe("ReportCard", () => {
it("renders report card with basic informations", () => {
const wrapper = generateWrapper();
expect(wrapper.find(".flex.gap-1 div p:first-child").text()).toBe(
reportData.reported.name
);
expect(wrapper.find(".flex.gap-1 div p:nth-child(2)").text()).toBe(
`@${reportData.reported.preferredUsername}`
);
expect(wrapper.find(".reported_by div:first-child").text()).toBe(
`Reported by John Snow`
);
});
it("renders report card with with a remote reporter", () => {
const wrapper = generateWrapper({
reporter: { domain: "somewhere.else", type: ActorType.APPLICATION },
});
expect(wrapper.find(".reported_by div:first-child").text()).toBe(
"Reported by someone on somewhere.else"
);
});
});

View File

@@ -0,0 +1,122 @@
import { config, mount } from "@vue/test-utils";
import ReportModal from "@/components/Report/ReportModal.vue";
import { vi, beforeEach, describe, it, expect } from "vitest";
import Oruga from "@oruga-ui/oruga-next";
config.global.plugins.push(Oruga);
const propsData = {
onConfirm: vi.fn(),
};
const generateWrapper = (customPropsData: Record<string, unknown> = {}) => {
return mount(ReportModal, {
props: {
...propsData,
...customPropsData,
},
});
};
beforeEach(() => {
vi.resetAllMocks();
});
describe("ReportModal", () => {
it("renders report modal with basic informations and submits it", async () => {
const wrapper = generateWrapper();
expect(wrapper.find("header").exists()).toBe(false);
expect(wrapper.find("section").text()).not.toContain(
"The content came from another server. Transfer an anonymous copy of the report?"
);
expect(wrapper.find("footer button:first-child").text()).toBe("Cancel");
const submit = wrapper.find("footer button.o-btn--primary");
expect(submit.text()).toBe("Send the report");
const textarea = wrapper.find("textarea");
textarea.setValue("some comment with my report");
submit.trigger("click");
await wrapper.vm.$nextTick();
expect(wrapper.emitted().close).toBeTruthy();
expect(wrapper.vm.$props.onConfirm).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$props.onConfirm).toHaveBeenCalledWith(
"some comment with my report",
false
);
expect(wrapper.html()).toMatchSnapshot();
});
it("renders report modal and shows an inline comment if it's provided", async () => {
const wrapper = generateWrapper({
comment: {
actor: { preferredUsername: "author", name: "I am the comment author" },
text: "this is my <b>comment</b> that will be reported",
},
});
const commentContainer = wrapper.find("article");
expect(commentContainer.find("strong").text()).toContain(
"I am the comment author"
);
expect(commentContainer.find("small").text()).toContain("author");
expect(commentContainer.find("p").html()).toContain(
"this is my <b>comment</b> that will be reported"
);
});
it("renders report modal with with a remote content", async () => {
const wrapper = generateWrapper({ outsideDomain: "somewhere.else" });
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 button.o-btn--primary");
submit.trigger("click");
await wrapper.vm.$nextTick();
expect(wrapper.vm.$props.onConfirm).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$props.onConfirm).toHaveBeenCalledWith("", false);
});
it("renders report modal with with a remote content and accept to forward", async () => {
const wrapper = generateWrapper({ outsideDomain: "somewhere.else" });
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.setValue(true);
const submit = wrapper.find("footer button.o-btn--primary");
submit.trigger("click");
await wrapper.vm.$nextTick();
expect(wrapper.vm.$props.onConfirm).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$props.onConfirm).toHaveBeenCalledWith("", true);
});
it("renders report modal custom title and buttons", async () => {
const wrapper = generateWrapper({
title: "want to report something?",
cancelText: "nah",
confirmText: "report!",
});
expect(wrapper.find("header h2").text()).toBe("want to report something?");
expect(wrapper.find("footer button:first-child").text()).toBe("nah");
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-e0cceef3=\\"\\">
<!--v-if-->
<section data-v-e0cceef3=\\"\\">
<div class=\\"flex gap-1 flex-row mb-3\\" data-v-e0cceef3=\\"\\"><span class=\\"o-icon o-icon--warning hidden md:block flex-1\\" data-v-e0cceef3=\\"\\"><i class=\\"mdi mdi-alert 48\\"></i></span>
<p data-v-e0cceef3=\\"\\">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-e0cceef3=\\"\\">
<!--v-if-->
<div class=\\"o-field o-field--filled\\" data-v-e0cceef3=\\"\\"><label for=\\"additional-comments\\" class=\\"o-field__label\\">Additional comments</label>
<div class=\\"o-ctrl-input\\" data-v-e0cceef3=\\"\\"><textarea id=\\"additional-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-e0cceef3=\\"\\"><button type=\\"button\\" class=\\"o-btn o-btn--outlined\\" data-v-e0cceef3=\\"\\"><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-e0cceef3=\\"\\"><span class=\\"o-btn__wrapper\\"><!--v-if--><span class=\\"o-btn__label\\">Send the report</span>
<!--v-if--></span>
</button></footer>
</div>"
`;

View File

@@ -0,0 +1,115 @@
const useRouterMock = vi.fn(() => ({
push: function () {
// do nothing
},
}));
import { config, mount } from "@vue/test-utils";
import PasswordReset from "@/views/User/PasswordReset.vue";
import { createMockClient, RequestHandler } from "mock-apollo-client";
import { RESET_PASSWORD } from "@/graphql/auth";
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";
config.global.plugins.push(Oruga);
vi.mock("vue-router/dist/vue-router.mjs", () => ({
useRouter: useRouterMock,
}));
let requestHandlers: Record<string, RequestHandler>;
const generateWrapper = (
customRequestHandlers: Record<string, RequestHandler> = {},
customMocks = {}
) => {
const mockClient = createMockClient();
requestHandlers = {
resetPasswordMutationHandler: vi
.fn()
.mockResolvedValue(resetPasswordResponseMock),
...customRequestHandlers,
};
mockClient.setRequestHandler(
RESET_PASSWORD,
requestHandlers.resetPasswordMutationHandler
);
return mount(PasswordReset, {
props: {
token: "some-token",
},
global: {
stubs: ["router-link", "router-view"],
mocks: {
...customMocks,
},
provide: {
[DefaultApolloClient]: mockClient,
},
},
});
};
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: vi.fn().mockResolvedValue({
errors: [{ message: "The token you provided is invalid." }],
}),
});
wrapper
.findAll('input[type="password"')
.forEach((inputField) => inputField.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 flushPromises();
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"')
.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",
});
await flushPromises();
expect(push).toHaveBeenCalledWith({ name: RouteName.HOME });
});
});

View File

@@ -0,0 +1,55 @@
// Vitest Snapshot v1
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 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=\\"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 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

@@ -0,0 +1,197 @@
const useRouterMock = vi.fn(() => ({
push: function () {
// do nothing
},
replace: function () {
// do nothing
},
}));
const useRouteMock = vi.fn(function () {
// do nothing
});
import { config, mount, VueWrapper } from "@vue/test-utils";
import Login from "@/views/User/LoginView.vue";
import {
createMockClient,
MockApolloClient,
RequestHandler,
} from "mock-apollo-client";
import buildCurrentUserResolver from "@/apollo/user";
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 { 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";
vi.mock("vue-router/dist/vue-router.mjs", () => ({
useRouter: useRouterMock,
useRoute: useRouteMock,
}));
config.global.plugins.push(Oruga);
describe("Render login form", () => {
let wrapper: VueWrapper;
let mockClient: MockApolloClient | null;
let requestHandlers: Record<string, RequestHandler>;
const generateWrapper = (
handlers: Record<string, unknown> = {},
customProps: Record<string, unknown> = {},
customMocks: Record<string, unknown> = {}
) => {
mockClient = createMockClient({
cache,
resolvers: buildCurrentUserResolver(cache),
});
requestHandlers = {
configQueryHandler: vi.fn().mockResolvedValue(loginConfigMock),
loginMutationHandler: vi.fn().mockResolvedValue(loginResponseMock),
...handlers,
};
mockClient.setRequestHandler(
LOGIN_CONFIG,
requestHandlers.configQueryHandler
);
mockClient.setRequestHandler(LOGIN, requestHandlers.loginMutationHandler);
wrapper = mount(Login, {
props: {
...customProps,
},
mocks: {
...customMocks,
},
stubs: ["router-link", "router-view"],
global: {
provide: {
[DefaultApolloClient]: mockClient,
},
},
});
};
afterEach(() => {
wrapper?.unmount();
cache.reset();
mockClient = null;
});
it("requires email and password to be filled", async () => {
generateWrapper();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
expect(wrapper.exists()).toBe(true);
expect(requestHandlers.configQueryHandler).toHaveBeenCalled();
wrapper.find('form input[type="email"]').setValue("");
wrapper.find('form input[type="password"]').setValue("");
wrapper.find('form button[type="submit"]').trigger("click");
const form = wrapper.find("form");
expect(form.exists()).toBe(true);
const formElement = form.element as HTMLFormElement;
expect(formElement.checkValidity()).toBe(false);
});
it("renders and submits the login form", async () => {
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();
wrapper.find('form input[type="email"]').setValue("some@email.tld");
wrapper.find('form input[type="password"]').setValue("somepassword");
wrapper.find("form").trigger("submit");
await wrapper.vm.$nextTick();
expect(requestHandlers.loginMutationHandler).toHaveBeenCalledWith({
...loginMock,
});
await flushPromises();
const currentUser = mockClient?.cache.readQuery<{
currentUser: ICurrentUser;
}>({
query: CURRENT_USER_CLIENT,
})?.currentUser;
await flushPromises();
expect(currentUser?.email).toBe("some@email.tld");
expect(currentUser?.id).toBe("1");
await flushPromises();
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: vi.fn().mockResolvedValue({
errors: [
{
message:
'"Impossible to authenticate, either your email or password are invalid."',
},
],
}),
});
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
expect(wrapper.exists()).toBe(true);
expect(requestHandlers.configQueryHandler).toHaveBeenCalled();
wrapper.find('form input[type="email"]').setValue("some@email.tld");
wrapper.find('form input[type="password"]').setValue("somepassword");
wrapper.find("form").trigger("submit");
await wrapper.vm.$nextTick();
expect(requestHandlers.loginMutationHandler).toHaveBeenCalledWith({
...loginMock,
});
await flushPromises();
expect(wrapper.find(".o-notification--danger").text()).toContain(
"Impossible to authenticate, either your email or password are invalid."
);
expect(push).not.toHaveBeenCalled();
});
it("handles redirection after login", async () => {
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();
wrapper.find('form input[type="email"]').setValue("some@email.tld");
wrapper.find('form input[type="password"]').setValue("somepassword");
wrapper.find("form").trigger("submit");
await flushPromises();
expect(push).toHaveBeenCalledWith("/about/instance");
});
});

View File

@@ -0,0 +1,196 @@
// Vitest Snapshot v1
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-zinc-900\\" id=\\"navbar\\">
<div class=\\"container mx-auto flex flex-wrap items-center mx-auto gap-4\\">
<router-link to=\\"[object Object]\\" class=\\"flex items-center\\">
<mobilizon-logo-stub invert=\\"false\\" class=\\"w-40\\"></mobilizon-logo-stub>
</router-link>
<!--v-if--><button type=\\"button\\" class=\\"inline-flex items-center p-2 ml-1 text-sm text-zinc-500 rounded-lg md:hidden hover:bg-zinc-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-zinc-400 dark:hover:bg-zinc-700 dark:focus:ring-gray-600\\" aria-controls=\\"mobile-menu-2\\" aria-expanded=\\"false\\"><span class=\\"sr-only\\">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\\">
<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\\"></path>
</svg></button>
<div class=\\"justify-between items-center w-full md:flex md:w-auto md:order-1 hidden\\" id=\\"mobile-menu-2\\">
<ul class=\\"flex flex-col md:flex-row md:space-x-8 mt-2 md:mt-0 md:font-lightbold\\">
<!--v-if-->
<!--v-if-->
<li>
<router-link to=\\"[object Object]\\" class=\\"block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700\\">Login</router-link>
</li>
<li>
<router-link to=\\"[object Object]\\" class=\\"block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700\\">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

@@ -0,0 +1,59 @@
const useRouterMock = vi.fn(() => ({
push: function () {
// do nothing
},
}));
import { shallowMount, VueWrapper } from "@vue/test-utils";
import NavBar from "@/components/NavBar.vue";
import { createMockClient, MockApolloClient } from "mock-apollo-client";
import buildCurrentUserResolver from "@/apollo/user";
import { InMemoryCache } from "@apollo/client/cache";
import { describe, it, vi, expect, afterEach } from "vitest";
import { DefaultApolloClient } from "@vue/apollo-composable";
vi.mock("vue-router/dist/vue-router.mjs", () => ({
useRouter: useRouterMock,
}));
describe("App component", () => {
let wrapper: VueWrapper;
let mockClient: MockApolloClient | null;
const createComponent = () => {
const cache = new InMemoryCache({ addTypename: false });
mockClient = createMockClient({
cache,
resolvers: buildCurrentUserResolver(cache),
});
wrapper = shallowMount(NavBar, {
// stubs: ["router-link", "router-view", "o-dropdown", "o-dropdown-item"],
global: {
provide: {
[DefaultApolloClient]: mockClient,
},
},
});
};
afterEach(() => {
wrapper?.unmount();
mockClient = 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(wrapper.html()).toMatchSnapshot();
// expect(wrapper.findComponent({ name: "b-navbar" }).exists()).toBeTruthy();
});
});

View File

@@ -0,0 +1,20 @@
import { mount } from "@vue/test-utils";
import Tag from "@/components/TagElement.vue";
import { it, expect } from "vitest";
const tagContent = "My tag";
const createComponent = () => {
return mount(Tag, {
slots: {
default: tagContent,
},
});
};
it("renders a Vue component", () => {
const wrapper = createComponent();
expect(wrapper.exists()).toBe(true);
expect(wrapper.find("span").text()).toEqual(tagContent);
});

View File

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

View File

@@ -0,0 +1,154 @@
export const configMock = {
data: {
config: {
__typename: "Config",
anonymous: {
__typename: "Anonymous",
actorId: "1",
eventCreation: {
__typename: "AnonymousEventCreation",
allowed: false,
validation: {
__typename: "AnonymousEventCreationValidation",
captcha: {
__typename: "AnonymousEventCreationValidationCaptcha",
enabled: false,
},
email: {
__typename: "AnonymousEventCreationValidationEmail",
confirmationRequired: true,
enabled: true,
},
},
},
participation: {
__typename: "AnonymousParticipation",
allowed: true,
validation: {
__typename: "AnonymousParticipationValidation",
captcha: {
__typename: "AnonymousParticipationValidationCaptcha",
enabled: false,
},
email: {
__typename: "AnonymousParticipationValidationEmail",
confirmationRequired: true,
enabled: true,
},
},
},
reports: {
__typename: "AnonymousReports",
allowed: false,
},
},
auth: {
__typename: "Auth",
ldap: false,
oauthProviders: [],
},
countryCode: "fr",
demoMode: false,
description: "Mobilizon.fr est l'instance Mobilizon de Framasoft.",
features: {
__typename: "Features",
eventCreation: true,
groups: true,
},
restrictions: {
__typename: "Restrictions",
onlyAdminCanCreateGroups: false,
onlyGroupsCanCreateEvents: false,
},
geocoding: {
__typename: "Geocoding",
autocomplete: true,
provider: "Elixir.Mobilizon.Service.Geospatial.Pelias",
},
languages: ["fr"],
location: {
__typename: "Lonlat",
latitude: 48.8717,
longitude: 2.32075,
},
maps: {
__typename: "Maps",
tiles: {
__typename: "Tiles",
attribution: "© The OpenStreetMap Contributors",
endpoint: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
},
routing: {
__typename: "Routing",
type: "OPENSTREETMAP",
},
},
name: "Mobilizon",
registrationsAllowlist: false,
registrationsOpen: true,
resourceProviders: [
{
__typename: "ResourceProvider",
endpoint: "https://lite.framacalc.org/",
software: "calc",
type: "ethercalc",
},
{
__typename: "ResourceProvider",
endpoint: "https://hebdo.framapad.org/p/",
software: "pad",
type: "etherpad",
},
{
__typename: "ResourceProvider",
endpoint: "https://framatalk.org/",
software: "visio",
type: "jitsi",
},
],
slogan: null,
uploadLimits: {
__typename: "UploadLimits",
default: 10_000_000,
avatar: 2_000_000,
banner: 4_000_000,
},
instanceFeeds: {
__typename: "InstanceFeeds",
enabled: false,
},
webPush: {
__typename: "WebPush",
enabled: true,
publicKey: "",
},
eventCategories: [],
analytics: [],
},
},
};
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

@@ -0,0 +1,190 @@
import { EventJoinOptions, ParticipantRole } from "@/types/enums";
type DataMock = {
data: Record<string, unknown>;
};
export const fetchEventBasicMock = {
data: {
event: {
__typename: "Event",
id: "1",
uuid: "f37910ea-fd5a-4756-9679-00971f3f4106",
joinOptions: EventJoinOptions.FREE,
participantStats: {
__typename: "ParticipantStats",
notApproved: 0,
notConfirmed: 0,
participant: 0,
going: 1,
},
},
},
};
export const joinEventResponseMock = {
data: {
joinEvent: {
__typename: "Participant",
id: "5",
role: ParticipantRole.NOT_APPROVED,
insertedAt: "2020-12-07T09:33:41Z",
metadata: {
__typename: "ParticipantMetadata",
cancellationToken: "some token",
message: "a message long enough",
},
event: {
__typename: "Event",
id: "1",
uuid: "f37910ea-fd5a-4756-9679-00971f3f4106",
},
actor: {
__typename: "Person",
preferredUsername: "some_actor",
name: "Some actor",
avatar: null,
domain: null,
id: "1",
},
},
},
};
export const joinEventMock = {
eventId: "1",
actorId: "1",
email: "some@email.tld",
message: "a message long enough",
locale: "en_US",
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
};
export const eventNoCommentThreadsMock = {
data: {
event: {
__typename: "Event",
id: "1",
uuid: "f37910ea-fd5a-4756-9679-00971f3f4106",
comments: [],
},
},
};
export const eventCommentThreadsMock = {
data: {
event: {
__typename: "Event",
id: "1",
uuid: "f37910ea-fd5a-4756-9679-00971f3f4106",
comments: [
{
__typename: "Comment",
id: "2",
uuid: "e37910ea-fd5a-4756-9679-00971f3f4107",
url: "https://some-instance.tld/comments/e37910ea-fd5a-4756-9679-00971f3f4107",
text: "my comment text",
local: true,
visibility: "PUBLIC",
totalReplies: 5,
updatedAt: "2020-12-03T09:02:00Z",
inReplyToComment: null,
originComment: null,
replies: [],
actor: {
__typename: "Person",
avatar: {
__typename: "Media",
id: "78",
url: "http://someavatar.url.me",
},
id: "89",
domain: null,
preferredUsername: "someauthor",
name: "Some author",
summary: "I am the senate",
},
deletedAt: null,
insertedAt: "2020-12-03T09:02:00Z",
isAnnouncement: false,
language: "en",
},
{
__typename: "Comment",
id: "29",
uuid: "e37910ea-fd5a-4756-9679-01171f3f4107",
url: "https://some-instance.tld/comments/e37910ea-fd5a-4756-9679-01171f3f4107",
text: "a second comment",
local: true,
visibility: "PUBLIC",
totalReplies: 0,
updatedAt: "2020-12-03T11:02:00Z",
inReplyToComment: null,
originComment: null,
replies: [],
actor: {
__typename: "Person",
avatar: {
__typename: "Media",
id: "78",
url: "http://someavatar.url.me",
},
id: "89",
domain: null,
preferredUsername: "someauthor",
name: "Some author",
summary: "I am the senate",
},
deletedAt: null,
insertedAt: "2020-12-03T11:02:00Z",
isAnnouncement: false,
language: "en",
},
],
},
},
};
export const newCommentForEventMock = {
eventId: "1",
text: "my new comment",
inReplyToCommentId: undefined,
isAnnouncement: false,
originCommentId: undefined,
};
export const newCommentForEventResponse: DataMock = {
data: {
createComment: {
__typename: "Comment",
id: "79",
uuid: "e37910ea-fd5a-4756-9679-01171f3f4444",
url: "https://some-instance.tld/comments/e37910ea-fd5a-4756-9679-01171f3f4444",
text: newCommentForEventMock.text,
local: true,
visibility: "PUBLIC",
totalReplies: 0,
updatedAt: "2020-12-03T13:02:00Z",
originComment: null,
inReplyToComment: null,
replies: [],
actor: {
__typename: "Person",
avatar: {
__typename: "Media",
id: "78",
url: "http://someavatar.url.me",
},
id: "89",
domain: null,
preferredUsername: "someauthor",
name: "Some author",
summary: "I am the senate",
},
deletedAt: null,
insertedAt: "2020-12-03T13:02:00Z",
isAnnouncement: false,
language: "en",
},
},
};

View File

@@ -0,0 +1,12 @@
import { vi } from "vitest";
vi.stubGlobal("matchMedia", (query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(), // deprecated
removeListener: vi.fn(), // deprecated
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
}));

View File

@@ -0,0 +1,15 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const vueJest = require("vue-jest/lib/template-compiler");
module.exports = {
process(content: any) {
const { render } = vueJest({
content,
attrs: {
functional: false,
},
});
return `module.exports = { render: ${render} }`;
},
};