Frontend tests (#426)

* test(useGoogleLogin): add unit tests

* test(useMagicLinkLogin): add unit tests

* test(usePassword): add unit tests

* test(useLogin): add unit tests

* test(Login): add unit tests
This commit is contained in:
Mamadou DICKO 2023-06-30 10:29:15 +02:00 committed by GitHub
parent 6f047f4a39
commit e8b374e8a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 299 additions and 49 deletions

View File

@ -0,0 +1,61 @@
import { render } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import Login from "../page";
const mockUseSearchParams = vi.fn(() => ({
get: vi.fn(),
}));
const mockRedirect = vi.fn((url: string) => ({ url }));
vi.mock("next/navigation", () => ({
redirect: (url: string) => mockRedirect(url),
useSearchParams: () => mockUseSearchParams(),
}));
const mockUseSupabase = vi.fn(() => ({
session: {
user: {},
},
}));
vi.mock("@/lib/context/SupabaseProvider", () => ({
useSupabase: () => mockUseSupabase(),
}));
vi.mock("@/services/analytics/useEventTracking", () => ({
useEventTracking: () => ({ track: vi.fn() }),
}));
describe("Login component", () => {
afterEach(() => {
vi.clearAllMocks();
});
it("redirects to /upload if user is already signed in and is not coming from another page", () => {
render(<Login />);
expect(mockRedirect).toHaveBeenCalledTimes(1);
expect(mockRedirect).toHaveBeenCalledWith("/upload");
});
it('redirects to "/previous-page" if user is already signed in and previous page is set', () => {
const previousPageUrl = "/my-interesting-page";
mockUseSearchParams.mockReturnValue({
get: vi.fn(() => previousPageUrl),
});
render(<Login />);
expect(mockRedirect).toHaveBeenCalledTimes(1);
expect(mockRedirect).toHaveBeenCalledWith(previousPageUrl);
});
it("should render the login form when user is not signed in", () => {
mockUseSupabase.mockReturnValue({
//@ts-expect-error doing this for testing purposes
session: { user: undefined },
});
const { getByTestId } = render(<Login />);
const signInForm = getByTestId("sign-in-form");
expect(signInForm).toBeDefined();
});
});

View File

@ -0,0 +1,28 @@
import { renderHook } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { useGoogleLogin } from "../useGoogleLogin";
const mockSignInWithOAuth = vi.fn(() => ({ error: null }));
const mockUseSupabase = () => ({
supabase: {
auth: {
signInWithOAuth: mockSignInWithOAuth,
},
},
});
vi.mock("@/lib/context/SupabaseProvider", () => ({
useSupabase: () => mockUseSupabase(),
}));
describe("useGoogleLogin", () => {
it("should call signInWithOAuth", async () => {
const { result } = renderHook(() => useGoogleLogin());
await result.current.signInWithGoogle();
expect(mockSignInWithOAuth).toHaveBeenCalledTimes(1);
});
});

View File

@ -8,7 +8,9 @@ export const useGoogleLogin = () => {
const { supabase } = useSupabase(); const { supabase } = useSupabase();
const { publish } = useToast(); const { publish } = useToast();
const [isPending, setIsPending] = useState(false); const [isPending, setIsPending] = useState(false);
const signInWithGoogle = async () => { const signInWithGoogle = async () => {
const { error } = await supabase.auth.signInWithOAuth({ const { error } = await supabase.auth.signInWithOAuth({
provider: "google", provider: "google",

View File

@ -0,0 +1,48 @@
import { act, renderHook } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { useMagicLinkLogin } from "../useMagicLinkLogin";
const mockSignInWithOtp = vi.fn(() => ({ error: null }));
const mockUseSupabase = () => ({
supabase: {
auth: {
signInWithOtp: mockSignInWithOtp,
},
},
});
vi.mock("@/lib/context/SupabaseProvider", () => ({
useSupabase: () => mockUseSupabase(),
}));
const setEmail = vi.fn();
describe("useMagicLinkLogin", () => {
it("should not call signInWithOtp if email is empty", async () => {
const { result } = renderHook(() =>
useMagicLinkLogin({
email: "",
setEmail,
})
);
await act(() => result.current.handleMagicLinkLogin());
expect(mockSignInWithOtp).toHaveBeenCalledTimes(0);
});
it("should call signInWithOtp with proper arguments", async () => {
const email = "user@quivr.app";
const { result } = renderHook(() =>
useMagicLinkLogin({
email,
setEmail,
})
);
await result.current.handleMagicLinkLogin();
expect(mockSignInWithOtp).toHaveBeenCalledTimes(1);
expect(mockSignInWithOtp).toHaveBeenCalledWith({
email,
options: { emailRedirectTo: window.location.hostname },
});
});
});

View File

@ -0,0 +1,57 @@
import { useState } from "react";
type UseMagicLinkLoginProps = {
email: string;
setEmail: (email: string) => void;
};
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { useToast } from "@/lib/hooks";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useMagicLinkLogin = ({
email,
setEmail,
}: UseMagicLinkLoginProps) => {
const { supabase } = useSupabase();
const [isPending, setIsPending] = useState(false);
const { publish } = useToast();
const handleMagicLinkLogin = async () => {
if (email === "") {
publish({
variant: "danger",
text: "Please enter your email address",
});
return;
}
setIsPending(true);
const { error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: window.location.hostname, // current domain name. for eg localhost:3000, localhost:3001, https://...
},
});
if (error) {
publish({
variant: "danger",
text: error.message,
});
} else {
publish({
variant: "success",
text: "Magic link sent successfully if email recognized",
});
setEmail("");
}
setIsPending(false);
};
return { handleMagicLinkLogin, isPending };
};

View File

@ -1,62 +1,28 @@
/* eslint-disable */
"use client"; "use client";
import { useState } from "react";
import Button from "@/lib/components/ui/Button"; import Button from "@/lib/components/ui/Button";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { useToast } from "@/lib/hooks/useToast"; import { useMagicLinkLogin } from "./hooks/useMagicLinkLogin";
type MaginLinkLoginProps = { type MaginLinkLoginProps = {
email: string; email: string;
setEmail: (email: string) => void; setEmail: (email: string) => void;
}; };
export const MagicLinkLogin = ({ email, setEmail }: MaginLinkLoginProps) => { export const MagicLinkLogin = ({
const { supabase } = useSupabase(); email,
const [isPending, setIsPending] = useState(false); setEmail,
}: MaginLinkLoginProps): JSX.Element => {
const { publish } = useToast(); const { handleMagicLinkLogin, isPending } = useMagicLinkLogin({
email,
const handleLogin = async () => { setEmail,
if (email === "") { });
publish({
variant: "danger",
text: "Please enter your email address",
});
return;
}
setIsPending(true);
const { error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: window.location.hostname, // current domain name. for eg localhost:3000, localhost:3001, https://...
},
});
if (error) {
publish({
variant: "danger",
text: error.message,
});
} else {
publish({
variant: "success",
text: "Magic link sent successfully if email recognized",
});
setEmail("");
}
setIsPending(false);
};
return ( return (
<Button <Button
type="button" type="button"
variant={"tertiary"} variant={"tertiary"}
onClick={handleLogin} onClick={() => void handleMagicLinkLogin()}
isLoading={isPending} isLoading={isPending}
> >
Send Magic Link Send Magic Link

View File

@ -0,0 +1,50 @@
import { act, renderHook } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { usePasswordForgotten } from "../usePasswordForgotten";
const mockResetPasswordForEmail = vi.fn(() => ({ error: null }));
const mockUseSupabase = () => ({
supabase: {
auth: {
resetPasswordForEmail: mockResetPasswordForEmail,
},
},
});
vi.mock("@/lib/context/SupabaseProvider", () => ({
useSupabase: () => mockUseSupabase(),
}));
const setEmail = vi.fn();
describe("usePassword", () => {
it("should not call resetPasswordForEmail if email is empty", async () => {
const { result } = renderHook(() =>
usePasswordForgotten({
email: "",
setEmail,
})
);
await act(() => result.current.handleRecoverPassword());
expect(mockResetPasswordForEmail).toHaveBeenCalledTimes(0);
});
it("should call resetPasswordForEmail with proper arguments", async () => {
const email = "user@quivr.app";
const { result } = renderHook(() =>
usePasswordForgotten({
email,
setEmail,
})
);
await act(() => result.current.handleRecoverPassword());
expect(mockResetPasswordForEmail).toHaveBeenCalledTimes(1);
expect(mockResetPasswordForEmail).toHaveBeenCalledWith(email, {
redirectTo: `${window.location.origin}/recover-password`,
});
});
});

View File

@ -0,0 +1,38 @@
import { act, renderHook } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { useLogin } from "../useLogin";
const mockSignInWithPassword = vi.fn(() => ({ error: null }));
const mockUseSupabase = () => ({
supabase: {
auth: {
signInWithPassword: mockSignInWithPassword,
},
},
});
vi.mock("@/lib/context/SupabaseProvider", () => ({
useSupabase: () => mockUseSupabase(),
}));
describe("useLogin", () => {
it("should call signInWithPassword with user email and password", async () => {
const { result } = renderHook(() => useLogin());
const email = "user@quivr.com";
const password = "password";
act(() => result.current.setEmail(email));
act(() => result.current.setPassword(password));
await act(() => result.current.handleLogin());
expect(mockSignInWithPassword).toHaveBeenCalledTimes(1);
expect(mockSignInWithPassword).toHaveBeenCalledWith({
email,
password,
});
});
});

View File

@ -1,7 +1,7 @@
/* eslint-disable */ /* eslint-disable */
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect, useSearchParams } from "next/navigation";
import Button from "@/lib/components/ui/Button"; import Button from "@/lib/components/ui/Button";
import Card from "@/lib/components/ui/Card"; import Card from "@/lib/components/ui/Card";
@ -10,8 +10,6 @@ import Field from "@/lib/components/ui/Field";
import PageHeading from "@/lib/components/ui/PageHeading"; import PageHeading from "@/lib/components/ui/PageHeading";
import { useSupabase } from "@/lib/context/SupabaseProvider"; import { useSupabase } from "@/lib/context/SupabaseProvider";
import { useSearchParams } from "next/navigation";
import { useEventTracking } from "@/services/analytics/useEventTracking"; import { useEventTracking } from "@/services/analytics/useEventTracking";
import { GoogleLoginButton } from "./components/GoogleLogin"; import { GoogleLoginButton } from "./components/GoogleLogin";
import { MagicLinkLogin } from "./components/MagicLinkLogin"; import { MagicLinkLogin } from "./components/MagicLinkLogin";
@ -31,8 +29,9 @@ export default function Login() {
void track("SIGNED_IN"); void track("SIGNED_IN");
if (previousPage === undefined || previousPage === null) { if (previousPage === undefined || previousPage === null) {
redirect("/upload"); redirect("/upload");
} else {
redirect(previousPage);
} }
redirect(previousPage as string);
} }
return ( return (
@ -41,6 +40,7 @@ export default function Login() {
<PageHeading title="Login" subtitle="Welcome back" /> <PageHeading title="Login" subtitle="Welcome back" />
<Card className="max-w-md w-full p-5 sm:p-10 text-left"> <Card className="max-w-md w-full p-5 sm:p-10 text-left">
<form <form
data-testid="sign-in-form"
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
handleLogin(); handleLogin();