| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| import { render, screen, waitFor } from 'spec/helpers/testing-library'; |
| import userEvent from '@testing-library/user-event'; |
| import { SupersetClient } from '@superset-ui/core'; |
| import MatrixifyDimensionControl, { |
| MatrixifyDimensionControlValue, |
| } from './MatrixifyDimensionControl'; |
| |
| import { fetchTopNValues } from './MatrixifyControl/utils/fetchTopNValues'; |
| |
| // Mock SupersetClient |
| jest.mock('@superset-ui/core', () => ({ |
| ...jest.requireActual('@superset-ui/core'), |
| SupersetClient: { |
| get: jest.fn(), |
| }, |
| t: (str: string, ...args: any[]) => { |
| if (args.length > 0 && str.includes('%s')) { |
| return str.replace('%s', args[0]); |
| } |
| return str; |
| }, |
| getColumnLabel: (col: string) => col, |
| })); |
| |
| // Mock fetchTopNValues utility |
| jest.mock('./MatrixifyControl/utils/fetchTopNValues', () => ({ |
| fetchTopNValues: jest.fn(), |
| extractDimensionValues: jest.fn(values => values.map((v: any) => v.value)), |
| })); |
| |
| // Mock ControlHeader |
| jest.mock('src/explore/components/ControlHeader', () => ({ |
| __esModule: true, |
| default: ({ label, description }: any) => ( |
| <div data-testid="control-header"> |
| {label && <span data-testid="label">{label}</span>} |
| {description && <span data-testid="description">{description}</span>} |
| </div> |
| ), |
| })); |
| |
| const mockDatasource = { |
| id: 1, |
| type: 'table', |
| columns: [ |
| { column_name: 'country', verbose_name: 'Country' }, |
| { column_name: 'region', verbose_name: 'Region' }, |
| { column_name: 'product', verbose_name: 'Product' }, |
| ], |
| filter_select: true, |
| }; |
| |
| const defaultProps = { |
| datasource: mockDatasource, |
| onChange: jest.fn(), |
| }; |
| |
| beforeEach(() => { |
| jest.clearAllMocks(); |
| }); |
| |
| test('should render dimension selector with default label', () => { |
| render(<MatrixifyDimensionControl {...defaultProps} />); |
| |
| expect(screen.getByText('Dimension')).toBeInTheDocument(); |
| expect(screen.getAllByText('Select a dimension')).toHaveLength(2); // Header description + placeholder |
| expect( |
| screen.getByRole('combobox', { name: 'Select dimension' }), |
| ).toBeInTheDocument(); |
| }); |
| |
| test('should render with custom label and description', () => { |
| render( |
| <MatrixifyDimensionControl |
| {...defaultProps} |
| label="Custom Label" |
| description="Custom Description" |
| />, |
| ); |
| |
| expect(screen.getByText('Custom Label')).toBeInTheDocument(); |
| expect(screen.getByText('Custom Description')).toBeInTheDocument(); |
| }); |
| |
| test('should display dimension options from datasource columns', async () => { |
| render(<MatrixifyDimensionControl {...defaultProps} />); |
| |
| const select = screen.getByRole('combobox', { name: 'Select dimension' }); |
| await userEvent.click(select); |
| |
| await waitFor(() => { |
| expect(screen.getByText('country')).toBeInTheDocument(); |
| expect(screen.getByText('region')).toBeInTheDocument(); |
| expect(screen.getByText('product')).toBeInTheDocument(); |
| }); |
| }); |
| |
| test('should call onChange when dimension is selected', async () => { |
| const onChange = jest.fn(); |
| render(<MatrixifyDimensionControl {...defaultProps} onChange={onChange} />); |
| |
| const select = screen.getByRole('combobox', { name: 'Select dimension' }); |
| await userEvent.click(select); |
| await userEvent.click(screen.getByText('country')); |
| |
| expect(onChange).toHaveBeenCalledWith({ |
| dimension: 'country', |
| values: [], |
| }); |
| }); |
| |
| test('should display selected dimension value', () => { |
| const value: MatrixifyDimensionControlValue = { |
| dimension: 'country', |
| values: ['USA', 'Canada'], |
| }; |
| |
| render(<MatrixifyDimensionControl {...defaultProps} value={value} />); |
| |
| // Check that the component renders with the selected dimension |
| expect( |
| screen.getByRole('combobox', { name: 'Select dimension' }), |
| ).toBeInTheDocument(); |
| // In members mode, the value selector should also appear |
| expect( |
| screen.getByRole('combobox', { name: 'Select dimension values' }), |
| ).toBeInTheDocument(); |
| }); |
| |
| test('should show value selector in members mode when dimension is selected', () => { |
| const value: MatrixifyDimensionControlValue = { |
| dimension: 'country', |
| values: [], |
| }; |
| |
| (SupersetClient.get as jest.Mock).mockResolvedValue({ |
| json: { result: ['USA', 'Canada'] }, |
| }); |
| |
| render( |
| <MatrixifyDimensionControl |
| {...defaultProps} |
| value={value} |
| selectionMode="members" |
| />, |
| ); |
| |
| expect( |
| screen.getByRole('combobox', { name: 'Select dimension values' }), |
| ).toBeInTheDocument(); |
| }); |
| |
| test('should load dimension values from API in members mode', async () => { |
| const value: MatrixifyDimensionControlValue = { |
| dimension: 'country', |
| values: [], |
| }; |
| |
| (SupersetClient.get as jest.Mock).mockResolvedValue({ |
| json: { result: ['USA', 'Canada', 'Mexico'] }, |
| }); |
| |
| render( |
| <MatrixifyDimensionControl |
| {...defaultProps} |
| value={value} |
| selectionMode="members" |
| />, |
| ); |
| |
| await waitFor(() => { |
| expect(SupersetClient.get).toHaveBeenCalledWith({ |
| signal: expect.any(AbortSignal), |
| endpoint: '/api/v1/datasource/table/1/column/country/values/', |
| }); |
| }); |
| }); |
| |
| test('should handle API errors gracefully in members mode', async () => { |
| const value: MatrixifyDimensionControlValue = { |
| dimension: 'country', |
| values: [], |
| }; |
| |
| (SupersetClient.get as jest.Mock).mockRejectedValue(new Error('API Error')); |
| |
| render( |
| <MatrixifyDimensionControl |
| {...defaultProps} |
| value={value} |
| selectionMode="members" |
| />, |
| ); |
| |
| await waitFor(() => { |
| expect(SupersetClient.get).toHaveBeenCalled(); |
| }); |
| }); |
| |
| test('should not show value selector in topn mode', () => { |
| const value: MatrixifyDimensionControlValue = { |
| dimension: 'country', |
| values: [], |
| }; |
| |
| render( |
| <MatrixifyDimensionControl |
| {...defaultProps} |
| value={value} |
| selectionMode="topn" |
| />, |
| ); |
| |
| expect( |
| screen.queryByRole('combobox', { name: 'Select dimension values' }), |
| ).not.toBeInTheDocument(); |
| }); |
| |
| test('should fetch TopN values when all params are provided', async () => { |
| const mockFetchTopNValues = fetchTopNValues as jest.MockedFunction< |
| typeof fetchTopNValues |
| >; |
| const onChange = jest.fn(); |
| const value: MatrixifyDimensionControlValue = { |
| dimension: 'country', |
| values: [], |
| }; |
| |
| mockFetchTopNValues.mockResolvedValue([ |
| { value: 'USA', metricValue: 1000 }, |
| { value: 'Canada', metricValue: 800 }, |
| ]); |
| |
| render( |
| <MatrixifyDimensionControl |
| {...defaultProps} |
| value={value} |
| onChange={onChange} |
| selectionMode="topn" |
| topNMetric="revenue" |
| topNValue={2} |
| topNOrder="DESC" |
| />, |
| ); |
| |
| await waitFor(() => { |
| expect(mockFetchTopNValues).toHaveBeenCalledWith({ |
| datasource: '1__table', |
| column: 'country', |
| metric: 'revenue', |
| limit: 2, |
| sortAscending: false, |
| filters: [], |
| timeRange: undefined, |
| }); |
| }); |
| }); |
| |
| test('should display error when TopN fetch fails', async () => { |
| const mockFetchTopNValues = fetchTopNValues as jest.MockedFunction< |
| typeof fetchTopNValues |
| >; |
| const value: MatrixifyDimensionControlValue = { |
| dimension: 'country', |
| values: [], |
| }; |
| |
| mockFetchTopNValues.mockRejectedValue(new Error('Fetch failed')); |
| |
| render( |
| <MatrixifyDimensionControl |
| {...defaultProps} |
| value={value} |
| selectionMode="topn" |
| topNMetric="revenue" |
| topNValue={5} |
| />, |
| ); |
| |
| await waitFor(() => { |
| expect(screen.getByText('Error: Fetch failed')).toBeInTheDocument(); |
| }); |
| }); |
| |
| test('should convert string topNValue to number', async () => { |
| const mockFetchTopNValues = fetchTopNValues as jest.MockedFunction< |
| typeof fetchTopNValues |
| >; |
| const value: MatrixifyDimensionControlValue = { |
| dimension: 'country', |
| values: [], |
| }; |
| |
| mockFetchTopNValues.mockResolvedValue([]); |
| |
| render( |
| <MatrixifyDimensionControl |
| {...defaultProps} |
| value={value} |
| selectionMode="topn" |
| topNMetric="revenue" |
| topNValue={'10' as any} // String instead of number |
| topNOrder="ASC" |
| />, |
| ); |
| |
| await waitFor(() => { |
| expect(fetchTopNValues).toHaveBeenCalledWith( |
| expect.objectContaining({ |
| limit: 10, // Should be converted to number |
| sortAscending: true, |
| }), |
| ); |
| }); |
| }); |
| |
| test('should not load values for datasource without filter_select', () => { |
| const datasourceNoFilter = { |
| ...mockDatasource, |
| filter_select: false, |
| }; |
| |
| const value: MatrixifyDimensionControlValue = { |
| dimension: 'country', |
| values: [], |
| }; |
| |
| render( |
| <MatrixifyDimensionControl |
| {...defaultProps} |
| datasource={datasourceNoFilter} |
| value={value} |
| selectionMode="members" |
| />, |
| ); |
| |
| expect(SupersetClient.get).not.toHaveBeenCalled(); |
| }); |
| |
| test('should handle empty dimension value', () => { |
| const value: MatrixifyDimensionControlValue = { |
| dimension: '', |
| values: [], |
| }; |
| |
| render(<MatrixifyDimensionControl {...defaultProps} value={value} />); |
| |
| expect( |
| screen.queryByRole('combobox', { name: 'Select dimension values' }), |
| ).not.toBeInTheDocument(); |
| }); |
| |
| test('should handle undefined value prop', () => { |
| render(<MatrixifyDimensionControl {...defaultProps} value={undefined} />); |
| |
| expect( |
| screen.getByRole('combobox', { name: 'Select dimension' }), |
| ).toBeInTheDocument(); |
| expect( |
| screen.queryByRole('combobox', { name: 'Select dimension values' }), |
| ).not.toBeInTheDocument(); |
| }); |
| |
| test('should handle datasources without columns', () => { |
| const datasourceWithoutColumns = { |
| ...mockDatasource, |
| columns: [], |
| }; |
| |
| render( |
| <MatrixifyDimensionControl |
| {...defaultProps} |
| datasource={datasourceWithoutColumns} |
| />, |
| ); |
| |
| expect( |
| screen.getByRole('combobox', { name: 'Select dimension' }), |
| ).toBeInTheDocument(); |
| }); |
| |
| test('should clear values when switching from topn to members mode', async () => { |
| const onChange = jest.fn(); |
| const value: MatrixifyDimensionControlValue = { |
| dimension: 'country', |
| values: ['USA', 'Canada'], |
| }; |
| |
| const { rerender } = render( |
| <MatrixifyDimensionControl |
| {...defaultProps} |
| value={value} |
| onChange={onChange} |
| selectionMode="topn" |
| />, |
| ); |
| |
| rerender( |
| <MatrixifyDimensionControl |
| {...defaultProps} |
| value={value} |
| onChange={onChange} |
| selectionMode="members" |
| />, |
| ); |
| |
| await waitFor(() => { |
| expect(onChange).toHaveBeenCalledWith({ |
| dimension: 'country', |
| values: [], |
| topNValues: [], |
| }); |
| }); |
| }); |