Skip to content

Commit 91e7f29

Browse files
frontend: add tests for iterating over all pages
1 parent 383d8e4 commit 91e7f29

3 files changed

Lines changed: 401 additions & 0 deletions

File tree

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/**
2+
* Copyright 2025 Redpanda Data, Inc.
3+
*
4+
* Use of this software is governed by the Business Source License
5+
* included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md
6+
*
7+
* As of the Change Date specified in that file, in accordance with
8+
* the Business Source License, use of this software will be governed
9+
* by the Apache License, Version 2.0
10+
*/
11+
12+
import { create } from '@bufbuild/protobuf';
13+
import { createRouterTransport } from '@connectrpc/connect';
14+
import { renderHook, waitFor } from '@testing-library/react';
15+
import { AIAgentSchema, ListAIAgentsResponseSchema } from 'protogen/redpanda/api/dataplane/v1alpha3/ai_agent_pb';
16+
import { listAIAgents } from 'protogen/redpanda/api/dataplane/v1alpha3/ai_agent-AIAgentService_connectquery';
17+
import { connectQueryWrapper } from 'test-utils';
18+
import { describe, expect, test } from 'vitest';
19+
20+
import { useListAIAgentsQuery } from './ai-agent';
21+
22+
describe('useListAIAgentsQuery', () => {
23+
test('fetches all pages and flattens agents into a single array', async () => {
24+
let callCount = 0;
25+
26+
const transport = createRouterTransport(({ rpc }) => {
27+
rpc(listAIAgents, (req) => {
28+
callCount += 1;
29+
const pageToken = req.pageToken;
30+
31+
if (pageToken === '') {
32+
return create(ListAIAgentsResponseSchema, {
33+
aiAgents: [create(AIAgentSchema, { id: 'agent-1', displayName: 'Agent 1' })],
34+
nextPageToken: 'page2',
35+
});
36+
}
37+
if (pageToken === 'page2') {
38+
return create(ListAIAgentsResponseSchema, {
39+
aiAgents: [create(AIAgentSchema, { id: 'agent-2', displayName: 'Agent 2' })],
40+
nextPageToken: 'page3',
41+
});
42+
}
43+
return create(ListAIAgentsResponseSchema, {
44+
aiAgents: [create(AIAgentSchema, { id: 'agent-3', displayName: 'Agent 3' })],
45+
nextPageToken: '',
46+
});
47+
});
48+
});
49+
50+
const { wrapper } = connectQueryWrapper({ defaultOptions: { queries: { retry: false } } }, transport);
51+
52+
const { result } = renderHook(() => useListAIAgentsQuery(), { wrapper });
53+
54+
await waitFor(() => {
55+
expect(result.current.isLoading).toBe(false);
56+
expect(result.current.data.aiAgents).toHaveLength(3);
57+
});
58+
59+
expect(callCount).toBe(3);
60+
expect(result.current.data.aiAgents.map((a) => a.id)).toEqual(['agent-1', 'agent-2', 'agent-3']);
61+
});
62+
63+
test('with page size 1, fetches once per record', async () => {
64+
let callCount = 0;
65+
66+
const transport = createRouterTransport(({ rpc }) => {
67+
rpc(listAIAgents, (req) => {
68+
callCount += 1;
69+
const pageToken = req.pageToken;
70+
71+
if (pageToken === '') {
72+
return create(ListAIAgentsResponseSchema, {
73+
aiAgents: [create(AIAgentSchema, { id: 'agent-1', displayName: 'Agent 1' })],
74+
nextPageToken: 'page2',
75+
});
76+
}
77+
// Last page
78+
return create(ListAIAgentsResponseSchema, {
79+
aiAgents: [create(AIAgentSchema, { id: 'agent-2', displayName: 'Agent 2' })],
80+
nextPageToken: '',
81+
});
82+
});
83+
});
84+
85+
const { wrapper } = connectQueryWrapper({ defaultOptions: { queries: { retry: false } } }, transport);
86+
87+
const { result } = renderHook(() => useListAIAgentsQuery(), { wrapper });
88+
89+
await waitFor(() => {
90+
expect(result.current.isLoading).toBe(false);
91+
expect(result.current.data.aiAgents).toHaveLength(2);
92+
});
93+
94+
// With page size 1 and 2 records, the query should execute exactly twice
95+
expect(callCount).toBe(2);
96+
expect(result.current.data.aiAgents.map((a) => a.id)).toEqual(['agent-1', 'agent-2']);
97+
});
98+
99+
test('returns all data in a single page when no nextPageToken', async () => {
100+
let callCount = 0;
101+
102+
const transport = createRouterTransport(({ rpc }) => {
103+
rpc(listAIAgents, () => {
104+
callCount += 1;
105+
return create(ListAIAgentsResponseSchema, {
106+
aiAgents: [
107+
create(AIAgentSchema, { id: 'agent-1', displayName: 'Agent 1' }),
108+
create(AIAgentSchema, { id: 'agent-2', displayName: 'Agent 2' }),
109+
],
110+
nextPageToken: '',
111+
});
112+
});
113+
});
114+
115+
const { wrapper } = connectQueryWrapper({ defaultOptions: { queries: { retry: false } } }, transport);
116+
117+
const { result } = renderHook(() => useListAIAgentsQuery(), { wrapper });
118+
119+
await waitFor(() => {
120+
expect(result.current.isLoading).toBe(false);
121+
expect(result.current.data.aiAgents).toHaveLength(2);
122+
});
123+
124+
expect(callCount).toBe(1);
125+
});
126+
127+
test('handles empty result', async () => {
128+
let callCount = 0;
129+
130+
const transport = createRouterTransport(({ rpc }) => {
131+
rpc(listAIAgents, () => {
132+
callCount += 1;
133+
return create(ListAIAgentsResponseSchema, {
134+
aiAgents: [],
135+
nextPageToken: '',
136+
});
137+
});
138+
});
139+
140+
const { wrapper } = connectQueryWrapper({ defaultOptions: { queries: { retry: false } } }, transport);
141+
142+
const { result } = renderHook(() => useListAIAgentsQuery(), { wrapper });
143+
144+
await waitFor(() => {
145+
expect(result.current.isLoading).toBe(false);
146+
});
147+
148+
expect(callCount).toBe(1);
149+
expect(result.current.data.aiAgents).toHaveLength(0);
150+
});
151+
});
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* Copyright 2025 Redpanda Data, Inc.
3+
*
4+
* Use of this software is governed by the Business Source License
5+
* included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md
6+
*
7+
* As of the Change Date specified in that file, in accordance with
8+
* the Business Source License, use of this software will be governed
9+
* by the Apache License, Version 2.0
10+
*/
11+
12+
import { create } from '@bufbuild/protobuf';
13+
import { createRouterTransport } from '@connectrpc/connect';
14+
import { renderHook, waitFor } from '@testing-library/react';
15+
import { ListPipelinesResponseSchema } from 'protogen/redpanda/api/console/v1alpha1/pipeline_pb';
16+
import { listPipelines } from 'protogen/redpanda/api/console/v1alpha1/pipeline-PipelineService_connectquery';
17+
import {
18+
ListPipelinesResponseSchema as DataPlaneListPipelinesResponseSchema,
19+
PipelineSchema,
20+
} from 'protogen/redpanda/api/dataplane/v1/pipeline_pb';
21+
import { connectQueryWrapper } from 'test-utils';
22+
import { describe, expect, test } from 'vitest';
23+
24+
import { useListPipelinesQuery } from './pipeline';
25+
26+
describe('useListPipelinesQuery', () => {
27+
test('fetches all pages and flattens pipelines into a single array', async () => {
28+
let callCount = 0;
29+
30+
const transport = createRouterTransport(({ rpc }) => {
31+
rpc(listPipelines, (req) => {
32+
callCount += 1;
33+
const pageToken = req.request?.pageToken ?? '';
34+
35+
if (pageToken === '') {
36+
return create(ListPipelinesResponseSchema, {
37+
response: create(DataPlaneListPipelinesResponseSchema, {
38+
pipelines: [create(PipelineSchema, { id: 'pipeline-1', displayName: 'Pipeline 1' })],
39+
nextPageToken: 'page2',
40+
}),
41+
});
42+
}
43+
if (pageToken === 'page2') {
44+
return create(ListPipelinesResponseSchema, {
45+
response: create(DataPlaneListPipelinesResponseSchema, {
46+
pipelines: [create(PipelineSchema, { id: 'pipeline-2', displayName: 'Pipeline 2' })],
47+
nextPageToken: 'page3',
48+
}),
49+
});
50+
}
51+
return create(ListPipelinesResponseSchema, {
52+
response: create(DataPlaneListPipelinesResponseSchema, {
53+
pipelines: [create(PipelineSchema, { id: 'pipeline-3', displayName: 'Pipeline 3' })],
54+
nextPageToken: '',
55+
}),
56+
});
57+
});
58+
});
59+
60+
const { wrapper } = connectQueryWrapper({ defaultOptions: { queries: { retry: false } } }, transport);
61+
62+
const { result } = renderHook(() => useListPipelinesQuery(), { wrapper });
63+
64+
await waitFor(() => {
65+
expect(result.current.isLoading).toBe(false);
66+
expect(result.current.data.pipelines).toHaveLength(3);
67+
});
68+
69+
expect(callCount).toBe(3);
70+
expect(result.current.data.pipelines.map((p) => p.id)).toEqual(['pipeline-1', 'pipeline-2', 'pipeline-3']);
71+
});
72+
73+
test('returns all data in a single page when no nextPageToken', async () => {
74+
let callCount = 0;
75+
76+
const transport = createRouterTransport(({ rpc }) => {
77+
rpc(listPipelines, () => {
78+
callCount += 1;
79+
return create(ListPipelinesResponseSchema, {
80+
response: create(DataPlaneListPipelinesResponseSchema, {
81+
pipelines: [
82+
create(PipelineSchema, { id: 'pipeline-1', displayName: 'Pipeline 1' }),
83+
create(PipelineSchema, { id: 'pipeline-2', displayName: 'Pipeline 2' }),
84+
],
85+
nextPageToken: '',
86+
}),
87+
});
88+
});
89+
});
90+
91+
const { wrapper } = connectQueryWrapper({ defaultOptions: { queries: { retry: false } } }, transport);
92+
93+
const { result } = renderHook(() => useListPipelinesQuery(), { wrapper });
94+
95+
await waitFor(() => {
96+
expect(result.current.isLoading).toBe(false);
97+
expect(result.current.data.pipelines).toHaveLength(2);
98+
});
99+
100+
expect(callCount).toBe(1);
101+
});
102+
103+
test('handles empty result', async () => {
104+
let callCount = 0;
105+
106+
const transport = createRouterTransport(({ rpc }) => {
107+
rpc(listPipelines, () => {
108+
callCount += 1;
109+
return create(ListPipelinesResponseSchema, {
110+
response: create(DataPlaneListPipelinesResponseSchema, {
111+
pipelines: [],
112+
nextPageToken: '',
113+
}),
114+
});
115+
});
116+
});
117+
118+
const { wrapper } = connectQueryWrapper({ defaultOptions: { queries: { retry: false } } }, transport);
119+
120+
const { result } = renderHook(() => useListPipelinesQuery(), { wrapper });
121+
122+
await waitFor(() => {
123+
expect(result.current.isLoading).toBe(false);
124+
});
125+
126+
expect(callCount).toBe(1);
127+
expect(result.current.data.pipelines).toHaveLength(0);
128+
});
129+
});

0 commit comments

Comments
 (0)