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 <isk...@outlook.com>
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);
+  });
+});
+

Reply via email to