This is an automated email from the ASF dual-hosted git repository.
young pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git
The following commit(s) were added to refs/heads/master by this push:
new 70712bd33 test: stream_routes in services (#3113)
70712bd33 is described below
commit 70712bd33f55f7979d4cb73a898e9778e0fbfe8b
Author: YYYoung <[email protected]>
AuthorDate: Mon Jun 16 15:38:00 2025 +0800
test: stream_routes in services (#3113)
* feat(services/routes): filter by `service_id`
* feat(services): filter stream_routes by service_id
* fix: type
* test: routes in services
* test: stream_routes in services
* Revert "test: stream_routes in services"
This reverts commit 77d7803250e0365f85dc0599f9d3fbaf8a1b10f5.
* test: stream_routes in services
* chore: rm stream_routes pom
* feat: add only show routes with service_id case
* test: add not show other service route case
* feat: add services stream_routes pom
* test: stream_routes in services
* fix: use exact match
* chore: rollback useless change
---
e2e/pom/services.ts | 41 +++++
e2e/tests/services.stream_routes.crud.spec.ts | 249 +++++++++++++++++++++++++
e2e/tests/services.stream_routes.list.spec.ts | 253 ++++++++++++++++++++++++++
3 files changed, 543 insertions(+)
diff --git a/e2e/pom/services.ts b/e2e/pom/services.ts
index 7ac701cf6..60334c78f 100644
--- a/e2e/pom/services.ts
+++ b/e2e/pom/services.ts
@@ -29,6 +29,11 @@ const locator = {
page.getByRole('tab', { name: 'Routes', exact: true }),
getAddRouteBtn: (page: Page) =>
page.getByRole('button', { name: 'Add Route', exact: true }),
+ // Service stream routes locators
+ getServiceStreamRoutesTab: (page: Page) =>
+ page.getByRole('tab', { name: 'Stream Routes', exact: true }),
+ getAddStreamRouteBtn: (page: Page) =>
+ page.getByRole('button', { name: 'Add Stream Route', exact: true }),
};
const assert = {
@@ -81,6 +86,36 @@ const assert = {
const title = page.getByRole('heading', { name: 'Route Detail' });
await expect(title).toBeVisible();
},
+ // Service stream routes assertions
+ isServiceStreamRoutesPage: async (page: Page) => {
+ await expect(page).toHaveURL(
+ (url) =>
+ url.pathname.includes('/services/detail') &&
+ url.pathname.includes('/stream_routes')
+ );
+ // Wait for page to load completely
+ await page.waitForLoadState('networkidle');
+ const title = page.getByRole('heading', { name: 'Stream Routes' });
+ await expect(title).toBeVisible();
+ },
+ isServiceStreamRouteAddPage: async (page: Page) => {
+ await expect(page).toHaveURL(
+ (url) =>
+ url.pathname.includes('/services/detail') &&
+ url.pathname.includes('/stream_routes/add')
+ );
+ const title = page.getByRole('heading', { name: 'Add Stream Route' });
+ await expect(title).toBeVisible();
+ },
+ isServiceStreamRouteDetailPage: async (page: Page) => {
+ await expect(page).toHaveURL(
+ (url) =>
+ url.pathname.includes('/services/detail') &&
+ url.pathname.includes('/stream_routes/detail')
+ );
+ const title = page.getByRole('heading', { name: 'Stream Route Detail' });
+ await expect(title).toBeVisible();
+ },
};
const goto = {
@@ -92,6 +127,12 @@ const goto = {
uiGoto(page, '/services/detail/$id/routes/add', {
id: serviceId,
}),
+ toServiceStreamRoutes: (page: Page, serviceId: string) =>
+ uiGoto(page, '/services/detail/$id/stream_routes', { id: serviceId }),
+ toServiceStreamRouteAdd: (page: Page, serviceId: string) =>
+ uiGoto(page, '/services/detail/$id/stream_routes/add', {
+ id: serviceId,
+ }),
};
export const servicesPom = {
diff --git a/e2e/tests/services.stream_routes.crud.spec.ts
b/e2e/tests/services.stream_routes.crud.spec.ts
new file mode 100644
index 000000000..a53f641b3
--- /dev/null
+++ b/e2e/tests/services.stream_routes.crud.spec.ts
@@ -0,0 +1,249 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { servicesPom } from '@e2e/pom/services';
+import { randomId } from '@e2e/utils/common';
+import { e2eReq } from '@e2e/utils/req';
+import { test } from '@e2e/utils/test';
+import { uiHasToastMsg } from '@e2e/utils/ui';
+import { expect } from '@playwright/test';
+
+import { deleteAllServices, postServiceReq } from '@/apis/services';
+import { deleteAllStreamRoutes } from '@/apis/stream_routes';
+
+const serviceName = randomId('test-service');
+const streamRouteServerAddr = '127.0.0.1';
+const streamRouteServerPort = 8080;
+const updatedStreamRouteServerAddr = '127.0.0.2';
+const updatedStreamRouteServerPort = 8081;
+
+let testServiceId: string;
+
+test.beforeAll(async () => {
+ await deleteAllStreamRoutes(e2eReq);
+ await deleteAllServices(e2eReq);
+
+ // Create a test service for testing service stream routes
+ const serviceResponse = await postServiceReq(e2eReq, {
+ name: serviceName,
+ desc: 'Test service for stream route CRUD testing',
+ });
+
+ testServiceId = serviceResponse.data.value.id;
+});
+
+test.afterAll(async () => {
+ await deleteAllStreamRoutes(e2eReq);
+ await deleteAllServices(e2eReq);
+});
+
+test('should CRUD stream route under service', async ({ page }) => {
+ // Navigate to service detail page
+ await servicesPom.toIndex(page);
+ await servicesPom.isIndexPage(page);
+
+ // Click on the service to go to detail page
+ await page
+ .getByRole('row', { name: serviceName })
+ .getByRole('button', { name: 'View' })
+ .click();
+ await servicesPom.isDetailPage(page);
+
+ // Navigate to Stream Routes tab
+ await servicesPom.getServiceStreamRoutesTab(page).click();
+ await servicesPom.isServiceStreamRoutesPage(page);
+
+ await servicesPom.getAddStreamRouteBtn(page).click();
+ await servicesPom.isServiceStreamRouteAddPage(page);
+
+ await test.step('can submit without any fields (no required fields)', async
() => {
+ // Verify service_id is pre-filled and disabled (since it's read-only in
service context)
+ const serviceIdField = page.getByLabel('Service ID', { exact: true });
+ await expect(serviceIdField).toHaveValue(testServiceId);
+ await expect(serviceIdField).toBeDisabled();
+
+ // Submit the form without filling any other fields
+ await servicesPom.getAddBtn(page).click();
+ await uiHasToastMsg(page, {
+ hasText: 'Add Stream Route Successfully',
+ });
+ });
+
+ await test.step('auto navigate to stream route detail page', async () => {
+ await servicesPom.isServiceStreamRouteDetailPage(page);
+
+ // Verify the stream route details
+ // Verify ID exists
+ const ID = page.getByRole('textbox', { name: 'ID', exact: true });
+ await expect(ID).toBeVisible();
+ await expect(ID).toBeDisabled();
+
+ // Verify service_id is still pre-filled and disabled
+ const serviceIdField = page.getByLabel('Service ID', { exact: true });
+ await expect(serviceIdField).toHaveValue(testServiceId);
+ await expect(serviceIdField).toBeDisabled();
+
+ // Verify default values for server address and port (should be empty
initially)
+ const serverAddrField = page.getByLabel('Server Address', { exact: true });
+ const serverPortField = page.getByLabel('Server Port', { exact: true });
+
+ // These fields might be empty or have default values
+ await expect(serverAddrField).toBeVisible();
+ await expect(serverPortField).toBeVisible();
+ });
+
+ await test.step('edit and update stream route with some fields', async () =>
{
+ // Click the Edit button in the detail page
+ await page.getByRole('button', { name: 'Edit' }).click();
+
+ // Verify we're in edit mode - fields should be editable now
+ const serverAddrField = page.getByLabel('Server Address', { exact: true });
+ await expect(serverAddrField).toBeEnabled();
+
+ // Service ID should still be disabled even in edit mode
+ const serviceIdField = page.getByLabel('Service ID', { exact: true });
+ await expect(serviceIdField).toBeDisabled();
+
+ // Fill in some fields
+ await serverAddrField.fill(streamRouteServerAddr);
+
+ const serverPortField = page.getByLabel('Server Port', { exact: true });
+ await serverPortField.fill(streamRouteServerPort.toString());
+
+ // Click the Save button to save changes
+ const saveBtn = page.getByRole('button', { name: 'Save' });
+ await saveBtn.click();
+
+ // Verify the update was successful
+ await uiHasToastMsg(page, {
+ hasText: 'success',
+ });
+
+ // Verify we're back in detail view mode
+ await servicesPom.isServiceStreamRouteDetailPage(page);
+
+ // Verify the updated fields
+ await expect(
+ page.getByLabel('Server Address', { exact: true })
+ ).toHaveValue(streamRouteServerAddr);
+ await expect(page.getByLabel('Server Port', { exact: true })).toHaveValue(
+ streamRouteServerPort.toString()
+ );
+ });
+
+ await test.step('edit again and update with different values', async () => {
+ // Click the Edit button again
+ await page.getByRole('button', { name: 'Edit' }).click();
+
+ // Update with different values
+ const serverAddrField = page.getByLabel('Server Address', { exact: true });
+ await serverAddrField.fill(updatedStreamRouteServerAddr);
+
+ const serverPortField = page.getByLabel('Server Port', { exact: true });
+ await serverPortField.fill(updatedStreamRouteServerPort.toString());
+
+ // Click the Save button
+ const saveBtn = page.getByRole('button', { name: 'Save' });
+ await saveBtn.click();
+
+ // Verify the update was successful
+ await uiHasToastMsg(page, {
+ hasText: 'success',
+ });
+
+ // Verify the updated values
+ await expect(
+ page.getByLabel('Server Address', { exact: true })
+ ).toHaveValue(updatedStreamRouteServerAddr);
+ await expect(page.getByLabel('Server Port', { exact: true })).toHaveValue(
+ updatedStreamRouteServerPort.toString()
+ );
+ });
+
+ await test.step('stream route should exist in service stream routes list',
async () => {
+ // Navigate back to service stream routes list
+ await servicesPom.toServiceStreamRoutes(page, testServiceId);
+ await servicesPom.isServiceStreamRoutesPage(page);
+
+ // Verify the stream route appears in the list with updated values
+ await expect(
+ page.getByRole('cell', { name: updatedStreamRouteServerAddr })
+ ).toBeVisible();
+ await expect(
+ page.getByRole('cell', { name: updatedStreamRouteServerPort.toString() })
+ ).toBeVisible();
+
+ // Click on the stream route to go to the detail page
+ await page
+ .getByRole('row', { name: updatedStreamRouteServerAddr })
+ .getByRole('button', { name: 'View' })
+ .click();
+ await servicesPom.isServiceStreamRouteDetailPage(page);
+ });
+
+ await test.step('delete stream route in detail page', async () => {
+ // We're already on the detail page from the previous step
+
+ // Delete the stream route
+ await page.getByRole('button', { name: 'Delete' }).click();
+
+ await page
+ .getByRole('dialog', { name: 'Delete Stream Route' })
+ .getByRole('button', { name: 'Delete' })
+ .click();
+
+ // Will redirect to service stream routes page
+ await servicesPom.isServiceStreamRoutesPage(page);
+ await uiHasToastMsg(page, {
+ hasText: 'Delete Stream Route Successfully',
+ });
+
+ // Verify the stream route is no longer in the list
+ await expect(
+ page.getByRole('cell', { name: updatedStreamRouteServerAddr })
+ ).toBeHidden();
+ });
+
+ await test.step('create another stream route with minimal fields', async ()
=> {
+ // Add another stream route to test creation with minimal data
+ await servicesPom.getAddStreamRouteBtn(page).click();
+ await servicesPom.isServiceStreamRouteAddPage(page);
+
+ // Just fill server address this time
+ const serverAddrField = page.getByLabel('Server Address', { exact: true });
+ await serverAddrField.fill('192.168.1.1');
+
+ // Submit the form
+ await servicesPom.getAddBtn(page).click();
+ await uiHasToastMsg(page, {
+ hasText: 'Add Stream Route Successfully',
+ });
+
+ // Verify we're on the detail page
+ await servicesPom.isServiceStreamRouteDetailPage(page);
+ await expect(
+ page.getByLabel('Server Address', { exact: true })
+ ).toHaveValue('192.168.1.1');
+
+ // Clean up - delete this stream route too
+ await page.getByRole('button', { name: 'Delete' }).click();
+ await page
+ .getByRole('dialog', { name: 'Delete Stream Route' })
+ .getByRole('button', { name: 'Delete' })
+ .click();
+ await servicesPom.isServiceStreamRoutesPage(page);
+ });
+});
diff --git a/e2e/tests/services.stream_routes.list.spec.ts
b/e2e/tests/services.stream_routes.list.spec.ts
new file mode 100644
index 000000000..07044fbed
--- /dev/null
+++ b/e2e/tests/services.stream_routes.list.spec.ts
@@ -0,0 +1,253 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { servicesPom } from '@e2e/pom/services';
+import { randomId } from '@e2e/utils/common';
+import { e2eReq } from '@e2e/utils/req';
+import { test } from '@e2e/utils/test';
+import { uiGoto } from '@e2e/utils/ui';
+import { expect } from '@playwright/test';
+
+import { deleteAllServices, postServiceReq } from '@/apis/services';
+import {
+ deleteAllStreamRoutes,
+ postStreamRouteReq,
+} from '@/apis/stream_routes';
+
+const serviceName = randomId('test-service');
+const anotherServiceName = randomId('another-service');
+const streamRoutes = [
+ {
+ server_addr: '127.0.0.1',
+ server_port: 8080,
+ },
+ {
+ server_addr: '127.0.0.2',
+ server_port: 8081,
+ },
+ {
+ server_addr: '127.0.0.3',
+ server_port: 8082,
+ },
+];
+
+// Stream route that uses upstream directly instead of service_id
+const upstreamStreamRoute = {
+ server_addr: '127.0.0.40',
+ server_port: 9090,
+ upstream: {
+ nodes: [{ host: 'example.com', port: 80, weight: 100 }],
+ },
+};
+
+// Stream route that belongs to another service
+const anotherServiceStreamRoute = {
+ server_addr: '127.0.0.20',
+ server_port: 9091,
+};
+
+let testServiceId: string;
+let anotherServiceId: string;
+const createdStreamRoutes: string[] = [];
+
+test.beforeAll(async () => {
+ await deleteAllStreamRoutes(e2eReq);
+ await deleteAllServices(e2eReq);
+
+ // Create a test service for testing service stream routes
+ const serviceResponse = await postServiceReq(e2eReq, {
+ name: serviceName,
+ desc: 'Test service for stream route listing',
+ });
+
+ testServiceId = serviceResponse.data.value.id;
+
+ // Create another service
+ const anotherServiceResponse = await postServiceReq(e2eReq, {
+ name: anotherServiceName,
+ desc: 'Another test service for stream route isolation testing',
+ });
+
+ anotherServiceId = anotherServiceResponse.data.value.id;
+
+ // Create test stream routes under the service
+ for (const streamRoute of streamRoutes) {
+ const streamRouteResponse = await postStreamRouteReq(e2eReq, {
+ server_addr: streamRoute.server_addr,
+ server_port: streamRoute.server_port,
+ service_id: testServiceId,
+ });
+ createdStreamRoutes.push(streamRouteResponse.data.value.id);
+ }
+
+ // Create a stream route that uses upstream directly instead of service_id
+ await postStreamRouteReq(e2eReq, upstreamStreamRoute);
+
+ // Create a stream route under another service
+ await postStreamRouteReq(e2eReq, {
+ ...anotherServiceStreamRoute,
+ service_id: anotherServiceId,
+ });
+});
+
+test.afterAll(async () => {
+ await deleteAllStreamRoutes(e2eReq);
+ await deleteAllServices(e2eReq);
+});
+
+test('should only show stream routes with current service_id', async ({
+ page,
+}) => {
+ await test.step('should only show stream routes with current service_id',
async () => {
+ await servicesPom.toIndex(page);
+ await servicesPom.isIndexPage(page);
+
+ await page
+ .getByRole('row', { name: serviceName })
+ .getByRole('button', { name: 'View' })
+ .click();
+ await servicesPom.isDetailPage(page);
+
+ await servicesPom.getServiceStreamRoutesTab(page).click();
+ await servicesPom.isServiceStreamRoutesPage(page);
+
+ // Stream routes from another service should not be visible
+ await expect(
+ page.getByRole('cell', { name: anotherServiceStreamRoute.server_addr })
+ ).toBeHidden();
+ // Upstream stream route (without service_id) should not be visible
+ await expect(
+ page.getByRole('cell', { name: upstreamStreamRoute.server_addr })
+ ).toBeHidden();
+ // Only stream routes belonging to current service should be visible
+ for (const streamRoute of streamRoutes) {
+ await expect(
+ page.getByRole('cell', { name: streamRoute.server_addr })
+ ).toBeVisible();
+ }
+ });
+
+ await test.step('without service_id stream routes should still exist in the
stream routes list', async () => {
+ await uiGoto(page, '/stream_routes');
+ await expect(page).toHaveURL((url) =>
+ url.pathname.endsWith('/stream_routes')
+ );
+ const title = page.getByRole('heading', { name: 'Stream Routes' });
+ await expect(title).toBeVisible();
+
+ // All stream routes should be visible in the global stream routes list
+ await expect(
+ page.getByRole('cell', { name: upstreamStreamRoute.server_addr })
+ ).toBeVisible();
+ await expect(
+ page.getByRole('cell', { name: anotherServiceStreamRoute.server_addr })
+ ).toBeVisible();
+ for (const streamRoute of streamRoutes) {
+ await expect(
+ page.getByRole('cell', { name: streamRoute.server_addr, exact: true })
+ ).toBeVisible();
+ }
+ });
+});
+
+test('should display stream routes list under service', async ({ page }) => {
+ // Navigate to service detail page
+ await servicesPom.toIndex(page);
+ await servicesPom.isIndexPage(page);
+
+ // Click on the service to go to detail page
+ await page
+ .getByRole('row', { name: serviceName })
+ .getByRole('button', { name: 'View' })
+ .click();
+ await servicesPom.isDetailPage(page);
+
+ // Navigate to Stream Routes tab
+ await servicesPom.getServiceStreamRoutesTab(page).click();
+ await servicesPom.isServiceStreamRoutesPage(page);
+
+ await test.step('should display all stream routes under service', async ()
=> {
+ // Verify all created stream routes are displayed
+ for (const streamRoute of streamRoutes) {
+ await expect(
+ page.getByRole('cell', { name: streamRoute.server_addr })
+ ).toBeVisible();
+ await expect(
+ page.getByRole('cell', { name: streamRoute.server_port.toString() })
+ ).toBeVisible();
+ }
+ });
+
+ await test.step('should have correct table headers', async () => {
+ await expect(page.getByRole('columnheader', { name: 'ID' })).toBeVisible();
+ await expect(
+ page.getByRole('columnheader', { name: 'Server Address' })
+ ).toBeVisible();
+ await expect(
+ page.getByRole('columnheader', { name: 'Server Port' })
+ ).toBeVisible();
+ await expect(
+ page.getByRole('columnheader', { name: 'Actions' })
+ ).toBeVisible();
+ });
+
+ await test.step('should be able to navigate to stream route detail', async
() => {
+ // Click on the first stream route's View button
+ await page
+ .getByRole('row', { name: streamRoutes[0].server_addr })
+ .getByRole('button', { name: 'View' })
+ .click();
+
+ await servicesPom.isServiceStreamRouteDetailPage(page);
+
+ // Verify we're on the correct stream route detail page
+ const serverAddrField = page.getByLabel('Server Address', { exact: true });
+ await expect(serverAddrField).toHaveValue(streamRoutes[0].server_addr);
+
+ // Verify service_id is correct
+ const serviceIdField = page.getByLabel('Service ID', { exact: true });
+ await expect(serviceIdField).toHaveValue(testServiceId);
+ });
+
+ await test.step('should have Add Stream Route button', async () => {
+ // Navigate back to service stream routes list
+ await servicesPom.toServiceStreamRoutes(page, testServiceId);
+ await servicesPom.isServiceStreamRoutesPage(page);
+
+ // Verify Add Stream Route button exists and is clickable
+ const addStreamRouteBtn = servicesPom.getAddStreamRouteBtn(page);
+ await expect(addStreamRouteBtn).toBeVisible();
+
+ await addStreamRouteBtn.click();
+ await servicesPom.isServiceStreamRouteAddPage(page);
+
+ // Verify service_id is pre-filled
+ const serviceIdField = page.getByLabel('Service ID', { exact: true });
+ await expect(serviceIdField).toHaveValue(testServiceId);
+ await expect(serviceIdField).toBeDisabled();
+ });
+
+ await test.step('should show correct stream route count', async () => {
+ // Navigate back to service stream routes list
+ await servicesPom.toServiceStreamRoutes(page, testServiceId);
+ await servicesPom.isServiceStreamRoutesPage(page);
+
+ // Check that all 3 stream routes are displayed in the table
+ const tableRows = page.locator('tbody tr');
+ await expect(tableRows).toHaveCount(streamRoutes.length);
+ });
+});
+