diff --git a/frontend/src/__tests__/apikeys.test.ts b/frontend/src/__tests__/apikeys.test.ts index ecf634f16..475939ec0 100644 --- a/frontend/src/__tests__/apikeys.test.ts +++ b/frontend/src/__tests__/apikeys.test.ts @@ -874,7 +874,7 @@ describe('API Keys Module', () => { jest.useRealTimers(); }); - test('copy button shows alert on clipboard error', async () => { + test('copy failure shows an inline error and selects the key, not a native alert (#705)', async () => { const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {}); const writeTextMock = jest.fn().mockRejectedValue(new Error('Clipboard error')); Object.defineProperty(navigator, 'clipboard', { @@ -885,12 +885,24 @@ describe('API Keys Module', () => { showKeyCreatedModal('test-api-key'); - const copyBtn = document.getElementById('copy-apikey-btn'); - copyBtn?.click(); + const copyError = document.getElementById('apikey-copy-error') as HTMLElement; + expect(copyError.getAttribute('role')).toBe('alert'); + expect(copyError.classList.contains('hidden')).toBe(true); + + document.getElementById('copy-apikey-btn')?.click(); await new Promise(resolve => setTimeout(resolve, 0)); - expect(window.alert).toHaveBeenCalledWith('Failed to copy to clipboard. Please copy manually.'); + expect(window.alert).not.toHaveBeenCalled(); + expect(copyError.classList.contains('hidden')).toBe(false); + expect(copyError.textContent).toContain('The key is selected'); + expect(window.getSelection()?.toString()).toBe('test-api-key'); + + // A later successful copy clears the error again. + writeTextMock.mockResolvedValueOnce(undefined); + document.getElementById('copy-apikey-btn')?.click(); + await new Promise(resolve => setTimeout(resolve, 0)); + expect(copyError.classList.contains('hidden')).toBe(true); consoleError.mockRestore(); }); diff --git a/frontend/src/__tests__/users.test.ts b/frontend/src/__tests__/users.test.ts index 755426520..96eb58552 100644 --- a/frontend/src/__tests__/users.test.ts +++ b/frontend/src/__tests__/users.test.ts @@ -2103,33 +2103,18 @@ describe('users/handlers', () => { expect(api.updateUser).not.toHaveBeenCalled(); }); - it('should set up bulk group handler', async () => { - const mockUsers = [{ id: '1', email: 'test@test.com', groups: [], mfa_enabled: false }]; - userState.setAllUsers(mockUsers as any); - userState.addSelectedUserId('1'); - (global.prompt as jest.Mock).mockReturnValue('admins'); - - userHandlers.setupUserHandlers(); - - const bulkGroupBtn = document.getElementById('bulk-group-btn'); - bulkGroupBtn?.click(); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(api.updateUser).toHaveBeenCalled(); - }); - - it('should handle cancelled group prompt', async () => { + it('never falls back to a native prompt() for bulk add to group (#705)', async () => { userState.addSelectedUserId('1'); - (global.prompt as jest.Mock).mockReturnValue(null); userHandlers.setupUserHandlers(); - const bulkGroupBtn = document.getElementById('bulk-group-btn'); - bulkGroupBtn?.click(); + // A stray legacy #bulk-group-btn must stay inert: the dropdown is the + // only bulk add-to-group path. + document.getElementById('bulk-group-btn')?.click(); await new Promise(resolve => setTimeout(resolve, 0)); + expect(global.prompt).not.toHaveBeenCalled(); expect(api.updateUser).not.toHaveBeenCalled(); }); }); diff --git a/frontend/src/apikeys.ts b/frontend/src/apikeys.ts index 31cd71265..ce3d3789f 100644 --- a/frontend/src/apikeys.ts +++ b/frontend/src/apikeys.ts @@ -431,6 +431,7 @@ export function showKeyCreatedModal(apiKey: string): void { ${escapeHtml(apiKey)} +