diff --git a/e2e/pom/global_rules.ts b/e2e/pom/global_rules.ts new file mode 100644 index 000000000..872dc35ec --- /dev/null +++ b/e2e/pom/global_rules.ts @@ -0,0 +1,62 @@ +/** + * 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 = { + getGlobalRuleNavBtn: (page: Page) => + page.getByRole('link', { name: 'Global Rules', exact: true }), + getAddGlobalRuleBtn: (page: Page) => + page.getByRole('button', { name: 'Add Global Rule', 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('/global_rules') + ); + const title = page.getByRole('heading', { name: 'Global Rules' }); + await expect(title).toBeVisible(); + }, + isAddPage: async (page: Page) => { + await expect(page).toHaveURL((url) => + url.pathname.endsWith('/global_rules/add') + ); + const title = page.getByRole('heading', { name: 'Add Global Rule' }); + await expect(title).toBeVisible(); + }, + isDetailPage: async (page: Page) => { + await expect(page).toHaveURL((url) => + url.pathname.includes('/global_rules/detail') + ); + const title = page.getByRole('heading', { name: 'Global Rule Detail' }); + await expect(title).toBeVisible(); + }, +}; + +const goto = { + toIndex: (page: Page) => uiGoto(page, '/global_rules'), + toAdd: (page: Page) => uiGoto(page, '/global_rules/add'), +}; + +export const globalRulePom = { + ...locator, + ...assert, + ...goto, +}; diff --git a/e2e/tests/global_rules.crud-all-fields.spec.ts b/e2e/tests/global_rules.crud-all-fields.spec.ts new file mode 100644 index 000000000..dda69a038 --- /dev/null +++ b/e2e/tests/global_rules.crud-all-fields.spec.ts @@ -0,0 +1,137 @@ +/** + * 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 { globalRulePom } from '@e2e/pom/global_rules'; +import { test } from '@e2e/utils/test'; +import { + uiFillMonacoEditor, + uiGetMonacoEditor, + uiHasToastMsg, +} from '@e2e/utils/ui'; +import { expect } from '@playwright/test'; + +test('should CRUD global rule with multiple plugins', async ({ page }) => { + let globalRuleId: string; + + await test.step('navigate to add global rule page', async () => { + await globalRulePom.toAdd(page); + await globalRulePom.isAddPage(page); + }); + + await test.step('add global rule with multiple plugins', async () => { + // ID field should be auto-generated + const idInput = page.getByLabel('ID'); + await expect(idInput).toBeVisible(); + await expect(idInput).not.toHaveValue(''); + globalRuleId = await idInput.inputValue(); + + // Add first plugin - response-rewrite + const selectPluginBtn = page.getByRole('button', { + name: 'Select Plugins', + }); + await selectPluginBtn.click(); + + const dialog = page.getByRole('dialog', { name: 'Select Plugins' }); + await expect(dialog).toBeVisible(); + + const searchInput = dialog.getByPlaceholder('Search'); + await searchInput.fill('response-rewrite'); + + await dialog + .getByTestId('plugin-response-rewrite') + .getByRole('button', { name: 'Add' }) + .click(); + + const pluginDialog = page.getByRole('dialog', { name: 'Add Plugin' }); + await expect(pluginDialog).toBeVisible(); + + // Configure response-rewrite with custom configuration using Monaco editor + const pluginEditor = await uiGetMonacoEditor(page, pluginDialog); + await uiFillMonacoEditor( + page, + pluginEditor, + JSON.stringify({ + body: 'test response', + headers: { + set: { + 'X-Global-Rule': 'test-global-rule', + }, + }, + }) + ); + + await pluginDialog.getByRole('button', { name: 'Add' }).click(); + await expect(pluginDialog).toBeHidden(); + + // Add second plugin - cors + await selectPluginBtn.click(); + + const corsDialog = page.getByRole('dialog', { name: 'Select Plugins' }); + await expect(corsDialog).toBeVisible(); + + const corsSearchInput = corsDialog.getByPlaceholder('Search'); + await corsSearchInput.fill('cors'); + + await corsDialog + .getByTestId('plugin-cors') + .getByRole('button', { name: 'Add' }) + .click(); + + const corsPluginDialog = page.getByRole('dialog', { name: 'Add Plugin' }); + await expect(corsPluginDialog).toBeVisible(); + + // Submit with simple configuration for cors + const corsEditor = await uiGetMonacoEditor(page, corsPluginDialog); + await uiFillMonacoEditor(page, corsEditor, '{}'); + + await corsPluginDialog.getByRole('button', { name: 'Add' }).click(); + await expect(corsPluginDialog).toBeHidden(); + + // Submit the form + await globalRulePom.getAddBtn(page).click(); + + await uiHasToastMsg(page, { + hasText: 'success', + }); + + await globalRulePom.isDetailPage(page); + }); + + await test.step('verify global rule with multiple plugins', async () => { + await expect(page).toHaveURL( + (url) => url.pathname.endsWith(`/global_rules/detail/${globalRuleId}`) + ); + + // Verify we're on the detail page + await globalRulePom.isDetailPage(page); + }); + + await test.step('delete global rule from detail page', async () => { + await page.getByRole('button', { name: 'Delete' }).click(); + + await page + .getByRole('dialog', { name: 'Delete Global Rule' }) + .getByRole('button', { name: 'Delete' }) + .click(); + + await globalRulePom.isIndexPage(page); + + await uiHasToastMsg(page, { + hasText: 'success', + }); + }); +}); diff --git a/e2e/tests/global_rules.crud-required-fields.spec.ts b/e2e/tests/global_rules.crud-required-fields.spec.ts new file mode 100644 index 000000000..fe04eec09 --- /dev/null +++ b/e2e/tests/global_rules.crud-required-fields.spec.ts @@ -0,0 +1,110 @@ +/** + * 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 { globalRulePom } from '@e2e/pom/global_rules'; +import { test } from '@e2e/utils/test'; +import { + uiFillMonacoEditor, + uiGetMonacoEditor, + uiHasToastMsg, +} from '@e2e/utils/ui'; +import { expect } from '@playwright/test'; + +test('should CRUD global rule with required fields only', async ({ page }) => { + let globalRuleId: string; + + await test.step('navigate to add global rule page', async () => { + await globalRulePom.toAdd(page); + await globalRulePom.isAddPage(page); + }); + + await test.step('add global rule with plugins only', async () => { + // ID field should be auto-generated + const idInput = page.getByLabel('ID'); + await expect(idInput).toBeVisible(); + await expect(idInput).not.toHaveValue(''); + globalRuleId = await idInput.inputValue(); + + // Select a plugin - using response-rewrite as it's simple + const selectPluginBtn = page.getByRole('button', { + name: 'Select Plugins', + }); + await selectPluginBtn.click(); + + // Plugin selection dialog should appear + const dialog = page.getByRole('dialog', { name: 'Select Plugins' }); + await expect(dialog).toBeVisible(); + + // Search and add response-rewrite plugin + const searchInput = dialog.getByPlaceholder('Search'); + await searchInput.fill('response-rewrite'); + + // Click Add button for the plugin + await dialog + .getByTestId('plugin-response-rewrite') + .getByRole('button', { name: 'Add' }) + .click(); + + // Plugin dialog should appear + const pluginDialog = page.getByRole('dialog', { name: 'Add Plugin' }); + await expect(pluginDialog).toBeVisible(); + + // Add minimal plugin configuration using Monaco editor + const pluginEditor = await uiGetMonacoEditor(page, pluginDialog); + await uiFillMonacoEditor(page, pluginEditor, '{"body": "test response"}'); + + // Submit plugin + await pluginDialog.getByRole('button', { name: 'Add' }).click(); + await expect(pluginDialog).toBeHidden(); + + // Submit the form + await globalRulePom.getAddBtn(page).click(); + + // Should show success message + await uiHasToastMsg(page, { + hasText: 'success', + }); + + // Should redirect to detail page + await globalRulePom.isDetailPage(page); + }); + + await test.step('verify global rule was created', async () => { + // Verify we're on the detail page with correct ID + await expect(page).toHaveURL( + (url) => url.pathname.endsWith(`/global_rules/detail/${globalRuleId}`) + ); + + // Verify we're on the detail page + await globalRulePom.isDetailPage(page); + }); + + await test.step('delete global rule from detail page', async () => { + await page.getByRole('button', { name: 'Delete' }).click(); + + await page + .getByRole('dialog', { name: 'Delete Global Rule' }) + .getByRole('button', { name: 'Delete' }) + .click(); + + await globalRulePom.isIndexPage(page); + + await uiHasToastMsg(page, { + hasText: 'success', + }); + }); +}); diff --git a/e2e/tests/global_rules.list.spec.ts b/e2e/tests/global_rules.list.spec.ts new file mode 100644 index 000000000..bef295f88 --- /dev/null +++ b/e2e/tests/global_rules.list.spec.ts @@ -0,0 +1,115 @@ +/** + * 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 { globalRulePom } from '@e2e/pom/global_rules'; +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 { API_GLOBAL_RULES } from '@/config/constant'; + +test('should navigate to global rules list page', async ({ page }) => { + await test.step('navigate to global rules page', async () => { + await globalRulePom.getGlobalRuleNavBtn(page).click(); + await globalRulePom.isIndexPage(page); + }); + + await test.step('verify global rules page components', async () => { + await expect(globalRulePom.getAddGlobalRuleBtn(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(); + }); +}); + +// Helper function to delete all global rules +const deleteAllGlobalRules = async (req: typeof e2eReq) => { + const res = await req.get(API_GLOBAL_RULES); + const globalRules = res.data?.list || []; + await Promise.all( + globalRules.map((item: { value: { id: string } }) => + req.delete(`${API_GLOBAL_RULES}/${item.value.id}`).catch(() => { + // Ignore errors + }) + ) + ); +}; + +interface GlobalRule { + id: string; + plugins: Record; +} + +const globalRules: GlobalRule[] = Array.from({ length: 11 }, (_, i) => ({ + id: `global_rule_id_${i + 1}`, + plugins: { + 'response-rewrite': { + headers: { + 'X-Test-Rule': `global-rule-${i + 1}`, + }, + }, + }, +})); + +test.describe('page and page_size should work correctly', () => { + test.describe.configure({ mode: 'serial' }); + test.beforeAll(async () => { + await deleteAllGlobalRules(e2eReq); + await Promise.all( + globalRules.map((d) => + e2eReq.put(`${API_GLOBAL_RULES}/${d.id}`, { + plugins: d.plugins, + }) + ) + ); + }); + + test.afterAll(async () => { + // Get current list and only delete those that exist + const res = await e2eReq.get(API_GLOBAL_RULES); + const existingRules = res.data?.list || []; + await Promise.all( + existingRules.map((item: { value: { id: string } }) => + e2eReq.delete(`${API_GLOBAL_RULES}/${item.value.id}`).catch(() => { + // Ignore errors + }) + ) + ); + }); + + // Setup pagination tests with global-rule-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: /global_rule_id_/ }) + .all(); + const ids = await Promise.all(itemsInPage.map((v) => v.textContent())); + return globalRules.filter((d) => !ids.includes(d.id)); + }; + + setupPaginationTests(test, { + pom: globalRulePom, + items: globalRules, + filterItemsNotInPage, + getCell: (page, item) => page.getByRole('cell', { name: item.id }).first(), + }); +});