Skip to content

Commit

Permalink
test: unit tests feature components (#1423)
Browse files Browse the repository at this point in the history
* DepositionFilterBanner tests

* InfoLink tests

* MLChallengeBanner tests

* ObjectIdLink tests

* mock css modules

* extract MLChallenge mock utils

* SurveyBanner tests

* TablePageLayout tests

* fix remix mock types

* add comments
  • Loading branch information
codemonkey800 authored Dec 24, 2024
1 parent bec698e commit 2473e30
Show file tree
Hide file tree
Showing 18 changed files with 699 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { createRemixStub } from '@remix-run/testing'
import { render, screen } from '@testing-library/react'

import { NCBI, OBO, WORMBASE } from 'app/constants/datasetInfoLinks'

import { InfoLinkProps } from './InfoLink'

async function renderInfoLink({ id, value }: InfoLinkProps) {
const { InfoLink } = await import('./InfoLink')

function InfoLinkWrapper() {
return <InfoLink id={id} value={value} />
}

const InfoLinkStub = createRemixStub([
{
path: '/',
Component: InfoLinkWrapper,
},
])

render(<InfoLinkStub />)
}

describe('<InfoLink />', () => {
it('should render placeholder if no value', async () => {
await renderInfoLink({ id: 123 })
expect(screen.getByText('--')).toBeInTheDocument()
})

it('should render ncbi link', async () => {
const id = 123
const value = 'value'
await renderInfoLink({ id, value })

const link = screen.queryByRole('link', { name: value })
expect(link).toBeVisible()
expect(link).toHaveAttribute('href', `${NCBI}${id}`)
})

it('should render ncbi link with id prefix', async () => {
const rawId = 123
const id = `NCBITaxon:${rawId}`
const value = 'value'
await renderInfoLink({ id, value })

const link = screen.queryByRole('link', { name: value })
expect(link).toBeVisible()
expect(link).toHaveAttribute('href', `${NCBI}${rawId}`)
})

it('should render wormbase link', async () => {
const id = 'WBStrain12345678'
const value = 'value'
await renderInfoLink({ id, value })

const link = screen.queryByRole('link', { name: value })
expect(link).toBeVisible()
expect(link).toHaveAttribute('href', `${WORMBASE}${id}`)
})

it('should render obo link', async () => {
const id = 'foobar:123'
const value = 'value'
await renderInfoLink({ id, value })

const link = screen.queryByRole('link', { name: value })
expect(link).toBeVisible()
expect(link).toHaveAttribute('href', `${OBO}${id.replaceAll(':', '_')}`)
})

it('should not render link if no pattern match', async () => {
const id = 'someid123'
const value = 'value'
await renderInfoLink({ id, value })

expect(screen.queryByRole('link', { name: value })).not.toBeInTheDocument()
expect(screen.getByText(value)).toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,20 @@ import {
WORMBASE_PATTERN,
} from 'app/constants/datasetInfoLinks'

export function InfoLink({
value,
id,
}: {
export interface InfoLinkProps {
value?: string | null
id?: string | null
}) {
id?: number | string | null
}

export function InfoLink({ value, id }: InfoLinkProps) {
if (!value) {
return <span>--</span>
}

if (id) {
let link
if (typeof id === 'number') {
link = `${NCBI}${id as number}`
link = `${NCBI}${id}`
} else if (id.match(NCBI_ONTOLOGY_PATTERN)) {
link = `${NCBI}${id.replace('NCBITaxon:', '')}`
} else if (id.match(WORMBASE_PATTERN)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { jest } from '@jest/globals'
import { createRemixStub } from '@remix-run/testing'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

import { MockI18n } from './I18n.mock'

const MOCK_DEPOSITION = {
id: 123,
title: 'Title',
}

jest.unstable_mockModule('app/components/I18n', () => ({
__esModule: true,
I18n: MockI18n,
}))

const setDepositionIdMock = jest.fn()

jest.unstable_mockModule('app/hooks/useQueryParam', () => ({
useQueryParam: jest.fn().mockReturnValue([null, setDepositionIdMock]),
}))

const setPreviousSingleDatasetParamsMock = jest.fn()

jest.unstable_mockModule('app/state/filterHistory', () => ({
useDepositionHistory: jest.fn(() => ({
previousSingleDepositionParams: 'object=foo',
})),

useSingleDatasetFilterHistory: jest.fn(() => ({
previousSingleDatasetParams: `object=foo&deposition-id=${MOCK_DEPOSITION.id}`,
setPreviousSingleDatasetParams: setPreviousSingleDatasetParamsMock,
})),
}))

async function renderDepositionFilterBanner() {
const { DepositionFilterBanner } = await import('./DepositionFilterBanner')

function DepositionFilterBannerWrapper() {
return (
<DepositionFilterBanner
deposition={MOCK_DEPOSITION}
labelI18n="onlyDisplayingRunsWithAnnotations"
/>
)
}

const DepositionFilterBannerStub = createRemixStub([
{
path: '/',
Component: DepositionFilterBannerWrapper,
},
])

render(<DepositionFilterBannerStub />)
}

describe('<DepositionFilterBanner />', () => {
it('should render deposition url', async () => {
await renderDepositionFilterBanner()

const text = screen.getByText('onlyDisplayingRunsWithAnnotations')
expect(text).toHaveAttribute(
'data-values',
JSON.stringify({
...MOCK_DEPOSITION,
url: `/depositions/${MOCK_DEPOSITION.id}?object=foo`,
}),
)
})

it('should remove filter on click', async () => {
await renderDepositionFilterBanner()

await userEvent.click(screen.getByRole('button', { name: 'removeFilter' }))

expect(setDepositionIdMock).toHaveBeenCalledWith(null)
expect(setPreviousSingleDatasetParamsMock).toHaveBeenCalledWith(
'object=foo',
)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function DepositionFilterBanner({
i18nKey={labelI18n}
values={{
...deposition,
url: `/depositions/${deposition.id}${previousSingleDepositionParams}`,
url: `/depositions/${deposition.id}?${previousSingleDepositionParams}`,
}}
tOptions={{ interpolation: { escapeValue: false } }}
/>
Expand Down
17 changes: 17 additions & 0 deletions frontend/packages/data-portal/app/components/I18n.mock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { I18nProps } from './I18n'

/**
* Mock I18n component for rendering span element with i18n key as the content.
* Any values are passed in the data-attribute prop in case values need to be
* tested.
*/
export function MockI18n({ i18nKey, values, linkProps }: I18nProps) {
return (
<span
data-values={JSON.stringify(values)}
data-link-props={JSON.stringify(linkProps)}
>
{i18nKey}
</span>
)
}
5 changes: 3 additions & 2 deletions frontend/packages/data-portal/app/components/I18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import type { I18nKeys } from 'app/types/i18n'

import { Link, VariantLinkProps } from './Link'

interface Props extends Omit<TransProps<I18nKeys>, 'ns' | 'i18nKey'> {
export interface I18nProps
extends Omit<TransProps<I18nKeys>, 'ns' | 'i18nKey'> {
i18nKey: I18nKeys
linkProps?: Partial<VariantLinkProps>
}
Expand All @@ -14,7 +15,7 @@ interface Props extends Omit<TransProps<I18nKeys>, 'ns' | 'i18nKey'> {
* Wrapper over `Trans` component with strong typing support for i18n keys. It
* also includes a few default components for rendering inline JSX.
*/
export function I18n({ i18nKey, components, linkProps, ...props }: Props) {
export function I18n({ i18nKey, components, linkProps, ...props }: I18nProps) {
return (
<Trans
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { beforeEach, jest } from '@jest/globals'
import { render, screen } from '@testing-library/react'

import { MockI18n } from 'app/components/I18n.mock'
import { LocalStorageMock } from 'app/mocks/LocalStorage.mock'
import { RemixMock } from 'app/mocks/Remix.mock'
import { getMockUser, setMockTime } from 'app/utils/mock'

async function renderMlChallengeBanner() {
const { MLChallengeBanner } = await import('./MLChallengeBanner')
render(<MLChallengeBanner />)
}

jest.unstable_mockModule('app/components/I18n', () => ({ I18n: MockI18n }))

const remixMock = new RemixMock()
const localStorageMock = new LocalStorageMock()

describe('<MLChallengeBanner />', () => {
beforeEach(() => {
jest.useRealTimers()
localStorageMock.reset()
remixMock.reset()
})

const paths = ['/', '/browse-data/datasets', '/browse-data/depositions']

paths.forEach((pathname) => {
it(`should render on ${pathname}`, async () => {
remixMock.mockPathname(pathname)
await renderMlChallengeBanner()
expect(screen.queryByRole('banner')).toBeVisible()
})
})

it('should not render on blocked pages', async () => {
remixMock.mockPathname('/competition')
await renderMlChallengeBanner()
expect(screen.queryByRole('banner')).not.toBeInTheDocument()
})

it('should render challenge began message', async () => {
setMockTime('2024-12-01')

await renderMlChallengeBanner()
expect(screen.getByText('mlCompetitionHasBegun')).toBeVisible()
})

it('should render challenge ending message', async () => {
setMockTime('2025-01-30')

await renderMlChallengeBanner()
expect(screen.getByText('mlCompetitionEnding')).toBeVisible()
})

it('should render challenge ended message', async () => {
setMockTime('2025-02-07')

await renderMlChallengeBanner()
expect(screen.getByText('mlCompetitionEnded')).toBeVisible()
})

it('should not render banner if was dismissed', async () => {
setMockTime('2024-12-01')
localStorageMock.mockValue('mlCompetitionHasBegun')

await renderMlChallengeBanner()
expect(screen.queryByRole('banner')).not.toBeInTheDocument()
})

it('should render banner if last dismissed was previous state', async () => {
setMockTime('2025-01-30')
localStorageMock.mockValue('mlCompetitionHasBegun')

await renderMlChallengeBanner()
expect(screen.getByRole('banner')).toBeVisible()
})

it('should dismiss banner on click', async () => {
setMockTime('2024-12-01')

await renderMlChallengeBanner()
await getMockUser().click(screen.getByRole('button'))

expect(screen.queryByRole('banner')).not.toBeInTheDocument()
expect(localStorageMock.setValue).toHaveBeenCalledWith(
'mlCompetitionHasBegun',
)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { createRemixStub } from '@remix-run/testing'
import { render, screen } from '@testing-library/react'

import { GO, UNIPROTKB } from 'app/constants/annotationObjectIdLinks'

async function renderObjectIdLink(id: string) {
const { ObjectIdLink } = await import('./ObjectIdLink')

function ObjectIdLinkWrapper() {
return <ObjectIdLink id={id} />
}

const ObjectIdLinkStub = createRemixStub([
{
path: '/',
Component: ObjectIdLinkWrapper,
},
])

render(<ObjectIdLinkStub />)
}

describe('<ObjectIdLink />', () => {
it('should render go link', async () => {
const id = 'GO:123'
await renderObjectIdLink(id)

expect(screen.getByRole('link')).toHaveAttribute('href', `${GO}${id}`)
})

it('should render UniProtKB link', async () => {
const rawId = 123
const id = `UniProtKB:${rawId}`
await renderObjectIdLink(id)

expect(screen.getByRole('link')).toHaveAttribute(
'href',
`${UNIPROTKB}${rawId}`,
)
})

it('should not render link if not matched', async () => {
const id = 'test-id-123'
await renderObjectIdLink(id)

expect(screen.queryByRole('link')).not.toBeInTheDocument()
expect(screen.getByText(id)).toBeVisible()
})
})
Loading

0 comments on commit 2473e30

Please sign in to comment.