Skip to content

Commit

Permalink
feat: add API for research search (#3987)
Browse files Browse the repository at this point in the history
  • Loading branch information
goratt12 authored Jan 17, 2025
1 parent 224814a commit 18d172e
Show file tree
Hide file tree
Showing 8 changed files with 437 additions and 268 deletions.
41 changes: 41 additions & 0 deletions packages/cypress/src/integration/research/list.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
describe('[Research - List Articles]', () => {
const researchPageUrl = '/research'

beforeEach(() => {
cy.visit(researchPageUrl)
})

it('[By Everyone - Lists all research articles]', () => {
cy.step('Verify page loads with research articles')
cy.get('[data-cy=ResearchList]').should('be.visible')
cy.get('[data-cy=ResearchListItem]').should('have.length.greaterThan', 0)
})

it('[Search Functionality - Filters articles]', () => {
const searchTerm = 'test'

cy.step('Type a keyword into the search bar')
cy.get('[data-cy=research-search-box]').clear().type(searchTerm)

cy.step('Verify filtered results are displayed')
cy.get('[data-cy=ResearchListItem]').should('have.length.at.least', 1)
})

it('[Pagination - Displays additional articles]', () => {
cy.step('Verify pagination is visible')
let itemCount
cy.get('[data-cy=ResearchListItem]').then((items) => {
itemCount = items.length
})
cy.get('[data-cy=loadMoreButton]').should('be.visible')
cy.get('[data-cy=loadMoreButton]').click()

cy.step('Verify additional articles are loaded')
cy.then(() => {
cy.get('[data-cy=ResearchListItem]').should(
'have.length.greaterThan',
itemCount,
)
})
})
})
6 changes: 1 addition & 5 deletions src/pages/Research/Content/Common/ResearchCategorySelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,7 @@ const ResearchFieldCategory = () => {
useEffect(() => {
const getCategories = async () => {
const categories = await researchService.getResearchCategories()
setOptions(
categories
.filter((x) => !x._deleted)
.map((x) => ({ label: x.label, value: x })),
)
setOptions(categories.map((x) => ({ label: x.label, value: x })))
}

getCategories()
Expand Down
30 changes: 15 additions & 15 deletions src/pages/Research/Content/ResearchList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@ import { logger } from 'src/logger'
import useDrafts from 'src/pages/common/Drafts/useDrafts'
import { Box, Flex } from 'theme-ui'

import { ITEMS_PER_PAGE } from '../constants'
import { listing } from '../labels'
import { researchService } from '../research.service'
import { ResearchFilterHeader } from './ResearchListHeader'
import ResearchListItem from './ResearchListItem'
import { ResearchSearchParams } from './ResearchSearchParams'

import type { DocumentData, QueryDocumentSnapshot } from 'firebase/firestore'
import type { IResearch, ResearchStatus } from 'oa-shared'
import type { ResearchSortOption } from '../ResearchSortOptions'

Expand All @@ -26,9 +24,7 @@ const ResearchList = observer(() => {
getDrafts: researchService.getDrafts,
})
const [total, setTotal] = useState<number>(0)
const [lastVisible, setLastVisible] = useState<
QueryDocumentSnapshot<DocumentData, DocumentData> | undefined
>(undefined)
const [lastId, setLastId] = useState<string | undefined>(undefined)

const [searchParams, setSearchParams] = useSearchParams()
const q = searchParams.get(ResearchSearchParams.q) || ''
Expand All @@ -55,9 +51,7 @@ const ResearchList = observer(() => {
}
}, [q, category, status, sort])

const fetchResearchItems = async (
skipFrom?: QueryDocumentSnapshot<DocumentData, DocumentData>,
) => {
const fetchResearchItems = async (lastDocId?: string) => {
setIsFetching(true)

try {
Expand All @@ -68,18 +62,17 @@ const ResearchList = observer(() => {
category,
sort,
status,
skipFrom,
ITEMS_PER_PAGE,
lastDocId,
)

if (skipFrom) {
if (lastDocId) {
// if skipFrom is set, means we are requesting another page that should be appended
setResearchItems((items) => [...items, ...result.items])
} else {
setResearchItems(result.items)
}

setLastVisible(result.lastVisible)
setLastId(result.items[result.items.length - 1]._id)

setTotal(result.total)
} catch (error) {
Expand All @@ -98,15 +91,21 @@ const ResearchList = observer(() => {
/>

{showDrafts ? (
<ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
<ul
style={{ listStyle: 'none', padding: 0, margin: 0 }}
data-cy="ResearchList"
>
{drafts.map((item) => {
return <ResearchListItem key={item._id} item={item} />
})}
</ul>
) : (
<>
{researchItems && researchItems.length !== 0 && (
<ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
<ul
style={{ listStyle: 'none', padding: 0, margin: 0 }}
data-cy="ResearchList"
>
{researchItems.map((item) => (
<ResearchListItem key={item._id} item={item} />
))}
Expand All @@ -128,7 +127,8 @@ const ResearchList = observer(() => {
>
<Button
type="button"
onClick={() => fetchResearchItems(lastVisible)}
data-cy="loadMoreButton"
onClick={() => fetchResearchItems(lastId)}
>
{listing.loadMore}
</Button>
Expand Down
197 changes: 93 additions & 104 deletions src/pages/Research/research.service.test.ts
Original file line number Diff line number Diff line change
@@ -1,120 +1,109 @@
import '@testing-library/jest-dom/vitest'

import { ResearchStatus } from 'oa-shared'
import { describe, expect, it, vi } from 'vitest'

import { exportedForTesting } from './research.service'

const mockWhere = vi.fn()
const mockOrderBy = vi.fn()
const mockLimit = vi.fn()
vi.mock('firebase/firestore', () => ({
collection: vi.fn(),
query: vi.fn(),
and: vi.fn(),
where: (path, op, value) => mockWhere(path, op, value),
limit: (limit) => mockLimit(limit),
orderBy: (field, direction) => mockOrderBy(field, direction),
}))

vi.mock('../../stores/databaseV2/endpoints', () => ({
DB_ENDPOINTS: {
research: 'research',
researchCategories: 'researchCategories',
},
}))

vi.mock('../../config/config', () => ({
getConfigurationOption: vi.fn(),
FIREBASE_CONFIG: {
apiKey: 'AIyChVN',
databaseURL: 'https://test.firebaseio.com',
projectId: 'test',
storageBucket: 'test.appspot.com',
},
localStorage: vi.fn(),
SITE: 'unit-tests',
}))

describe('research.search', () => {
it('searches for text', () => {
// prepare
const words = ['test', 'text']

// act
exportedForTesting.createSearchQuery(words, '', 'MostRelevant', null)

// assert
expect(mockWhere).toHaveBeenCalledWith(
'keywords',
'array-contains-any',
words,
)
import { researchService } from './research.service'

describe('research.service', () => {
describe('search', () => {
it('fetches research articles based on search criteria', async () => {
// Mock successful fetch response
global.fetch = vi.fn().mockResolvedValue({
json: () =>
Promise.resolve({
items: [{ id: '1', title: 'Sample Research' }],
total: 1,
}),
})

// Call search with mock parameters
const result = await researchService.search(
['sample'],
'science',
'Newest',
null,
)

// Assert results
expect(result).toEqual({
items: [{ id: '1', title: 'Sample Research' }],
total: 1,
})
})

it('handles errors in search', async () => {
global.fetch = vi.fn().mockRejectedValue('error')

const result = await researchService.search(
['sample'],
'science',
'Newest',
null,
)

expect(result).toEqual({ items: [], total: 0 })
})
})

it('filters by category', () => {
// prepare
const category = 'cat1'
describe('getResearchCategories', () => {
it('fetches research categories', async () => {
global.fetch = vi.fn().mockResolvedValue({
json: () =>
Promise.resolve({ categories: [{ id: 'cat1', name: 'Science' }] }),
})

// act
exportedForTesting.createSearchQuery([], category, 'MostRelevant', null)
const result = await researchService.getResearchCategories()

// assert
expect(mockWhere).toHaveBeenCalledWith(
'researchCategory._id',
'==',
category,
)
})
expect(result).toEqual([{ id: 'cat1', name: 'Science' }])
})

it('handles errors in fetching research categories', async () => {
global.fetch = vi.fn().mockRejectedValue('error')

it('should not call orderBy if sorting by most relevant', () => {
// act
exportedForTesting.createSearchQuery(['test'], '', 'MostRelevant', null)
const result = await researchService.getResearchCategories()

// assert
expect(mockOrderBy).toHaveBeenCalledTimes(0)
expect(result).toEqual([])
})
})

it('should call orderBy when sorting is not MostRelevant', () => {
// act
exportedForTesting.createSearchQuery(['test'], '', 'Newest', null)
describe('getDraftCount', () => {
it('fetches draft count for a user', async () => {
global.fetch = vi.fn().mockResolvedValue({
json: () => Promise.resolve({ total: 5 }),
})

// assert
expect(mockOrderBy).toHaveBeenLastCalledWith('_created', 'desc')
})
const result = await researchService.getDraftCount('user123')

expect(result).toBe(5)
})

it('handles errors in fetching draft count', async () => {
global.fetch = vi.fn().mockRejectedValue('error')

it('should filter by research status', () => {
// act
exportedForTesting.createSearchQuery(
['test'],
'',
'Newest',
ResearchStatus.COMPLETED,
)

// assert
expect(mockWhere).toHaveBeenCalledWith(
'researchStatus',
'==',
ResearchStatus.COMPLETED,
)
const result = await researchService.getDraftCount('user123')

expect(result).toBe(0)
})
})

it('should limit results', () => {
// prepare
const take = 12

// act
exportedForTesting.createSearchQuery(
['test'],
'',
'Newest',
null,
undefined,
take,
)

// assert
expect(mockLimit).toHaveBeenLastCalledWith(take)
describe('getDrafts', () => {
it('fetches research drafts for a user', async () => {
global.fetch = vi.fn().mockResolvedValue({
json: () =>
Promise.resolve({
items: [{ id: 'draft1', title: 'Draft Research' }],
}),
})

const result = await researchService.getDrafts('user123')

expect(result).toEqual([{ id: 'draft1', title: 'Draft Research' }])
})

it('handles errors in fetching drafts', async () => {
global.fetch = vi.fn().mockRejectedValue('error')

const result = await researchService.getDrafts('user123')

expect(result).toEqual([])
})
})
})
Loading

0 comments on commit 18d172e

Please sign in to comment.