You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ms...@apache.org on 2025/12/17 17:41:19 UTC
(superset) branch master updated: fix(SearchFilter): prevent unintended autocomplete on search input (#36209)
This is an automated email from the ASF dual-hosted git repository.
msyavuz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new c9ec173647 fix(SearchFilter): prevent unintended autocomplete on search input (#36209)
c9ec173647 is described below
commit c9ec173647c4c1d71758153df4d2f6809d2dc0a2
Author: Luis Sánchez <lu...@gmail.com>
AuthorDate: Wed Dec 17 14:41:07 2025 -0300
fix(SearchFilter): prevent unintended autocomplete on search input (#36209)
---
.../components/ListView/Filters/Search.test.tsx | 230 +++++++++++++++++++++
.../src/components/ListView/Filters/Search.tsx | 3 +
.../src/components/ListView/Filters/index.test.tsx | 132 ++++++++++++
.../src/components/ListView/Filters/index.tsx | 5 +-
superset-frontend/src/components/ListView/types.ts | 2 +
.../src/pages/AlertReportList/index.tsx | 1 +
.../src/pages/AnnotationLayerList/index.tsx | 1 +
superset-frontend/src/pages/GroupsList/index.tsx | 1 +
superset-frontend/src/pages/RolesList/index.tsx | 1 +
.../src/pages/RowLevelSecurityList/index.tsx | 1 +
superset-frontend/src/pages/Tags/index.tsx | 1 +
11 files changed, 377 insertions(+), 1 deletion(-)
diff --git a/superset-frontend/src/components/ListView/Filters/Search.test.tsx b/superset-frontend/src/components/ListView/Filters/Search.test.tsx
new file mode 100644
index 0000000000..a117988aab
--- /dev/null
+++ b/superset-frontend/src/components/ListView/Filters/Search.test.tsx
@@ -0,0 +1,230 @@
+/**
+ * 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,
+ fireEvent,
+} from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
+import { createRef } from 'react';
+import SearchFilter from './Search';
+import type { FilterHandler } from './types';
+
+const mockOnSubmit = jest.fn();
+
+const defaultProps = {
+ Header: 'Search Header',
+ name: 'search-input',
+ initialValue: '',
+ toolTipDescription: undefined,
+ onSubmit: mockOnSubmit,
+ autoComplete: undefined,
+};
+
+const setup = (props = {}) =>
+ render(<SearchFilter {...defaultProps} {...props} />);
+
+beforeEach(() => {
+ mockOnSubmit.mockClear();
+});
+
+test('renders search filter with header', () => {
+ setup();
+ expect(screen.getByText('Search Header')).toBeInTheDocument();
+});
+
+test('renders input with placeholder', () => {
+ setup();
+ expect(screen.getByPlaceholderText('Type a value')).toBeInTheDocument();
+});
+
+test('renders search icon', () => {
+ setup();
+ expect(screen.getByTestId('filters-search')).toBeInTheDocument();
+ // The icon should be present as a prefix
+ const input = screen.getByTestId('filters-search');
+ expect(input.parentElement?.querySelector('.anticon-search')).toBeTruthy();
+});
+
+test('renders with initial value', () => {
+ setup({ initialValue: 'initial search term' });
+ const input = screen.getByTestId('filters-search') as HTMLInputElement;
+ expect(input.value).toBe('initial search term');
+});
+
+test('renders tooltip when toolTipDescription is provided', () => {
+ setup({ toolTipDescription: 'This is a helpful tooltip' });
+ const tooltip = screen.getByRole('img', { name: /info-circle/i });
+ expect(tooltip).toBeInTheDocument();
+});
+
+test('does not render tooltip when toolTipDescription is undefined', () => {
+ setup({ toolTipDescription: undefined });
+ const tooltip = screen.queryByRole('img', { name: /info-circle/i });
+ expect(tooltip).not.toBeInTheDocument();
+});
+
+test('updates input value on change', async () => {
+ setup();
+ const input = screen.getByTestId('filters-search') as HTMLInputElement;
+
+ await userEvent.type(input, 'test query');
+
+ expect(input.value).toBe('test query');
+});
+
+test('calls onSubmit with trimmed value on blur', async () => {
+ setup();
+ const input = screen.getByTestId('filters-search');
+
+ await userEvent.type(input, ' search term ');
+ await userEvent.tab(); // Trigger blur
+
+ await waitFor(() => {
+ expect(mockOnSubmit).toHaveBeenCalledWith('search term');
+ });
+});
+
+test('calls onSubmit with trimmed value on Enter key', async () => {
+ setup();
+ const input = screen.getByTestId('filters-search');
+
+ await userEvent.type(input, ' another search ');
+ fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
+
+ await waitFor(() => {
+ expect(mockOnSubmit).toHaveBeenCalledWith('another search');
+ });
+});
+
+test('calls onSubmit with empty string when input is cleared', async () => {
+ setup({ initialValue: 'initial value' });
+ const input = screen.getByTestId('filters-search');
+
+ await userEvent.clear(input);
+
+ await waitFor(() => {
+ expect(mockOnSubmit).toHaveBeenCalledWith('');
+ });
+});
+
+test('does not call onSubmit when empty value is submitted on blur', async () => {
+ setup();
+ const input = screen.getByTestId('filters-search');
+
+ await userEvent.click(input);
+ await userEvent.tab(); // Trigger blur without typing
+
+ // onSubmit should not be called for empty value on blur/enter
+ expect(mockOnSubmit).not.toHaveBeenCalled();
+});
+
+test('calls onSubmit with empty string when only whitespace is entered and cleared', async () => {
+ setup();
+ const input = screen.getByTestId('filters-search');
+
+ await userEvent.type(input, ' ');
+
+ // When cleared (which happens when the value becomes empty), onSubmit is called with ''
+ await userEvent.clear(input);
+
+ await waitFor(() => {
+ expect(mockOnSubmit).toHaveBeenCalledWith('');
+ });
+});
+
+test('clearFilter resets value and calls onSubmit with empty string', async () => {
+ const ref = createRef<FilterHandler>();
+ render(<SearchFilter {...defaultProps} initialValue="test" ref={ref} />);
+
+ const input = screen.getByTestId('filters-search') as HTMLInputElement;
+ expect(input.value).toBe('test');
+
+ // Call clearFilter via ref
+ ref.current?.clearFilter();
+
+ await waitFor(() => {
+ expect(input.value).toBe('');
+ expect(mockOnSubmit).toHaveBeenCalledWith('');
+ });
+});
+
+test('uses custom name attribute when provided', () => {
+ setup({ name: 'custom-search-name' });
+ const input = screen.getByTestId('filters-search') as HTMLInputElement;
+ expect(input.name).toBe('custom-search-name');
+});
+
+test('sets autocomplete to off by default', () => {
+ setup();
+ const input = screen.getByTestId('filters-search') as HTMLInputElement;
+ expect(input.autocomplete).toBe('off');
+});
+
+test('uses custom autocomplete value when provided', () => {
+ setup({ autoComplete: 'new-password' });
+ const input = screen.getByTestId('filters-search') as HTMLInputElement;
+ expect(input.autocomplete).toBe('new-password');
+});
+
+test('renders with allowClear prop', () => {
+ setup({ initialValue: 'test value' });
+ const input = screen.getByTestId('filters-search');
+ // Ant Design Input with allowClear should have a clear button when there's a value
+ expect(input).toBeInTheDocument();
+});
+
+test('multiple rapid changes only submit the final value', async () => {
+ setup();
+ const input = screen.getByTestId('filters-search');
+
+ await userEvent.type(input, 'first');
+ await userEvent.type(input, ' second');
+ await userEvent.type(input, ' third');
+ await userEvent.tab();
+
+ await waitFor(() => {
+ expect(mockOnSubmit).toHaveBeenCalledWith('first second third');
+ // Should only be called once on blur, not for each keystroke
+ expect(mockOnSubmit).toHaveBeenCalledTimes(1);
+ });
+});
+
+test('handles programmatic value changes through clearFilter', async () => {
+ const ref = createRef<FilterHandler>();
+ render(
+ <SearchFilter {...defaultProps} initialValue="initial value" ref={ref} />,
+ );
+
+ const input = screen.getByTestId('filters-search') as HTMLInputElement;
+
+ // Type something new
+ await userEvent.clear(input);
+ await userEvent.type(input, 'new value');
+ expect(input.value).toBe('new value');
+
+ // Clear using ref
+ ref.current?.clearFilter();
+
+ await waitFor(() => {
+ expect(input.value).toBe('');
+ expect(mockOnSubmit).toHaveBeenCalledWith('');
+ });
+});
diff --git a/superset-frontend/src/components/ListView/Filters/Search.tsx b/superset-frontend/src/components/ListView/Filters/Search.tsx
index 25df6632d0..c512b0aa2b 100644
--- a/superset-frontend/src/components/ListView/Filters/Search.tsx
+++ b/superset-frontend/src/components/ListView/Filters/Search.tsx
@@ -42,6 +42,7 @@ interface SearchHeaderProps extends BaseFilter {
onSubmit: (val: string) => void;
name: string;
toolTipDescription: string | undefined;
+ autoComplete?: string;
}
function SearchFilter(
@@ -51,6 +52,7 @@ function SearchFilter(
initialValue,
toolTipDescription,
onSubmit,
+ autoComplete = 'off',
}: SearchHeaderProps,
ref: RefObject<FilterHandler>,
) {
@@ -91,6 +93,7 @@ function SearchFilter(
allowClear
data-test="filters-search"
placeholder={t('Type a value')}
+ autoComplete={autoComplete}
name={name}
value={value}
onChange={handleChange}
diff --git a/superset-frontend/src/components/ListView/Filters/index.test.tsx b/superset-frontend/src/components/ListView/Filters/index.test.tsx
new file mode 100644
index 0000000000..1d75dbe917
--- /dev/null
+++ b/superset-frontend/src/components/ListView/Filters/index.test.tsx
@@ -0,0 +1,132 @@
+/**
+ * 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 } from 'spec/helpers/testing-library';
+import { ListViewFilterOperator } from '../types';
+import UIFilters from './index';
+
+const mockUpdateFilterValue = jest.fn();
+
+beforeEach(() => {
+ mockUpdateFilterValue.mockClear();
+});
+
+test('search filter uses id as input name when inputName is not provided', () => {
+ const filters = [
+ {
+ Header: 'Name',
+ key: 'name',
+ id: 'name',
+ input: 'search' as const,
+ operator: ListViewFilterOperator.Contains,
+ },
+ ];
+
+ render(
+ <UIFilters
+ filters={filters}
+ internalFilters={[]}
+ updateFilterValue={mockUpdateFilterValue}
+ />,
+ );
+
+ const input = screen.getByTestId('filters-search') as HTMLInputElement;
+ expect(input.name).toBe('name');
+});
+
+test('search filter uses inputName when provided instead of id', () => {
+ const filters = [
+ {
+ Header: 'Name',
+ key: 'name',
+ id: 'name',
+ input: 'search' as const,
+ operator: ListViewFilterOperator.Contains,
+ inputName: 'custom_search_name',
+ },
+ ];
+
+ render(
+ <UIFilters
+ filters={filters}
+ internalFilters={[]}
+ updateFilterValue={mockUpdateFilterValue}
+ />,
+ );
+
+ const input = screen.getByTestId('filters-search') as HTMLInputElement;
+ expect(input.name).toBe('custom_search_name');
+});
+
+test('search filter passes autoComplete prop correctly', () => {
+ const filters = [
+ {
+ Header: 'Name',
+ key: 'name',
+ id: 'name',
+ input: 'search' as const,
+ operator: ListViewFilterOperator.Contains,
+ autoComplete: 'new-password',
+ },
+ ];
+
+ render(
+ <UIFilters
+ filters={filters}
+ internalFilters={[]}
+ updateFilterValue={mockUpdateFilterValue}
+ />,
+ );
+
+ const input = screen.getByTestId('filters-search') as HTMLInputElement;
+ expect(input.autocomplete).toBe('new-password');
+});
+
+test('renders multiple search filters with different inputName values', () => {
+ const filters = [
+ {
+ Header: 'Name',
+ key: 'name',
+ id: 'name',
+ input: 'search' as const,
+ operator: ListViewFilterOperator.Contains,
+ inputName: 'filter_name_search',
+ },
+ {
+ Header: 'Description',
+ key: 'description',
+ id: 'description',
+ input: 'search' as const,
+ operator: ListViewFilterOperator.Contains,
+ // No inputName - should use id
+ },
+ ];
+
+ render(
+ <UIFilters
+ filters={filters}
+ internalFilters={[]}
+ updateFilterValue={mockUpdateFilterValue}
+ />,
+ );
+
+ const inputs = screen.getAllByTestId('filters-search') as HTMLInputElement[];
+ expect(inputs).toHaveLength(2);
+ expect(inputs[0].name).toBe('filter_name_search');
+ expect(inputs[1].name).toBe('description');
+});
diff --git a/superset-frontend/src/components/ListView/Filters/index.tsx b/superset-frontend/src/components/ListView/Filters/index.tsx
index abac7d54e1..de85669a71 100644
--- a/superset-frontend/src/components/ListView/Filters/index.tsx
+++ b/superset-frontend/src/components/ListView/Filters/index.tsx
@@ -81,6 +81,8 @@ function UIFilters(
min,
max,
dropdownStyle,
+ autoComplete,
+ inputName,
},
index,
) => {
@@ -121,7 +123,7 @@ function UIFilters(
Header={Header}
initialValue={initialValue}
key={key}
- name={id}
+ name={inputName ?? id}
toolTipDescription={toolTipDescription}
onSubmit={(value: string) => {
if (onFilterUpdate) {
@@ -130,6 +132,7 @@ function UIFilters(
updateFilterValue(index, value);
}}
+ autoComplete={autoComplete}
/>
);
}
diff --git a/superset-frontend/src/components/ListView/types.ts b/superset-frontend/src/components/ListView/types.ts
index 5f0275ecad..1d7042798a 100644
--- a/superset-frontend/src/components/ListView/types.ts
+++ b/superset-frontend/src/components/ListView/types.ts
@@ -65,6 +65,8 @@ export interface ListViewFilter {
min?: number;
max?: number;
dropdownStyle?: React.CSSProperties;
+ autoComplete?: string;
+ inputName?: string;
}
export type ListViewFilters = ListViewFilter[];
diff --git a/superset-frontend/src/pages/AlertReportList/index.tsx b/superset-frontend/src/pages/AlertReportList/index.tsx
index 532c97fd1e..918ffdfac5 100644
--- a/superset-frontend/src/pages/AlertReportList/index.tsx
+++ b/superset-frontend/src/pages/AlertReportList/index.tsx
@@ -471,6 +471,7 @@ function AlertList({
id: 'name',
input: 'search',
operator: FilterOperator.Contains,
+ inputName: 'alert_report_list_search',
},
{
Header: t('Owner'),
diff --git a/superset-frontend/src/pages/AnnotationLayerList/index.tsx b/superset-frontend/src/pages/AnnotationLayerList/index.tsx
index 6a177a64f3..523cca2ece 100644
--- a/superset-frontend/src/pages/AnnotationLayerList/index.tsx
+++ b/superset-frontend/src/pages/AnnotationLayerList/index.tsx
@@ -255,6 +255,7 @@ function AnnotationLayersList({
id: 'name',
input: 'search',
operator: FilterOperator.Contains,
+ inputName: 'annotation_layer_list_search',
},
{
Header: t('Changed by'),
diff --git a/superset-frontend/src/pages/GroupsList/index.tsx b/superset-frontend/src/pages/GroupsList/index.tsx
index 2df81ef95a..f11c93cec4 100644
--- a/superset-frontend/src/pages/GroupsList/index.tsx
+++ b/superset-frontend/src/pages/GroupsList/index.tsx
@@ -299,6 +299,7 @@ function GroupsList({ user }: GroupsListProps) {
id: 'name',
input: 'search',
operator: ListViewFilterOperator.Contains,
+ inputName: 'group_list_search',
},
{
Header: t('Label'),
diff --git a/superset-frontend/src/pages/RolesList/index.tsx b/superset-frontend/src/pages/RolesList/index.tsx
index 7ea9267277..0a27cd019f 100644
--- a/superset-frontend/src/pages/RolesList/index.tsx
+++ b/superset-frontend/src/pages/RolesList/index.tsx
@@ -300,6 +300,7 @@ function RolesList({ addDangerToast, addSuccessToast, user }: RolesListProps) {
id: 'name',
input: 'search',
operator: FilterOperator.Contains,
+ inputName: 'role_list_search',
},
{
Header: t('Users'),
diff --git a/superset-frontend/src/pages/RowLevelSecurityList/index.tsx b/superset-frontend/src/pages/RowLevelSecurityList/index.tsx
index c4c33a0df5..2e57c8232a 100644
--- a/superset-frontend/src/pages/RowLevelSecurityList/index.tsx
+++ b/superset-frontend/src/pages/RowLevelSecurityList/index.tsx
@@ -266,6 +266,7 @@ function RowLevelSecurityList(props: RLSProps) {
id: 'name',
input: 'search',
operator: FilterOperator.StartsWith,
+ inputName: 'rls_list_search',
},
{
Header: t('Filter Type'),
diff --git a/superset-frontend/src/pages/Tags/index.tsx b/superset-frontend/src/pages/Tags/index.tsx
index c653bcbbd9..933f5c3a3b 100644
--- a/superset-frontend/src/pages/Tags/index.tsx
+++ b/superset-frontend/src/pages/Tags/index.tsx
@@ -266,6 +266,7 @@ function TagList(props: TagListProps) {
id: 'name',
input: 'search',
operator: FilterOperator.Contains,
+ inputName: 'tag_list_search',
},
{
Header: t('Modified by'),