Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
014cb74
feat(e2e): add comprehensive service CRUD tests and UI utility functions
DSingh0304 Nov 15, 2025
c33abc9
test(e2e): remove unnecessary timeout from services test
DSingh0304 Nov 17, 2025
6e59e0c
e2e: update apisix_conf and add retry for upstream cleanup
DSingh0304 Nov 19, 2025
72a628e
Add Spanish and Turkish translations to i18n
DSingh0304 Nov 19, 2025
fc7ebd9
Fix import statement for zh_common
DSingh0304 Nov 19, 2025
98e8012
test(e2e): configure tests to run in serial mode and enhance service …
DSingh0304 Nov 19, 2025
97cc6ef
docs: add comment to retry function in deleteAllUpstreams
DSingh0304 Nov 21, 2025
1e67a89
docs: add detailed comments to upstream deletion logic
DSingh0304 Nov 21, 2025
999229d
test(e2e): enhance upstream deletion robustness for CI
DSingh0304 Nov 21, 2025
b6c8708
Merge branch 'apache:master' into restore/e2e-services
DSingh0304 Nov 21, 2025
8f72b19
test(e2e): increase timeouts for CI reliability
DSingh0304 Nov 21, 2025
1595ee2
Merge branch 'apache:master' into restore/e2e-services
DSingh0304 Nov 24, 2025
f67e0b5
docs: Add Apache license header and inline comments to apisix_conf.ym…
DSingh0304 Nov 24, 2025
e913fb7
fix(e2e): resolve flaky plugin metadata test and service upstream val…
DSingh0304 Nov 24, 2025
f73352e
chore: remove unnecessary comments and whitespace from e2e tests and …
DSingh0304 Dec 16, 2025
465f69c
test: add e2e validation for required fields in plugin configs and re…
DSingh0304 Dec 17, 2025
939b235
refactor: replace retry loop with Playwright's waitFor mechanism
DSingh0304 Dec 23, 2025
c970df7
refactor: remove retry mechanism from `deleteAllUpstreams` function.
DSingh0304 Dec 23, 2025
2888370
Merge branch 'master' into restore/e2e-services
DSingh0304 Dec 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion e2e/tests/plugin_metadata.crud-all-fields.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,26 @@ const deletePluginMetadata = async (req: typeof e2eReq, name: string) => {
});
};
const getMonacoEditorValue = async (editPluginDialog: Locator) => {
let editorValue = '';
const textarea = editPluginDialog.locator('textarea');

// Wait for Monaco editor to be fully loaded with content (increased timeout for CI)
await textarea.waitFor({ state: 'attached', timeout: 10000 });

let editorValue = '';

// Try to get value from textarea first
if (await textarea.count() > 0) {
editorValue = await textarea.inputValue();
}

// Fallback to reading view-lines if textarea value is incomplete
if (!editorValue || editorValue.trim() === '{') {
// Wait for view-lines to be populated
await editPluginDialog.locator('.view-line').first().waitFor({ timeout: 10000 });
const lines = await editPluginDialog.locator('.view-line').allTextContents();
editorValue = lines.join('\n').replace(/\s+/g, ' ');
}

if (!editorValue || editorValue.trim() === '{') {
const allText = await editPluginDialog.textContent();
console.log('DEBUG: editorValue fallback failed, dialog text:', allText);
Expand Down
120 changes: 120 additions & 0 deletions e2e/tests/services.crud-all-fields.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* 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 {
uiCheckServiceAllFields,
uiFillServiceAllFields,
} from '@e2e/utils/ui/services';
import { expect } from '@playwright/test';

import { deleteAllServices } from '@/apis/services';

test.describe.configure({ mode: 'serial' });

test.beforeAll(async () => {
await deleteAllServices(e2eReq);
});

test('should CRUD service with all fields', async ({ page }) => {
const serviceNameWithAllFields = randomId('test-service-full');
const description =
'This is a test description for the service with all fields';

// Navigate to the service list page
await servicesPom.toIndex(page);
await servicesPom.isIndexPage(page);

// Click the add service button
await servicesPom.getAddServiceBtn(page).click();
await servicesPom.isAddPage(page);

await uiFillServiceAllFields(test, page, {
name: serviceNameWithAllFields,
desc: description,
});

// Submit the form
const addBtn = page.getByRole('button', { name: 'Add', exact: true });
await addBtn.click();

// Wait for success message
await uiHasToastMsg(page, {
hasText: 'Add Service Successfully',
});

// Verify automatic redirection to detail page
await servicesPom.isDetailPage(page);

await test.step('verify all fields in detail page', async () => {
await uiCheckServiceAllFields(page, {
name: serviceNameWithAllFields,
desc: description,
});
});

await test.step('return to list page and verify', async () => {
// Return to the service list page
await servicesPom.getServiceNavBtn(page).click();
await servicesPom.isIndexPage(page);

// Verify the created service is visible in the list
await expect(page.locator('.ant-table-tbody')).toBeVisible();

// Use expect to wait for the service name to appear
await expect(page.getByText(serviceNameWithAllFields)).toBeVisible();
});

await test.step('delete the created service', async () => {
// Find the row containing the service name
const row = page.locator('tr').filter({ hasText: serviceNameWithAllFields });
await expect(row).toBeVisible();

// Click to view details
await row.getByRole('button', { name: 'View' }).click();

// Verify entered detail page
await servicesPom.isDetailPage(page);

// Delete the service
await page.getByRole('button', { name: 'Delete' }).click();

// Confirm deletion
const deleteDialog = page.getByRole('dialog', { name: 'Delete Service' });
await expect(deleteDialog).toBeVisible();
await deleteDialog.getByRole('button', { name: 'Delete' }).click();

// Verify successful deletion
await servicesPom.isIndexPage(page);
await uiHasToastMsg(page, {
hasText: 'Delete Service Successfully',
});

// Verify removed from the list
await expect(page.getByText(serviceNameWithAllFields)).toBeHidden();

// Final verification: Reload the page and check again to ensure it's really gone
await page.reload();
await servicesPom.isIndexPage(page);

// After reload, the service should still be gone
await expect(page.getByText(serviceNameWithAllFields)).toBeHidden();
});
});
182 changes: 182 additions & 0 deletions e2e/tests/services.crud-required-fields.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/**
* 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 {
uiCheckServiceRequiredFields,
uiFillServiceRequiredFields,
} from '@e2e/utils/ui/services';
import { expect } from '@playwright/test';

import { deleteAllServices } from '@/apis/services';

test.describe.configure({ mode: 'serial' });

const serviceName = randomId('test-service');

test.beforeAll(async () => {
await deleteAllServices(e2eReq);
});

test('should CRUD service with required fields', async ({ page }) => {
await servicesPom.toIndex(page);
await servicesPom.isIndexPage(page);

await servicesPom.getAddServiceBtn(page).click();
await servicesPom.isAddPage(page);
await test.step('submit with required fields', async () => {
await uiFillServiceRequiredFields(page, {
name: serviceName,
});

// Ensure upstream is valid. In some configurations (e.g. http&stream),
// the backend might require a valid upstream configuration.
const upstreamSection = page.getByRole('group', { name: 'Upstream' }).first();
const addNodeBtn = page.getByRole('button', { name: 'Add a Node' });
await addNodeBtn.click();

const rows = upstreamSection.locator('tr.ant-table-row');
await rows.first().locator('input').first().fill('127.0.0.1');
await rows.first().locator('input').nth(1).fill('80');
await rows.first().locator('input').nth(2).fill('1');

// Ensure the name field is properly filled before submitting
const nameField = page.getByRole('textbox', { name: 'Name' }).first();
await expect(nameField).toHaveValue(serviceName);

await servicesPom.getAddBtn(page).click();

// Wait for either success or error toast (longer timeout for CI)
const alertMsg = page.getByRole('alert');
await expect(alertMsg).toBeVisible({ timeout: 30000 });

// Check if it's a success message
await expect(alertMsg).toContainText('Add Service Successfully', { timeout: 5000 });

// Close the toast
await alertMsg.getByRole('button').click();
await expect(alertMsg).toBeHidden();
});

await test.step('auto navigate to service detail page', async () => {
await servicesPom.isDetailPage(page);
// Verify ID exists
const ID = page.getByRole('textbox', { name: 'ID', exact: true });
await expect(ID).toBeVisible();
await expect(ID).toBeDisabled();
await uiCheckServiceRequiredFields(page, {
name: serviceName,
});
});

await test.step('can see service in list page', async () => {
await servicesPom.getServiceNavBtn(page).click();
await expect(page.getByRole('cell', { name: serviceName })).toBeVisible();
});

await test.step('navigate to service detail page', async () => {
// Click on the service name to go to the detail page
await page
.getByRole('row', { name: serviceName })
.getByRole('button', { name: 'View' })
.click();
await servicesPom.isDetailPage(page);
const name = page.getByRole('textbox', { name: 'Name' }).first();
await expect(name).toHaveValue(serviceName);
});

await test.step('edit and update service in detail page', 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 nameField = page.getByRole('textbox', { name: 'Name' }).first();
await expect(nameField).toBeEnabled();

// Update the description field (use first() to get service description, not upstream description)
const descriptionField = page.getByLabel('Description').first();
await descriptionField.fill('Updated description for testing');

// Add a simple label (key:value format)
// Use first() to get service labels field, not upstream labels
const labelsField = page.getByPlaceholder('Input text like `key:value`,').first();
await expect(labelsField).toBeEnabled();

// Add a single label in key:value format
await labelsField.click();
await labelsField.fill('version:v1');
await labelsField.press('Enter');

// Verify the label was added by checking if the input is cleared
// This indicates the tag was successfully created
await expect(labelsField).toHaveValue('');

// 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.isDetailPage(page);

// Verify the updated fields
await expect(page.getByLabel('Description').first()).toHaveValue(
'Updated description for testing'
);

// check labels
await expect(page.getByText('version:v1')).toBeVisible();

// Return to list page and verify the service exists
await servicesPom.getServiceNavBtn(page).click();
await servicesPom.isIndexPage(page);

// Find the row with our service
const row = page.getByRole('row', { name: serviceName });
await expect(row).toBeVisible();
});

await test.step('delete service in detail page', async () => {
// Navigate back to detail page
await page
.getByRole('row', { name: serviceName })
.getByRole('button', { name: 'View' })
.click();
await servicesPom.isDetailPage(page);

await page.getByRole('button', { name: 'Delete' }).click();

await page
.getByRole('dialog', { name: 'Delete Service' })
.getByRole('button', { name: 'Delete' })
.click();

// will redirect to services page
await servicesPom.isIndexPage(page);
await uiHasToastMsg(page, {
hasText: 'Delete Service Successfully',
});
await expect(page.getByRole('cell', { name: serviceName })).toBeHidden();
});
});
Loading
Loading