diff --git a/apps/e2e/src/generated/test-ids.ts b/apps/e2e/src/generated/test-ids.ts index 8e71fd15b..99b3ea413 100644 --- a/apps/e2e/src/generated/test-ids.ts +++ b/apps/e2e/src/generated/test-ids.ts @@ -1283,7 +1283,7 @@ export const TestIds = { credAddBtn: 'cred-add-btn', /** button · Clear filters */ credClearFilters: 'cred-clear-filters', - /** button */ + /** button · Close saved credentials */ credCloseBtn: 'cred-close-btn', /** button · Delete */ credDelete: (id: string | number) => `cred-delete-${id}`, diff --git a/apps/web/src/frontend/features/connections/components/CredentialManager.test.tsx b/apps/web/src/frontend/features/connections/components/CredentialManager.test.tsx new file mode 100644 index 000000000..c2a952b90 --- /dev/null +++ b/apps/web/src/frontend/features/connections/components/CredentialManager.test.tsx @@ -0,0 +1,48 @@ +// @vitest-environment jsdom +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +// The editor store loads the theme store, which reads the colour scheme. +window.matchMedia ??= ((media: string) => ({ + matches: false, + media, + onchange: null, + addEventListener: () => {}, + removeEventListener: () => {}, + addListener: () => {}, + removeListener: () => {}, + dispatchEvent: () => false, +})) as unknown as typeof window.matchMedia; + +// The credential form is its own layer; a stand-in is enough to open it. +vi.mock('./ConnectionModal', () => ({ + ConnectionModal: ({ open }: { open: boolean }) => (open ?
: null), +})); + +import { CredentialManager } from './CredentialManager'; + +describe('CredentialManager', () => { + it('is a modal dialog named Saved Credentials, with a named close button', () => { + render( undefined} />); + const dialog = screen.getByRole('dialog', { name: 'Saved Credentials' }); + expect(dialog.getAttribute('aria-modal')).toBe('true'); + expect(screen.getByRole('button', { name: 'Close saved credentials' })).toBeTruthy(); + }); + + it('closes on Escape', () => { + const onClose = vi.fn(); + render(); + fireEvent.keyDown(window, { key: 'Escape' }); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it('leaves Escape to the credential form while it is open on top', () => { + const onClose = vi.fn(); + render(); + fireEvent.click(screen.getByRole('button', { name: /Add Credential/ })); + expect(screen.getByTestId('connection-form')).toBeTruthy(); + fireEvent.keyDown(window, { key: 'Escape' }); + expect(onClose).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/web/src/frontend/features/connections/components/CredentialManager.tsx b/apps/web/src/frontend/features/connections/components/CredentialManager.tsx index a32f23176..7a300b5da 100644 --- a/apps/web/src/frontend/features/connections/components/CredentialManager.tsx +++ b/apps/web/src/frontend/features/connections/components/CredentialManager.tsx @@ -21,6 +21,7 @@ import { CloudProviderCredentialsSection } from '@/features/connections/componen import { PROVIDER_SETTINGS, type ConnectionOptions, type Dialect } from '@/shared/lib/provider-settings'; import { dialectLabel } from '@/shared/lib/dialectLabel'; import { sessionPasswordMap, setSessionPassword } from '@/shared/lib/sessionPasswords'; +import { useEscapeKey } from '@/shared/lib/useEscapeKey'; import type { SavedConnectionSummary } from '@/shared/api/authApi'; interface Props { @@ -157,6 +158,9 @@ export const CredentialManager: React.FC = ({ open, onClose }) => { return groups; }, [visible]); + // Escape closes the manager, but not while the credential form is open on top. + useEscapeKey(onClose, open && !adding && !editing); + if (!open) return null; return createPortal( @@ -165,6 +169,9 @@ export const CredentialManager: React.FC = ({ open, onClose }) => { className="fixed inset-0 z-[90] flex items-center justify-center bg-black/80 backdrop-blur-sm p-4" >
e.stopPropagation()} > @@ -174,7 +181,7 @@ export const CredentialManager: React.FC = ({ open, onClose }) => {
-

Saved Credentials

+

Saved Credentials

{tab === 'database' ? 'Encrypted DB connections · reusable from the connection dropdowns' @@ -192,6 +199,8 @@ export const CredentialManager: React.FC = ({ open, onClose }) => { )}