Skip to content

Commit 70b8c7d

Browse files
committed
test: add stream route E2E tests with detail page flows
- Add stream_routes.crud-required-fields.spec.ts for minimal CRUD - Add stream_routes.crud-all-fields.spec.ts for extended CRUD with optional fields - Add stream_routes.list.spec.ts for pagination testing - Create e2e/utils/ui/stream_routes.ts helper for form interactions - Update stream_routes POM with isIndexPage, isAddPage, isDetailPage assertions All CRUD operations now use detail page for edit/delete workflows instead of relying on non-existent list-page Edit buttons. Tests verify creation lands on detail page, editing works via detail toolbar, and deletion happens through detail dialog with proper list verification. Fixes #3085
1 parent 2d7e194 commit 70b8c7d

File tree

5 files changed

+656
-1
lines changed

5 files changed

+656
-1
lines changed

e2e/pom/stream_routes.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,46 @@
1515
* limitations under the License.
1616
*/
1717
import { uiGoto } from '@e2e/utils/ui';
18-
import type { Page } from '@playwright/test';
18+
import { expect, type Page } from '@playwright/test';
19+
20+
const locator = {
21+
getAddBtn: (page: Page) =>
22+
page.getByRole('link', { name: 'Add Stream Route' }),
23+
};
24+
25+
const assert = {
26+
isIndexPage: async (page: Page) => {
27+
await expect(page).toHaveURL((url) =>
28+
url.pathname.endsWith('/stream_routes')
29+
);
30+
const title = page.getByRole('heading', { name: 'Stream Routes' });
31+
await expect(title).toBeVisible();
32+
},
33+
isAddPage: async (page: Page) => {
34+
await expect(page).toHaveURL((url) =>
35+
url.pathname.endsWith('/stream_routes/add')
36+
);
37+
const title = page.getByRole('heading', { name: 'Add Stream Route' });
38+
await expect(title).toBeVisible();
39+
},
40+
isDetailPage: async (page: Page) => {
41+
await expect(page).toHaveURL((url) =>
42+
url.pathname.includes('/stream_routes/detail')
43+
);
44+
const title = page.getByRole('heading', {
45+
name: 'Stream Route Detail',
46+
});
47+
await expect(title).toBeVisible();
48+
},
49+
};
1950

2051
const goto = {
2152
toIndex: (page: Page) => uiGoto(page, '/stream_routes'),
53+
toAdd: (page: Page) => uiGoto(page, '/stream_routes/add'),
2254
};
2355

