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}`)) + ); + } +};