Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<string, string> = {}): 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);
});
});
9 changes: 7 additions & 2 deletions src/modules/creators/creators.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ export const httpListCreators: AsyncController = async (req, res, next) => {
limit: validatedQuery.limit,
offset: validatedQuery.offset,
total,
})
}),
validatedQuery.search
);

attachTimestampHeader(res);
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions src/modules/creators/creators.serializers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}

Expand Down
13 changes: 11 additions & 2 deletions src/modules/creators/public-creator-list-envelope.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
export type PublicCreatorListEnvelope<TItem, TMeta> = {
items: TItem[];
meta: TMeta;
searchTerm?: string;
};

/**
Expand All @@ -15,7 +16,15 @@ export type PublicCreatorListEnvelope<TItem, TMeta> = {
*/
export function wrapPublicCreatorListResponse<TItem, TMeta>(
items: TItem[] | null | undefined,
meta: TMeta
meta: TMeta,
searchTerm?: string
): PublicCreatorListEnvelope<TItem, TMeta> {
return { items: items ?? [], meta };
const envelope: PublicCreatorListEnvelope<TItem, TMeta> = {
items: items ?? [],
meta,
};
if (searchTerm !== undefined) {
envelope.searchTerm = searchTerm;
}
return envelope;
}
Loading