diff --git a/e2e/pom/plugin_metadata.ts b/e2e/pom/plugin_metadata.ts new file mode 100644 index 0000000000..fa2a64a3e2 --- /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 0000000000..cfe1fb2a6a --- /dev/null +++ b/e2e/tests/plugin_metadata.crud-all-fields.spec.ts @@ -0,0 +1,169 @@ +/** + * 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, '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('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 0000000000..1175fefb9b --- /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 0000000000..c6b90cd27b --- /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(''); + }); +});