diff --git a/packages/frontend/editor-ui/src/features/ai/mcpAccess/components/ConnectionParameter.vue b/packages/frontend/editor-ui/src/features/ai/mcpAccess/components/ConnectionParameter.vue index 520990e4adf..4442cc394e0 100644 --- a/packages/frontend/editor-ui/src/features/ai/mcpAccess/components/ConnectionParameter.vue +++ b/packages/frontend/editor-ui/src/features/ai/mcpAccess/components/ConnectionParameter.vue @@ -170,6 +170,7 @@ const handleCopy = async (value: string) => { } button { + height: auto; border: none; border-radius: 0; box-shadow: none; @@ -196,8 +197,13 @@ const handleCopy = async (value: string) => { .copy-button-wrapper { display: flex; - align-items: center; + align-items: stretch; border-left: var(--border); + + // Tooltip triggers wrap each button in a span, which must stretch it too. + > span { + display: flex; + } } .copy-button { diff --git a/packages/testing/playwright/pages/SettingsMcpPage.ts b/packages/testing/playwright/pages/SettingsMcpPage.ts new file mode 100644 index 00000000000..dbf8f01b44b --- /dev/null +++ b/packages/testing/playwright/pages/SettingsMcpPage.ts @@ -0,0 +1,23 @@ +import { BasePage } from './BasePage'; + +export class SettingsMcpPage extends BasePage { + async goto() { + await this.page.goto('/settings/mcp'); + } + + getConnectButton() { + return this.page.getByTestId('mcp-connect-client-button'); + } + + getCopyButton() { + return this.page + .getByRole('dialog') + .getByTestId('connection-parameter-value') + .first() + .getByRole('button'); + } + + getCopyButtonContainer() { + return this.page.getByRole('dialog').getByTestId('connection-parameter-value').first(); + } +} diff --git a/packages/testing/playwright/pages/n8nPage.ts b/packages/testing/playwright/pages/n8nPage.ts index 0ff70d5f051..2253b208a9a 100644 --- a/packages/testing/playwright/pages/n8nPage.ts +++ b/packages/testing/playwright/pages/n8nPage.ts @@ -39,6 +39,7 @@ import { SecretsProviderSettingsPage } from './SecretsProviderSettingsPage'; import { SecuritySettingsPage } from './SecuritySettingsPage'; import { SettingsEnvironmentPage } from './SettingsEnvironmentPage'; import { SettingsLogStreamingPage } from './SettingsLogStreamingPage'; +import { SettingsMcpPage } from './SettingsMcpPage'; import { SettingsPersonalPage } from './SettingsPersonalPage'; import { SettingsSsoPage } from './SettingsSsoPage'; import { SettingsUsersPage } from './SettingsUsersPage'; @@ -100,6 +101,7 @@ export class n8nPage { readonly oauthConsent: OAuthConsentPage; readonly projectSettings: ProjectSettingsPage; readonly settingsPersonal: SettingsPersonalPage; + readonly settingsMcp: SettingsMcpPage; readonly settingsLogStreaming: SettingsLogStreamingPage; readonly templateCredentialSetup: TemplateCredentialSetupPage; readonly templates: TemplatesPage; @@ -191,6 +193,7 @@ export class n8nPage { this.oauthConsent = new OAuthConsentPage(page); this.projectSettings = new ProjectSettingsPage(page); this.settingsPersonal = new SettingsPersonalPage(page); + this.settingsMcp = new SettingsMcpPage(page); this.settingsLogStreaming = new SettingsLogStreamingPage(page); this.templateCredentialSetup = new TemplateCredentialSetupPage(page); this.templates = new TemplatesPage(page); diff --git a/packages/testing/playwright/tests/e2e/mcp/mcp-copy-button-hover.spec.ts b/packages/testing/playwright/tests/e2e/mcp/mcp-copy-button-hover.spec.ts new file mode 100644 index 00000000000..08f7d8b7e26 --- /dev/null +++ b/packages/testing/playwright/tests/e2e/mcp/mcp-copy-button-hover.spec.ts @@ -0,0 +1,37 @@ +import { test, expect } from '../../../fixtures/base'; + +test.describe( + 'MCP connection parameter copy button', + { annotation: [{ type: 'owner', description: 'AI' }] }, + () => { + test('fills the copy control height on hover', async ({ n8n, api }) => { + // ADO-5385: The hover background must reach both edges of the copy control. + await api.setMcpAccess(true); + await n8n.start.fromHome(); + await n8n.settingsMcp.goto(); + await n8n.settingsMcp.getConnectButton().click(); + + const button = n8n.settingsMcp.getCopyButton(); + await expect(button).toBeVisible(); + const backgroundBeforeHover = await button.evaluate( + (element) => getComputedStyle(element).backgroundColor, + ); + await button.hover(); + const backgroundOnHover = await button.evaluate( + (element) => getComputedStyle(element).backgroundColor, + ); + expect(backgroundOnHover).not.toBe(backgroundBeforeHover); + + const buttonBox = await button.boundingBox(); + const containerBox = await n8n.settingsMcp.getCopyButtonContainer().boundingBox(); + expect(buttonBox).not.toBeNull(); + expect(containerBox).not.toBeNull(); + + // The input container has a one-pixel border on each edge. + expect.soft(Math.abs(buttonBox!.y - containerBox!.y)).toBeLessThanOrEqual(1); + expect( + Math.abs(buttonBox!.y + buttonBox!.height - containerBox!.y - containerBox!.height), + ).toBeLessThanOrEqual(1); + }); + }, +);