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 9a7661b6f test: add E2E tests for global rules (#3248)
9a7661b6f is described below
commit 9a7661b6f6144bfd1002b941710c142a30777fb6
Author: Deep Shekhar Singh <[email protected]>
AuthorDate: Fri Nov 21 07:24:24 2025 +0530
test: add E2E tests for global rules (#3248)
* test: add E2E tests for global rules (#3088)
- Added global_rules POM with navigation and assertions
- Added list test with pagination support (11 items)
- Added CRUD test with required fields (single plugin)
- Added CRUD test with all fields (multiple plugins)
- Tests verify create and delete functionality
- All 5 tests passing
Closes #3088
* chore: add license header to apisix_conf.yml
* Restored apisix_conf.yml with explanatory comments
Restored comments for clarity on configuration options.
---
e2e/pom/global_rules.ts | 62 ++++++++++
e2e/tests/global_rules.crud-all-fields.spec.ts | 137 +++++++++++++++++++++
.../global_rules.crud-required-fields.spec.ts | 110 +++++++++++++++++
e2e/tests/global_rules.list.spec.ts | 115 +++++++++++++++++
4 files changed, 424 insertions(+)
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<string, unknown>;
+}
+
+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(),
+ });
+});