mirror of
https://github.com/apache/superset.git
synced 2026-05-07 08:54:23 +00:00
refactor(tests): improve type safety and modernize dataset hook tests
- Add typed response helpers (buildSupersetResponse, buildCachedResponse) to consolidate mocking boilerplate - Replace waitForNextUpdate() with explicit waitFor(() => expect(result.current.X)...) patterns for better async assertions - Flatten describe() blocks to standalone test() calls following Kent C. Dodds style - Add proper TypeScript interfaces for Rison query decoding - Remove eslint-disable comments for describe blocks - Use 'as unknown as T' double-cast pattern (matching DashboardCard.test.tsx) for incomplete test mocks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
import { SupersetClient, JsonResponse } from '@superset-ui/core';
|
||||
import rison from 'rison';
|
||||
import useDatasetsList from './useDatasetLists';
|
||||
|
||||
@@ -26,6 +26,14 @@ jest.mock('src/components/MessageToasts/actions', () => ({
|
||||
addDangerToast: (msg: string) => mockAddDangerToast(msg),
|
||||
}));
|
||||
|
||||
// Typed response helper to consolidate mocking boilerplate
|
||||
// Uses 'as unknown as JsonResponse' because we're intentionally mocking
|
||||
// only the json field without the full Response object for test simplicity
|
||||
const buildSupersetResponse = <T>(data: { count: number; result: T[] }) =>
|
||||
({
|
||||
json: data,
|
||||
}) as unknown as JsonResponse;
|
||||
|
||||
// Shared test fixtures
|
||||
const mockDb = {
|
||||
id: 1,
|
||||
@@ -47,20 +55,18 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
test('useDatasetsList fetches first page of datasets successfully', async () => {
|
||||
const getSpy = jest.spyOn(SupersetClient, 'get').mockResolvedValue({
|
||||
json: {
|
||||
count: 2,
|
||||
result: mockDatasets,
|
||||
},
|
||||
} as any);
|
||||
const getSpy = jest
|
||||
.spyOn(SupersetClient, 'get')
|
||||
.mockResolvedValue(
|
||||
buildSupersetResponse({ count: 2, result: mockDatasets }),
|
||||
);
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useDatasetsList(mockDb, 'public'),
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitFor(() => expect(result.current.datasets).toEqual(mockDatasets));
|
||||
|
||||
expect(result.current.datasets).toEqual(mockDatasets);
|
||||
expect(result.current.datasetNames).toEqual(['table1', 'table2']);
|
||||
expect(getSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -74,49 +80,48 @@ test('useDatasetsList fetches multiple pages (pagination) until count reached',
|
||||
|
||||
const getSpy = jest
|
||||
.spyOn(SupersetClient, 'get')
|
||||
.mockResolvedValueOnce({
|
||||
json: {
|
||||
count: 3,
|
||||
result: page1Data,
|
||||
},
|
||||
} as any)
|
||||
.mockResolvedValueOnce({
|
||||
json: {
|
||||
count: 3,
|
||||
result: page2Data,
|
||||
},
|
||||
} as any);
|
||||
.mockResolvedValueOnce(
|
||||
buildSupersetResponse({ count: 3, result: page1Data }),
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
buildSupersetResponse({ count: 3, result: page2Data }),
|
||||
);
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useDatasetsList(mockDb, 'public'),
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitFor(() =>
|
||||
expect(result.current.datasets).toEqual([...page1Data, ...page2Data]),
|
||||
);
|
||||
|
||||
expect(result.current.datasets).toEqual([...page1Data, ...page2Data]);
|
||||
expect(result.current.datasetNames).toEqual(['table1', 'table2', 'table3']);
|
||||
expect(getSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('useDatasetsList extracts dataset names correctly', async () => {
|
||||
const getSpy = jest.spyOn(SupersetClient, 'get').mockResolvedValue({
|
||||
json: {
|
||||
count: 3,
|
||||
result: [
|
||||
{ id: 1, table_name: 'users' },
|
||||
{ id: 2, table_name: 'orders' },
|
||||
{ id: 3, table_name: 'products' },
|
||||
],
|
||||
},
|
||||
} as any);
|
||||
const datasets = [
|
||||
{ id: 1, table_name: 'users' },
|
||||
{ id: 2, table_name: 'orders' },
|
||||
{ id: 3, table_name: 'products' },
|
||||
];
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
const getSpy = jest
|
||||
.spyOn(SupersetClient, 'get')
|
||||
.mockResolvedValue(buildSupersetResponse({ count: 3, result: datasets }));
|
||||
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useDatasetsList(mockDb, 'public'),
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitFor(() =>
|
||||
expect(result.current.datasetNames).toEqual([
|
||||
'users',
|
||||
'orders',
|
||||
'products',
|
||||
]),
|
||||
);
|
||||
|
||||
expect(result.current.datasetNames).toEqual(['users', 'orders', 'products']);
|
||||
expect(getSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -125,20 +130,14 @@ test('useDatasetsList handles API 500 error gracefully', async () => {
|
||||
const getSpy = jest
|
||||
.spyOn(SupersetClient, 'get')
|
||||
.mockRejectedValueOnce(new Error('Internal Server Error'))
|
||||
.mockResolvedValueOnce({
|
||||
json: {
|
||||
count: 0,
|
||||
result: [],
|
||||
},
|
||||
} as any);
|
||||
.mockResolvedValueOnce(buildSupersetResponse({ count: 0, result: [] }));
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useDatasetsList(mockDb, 'public'),
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitFor(() => expect(result.current.datasets).toEqual([]));
|
||||
|
||||
expect(result.current.datasets).toEqual([]);
|
||||
expect(result.current.datasetNames).toEqual([]);
|
||||
expect(mockAddDangerToast).toHaveBeenCalledWith(
|
||||
'There was an error fetching dataset',
|
||||
@@ -148,96 +147,83 @@ test('useDatasetsList handles API 500 error gracefully', async () => {
|
||||
});
|
||||
|
||||
test('useDatasetsList handles empty dataset response', async () => {
|
||||
const getSpy = jest.spyOn(SupersetClient, 'get').mockResolvedValue({
|
||||
json: {
|
||||
count: 0,
|
||||
result: [],
|
||||
},
|
||||
} as any);
|
||||
const getSpy = jest
|
||||
.spyOn(SupersetClient, 'get')
|
||||
.mockResolvedValue(buildSupersetResponse({ count: 0, result: [] }));
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useDatasetsList(mockDb, 'public'),
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitFor(() => expect(result.current.datasets).toEqual([]));
|
||||
|
||||
expect(result.current.datasets).toEqual([]);
|
||||
expect(result.current.datasetNames).toEqual([]);
|
||||
expect(getSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('useDatasetsList stops pagination when results reach count', async () => {
|
||||
// First page returns 2 items, second page returns empty (no more results)
|
||||
const datasets = [
|
||||
{ id: 1, table_name: 'table1' },
|
||||
{ id: 2, table_name: 'table2' },
|
||||
];
|
||||
|
||||
const getSpy = jest
|
||||
.spyOn(SupersetClient, 'get')
|
||||
.mockResolvedValueOnce({
|
||||
json: {
|
||||
count: 2,
|
||||
result: [
|
||||
{ id: 1, table_name: 'table1' },
|
||||
{ id: 2, table_name: 'table2' },
|
||||
],
|
||||
},
|
||||
} as any)
|
||||
.mockResolvedValueOnce({
|
||||
json: {
|
||||
count: 2,
|
||||
result: [], // No more results
|
||||
},
|
||||
} as any);
|
||||
.mockResolvedValueOnce(
|
||||
buildSupersetResponse({ count: 2, result: datasets }),
|
||||
)
|
||||
.mockResolvedValueOnce(buildSupersetResponse({ count: 2, result: [] }));
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useDatasetsList(mockDb, 'public'),
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitFor(() => expect(result.current.datasets).toHaveLength(2));
|
||||
|
||||
expect(result.current.datasets).toHaveLength(2);
|
||||
expect(result.current.datasetNames).toEqual(['table1', 'table2']);
|
||||
// Should stop after results.length >= count
|
||||
expect(getSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('useDatasetsList resets datasets when schema changes', async () => {
|
||||
const publicDatasets = [
|
||||
{ id: 1, table_name: 'public_table1' },
|
||||
{ id: 2, table_name: 'public_table2' },
|
||||
];
|
||||
const privateDatasets = [{ id: 3, table_name: 'private_table1' }];
|
||||
|
||||
const getSpy = jest
|
||||
.spyOn(SupersetClient, 'get')
|
||||
.mockResolvedValueOnce({
|
||||
json: {
|
||||
count: 2,
|
||||
result: [
|
||||
{ id: 1, table_name: 'public_table1' },
|
||||
{ id: 2, table_name: 'public_table2' },
|
||||
],
|
||||
},
|
||||
} as any)
|
||||
.mockResolvedValueOnce({
|
||||
json: {
|
||||
count: 1,
|
||||
result: [{ id: 3, table_name: 'private_table1' }],
|
||||
},
|
||||
} as any);
|
||||
.mockResolvedValueOnce(
|
||||
buildSupersetResponse({ count: 2, result: publicDatasets }),
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
buildSupersetResponse({ count: 1, result: privateDatasets }),
|
||||
);
|
||||
|
||||
const { result, waitForNextUpdate, rerender } = renderHook(
|
||||
const { result, waitFor, rerender } = renderHook(
|
||||
({ db, schema }) => useDatasetsList(db, schema),
|
||||
{
|
||||
initialProps: { db: mockDb, schema: 'public' },
|
||||
},
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.datasetNames).toEqual([
|
||||
'public_table1',
|
||||
'public_table2',
|
||||
]);
|
||||
await waitFor(() =>
|
||||
expect(result.current.datasetNames).toEqual([
|
||||
'public_table1',
|
||||
'public_table2',
|
||||
]),
|
||||
);
|
||||
|
||||
// Change schema
|
||||
rerender({ db: mockDb, schema: 'private' });
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
// Should have new datasets from private schema
|
||||
expect(result.current.datasetNames).toEqual(['private_table1']);
|
||||
await waitFor(() =>
|
||||
expect(result.current.datasetNames).toEqual(['private_table1']),
|
||||
);
|
||||
|
||||
expect(getSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -249,20 +235,14 @@ test('useDatasetsList handles network timeout gracefully', async () => {
|
||||
const getSpy = jest
|
||||
.spyOn(SupersetClient, 'get')
|
||||
.mockRejectedValueOnce(timeoutError)
|
||||
.mockResolvedValueOnce({
|
||||
json: {
|
||||
count: 0,
|
||||
result: [],
|
||||
},
|
||||
} as any);
|
||||
.mockResolvedValueOnce(buildSupersetResponse({ count: 0, result: [] }));
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useDatasetsList(mockDb, 'public'),
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitFor(() => expect(result.current.datasets).toEqual([]));
|
||||
|
||||
expect(result.current.datasets).toEqual([]);
|
||||
expect(result.current.datasetNames).toEqual([]);
|
||||
expect(mockAddDangerToast).toHaveBeenCalledWith(
|
||||
'There was an error fetching dataset',
|
||||
@@ -276,7 +256,7 @@ test('useDatasetsList skips fetching when schema is null or undefined', () => {
|
||||
// Test with null schema
|
||||
const { result: resultNull, rerender } = renderHook(
|
||||
({ db, schema }) => useDatasetsList(db, schema),
|
||||
{ initialProps: { db: mockDb, schema: null as any } },
|
||||
{ initialProps: { db: mockDb, schema: null as unknown as string } },
|
||||
);
|
||||
|
||||
// Schema is null - should NOT call API
|
||||
@@ -285,22 +265,22 @@ test('useDatasetsList skips fetching when schema is null or undefined', () => {
|
||||
expect(resultNull.current.datasetNames).toEqual([]);
|
||||
|
||||
// Change to undefined - still should NOT call API
|
||||
rerender({ db: mockDb, schema: undefined as any });
|
||||
rerender({ db: mockDb, schema: undefined as unknown as string });
|
||||
expect(getSpy).not.toHaveBeenCalled();
|
||||
expect(resultNull.current.datasets).toEqual([]);
|
||||
expect(resultNull.current.datasetNames).toEqual([]);
|
||||
});
|
||||
|
||||
test('useDatasetsList encodes schemas with spaces and special characters in endpoint URL', async () => {
|
||||
const getSpy = jest.spyOn(SupersetClient, 'get').mockResolvedValue({
|
||||
json: { count: 0, result: [] },
|
||||
} as any);
|
||||
const getSpy = jest
|
||||
.spyOn(SupersetClient, 'get')
|
||||
.mockResolvedValue(buildSupersetResponse({ count: 0, result: [] }));
|
||||
|
||||
const { waitForNextUpdate } = renderHook(() =>
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useDatasetsList(mockDb, 'sales analytics'),
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitFor(() => expect(result.current.datasets).toEqual([]));
|
||||
|
||||
// Verify API was called with encoded schema
|
||||
expect(getSpy).toHaveBeenCalledTimes(1);
|
||||
@@ -313,7 +293,18 @@ test('useDatasetsList encodes schemas with spaces and special characters in endp
|
||||
|
||||
// Decode rison to verify filter structure
|
||||
const risonParam = callArg!.split('?q=')[1];
|
||||
const decoded = rison.decode(decodeURIComponent(risonParam)) as any;
|
||||
|
||||
interface RisonFilter {
|
||||
col: string;
|
||||
opr: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface RisonQuery {
|
||||
filters: RisonFilter[];
|
||||
}
|
||||
|
||||
const decoded = rison.decode(decodeURIComponent(risonParam)) as RisonQuery;
|
||||
|
||||
// After rison decoding, the schema should be the encoded version (encodeURIComponent output)
|
||||
expect(decoded.filters[1]).toEqual({
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { JsonResponse } from '@superset-ui/core';
|
||||
import { Dataset } from 'src/components/Chart/types';
|
||||
import {
|
||||
cachedSupersetGet,
|
||||
@@ -45,6 +46,14 @@ jest.mock('@superset-ui/core', () => ({
|
||||
const mockedCachedSupersetGet = jest.mocked(cachedSupersetGet);
|
||||
const mockedSupersetGetCacheDelete = jest.mocked(supersetGetCache.delete);
|
||||
|
||||
// Typed response helper to consolidate mocking boilerplate
|
||||
// Uses 'as unknown as JsonResponse' because we're intentionally mocking
|
||||
// only the json field without the full Response object for test simplicity
|
||||
const buildCachedResponse = <T>(result: T) =>
|
||||
({
|
||||
json: { result },
|
||||
}) as unknown as JsonResponse;
|
||||
|
||||
test('getDatasetId extracts numeric ID from string datasource ID', () => {
|
||||
expect(getDatasetId('123__table')).toBe(123);
|
||||
expect(getDatasetId('456__another_table')).toBe(456);
|
||||
@@ -75,6 +84,7 @@ test('createVerboseMap creates verbose_map from columns', () => {
|
||||
});
|
||||
|
||||
test('createVerboseMap creates verbose_map from metrics', () => {
|
||||
// Partial dataset with only metrics - createVerboseMap doesn't require full Dataset
|
||||
const dataset = {
|
||||
columns: [],
|
||||
metrics: [
|
||||
@@ -82,7 +92,7 @@ test('createVerboseMap creates verbose_map from metrics', () => {
|
||||
{ metric_name: 'metric2', verbose_name: 'Metric 2' },
|
||||
{ metric_name: 'metric3' }, // no verbose_name
|
||||
],
|
||||
} as any;
|
||||
} as unknown as Dataset;
|
||||
|
||||
const verboseMap = createVerboseMap(dataset);
|
||||
|
||||
@@ -112,311 +122,262 @@ test('createVerboseMap handles undefined dataset', () => {
|
||||
expect(verboseMap).toEqual({});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
describe('useDatasetDrillInfo', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('fetches dataset drill info successfully', async () => {
|
||||
const mockDataset = {
|
||||
id: 123,
|
||||
columns: [{ column_name: 'col1', verbose_name: 'Column 1' }],
|
||||
metrics: [{ metric_name: 'metric1', verbose_name: 'Metric 1' }],
|
||||
};
|
||||
test('useDatasetDrillInfo fetches dataset drill info successfully', async () => {
|
||||
const mockDataset = {
|
||||
id: 123,
|
||||
columns: [{ column_name: 'col1', verbose_name: 'Column 1' }],
|
||||
metrics: [{ metric_name: 'metric1', verbose_name: 'Metric 1' }],
|
||||
};
|
||||
|
||||
mockedCachedSupersetGet.mockResolvedValue({
|
||||
json: {
|
||||
result: mockDataset,
|
||||
},
|
||||
} as any);
|
||||
mockedCachedSupersetGet.mockResolvedValue(buildCachedResponse(mockDataset));
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useDatasetDrillInfo(123, 456),
|
||||
);
|
||||
const { result, waitFor } = renderHook(() => useDatasetDrillInfo(123, 456));
|
||||
|
||||
expect(result.current.status).toBe('loading');
|
||||
expect(result.current.status).toBe('loading');
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitFor(() => expect(result.current.status).toBe('complete'));
|
||||
|
||||
expect(result.current.status).toBe('complete');
|
||||
expect(result.current.result).toEqual({
|
||||
...mockDataset,
|
||||
verbose_map: {
|
||||
col1: 'Column 1',
|
||||
metric1: 'Metric 1',
|
||||
},
|
||||
});
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
|
||||
test('handles network errors', async () => {
|
||||
mockedCachedSupersetGet.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useDatasetDrillInfo(123, 456),
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.status).toBe('error');
|
||||
expect(result.current.result).toBeNull();
|
||||
expect(result.current.error).toBeInstanceOf(Error);
|
||||
expect(result.current.error?.message).toBe('Network error');
|
||||
expect(mockedSupersetGetCacheDelete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('skips fetch when skip is true', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useDatasetDrillInfo(123, 456, undefined, true),
|
||||
);
|
||||
|
||||
// Should immediately return complete status without fetching
|
||||
expect(result.current.status).toBe('complete');
|
||||
expect(result.current.result).toEqual({});
|
||||
expect(result.current.error).toBeNull();
|
||||
|
||||
// Verify no API call was made
|
||||
expect(mockedCachedSupersetGet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('extracts dataset ID from string format', async () => {
|
||||
const mockDataset = {
|
||||
id: 123,
|
||||
columns: [],
|
||||
metrics: [],
|
||||
};
|
||||
|
||||
mockedCachedSupersetGet.mockResolvedValue({
|
||||
json: {
|
||||
result: mockDataset,
|
||||
},
|
||||
} as any);
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useDatasetDrillInfo('123__table', 456),
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.status).toBe('complete');
|
||||
expect(mockedCachedSupersetGet).toHaveBeenCalledWith({
|
||||
endpoint: '/api/v1/dataset/123/drill_info/?q=(dashboard_id:456)',
|
||||
});
|
||||
});
|
||||
|
||||
test('does not clear cache on successful fetch', async () => {
|
||||
const mockDataset = {
|
||||
id: 123,
|
||||
columns: [],
|
||||
metrics: [],
|
||||
};
|
||||
|
||||
mockedCachedSupersetGet.mockResolvedValue({
|
||||
json: {
|
||||
result: mockDataset,
|
||||
},
|
||||
} as any);
|
||||
|
||||
const { waitForNextUpdate } = renderHook(() =>
|
||||
useDatasetDrillInfo(123, 456),
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
// Cache should NOT be deleted on success
|
||||
expect(mockedSupersetGetCacheDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('creates new verbose_map from columns and metrics', async () => {
|
||||
const mockDataset = {
|
||||
id: 123,
|
||||
verbose_map: { old_key: 'Old Value' }, // Existing verbose_map will be replaced
|
||||
columns: [{ column_name: 'col1', verbose_name: 'Column 1' }],
|
||||
metrics: [{ metric_name: 'metric1', verbose_name: 'Metric 1' }],
|
||||
};
|
||||
|
||||
mockedCachedSupersetGet.mockResolvedValue({
|
||||
json: {
|
||||
result: mockDataset,
|
||||
},
|
||||
} as any);
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useDatasetDrillInfo(123, 456),
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.status).toBe('complete');
|
||||
// Verify verbose_map is created from columns/metrics (existing verbose_map replaced)
|
||||
expect(result.current.result?.verbose_map).toEqual({
|
||||
expect(result.current.result).toEqual({
|
||||
...mockDataset,
|
||||
verbose_map: {
|
||||
col1: 'Column 1',
|
||||
metric1: 'Metric 1',
|
||||
});
|
||||
// Old key should not be present
|
||||
expect(result.current.result?.verbose_map).not.toHaveProperty('old_key');
|
||||
},
|
||||
});
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
|
||||
test('handles NaN datasource ID from malformed string', async () => {
|
||||
mockedCachedSupersetGet.mockResolvedValue({
|
||||
json: {
|
||||
result: { id: NaN, columns: [], metrics: [] },
|
||||
},
|
||||
} as any);
|
||||
test('useDatasetDrillInfo handles network errors', async () => {
|
||||
mockedCachedSupersetGet.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useDatasetDrillInfo('abc', 456),
|
||||
);
|
||||
const { result, waitFor } = renderHook(() => useDatasetDrillInfo(123, 456));
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitFor(() => expect(result.current.status).toBe('error'));
|
||||
|
||||
// Verify hook calls endpoint with NaN (API will handle validation)
|
||||
expect(mockedCachedSupersetGet).toHaveBeenCalledWith({
|
||||
endpoint: '/api/v1/dataset/NaN/drill_info/?q=(dashboard_id:456)',
|
||||
});
|
||||
expect(result.current.status).toBe('complete');
|
||||
expect(result.current.result).toBeNull();
|
||||
expect(result.current.error).toBeInstanceOf(Error);
|
||||
expect(result.current.error?.message).toBe('Network error');
|
||||
expect(mockedSupersetGetCacheDelete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('useDatasetDrillInfo skips fetch when skip is true', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useDatasetDrillInfo(123, 456, undefined, true),
|
||||
);
|
||||
|
||||
// Should immediately return complete status without fetching
|
||||
expect(result.current.status).toBe('complete');
|
||||
expect(result.current.result).toEqual({});
|
||||
expect(result.current.error).toBeNull();
|
||||
|
||||
// Verify no API call was made
|
||||
expect(mockedCachedSupersetGet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('useDatasetDrillInfo extracts dataset ID from string format', async () => {
|
||||
const mockDataset = {
|
||||
id: 123,
|
||||
columns: [],
|
||||
metrics: [],
|
||||
};
|
||||
|
||||
mockedCachedSupersetGet.mockResolvedValue(buildCachedResponse(mockDataset));
|
||||
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useDatasetDrillInfo('123__table', 456),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.status).toBe('complete'));
|
||||
|
||||
expect(mockedCachedSupersetGet).toHaveBeenCalledWith({
|
||||
endpoint: '/api/v1/dataset/123/drill_info/?q=(dashboard_id:456)',
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
describe('getDatasetId - malformed IDs', () => {
|
||||
test('handles non-numeric string ID', () => {
|
||||
const result = getDatasetId('abc');
|
||||
expect(result).toBeNaN();
|
||||
});
|
||||
test('useDatasetDrillInfo does not clear cache on successful fetch', async () => {
|
||||
const mockDataset = {
|
||||
id: 123,
|
||||
columns: [],
|
||||
metrics: [],
|
||||
};
|
||||
|
||||
test('handles empty string ID', () => {
|
||||
const result = getDatasetId('');
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
mockedCachedSupersetGet.mockResolvedValue(buildCachedResponse(mockDataset));
|
||||
|
||||
test('handles string with trailing underscores', () => {
|
||||
const result = getDatasetId('123__');
|
||||
expect(result).toBe(123);
|
||||
const { result, waitFor } = renderHook(() => useDatasetDrillInfo(123, 456));
|
||||
|
||||
await waitFor(() => expect(result.current.status).toBe('complete'));
|
||||
|
||||
// Cache should NOT be deleted on success
|
||||
expect(mockedSupersetGetCacheDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('useDatasetDrillInfo creates new verbose_map from columns and metrics', async () => {
|
||||
const mockDataset = {
|
||||
id: 123,
|
||||
verbose_map: { old_key: 'Old Value' }, // Existing verbose_map will be replaced
|
||||
columns: [{ column_name: 'col1', verbose_name: 'Column 1' }],
|
||||
metrics: [{ metric_name: 'metric1', verbose_name: 'Metric 1' }],
|
||||
};
|
||||
|
||||
mockedCachedSupersetGet.mockResolvedValue(buildCachedResponse(mockDataset));
|
||||
|
||||
const { result, waitFor } = renderHook(() => useDatasetDrillInfo(123, 456));
|
||||
|
||||
await waitFor(() => expect(result.current.status).toBe('complete'));
|
||||
|
||||
// Verify verbose_map is created from columns/metrics (existing verbose_map replaced)
|
||||
expect(result.current.result?.verbose_map).toEqual({
|
||||
col1: 'Column 1',
|
||||
metric1: 'Metric 1',
|
||||
});
|
||||
// Old key should not be present
|
||||
expect(result.current.result?.verbose_map).not.toHaveProperty('old_key');
|
||||
});
|
||||
|
||||
test('useDatasetDrillInfo handles NaN datasource ID from malformed string', async () => {
|
||||
mockedCachedSupersetGet.mockResolvedValue(
|
||||
buildCachedResponse({ id: NaN, columns: [], metrics: [] }),
|
||||
);
|
||||
|
||||
const { result, waitFor } = renderHook(() => useDatasetDrillInfo('abc', 456));
|
||||
|
||||
await waitFor(() => expect(result.current.status).toBe('complete'));
|
||||
|
||||
// Verify hook calls endpoint with NaN (API will handle validation)
|
||||
expect(mockedCachedSupersetGet).toHaveBeenCalledWith({
|
||||
endpoint: '/api/v1/dataset/NaN/drill_info/?q=(dashboard_id:456)',
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
describe('useDatasetDrillInfo - extension path', () => {
|
||||
const mockExtension = jest.fn();
|
||||
test('getDatasetId handles non-numeric string ID', () => {
|
||||
const result = getDatasetId('abc');
|
||||
expect(result).toBeNaN();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
test('getDatasetId handles empty string ID', () => {
|
||||
const result = getDatasetId('');
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
// Configure the module-level mock to return our extension
|
||||
mockGetExtensionsRegistry.mockReturnValue({
|
||||
get: jest.fn((key: any) =>
|
||||
key === 'load.drillby.options' ? mockExtension : undefined,
|
||||
) as any,
|
||||
});
|
||||
});
|
||||
test('getDatasetId handles string with trailing underscores', () => {
|
||||
const result = getDatasetId('123__');
|
||||
expect(result).toBe(123);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore default behavior to prevent test pollution
|
||||
mockGetExtensionsRegistry.mockReturnValue({ get: () => undefined });
|
||||
});
|
||||
// Extension tests - mock setup/teardown for extension registry
|
||||
const mockExtension = jest.fn();
|
||||
|
||||
test('fetches dataset via extension when extension and formData provided', async () => {
|
||||
const mockFormData = {
|
||||
viz_type: 'table',
|
||||
datasource: '123__table',
|
||||
adhoc_filters: [],
|
||||
};
|
||||
const mockDataset = {
|
||||
id: 123,
|
||||
columns: [{ column_name: 'col1', verbose_name: 'Column 1' }],
|
||||
metrics: [{ metric_name: 'metric1', verbose_name: 'Metric 1' }],
|
||||
};
|
||||
|
||||
mockExtension.mockResolvedValue({
|
||||
json: { result: mockDataset },
|
||||
} as any);
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useDatasetDrillInfo(123, 456, mockFormData),
|
||||
);
|
||||
|
||||
expect(result.current.status).toBe('loading');
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
// Verify extension was called with correct arguments
|
||||
expect(mockExtension).toHaveBeenCalledWith(123, mockFormData);
|
||||
|
||||
// Verify result contains dataset with verbose_map
|
||||
expect(result.current.status).toBe('complete');
|
||||
expect(result.current.result).toEqual({
|
||||
...mockDataset,
|
||||
verbose_map: {
|
||||
col1: 'Column 1',
|
||||
metric1: 'Metric 1',
|
||||
},
|
||||
});
|
||||
expect(result.current.error).toBeNull();
|
||||
|
||||
// Verify cachedSupersetGet was NOT called (extension path bypasses REST API)
|
||||
expect(mockedCachedSupersetGet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('handles extension throwing error', async () => {
|
||||
const mockFormData = { viz_type: 'table', datasource: '123__table' };
|
||||
const extensionError = new Error('Extension failed');
|
||||
|
||||
mockExtension.mockRejectedValue(extensionError);
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useDatasetDrillInfo(123, 456, mockFormData),
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
// Verify error state
|
||||
expect(result.current.status).toBe('error');
|
||||
expect(result.current.result).toBeNull();
|
||||
expect(result.current.error).toBeInstanceOf(Error);
|
||||
expect(result.current.error?.message).toBe('Extension failed');
|
||||
|
||||
// Verify REST API was not called
|
||||
expect(mockedCachedSupersetGet).not.toHaveBeenCalled();
|
||||
|
||||
// Verify cache is NOT deleted for extension errors (extensions don't use cache)
|
||||
expect(mockedSupersetGetCacheDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('handles extension returning malformed payload with undefined result', async () => {
|
||||
const mockFormData = { viz_type: 'table', datasource: '123__table' };
|
||||
|
||||
// Extension returns undefined instead of expected shape
|
||||
mockExtension.mockResolvedValue(undefined as any);
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useDatasetDrillInfo(123, 456, mockFormData),
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
// Hook should handle gracefully and set result with empty verbose_map
|
||||
expect(result.current.status).toBe('complete');
|
||||
expect(result.current.result).toEqual({ verbose_map: {} });
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
|
||||
test('handles extension returning malformed payload with missing json.result', async () => {
|
||||
const mockFormData = { viz_type: 'table', datasource: '123__table' };
|
||||
|
||||
// Extension returns object but missing json.result
|
||||
mockExtension.mockResolvedValue({ json: {} } as any);
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useDatasetDrillInfo(123, 456, mockFormData),
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
// Hook should handle gracefully - undefined result gets empty verbose_map
|
||||
expect(result.current.status).toBe('complete');
|
||||
expect(result.current.result).toEqual({ verbose_map: {} });
|
||||
expect(result.current.error).toBeNull();
|
||||
beforeEach(() => {
|
||||
// Configure the module-level mock to return our extension for extension tests
|
||||
mockGetExtensionsRegistry.mockReturnValue({
|
||||
get: jest.fn((key: string) =>
|
||||
key === 'load.drillby.options' ? mockExtension : undefined,
|
||||
) as any,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore default behavior to prevent test pollution
|
||||
mockGetExtensionsRegistry.mockReturnValue({ get: () => undefined });
|
||||
});
|
||||
|
||||
test('useDatasetDrillInfo fetches dataset via extension when extension and formData provided', async () => {
|
||||
const mockFormData = {
|
||||
viz_type: 'table',
|
||||
datasource: '123__table',
|
||||
adhoc_filters: [],
|
||||
};
|
||||
const mockDataset = {
|
||||
id: 123,
|
||||
columns: [{ column_name: 'col1', verbose_name: 'Column 1' }],
|
||||
metrics: [{ metric_name: 'metric1', verbose_name: 'Metric 1' }],
|
||||
};
|
||||
|
||||
mockExtension.mockResolvedValue(buildCachedResponse(mockDataset));
|
||||
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useDatasetDrillInfo(123, 456, mockFormData),
|
||||
);
|
||||
|
||||
expect(result.current.status).toBe('loading');
|
||||
|
||||
await waitFor(() => expect(result.current.status).toBe('complete'));
|
||||
|
||||
// Verify extension was called with correct arguments
|
||||
expect(mockExtension).toHaveBeenCalledWith(123, mockFormData);
|
||||
|
||||
// Verify result contains dataset with verbose_map
|
||||
expect(result.current.result).toEqual({
|
||||
...mockDataset,
|
||||
verbose_map: {
|
||||
col1: 'Column 1',
|
||||
metric1: 'Metric 1',
|
||||
},
|
||||
});
|
||||
expect(result.current.error).toBeNull();
|
||||
|
||||
// Verify cachedSupersetGet was NOT called (extension path bypasses REST API)
|
||||
expect(mockedCachedSupersetGet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('useDatasetDrillInfo handles extension throwing error', async () => {
|
||||
const mockFormData = { viz_type: 'table', datasource: '123__table' };
|
||||
const extensionError = new Error('Extension failed');
|
||||
|
||||
mockExtension.mockRejectedValue(extensionError);
|
||||
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useDatasetDrillInfo(123, 456, mockFormData),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.status).toBe('error'));
|
||||
|
||||
// Verify error state
|
||||
expect(result.current.result).toBeNull();
|
||||
expect(result.current.error).toBeInstanceOf(Error);
|
||||
expect(result.current.error?.message).toBe('Extension failed');
|
||||
|
||||
// Verify REST API was not called
|
||||
expect(mockedCachedSupersetGet).not.toHaveBeenCalled();
|
||||
|
||||
// Verify cache is NOT deleted for extension errors (extensions don't use cache)
|
||||
expect(mockedSupersetGetCacheDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('useDatasetDrillInfo handles extension returning malformed payload with undefined result', async () => {
|
||||
const mockFormData = { viz_type: 'table', datasource: '123__table' };
|
||||
|
||||
// Extension returns undefined instead of expected shape
|
||||
mockExtension.mockResolvedValue(undefined);
|
||||
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useDatasetDrillInfo(123, 456, mockFormData),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.status).toBe('complete'));
|
||||
|
||||
// Hook should handle gracefully and set result with empty verbose_map
|
||||
expect(result.current.result).toEqual({ verbose_map: {} });
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
|
||||
test('useDatasetDrillInfo handles extension returning malformed payload with missing json.result', async () => {
|
||||
const mockFormData = { viz_type: 'table', datasource: '123__table' };
|
||||
|
||||
// Extension returns object but missing json.result
|
||||
mockExtension.mockResolvedValue({ json: {} });
|
||||
|
||||
const { result, waitFor } = renderHook(() =>
|
||||
useDatasetDrillInfo(123, 456, mockFormData),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.status).toBe('complete'));
|
||||
|
||||
// Hook should handle gracefully - undefined result gets empty verbose_map
|
||||
expect(result.current.result).toEqual({ verbose_map: {} });
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user