diff --git a/src/modules/creators/creator-list-search-no-results-state.integration.test.ts b/src/modules/creators/creator-list-search-no-results-state.integration.test.ts new file mode 100644 index 0000000..821c0cc --- /dev/null +++ b/src/modules/creators/creator-list-search-no-results-state.integration.test.ts @@ -0,0 +1,94 @@ +// Integration test: creator list search no-results state +// +// When a search query returns zero creators, the response should include the +// search term so that clients can display a no-results message that references +// the search term, distinct from the unfiltered empty state. +// +// Uses Jest mocks — no database connection required. + +import { httpListCreators } from './creators.controllers'; +import * as creatorsUtils from './creators.utils'; + +function makeReq(query: Record = {}): any { + return { query }; +} + +function makeRes(): any { + const res: any = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + res.setHeader = jest.fn().mockReturnValue(res); + res.set = jest.fn().mockReturnValue(res); + return res; +} + +function makeNext(): jest.Mock { + return jest.fn(); +} + +describe('GET /api/v1/creators — search no-results state', () => { + beforeEach(() => { + jest.spyOn(creatorsUtils, 'fetchCreatorList').mockResolvedValue([[], 0]); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('includes searchTerm in response when search returns zero results', async () => { + const req = makeReq({ search: 'zzznomatch' }); + const res = makeRes(); + await httpListCreators(req, res, makeNext()); + + expect(res.status).toHaveBeenCalledWith(200); + const body = res.json.mock.calls[0][0]; + expect(body.data).toHaveProperty('searchTerm', 'zzznomatch'); + }); + + it('does not include searchTerm when no search filter is applied', async () => { + const req = makeReq(); + const res = makeRes(); + await httpListCreators(req, res, makeNext()); + + const body = res.json.mock.calls[0][0]; + expect(body.data).not.toHaveProperty('searchTerm'); + }); + + it('does not include searchTerm when search filter is whitespace-only', async () => { + const req = makeReq({ search: ' ' }); + const res = makeRes(); + await httpListCreators(req, res, makeNext()); + + const body = res.json.mock.calls[0][0]; + expect(body.data).not.toHaveProperty('searchTerm'); + }); + + it('does not include searchTerm when search filter is an empty string', async () => { + const req = makeReq({ search: '' }); + const res = makeRes(); + await httpListCreators(req, res, makeNext()); + + const body = res.json.mock.calls[0][0]; + expect(body.data).not.toHaveProperty('searchTerm'); + }); + + it('search no-results state is distinct from unfiltered empty state', async () => { + const searchReq = makeReq({ search: 'zzznomatch' }); + const searchRes = makeRes(); + await httpListCreators(searchReq, searchRes, makeNext()); + + const noFilterReq = makeReq(); + const noFilterRes = makeRes(); + await httpListCreators(noFilterReq, noFilterRes, makeNext()); + + const searchBody = searchRes.json.mock.calls[0][0]; + const noFilterBody = noFilterRes.json.mock.calls[0][0]; + + expect(searchBody.data).toHaveProperty('searchTerm'); + expect(noFilterBody.data).not.toHaveProperty('searchTerm'); + + expect(searchBody.data.items).toEqual([]); + expect(noFilterBody.data.items).toEqual([]); + expect(searchBody.data.meta).toEqual(noFilterBody.data.meta); + }); +}); \ No newline at end of file diff --git a/src/modules/creators/creators.controllers.ts b/src/modules/creators/creators.controllers.ts index 72dcc0a..de85e08 100644 --- a/src/modules/creators/creators.controllers.ts +++ b/src/modules/creators/creators.controllers.ts @@ -76,7 +76,8 @@ export const httpListCreators: AsyncController = async (req, res, next) => { limit: validatedQuery.limit, offset: validatedQuery.offset, total, - }) + }), + validatedQuery.search ); attachTimestampHeader(res); @@ -309,7 +310,11 @@ export const httpGetTrendingCreators: AsyncController = async ( // Compute volume for each creator const creatorsWithVolume = await Promise.all( + feature/creator-search-no-results-state + creators.map(async (creator: { id: string; handle: string; displayName: string | null; avatarUrl: string | null; isVerified: boolean; createdAt: Date; updatedAt: Date }) => { + creators.map(async creator => { + main const volume = await compute24hVolume(creator.id); return { id: creator.id, @@ -325,7 +330,7 @@ export const httpGetTrendingCreators: AsyncController = async ( ); // Sort by volume descending - creatorsWithVolume.sort((a, b) => { + creatorsWithVolume.sort((a: { volume_24h: string }, b: { volume_24h: string }) => { const volA = BigInt(a.volume_24h); const volB = BigInt(b.volume_24h); if (volB > volA) return 1; diff --git a/src/modules/creators/creators.serializers.ts b/src/modules/creators/creators.serializers.ts index e24419e..b0d340f 100644 --- a/src/modules/creators/creators.serializers.ts +++ b/src/modules/creators/creators.serializers.ts @@ -188,11 +188,13 @@ export type CreatorCursorListResponse = PublicCreatorListEnvelope< */ export function serializeCreatorListResponse( profiles: CreatorProfile[], - meta: OffsetPaginationMeta + meta: OffsetPaginationMeta, + searchTerm?: string ): CreatorListResponse { return wrapPublicCreatorListResponse( serializeCreatorList(profiles), - serializeCreatorListOffsetMeta(meta) + serializeCreatorListOffsetMeta(meta), + searchTerm ); } diff --git a/src/modules/creators/public-creator-list-envelope.utils.ts b/src/modules/creators/public-creator-list-envelope.utils.ts index a80bfa8..caa7b68 100644 --- a/src/modules/creators/public-creator-list-envelope.utils.ts +++ b/src/modules/creators/public-creator-list-envelope.utils.ts @@ -7,6 +7,7 @@ export type PublicCreatorListEnvelope = { items: TItem[]; meta: TMeta; + searchTerm?: string; }; /** @@ -15,7 +16,15 @@ export type PublicCreatorListEnvelope = { */ export function wrapPublicCreatorListResponse( items: TItem[] | null | undefined, - meta: TMeta + meta: TMeta, + searchTerm?: string ): PublicCreatorListEnvelope { - return { items: items ?? [], meta }; + const envelope: PublicCreatorListEnvelope = { + items: items ?? [], + meta, + }; + if (searchTerm !== undefined) { + envelope.searchTerm = searchTerm; + } + return envelope; }