diff --git a/superset-frontend/src/features/databases/DatabaseModal/ExtraOptions.test.tsx b/superset-frontend/src/features/databases/DatabaseModal/ExtraOptions.test.tsx
new file mode 100644
index 000000000000..c96ab3073fb8
--- /dev/null
+++ b/superset-frontend/src/features/databases/DatabaseModal/ExtraOptions.test.tsx
@@ -0,0 +1,194 @@
+/**
+ * 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 {
+ act,
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from 'spec/helpers/testing-library';
+import { t } from '@superset-ui/core';
+import * as ace from 'ace-builds';
+
+import ExtraOptions from './ExtraOptions';
+import { DatabaseObject } from '../types';
+
+const defaultDb = {
+ expose_in_sqllab: true,
+ allow_ctas: false,
+ allow_cvas: false,
+ allow_dml: false,
+ allow_run_async: false,
+ cache_timeout: 300,
+ force_ctas_schema: 'public',
+ masked_encrypted_extra: '',
+ server_cert: '',
+ impersonate_user: false,
+ extra: JSON.stringify({
+ cost_estimate_enabled: false,
+ allows_virtual_table_explore: true,
+ disable_data_preview: false,
+ schema_options: { expand_rows: false },
+ metadata_cache_timeout: {
+ schema_cache_timeout: 600,
+ table_cache_timeout: 1200,
+ },
+ disable_drill_to_detail: false,
+ metadata_params: {},
+ engine_params: {},
+ version: '',
+ cancel_query_on_windows_unload: false,
+ }),
+ engine_information: {
+ supports_file_upload: true,
+ supports_dynamic_catalog: true,
+ },
+ configuration_method: '', // added dummy value for configuration_method
+ database_name: 'Test Database', // added dummy value for database_name
+ driver: 'sqlite', // added dummy value for driver
+ id: 1, // added dummy value for id
+ sqlalchemy_uri: 'sqlite:///:memory:', // added dummy value for sqlalchemy_uri
+ parameters: {}, // added dummy value for parameters
+};
+
+describe('ExtraOptions Component', () => {
+ const onInputChange = jest.fn();
+ const onTextChange = jest.fn();
+ const onEditorChange = jest.fn();
+ const onExtraInputChange = jest.fn();
+ const onExtraEditorChange = jest.fn();
+
+ const renderComponent = (dbProps = defaultDb, extension = undefined) =>
+ render(
+ ,
+ );
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders all main panels', () => {
+ renderComponent();
+
+ expect(screen.getByText(t('SQL Lab'))).toBeInTheDocument();
+ expect(screen.getByText(t('Performance'))).toBeInTheDocument();
+ expect(screen.getByText(t('Security'))).toBeInTheDocument();
+ expect(screen.getByText(t('Other'))).toBeInTheDocument();
+ });
+
+ it('calls onInputChange when "Expose database in SQL Lab" checkbox is clicked', () => {
+ renderComponent();
+ const sqlLabText = screen.getByText(t('SQL Lab'));
+ fireEvent.click(sqlLabText);
+
+ const checkbox = screen.getByLabelText(t('Expose database in SQL Lab'));
+ fireEvent.click(checkbox);
+ expect(onInputChange).toHaveBeenCalled();
+ });
+
+ it('calls onExtraInputChange when "Enable query cost estimation" checkbox is clicked', () => {
+ renderComponent();
+ const sqlLabText = screen.getByText(t('SQL Lab'));
+ fireEvent.click(sqlLabText);
+ const checkbox = screen.getByLabelText(t('Enable query cost estimation'));
+ fireEvent.click(checkbox);
+ expect(onExtraInputChange).toHaveBeenCalled();
+ });
+
+ it('calls onExtraEditorChange when metadata_params json editor changes', async () => {
+ renderComponent();
+
+ // Click to open the editor tab/section
+ const otherHeader = screen.getByText(t('Other'));
+ fireEvent.click(otherHeader);
+
+ // Wait for Ace to initialize (in case it's async)
+ await waitFor(() => {
+ expect(document.querySelector('#metadata_params')).toBeInTheDocument();
+ });
+
+ // Grab editor instance by ID or name
+ const editorInstance = ace.edit('metadata_params');
+
+ act(() => {
+ editorInstance.setValue('{"key":"value"}');
+ });
+
+ expect(onExtraEditorChange).toHaveBeenCalledWith({
+ json: '{"key":"value"}',
+ name: 'metadata_params',
+ });
+
+ act(() => {
+ editorInstance.setValue('foo');
+ });
+
+ expect(onExtraEditorChange).toHaveBeenCalledWith({
+ json: 'foo',
+ name: 'metadata_params',
+ });
+
+ // it accepts invalid json strings
+ act(() => {
+ editorInstance.setValue('{"key":"value');
+ });
+
+ expect(onExtraEditorChange).toHaveBeenCalledWith({
+ json: '{"key":"value',
+ name: 'metadata_params',
+ });
+ });
+
+ it('calls onTextChange when server certificate textarea is changed', () => {
+ renderComponent();
+ // Click to open the security tab/section
+ const securityHeader = screen.getByText(t('Security'));
+ fireEvent.click(securityHeader);
+
+ const textarea = screen.getByPlaceholderText(t('Enter CA_BUNDLE'));
+ fireEvent.change(textarea, { target: { value: 'new cert' } });
+ expect(onTextChange).toHaveBeenCalled();
+ });
+
+ it('handles input change for schema cache timeout', () => {
+ renderComponent();
+ const performanceHeader = screen.getByText(t('Performance'));
+ fireEvent.click(performanceHeader);
+ const input = screen.getByTestId('schema-cache-timeout-test');
+ fireEvent.change(input, { target: { value: '500' } });
+ expect(onExtraInputChange).toHaveBeenCalled();
+ });
+
+ it('handles input change for table cache timeout', () => {
+ renderComponent();
+ const performanceHeader = screen.getByText(t('Performance'));
+ fireEvent.click(performanceHeader);
+ const input = screen.getByTestId('table-cache-timeout-test');
+ fireEvent.change(input, { target: { value: '1000' } });
+ expect(onExtraInputChange).toHaveBeenCalled();
+ });
+});
diff --git a/superset-frontend/src/features/databases/DatabaseModal/ExtraOptions.tsx b/superset-frontend/src/features/databases/DatabaseModal/ExtraOptions.tsx
index e4de2df22230..301321114280 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/ExtraOptions.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/ExtraOptions.tsx
@@ -271,6 +271,7 @@ const ExtraOptions = ({
value={db?.cache_timeout || ''}
placeholder={t('Enter duration in seconds')}
onChange={onInputChange}
+ data-test="cache-timeout-test"
/>
@@ -533,7 +534,9 @@ const ExtraOptions = ({
value={
!Object.keys(extraJson?.metadata_params || {}).length
? ''
- : extraJson?.metadata_params
+ : typeof extraJson?.metadata_params === 'string'
+ ? extraJson?.metadata_params
+ : JSON.stringify(extraJson?.metadata_params)
}
/>