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

Reply via email to