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 855218205 test: add E2E tests for plugin metadata resource (#3247)
855218205 is described below

commit 85521820539677ac84761fbfdfb6ebb31fd5a323
Author: Deep Shekhar Singh <[email protected]>
AuthorDate: Fri Nov 21 07:34:57 2025 +0530

    test: add E2E tests for plugin metadata resource (#3247)
    
    * test(plugin_metadata): add E2E tests for plugin metadata resource
    
    - Add POM (Page Object Model) for plugin metadata pages
    - Implement list page tests for navigation and search functionality
    - Implement CRUD tests with required fields only (simple metadata)
    - Implement CRUD tests with all fields (comprehensive metadata)
    - Tests verify add, edit, and delete operations
    - All 4 tests passing
    
    Closes #3089
    
    * test(e2e): add verification after editing plugin metadata
    
    - Add API verification to confirm configuration changes persist
    - Verify updated fields (time_iso8601, http_user_agent) appear in saved 
metadata
    - Address review feedback requesting verification of configuration changes
    
    * test(e2e): enhance plugin metadata dialog interactions with helper 
functions
---
 e2e/pom/plugin_metadata.ts                         |  45 +++++
 e2e/tests/plugin_metadata.crud-all-fields.spec.ts  | 222 +++++++++++++++++++++
 .../plugin_metadata.crud-required-fields.spec.ts   | 143 +++++++++++++
 e2e/tests/plugin_metadata.list.spec.ts             |  83 ++++++++
 4 files changed, 493 insertions(+)

diff --git a/e2e/pom/plugin_metadata.ts b/e2e/pom/plugin_metadata.ts
new file mode 100644
index 000000000..fa2a64a3e
--- /dev/null
+++ b/e2e/pom/plugin_metadata.ts
@@ -0,0 +1,45 @@
+/**
+ * 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 = {
+  getPluginMetadataNavBtn: (page: Page) =>
+    page.getByRole('link', { name: 'Plugin Metadata', exact: true }),
+  getSelectPluginsBtn: (page: Page) =>
+    page.getByRole('button', { name: 'Select Plugins' }),
+};
+
+const assert = {
+  isIndexPage: async (page: Page) => {
+    await expect(page).toHaveURL((url) =>
+      url.pathname.endsWith('/plugin_metadata')
+    );
+    const title = page.getByRole('heading', { name: 'Plugin Metadata' });
+    await expect(title).toBeVisible();
+  },
+};
+
+const goto = {
+  toIndex: (page: Page) => uiGoto(page, '/plugin_metadata'),
+};
+
+export const pluginMetadataPom = {
+  ...locator,
+  ...assert,
+  ...goto,
+};
diff --git a/e2e/tests/plugin_metadata.crud-all-fields.spec.ts 
b/e2e/tests/plugin_metadata.crud-all-fields.spec.ts
new file mode 100644
index 000000000..4f495bc15
--- /dev/null
+++ b/e2e/tests/plugin_metadata.crud-all-fields.spec.ts
@@ -0,0 +1,222 @@
+/**
+ * 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 { pluginMetadataPom } from '@e2e/pom/plugin_metadata';
+import { e2eReq } from '@e2e/utils/req';
+import { test } from '@e2e/utils/test';
+import {
+  uiFillMonacoEditor,
+  uiGetMonacoEditor,
+  uiHasToastMsg,
+} from '@e2e/utils/ui';
+import type { Locator } from '@playwright/test';
+import { expect } from '@playwright/test';
+
+import { API_PLUGIN_METADATA } from '@/config/constant';
+
+// Helper function to delete plugin metadata
+const deletePluginMetadata = async (req: typeof e2eReq, name: string) => {
+  await req.delete(`${API_PLUGIN_METADATA}/${name}`).catch(() => {
+    // Ignore errors if metadata doesn't exist
+  });
+};
+const getMonacoEditorValue = async (editPluginDialog: Locator) => {
+  let editorValue = '';
+  const textarea = editPluginDialog.locator('textarea');
+  if (await textarea.count() > 0) {
+    editorValue = await textarea.inputValue();
+  }
+  if (!editorValue || editorValue.trim() === '{') {
+    const lines = await 
editPluginDialog.locator('.view-line').allTextContents();
+    editorValue = lines.join('\n').replace(/\s+/g, ' ');
+  }
+  if (!editorValue || editorValue.trim() === '{') {
+    const allText = await editPluginDialog.textContent();
+    console.log('DEBUG: editorValue fallback failed, dialog text:', allText);
+  }
+  return editorValue;
+};
+
+// Helper function to close edit dialog
+const closeEditDialog = async (editPluginDialog: Locator) => {
+  const buttons = await editPluginDialog.locator('button').allTextContents();
+  console.log('DEBUG: Edit Plugin dialog buttons:', buttons);
+  let closed = false;
+  for (const [i, name] of buttons.entries()) {
+    if (name.trim().toLowerCase() === 'cancel') {
+      await editPluginDialog.locator('button').nth(i).click();
+      closed = true;
+      break;
+    }
+  }
+  if (!closed && buttons.length > 0) {
+    await editPluginDialog.locator('button').first().click();
+  }
+};
+
+test.beforeAll(async () => {
+  await deletePluginMetadata(e2eReq, 'http-logger');
+});
+
+test.afterAll(async () => {
+  await deletePluginMetadata(e2eReq, 'http-logger');
+});
+
+test('should CRUD plugin metadata with all fields', async ({ page }) => {
+  await pluginMetadataPom.toIndex(page);
+  await pluginMetadataPom.isIndexPage(page);
+
+  await test.step('add plugin metadata with comprehensive configuration', 
async () => {
+    // Click Select Plugins button
+    await pluginMetadataPom.getSelectPluginsBtn(page).click();
+
+    // Select Plugins dialog should appear
+    const selectPluginsDialog = page.getByRole('dialog', {
+      name: 'Select Plugins',
+    });
+    await expect(selectPluginsDialog).toBeVisible();
+
+    // Search for http-logger plugin
+    const searchInput = selectPluginsDialog.getByPlaceholder('Search');
+    await searchInput.fill('http-logger');
+
+    // Click Add button for http-logger
+    await selectPluginsDialog
+      .getByTestId('plugin-http-logger')
+      .getByRole('button', { name: 'Add' })
+      .click();
+
+    // Add Plugin dialog should appear
+    const addPluginDialog = page.getByRole('dialog', { name: 'Add Plugin' });
+    await expect(addPluginDialog).toBeVisible();
+
+    // Fill in comprehensive configuration with all available fields
+    const pluginEditor = await uiGetMonacoEditor(page, addPluginDialog);
+    await uiFillMonacoEditor(
+      page,
+      pluginEditor,
+      JSON.stringify({
+        log_format: {
+          host: '$host',
+          client_ip: '$remote_addr',
+          request_method: '$request_method',
+          request_uri: '$request_uri',
+          status: '$status',
+          body_bytes_sent: '$body_bytes_sent',
+          request_time: '$request_time',
+          upstream_response_time: '$upstream_response_time',
+        },
+      })
+    );
+
+    // Click Add button
+    await addPluginDialog.getByRole('button', { name: 'Add' }).click();
+
+    // Should show success message
+    await uiHasToastMsg(page, {
+      hasText: 'success',
+    });
+
+    // Dialog should close
+    await expect(addPluginDialog).toBeHidden();
+
+    // Plugin card should now be visible
+    const httpLoggerCard = page.getByTestId('plugin-http-logger');
+    await expect(httpLoggerCard).toBeVisible();
+  });
+
+  await test.step('edit plugin metadata with extended fields', async () => {
+    // Find the http-logger card
+    const httpLoggerCard = page.getByTestId('plugin-http-logger');
+
+    // Click Edit button
+    await httpLoggerCard.getByRole('button', { name: 'Edit' }).click();
+
+    // Edit Plugin dialog should appear
+    const editPluginDialog = page.getByRole('dialog', { name: 'Edit Plugin' });
+    await expect(editPluginDialog).toBeVisible();
+
+    // Verify existing configuration is shown
+    await expect(editPluginDialog.getByText('log_format')).toBeVisible();
+
+    // Update the configuration with additional fields
+    const pluginEditor = await uiGetMonacoEditor(page, editPluginDialog);
+    await uiFillMonacoEditor(
+      page,
+      pluginEditor,
+      JSON.stringify({
+        log_format: {
+          host: '$host',
+          client_ip: '$remote_addr',
+          request_method: '$request_method',
+          request_uri: '$request_uri',
+          status: '$status',
+          body_bytes_sent: '$body_bytes_sent',
+          request_time: '$request_time',
+          upstream_response_time: '$upstream_response_time',
+          time: '$time_iso8601',
+          user_agent: '$http_user_agent',
+        },
+      })
+    );
+
+    // Click Save button
+    await editPluginDialog.getByRole('button', { name: 'Save' }).click();
+
+    // Should show success message
+    await uiHasToastMsg(page, {
+      hasText: 'success',
+    });
+
+    // Dialog should close
+    await expect(editPluginDialog).toBeHidden();
+  });
+
+  await test.step('verify configuration changes were saved', async () => {
+    // Re-open the edit dialog via UI
+    const httpLoggerCard = page.getByTestId('plugin-http-logger');
+    await httpLoggerCard.getByRole('button', { name: 'Edit' }).click();
+    const editPluginDialog = page.getByRole('dialog', { name: 'Edit Plugin' });
+    await expect(editPluginDialog).toBeVisible();
+
+    // Get Monaco editor value using helper
+    const editorValue = await getMonacoEditorValue(editPluginDialog);
+    expect(editorValue).toMatch(/"time"\s*:\s*"\$time_iso8601"/);
+    expect(editorValue).toMatch(/"user_agent"\s*:\s*"\$http_user_agent"/);
+    expect(editorValue).toMatch(/"host"\s*:\s*"\$host"/);
+    expect(editorValue).toMatch(/"client_ip"\s*:\s*"\$remote_addr"/);
+
+    // Close the dialog using helper
+    await closeEditDialog(editPluginDialog);
+    await expect(editPluginDialog).toBeHidden();
+  });
+
+  await test.step('delete plugin metadata', async () => {
+    // Find the http-logger card
+    const httpLoggerCard = page.getByTestId('plugin-http-logger');
+
+    // Click Delete button
+    await httpLoggerCard.getByRole('button', { name: 'Delete' }).click();
+
+    // Should show success message
+    await uiHasToastMsg(page, {
+      hasText: 'success',
+    });
+
+    // Card should be removed
+    await expect(httpLoggerCard).toBeHidden();
+  });
+});
diff --git a/e2e/tests/plugin_metadata.crud-required-fields.spec.ts 
b/e2e/tests/plugin_metadata.crud-required-fields.spec.ts
new file mode 100644
index 000000000..1175fefb9
--- /dev/null
+++ b/e2e/tests/plugin_metadata.crud-required-fields.spec.ts
@@ -0,0 +1,143 @@
+/**
+ * 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 { pluginMetadataPom } from '@e2e/pom/plugin_metadata';
+import { e2eReq } from '@e2e/utils/req';
+import { test } from '@e2e/utils/test';
+import {
+  uiFillMonacoEditor,
+  uiGetMonacoEditor,
+  uiHasToastMsg,
+} from '@e2e/utils/ui';
+import { expect } from '@playwright/test';
+
+import { API_PLUGIN_METADATA } from '@/config/constant';
+
+// Helper function to delete plugin metadata
+const deletePluginMetadata = async (req: typeof e2eReq, name: string) => {
+  await req.delete(`${API_PLUGIN_METADATA}/${name}`).catch(() => {
+    // Ignore errors if metadata doesn't exist
+  });
+};
+
+test.beforeAll(async () => {
+  await deletePluginMetadata(e2eReq, 'syslog');
+});
+
+test.afterAll(async () => {
+  await deletePluginMetadata(e2eReq, 'syslog');
+});
+
+test('should CRUD plugin metadata with required fields only', async ({
+  page,
+}) => {
+  await pluginMetadataPom.toIndex(page);
+  await pluginMetadataPom.isIndexPage(page);
+
+  await test.step('add plugin metadata with simple configuration', async () => 
{
+    // Click Select Plugins button
+    await pluginMetadataPom.getSelectPluginsBtn(page).click();
+
+    // Select Plugins dialog should appear
+    const selectPluginsDialog = page.getByRole('dialog', {
+      name: 'Select Plugins',
+    });
+    await expect(selectPluginsDialog).toBeVisible();
+
+    // Search for syslog plugin
+    const searchInput = selectPluginsDialog.getByPlaceholder('Search');
+    await searchInput.fill('syslog');
+
+    // Click Add button for syslog
+    await selectPluginsDialog
+      .getByTestId('plugin-syslog')
+      .getByRole('button', { name: 'Add' })
+      .click();
+
+    // Add Plugin dialog should appear
+    const addPluginDialog = page.getByRole('dialog', { name: 'Add Plugin' });
+    await expect(addPluginDialog).toBeVisible();
+
+    // Fill in minimal required configuration
+    const pluginEditor = await uiGetMonacoEditor(page, addPluginDialog);
+    await uiFillMonacoEditor(page, pluginEditor, '{"host": "127.0.0.1"}');
+
+    // Click Add button
+    await addPluginDialog.getByRole('button', { name: 'Add' }).click();
+
+    // Should show success message
+    await uiHasToastMsg(page, {
+      hasText: 'success',
+    });
+
+    // Dialog should close
+    await expect(addPluginDialog).toBeHidden();
+
+    // Plugin card should now be visible
+    const syslogCard = page.getByTestId('plugin-syslog');
+    await expect(syslogCard).toBeVisible();
+  });
+
+  await test.step('edit plugin metadata with simple update', async () => {
+    // Find the syslog card
+    const syslogCard = page.getByTestId('plugin-syslog');
+
+    // Click Edit button
+    await syslogCard.getByRole('button', { name: 'Edit' }).click();
+
+    // Edit Plugin dialog should appear
+    const editPluginDialog = page.getByRole('dialog', { name: 'Edit Plugin' });
+    await expect(editPluginDialog).toBeVisible();
+
+    // Verify existing configuration is shown
+    await expect(editPluginDialog.getByText('host')).toBeVisible();
+
+    // Update the configuration
+    const pluginEditor = await uiGetMonacoEditor(page, editPluginDialog);
+    await uiFillMonacoEditor(
+      page,
+      pluginEditor,
+      '{"host": "127.0.0.1", "port": 5140}'
+    );
+
+    // Click Save button
+    await editPluginDialog.getByRole('button', { name: 'Save' }).click();
+
+    // Should show success message
+    await uiHasToastMsg(page, {
+      hasText: 'success',
+    });
+
+    // Dialog should close
+    await expect(editPluginDialog).toBeHidden();
+  });
+
+  await test.step('delete plugin metadata', async () => {
+    // Find the syslog card
+    const syslogCard = page.getByTestId('plugin-syslog');
+
+    // Click Delete button
+    await syslogCard.getByRole('button', { name: 'Delete' }).click();
+
+    // Should show success message
+    await uiHasToastMsg(page, {
+      hasText: 'success',
+    });
+
+    // Card should be removed
+    await expect(syslogCard).toBeHidden();
+  });
+});
diff --git a/e2e/tests/plugin_metadata.list.spec.ts 
b/e2e/tests/plugin_metadata.list.spec.ts
new file mode 100644
index 000000000..c6b90cd27
--- /dev/null
+++ b/e2e/tests/plugin_metadata.list.spec.ts
@@ -0,0 +1,83 @@
+/**
+ * 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 { pluginMetadataPom } from '@e2e/pom/plugin_metadata';
+import { e2eReq } from '@e2e/utils/req';
+import { test } from '@e2e/utils/test';
+import { expect } from '@playwright/test';
+
+import { API_PLUGIN_METADATA } from '@/config/constant';
+
+// Helper function to delete all plugin metadata
+const deleteAllPluginMetadata = async (req: typeof e2eReq) => {
+  // Plugin metadata doesn't have a list endpoint, so we'll delete known 
plugins
+  const pluginsToClean = [
+    'http-logger',
+    'syslog',
+    'skywalking',
+    'error-log-logger',
+  ];
+  await Promise.all(
+    pluginsToClean.map((name) =>
+      req.delete(`${API_PLUGIN_METADATA}/${name}`).catch(() => {
+        // Ignore errors if metadata doesn't exist
+      })
+    )
+  );
+};
+
+test.beforeAll(async () => {
+  await deleteAllPluginMetadata(e2eReq);
+});
+
+test.afterAll(async () => {
+  await deleteAllPluginMetadata(e2eReq);
+});
+
+test('should navigate to plugin metadata page', async ({ page }) => {
+  await test.step('navigate to plugin metadata page', async () => {
+    await pluginMetadataPom.getPluginMetadataNavBtn(page).click();
+    await pluginMetadataPom.isIndexPage(page);
+  });
+
+  await test.step('verify plugin metadata page components', async () => {
+    // Search box should be visible
+    const searchBox = page.getByPlaceholder('Search');
+    await expect(searchBox).toBeVisible();
+
+    // Select Plugins button should be visible
+    await expect(pluginMetadataPom.getSelectPluginsBtn(page)).toBeVisible();
+  });
+});
+
+test('should search for plugin metadata', async ({ page }) => {
+  await pluginMetadataPom.toIndex(page);
+  await pluginMetadataPom.isIndexPage(page);
+
+  await test.step('search filters plugin cards', async () => {
+    const searchBox = page.getByPlaceholder('Search');
+    await searchBox.fill('http-logger');
+
+    // Only http-logger related cards should be visible if they exist
+    // For now just verify search box works
+    await expect(searchBox).toHaveValue('http-logger');
+
+    // Clear search
+    await searchBox.clear();
+    await expect(searchBox).toHaveValue('');
+  });
+});

Reply via email to