Skip to content

Commit

Permalink
sign in test + sign standardize buttons
Browse files Browse the repository at this point in the history
  • Loading branch information
codyzu committed Jul 26, 2023
1 parent c8a955d commit 1d39992
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 84 deletions.
15 changes: 8 additions & 7 deletions src/components/DevSignIn.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import {signInWithEmailAndPassword} from 'firebase/auth';
import {auth} from '../firebase';
import Button from './toolbar/Button';

export default function DevSignIn() {
return (
<div className="flex flex-col items-center">
<div className="flex flex-col items-center gap-2 mt-8">
<div className="text-red-600">
This button only exists on emulator mode builds.
</div>
<button
type="button"
className="btn"
<Button
border
label="Sign In with Test Account"
title="Sign In with Test Account"
icon="i-tabler-bug"
onClick={() => {
void signInWithEmailAndPassword(
auth,
'test@doesnotexist.com',
'123456',
);
}}
>
Sign In with Test Account
</button>
/>
</div>
);
}
13 changes: 10 additions & 3 deletions src/components/toolbar/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,26 @@ export default function Button({
disabled,
border,
className,
submit,
}: {
onClick: MouseEventHandler<HTMLButtonElement>;
onClick?: MouseEventHandler<HTMLButtonElement>;
icon: string;
label: string;
title: string;
disabled?: boolean;
border?: boolean;
className?: string;
submit?: boolean;
}) {
const optionalProps: {onClick?: MouseEventHandler<HTMLButtonElement>} = {};
if (onClick !== undefined) {
optionalProps.onClick = onClick;
}

return (
<button
disabled={Boolean(disabled)}
type="button"
type={submit ? 'submit' : 'button'}
title={title}
className={clsx(
'flex flex-col justify-center items-center bg-gray-800 bg-opacity-90',
Expand All @@ -32,7 +39,7 @@ export default function Button({
: 'text-teal hover:(bg-gray-700) active:(text-white bg-black)',
className,
)}
onClick={onClick}
{...optionalProps}
>
<div className={clsx(icon)} />
<div
Expand Down
70 changes: 70 additions & 0 deletions src/pages/SignIn.Test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {type PropsWithChildren, Suspense} from 'react';
import {screen, userEvent, renderRoute, cleanup} from '../test/test-utils';
import {UserProvider} from '../components/UserProvider';
import {auth} from '../firebase';

type OobCode = {
oobLink: string;
};

type OobCodesResponse = {
oobCodes: OobCode[];
};

describe('SignIn', () => {
let loginLinkSearchParameters: string;
it('can sign in with an email address', async () => {
renderRoute('/signin');
const emailInputElement = await screen.findByLabelText(/email/i);
await userEvent.type(
emailInputElement,
'signin.test.user@doesnotexist.com{Enter}',
);
await screen.findByRole('button', {
name: /email sent/i,
});

cleanup();

// Get the list of Out Of Bound codes waiting to sign in
// https://firebase.google.com/docs/reference/rest/auth#section-auth-emulator-oob
const result = await fetch(
'http://127.0.0.1:9099/emulator/v1/projects/demo-test/oobCodes',
);
const data = (await result.json()) as OobCodesResponse;

expect(data.oobCodes.length).toBeGreaterThanOrEqual(1);

// Assume the last code is ours
const oobCode = data.oobCodes.at(-1)!;

// Follow the link in the code, but don't redirect so we can capture the search params
const loginResult = await fetch(oobCode.oobLink, {redirect: 'manual'});

// Parse the search params in the redirect
const redirectLocationHeader = loginResult.headers.get('location');
expect(redirectLocationHeader).not.toBeNull();
const redirectUrl = new URL(redirectLocationHeader!);
loginLinkSearchParameters = redirectUrl.searchParams.toString();
expect(loginLinkSearchParameters.length).toBeGreaterThan(0);
});

it('signs in', async () => {
// Login with the search params that were captured in the previous test
renderRoute(`/signin?${loginLinkSearchParameters}`, {
wrapper: ({children}: PropsWithChildren) => (
<UserProvider>
<Suspense>{children}</Suspense>
</UserProvider>
),
});

// Wait for the login to complete
await screen.findByRole('button', {name: 'account'}, {timeout: 3000});

expect(auth.currentUser).toHaveProperty(
'email',
'signin.test.user@doesnotexist.com',
);
});
});
156 changes: 82 additions & 74 deletions src/pages/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import {
sendSignInLinkToEmail,
signInWithEmailLink,
} from 'firebase/auth';
import {Link, useNavigate} from 'react-router-dom';
import {Link, useLocation, useNavigate} from 'react-router-dom';
import clsx from 'clsx';
import {auth} from '../firebase';
import Loading from '../components/Loading';
import Button from '../components/toolbar/Button';
import DefaultLayout from '../layouts/DefaultLayout';

const DevSignIn = lazy(async () => import('../components/DevSignIn'));

Expand All @@ -36,7 +38,11 @@ export default function SignIn() {
// dynamicLinkDomain: 'example.page.link',
};

const isLink = isSignInWithEmailLink(auth, window.location.href);
// Build the current url with react router location, allowing this to be tested
const location = useLocation();
const currentUrl = `${window.location.origin}${location.pathname}${location.search}`;

const isLink = isSignInWithEmailLink(auth, currentUrl);

const navigate = useNavigate();
const [signInError, setSignInError] = useState<Error>();
Expand All @@ -51,24 +57,24 @@ export default function SignIn() {
async (signInEmail: string) => {
setSigningIn(true);
try {
await signInWithEmailLink(auth, signInEmail, window.location.href);
await signInWithEmailLink(auth, signInEmail, currentUrl);
navigate('/');
} catch (error) {
setSignInError(error as Error);
} finally {
window.localStorage.removeItem('emailForSignIn');
}
},
[navigate],
[navigate, currentUrl],
);

useEffect(() => {
const storedEmail = window.localStorage.getItem('emailForSignIn');
if (isSignInWithEmailLink(auth, window.location.href) && storedEmail) {
if (isSignInWithEmailLink(auth, currentUrl) && storedEmail) {
console.log('signing in');
void signInAndRemoveEmail(storedEmail);
}
}, [signInAndRemoveEmail]);
}, [signInAndRemoveEmail, currentUrl]);

const [email, setEmail] = useState(
window.localStorage.getItem('emailForSignIn') ?? '',
Expand All @@ -87,79 +93,81 @@ export default function SignIn() {
const [emailSending, setEmailSending] = useState(false);
const [emailSent, setEmailSent] = useState(false);

if (signingIn) {
return (
<div className="w-screen h-screen">
<Loading message="Signing In..." />
</div>
);
}

return (
<div className="flex flex-col items-center gap-4 p-4 w-screen">
{user && (
<div className="prose">
You are already signed as {user.email}. You should probably head{' '}
<Link to="/">home</Link>.
<DefaultLayout title="Sign In to Slidr">
{signingIn ? (
<div className="w-screen h-screen">
<Loading message="Signing In..." />
</div>
)}
<form
className="flex flex-row justify-center items-center gap-2 flex-wrap w-full"
onSubmit={async (event) => {
event.preventDefault();

if (isLink) {
void signInAndRemoveEmail(email);
return;
}

setEmailSending(true);
await sendSignInLinkToEmail(auth, email, actionCodeSettings);
window.localStorage.setItem('emailForSignIn', email);
setEmailSent(true);
}}
>
<label className="flex flex-row gap-2 items-center">
Email:
<input
className="input w-auto invalid:(border-red-700 shadow-red-700) invalid-focus:(border-red-700 shadow-red-700)"
id="email"
type="email"
placeholder="email address..."
value={email}
disabled={emailSent}
size={30}
onChange={(event) => {
setEmail(event.target.value);
) : (
<div className="flex flex-col items-center gap-4 p-4">
{user && (
<div className="prose">
You are already signed as {user.email}. You should probably head{' '}
<Link to="/">home</Link>.
</div>
)}
<form
className="flex flex-row justify-center items-stretch gap-2 flex-wrap w-full"
onSubmit={async (event) => {
event.preventDefault();
if (isLink) {
void signInAndRemoveEmail(email);
return;
}

setEmailSending(true);
await sendSignInLinkToEmail(auth, email, actionCodeSettings);
window.localStorage.setItem('emailForSignIn', email);
setEmailSent(true);
}}
/>
</label>
{isLink ? (
<button className="btn" type="submit">
Verify & Sign In
</button>
) : (
<button
className="btn flex flex-row items-center gap-2"
type="submit"
disabled={emailSent}
>
<div
className={clsx(
emailSent ? 'i-tabler-mail-fast' : 'i-tabler-send',
!emailSent && emailSending && 'animate-spin',
)}
/>
<div>{emailSent ? 'Email Sent' : 'Send Email'}</div>
</button>
)}
</form>
{emailSent && (
<div>
Please click on the link sent in the email. You can close this page.
<label className="flex flex-row gap-2 items-stretch">
<div className="flex flex-row items-center">Email:</div>
<input
className="input w-auto invalid:(border-red-700 shadow-red-700) invalid-focus:(border-red-700 shadow-red-700)"
id="email"
type="email"
placeholder="email address..."
value={email}
disabled={emailSent}
size={30}
onChange={(event) => {
setEmail(event.target.value);
}}
/>
</label>
{isLink ? (
<Button
border
submit
icon="i-tabler-user-check"
label="Verify & Sign In"
title="Verify & Sign In"
/>
) : (
<Button
border
submit
icon={clsx(
emailSent ? 'i-tabler-mail-fast' : 'i-tabler-send',
!emailSent && emailSending && 'animate-spin',
)}
label={emailSent ? 'Email Sent' : 'Send Email'}
title={emailSent ? 'Email Sent' : 'Send Email'}
disabled={emailSent}
/>
)}
</form>
{emailSent && (
<div>
Please click on the link sent in the email. You can close this
page.
</div>
)}
{import.meta.env.MODE === 'emulator' && <DevSignIn />}
</div>
)}
{import.meta.env.MODE === 'emulator' && <DevSignIn />}
</div>
</DefaultLayout>
);
}

0 comments on commit 1d39992

Please sign in to comment.