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 bb5dddb62 test(e2e): Add E2E tests for consumer and credentials (#3243)
bb5dddb62 is described below
commit bb5dddb62a53af851354dbd9927abcd5f4d7c622
Author: Deep Shekhar Singh <[email protected]>
AuthorDate: Fri Nov 21 07:33:13 2025 +0530
test(e2e): Add E2E tests for consumer and credentials (#3243)
* feat(e2e): add E2E tests for Consumers resource
- Add Page Object Model for Consumers (e2e/pom/consumers.ts)
- Add list and pagination tests (consumers.list.spec.ts)
- Add CRUD tests with required fields
(consumers.crud-required-fields.spec.ts)
- Add CRUD tests with all fields (consumers.crud-all-fields.spec.ts)
- Add deleteAllConsumers() helper function in src/apis/consumers.ts
All tests follow the established pattern:
- *.list.spec.ts for list page and pagination
- *.crud-required-fields.spec.ts for basic CRUD operations
- *.crud-all-fields.spec.ts for comprehensive CRUD with all fields
Tests cover:
- Navigation and page assertions
- Form validation
- Create, Read, Update, Delete operations
- Labels management with tags input
- Pagination with table controls and URL params
Fixes username validation by using only allowed characters [a-zA-Z0-9_-]
* feat: add e2e tests for consumer credentials
- Add credentialsPom for page object model
- Add comprehensive test suite for credentials CRUD operations
- Test credential isolation between consumers
- Test empty state handling
- Configure serial mode to avoid race conditions
All 8 tests passing
---
e2e/pom/consumers.ts | 58 ++++
e2e/pom/credentials.ts | 70 ++++
e2e/tests/consumers.credentials.list.spec.ts | 413 +++++++++++++++++++++++
e2e/tests/consumers.crud-all-fields.spec.ts | 145 ++++++++
e2e/tests/consumers.crud-required-fields.spec.ts | 127 +++++++
e2e/tests/consumers.list.spec.ts | 81 +++++
src/apis/consumers.ts | 20 +-
7 files changed, 913 insertions(+), 1 deletion(-)
diff --git a/e2e/pom/consumers.ts b/e2e/pom/consumers.ts
new file mode 100644
index 000000000..9b811f1a7
--- /dev/null
+++ b/e2e/pom/consumers.ts
@@ -0,0 +1,58 @@
+/**
+ * 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 { uiGoto } from '@e2e/utils/ui';
+import { expect, type Page } from '@playwright/test';
+
+const locator = {
+ getConsumerNavBtn: (page: Page) =>
+ page.getByRole('link', { name: 'Consumers', exact: true }),
+ getAddConsumerBtn: (page: Page) =>
+ page.getByRole('button', { name: 'Add Consumer', exact: true }),
+ getAddBtn: (page: Page) =>
+ page.getByRole('button', { name: 'Add', exact: true }),
+};
+
+const assert = {
+ isIndexPage: async (page: Page) => {
+ await expect(page).toHaveURL((url) => url.pathname.endsWith('/consumers'));
+ const title = page.getByRole('heading', { name: 'Consumers' });
+ await expect(title).toBeVisible();
+ },
+ isAddPage: async (page: Page) => {
+ await expect(page).toHaveURL((url) =>
url.pathname.endsWith('/consumers/add'));
+ const title = page.getByRole('heading', { name: 'Add Consumer' });
+ await expect(title).toBeVisible();
+ },
+ isDetailPage: async (page: Page) => {
+ await expect(page).toHaveURL((url) =>
+ url.pathname.includes('/consumers/detail')
+ );
+ const title = page.getByRole('heading', { name: 'Consumer Detail' });
+ await expect(title).toBeVisible();
+ },
+};
+
+const goto = {
+ toIndex: (page: Page) => uiGoto(page, '/consumers'),
+ toAdd: (page: Page) => uiGoto(page, '/consumers/add'),
+};
+
+export const consumersPom = {
+ ...locator,
+ ...assert,
+ ...goto,
+};
diff --git a/e2e/pom/credentials.ts b/e2e/pom/credentials.ts
new file mode 100644
index 000000000..94b37a10c
--- /dev/null
+++ b/e2e/pom/credentials.ts
@@ -0,0 +1,70 @@
+/**
+ * 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 { uiGoto } from '@e2e/utils/ui';
+import { expect, type Page } from '@playwright/test';
+
+const locator = {
+ getCredentialsTab: (page: Page) =>
+ page.getByRole('tab', { name: 'Credentials' }),
+ getAddCredentialBtn: (page: Page) =>
+ page.getByRole('button', { name: 'Add Credential', exact: true }),
+ getAddBtn: (page: Page) =>
+ page.getByRole('button', { name: 'Add', exact: true }),
+};
+
+const assert = {
+ isCredentialsIndexPage: async (page: Page, username: string) => {
+ await expect(page).toHaveURL((url) =>
+ url.pathname.includes(`/consumers/detail/${username}/credentials`)
+ );
+ const title = page.getByRole('heading', { name: 'Credentials' });
+ await expect(title).toBeVisible();
+ },
+ isCredentialAddPage: async (page: Page, username: string) => {
+ await expect(page).toHaveURL((url) =>
+ url.pathname.endsWith(`/consumers/detail/${username}/credentials/add`)
+ );
+ const title = page.getByRole('heading', { name: 'Add Credential' });
+ await expect(title).toBeVisible();
+ },
+ isCredentialDetailPage: async (page: Page) => {
+ await expect(page).toHaveURL((url) =>
+ url.pathname.includes('/consumers/detail/') &&
+ url.pathname.includes('/credentials/detail/')
+ );
+ const title = page.getByRole('heading', { name: 'Credential Detail' });
+ await expect(title).toBeVisible();
+ },
+};
+
+const goto = {
+ toCredentialsIndex: (page: Page, username: string) =>
+ uiGoto(page, '/consumers/detail/$username/credentials', { username }),
+ toCredentialAdd: (page: Page, username: string) =>
+ uiGoto(page, '/consumers/detail/$username/credentials/add', { username }),
+ toCredentialDetail: (page: Page, username: string, id: string) =>
+ uiGoto(page, '/consumers/detail/$username/credentials/detail/$id', {
+ username,
+ id,
+ }),
+};
+
+export const credentialsPom = {
+ ...locator,
+ ...assert,
+ ...goto,
+};
diff --git a/e2e/tests/consumers.credentials.list.spec.ts
b/e2e/tests/consumers.credentials.list.spec.ts
new file mode 100644
index 000000000..462370a3c
--- /dev/null
+++ b/e2e/tests/consumers.credentials.list.spec.ts
@@ -0,0 +1,413 @@
+/**
+ * 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 { consumersPom } from '@e2e/pom/consumers';
+import { credentialsPom } from '@e2e/pom/credentials';
+import { randomId } from '@e2e/utils/common';
+import { e2eReq } from '@e2e/utils/req';
+import { test } from '@e2e/utils/test';
+import { expect } from '@playwright/test';
+
+import { deleteAllConsumers, putConsumerReq } from '@/apis/consumers';
+import { putCredentialReq } from '@/apis/credentials';
+import { API_CONSUMERS } from '@/config/constant';
+import type { APISIXType } from '@/types/schema/apisix';
+
+const testConsumerUsername = randomId('test-consumer');
+const anotherConsumerUsername = randomId('another-consumer');
+
+const credentials: (APISIXType['CredentialPut'] & { username: string })[] = [
+ {
+ username: testConsumerUsername,
+ id: randomId('cred-1'),
+ desc: 'Test credential 1',
+ plugins: {
+ 'key-auth': {
+ key: randomId('key-1'),
+ },
+ },
+ },
+ {
+ username: testConsumerUsername,
+ id: randomId('cred-2'),
+ desc: 'Test credential 2',
+ plugins: {
+ 'key-auth': {
+ key: randomId('key-2'),
+ },
+ },
+ },
+ {
+ username: testConsumerUsername,
+ id: randomId('cred-3'),
+ desc: 'Test credential 3',
+ plugins: {
+ 'basic-auth': {
+ username: 'testuser',
+ password: 'testpass',
+ },
+ },
+ },
+];
+
+// Credential that belongs to another consumer
+const anotherConsumerCredential: APISIXType['CredentialPut'] & {
+ username: string;
+} = {
+ username: anotherConsumerUsername,
+ id: randomId('another-cred'),
+ desc: 'Another consumer credential',
+ plugins: {
+ 'key-auth': {
+ key: randomId('another-key'),
+ },
+ },
+};
+
+// Configure tests to run serially to avoid race conditions
+test.describe.configure({ mode: 'serial' });
+
+test.beforeAll(async () => {
+ // Clean up any existing consumers first
+ await deleteAllConsumers(e2eReq);
+
+ // Create test consumer first
+ await putConsumerReq(e2eReq, {
+ username: testConsumerUsername,
+ desc: 'Test consumer for credential testing',
+ });
+
+ // Create another consumer
+ await putConsumerReq(e2eReq, {
+ username: anotherConsumerUsername,
+ desc: 'Another test consumer for credential isolation testing',
+ });
+
+ // Wait a bit to ensure consumers are created
+ await new Promise((resolve) => setTimeout(resolve, 100));
+
+ // Create credentials for test consumer - now that consumer exists
+ for (const credential of credentials) {
+ await putCredentialReq(e2eReq, credential);
+ }
+
+ // Create credential for another consumer - now that consumer exists
+ await putCredentialReq(e2eReq, anotherConsumerCredential);
+});
+
+test.afterAll(async () => {
+ await deleteAllConsumers(e2eReq);
+});
+
+test('should navigate to consumer credentials page', async ({ page }) => {
+ await test.step('navigate to consumer detail page', async () => {
+ await consumersPom.toIndex(page);
+ await consumersPom.isIndexPage(page);
+
+ await page
+ .getByRole('row', { name: testConsumerUsername })
+ .getByRole('button', { name: 'View' })
+ .click();
+ await consumersPom.isDetailPage(page);
+ });
+
+ await test.step('navigate to credentials tab', async () => {
+ // Directly navigate to credentials page instead of clicking tab
+ await credentialsPom.toCredentialsIndex(page, testConsumerUsername);
+ await credentialsPom.isCredentialsIndexPage(page, testConsumerUsername);
+ });
+
+ await test.step('verify credentials page components', async () => {
+ await expect(credentialsPom.getAddCredentialBtn(page)).toBeVisible();
+
+ // list table exists
+ const table = page.getByRole('table');
+ await expect(table).toBeVisible();
+ await expect(table.getByText('ID', { exact: true })).toBeVisible();
+ await expect(table.getByText('Actions', { exact: true })).toBeVisible();
+ });
+});
+
+test('should only show credentials for current consumer', async ({ page }) => {
+ await test.step('should only show credentials for current consumer', async
() => {
+ await credentialsPom.toCredentialsIndex(page, testConsumerUsername);
+ await credentialsPom.isCredentialsIndexPage(page, testConsumerUsername);
+
+ // Credentials from another consumer should not be visible
+ await expect(
+ page.getByRole('cell', { name: anotherConsumerCredential.id })
+ ).toBeHidden();
+
+ // Only credentials belonging to current consumer should be visible
+ for (const credential of credentials) {
+ await expect(
+ page.getByRole('cell', { name: credential.id })
+ ).toBeVisible();
+ }
+ });
+
+ await test.step('verify credential isolation', async () => {
+ // Navigate to another consumer's credentials
+ await credentialsPom.toCredentialsIndex(page, anotherConsumerUsername);
+ await credentialsPom.isCredentialsIndexPage(page, anotherConsumerUsername);
+
+ // Should only see the other consumer's credential
+ await expect(
+ page.getByRole('cell', { name: anotherConsumerCredential.id })
+ ).toBeVisible();
+
+ // Should not see test consumer's credentials
+ for (const credential of credentials) {
+ await expect(
+ page.getByRole('cell', { name: credential.id })
+ ).toBeHidden();
+ }
+ });
+});
+
+test('should display credentials list under consumer', async ({ page }) => {
+ await test.step('navigate to consumer credentials page', async () => {
+ // Directly navigate to credentials page
+ await credentialsPom.toCredentialsIndex(page, testConsumerUsername);
+ await credentialsPom.isCredentialsIndexPage(page, testConsumerUsername);
+ });
+
+ await test.step('should display all credentials for consumer', async () => {
+ // Verify all created credentials are displayed
+ for (const credential of credentials) {
+ await expect(
+ page.getByRole('cell', { name: credential.id })
+ ).toBeVisible();
+ await expect(
+ page.getByRole('cell', { name: credential.desc || '' })
+ ).toBeVisible();
+ }
+ });
+
+ await test.step('should have correct table headers', async () => {
+ await expect(page.getByRole('columnheader', { name: 'ID' })).toBeVisible();
+ await expect(
+ page.getByRole('columnheader', { name: 'Description' })
+ ).toBeVisible();
+ await expect(
+ page.getByRole('columnheader', { name: 'Actions' })
+ ).toBeVisible();
+ });
+
+ await test.step('should show correct credential count', async () => {
+ // Check that all 3 credentials are displayed in the table
+ const tableRows = page.locator('tbody tr');
+ await expect(tableRows).toHaveCount(credentials.length);
+ });
+});
+
+test('should be able to navigate to credential detail', async ({ page }) => {
+ await test.step('navigate to credentials list', async () => {
+ await credentialsPom.toCredentialsIndex(page, testConsumerUsername);
+ await credentialsPom.isCredentialsIndexPage(page, testConsumerUsername);
+ });
+
+ await test.step('click on credential view button', async () => {
+ // Click on the first credential's View button
+ await page
+ .getByRole('row', { name: credentials[0].id })
+ .getByRole('button', { name: 'View' })
+ .click();
+
+ await credentialsPom.isCredentialDetailPage(page);
+ });
+
+ await test.step('verify credential detail page', async () => {
+ // Verify we're on the correct credential detail page
+ const idField = page.getByLabel('ID', { exact: true }).first();
+ await expect(idField).toHaveValue(credentials[0].id);
+
+ const descField = page.getByLabel('Description', { exact: true });
+ await expect(descField).toHaveValue(credentials[0].desc || '');
+ });
+});
+
+test('should have Add Credential button', async ({ page }) => {
+ await test.step('navigate to credentials list', async () => {
+ await credentialsPom.toCredentialsIndex(page, testConsumerUsername);
+ await credentialsPom.isCredentialsIndexPage(page, testConsumerUsername);
+ });
+
+ await test.step('verify Add Credential button exists and works', async () =>
{
+ const addCredentialBtn = credentialsPom.getAddCredentialBtn(page);
+ await expect(addCredentialBtn).toBeVisible();
+
+ await addCredentialBtn.click();
+ await credentialsPom.isCredentialAddPage(page, testConsumerUsername);
+ });
+
+ await test.step('verify add page has required fields', async () => {
+ // Verify ID field exists
+ const idField = page.getByLabel('ID', { exact: true }).first();
+ await expect(idField).toBeVisible();
+
+ // Verify Description field exists
+ const descField = page.getByLabel('Description', { exact: true });
+ await expect(descField).toBeVisible();
+ });
+});
+
+test('should be able to delete credential', async ({ page }) => {
+ // Create a temporary credential for deletion test
+ const tempCredential: APISIXType['CredentialPut'] & { username: string } = {
+ username: testConsumerUsername,
+ id: randomId('temp-cred'),
+ desc: 'Temporary credential for deletion',
+ plugins: {
+ 'key-auth': {
+ key: randomId('temp-key'),
+ },
+ },
+ };
+
+ await test.step('create temporary credential', async () => {
+ await putCredentialReq(e2eReq, tempCredential);
+ });
+
+ await test.step('navigate to credentials list', async () => {
+ await credentialsPom.toCredentialsIndex(page, testConsumerUsername);
+ await credentialsPom.isCredentialsIndexPage(page, testConsumerUsername);
+ });
+
+ await test.step('verify temporary credential exists', async () => {
+ await expect(
+ page.getByRole('cell', { name: tempCredential.id })
+ ).toBeVisible();
+ });
+
+ await test.step('delete the credential', async () => {
+ // Click delete button on the temporary credential
+ await page
+ .getByRole('row', { name: tempCredential.id })
+ .getByRole('button', { name: 'Delete' })
+ .click();
+
+ // Confirm deletion in modal
+ const deleteDialog = page.getByRole('dialog', { name: 'Delete Credential'
});
+ await expect(deleteDialog).toBeVisible();
+ await deleteDialog.getByRole('button', { name: 'Delete' }).click();
+
+ // Wait for success notification
+ await expect(page.getByRole('alert')).toBeVisible();
+ });
+
+ await test.step('verify credential is deleted', async () => {
+ // Reload the page to ensure the credential is gone
+ await page.reload();
+ await credentialsPom.isCredentialsIndexPage(page, testConsumerUsername);
+
+ // Verify the credential no longer appears
+ await expect(
+ page.getByRole('cell', { name: tempCredential.id })
+ ).toBeHidden();
+ });
+});
+
+test('should be able to edit credential', async ({ page }) => {
+ const credentialToEdit = credentials[0];
+ const updatedDesc = randomId('updated-desc');
+
+ await test.step('navigate to credential detail', async () => {
+ await credentialsPom.toCredentialDetail(
+ page,
+ testConsumerUsername,
+ credentialToEdit.id
+ );
+ await credentialsPom.isCredentialDetailPage(page);
+ });
+
+ await test.step('enable edit mode', async () => {
+ const editBtn = page.getByRole('button', { name: 'Edit' });
+ await expect(editBtn).toBeVisible();
+ await editBtn.click();
+ });
+
+ await test.step('update credential description', async () => {
+ const descField = page.getByLabel('Description', { exact: true });
+ await expect(descField).toBeEnabled();
+ await descField.clear();
+ await descField.fill(updatedDesc);
+ });
+
+ await test.step('save changes', async () => {
+ const saveBtn = page.getByRole('button', { name: 'Save' });
+ await expect(saveBtn).toBeVisible();
+ await saveBtn.click();
+
+ // Wait for success notification
+ await expect(page.getByRole('alert')).toBeVisible();
+ });
+
+ await test.step('verify changes are saved', async () => {
+ // Reload the page to ensure changes persisted
+ await page.reload();
+ await credentialsPom.isCredentialDetailPage(page);
+
+ const descField = page.getByLabel('Description', { exact: true });
+ await expect(descField).toHaveValue(updatedDesc);
+ });
+
+ await test.step('restore original description', async () => {
+ // Edit again to restore original value
+ const editBtn = page.getByRole('button', { name: 'Edit' });
+ await editBtn.click();
+
+ const descField = page.getByLabel('Description', { exact: true });
+ await descField.clear();
+ await descField.fill(credentialToEdit.desc || '');
+
+ const saveBtn = page.getByRole('button', { name: 'Save' });
+ await saveBtn.click();
+
+ await expect(page.getByRole('alert')).toBeVisible();
+ });
+});
+
+test('should handle empty credentials list', async ({ page }) => {
+ const emptyConsumerUsername = randomId('empty-consumer');
+
+ await test.step('create consumer without credentials', async () => {
+ await putConsumerReq(e2eReq, {
+ username: emptyConsumerUsername,
+ desc: 'Consumer without credentials',
+ });
+ });
+
+ await test.step('navigate to empty credentials list', async () => {
+ await credentialsPom.toCredentialsIndex(page, emptyConsumerUsername);
+ await credentialsPom.isCredentialsIndexPage(page, emptyConsumerUsername);
+ });
+
+ await test.step('verify empty state', async () => {
+ // Table should exist but be empty or show empty message
+ const table = page.getByRole('table');
+ await expect(table).toBeVisible();
+
+ // Check that no actual credential data rows exist (excluding empty state
row)
+ const credentialCells = page.getByRole('cell').filter({ hasText: /cred-/
});
+ await expect(credentialCells).toHaveCount(0);
+ });
+
+ await test.step('cleanup empty consumer', async () => {
+ await e2eReq.delete(`${API_CONSUMERS}/${emptyConsumerUsername}`);
+ });
+});
diff --git a/e2e/tests/consumers.crud-all-fields.spec.ts
b/e2e/tests/consumers.crud-all-fields.spec.ts
new file mode 100644
index 000000000..b13eb93ac
--- /dev/null
+++ b/e2e/tests/consumers.crud-all-fields.spec.ts
@@ -0,0 +1,145 @@
+/**
+ * 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 { consumersPom } from '@e2e/pom/consumers';
+import { e2eReq } from '@e2e/utils/req';
+import { test } from '@e2e/utils/test';
+import { uiHasToastMsg } from '@e2e/utils/ui';
+import { expect } from '@playwright/test';
+import { customAlphabet } from 'nanoid';
+
+import { deleteAllConsumers } from '@/apis/consumers';
+
+// Consumer usernames can only contain: a-zA-Z0-9_-
+const nanoid =
customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
10);
+const consumerUsername = `testconsumer${nanoid()}`;
+const description = 'Test consumer with all fields filled';
+
+test.beforeAll(async () => {
+ await deleteAllConsumers(e2eReq);
+});
+
+test('should CRUD consumer with all fields', async ({ page }) => {
+ test.slow();
+
+ await consumersPom.toIndex(page);
+ await consumersPom.isIndexPage(page);
+
+ await consumersPom.getAddConsumerBtn(page).click();
+ await consumersPom.isAddPage(page);
+
+ await test.step('submit with all fields', async () => {
+ // Fill username (required)
+ await page.getByRole('textbox', { name: 'Username'
}).fill(consumerUsername);
+
+ // Fill description (optional)
+ await page.getByRole('textbox', { name: 'Description' }).fill(description);
+
+ // Add labels using tags input
+ const labelsInput = page.getByPlaceholder('Input text like `key:value`,
then enter or blur');
+ await labelsInput.fill('version:v1');
+ await labelsInput.press('Enter');
+ await labelsInput.fill('env:test');
+ await labelsInput.press('Enter');
+ await labelsInput.fill('team:engineering');
+ await labelsInput.press('Enter');
+
+ // Submit the form
+ await consumersPom.getAddBtn(page).click();
+ await uiHasToastMsg(page, {
+ hasText: 'Add Consumer Successfully',
+ });
+ });
+
+ await test.step('auto navigate to consumer detail page', async () => {
+ await consumersPom.isDetailPage(page);
+
+ // Verify the consumer username
+ await expect(page.getByRole('textbox', { name: 'Username' }))
+ .toHaveValue(consumerUsername);
+ });
+
+ await test.step('edit and update all fields', async () => {
+ // Enter edit mode
+ await page.getByRole('button', { name: 'Edit' }).click();
+
+ // Update description
+ await page.getByRole('textbox', { name: 'Description' }).fill('Updated: '
+ description);
+
+ // Update labels - remove old ones and add new ones
+ // First, remove existing labels by clicking the X button
+ const labelsSection = page.getByRole('group', { name: 'Basic Infomation'
});
+ const removeButtons =
labelsSection.locator('button[aria-label^="Remove"]');
+ const count = await removeButtons.count();
+ for (let i = 0; i < count; i++) {
+ await removeButtons.first().click();
+ }
+
+ // Add new labels
+ const labelsInput = page.getByPlaceholder('Input text like `key:value`,
then enter or blur');
+ await labelsInput.fill('version:v2');
+ await labelsInput.press('Enter');
+ await labelsInput.fill('env:production');
+ await labelsInput.press('Enter');
+ await labelsInput.fill('team:platform');
+ await labelsInput.press('Enter');
+
+ // Save changes
+ await page.getByRole('button', { name: 'Save' }).click();
+ await uiHasToastMsg(page, {
+ hasText: 'success',
+ });
+
+ // Verify updates
+ await expect(page.getByRole('textbox', { name: 'Description' }))
+ .toHaveValue('Updated: ' + description);
+ });
+
+ await test.step('verify consumer in list page', async () => {
+ await consumersPom.getConsumerNavBtn(page).click();
+ await consumersPom.isIndexPage(page);
+
+ // Find the consumer in the list
+ const row = page.getByRole('row', { name: consumerUsername });
+ await expect(row).toBeVisible();
+ });
+
+ await test.step('delete consumer', async () => {
+ // Navigate to detail page
+ await page
+ .getByRole('row', { name: consumerUsername })
+ .getByRole('button', { name: 'View' })
+ .click();
+ await consumersPom.isDetailPage(page);
+
+ // Delete
+ await page.getByRole('button', { name: 'Delete' }).click();
+ await page
+ .getByRole('dialog', { name: 'Delete Consumer' })
+ .getByRole('button', { name: 'Delete' })
+ .click();
+
+ // Verify deletion
+ await uiHasToastMsg(page, {
+ hasText: 'Delete Consumer Successfully',
+ });
+
+ // Navigate to consumers list to verify consumer is gone
+ await consumersPom.toIndex(page);
+ await consumersPom.isIndexPage(page);
+ await expect(page.getByRole('cell', { name: consumerUsername
})).toBeHidden();
+ });
+});
diff --git a/e2e/tests/consumers.crud-required-fields.spec.ts
b/e2e/tests/consumers.crud-required-fields.spec.ts
new file mode 100644
index 000000000..0354abc73
--- /dev/null
+++ b/e2e/tests/consumers.crud-required-fields.spec.ts
@@ -0,0 +1,127 @@
+/**
+ * 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 { consumersPom } from '@e2e/pom/consumers';
+import { e2eReq } from '@e2e/utils/req';
+import { test } from '@e2e/utils/test';
+import { uiHasToastMsg } from '@e2e/utils/ui';
+import { expect } from '@playwright/test';
+import { customAlphabet } from 'nanoid';
+
+import { deleteAllConsumers } from '@/apis/consumers';
+
+// Consumer usernames can only contain: a-zA-Z0-9_-
+const nanoid =
customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
10);
+const consumerUsername = `testconsumer${nanoid()}`;
+
+test.beforeAll(async () => {
+ await deleteAllConsumers(e2eReq);
+});
+
+test('should CRUD consumer with required fields', async ({ page }) => {
+ await consumersPom.toIndex(page);
+ await consumersPom.isIndexPage(page);
+
+ await consumersPom.getAddConsumerBtn(page).click();
+ await consumersPom.isAddPage(page);
+
+ await test.step('cannot submit without required fields', async () => {
+ await consumersPom.getAddBtn(page).click();
+ // Should stay on add page - form validation prevents submission
+ await consumersPom.isAddPage(page);
+ });
+
+ await test.step('submit with required fields', async () => {
+ // Fill in the Username field (only required field for consumers)
+ await page.getByRole('textbox', { name: 'Username'
}).fill(consumerUsername);
+
+ // Submit the form
+ await consumersPom.getAddBtn(page).click();
+ await uiHasToastMsg(page, {
+ hasText: 'Add Consumer Successfully',
+ });
+ });
+
+ await test.step('auto navigate to consumer detail page', async () => {
+ await consumersPom.isDetailPage(page);
+
+ // Verify the consumer username
+ const username = page.getByRole('textbox', { name: 'Username' });
+ await expect(username).toHaveValue(consumerUsername);
+ await expect(username).toBeDisabled();
+ });
+
+ await test.step('edit and update consumer in detail page', async () => {
+ // Click the Edit button in the detail page
+ await page.getByRole('button', { name: 'Edit' }).click();
+
+ // Update the description field
+ const descriptionField = page.getByRole('textbox', { name: 'Description'
});
+ await descriptionField.fill('Updated description for testing');
+
+ // 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 consumersPom.isDetailPage(page);
+
+ // Verify the updated fields
+ await expect(page.getByRole('textbox', { name: 'Description'
})).toHaveValue(
+ 'Updated description for testing'
+ );
+ });
+
+ await test.step('consumer should exist in list page', async () => {
+ await consumersPom.getConsumerNavBtn(page).click();
+ await consumersPom.isIndexPage(page);
+ await expect(page.getByRole('cell', { name: consumerUsername
})).toBeVisible();
+
+ // Click on the view button to go to the detail page
+ await page
+ .getByRole('row', { name: consumerUsername })
+ .getByRole('button', { name: 'View' })
+ .click();
+ await consumersPom.isDetailPage(page);
+ });
+
+ await test.step('delete consumer in detail page', async () => {
+ // We're already on the detail page from the previous step
+
+ // Delete the consumer
+ await page.getByRole('button', { name: 'Delete' }).click();
+
+ await page
+ .getByRole('dialog', { name: 'Delete Consumer' })
+ .getByRole('button', { name: 'Delete' })
+ .click();
+
+ // Verify deletion was successful with toast
+ await uiHasToastMsg(page, {
+ hasText: 'Delete Consumer Successfully',
+ });
+
+ // Navigate to consumers index to verify consumer is gone
+ await consumersPom.toIndex(page);
+ await consumersPom.isIndexPage(page);
+ await expect(page.getByRole('cell', { name: consumerUsername
})).toBeHidden();
+ });
+});
diff --git a/e2e/tests/consumers.list.spec.ts b/e2e/tests/consumers.list.spec.ts
new file mode 100644
index 000000000..43ff5ce9b
--- /dev/null
+++ b/e2e/tests/consumers.list.spec.ts
@@ -0,0 +1,81 @@
+/**
+ * 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 { consumersPom } from '@e2e/pom/consumers';
+import { setupPaginationTests } from '@e2e/utils/pagination-test-helper';
+import { e2eReq } from '@e2e/utils/req';
+import { test } from '@e2e/utils/test';
+import { expect, type Page } from '@playwright/test';
+
+import { deleteAllConsumers, putConsumerReq } from '@/apis/consumers';
+import { API_CONSUMERS } from '@/config/constant';
+import type { APISIXType } from '@/types/schema/apisix';
+
+test('should navigate to consumers page', async ({ page }) => {
+ await test.step('navigate to consumers page', async () => {
+ await consumersPom.getConsumerNavBtn(page).click();
+ await consumersPom.isIndexPage(page);
+ });
+
+ await test.step('verify consumers page components', async () => {
+ await expect(consumersPom.getAddConsumerBtn(page)).toBeVisible();
+
+ // list table exists
+ const table = page.getByRole('table');
+ await expect(table).toBeVisible();
+ await expect(table.getByText('Username', { exact: true })).toBeVisible();
+ await expect(table.getByText('Actions', { exact: true })).toBeVisible();
+ });
+});
+
+const consumers: APISIXType['ConsumerPut'][] = Array.from({ length: 11 }, (_,
i) => ({
+ username: `test_consumer_${i + 1}`,
+ desc: `Description for consumer ${i + 1}`,
+}));
+
+test.describe('page and page_size should work correctly', () => {
+ test.describe.configure({ mode: 'serial' });
+ test.beforeAll(async () => {
+ await deleteAllConsumers(e2eReq);
+ await Promise.all(consumers.map((d) => putConsumerReq(e2eReq, d)));
+ });
+
+ test.afterAll(async () => {
+ await Promise.all(
+ consumers.map((d) => e2eReq.delete(`${API_CONSUMERS}/${d.username}`))
+ );
+ });
+
+ // Setup pagination tests with consumer-specific configurations
+ const filterItemsNotInPage = async (page: Page) => {
+ // filter the item which not in the current page
+ // it should be random, so we need get all items in the table
+ const itemsInPage = await page
+ .getByRole('cell', { name: /test_consumer_/ })
+ .all();
+ const names = await Promise.all(itemsInPage.map((v) => v.textContent()));
+ return consumers.filter((d) => !names.includes(d.username));
+ };
+
+ setupPaginationTests(test, {
+ pom: consumersPom,
+ items: consumers,
+ filterItemsNotInPage,
+ getCell: (page, item) =>
+ page.getByRole('cell', { name: item.username }).first(),
+ });
+});
diff --git a/src/apis/consumers.ts b/src/apis/consumers.ts
index c4779df1c..7b7751290 100644
--- a/src/apis/consumers.ts
+++ b/src/apis/consumers.ts
@@ -16,7 +16,7 @@
*/
import type { AxiosInstance } from 'axios';
-import { API_CONSUMERS } from '@/config/constant';
+import { API_CONSUMERS, PAGE_SIZE_MAX, PAGE_SIZE_MIN } from
'@/config/constant';
import type { APISIXType } from '@/types/schema/apisix';
import type { PageSearchType } from '@/types/schema/pageSearch';
@@ -43,3 +43,21 @@ export const putConsumerReq = (
data
);
};
+
+export const deleteAllConsumers = async (req: AxiosInstance) => {
+ const totalRes = await getConsumerListReq(req, {
+ page: 1,
+ page_size: PAGE_SIZE_MIN,
+ });
+ const total = totalRes.total;
+ if (total === 0) return;
+ for (let times = Math.ceil(total / PAGE_SIZE_MAX); times > 0; times--) {
+ const res = await getConsumerListReq(req, {
+ page: 1,
+ page_size: PAGE_SIZE_MAX,
+ });
+ await Promise.all(
+ res.list.map((d) => req.delete(`${API_CONSUMERS}/${d.value.username}`))
+ );
+ }
+};