Skip to content

Commit d0cda70

Browse files
authored
test(routes): list page (#3069)
1 parent a8c4e73 commit d0cda70

File tree

8 files changed

+216
-64
lines changed

8 files changed

+216
-64
lines changed

e2e/pom/routes.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
import { uiGoto } from '@e2e/utils/ui';
18+
import { expect, type Page } from '@playwright/test';
19+
20+
const locator = {
21+
getRouteNavBtn: (page: Page) =>
22+
page.getByRole('link', { name: 'Routes', exact: true }),
23+
getAddRouteBtn: (page: Page) =>
24+
page.getByRole('button', { name: 'Add Route', exact: true }),
25+
};
26+
27+
const assert = {
28+
isIndexPage: async (page: Page) => {
29+
await expect(page).toHaveURL((url) => url.pathname.endsWith('/routes'));
30+
const title = page.getByRole('heading', { name: 'Routes' });
31+
await expect(title).toBeVisible();
32+
},
33+
isAddPage: async (page: Page) => {
34+
await expect(page).toHaveURL((url) => url.pathname.endsWith('/routes/add'));
35+
const title = page.getByRole('heading', { name: 'Add Route' });
36+
await expect(title).toBeVisible();
37+
},
38+
isDetailPage: async (page: Page) => {
39+
await expect(page).toHaveURL((url) =>
40+
url.pathname.includes('/routes/detail')
41+
);
42+
const title = page.getByRole('heading', { name: 'Route Detail' });
43+
await expect(title).toBeVisible();
44+
},
45+
};
46+
47+
const goto = {
48+
toIndex: (page: Page) => uiGoto(page, '/routes'),
49+
toAdd: (page: Page) => uiGoto(page, '/routes/add'),
50+
};
51+
52+
export const routesPom = {
53+
...locator,
54+
...assert,
55+
...goto,
56+
};

e2e/tests/routes.list.spec.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { routesPom } from '@e2e/pom/routes';
19+
import { setupPaginationTests } from '@e2e/utils/pagination-test-helper';
20+
import { e2eReq } from '@e2e/utils/req';
21+
import { test } from '@e2e/utils/test';
22+
import { expect, type Page } from '@playwright/test';
23+
24+
import { deleteAllRoutes, putRouteReq } from '@/apis/routes';
25+
import { API_ROUTES } from '@/config/constant';
26+
import type { APISIXType } from '@/types/schema/apisix';
27+
28+
test('should navigate to routes page', async ({ page }) => {
29+
await test.step('navigate to routes page', async () => {
30+
await routesPom.getRouteNavBtn(page).click();
31+
await routesPom.isIndexPage(page);
32+
});
33+
34+
await test.step('verify routes page components', async () => {
35+
await expect(routesPom.getAddRouteBtn(page)).toBeVisible();
36+
37+
// list table exists
38+
const table = page.getByRole('table');
39+
await expect(table).toBeVisible();
40+
await expect(table.getByText('ID', { exact: true })).toBeVisible();
41+
await expect(table.getByText('Name', { exact: true })).toBeVisible();
42+
await expect(table.getByText('URI', { exact: true })).toBeVisible();
43+
await expect(table.getByText('Actions', { exact: true })).toBeVisible();
44+
});
45+
});
46+
47+
const routes: APISIXType['Route'][] = Array.from({ length: 11 }, (_, i) => ({
48+
id: `route_id_${i + 1}`,
49+
name: `route_name_${i + 1}`,
50+
uri: `/test_route_${i + 1}`,
51+
desc: `Description for route ${i + 1}`,
52+
methods: ['GET'],
53+
upstream: {
54+
nodes: [
55+
{
56+
host: `node_${i + 1}`,
57+
port: 80,
58+
weight: 100,
59+
},
60+
],
61+
},
62+
}));
63+
64+
test.describe('page and page_size should work correctly', () => {
65+
test.describe.configure({ mode: 'serial' });
66+
test.beforeAll(async () => {
67+
await deleteAllRoutes(e2eReq);
68+
await Promise.all(routes.map((d) => putRouteReq(e2eReq, d)));
69+
});
70+
71+
test.afterAll(async () => {
72+
await Promise.all(
73+
routes.map((d) => e2eReq.delete(`${API_ROUTES}/${d.id}`))
74+
);
75+
});
76+
77+
// Setup pagination tests with route-specific configurations
78+
const filterItemsNotInPage = async (page: Page) => {
79+
// filter the item which not in the current page
80+
// it should be random, so we need get all items in the table
81+
const itemsInPage = await page
82+
.getByRole('cell', { name: /route_name_/ })
83+
.all();
84+
const names = await Promise.all(itemsInPage.map((v) => v.textContent()));
85+
return routes.filter((d) => !names.includes(d.name));
86+
};
87+
88+
setupPaginationTests(test, {
89+
pom: routesPom,
90+
items: routes,
91+
filterItemsNotInPage,
92+
getCell: (page, item) =>
93+
page.getByRole('cell', { name: item.name }).first(),
94+
});
95+
});

playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { env } from './e2e/utils/env';
2222
* See https://playwright.dev/docs/test-configuration.
2323
*/
2424
export default defineConfig({
25-
testDir: './e2e',
25+
testDir: './e2e/tests',
2626
outputDir: './test-results',
2727
fullyParallel: true,
2828
forbidOnly: !!process.env.CI,

src/apis/hooks.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { type PageSearchType } from '@/types/schema/pageSearch';
2222
import { useSearchParams } from '@/utils/useSearchParams';
2323
import { useTablePagination } from '@/utils/useTablePagination';
2424

25+
import { getRouteListReq, getRouteReq } from './routes';
26+
2527
export const getUpstreamListQueryOptions = (props: PageSearchType) => {
2628
return queryOptions({
2729
queryKey: ['upstreams', props.page, props.page_size],
@@ -36,3 +38,24 @@ export const useUpstreamList = () => {
3638
const pagination = useTablePagination({ data, setParams, params });
3739
return { data, isLoading, refetch, pagination };
3840
};
41+
42+
export const getRouteListQueryOptions = (props: PageSearchType) => {
43+
return queryOptions({
44+
queryKey: ['routes', props.page, props.page_size],
45+
queryFn: () => getRouteListReq(req, props),
46+
});
47+
};
48+
49+
export const useRouteList = () => {
50+
const { params, setParams } = useSearchParams('/routes/');
51+
const routeQuery = useSuspenseQuery(getRouteListQueryOptions(params));
52+
const { data, isLoading, refetch } = routeQuery;
53+
const pagination = useTablePagination({ data, setParams, params });
54+
return { data, isLoading, refetch, pagination };
55+
};
56+
57+
export const getRouteQueryOptions = (id: string) =>
58+
queryOptions({
59+
queryKey: ['route', id],
60+
queryFn: () => getRouteReq(req, id),
61+
});

src/apis/routes.ts

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,43 +14,42 @@
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17-
import { queryOptions } from '@tanstack/react-query';
17+
import type { AxiosInstance } from 'axios';
1818

1919
import type { RoutePostType } from '@/components/form-slice/FormPartRoute/schema';
2020
import { API_ROUTES } from '@/config/constant';
21-
import { req } from '@/config/req';
2221
import type { APISIXType } from '@/types/schema/apisix';
2322
import type { PageSearchType } from '@/types/schema/pageSearch';
2423

25-
export const getRouteListQueryOptions = (props: PageSearchType) => {
26-
const { page, pageSize } = props;
27-
return queryOptions({
28-
queryKey: ['routes', page, pageSize],
29-
queryFn: () =>
30-
req
31-
.get<unknown, APISIXType['RespRouteList']>(API_ROUTES, {
32-
params: { page, page_size: pageSize },
33-
})
34-
.then((v) => v.data),
35-
});
36-
};
24+
export const getRouteListReq = (req: AxiosInstance, params: PageSearchType) =>
25+
req
26+
.get<undefined, APISIXType['RespRouteList']>(API_ROUTES, { params })
27+
.then((v) => v.data);
3728

38-
export const getRouteQueryOptions = (id: string) =>
39-
queryOptions({
40-
queryKey: ['route', id],
41-
queryFn: () =>
42-
req
43-
.get<unknown, APISIXType['RespRouteDetail']>(`${API_ROUTES}/${id}`)
44-
.then((v) => v.data),
45-
});
29+
export const getRouteReq = (req: AxiosInstance, id: string) =>
30+
req
31+
.get<unknown, APISIXType['RespRouteDetail']>(`${API_ROUTES}/${id}`)
32+
.then((v) => v.data);
4633

47-
export const putRouteReq = (data: APISIXType['Route']) => {
34+
export const putRouteReq = (req: AxiosInstance, data: APISIXType['Route']) => {
4835
const { id, ...rest } = data;
4936
return req.put<APISIXType['Route'], APISIXType['RespRouteDetail']>(
5037
`${API_ROUTES}/${id}`,
5138
rest
5239
);
5340
};
5441

55-
export const postRouteReq = (data: RoutePostType) =>
42+
export const postRouteReq = (req: AxiosInstance, data: RoutePostType) =>
5643
req.post<unknown, APISIXType['RespRouteDetail']>(API_ROUTES, data);
44+
45+
export const deleteAllRoutes = async (req: AxiosInstance) => {
46+
const res = await getRouteListReq(req, {
47+
page: 1,
48+
page_size: 1000,
49+
pageSize: 1000,
50+
});
51+
if (res.total === 0) return;
52+
return await Promise.all(
53+
res.list.map((d) => req.delete(`${API_ROUTES}/${d.value.id}`))
54+
);
55+
};

src/routes/routes/add.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,21 @@ import { useTranslation } from 'react-i18next';
2424
import { postRouteReq } from '@/apis/routes';
2525
import { FormSubmitBtn } from '@/components/form/Btn';
2626
import { FormPartRoute } from '@/components/form-slice/FormPartRoute';
27-
import { RoutePostSchema } from '@/components/form-slice/FormPartRoute/schema';
27+
import {
28+
RoutePostSchema,
29+
type RoutePostType,
30+
} from '@/components/form-slice/FormPartRoute/schema';
2831
import { FormTOCBox } from '@/components/form-slice/FormSection';
2932
import PageHeader from '@/components/page/PageHeader';
33+
import { req } from '@/config/req';
3034
import { pipeProduce } from '@/utils/producer';
3135

3236
const RouteAddForm = () => {
3337
const { t } = useTranslation();
3438
const router = useRouter();
3539

3640
const postRoute = useMutation({
37-
mutationFn: postRouteReq,
41+
mutationFn: (d: RoutePostType) => postRouteReq(req, pipeProduce()(d)),
3842
async onSuccess() {
3943
notifications.show({
4044
message: t('info.add.success', { name: t('routes.singular') }),
@@ -53,11 +57,7 @@ const RouteAddForm = () => {
5357

5458
return (
5559
<FormProvider {...form}>
56-
<form
57-
onSubmit={form.handleSubmit((d) =>
58-
postRoute.mutateAsync(pipeProduce()(d))
59-
)}
60-
>
60+
<form onSubmit={form.handleSubmit((d) => postRoute.mutateAsync(d))}>
6161
<FormPartRoute />
6262
<FormSubmitBtn>{t('form.btn.add')}</FormSubmitBtn>
6363
</form>

src/routes/routes/detail.$id.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,17 @@ import { FormProvider, useForm } from 'react-hook-form';
2828
import { useTranslation } from 'react-i18next';
2929
import { useBoolean } from 'react-use';
3030

31-
import { getRouteQueryOptions, putRouteReq } from '@/apis/routes';
31+
import { getRouteQueryOptions } from '@/apis/hooks';
32+
import { putRouteReq } from '@/apis/routes';
3233
import { FormSubmitBtn } from '@/components/form/Btn';
3334
import { FormPartRoute } from '@/components/form-slice/FormPartRoute';
3435
import { FormTOCBox } from '@/components/form-slice/FormSection';
3536
import { FormSectionGeneral } from '@/components/form-slice/FormSectionGeneral';
3637
import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn';
3738
import PageHeader from '@/components/page/PageHeader';
3839
import { API_ROUTES } from '@/config/constant';
39-
import { APISIX } from '@/types/schema/apisix';
40+
import { req } from '@/config/req';
41+
import { APISIX, type APISIXType } from '@/types/schema/apisix';
4042
import { pipeProduce } from '@/utils/producer';
4143

4244
type Props = {
@@ -67,7 +69,7 @@ const RouteDetailForm = (props: Props) => {
6769
}, [routeData, form, isLoading]);
6870

6971
const putRoute = useMutation({
70-
mutationFn: putRouteReq,
72+
mutationFn: (d: APISIXType['Route']) => putRouteReq(req, pipeProduce()(d)),
7173
async onSuccess() {
7274
notifications.show({
7375
message: t('info.edit.success', { name: t('routes.singular') }),
@@ -84,11 +86,7 @@ const RouteDetailForm = (props: Props) => {
8486

8587
return (
8688
<FormProvider {...form}>
87-
<form
88-
onSubmit={form.handleSubmit((d) => {
89-
putRoute.mutateAsync(pipeProduce()(d));
90-
})}
91-
>
89+
<form onSubmit={form.handleSubmit((d) => putRoute.mutateAsync(d))}>
9290
<FormSectionGeneral />
9391
<FormPartRoute />
9492
{!readOnly && (

src/routes/routes/index.tsx

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,24 @@
1616
*/
1717
import type { ProColumns } from '@ant-design/pro-components';
1818
import { ProTable } from '@ant-design/pro-components';
19-
import { useSuspenseQuery } from '@tanstack/react-query';
2019
import { createFileRoute } from '@tanstack/react-router';
21-
import { useEffect, useMemo } from 'react';
20+
import { useMemo } from 'react';
2221
import { useTranslation } from 'react-i18next';
2322

24-
import { getRouteListQueryOptions } from '@/apis/routes';
23+
import { getRouteListQueryOptions, useRouteList } from '@/apis/hooks';
2524
import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn';
2625
import PageHeader from '@/components/page/PageHeader';
27-
import { ToAddPageBtn,ToDetailPageBtn } from '@/components/page/ToAddPageBtn';
26+
import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn';
2827
import { AntdConfigProvider } from '@/config/antdConfigProvider';
2928
import { API_ROUTES } from '@/config/constant';
3029
import { queryClient } from '@/config/global';
3130
import type { APISIXType } from '@/types/schema/apisix';
3231
import { pageSearchSchema } from '@/types/schema/pageSearch';
33-
import { usePagination } from '@/utils/usePagination';
3432

3533
const RouteList = () => {
36-
const { pagination, handlePageChange, updateTotal } = usePagination({
37-
queryKey: 'routes',
38-
});
39-
40-
const query = useSuspenseQuery(getRouteListQueryOptions(pagination));
41-
const { data, isLoading, refetch } = query;
34+
const { data, isLoading, refetch, pagination } = useRouteList();
4235
const { t } = useTranslation();
4336

44-
useEffect(() => {
45-
if (data?.total) {
46-
updateTotal(data.total);
47-
}
48-
}, [data?.total, updateTotal]);
49-
5037
const columns = useMemo<ProColumns<APISIXType['RespRouteItem']>[]>(() => {
5138
return [
5239
{
@@ -105,13 +92,7 @@ const RouteList = () => {
10592
loading={isLoading}
10693
search={false}
10794
options={false}
108-
pagination={{
109-
current: pagination.page,
110-
pageSize: pagination.pageSize,
111-
total: pagination.total,
112-
showSizeChanger: true,
113-
onChange: handlePageChange,
114-
}}
95+
pagination={pagination}
11596
cardProps={{ bodyStyle: { padding: 0 } }}
11697
toolbar={{
11798
menu: {

0 commit comments

Comments
 (0)