| /** |
| * 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 fetchMock from 'fetch-mock'; |
| import * as hooks from 'src/views/CRUD/hooks'; |
| import * as themeApi from 'src/features/themes/api'; |
| import * as getBootstrapData from 'src/utils/getBootstrapData'; |
| import ThemesList from './index'; |
| |
| // Mock the getBootstrapData function |
| jest.mock('src/utils/getBootstrapData', () => ({ |
| __esModule: true, |
| default: () => ({ |
| common: { |
| theme: { |
| enableUiThemeAdministration: true, |
| }, |
| }, |
| user: { |
| userId: 1, |
| username: 'admin', |
| roles: { |
| Admin: [['can_write', 'Theme']], |
| }, |
| }, |
| }), |
| })); |
| |
| // Mock theme API functions |
| jest.mock('src/features/themes/api', () => ({ |
| setSystemDefaultTheme: jest.fn(() => Promise.resolve()), |
| setSystemDarkTheme: jest.fn(() => Promise.resolve()), |
| unsetSystemDefaultTheme: jest.fn(() => Promise.resolve()), |
| unsetSystemDarkTheme: jest.fn(() => Promise.resolve()), |
| })); |
| |
| // Mock the CRUD hooks |
| jest.mock('src/views/CRUD/hooks', () => ({ |
| ...jest.requireActual('src/views/CRUD/hooks'), |
| useListViewResource: jest.fn(), |
| })); |
| |
| // Mock the useThemeContext hook |
| jest.mock('src/theme/ThemeProvider', () => ({ |
| ...jest.requireActual('src/theme/ThemeProvider'), |
| useThemeContext: jest.fn().mockReturnValue({ |
| getCurrentCrudThemeId: jest.fn().mockReturnValue('1'), |
| appliedTheme: { theme_name: 'Light Theme', id: 1 }, |
| }), |
| })); |
| |
| const mockThemes = [ |
| { |
| id: 1, |
| theme_name: 'Light Theme', |
| is_system_default: true, |
| is_system_dark: false, |
| created_by: { id: 1, first_name: 'Admin', last_name: 'User' }, |
| changed_on_delta_humanized: '1 day ago', |
| }, |
| { |
| id: 2, |
| theme_name: 'Dark Theme', |
| is_system_default: false, |
| is_system_dark: true, |
| created_by: { id: 1, first_name: 'Admin', last_name: 'User' }, |
| changed_on_delta_humanized: '2 days ago', |
| }, |
| { |
| id: 3, |
| theme_name: 'Custom Theme', |
| is_system_default: false, |
| is_system_dark: false, |
| created_by: { id: 2, first_name: 'Test', last_name: 'User' }, |
| changed_on_delta_humanized: '3 days ago', |
| }, |
| ]; |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('ThemesList', () => { |
| beforeEach(() => { |
| // Mock the useListViewResource hook |
| (hooks.useListViewResource as jest.Mock).mockReturnValue({ |
| state: { |
| loading: false, |
| resourceCollection: mockThemes, |
| resourceCount: 3, |
| bulkSelectEnabled: false, |
| }, |
| setResourceCollection: jest.fn(), |
| hasPerm: jest.fn().mockReturnValue(true), |
| refreshData: jest.fn(), |
| fetchData: jest.fn(), |
| toggleBulkSelect: jest.fn(), |
| }); |
| |
| fetchMock.reset(); |
| fetchMock.get('glob:*/api/v1/theme/*', { |
| count: 3, |
| result: mockThemes, |
| }); |
| }); |
| |
| afterEach(() => { |
| fetchMock.restore(); |
| jest.clearAllMocks(); |
| }); |
| |
| test('renders the themes list with proper structure', async () => { |
| render( |
| <ThemesList addDangerToast={jest.fn()} addSuccessToast={jest.fn()} />, |
| { |
| useRedux: true, |
| useDnd: true, |
| useQueryParams: true, |
| }, |
| ); |
| |
| await waitFor(() => { |
| expect(screen.getByText('Light Theme')).toBeInTheDocument(); |
| expect(screen.getByText('Dark Theme')).toBeInTheDocument(); |
| expect(screen.getByText('Custom Theme')).toBeInTheDocument(); |
| }); |
| }); |
| |
| test('shows system theme badges for default and dark themes', async () => { |
| render( |
| <ThemesList addDangerToast={jest.fn()} addSuccessToast={jest.fn()} />, |
| { |
| useRedux: true, |
| useDnd: true, |
| useQueryParams: true, |
| }, |
| ); |
| |
| await waitFor(() => { |
| // Check for system badges |
| expect(screen.getByText('Light Theme')).toBeInTheDocument(); |
| expect(screen.getByText('Dark Theme')).toBeInTheDocument(); |
| }); |
| |
| // Verify that themes with system flags are marked appropriately |
| expect(mockThemes[0].is_system_default).toBe(true); |
| expect(mockThemes[1].is_system_dark).toBe(true); |
| expect(mockThemes[2].is_system_default).toBe(false); |
| expect(mockThemes[2].is_system_dark).toBe(false); |
| }); |
| |
| test('uses flat theme structure for enableUiThemeAdministration', () => { |
| // Verify the component accesses the correct bootstrap data structure |
| const { common } = getBootstrapData.default(); |
| |
| // Should access flat structure |
| expect(common.theme.enableUiThemeAdministration).toBe(true); |
| |
| // Should NOT have nested settings structure |
| expect((common.theme as any).settings).toBeUndefined(); |
| }); |
| |
| test('shows admin controls when user has proper permissions', async () => { |
| render( |
| <ThemesList addDangerToast={jest.fn()} addSuccessToast={jest.fn()} />, |
| { |
| useRedux: true, |
| useDnd: true, |
| useQueryParams: true, |
| }, |
| ); |
| |
| await waitFor(() => { |
| expect(screen.getByText('Light Theme')).toBeInTheDocument(); |
| }); |
| |
| // Admin controls should be visible |
| const adminElements = screen.queryAllByRole('button'); |
| expect(adminElements.length).toBeGreaterThan(0); |
| }); |
| |
| test('calls setSystemDefaultTheme API when setting a theme as default', async () => { |
| const { setSystemDefaultTheme } = themeApi; |
| const addSuccessToast = jest.fn(); |
| const refreshData = jest.fn(); |
| |
| (hooks.useListViewResource as jest.Mock).mockReturnValue({ |
| state: { |
| loading: false, |
| resourceCollection: mockThemes, |
| resourceCount: 3, |
| bulkSelectEnabled: false, |
| }, |
| setResourceCollection: jest.fn(), |
| hasPerm: jest.fn().mockReturnValue(true), |
| refreshData, |
| fetchData: jest.fn(), |
| toggleBulkSelect: jest.fn(), |
| }); |
| |
| render( |
| <ThemesList |
| addDangerToast={jest.fn()} |
| addSuccessToast={addSuccessToast} |
| />, |
| { |
| useRedux: true, |
| useDnd: true, |
| useQueryParams: true, |
| }, |
| ); |
| |
| await waitFor(() => { |
| expect(screen.getByText('Custom Theme')).toBeInTheDocument(); |
| }); |
| |
| // Since the actual UI interaction would be complex to test, |
| // we verify the API function exists and can be called |
| expect(setSystemDefaultTheme).toBeDefined(); |
| |
| // Test calling the API directly |
| await setSystemDefaultTheme(3); |
| expect(setSystemDefaultTheme).toHaveBeenCalledWith(3); |
| }); |
| |
| test('configures theme deletion endpoint', async () => { |
| const addDangerToast = jest.fn(); |
| const addSuccessToast = jest.fn(); |
| const refreshData = jest.fn(); |
| |
| // Setup delete mock before rendering |
| fetchMock.delete('glob:*/api/v1/theme/*', 200); |
| |
| // Mock the useListViewResource hook with refreshData |
| (hooks.useListViewResource as jest.Mock).mockReturnValue({ |
| state: { |
| loading: false, |
| resourceCollection: mockThemes, |
| resourceCount: 3, |
| bulkSelectEnabled: false, |
| }, |
| setResourceCollection: jest.fn(), |
| hasPerm: jest.fn().mockReturnValue(true), |
| refreshData, |
| fetchData: jest.fn(), |
| toggleBulkSelect: jest.fn(), |
| }); |
| |
| render( |
| <ThemesList |
| addDangerToast={addDangerToast} |
| addSuccessToast={addSuccessToast} |
| />, |
| { |
| useRedux: true, |
| useDnd: true, |
| useQueryParams: true, |
| }, |
| ); |
| |
| await waitFor(() => { |
| expect(screen.getByText('Custom Theme')).toBeInTheDocument(); |
| }); |
| |
| // Verify that themes are rendered correctly |
| expect(screen.getByText('Light Theme')).toBeInTheDocument(); |
| expect(screen.getByText('Dark Theme')).toBeInTheDocument(); |
| expect(screen.getByText('Custom Theme')).toBeInTheDocument(); |
| |
| // Verify that action buttons are present (which include delete for non-system themes) |
| const actionButtons = screen.getAllByRole('button'); |
| expect(actionButtons.length).toBeGreaterThan(0); |
| }); |
| }); |