diff --git a/superset-frontend/src/features/datasets/hooks/useDatasetLists.test.ts b/superset-frontend/src/features/datasets/hooks/useDatasetLists.test.ts index 44474687927..6f12d72255b 100644 --- a/superset-frontend/src/features/datasets/hooks/useDatasetLists.test.ts +++ b/superset-frontend/src/features/datasets/hooks/useDatasetLists.test.ts @@ -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 = (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({ diff --git a/superset-frontend/src/hooks/apiResources/datasets.test.ts b/superset-frontend/src/hooks/apiResources/datasets.test.ts index b708415ba02..19ac5c0d243 100644 --- a/superset-frontend/src/hooks/apiResources/datasets.test.ts +++ b/superset-frontend/src/hooks/apiResources/datasets.test.ts @@ -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 = (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(); +});