Skip to content

Commit

Permalink
feat: update importfrommtnproj component and add some basic tests #913
Browse files Browse the repository at this point in the history
  • Loading branch information
clintonlunn committed Aug 21, 2023
1 parent 62caa63 commit b60f5da
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 94 deletions.
2 changes: 1 addition & 1 deletion src/components/edit/RecentChangeHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ const UpdatedFields = ({ fields, doc }: UpdatedFieldsProps): JSX.Element | null

// double access - doc[parent][child]
if (field.includes('.')) {
var [parent, child] = field.split('.')
let [parent, child] = field.split('.')
if (parent === 'content' && doc.__typename === DocumentTypeName.Area) {
parent = 'areaContent' // I had to alias this in the query bc of the overlap with ClimbType
}
Expand Down
5 changes: 3 additions & 2 deletions src/components/ui/micro/AlertDialogue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ interface LeanAlertProps {
description?: ReactNode
children?: ReactNode
className?: string
stackChildren?: boolean
}
/**
* A reusable popup alert
Expand All @@ -150,7 +151,7 @@ interface LeanAlertProps {
* @param cancelAction A button of type `AlertDialogPrimitive.Action` that closes the alert on click. You can register an `onClick()` to perform some action.
* @param noncancelAction Any kind of React component/button that doesn't close the alert on click. Use this if you want to perform an action on click and keep the alert open.
*/
export const LeanAlert = ({ icon = null, title = null, description = null, children = DefaultOkButton, closeOnEsc = true, className = '' }: LeanAlertProps): JSX.Element => {
export const LeanAlert = ({ icon = null, title = null, description = null, children = DefaultOkButton, closeOnEsc = true, className = '', stackChildren = false }: LeanAlertProps): JSX.Element => {
return (
<AlertDialogPrimitive.Root defaultOpen>
<AlertDialogPrimitive.Overlay className='fixed inset-0 bg-black/60 z-50' />
Expand All @@ -164,7 +165,7 @@ export const LeanAlert = ({ icon = null, title = null, description = null, child
{title}
</AlertDialogPrimitive.Title>
<AlertDialogPrimitive.Description className='my-8 text-inherit'>{description}</AlertDialogPrimitive.Description>
<div className='flex items-center justify-center gap-x-6'>
<div className={stackChildren ? 'flex-col items-center justify-center gap-x-6' : 'flex items-center justify-center gap-x-6'}>
{children}
</div>
</div>
Expand Down
162 changes: 71 additions & 91 deletions src/components/users/ImportFromMtnProj.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Fragment, useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { Transition } from '@headlessui/react'
import { FolderArrowDownIcon, XMarkIcon } from '@heroicons/react/24/outline'
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
import { FolderArrowDownIcon } from '@heroicons/react/24/outline'
import { useMutation } from '@apollo/client'
import { signIn, useSession } from 'next-auth/react'
import { toast } from 'react-toastify'
Expand All @@ -11,6 +11,7 @@ import { graphqlClient } from '../../js/graphql/Client'
import { MUTATION_IMPORT_TICKS } from '../../js/graphql/gql/fragments'
import { INPUT_DEFAULT_CSS } from '../ui/form/TextArea'
import Spinner from '../ui/Spinner'
import { LeanAlert } from '../ui/micro/AlertDialogue'

interface Props {
isButton: boolean
Expand Down Expand Up @@ -132,96 +133,75 @@ export function ImportFromMtnProj ({ isButton, username }: Props): JSX.Element {
return (
<>
{isButton && <button onClick={straightToInput} className='btn btn-xs md:btn-sm btn-primary'>Import ticks</button>}
<div
aria-live='assertive'
className='fixed inset-0 z-10 flex items-end px-4 py-6 mt-24 pointer-events-none sm:p-6 sm:items-start'
>
<div className='w-full flex flex-col items-center space-y-4 sm:items-end'>
<Transition.Root
show={show}
as={Fragment}
enter='transform ease-out duration-300 transition'
enterFrom='translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2'
enterTo='translate-y-0 opacity-100 sm:translate-x-0'
leave='transition ease-in duration-100'
leaveFrom='opacity-100'
leaveTo='opacity-0'
>
<div className='max-w-xl w-full bg-white shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden'>
<div className='p-4'>
<div className='flex items-start'>
<div className='flex-shrink-0'>
<FolderArrowDownIcon className='h-6 w-6 text-gray-400' aria-hidden='true' />
</div>
<div className='ml-3 w-0 flex-1 pt-0.5'>
{(errors != null) && errors.length > 0 && errors.map((err, i) => <p className='mt-2 text-ob-primary' key={i}>{err}</p>)}
<p className='text-sm font-medium text-gray-900'>{showInput ? 'Input your Mountain Project profile link' : 'Import your ticks from Mountain Project'}</p>
{!showInput &&
<p className='mt-1 text-sm text-gray-500'>
Don't lose your progress, bring it over to Open Beta.
</p>}
{showInput &&
<div>
<div className='mt-1 relative rounded-md shadow-sm'>
<input
type='text'
name='website'
id='website'
value={mpUID}
onChange={(e) => setMPUID(e.target.value)}
className={clx(INPUT_DEFAULT_CSS, 'w-full')}
placeholder='https://www.mountainproject.com/user/123456789/username'
/>
</div>
</div>}
<div className='mt-3 flex space-x-7'>
{!showInput &&
<button
type='button'
onClick={() => setShowInput(true)}
className='text-center p-2 border-2 rounded-xl border-ob-primary transition
text-ob-primary hover:bg-ob-primary hover:ring hover:ring-ob-primary ring-offset-2
hover:text-white w-32 font-bold'
>
{loading ? 'Working...' : 'Show me how'}
</button>}
{showInput &&
<button
type='button'
onClick={getTicks}
className='btn btn-primary'
>
{loading ? <Spinner /> : 'Get my ticks!'}
</button>}
{!isButton &&
<button
type='button'
onClick={dontShowAgain}
className='bg-white rounded-md text-sm font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500'
>
{loading ? 'Working...' : "Don't show again"}
</button>}
</div>
</div>
<div className='ml-4 flex-shrink-0 flex'>
<button
type='button'
className='bg-white rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500'
onClick={() => {
setShow(false)
setErrors([])
}}
>
<span className='sr-only'>Close</span>
<XMarkIcon className='h-5 w-5' aria-hidden='true' />
</button>
</div>
</div>
</div>

{show && (
<LeanAlert
icon={<FolderArrowDownIcon className='h-6 w-6 text-gray-400' aria-hidden='true' />}
title={showInput ? 'Input your Mountain Project profile link' : 'Import your ticks from Mountain Project'}
description={!showInput ? "Don't lose your progress, bring it over to Open Beta." : null}
closeOnEsc
stackChildren
>
{(errors != null) && errors.length > 0 && errors.map((err, i) => <p className='mt-2 text-ob-primary' key={i}>{err}</p>)}

{showInput && (
<div className='mt-1 relative rounded-md shadow-sm'>
<input
type='text'
name='website'
id='website'
value={mpUID}
onChange={(e) => setMPUID(e.target.value)}
className={clx(INPUT_DEFAULT_CSS, 'w-full')}
placeholder='https://www.mountainproject.com/user/123456789/username'
/>
</div>
</Transition.Root>
</div>
</div>
)}

<div className='mt-3 flex space-x-7 justify-center'>
{!showInput && (
<button
type='button'
onClick={() => setShowInput(true)}
className='text-center p-2 border-2 rounded-xl border-ob-primary transition
text-ob-primary hover:bg-ob-primary hover:ring hover:ring-ob-primary ring-offset-2
hover:text-white w-32 font-bold'
>
{loading ? 'Working...' : 'Show me how'}
</button>
)}

{showInput && (
<button
type='button'
onClick={getTicks}
className='btn btn-primary'
>
{loading ? <Spinner /> : 'Get my ticks!'}
</button>
)}

{!isButton && (
<button
type='button'
onClick={dontShowAgain}
className='bg-white rounded-md text-sm font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500'
>
{loading ? 'Working...' : "Don't show again"}
</button>
)}

<AlertDialogPrimitive.Cancel
asChild onClick={() => {
setShow(false)
setErrors([])
}}
>
<button className='Button mauve'>Cancel</button>
</AlertDialogPrimitive.Cancel>
</div>
</LeanAlert>
)}
</>
)
}
Expand Down
96 changes: 96 additions & 0 deletions src/components/users/__tests__/ImportFromMtnProj.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React from 'react'
import { render, fireEvent, waitFor, screen, act } from '@testing-library/react'
import { MockedProvider } from '@apollo/client/testing'
import ImportFromMtnProj from '../ImportFromMtnProj'
import '@testing-library/jest-dom/extend-expect'

jest.mock('next-auth/react', () => ({
useSession: jest.fn(() => ({ status: 'authenticated' }))
}))

jest.mock('next/router', () => ({
useRouter: jest.fn(() => ({ replace: jest.fn() }))
}))

jest.mock('../../../js/graphql/Client', () => ({

graphqlClient: jest.fn()
}))

jest.mock('react-toastify', () => ({
toast: {
info: jest.fn(),
error: jest.fn()
}
}))

describe('<ImportFromMtnProj />', () => {
it('renders without crashing', () => {
render(
<MockedProvider mocks={[]} addTypename={false}>
<ImportFromMtnProj isButton username='testuser' />
</MockedProvider>
)
})

it('renders a button when isButton prop is true', () => {
render(
<MockedProvider mocks={[]} addTypename={false}>
<ImportFromMtnProj isButton username='testuser' />
</MockedProvider>
)

const button = screen.getByText('Import ticks')
expect(button).toBeInTheDocument()
})

it('renders modal on button click', async () => {
render(
<MockedProvider mocks={[]} addTypename={false}>
<ImportFromMtnProj isButton username='testuser' />
</MockedProvider>
)

const button = screen.getByText('Import ticks')
await waitFor(() => {
act(() => {
fireEvent.click(button)
})
})

await waitFor(() => {
const modalText = screen.getByText('Input your Mountain Project profile link')
expect(modalText).toBeInTheDocument()
})
})

it('accepts input for the Mountain Project profile link', async () => {
render(<ImportFromMtnProj isButton username='testuser' />
)

// Simulate a click to open the modal.
const openModalButton = screen.getByText('Import ticks')
await waitFor(() => {
act(() => {
fireEvent.click(openModalButton)
})
})

// Use findBy to wait for the input field to appear.
const inputField = await screen.findByPlaceholderText('https://www.mountainproject.com/user/123456789/username')

if (!(inputField instanceof HTMLInputElement)) {
throw new Error('Expected an input field')
}

// Simulate entering a Mountain Project URL.

await waitFor(() => {
act(() => {
fireEvent.change(inputField, { target: { value: 'https://www.mountainproject.com/user/123456789/sampleuser' } })
})
})

expect(inputField.value).toBe('https://www.mountainproject.com/user/123456789/sampleuser')
})
})

0 comments on commit b60f5da

Please sign in to comment.