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'),