diff --git a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js index cd8f6bcbc..111e2aaf8 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js +++ b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js @@ -474,10 +474,16 @@ define('pgadmin.node.database', [ pgBrowser.Events.trigger( 'pgadmin:database:connected', _item, _data ); - /* Call enable/disable menu function after database is connected. - To make sure all the menus for database is in the right state */ - pgBrowser.enable_disable_menus(_item); - pgBrowser.Nodes['database'].callbacks.selected(_item, _data); + /* Refresh the menus after the database is connected, so that + they are in the right state. The menus always belong to the + selected node, which may not be this database: it can be + connected by expanding it, e.g. whilst the previous tree state + is restored, so only treat it as selected if it really is. */ + if (_tree.selected() === _item) { + pgBrowser.Nodes['database'].callbacks.selected(_item, _data); + } else { + pgBrowser.enable_disable_menus(); + } if (!_connected) { setTimeout(function() { diff --git a/web/pgadmin/browser/server_groups/servers/static/js/server.js b/web/pgadmin/browser/server_groups/servers/static/js/server.js index 523ab79b2..f4e6d0dc0 100644 --- a/web/pgadmin/browser/server_groups/servers/static/js/server.js +++ b/web/pgadmin/browser/server_groups/servers/static/js/server.js @@ -698,9 +698,11 @@ define('pgadmin.node.server', [ // Load dashboard pgBrowser.Events.trigger('pgadmin-browser:tree:selected', _item, _data, node); - /* Call enable/disable menu function after database is connected. - To make sure all the menus for database is in the right state */ - pgBrowser.enable_disable_menus(_item); + /* Refresh the menus after the server is connected, so that they + are in the right state. They always belong to the selected node, + which may not be this server if it was connected by expanding + it, so do not pass the server's item here. */ + pgBrowser.enable_disable_menus(); /* Below code is added to show the error message if connection is successful, but there is an error to diff --git a/web/regression/javascript/browser/node_connect_menus_spec.js b/web/regression/javascript/browser/node_connect_menus_spec.js new file mode 100644 index 000000000..f50609903 --- /dev/null +++ b/web/regression/javascript/browser/node_connect_menus_spec.js @@ -0,0 +1,148 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2026, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +const mockPost = jest.fn(); + +jest.mock('../../../pgadmin/static/js/api_instance', () => ({ + __esModule: true, + default: () => ({ + post: mockPost, get: jest.fn(() => Promise.resolve({data: {}})), + delete: jest.fn(), + }), + parseApiError: (e) => e, +})); +jest.mock('../../../pgadmin/static/js/Dialogs/index', () => ({ + showServerPassword: jest.fn(), +})); + +const pgBrowser = { + Nodes: {}, + Node: { + extend: (props) => ({...props, generate_url: () => '/connect'}), + callbacks: {selected: jest.fn()}, + }, + Collection: {extend: (props) => props}, + Events: {trigger: jest.fn(), on: jest.fn()}, + enable_disable_menus: jest.fn(), + notifier: {success: jest.fn(), info: jest.fn(), warning: jest.fn()}, + stdW: {md: 650}, +}; +const pgAdmin = {Browser: pgBrowser}; + +// The node modules register themselves through an AMD style define(); load +// them with a stub that hands each factory our fake browser. +global.define = (_name, _deps, factory) => { + factory((s) => s, () => '', pgAdmin, pgBrowser, {id: 1}, {}); +}; +require('../../../pgadmin/browser/server_groups/servers/databases/static/js/database'); +require('../../../pgadmin/browser/server_groups/servers/static/js/server'); +delete global.define; + +// Let the mocked API calls resolve and the success handlers run. +const flushPromises = async () => { + for (let i = 0; i < 5; i++) { + await Promise.resolve(); + } +}; + +describe('database node connection', () => { + let tree, serverItem, dbItem, dbData; + + beforeEach(() => { + jest.useFakeTimers(); + serverItem = {id: 'server'}; + dbItem = {id: 'db'}; + dbData = {_type: 'database', connected: false, allowConn: true, icon: 'x'}; + tree = { + selected: jest.fn(), + addIcon: jest.fn(), + removeIcon: jest.fn(), + select: jest.fn(), + open: jest.fn(), + }; + pgBrowser.tree = tree; + pgBrowser.enable_disable_menus.mockClear(); + pgBrowser.Node.callbacks.selected.mockClear(); + mockPost.mockReset(); + mockPost.mockResolvedValue({data: { + success: 1, info: 'Connected', data: {connected: true, icon: 'y'}, + }}); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + const connectByExpanding = async () => { + const node = pgBrowser.Nodes['database']; + expect(node.callbacks.beforeopen.call(node, dbItem, dbData)).toBe(false); + await flushPromises(); + }; + + it('keeps the menus on the selected server when a database is connected by expanding it', async () => { + tree.selected.mockReturnValue(serverItem); + + await connectByExpanding(); + + expect(dbData.connected).toBe(true); + expect(pgBrowser.enable_disable_menus).toHaveBeenCalledWith(); + expect(pgBrowser.enable_disable_menus).not.toHaveBeenCalledWith(dbItem); + expect(pgBrowser.Node.callbacks.selected).not.toHaveBeenCalled(); + }); + + it('updates the menus for the database when it is the selected node', async () => { + tree.selected.mockReturnValue(dbItem); + + await connectByExpanding(); + + expect(dbData.connected).toBe(true); + expect(pgBrowser.Node.callbacks.selected).toHaveBeenCalledWith(dbItem, dbData); + }); +}); + +describe('server node connection', () => { + let tree, otherItem, serverItem, serverData; + + beforeEach(() => { + jest.useFakeTimers(); + otherItem = {id: 'other'}; + serverItem = {id: 'server'}; + serverData = {_type: 'server', _id: 1, connected: false, icon: 'x'}; + tree = { + selected: jest.fn(() => otherItem), + addIcon: jest.fn(), + removeIcon: jest.fn(), + setLeaf: jest.fn(), + setInode: jest.fn(), + select: jest.fn(), + open: jest.fn(), + }; + pgBrowser.tree = tree; + pgBrowser.enable_disable_menus.mockClear(); + mockPost.mockReset(); + mockPost.mockResolvedValue({data: { + success: 1, info: 'Connected', + data: {connected: true, icon: 'y', version: 180000}, + }}); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('keeps the menus on the selected node when a server is connected by expanding it', async () => { + const node = pgBrowser.Nodes['server']; + expect(node.callbacks.beforeopen.call(node, serverItem, serverData)).toBe(false); + await flushPromises(); + + expect(serverData.connected).toBe(true); + expect(pgBrowser.enable_disable_menus).toHaveBeenCalledWith(); + expect(pgBrowser.enable_disable_menus).not.toHaveBeenCalledWith(serverItem); + }); +});