diff --git a/superset-frontend/spec/helpers/testing-library.tsx b/superset-frontend/spec/helpers/testing-library.tsx index e97a5ce85a7..7e8ab6151a7 100644 --- a/superset-frontend/spec/helpers/testing-library.tsx +++ b/superset-frontend/spec/helpers/testing-library.tsx @@ -152,3 +152,33 @@ export async function selectOption(option: string, selectName?: string) { ); await userEvent.click(item); } + +/** + * Select an option from a compact pill filter (new UI that replaced comboboxes). + * Clicks the pill button matching the label, then clicks the option in the panel. + */ +export async function selectPillOption(option: string, pillLabel?: string) { + let pill: HTMLElement; + if (pillLabel) { + // Find the pill whose text content includes the label + pill = await waitFor(() => { + const pills = screen.getAllByTestId('compact-filter-pill'); + const match = pills.find(p => p.textContent?.includes(pillLabel)); + if (!match) + throw new Error(`Could not find pill with label "${pillLabel}"`); + return match; + }); + } else { + pill = await screen.findByTestId('compact-filter-pill'); + } + await userEvent.click(pill); + // Wait for the option list to appear and click the item + const item = await waitFor(() => { + const listbox = document.querySelector('[role="listbox"]'); + if (!listbox) throw new Error('No listbox found'); + const opt = within(listbox as HTMLElement).getByText(option); + if (!opt) throw new Error(`Option "${option}" not found`); + return opt; + }); + await userEvent.click(item); +} diff --git a/superset-frontend/src/components/ListView/CardSortSelect.tsx b/superset-frontend/src/components/ListView/CardSortSelect.tsx index 0307a73c2bb..a1e3f4be2d0 100644 --- a/superset-frontend/src/components/ListView/CardSortSelect.tsx +++ b/superset-frontend/src/components/ListView/CardSortSelect.tsx @@ -62,18 +62,20 @@ export const CardSortSelect = ({ }; return ( - {}} - tooltipTitle={String(currentValue.label)} - > - - + + {}} + tooltipTitle={String(currentValue.label)} + > + + + ); }; diff --git a/superset-frontend/src/components/ListView/Filters/index.test.tsx b/superset-frontend/src/components/ListView/Filters/index.test.tsx index 613645786f3..74b2cc36ba0 100644 --- a/superset-frontend/src/components/ListView/Filters/index.test.tsx +++ b/superset-frontend/src/components/ListView/Filters/index.test.tsx @@ -260,7 +260,7 @@ test('numerical_range pill shows active state when value is set', () => { expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument(); }); -test('renders multiple search filters with different inputName values', () => { +test('renders only the first search filter when multiple search filters are configured', () => { const filters = [ { Header: 'Name', @@ -288,8 +288,8 @@ test('renders multiple search filters with different inputName values', () => { />, ); + // Only the first search filter renders — one search box per page const inputs = screen.getAllByTestId('filters-search') as HTMLInputElement[]; - expect(inputs).toHaveLength(2); + expect(inputs).toHaveLength(1); 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 7849519e36f..1086928abc4 100644 --- a/superset-frontend/src/components/ListView/Filters/index.tsx +++ b/superset-frontend/src/components/ListView/Filters/index.tsx @@ -126,47 +126,48 @@ function UIFilters( ? tooltipLabels[index] : undefined; return ( - { - filterRefs[index]?.current?.clearFilter?.(); - updateFilterValue(index, undefined); - setTooltipLabels(prev => { - const next = { ...prev }; - delete next[index]; - return next; - }); - }} - > - { - if (option && !isClear) { - setTooltipLabels(prev => ({ - ...prev, - [index]: - typeof option.label === 'string' - ? option.label - : String(option.value ?? ''), - })); - } - if (onFilterUpdate && !isClear) { - onFilterUpdate(option); - } - updateFilterValue(index, option); + + { + filterRefs[index]?.current?.clearFilter?.(); + updateFilterValue(index, undefined); + setTooltipLabels(prev => { + const next = { ...prev }; + delete next[index]; + return next; + }); }} - /> - + > + { + if (option && !isClear) { + setTooltipLabels(prev => ({ + ...prev, + [index]: + typeof option.label === 'string' + ? option.label + : String(option.value ?? ''), + })); + } + if (onFilterUpdate && !isClear) { + onFilterUpdate(option); + } + updateFilterValue(index, option); + }} + /> + + ); } if (input === 'search' && typeof Header === 'string') { diff --git a/superset-frontend/src/components/ListView/ListView.test.tsx b/superset-frontend/src/components/ListView/ListView.test.tsx index 797b02afa22..0d08bcf9fd9 100644 --- a/superset-frontend/src/components/ListView/ListView.test.tsx +++ b/superset-frontend/src/components/ListView/ListView.test.tsx @@ -345,7 +345,10 @@ describe('ListView', () => { initialSort: [{ id: 'something' }], }); - const sortSelect = screen.getByTestId('card-sort-select'); + const sortSelectContainer = screen.getByTestId('card-sort-select'); + const sortSelect = sortSelectContainer.querySelector( + '[data-test="compact-filter-pill"]', + ) as HTMLElement; await userEvent.click(sortSelect); const sortOption = screen.getByText('Alphabetical'); diff --git a/superset-frontend/src/pages/ChartList/ChartList.test.tsx b/superset-frontend/src/pages/ChartList/ChartList.test.tsx index 6f9dedc5a09..1b16c7674b7 100644 --- a/superset-frontend/src/pages/ChartList/ChartList.test.tsx +++ b/superset-frontend/src/pages/ChartList/ChartList.test.tsx @@ -57,9 +57,12 @@ const mockUser = { const findFilterByLabel = (labelText: string) => { const containers = screen.getAllByTestId('select-filter-container'); for (const container of containers) { - const label = container.querySelector('label'); - if (label?.textContent === labelText) { - return container.querySelector('[role="combobox"], .ant-select'); + // Compact pill filters show the label as button text + const pill = container.querySelector( + '[data-test="compact-filter-pill"]', + ) as HTMLElement | null; + if (pill && pill.textContent?.includes(labelText)) { + return pill; } } return null; diff --git a/superset-frontend/src/pages/DashboardList/DashboardList.cardview.test.tsx b/superset-frontend/src/pages/DashboardList/DashboardList.cardview.test.tsx index c03299cfc43..7733251176d 100644 --- a/superset-frontend/src/pages/DashboardList/DashboardList.cardview.test.tsx +++ b/superset-frontend/src/pages/DashboardList/DashboardList.cardview.test.tsx @@ -156,18 +156,16 @@ describe('DashboardList Card View Tests', () => { ).toBeInTheDocument(); }); - // Find the sort select by its testId, then the combobox within it + // Find the sort select by its testId, then the pill button within it const sortContainer = screen.getByTestId('card-sort-select'); - const sortCombobox = within(sortContainer).getByRole('combobox'); - await userEvent.click(sortCombobox); + // eslint-disable-next-line testing-library/no-node-access + const sortPill = sortContainer.querySelector( + '[data-test="compact-filter-pill"]', + ) as HTMLElement; + await userEvent.click(sortPill); // Select "Alphabetical" from the dropdown - const alphabeticalOption = await waitFor(() => - within( - // eslint-disable-next-line testing-library/no-node-access - document.querySelector('.rc-virtual-list')!, - ).getByText('Alphabetical'), - ); + const alphabeticalOption = await screen.findByText('Alphabetical'); await userEvent.click(alphabeticalOption); await waitFor(() => { diff --git a/superset-frontend/src/pages/DatasetList/DatasetList.integration.test.tsx b/superset-frontend/src/pages/DatasetList/DatasetList.integration.test.tsx index 2a28b7bb11c..ee654a77458 100644 --- a/superset-frontend/src/pages/DatasetList/DatasetList.integration.test.tsx +++ b/superset-frontend/src/pages/DatasetList/DatasetList.integration.test.tsx @@ -20,7 +20,7 @@ import { act, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import fetchMock from 'fetch-mock'; import rison from 'rison'; -import { selectOption } from 'spec/helpers/testing-library'; +import { selectPillOption } from 'spec/helpers/testing-library'; import { setupMocks, renderDatasetList, @@ -102,11 +102,11 @@ test('ListView provider correctly merges filter + sort + pagination state on ref ).toBeGreaterThan(callsBeforeSort); }); - // 2. Apply a filter using selectOption helper + // 2. Apply a filter using selectPillOption helper (compact pill UI) const beforeFilterCallCount = fetchMock.callHistory.calls( API_ENDPOINTS.DATASOURCE_COMBINED, ).length; - await selectOption('Virtual', 'Type'); + await selectPillOption('Virtual', 'Type'); // Wait for filter API call to complete await waitFor(() => { diff --git a/superset-frontend/src/pages/DatasetList/DatasetList.listview.test.tsx b/superset-frontend/src/pages/DatasetList/DatasetList.listview.test.tsx index 883658156c9..6ad588a8128 100644 --- a/superset-frontend/src/pages/DatasetList/DatasetList.listview.test.tsx +++ b/superset-frontend/src/pages/DatasetList/DatasetList.listview.test.tsx @@ -27,7 +27,7 @@ import userEvent from '@testing-library/user-event'; import fetchMock from 'fetch-mock'; import rison from 'rison'; import { SupersetClient } from '@superset-ui/core'; -import { selectOption } from 'spec/helpers/testing-library'; +import { selectPillOption } from 'spec/helpers/testing-library'; import { setupMocks, renderDatasetList, @@ -1510,11 +1510,8 @@ test('bulk selection clears when filter changes', async () => { API_ENDPOINTS.DATASOURCE_COMBINED, ).length; - // Wait for filter combobox to be ready before applying filter - await screen.findByRole('combobox', { name: 'Type' }); - - // Apply a filter using selectOption helper - await selectOption('Virtual', 'Type'); + // Apply a filter using selectPillOption helper (compact pill UI) + await selectPillOption('Virtual', 'Type'); // Wait for filter API call to complete await waitFor(() => { @@ -1556,16 +1553,13 @@ test('type filter API call includes correct filter parameter', async () => { expect(screen.getByTestId('listview-table')).toBeInTheDocument(); }); - // Wait for Type filter combobox - await screen.findByRole('combobox', { name: 'Type' }); - // Snapshot call count before filter const callsBeforeFilter = fetchMock.callHistory.calls( API_ENDPOINTS.DATASOURCE_COMBINED, ).length; - // Apply Type filter - await selectOption('Virtual', 'Type'); + // Apply Type filter using compact pill UI + await selectPillOption('Virtual', 'Type'); // Wait for filter API call to complete await waitFor(() => { @@ -1606,16 +1600,13 @@ test('type filter persists after duplicating a dataset', async () => { expect(screen.getByTestId('listview-table')).toBeInTheDocument(); }); - // Wait for Type filter combobox - await screen.findByRole('combobox', { name: 'Type' }); - // Snapshot call count before filter const callsBeforeFilter = fetchMock.callHistory.calls( API_ENDPOINTS.DATASOURCE_COMBINED, ).length; - // Apply Type filter - await selectOption('Virtual', 'Type'); + // Apply Type filter using compact pill UI + await selectPillOption('Virtual', 'Type'); // Wait for filter API call to complete await waitFor(() => { diff --git a/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx b/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx index 2d1c4752e9b..ee4c1257aae 100644 --- a/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx +++ b/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx @@ -200,8 +200,8 @@ test('renders Name search filter', async () => { test('renders Type filter (Virtual/Physical dropdown)', async () => { renderDatasetList(mockAdminUser); - // Filter dropdowns should be present - const filters = await screen.findAllByRole('combobox'); + // Filter pills should be present (compact pill UI) + const filters = await screen.findAllByTestId('compact-filter-pill'); expect(filters.length).toBeGreaterThan(0); }); @@ -445,7 +445,8 @@ test('selecting Database filter triggers API call with database relation filter' await waitForDatasetsPageReady(); - const filtersContainers = screen.getAllByRole('combobox'); + // Filter pills should be present (compact pill UI replaces comboboxes) + const filtersContainers = screen.getAllByTestId('compact-filter-pill'); expect(filtersContainers.length).toBeGreaterThan(0); }); diff --git a/superset-frontend/src/pages/GroupsList/GroupsList.test.tsx b/superset-frontend/src/pages/GroupsList/GroupsList.test.tsx index 785066048e8..81bd86f8078 100644 --- a/superset-frontend/src/pages/GroupsList/GroupsList.test.tsx +++ b/superset-frontend/src/pages/GroupsList/GroupsList.test.tsx @@ -121,13 +121,17 @@ describe('GroupsList', () => { test('renders the filters correctly', async () => { await renderComponent(); - const filtersSelect = screen.getAllByTestId('filters-select')[0]; - expect(within(filtersSelect).getByText(/name/i)).toBeInTheDocument(); - expect(within(filtersSelect).getByText(/label/i)).toBeInTheDocument(); - expect(within(filtersSelect).getByText(/description/i)).toBeInTheDocument(); - expect(within(filtersSelect).getByText(/roles/i)).toBeInTheDocument(); - expect(within(filtersSelect).getByText(/users/i)).toBeInTheDocument(); + // The compact filter UI renders the first search filter as an input, + // and select filters as pill buttons. Only "Name" search renders inline; + // "Label" and "Description" searches are hidden (one search box per page). + expect(screen.getByTestId('filters-search')).toBeInTheDocument(); + + // Select filters render as compact pill buttons + const pills = screen.getAllByTestId('compact-filter-pill'); + const pillLabels = pills.map(p => p.textContent ?? ''); + expect(pillLabels.some(l => /roles/i.test(l))).toBe(true); + expect(pillLabels.some(l => /users/i.test(l))).toBe(true); }); test('renders correct columns in the table', async () => { diff --git a/superset-frontend/src/pages/RolesList/RolesList.test.tsx b/superset-frontend/src/pages/RolesList/RolesList.test.tsx index 39d8069429c..6a179238d4e 100644 --- a/superset-frontend/src/pages/RolesList/RolesList.test.tsx +++ b/superset-frontend/src/pages/RolesList/RolesList.test.tsx @@ -151,8 +151,11 @@ describe('RolesList', () => { test('renders filters options', async () => { await renderAndWait(); - const typeFilter = screen.queryAllByTestId('filters-select'); - expect(typeFilter).toHaveLength(4); + // Compact filter UI: one search input for "Name" and 3 select pills + // (Users, Permissions, Groups). + expect(screen.getByTestId('filters-search')).toBeInTheDocument(); + const selectContainers = screen.getAllByTestId('select-filter-container'); + expect(selectContainers).toHaveLength(3); }); test('renders correct list columns', async () => { diff --git a/superset-frontend/src/pages/RowLevelSecurityList/RowLevelSecurityList.test.tsx b/superset-frontend/src/pages/RowLevelSecurityList/RowLevelSecurityList.test.tsx index b23b2421461..538e7ae41b1 100644 --- a/superset-frontend/src/pages/RowLevelSecurityList/RowLevelSecurityList.test.tsx +++ b/superset-frontend/src/pages/RowLevelSecurityList/RowLevelSecurityList.test.tsx @@ -166,11 +166,14 @@ describe('RuleList RTL', () => { test('renders filter options', async () => { await renderAndWait(); + // Compact filter UI: only the first search filter renders (Name), + // subsequent search filters (Group Key) are hidden — one search box per page. const searchFilters = screen.queryAllByTestId('filters-search'); - expect(searchFilters).toHaveLength(2); + expect(searchFilters).toHaveLength(1); - const typeFilter = screen.queryAllByTestId('filters-select'); - expect(typeFilter).toHaveLength(3); // Update to expect 3 select filters + // Select filters render as compact pill buttons (Filter Type, Modified by) + const selectContainers = screen.queryAllByTestId('select-filter-container'); + expect(selectContainers).toHaveLength(2); }); test('renders correct list columns', async () => { diff --git a/superset-frontend/src/pages/UsersList/UsersList.test.tsx b/superset-frontend/src/pages/UsersList/UsersList.test.tsx index e994bd485cc..63539113eed 100644 --- a/superset-frontend/src/pages/UsersList/UsersList.test.tsx +++ b/superset-frontend/src/pages/UsersList/UsersList.test.tsx @@ -138,16 +138,16 @@ describe('UsersList', () => { test('renders filters options', async () => { await renderAndWait(); - const submenu = screen.queryAllByTestId('filters-select')[0]; - expect(within(submenu).getByText(/first name/i)).toBeInTheDocument(); - expect(within(submenu).getByText(/last name/i)).toBeInTheDocument(); - expect(within(submenu).getByText(/email/i)).toBeInTheDocument(); - expect(within(submenu).getByText(/username/i)).toBeInTheDocument(); - expect(within(submenu).getByText(/roles/i)).toBeInTheDocument(); - expect(within(submenu).getByText(/is active?/i)).toBeInTheDocument(); - expect(within(submenu).getByText(/created on/i)).toBeInTheDocument(); - expect(within(submenu).getByText(/changed on/i)).toBeInTheDocument(); - expect(within(submenu).getByText(/last login/i)).toBeInTheDocument(); + // The compact filter UI shows: only the first search filter as an input, + // and select/datetime filters as pill buttons. Only "First name" search + // renders (subsequent search filters are hidden — one search box per page). + expect(screen.getByTestId('filters-search')).toBeInTheDocument(); + + // Select and datetime filters render as compact pill buttons + const pills = screen.getAllByTestId('compact-filter-pill'); + const pillLabels = pills.map(p => p.textContent ?? ''); + expect(pillLabels.some(l => /roles/i.test(l))).toBe(true); + expect(pillLabels.some(l => /is active\?/i.test(l))).toBe(true); }); test('renders correct list columns', async () => {