Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions src/components/CrateTabsShad/CrateTabsShad.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@ function CrateTabsShad({
stickyTabBar = false,
onChange,
}: CrateTabsShadProps) {
const [activeTab, setActiveTab] = useState<string | undefined>(undefined);
const hideTabs = hideWhenSingleTab && items.length === 1;

const getDefaultTab = (): string => {
if (initialActiveTab && items.map(item => item.key).includes(initialActiveTab)) {
return initialActiveTab;
}
return items[0].key;
};

const [activeTab, setActiveTab] = useState<string>(getDefaultTab);
const hideTabs = hideWhenSingleTab && items.length === 1;

const onTabChange = (value: string) => {
if (onChange) {
onChange(value);
Expand All @@ -54,7 +54,6 @@ function CrateTabsShad({
<Tabs
className={stickyTabBar ? 'flex h-full w-full flex-col' : ''}
data-testid="tabs-container"
defaultValue={getDefaultTab()}
value={activeTab}
onValueChange={onTabChange}
>
Expand Down
1 change: 1 addition & 0 deletions src/components/DataTable/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export function DataTable<TData, TValue>({
onGlobalFilterChange: setSearchTerm,
onPaginationChange: setPagination,
globalFilterFn: 'includesString',
autoResetPageIndex: false,
getRowId,
};

Expand Down
28 changes: 16 additions & 12 deletions src/components/SQLResults/SQLResultsTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,24 +140,26 @@ 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' }),
);
});

it('the file name is query-results-{TIMESTAMP}.csv', 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('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 () => {
Expand All @@ -171,24 +173,26 @@ 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' }),
);
});

it('the file name is query-results-{TIMESTAMP}.json', 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('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 () => {
Expand Down
52 changes: 26 additions & 26 deletions src/components/SQLResults/SQLResultsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ type DataTableColumnData<T> = {
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();
Expand Down Expand Up @@ -307,33 +317,23 @@ function SQLResultsTable({ result, onDownloadResult }: SQLResultsTableProps) {
</DropdownMenu.Trigger>

<DropdownMenu.Content>
<DropdownMenu.Item>
<a
href={`data:text/csv;charset=utf-8,${generateCsv(result)}`}
download={'query-results-' + Date.now() + '.csv'}
className="text-black"
onClick={() => {
if (onDownloadResult) {
onDownloadResult('csv');
}
}}
>
Export as .csv
</a>
<DropdownMenu.Item
className="cursor-pointer"
onClick={() => {
triggerDownload(generateCsv(result), 'text/csv', 'query-results-' + Date.now() + '.csv');
if (onDownloadResult) onDownloadResult('csv');
}}
>
Export as .csv
</DropdownMenu.Item>
<DropdownMenu.Item>
<a
href={`data:application/json;charset=utf-8,${generateJson(result)}`}
download={'query-results-' + Date.now() + '.json'}
className="text-black"
onClick={() => {
if (onDownloadResult) {
onDownloadResult('json');
}
}}
>
Export as .json
</a>
<DropdownMenu.Item
className="cursor-pointer"
onClick={() => {
triggerDownload(generateJson(result), 'application/json', 'query-results-' + Date.now() + '.json');
if (onDownloadResult) onDownloadResult('json');
}}
>
Export as .json
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Menu>
Expand Down
30 changes: 29 additions & 1 deletion test/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -54,10 +61,31 @@ 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,
});

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({
Expand Down
Loading