2456
export const streamRoutesPom = {
57+
...locator,
58+
...assert,
2559
...goto,
2660
};
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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 { streamRoutesPom } from '@e2e/pom/stream_routes';
18+
import { e2eReq } from '@e2e/utils/req';
19+
import { test } from '@e2e/utils/test';
20+
import {
21+
uiCheckStreamRouteRequiredFields,
22+
uiFillStreamRouteRequiredFields,
23+
} from '@e2e/utils/ui/stream_routes';
24+
import { expect } from '@playwright/test';
25+
26+
import { deleteAllStreamRoutes } from '@/apis/stream_routes';
27+
28+
test.beforeAll('clean up', async () => {
29+
await deleteAllStreamRoutes(e2eReq);
30+
});
31+
32+
test('CRUD stream route with all fields', async ({ page }) => {
33+
// Navigate to stream routes page
34+
await streamRoutesPom.toIndex(page);
35+
await expect(page.getByRole('heading', { name: 'Stream Routes' })).toBeVisible();
36+
37+
// Navigate to add page
38+
await streamRoutesPom.toAdd(page);
39+
await expect(page.getByRole('heading', { name: 'Add Stream Route' })).toBeVisible();
40+
41+
const streamRouteData = {
42+
server_addr: '127.0.0.10',
43+
server_port: 9100,
44+
remote_addr: '192.168.10.0/24',
45+
sni: 'edge.example.com',
46+
desc: 'Stream route with optional fields',
47+
labels: {
48+
env: 'production',
49+
version: '2.0',
50+
region: 'us-west',
51+
},
52+
} as const;
53+
54+
await uiFillStreamRouteRequiredFields(page, streamRouteData);
55+
56+
// Submit and land on detail page
57+
await page.getByRole('button', { name: 'Add', exact: true }).click();
58+
await streamRoutesPom.isDetailPage(page);
59+
60+
// Verify initial values in detail view
61+
await uiCheckStreamRouteRequiredFields(page, streamRouteData);
62+
63+
// Enter edit mode from detail page
64+
await page.getByRole('button', { name: 'Edit' }).click();
65+
await expect(page.getByRole('heading', { name: 'Edit Stream Route' })).toBeVisible();
66+
await uiCheckStreamRouteRequiredFields(page, streamRouteData);
67+
68+
// Edit fields - update description, add a label, and modify server settings
69+
const updatedData = {
70+
server_addr: '127.0.0.20',
71+
server_port: 9200,
72+
remote_addr: '10.10.0.0/16',
73+
sni: 'edge-updated.example.com',
74+
desc: 'Updated stream route with optional fields',
75+
labels: {
76+
...streamRouteData.labels,
77+
updated: 'true',
78+
},
79+
} as const;
80+
81+
await page
82+
.getByLabel('Server Address', { exact: true })
83+
.fill(updatedData.server_addr);
84+
await page
85+
.getByLabel('Server Port', { exact: true })
86+
.fill(updatedData.server_port.toString());
87+
await page.getByLabel('Remote Address').fill(updatedData.remote_addr);
88+
await page.getByLabel('SNI').fill(updatedData.sni);
89+
await page.getByLabel('Description').first().fill(updatedData.desc);
90+
91+
const labelsField = page.getByPlaceholder('Input text like `key:value`,').first();
92+
await labelsField.fill('updated:true');
93+
await labelsField.press('Enter');
94+
95+
// Submit edit and return to detail page
96+
await page.getByRole('button', { name: 'Save', exact: true }).click();
97+
await streamRoutesPom.isDetailPage(page);
98+
99+
// Verify updated values from detail view
100+
await uiCheckStreamRouteRequiredFields(page, updatedData);
101+
102+
// Navigate back to index and locate the updated row
103+
await streamRoutesPom.toIndex(page);
104+
const updatedRow = page
105+
.getByRole('row')
106+
.filter({ hasText: updatedData.server_addr });
107+
await expect(updatedRow).toBeVisible();
108+
109+
// View detail page from the list to double-check values
110+
await updatedRow.getByRole('button', { name: 'View' }).click();
111+
await streamRoutesPom.isDetailPage(page);
112+
await uiCheckStreamRouteRequiredFields(page, updatedData);
113+
114+
// Delete from detail page
115+
await page.getByRole('button', { name: 'Delete' }).click();
116+
await page.getByRole('dialog').getByRole('button', { name: 'Delete' }).click();
117+
118+
await streamRoutesPom.isIndexPage(page);
119+
await expect(
120+
page.getByRole('row').filter({ hasText: updatedData.server_addr })
121+
).toHaveCount(0);
122+
});
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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 { streamRoutesPom } from '@e2e/pom/stream_routes';
18+
import { e2eReq } from '@e2e/utils/req';
19+
import { test } from '@e2e/utils/test';
20+
import {
21+
uiCheckStreamRouteRequiredFields,
22+
uiFillStreamRouteRequiredFields,
23+
} from '@e2e/utils/ui/stream_routes';
24+
import { expect } from '@playwright/test';
25+
26+
import { deleteAllStreamRoutes } from '@/apis/stream_routes';
27+
28+
test.beforeAll('clean up', async () => {
29+
await deleteAllStreamRoutes(e2eReq);
30+
});
31+
32+
test('CRUD stream route with required fields', async ({ page }) => {
33+
// Navigate to stream routes page
34+
await streamRoutesPom.toIndex(page);
35+
await expect(page.getByRole('heading', { name: 'Stream Routes' })).toBeVisible();
36+
37+
// Navigate to add page
38+
await streamRoutesPom.toAdd(page);
39+
await expect(page.getByRole('heading', { name: 'Add Stream Route' })).toBeVisible();
40+
41+
const streamRouteData = {
42+
server_addr: '127.0.0.1',
43+
server_port: 9000,
44+
};
45+
46+
// Fill required fields
47+
await uiFillStreamRouteRequiredFields(page, streamRouteData);
48+
49+
// Submit and land on detail page
50+
await page.getByRole('button', { name: 'Add', exact: true }).click();
51+
await streamRoutesPom.isDetailPage(page);
52+
53+
// Verify created values in detail view
54+
await uiCheckStreamRouteRequiredFields(page, streamRouteData);
55+
56+
// Enter edit mode from detail page
57+
await page.getByRole('button', { name: 'Edit' }).click();
58+
await expect(page.getByRole('heading', { name: 'Edit Stream Route' })).toBeVisible();
59+
60+
// Verify pre-filled values
61+
await uiCheckStreamRouteRequiredFields(page, streamRouteData);
62+
63+
// Edit fields - add description and labels
64+
const updatedData = {
65+
...streamRouteData,
66+
desc: 'Updated stream route description',
67+
labels: {
68+
env: 'test',
69+
version: '1.0',
70+
},
71+
};
72+
73+
await uiFillStreamRouteRequiredFields(page, {
74+
desc: updatedData.desc,
75+
labels: updatedData.labels,
76+
});
77+
78+
// Submit edit and return to detail page
79+
await page.getByRole('button', { name: 'Save', exact: true }).click();
80+
await streamRoutesPom.isDetailPage(page);
81+
82+
// Verify updated values on detail page
83+
await uiCheckStreamRouteRequiredFields(page, updatedData);
84+
85+
// Navigate back to index and ensure the row exists
86+
await streamRoutesPom.toIndex(page);
87+
const row = page.getByRole('row').filter({ hasText: streamRouteData.server_addr });
88+
await expect(row).toBeVisible();
89+
90+
// View detail page from the list
91+
await row.getByRole('button', { name: 'View' }).click();
92+
await streamRoutesPom.isDetailPage(page);
93+
await uiCheckStreamRouteRequiredFields(page, updatedData);
94+
95+
// Delete from the detail page
96+
await page.getByRole('button', { name: 'Delete' }).click();
97+
await page.getByRole('dialog').getByRole('button', { name: 'Delete' }).click();
98+
99+
await streamRoutesPom.isIndexPage(page);
100+
await expect(
101+
page.getByRole('row').filter({ hasText: streamRouteData.server_addr })
102+
).toHaveCount(0);
103+
});
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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 { streamRoutesPom } from '@e2e/pom/stream_routes';
18+
import { setupPaginationTests } from '@e2e/utils/pagination-test-helper';
19+
import { e2eReq } from '@e2e/utils/req';
20+
import { test } from '@e2e/utils/test';
21+
import { expect, type Page } from '@playwright/test';
22+
23+
import { deleteAllStreamRoutes } from '@/apis/stream_routes';
24+
import { API_STREAM_ROUTES } from '@/config/constant';
25+
import type { APISIXType } from '@/types/schema/apisix';
26+
27+
test('should navigate to stream routes page', async ({ page }) => {
28+
await test.step('navigate to stream routes page', async () => {
29+
await streamRoutesPom.toIndex(page);
30+
await streamRoutesPom.isIndexPage(page);
31+
});
32+
33+
await test.step('verify stream routes page components', async () => {
34+
// list table exists
35+
const table = page.getByRole('table');
36+
await expect(table).toBeVisible();
37+
await expect(table.getByText('ID', { exact: true })).toBeVisible();
38+
await expect(
39+
table.getByText('Server Address', { exact: true })
40+
).toBeVisible();
41+
await expect(
42+
table.getByText('Server Port', { exact: true })
43+
).toBeVisible();
44+
await expect(table.getByText('Actions', { exact: true })).toBeVisible();
45+
});
46+
});
47+
48+
const streamRoutes: APISIXType['StreamRoute'][] = Array.from(
49+
{ length: 11 },
50+
(_, i) => ({
51+
id: `stream_route_id_${i + 1}`,
52+
server_addr: `127.0.0.${i + 1}`,
53+
server_port: 9000 + i,
54+
create_time: Date.now(),
55+
update_time: Date.now(),
56+
})
57+
);
58+
59+
test.describe('page and page_size should work correctly', () => {
60+
test.describe.configure({ mode: 'serial' });
61+
test.beforeAll(async () => {
62+
await deleteAllStreamRoutes(e2eReq);
63+
await Promise.all(
64+
streamRoutes.map((d) => {
65+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
66+
const { id, create_time: _createTime, update_time: _updateTime, ...rest } = d;
67+
return e2eReq.put(`${API_STREAM_ROUTES}/${id}`, rest);
68+
})
69+
);
70+
});
71+
72+
test.afterAll(async () => {
73+
await Promise.all(
74+
streamRoutes.map((d) => e2eReq.delete(`${API_STREAM_ROUTES}/${d.id}`))
75+
);
76+
});
77+
78+
// Setup pagination tests with stream route-specific configurations
79+
const filterItemsNotInPage = async (page: Page) => {
80+
// filter the item which not in the current page
81+
// it should be random, so we need get all items in the table
82+
const itemsInPage = await page
83+
.getByRole('cell', { name: /stream_route_id_/ })
84+
.all();
85+
const ids = await Promise.all(itemsInPage.map((v) => v.textContent()));
86+
return streamRoutes.filter((d) => !ids.includes(d.id));
87+
};
88+
89+
setupPaginationTests(test, {
90+
pom: streamRoutesPom,
91+
items: streamRoutes,
92+
filterItemsNotInPage,
93+
getCell: (page, item) =>
94+
page.getByRole('cell', { name: item.id }).first(),
95+
});
96+
});
97+

0 commit comments

Comments
 (0)