diff --git a/docs/en_US/release_notes_9_16.rst b/docs/en_US/release_notes_9_16.rst index 4f0740cdff0..32cc5a1859c 100644 --- a/docs/en_US/release_notes_9_16.rst +++ b/docs/en_US/release_notes_9_16.rst @@ -40,6 +40,7 @@ Bug fixes ********* | `Issue #6308 `_ - Fix the infinite loading spinner after an idle database connection is silently dropped, by detecting stale connections and offering a reconnect dialog. + | `Issue #7596 `_ - Fix the Query Tool turning into a blank white screen when the runtime has a malformed default locale, by guarding the Query History date/time formatting against the resulting RangeError. | `Issue #9595 `_ - Fix missing ALTER ... SET DEFAULT statements for inherited columns in the generated table SQL/EDIT script. | `Issue #9677 `_ - Fix the Unlogged table toggle in table properties not generating any ALTER TABLE ... SET LOGGED/UNLOGGED statement. | `Issue #9828 `_ - Fix tool calls failing against OpenAI-compatible providers that emit empty/null name, arguments, or id fields in streaming continuation deltas. diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/QueryHistory.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/QueryHistory.jsx index 289e0b037d1..00422cf6d88 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/QueryHistory.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/QueryHistory.jsx @@ -124,12 +124,27 @@ export const QuerySources = { }, }; -function getDateFormatted(date) { - return date.toLocaleDateString(); +// On some runtimes the default locale (derived from the OS/environment) is +// malformed, which makes Date.prototype.toLocaleDateString/toLocaleTimeString +// throw "RangeError: Incorrect locale information provided". As these are +// called while rendering the Query History panel, an uncaught throw unmounts +// the whole SQL editor and the user sees a blank white screen, losing any +// unsaved work. Fall back to a moment-based format (moment uses its own +// locale data and does not depend on the broken Intl default). See #7596. +export function getDateFormatted(date) { + try { + return date.toLocaleDateString(); + } catch { + return moment(date).format('L'); + } } -function getTimeFormatted(time) { - return time.toLocaleTimeString(); +export function getTimeFormatted(time) { + try { + return time.toLocaleTimeString(); + } catch { + return moment(time).format('LTS'); + } } class QueryHistoryUtils { diff --git a/web/regression/javascript/sqleditor/QueryHistory.spec.js b/web/regression/javascript/sqleditor/QueryHistory.spec.js new file mode 100644 index 00000000000..6b8b9b6d2dc --- /dev/null +++ b/web/regression/javascript/sqleditor/QueryHistory.spec.js @@ -0,0 +1,85 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2025, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +// Mock url_for +jest.mock('sources/url_for', () => ({ + __esModule: true, + default: jest.fn((endpoint) => `/mock/${endpoint}`), +})); + +// Mock the QueryToolComponent to avoid importing all its dependencies +jest.mock('../../../pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx', () => { + const React = require('react'); + return { + QueryToolContext: React.createContext(null), + QueryToolConnectionContext: React.createContext(null), + QueryToolEventsContext: React.createContext(null), + }; +}); + +// Mock CodeMirror +jest.mock('../../../pgadmin/static/js/components/ReactCodeMirror', () => ({ + __esModule: true, + default: ({ value }) => value, +})); + +import { getDateFormatted, getTimeFormatted } from '../../../pgadmin/tools/sqleditor/static/js/components/sections/QueryHistory.jsx'; + +describe('QueryHistory date/time formatting', () => { + it('formats a date using the native locale formatter', () => { + const date = new Date(2025, 0, 15); + expect(getDateFormatted(date)).toBe(date.toLocaleDateString()); + }); + + it('formats a time using the native locale formatter', () => { + const time = new Date(2025, 0, 15, 10, 30, 45); + expect(getTimeFormatted(time)).toBe(time.toLocaleTimeString()); + }); + + // Regression test for #7596: a malformed default locale makes + // toLocaleDateString/toLocaleTimeString throw "RangeError: Incorrect + // locale information provided". The helpers must not propagate the throw + // (which would white-screen the SQL editor) and must return a usable + // string instead. + describe('when the runtime locale is broken', () => { + let dateSpy, timeSpy; + + beforeEach(() => { + dateSpy = jest.spyOn(Date.prototype, 'toLocaleDateString') + .mockImplementation(() => { + throw new RangeError('Incorrect locale information provided'); + }); + timeSpy = jest.spyOn(Date.prototype, 'toLocaleTimeString') + .mockImplementation(() => { + throw new RangeError('Incorrect locale information provided'); + }); + }); + + afterEach(() => { + dateSpy.mockRestore(); + timeSpy.mockRestore(); + }); + + it('does not throw and returns a non-empty date string', () => { + const date = new Date(2025, 0, 15); + let result; + expect(() => { result = getDateFormatted(date); }).not.toThrow(); + expect(typeof result).toBe('string'); + expect(result.length).toBeGreaterThan(0); + }); + + it('does not throw and returns a non-empty time string', () => { + const time = new Date(2025, 0, 15, 10, 30, 45); + let result; + expect(() => { result = getTimeFormatted(time); }).not.toThrow(); + expect(typeof result).toBe('string'); + expect(result.length).toBeGreaterThan(0); + }); + }); +});