Skip to content

Commit a2ee27a

Browse files
authored
test(e2e): add comprehensive service CRUD tests and UI utility functions (#3258)
1 parent 05e30a5 commit a2ee27a

12 files changed

+625
-7
lines changed

e2e/tests/plugin_metadata.crud-all-fields.spec.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,26 @@ const deletePluginMetadata = async (req: typeof e2eReq, name: string) => {
3434
});
3535
};
3636
const getMonacoEditorValue = async (editPluginDialog: Locator) => {
37-
let editorValue = '';
3837
const textarea = editPluginDialog.locator('textarea');
38+
39+
// Wait for Monaco editor to be fully loaded with content (increased timeout for CI)
40+
await textarea.waitFor({ state: 'attached', timeout: 10000 });
41+
42+
let editorValue = '';
43+
44+
// Try to get value from textarea first
3945
if (await textarea.count() > 0) {
4046
editorValue = await textarea.inputValue();
4147
}
48+
49+
// Fallback to reading view-lines if textarea value is incomplete
4250
if (!editorValue || editorValue.trim() === '{') {
51+
// Wait for view-lines to be populated
52+
await editPluginDialog.locator('.view-line').first().waitFor({ timeout: 10000 });
4353
const lines = await editPluginDialog.locator('.view-line').allTextContents();
4454
editorValue = lines.join('\n').replace(/\s+/g, ' ');
4555
}
56+
4657
if (!editorValue || editorValue.trim() === '{') {
4758
const allText = await editPluginDialog.textContent();
4859
console.log('DEBUG: editorValue fallback failed, dialog text:', allText);
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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 { servicesPom } from '@e2e/pom/services';
18+
import { randomId } from '@e2e/utils/common';
19+
import { e2eReq } from '@e2e/utils/req';
20+
import { test } from '@e2e/utils/test';
21+
import { uiHasToastMsg } from '@e2e/utils/ui';
22+
import {
23+
uiCheckServiceAllFields,
24+
uiFillServiceAllFields,
25+
} from '@e2e/utils/ui/services';
26+
import { expect } from '@playwright/test';
27+
28+
import { deleteAllServices } from '@/apis/services';
29+
30+
test.describe.configure({ mode: 'serial' });
31+
32+
test.beforeAll(async () => {
33+
await deleteAllServices(e2eReq);
34+
});
35+
36+
test('should CRUD service with all fields', async ({ page }) => {
37+
const serviceNameWithAllFields = randomId('test-service-full');
38+
const description =
39+
'This is a test description for the service with all fields';
40+
41+
// Navigate to the service list page
42+
await servicesPom.toIndex(page);
43+
await servicesPom.isIndexPage(page);
44+
45+
// Click the add service button
46+
await servicesPom.getAddServiceBtn(page).click();
47+
await servicesPom.isAddPage(page);
48+
49+
await uiFillServiceAllFields(test, page, {
50+
name: serviceNameWithAllFields,
51+
desc: description,
52+
});
53+
54+
// Submit the form
55+
const addBtn = page.getByRole('button', { name: 'Add', exact: true });
56+
await addBtn.click();
57+
58+
// Wait for success message
59+
await uiHasToastMsg(page, {
60+
hasText: 'Add Service Successfully',
61+
});
62+
63+
// Verify automatic redirection to detail page
64+
await servicesPom.isDetailPage(page);
65+
66+
await test.step('verify all fields in detail page', async () => {
67+
await uiCheckServiceAllFields(page, {
68+
name: serviceNameWithAllFields,
69+
desc: description,
70+
});
71+
});
72+
73+
await test.step('return to list page and verify', async () => {
74+
// Return to the service list page
75+
await servicesPom.getServiceNavBtn(page).click();
76+
await servicesPom.isIndexPage(page);
77+
78+
// Verify the created service is visible in the list
79+
await expect(page.locator('.ant-table-tbody')).toBeVisible();
80+
81+
// Use expect to wait for the service name to appear
82+
await expect(page.getByText(serviceNameWithAllFields)).toBeVisible();
83+
});
84+
85+
await test.step('delete the created service', async () => {
86+
// Find the row containing the service name
87+
const row = page.locator('tr').filter({ hasText: serviceNameWithAllFields });
88+
await expect(row).toBeVisible();
89+
90+
// Click to view details
91+
await row.getByRole('button', { name: 'View' }).click();
92+
93+
// Verify entered detail page
94+
await servicesPom.isDetailPage(page);
95+
96+
// Delete the service
97+
await page.getByRole('button', { name: 'Delete' }).click();
98+
99+
// Confirm deletion
100+
const deleteDialog = page.getByRole('dialog', { name: 'Delete Service' });
101+
await expect(deleteDialog).toBeVisible();
102+
await deleteDialog.getByRole('button', { name: 'Delete' }).click();
103+
104+
// Verify successful deletion
105+
await servicesPom.isIndexPage(page);
106+
await uiHasToastMsg(page, {
107+
hasText: 'Delete Service Successfully',
108+
});
109+
110+
// Verify removed from the list
111+
await expect(page.getByText(serviceNameWithAllFields)).toBeHidden();
112+
113+
// Final verification: Reload the page and check again to ensure it's really gone
114+
await page.reload();
115+
await servicesPom.isIndexPage(page);
116+
117+
// After reload, the service should still be gone
118+
await expect(page.getByText(serviceNameWithAllFields)).toBeHidden();
119+
});
120+
});
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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 { servicesPom } from '@e2e/pom/services';
18+
import { randomId } from '@e2e/utils/common';
19+
import { e2eReq } from '@e2e/utils/req';
20+
import { test } from '@e2e/utils/test';
21+
import { uiHasToastMsg } from '@e2e/utils/ui';
22+
import {
23+
uiCheckServiceRequiredFields,
24+
uiFillServiceRequiredFields,
25+
} from '@e2e/utils/ui/services';
26+
import { expect } from '@playwright/test';
27+
28+
import { deleteAllServices } from '@/apis/services';
29+
30+
test.describe.configure({ mode: 'serial' });
31+
32+
const serviceName = randomId('test-service');
33+
34+
test.beforeAll(async () => {
35+
await deleteAllServices(e2eReq);
36+
});
37+
38+
test('should CRUD service with required fields', async ({ page }) => {
39+
await servicesPom.toIndex(page);
40+
await servicesPom.isIndexPage(page);
41+
42+
await servicesPom.getAddServiceBtn(page).click();
43+
await servicesPom.isAddPage(page);
44+
await test.step('submit with required fields', async () => {
45+
await uiFillServiceRequiredFields(page, {
46+
name: serviceName,
47+
});
48+
49+
// Ensure upstream is valid. In some configurations (e.g. http&stream),
50+
// the backend might require a valid upstream configuration.
51+
const upstreamSection = page.getByRole('group', { name: 'Upstream' }).first();
52+
const addNodeBtn = page.getByRole('button', { name: 'Add a Node' });
53+
await addNodeBtn.click();
54+
55+
const rows = upstreamSection.locator('tr.ant-table-row');
56+
await rows.first().locator('input').first().fill('127.0.0.1');
57+
await rows.first().locator('input').nth(1).fill('80');
58+
await rows.first().locator('input').nth(2).fill('1');
59+
60+
// Ensure the name field is properly filled before submitting
61+
const nameField = page.getByRole('textbox', { name: 'Name' }).first();
62+
await expect(nameField).toHaveValue(serviceName);
63+
64+
await servicesPom.getAddBtn(page).click();
65+
66+
// Wait for either success or error toast (longer timeout for CI)
67+
const alertMsg = page.getByRole('alert');
68+
await expect(alertMsg).toBeVisible({ timeout: 30000 });
69+
70+
// Check if it's a success message
71+
await expect(alertMsg).toContainText('Add Service Successfully', { timeout: 5000 });
72+
73+
// Close the toast
74+
await alertMsg.getByRole('button').click();
75+
await expect(alertMsg).toBeHidden();
76+
});
77+
78+
await test.step('auto navigate to service detail page', async () => {
79+
await servicesPom.isDetailPage(page);
80+
// Verify ID exists
81+
const ID = page.getByRole('textbox', { name: 'ID', exact: true });
82+
await expect(ID).toBeVisible();
83+
await expect(ID).toBeDisabled();
84+
await uiCheckServiceRequiredFields(page, {
85+
name: serviceName,
86+
});
87+
});
88+
89+
await test.step('can see service in list page', async () => {
90+
await servicesPom.getServiceNavBtn(page).click();
91+
await expect(page.getByRole('cell', { name: serviceName })).toBeVisible();
92+
});
93+
94+
await test.step('navigate to service detail page', async () => {
95+
// Click on the service name to go to the detail page
96+
await page
97+
.getByRole('row', { name: serviceName })
98+
.getByRole('button', { name: 'View' })
99+
.click();
100+
await servicesPom.isDetailPage(page);
101+
const name = page.getByRole('textbox', { name: 'Name' }).first();
102+
await expect(name).toHaveValue(serviceName);
103+
});
104+
105+
await test.step('edit and update service in detail page', async () => {
106+
// Click the Edit button in the detail page
107+
await page.getByRole('button', { name: 'Edit' }).click();
108+
109+
// Verify we're in edit mode - fields should be editable now
110+
const nameField = page.getByRole('textbox', { name: 'Name' }).first();
111+
await expect(nameField).toBeEnabled();
112+
113+
// Update the description field (use first() to get service description, not upstream description)
114+
const descriptionField = page.getByLabel('Description').first();
115+
await descriptionField.fill('Updated description for testing');
116+
117+
// Add a simple label (key:value format)
118+
// Use first() to get service labels field, not upstream labels
119+
const labelsField = page.getByPlaceholder('Input text like `key:value`,').first();
120+
await expect(labelsField).toBeEnabled();
121+
122+
// Add a single label in key:value format
123+
await labelsField.click();
124+
await labelsField.fill('version:v1');
125+
await labelsField.press('Enter');
126+
127+
// Verify the label was added by checking if the input is cleared
128+
// This indicates the tag was successfully created
129+
await expect(labelsField).toHaveValue('');
130+
131+
// Click the Save button to save changes
132+
const saveBtn = page.getByRole('button', { name: 'Save' });
133+
await saveBtn.click();
134+
135+
// Verify the update was successful
136+
await uiHasToastMsg(page, {
137+
hasText: 'success',
138+
});
139+
140+
// Verify we're back in detail view mode
141+
await servicesPom.isDetailPage(page);
142+
143+
// Verify the updated fields
144+
await expect(page.getByLabel('Description').first()).toHaveValue(
145+
'Updated description for testing'
146+
);
147+
148+
// check labels
149+
await expect(page.getByText('version:v1')).toBeVisible();
150+
151+
// Return to list page and verify the service exists
152+
await servicesPom.getServiceNavBtn(page).click();
153+
await servicesPom.isIndexPage(page);
154+
155+
// Find the row with our service
156+
const row = page.getByRole('row', { name: serviceName });
157+
await expect(row).toBeVisible();
158+
});
159+
160+
await test.step('delete service in detail page', async () => {
161+
// Navigate back to detail page
162+
await page
163+
.getByRole('row', { name: serviceName })
164+
.getByRole('button', { name: 'View' })
165+
.click();
166+
await servicesPom.isDetailPage(page);
167+
168+
await page.getByRole('button', { name: 'Delete' }).click();
169+
170+
await page
171+
.getByRole('dialog', { name: 'Delete Service' })
172+
.getByRole('button', { name: 'Delete' })
173+
.click();
174+
175+
// will redirect to services page
176+
await servicesPom.isIndexPage(page);
177+
await uiHasToastMsg(page, {
178+
hasText: 'Delete Service Successfully',
179+
});
180+
await expect(page.getByRole('cell', { name: serviceName })).toBeHidden();
181+
});
182+
});

0 commit comments

Comments
 (0)