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:
172
tests/e2e/login.spec.ts
Normal file
172
tests/e2e/login.spec.ts
Normal 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
9
tests/unit/.eslintrc.js
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
mocha: true,
|
||||
},
|
||||
globals: {
|
||||
expect: true,
|
||||
sinon: true,
|
||||
},
|
||||
};
|
||||
16
tests/unit/setup.ts
Normal file
16
tests/unit/setup.ts
Normal 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);
|
||||
20
tests/unit/specs/common.ts
Normal file
20
tests/unit/specs/common.ts
Normal 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",
|
||||
}),
|
||||
},
|
||||
};
|
||||
182
tests/unit/specs/components/Comment/CommentTree.spec.ts
Normal file
182
tests/unit/specs/components/Comment/CommentTree.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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>"
|
||||
`;
|
||||
98
tests/unit/specs/components/Group/GroupSection.spec.ts
Normal file
98
tests/unit/specs/components/Group/GroupSection.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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>"
|
||||
`;
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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>"
|
||||
`;
|
||||
89
tests/unit/specs/components/Post/PostListItem.spec.ts
Normal file
89
tests/unit/specs/components/Post/PostListItem.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
@@ -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>"
|
||||
`;
|
||||
57
tests/unit/specs/components/Report/ReportCard.spec.ts
Normal file
57
tests/unit/specs/components/Report/ReportCard.spec.ts
Normal 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"
|
||||
);
|
||||
});
|
||||
});
|
||||
122
tests/unit/specs/components/Report/ReportModal.spec.ts
Normal file
122
tests/unit/specs/components/Report/ReportModal.spec.ts
Normal 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!");
|
||||
});
|
||||
});
|
||||
@@ -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>"
|
||||
`;
|
||||
115
tests/unit/specs/components/User/PasswordReset.spec.ts
Normal file
115
tests/unit/specs/components/User/PasswordReset.spec.ts
Normal 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 });
|
||||
});
|
||||
});
|
||||
@@ -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>"
|
||||
`;
|
||||
197
tests/unit/specs/components/User/login.spec.ts
Normal file
197
tests/unit/specs/components/User/login.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
196
tests/unit/specs/components/__snapshots__/navbar.spec.ts.snap
Normal file
196
tests/unit/specs/components/__snapshots__/navbar.spec.ts.snap
Normal 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> -->"
|
||||
`;
|
||||
59
tests/unit/specs/components/navbar.spec.ts
Normal file
59
tests/unit/specs/components/navbar.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
20
tests/unit/specs/components/tag.spec.ts
Normal file
20
tests/unit/specs/components/tag.spec.ts
Normal 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);
|
||||
});
|
||||
34
tests/unit/specs/mocks/auth.ts
Normal file
34
tests/unit/specs/mocks/auth.ts
Normal 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",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
154
tests/unit/specs/mocks/config.ts
Normal file
154
tests/unit/specs/mocks/config.ts
Normal 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",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
190
tests/unit/specs/mocks/event.ts
Normal file
190
tests/unit/specs/mocks/event.ts
Normal 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",
|
||||
},
|
||||
},
|
||||
};
|
||||
12
tests/unit/specs/mocks/matchMedia.ts
Normal file
12
tests/unit/specs/mocks/matchMedia.ts
Normal 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(),
|
||||
}));
|
||||
15
tests/unit/svgTransform.ts
Normal file
15
tests/unit/svgTransform.ts
Normal 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} }`;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user