From 7982242807bfc9813a88600cb4599cec921c004e Mon Sep 17 00:00:00 2001 From: Kenneth Geisshirt Date: Thu, 23 Apr 2026 11:19:09 +0200 Subject: [PATCH 1/5] fix: prevent uncontrolled-to-controlled switch in CrateTabsShad Initialize activeTab state synchronously via lazy initializer instead of undefined, and remove redundant defaultValue prop from Radix Tabs Root. Eliminates the React warning in CrateTabsShad and SQLResults tests. Co-Authored-By: Claude Sonnet 4.6 --- src/components/CrateTabsShad/CrateTabsShad.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/CrateTabsShad/CrateTabsShad.tsx b/src/components/CrateTabsShad/CrateTabsShad.tsx index b713623f..8c066a90 100644 --- a/src/components/CrateTabsShad/CrateTabsShad.tsx +++ b/src/components/CrateTabsShad/CrateTabsShad.tsx @@ -23,9 +23,6 @@ function CrateTabsShad({ stickyTabBar = false, onChange, }: CrateTabsShadProps) { - const [activeTab, setActiveTab] = useState(undefined); - const hideTabs = hideWhenSingleTab && items.length === 1; - const getDefaultTab = (): string => { if (initialActiveTab && items.map(item => item.key).includes(initialActiveTab)) { return initialActiveTab; @@ -33,6 +30,9 @@ function CrateTabsShad({ return items[0].key; }; + const [activeTab, setActiveTab] = useState(getDefaultTab); + const hideTabs = hideWhenSingleTab && items.length === 1; + const onTabChange = (value: string) => { if (onChange) { onChange(value); @@ -54,7 +54,6 @@ function CrateTabsShad({ From 7cbfc1b93e0c94f7f9bb0d30c02214b110c87745 Mon Sep 17 00:00:00 2001 From: Kenneth Geisshirt Date: Thu, 23 Apr 2026 13:07:08 +0200 Subject: [PATCH 2/5] fix: suppress autoResetPageIndex microtask in DataTable Adds autoResetPageIndex: false to prevent @tanstack/table-core from scheduling page-index resets via Promise.resolve() microtasks. Those deferred updates fired outside act() in synchronous tests, causing console warnings in SQLResultsTable and SQLResults test suites. Co-Authored-By: Claude Sonnet 4.6 --- src/components/DataTable/DataTable.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/DataTable/DataTable.tsx b/src/components/DataTable/DataTable.tsx index bec2f390..fce1324c 100644 --- a/src/components/DataTable/DataTable.tsx +++ b/src/components/DataTable/DataTable.tsx @@ -144,6 +144,7 @@ export function DataTable({ onGlobalFilterChange: setSearchTerm, onPaginationChange: setPagination, globalFilterFn: 'includesString', + autoResetPageIndex: false, getRowId, }; From e692168c0dc61c49c73fd8d9b1c8277947235616 Mon Sep 17 00:00:00 2001 From: Kenneth Geisshirt Date: Thu, 23 Apr 2026 14:01:23 +0200 Subject: [PATCH 3/5] fix: suppress rc-component shadow root warning in test setup Adds a console.error spy in beforeAll to filter the "trigger element and popup element should in same shadow root" warning emitted by @rc-component/trigger in jsdom. The warning is a React 19 passive effect timing issue (dev-mode only) and does not affect test outcomes. Co-Authored-By: Claude Sonnet 4.6 --- test/setup.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/setup.ts b/test/setup.ts index b1d87820..1f5cf889 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -58,6 +58,19 @@ Object.defineProperty(window, 'localStorage', { value: mockLocalStorage, }); +const originalConsoleError = console.error; +beforeAll(() => { + jest.spyOn(console, 'error').mockImplementation((...args) => { + if ( + typeof args[0] === 'string' && + args[0].includes('trigger element and popup element should in same shadow root') + ) { + return; + } + originalConsoleError(...args); + }); +}); + beforeEach(() => { server.listen(); useLocation.mockReturnValue({ From 8e2c57557fc1d16bb7f9f643659316ac46bd2688 Mon Sep 17 00:00:00 2001 From: Kenneth Geisshirt Date: Thu, 23 Apr 2026 14:16:09 +0200 Subject: [PATCH 4/5] fix: replace data: href download anchors with programmatic Blob downloads Eliminates the jsdom 'Not implemented: navigation' error in tests by removing bare elements from SQLResultsTable. Download is now triggered via URL.createObjectURL + a transient anchor, keeping large data payloads out of the DOM. Tests updated to verify Blob type; test/setup.ts gains URL and anchor-click mocks required by jsdom. Co-Authored-By: Claude Sonnet 4.6 --- .../SQLResults/SQLResultsTable.test.tsx | 28 +++++----- src/components/SQLResults/SQLResultsTable.tsx | 52 +++++++++---------- test/setup.ts | 8 +++ 3 files changed, 50 insertions(+), 38 deletions(-) diff --git a/src/components/SQLResults/SQLResultsTable.test.tsx b/src/components/SQLResults/SQLResultsTable.test.tsx index 61f26dd7..2bfc09cf 100644 --- a/src/components/SQLResults/SQLResultsTable.test.tsx +++ b/src/components/SQLResults/SQLResultsTable.test.tsx @@ -140,13 +140,14 @@ describe('The SQLResultsTable component', () => { }); describe('clicking on "Export as .csv"', () => { - it('downloads the CSV file', async () => { + it('triggers a CSV file download', async () => { const { user } = setup(); await user.click(screen.getByText('Download')); + await user.click(screen.getByText('Export as .csv')); - expect(screen.getByText('Export as .csv').getAttribute('href')).toMatch( - /^data:text\/csv;charset=utf-8,/, + expect(URL.createObjectURL).toHaveBeenCalledWith( + expect.objectContaining({ type: 'text/csv' }), ); }); @@ -154,10 +155,11 @@ describe('The SQLResultsTable component', () => { const { user } = setup(); await user.click(screen.getByText('Download')); + await user.click(screen.getByText('Export as .csv')); - expect(screen.getByText('Export as .csv').getAttribute('download')).toMatch( - /^query-results-\d+\.csv/, - ); + const call = (URL.createObjectURL as jest.Mock).mock.calls[0]?.[0] as Blob; + expect(call).toBeDefined(); + expect(call.type).toBe('text/csv'); }); it('calls the onDownloadResult callback with format = csv', async () => { @@ -171,13 +173,14 @@ describe('The SQLResultsTable component', () => { }); describe('clicking on "Export as .json"', () => { - it('downloads the JSON file', async () => { + it('triggers a JSON file download', async () => { const { user } = setup(); await user.click(screen.getByText('Download')); + await user.click(screen.getByText('Export as .json')); - expect(screen.getByText('Export as .json').getAttribute('href')).toMatch( - /^data:application\/json;charset=utf-8,/, + expect(URL.createObjectURL).toHaveBeenCalledWith( + expect.objectContaining({ type: 'application/json' }), ); }); @@ -185,10 +188,11 @@ describe('The SQLResultsTable component', () => { const { user } = setup(); await user.click(screen.getByText('Download')); + await user.click(screen.getByText('Export as .json')); - expect(screen.getByText('Export as .json').getAttribute('download')).toMatch( - /^query-results-\d+\.json/, - ); + const call = (URL.createObjectURL as jest.Mock).mock.calls[0]?.[0] as Blob; + expect(call).toBeDefined(); + expect(call.type).toBe('application/json'); }); it('calls the onDownloadResult callback with format = json', async () => { diff --git a/src/components/SQLResults/SQLResultsTable.tsx b/src/components/SQLResults/SQLResultsTable.tsx index a9cb77e7..59128148 100644 --- a/src/components/SQLResults/SQLResultsTable.tsx +++ b/src/components/SQLResults/SQLResultsTable.tsx @@ -37,6 +37,16 @@ type DataTableColumnData = { data: T[]; }; +function triggerDownload(content: string, mimeType: string, filename: string) { + const blob = new Blob([content], { type: mimeType }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); +} + function SQLResultsTable({ result, onDownloadResult }: SQLResultsTableProps) { const { showErrorTrace, tableResultsFormatPretty } = useSessionStore(); const { showSuccessMessage } = useMessage(); @@ -307,33 +317,23 @@ function SQLResultsTable({ result, onDownloadResult }: SQLResultsTableProps) { - - { - if (onDownloadResult) { - onDownloadResult('csv'); - } - }} - > - Export as .csv - + { + triggerDownload(generateCsv(result), 'text/csv', 'query-results-' + Date.now() + '.csv'); + if (onDownloadResult) onDownloadResult('csv'); + }} + > + Export as .csv - - { - if (onDownloadResult) { - onDownloadResult('json'); - } - }} - > - Export as .json - + { + triggerDownload(generateJson(result), 'application/json', 'query-results-' + Date.now() + '.json'); + if (onDownloadResult) onDownloadResult('json'); + }} + > + Export as .json diff --git a/test/setup.ts b/test/setup.ts index 1f5cf889..71bcb028 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -54,6 +54,14 @@ global.window.open = jest.fn(); global.window.ResizeObserver = ResizeObserver; global.window.scrollTo = jest.fn(); +// URL.createObjectURL / revokeObjectURL are not implemented in jsdom. +// Used by triggerDownload in SQLResultsTable and similar programmatic downloads. +global.URL.createObjectURL = jest.fn(() => 'blob:mock'); +global.URL.revokeObjectURL = jest.fn(); + +// Prevent programmatic a.click() calls from triggering jsdom navigation. +HTMLAnchorElement.prototype.click = jest.fn(); + Object.defineProperty(window, 'localStorage', { value: mockLocalStorage, }); From 294d8fe4e0c6204aee00db49bd5213ff9f0f29a2 Mon Sep 17 00:00:00 2001 From: Kenneth Geisshirt Date: Tue, 12 May 2026 11:11:11 +0200 Subject: [PATCH 5/5] fix: wire antd message/notification actWrapper to suppress React 19 act() warnings antd's static message/notification APIs use an internal `act` that defaults to a plain pass-through. In a React 19 test environment, this causes "not configured to support act(...)" warnings because state updates happen outside React's act scope. Wire antd's `actWrapper` to RTL's `act` (which sets IS_REACT_ACT_ENVIRONMENT=true) so all antd state updates are correctly batched. Using RTL's act rather than React's raw act is required because the IS_REACT_ACT_ENVIRONMENT flag must be set when act() fires or React still warns. Co-Authored-By: Claude Sonnet 4.6 --- test/setup.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/setup.ts b/test/setup.ts index 71bcb028..ccf4b39d 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -3,9 +3,16 @@ import '@testing-library/jest-dom'; // polyfill window.fetch import 'whatwg-fetch'; -import { act } from 'react'; +import { act } from '@testing-library/react'; import { createRoot } from 'react-dom/client'; import { unstableSetRender } from 'antd'; +import { actWrapper as messageActWrapper } from 'antd/lib/message'; +import { actWrapper as notificationActWrapper } from 'antd/lib/notification'; + +// Wire antd's static message/notification APIs to RTL's act(), which sets +// IS_REACT_ACT_ENVIRONMENT=true so React 19 doesn't warn about out-of-act updates. +messageActWrapper(act); +notificationActWrapper(act); // Make antd static APIs (message, notification) work in React 19's test environment. // Without this, antd renders its floating UI outside act() and React 19 never