Skip to content

Commit 74ad66d

Browse files
committed
Fixes quota tests
1 parent 37c9237 commit 74ad66d

File tree

5 files changed

+150
-38
lines changed

5 files changed

+150
-38
lines changed

frontend/src/components/pages/quotas/quotas-list.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import { createConnectQueryKey } from '@connectrpc/connect-query';
1313
import { Alert, AlertIcon, Button, DataTable, Result, Skeleton } from '@redpanda-data/ui';
1414
import { useQuery } from '@tanstack/react-query';
15+
import { useNavigate, useSearch } from '@tanstack/react-router';
1516
import { SkipIcon } from 'components/icons';
1617
import { config } from 'config';
1718
import { useMemo } from 'react';
@@ -96,6 +97,8 @@ const useQuotasQuery = () => {
9697
};
9798

9899
const QuotasList = () => {
100+
const navigate = useNavigate({ from: '/quotas' });
101+
const search = useSearch({ from: '/quotas' });
99102
const { data, error, isLoading } = useQuotasQuery();
100103

101104
const quotasData = useMemo(() => {
@@ -264,6 +267,26 @@ const QuotasList = () => {
264267
},
265268
]}
266269
data={quotasData}
270+
defaultPageSize={50}
271+
onPaginationChange={(updater) => {
272+
const newPagination =
273+
typeof updater === 'function'
274+
? updater({ pageIndex: search.page ?? 0, pageSize: search.pageSize ?? 50 })
275+
: updater;
276+
277+
navigate({
278+
search: (prev) => ({
279+
...prev,
280+
page: newPagination.pageIndex,
281+
pageSize: newPagination.pageSize,
282+
}),
283+
replace: true,
284+
});
285+
}}
286+
pagination={{
287+
pageIndex: search.page ?? 0,
288+
pageSize: search.pageSize ?? 50,
289+
}}
267290
/>
268291
</Section>
269292
</PageContent>

frontend/src/components/redpanda-ui/components/data-table.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,7 @@ export function DataTablePagination<TData>({ table, testId }: DataTablePaginatio
418418
className="hidden size-8 lg:flex"
419419
onClick={() => table.setPageIndex(0)}
420420
disabled={!table.getCanPreviousPage()}
421+
aria-label="First Page"
421422
>
422423
<span className="sr-only">Go to first page</span>
423424
<ChevronsLeft />
@@ -428,6 +429,7 @@ export function DataTablePagination<TData>({ table, testId }: DataTablePaginatio
428429
className="size-8"
429430
onClick={() => table.previousPage()}
430431
disabled={!table.getCanPreviousPage()}
432+
aria-label="Previous Page"
431433
>
432434
<span className="sr-only">Go to previous page</span>
433435
<ChevronLeft />
@@ -438,6 +440,7 @@ export function DataTablePagination<TData>({ table, testId }: DataTablePaginatio
438440
className="size-8"
439441
onClick={() => table.nextPage()}
440442
disabled={!table.getCanNextPage()}
443+
aria-label="Next Page"
441444
>
442445
<span className="sr-only">Go to next page</span>
443446
<ChevronRight />
@@ -448,6 +451,7 @@ export function DataTablePagination<TData>({ table, testId }: DataTablePaginatio
448451
className="hidden size-8 lg:flex"
449452
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
450453
disabled={!table.getCanNextPage()}
454+
aria-label="Last Page"
451455
>
452456
<span className="sr-only">Go to last page</span>
453457
<ChevronsRight />

frontend/src/routes/quotas.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,22 @@
1212
import { createFileRoute } from '@tanstack/react-router';
1313
import { ScaleIcon } from 'components/icons';
1414
import { useLayoutEffect } from 'react';
15+
import { z } from 'zod';
1516

1617
import QuotasList from '../components/pages/quotas/quotas-list';
1718
import { uiState } from '../state/ui-state';
1819

20+
const quotasSearchSchema = z.object({
21+
page: z.number().int().min(0).catch(0).optional(),
22+
pageSize: z.number().int().min(10).max(100).catch(50).optional(),
23+
});
24+
1925
export const Route = createFileRoute('/quotas')({
2026
staticData: {
2127
title: 'Quotas',
2228
icon: ScaleIcon,
2329
},
30+
validateSearch: quotasSearchSchema,
2431
component: QuotasWrapper,
2532
});
2633

frontend/tests/test-variant-console/quotas/quota-pagination.spec.ts

Lines changed: 97 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import { expect, test } from '@playwright/test';
33
import { createClientIdQuota, deleteClientIdQuota } from '../../shared/quota.utils';
44
import { QuotaPage } from '../utils/quota-page';
55

6-
const DEFAULT_PAGE_SIZE = 50;
7-
86
test.describe('Quotas - Pagination', () => {
97
test('should not show pagination controls when quotas count is less than page size', async ({ page }) => {
108
const quotaPage = new QuotaPage(page);
@@ -33,6 +31,7 @@ test.describe('Quotas - Pagination', () => {
3331
const timestamp = Date.now();
3432
const quotaIds: string[] = [];
3533
const QUOTA_COUNT = 55; // More than one page
34+
const PAGE_SIZE = 20;
3635

3736
await test.step(`Create ${QUOTA_COUNT} quotas`, async () => {
3837
for (let i = 1; i <= QUOTA_COUNT; i++) {
@@ -46,36 +45,51 @@ test.describe('Quotas - Pagination', () => {
4645
}
4746
});
4847

49-
await test.step('Navigate to quotas page', async () => {
50-
await quotaPage.goToQuotasList();
48+
const page1Quotas: string[] = [];
49+
50+
await test.step('Navigate to quotas page with explicit page size', async () => {
51+
await page.goto(`/quotas?page=0&pageSize=${PAGE_SIZE}`);
5152
});
5253

53-
await test.step('Wait for quotas to load', async () => {
54+
await test.step('Wait for quotas to load and capture page 1 quotas', async () => {
5455
await expect(async () => {
5556
const visibleQuotaCount = await page
5657
.locator('tr')
5758
.filter({ hasText: `page-nav-test-${timestamp}` })
5859
.count();
5960
expect(visibleQuotaCount).toBeGreaterThan(0);
6061
}).toPass({ timeout: 15_000, intervals: [500, 1000, 5000] });
62+
63+
// Capture which quotas are visible on page 1
64+
const rows = page.locator('tr').filter({ hasText: `page-nav-test-${timestamp}` });
65+
const rowCount = await rows.count();
66+
for (let i = 0; i < rowCount; i++) {
67+
const text = await rows.nth(i).textContent();
68+
const match = text?.match(/page-nav-test-\d+-\d+/);
69+
if (match) {
70+
page1Quotas.push(match[0]);
71+
}
72+
}
73+
expect(page1Quotas.length).toBeGreaterThan(0);
6174
});
6275

6376
await test.step('Click next page button', async () => {
64-
const nextButton = page.getByRole('button', { name: /next/i }).or(page.locator('[aria-label*="next"]'));
65-
await nextButton.click();
77+
await quotaPage.clickNextPage();
6678

6779
// Wait for page navigation to complete
6880
await page.waitForURL(/page=1/, { timeout: 5000 });
6981
});
7082

71-
await test.step('Verify second page quotas are visible', async () => {
72-
// First quota (from page 1) should not be visible anymore
73-
await quotaPage.verifyQuotaNotExists(quotaIds[0]);
83+
await test.step('Verify page 1 quotas are no longer visible on page 2', async () => {
84+
// At least one quota from page 1 should not be visible on page 2
85+
await quotaPage.verifyQuotaNotExists(page1Quotas[0]);
7486

75-
// Last quota should now be visible on page 2
76-
await expect(async () => {
77-
await quotaPage.verifyQuotaExists(quotaIds[QUOTA_COUNT - 1]);
78-
}).toPass({ timeout: 10_000 });
87+
// Verify we have different quotas on page 2
88+
const page2Rows = await page
89+
.locator('tr')
90+
.filter({ hasText: `page-nav-test-${timestamp}` })
91+
.count();
92+
expect(page2Rows).toBeGreaterThan(0);
7993
});
8094

8195
await test.step('Cleanup: Delete all test quotas', async () => {
@@ -90,6 +104,7 @@ test.describe('Quotas - Pagination', () => {
90104
const timestamp = Date.now();
91105
const quotaIds: string[] = [];
92106
const QUOTA_COUNT = 55;
107+
const PAGE_SIZE = 20;
93108

94109
await test.step(`Create ${QUOTA_COUNT} quotas`, async () => {
95110
for (let i = 1; i <= QUOTA_COUNT; i++) {
@@ -103,41 +118,69 @@ test.describe('Quotas - Pagination', () => {
103118
}
104119
});
105120

106-
await test.step('Navigate to quotas page', async () => {
107-
await quotaPage.goToQuotasList();
121+
const page1Quotas: string[] = [];
122+
const page2Quotas: string[] = [];
123+
124+
await test.step('Navigate to quotas page with explicit page size', async () => {
125+
await page.goto(`/quotas?page=0&pageSize=${PAGE_SIZE}`);
108126
});
109127

110-
await test.step('Wait for quotas to load', async () => {
128+
await test.step('Wait for quotas to load and capture page 1 quotas', async () => {
111129
await expect(async () => {
112130
const visibleQuotaCount = await page
113131
.locator('tr')
114132
.filter({ hasText: `prev-page-test-${timestamp}` })
115133
.count();
116134
expect(visibleQuotaCount).toBeGreaterThan(0);
117135
}).toPass({ timeout: 15_000, intervals: [500, 1000, 5000] });
136+
137+
// Capture which quotas are visible on page 1
138+
const rows = page.locator('tr').filter({ hasText: `prev-page-test-${timestamp}` });
139+
const rowCount = await rows.count();
140+
for (let i = 0; i < rowCount; i++) {
141+
const text = await rows.nth(i).textContent();
142+
const match = text?.match(/prev-page-test-\d+-\d+/);
143+
if (match) {
144+
page1Quotas.push(match[0]);
145+
}
146+
}
147+
expect(page1Quotas.length).toBeGreaterThan(0);
118148
});
119149

120150
await test.step('Navigate to page 2', async () => {
121-
const nextButton = page.getByRole('button', { name: /next/i }).or(page.locator('[aria-label*="next"]'));
122-
await nextButton.click();
151+
await quotaPage.clickNextPage();
123152
await page.waitForURL(/page=1/, { timeout: 5000 });
124153
});
125154

155+
await test.step('Capture page 2 quotas', async () => {
156+
// Capture which quotas are visible on page 2
157+
const rows = page.locator('tr').filter({ hasText: `prev-page-test-${timestamp}` });
158+
const rowCount = await rows.count();
159+
for (let i = 0; i < rowCount; i++) {
160+
const text = await rows.nth(i).textContent();
161+
const match = text?.match(/prev-page-test-\d+-\d+/);
162+
if (match) {
163+
page2Quotas.push(match[0]);
164+
}
165+
}
166+
expect(page2Quotas.length).toBeGreaterThan(0);
167+
168+
// Verify page 1 quota is not on page 2
169+
await quotaPage.verifyQuotaNotExists(page1Quotas[0]);
170+
});
171+
126172
await test.step('Navigate back to page 1', async () => {
127-
const previousButton = page
128-
.getByRole('button', { name: /previous/i })
129-
.or(page.locator('[aria-label*="previous"]'));
130-
await previousButton.click();
173+
await quotaPage.clickPreviousPage();
131174
await page.waitForURL(/page=0/, { timeout: 5000 });
132175
});
133176

134-
await test.step('Verify first page quotas are visible again', async () => {
177+
await test.step('Verify page 1 quotas are visible again', async () => {
135178
await expect(async () => {
136-
await quotaPage.verifyQuotaExists(quotaIds[0]);
179+
await quotaPage.verifyQuotaExists(page1Quotas[0]);
137180
}).toPass({ timeout: 10_000 });
138181

139-
// Last quota should not be visible on page 1
140-
await quotaPage.verifyQuotaNotExists(quotaIds[QUOTA_COUNT - 1]);
182+
// Page 2 quota should not be visible on page 1
183+
await quotaPage.verifyQuotaNotExists(page2Quotas[0]);
141184
});
142185

143186
await test.step('Cleanup: Delete all test quotas', async () => {
@@ -152,6 +195,7 @@ test.describe('Quotas - Pagination', () => {
152195
const timestamp = Date.now();
153196
const quotaIds: string[] = [];
154197
const QUOTA_COUNT = 60;
198+
const PAGE_SIZE = 20;
155199

156200
await test.step(`Create ${QUOTA_COUNT} quotas`, async () => {
157201
for (let i = 1; i <= QUOTA_COUNT; i++) {
@@ -165,29 +209,43 @@ test.describe('Quotas - Pagination', () => {
165209
}
166210
});
167211

168-
await test.step('Navigate to quotas page', async () => {
169-
await quotaPage.goToQuotasList();
212+
const page1Quotas: string[] = [];
213+
214+
await test.step('Navigate to quotas page with explicit page size', async () => {
215+
await page.goto(`/quotas?page=0&pageSize=${PAGE_SIZE}`);
170216
});
171217

172-
await test.step('Wait for quotas to load', async () => {
218+
await test.step('Wait for quotas to load and capture page 1 quotas', async () => {
173219
await expect(async () => {
174220
const visibleQuotaCount = await page
175221
.locator('tr')
176222
.filter({ hasText: `url-state-test-${timestamp}` })
177223
.count();
178224
expect(visibleQuotaCount).toBeGreaterThan(0);
179225
}).toPass({ timeout: 15_000, intervals: [500, 1000, 5000] });
226+
227+
// Capture which quotas are visible on page 1
228+
const rows = page.locator('tr').filter({ hasText: `url-state-test-${timestamp}` });
229+
const rowCount = await rows.count();
230+
for (let i = 0; i < rowCount; i++) {
231+
const text = await rows.nth(i).textContent();
232+
const match = text?.match(/url-state-test-\d+-\d+/);
233+
if (match) {
234+
page1Quotas.push(match[0]);
235+
}
236+
}
237+
expect(page1Quotas.length).toBeGreaterThan(0);
180238
});
181239

182240
await test.step('Navigate to page 2', async () => {
183-
const nextButton = page.getByRole('button', { name: /next/i }).or(page.locator('[aria-label*="next"]'));
184-
await nextButton.click();
241+
await quotaPage.clickNextPage();
185242
await page.waitForURL(/page=1/, { timeout: 5000 });
186243
});
187244

188245
await test.step('Verify URL contains pagination state', async () => {
189246
const url = page.url();
190247
expect(url).toContain('page=1');
248+
expect(url).toContain(`pageSize=${PAGE_SIZE}`);
191249
});
192250

193251
await test.step('Reload page and verify pagination state persists', async () => {
@@ -196,8 +254,8 @@ test.describe('Quotas - Pagination', () => {
196254
// Should still be on page 2
197255
expect(page.url()).toContain('page=1');
198256

199-
// First quota should not be visible (it's on page 1)
200-
await quotaPage.verifyQuotaNotExists(quotaIds[0]);
257+
// Page 1 quota should not be visible (we're on page 2)
258+
await quotaPage.verifyQuotaNotExists(page1Quotas[0]);
201259
});
202260

203261
await test.step('Cleanup: Delete all test quotas', async () => {
@@ -212,6 +270,7 @@ test.describe('Quotas - Pagination', () => {
212270
const timestamp = Date.now();
213271
const quotaIds: string[] = [];
214272
const QUOTA_COUNT = 55;
273+
const PAGE_SIZE = 20;
215274

216275
await test.step(`Create ${QUOTA_COUNT} quotas`, async () => {
217276
for (let i = 1; i <= QUOTA_COUNT; i++) {
@@ -225,8 +284,8 @@ test.describe('Quotas - Pagination', () => {
225284
}
226285
});
227286

228-
await test.step('Navigate to quotas page', async () => {
229-
await quotaPage.goToQuotasList();
287+
await test.step('Navigate to quotas page with explicit page size', async () => {
288+
await page.goto(`/quotas?page=0&pageSize=${PAGE_SIZE}`);
230289
});
231290

232291
await test.step('Wait for quotas to load', async () => {
@@ -240,8 +299,8 @@ test.describe('Quotas - Pagination', () => {
240299
});
241300

242301
await test.step('Verify page info text is displayed', async () => {
243-
// Look for text like "1-50 of 55" or similar pagination info
244-
const pageInfo = page.locator('text=/\\d+-\\d+ of \\d+/').or(page.locator('text=/Page \\d+ of \\d+/'));
302+
// Look for text like "Page 1 of 3" or similar pagination info
303+
const pageInfo = page.locator('text=/Page \\d+ of \\d+/');
245304

246305
const isVisible = await pageInfo.isVisible({ timeout: 2000 }).catch(() => false);
247306

frontend/tests/test-variant-console/utils/quota-page.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,23 @@ export class QuotaPage {
6363
await this.page.goto(`${baseURL}/quotas`);
6464
await expect(this.page.getByRole('heading', { name: 'Quotas' })).toBeVisible();
6565
}
66+
67+
/**
68+
* Pagination methods
69+
*/
70+
getNextPageButton() {
71+
return this.page.locator('button[aria-label="Next Page"]');
72+
}
73+
74+
getPreviousPageButton() {
75+
return this.page.locator('button[aria-label="Previous Page"]');
76+
}
77+
78+
async clickNextPage() {
79+
await this.getNextPageButton().click();
80+
}
81+
82+
async clickPreviousPage() {
83+
await this.getPreviousPageButton().click();
84+
}
6685
}

0 commit comments

Comments
 (0)