codeant-ai-for-open-source[bot] commented on code in PR #38792:
URL: https://github.com/apache/superset/pull/38792#discussion_r3004443283
##########
superset-frontend/src/explore/components/controls/SelectControl.test.tsx:
##########
@@ -16,459 +16,185 @@
* specific language governing permissions and limitations
* under the License.
*/
+import { waitFor } from 'spec/helpers/testing-library';
import {
- act,
createEvent,
fireEvent,
render,
screen,
userEvent,
- within,
} from 'spec/helpers/testing-library';
+
import SelectControl, {
innerGetOptions,
areAllValuesNumbers,
getSortComparator,
} from 'src/explore/components/controls/SelectControl';
-const defaultProps: {
- choices: [string | number, string][];
- name: string;
- label: string;
- valueKey: string;
- onChange: jest.Mock;
-} = {
- choices: [
- ['1 year ago', '1 year ago'],
- ['1 week ago', '1 week ago'],
- ['today', 'today'],
- ],
+const choices: [string, string][] = [
+ ['1 year ago', '1 year ago'],
+ ['1 week ago', '1 week ago'],
+ ['today', 'today'],
+];
+
+const defaultProps = {
+ choices,
name: 'row_limit',
label: 'Row Limit',
valueKey: 'value',
onChange: jest.fn(),
};
-beforeEach(() => {
- jest.useFakeTimers();
-});
-
-afterEach(() => {
- jest.useRealTimers();
-});
-
-const options = [
+const expectedOptions = [
{ value: '1 year ago', label: '1 year ago' },
{ value: '1 week ago', label: '1 week ago' },
{ value: 'today', label: 'today' },
];
const renderSelectControl = (props = {}) => {
- const overrideProps = {
- ...defaultProps,
- ...props,
- };
- const { container } = render(<SelectControl {...overrideProps} />);
- return container;
+ const overrideProps = { ...defaultProps, ...props };
+ return render(<SelectControl {...overrideProps} />);
};
-// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from
describe blocks
describe('SelectControl', () => {
- test('calls props.onChange when select', async () => {
+ test('calls props.onChange when select', () => {
renderSelectControl();
defaultProps.onChange(50);
expect(defaultProps.onChange).toHaveBeenCalledWith(50);
Review Comment:
**Suggestion:** The "calls props.onChange when select" test does not
exercise the SelectControl at all; it directly invokes the Jest mock `onChange`
and then asserts that it was called, so the test will still pass even if the
component stops calling `onChange` when the user selects an option. This hides
regressions in the actual wiring between the Select and the `onChange` prop.
Instead, the test should simulate a real user selection via `userEvent` and
then assert that the injected `onChange` mock was called with the expected
value. [logic error]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ⚠️ onChange wiring regressions not caught by this test.
- ⚠️ Chart control dropdown changes may silently stop updating state.
```
</details>
```suggestion
test('calls props.onChange when select', async () => {
const onChange = jest.fn();
renderSelectControl({ onChange });
const input = screen.getByRole('combobox', { name: /row limit/i });
await userEvent.click(input);
const option = await screen.findByText('1 year ago');
await userEvent.click(option);
expect(onChange).toHaveBeenCalledWith('1 year ago');
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Open
`superset-frontend/src/explore/components/controls/SelectControl.tsx` and locate
the `onChange` implementation at lines 195–219, where it ultimately calls
`this.props.onChange?.(onChangeVal, []);`.
2. Introduce a regression by commenting out or removing the
`this.props.onChange?.(onChangeVal, []);` call so that `SelectControl` no
longer
propagates changes to its `onChange` prop.
3. Run the unit tests for this file (for example, `npm test --
SelectControl.test.tsx`) so
that
`superset-frontend/src/explore/components/controls/SelectControl.test.tsx`
executes
the test `"calls props.onChange when select"` at lines 60–64.
4. Observe that the test still passes because it directly calls
`defaultProps.onChange(50);` instead of simulating a user selection through
the rendered
`<SelectControl />`, so the broken wiring between the Select component and
`onChange` is
not detected, even though real usages of `SelectControl` like the chart
controls
configured with `type: 'SelectControl'` in
`plugins/legacy-plugin-chart-rose/src/controlPanel.tsx:65-81` would now fail
to receive
change notifications.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset-frontend/src/explore/components/controls/SelectControl.test.tsx
**Line:** 60:63
**Comment:**
*Logic Error: The "calls props.onChange when select" test does not
exercise the SelectControl at all; it directly invokes the Jest mock `onChange`
and then asserts that it was called, so the test will still pass even if the
component stops calling `onChange` when the user selects an option. This hides
regressions in the actual wiring between the Select and the `onChange` prop.
Instead, the test should simulate a real user selection via `userEvent` and
then assert that the injected `onChange` mock was called with the expected
value.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38792&comment_hash=1010023365e2866de1377e7628c50f8bbd1746325e83585b238320ad442dbedf&reaction=like'>👍</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38792&comment_hash=1010023365e2866de1377e7628c50f8bbd1746325e83585b238320ad442dbedf&reaction=dislike'>👎</a>
##########
superset-frontend/src/explore/components/controls/SelectControl.test.tsx:
##########
@@ -16,459 +16,185 @@
* specific language governing permissions and limitations
* under the License.
*/
+import { waitFor } from 'spec/helpers/testing-library';
import {
- act,
createEvent,
fireEvent,
render,
screen,
userEvent,
- within,
} from 'spec/helpers/testing-library';
+
import SelectControl, {
innerGetOptions,
areAllValuesNumbers,
getSortComparator,
} from 'src/explore/components/controls/SelectControl';
-const defaultProps: {
- choices: [string | number, string][];
- name: string;
- label: string;
- valueKey: string;
- onChange: jest.Mock;
-} = {
- choices: [
- ['1 year ago', '1 year ago'],
- ['1 week ago', '1 week ago'],
- ['today', 'today'],
- ],
+const choices: [string, string][] = [
+ ['1 year ago', '1 year ago'],
+ ['1 week ago', '1 week ago'],
+ ['today', 'today'],
+];
+
+const defaultProps = {
+ choices,
name: 'row_limit',
label: 'Row Limit',
valueKey: 'value',
onChange: jest.fn(),
};
-beforeEach(() => {
- jest.useFakeTimers();
-});
-
-afterEach(() => {
- jest.useRealTimers();
-});
-
-const options = [
+const expectedOptions = [
{ value: '1 year ago', label: '1 year ago' },
{ value: '1 week ago', label: '1 week ago' },
{ value: 'today', label: 'today' },
];
const renderSelectControl = (props = {}) => {
- const overrideProps = {
- ...defaultProps,
- ...props,
- };
- const { container } = render(<SelectControl {...overrideProps} />);
- return container;
+ const overrideProps = { ...defaultProps, ...props };
+ return render(<SelectControl {...overrideProps} />);
};
-// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from
describe blocks
describe('SelectControl', () => {
- test('calls props.onChange when select', async () => {
+ test('calls props.onChange when select', () => {
renderSelectControl();
defaultProps.onChange(50);
expect(defaultProps.onChange).toHaveBeenCalledWith(50);
});
- // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from
describe blocks
describe('render', () => {
- test('renders with Select by default', () => {
+ test('renders with Select by default', async () => {
renderSelectControl();
- const selectorWrapper = screen.getByLabelText('Row Limit', {
+
+ const selectorWrapper = await screen.findByLabelText('Row Limit', {
selector: 'div',
});
- const selectorInput = within(selectorWrapper).getByLabelText(
- 'Row Limit',
- { selector: 'input' },
- );
+
+ const selectorInput = await screen.findByLabelText('Row Limit', {
+ selector: 'input',
+ });
+
expect(selectorWrapper).toBeInTheDocument();
expect(selectorInput).toBeInTheDocument();
});
- test('renders as mode multiple', () => {
+ test('renders as mode multiple', async () => {
renderSelectControl({ multi: true });
- const selectorWrapper = screen.getByLabelText('Row Limit', {
- selector: 'div',
- });
- const selectorInput = within(selectorWrapper).getByLabelText(
- 'Row Limit',
- { selector: 'input' },
- );
- expect(selectorWrapper).toBeInTheDocument();
- expect(selectorInput).toBeInTheDocument();
- userEvent.click(selectorInput);
- expect(screen.getByText('Select all (3)')).toBeInTheDocument();
+ const input = screen.getByRole('combobox', { name: /row limit/i });
+ await userEvent.click(input);
+ expect(await screen.findByText('Select all (3)')).toBeInTheDocument();
});
- test('renders with allowNewOptions when freeForm', () => {
+ test('renders with allowNewOptions when freeForm', async () => {
renderSelectControl({ freeForm: true });
- const selectorWrapper = screen.getByLabelText('Row Limit', {
- selector: 'div',
+ const input = screen.getByRole('combobox', { name: /row limit/i });
+ await userEvent.click(input);
+ await userEvent.type(input, 'a new option');
+ await waitFor(() => {
+ expect(screen.getByText('a new option')).toBeInTheDocument();
});
- const selectorInput = within(selectorWrapper).getByLabelText(
- 'Row Limit',
- { selector: 'input' },
- );
- expect(selectorWrapper).toBeInTheDocument();
- expect(selectorInput).toBeInTheDocument();
-
- // Expect a new option to be selectable.
- userEvent.click(selectorInput);
- userEvent.type(selectorInput, 'a new option');
- act(() => jest.runAllTimers());
- expect(within(selectorWrapper).getByRole('option')).toHaveTextContent(
- 'a new option',
- );
});
- test('renders with allowNewOptions=false when freeForm=false', () => {
- const container = renderSelectControl({ freeForm: false });
- const selectorWrapper = screen.getByLabelText('Row Limit', {
- selector: 'div',
- });
- const selectorInput = within(selectorWrapper).getByLabelText(
- 'Row Limit',
- { selector: 'input' },
- );
- expect(selectorWrapper).toBeInTheDocument();
- expect(selectorInput).toBeInTheDocument();
+ test('renders with allowNewOptions=false when freeForm=false', async () =>
{
+ renderSelectControl({ freeForm: false });
- // Expect no new option to be selectable.
- userEvent.click(selectorInput);
- userEvent.type(selectorInput, 'a new option');
- act(() => jest.advanceTimersByTime(300));
+ const input = screen.getByRole('combobox', { name: /row limit/i });
- expect(
- container.querySelector('[role="option"]'),
- ).not.toBeInTheDocument();
- expect(
- within(selectorWrapper).getByText('No data', { selector: 'div' }),
- ).toBeInTheDocument();
+ await userEvent.click(input);
+ await userEvent.type(input, 'a new option');
+
+ expect(screen.queryByText('a new option')).not.toBeInTheDocument();
+
+ await waitFor(() => {
+ expect(
+ screen.getByText(/no data/i, {
+ selector: '.ant-empty-description',
+ }),
+ ).toBeInTheDocument();
+ });
});
- test('renders with tokenSeparators', () => {
+ test('renders with tokenSeparators', async () => {
renderSelectControl({ tokenSeparators: ['\n', '\t', ';'], multi: true });
- const selectorWrapper = screen.getByLabelText('Row Limit', {
- selector: 'div',
- });
- const selectorInput = within(selectorWrapper).getByLabelText(
- 'Row Limit',
- { selector: 'input' },
- );
- expect(selectorWrapper).toBeInTheDocument();
- expect(selectorInput).toBeInTheDocument();
+ const input = screen.getByRole('combobox', { name: /row limit/i });
+ await userEvent.click(input);
- userEvent.click(selectorInput);
- const paste = createEvent.paste(selectorInput, {
+ const paste = createEvent.paste(input, {
clipboardData: {
getData: () => '1 year ago;1 week ago',
},
});
- fireEvent(selectorInput, paste);
- const yearOption = screen.getByRole('option', { name: /1 year ago/i });
- expect(yearOption).toBeInTheDocument();
- expect(yearOption).toHaveAttribute('aria-selected', 'true');
- const weekOption = screen.getByRole('option', { name: /1 week ago/ });
- expect(weekOption?.getAttribute('aria-selected')).toEqual('true');
+ fireEvent(input, paste);
+
+ // FIX: Use selector to target the selection items, avoiding Space/Tag
duplicates
+ expect(
+ await screen.findByText('1 year ago', {
+ selector: '.ant-select-selection-item-content',
+ }),
+ ).toBeInTheDocument();
+ expect(
+ await screen.findByText('1 week ago', {
+ selector: '.ant-select-selection-item-content',
+ }),
+ ).toBeInTheDocument();
});
- // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from
describe blocks
describe('empty placeholder', () => {
- // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from
describe blocks
- describe('withMulti', () => {
- test('does not show a placeholder if there are no choices', () => {
- renderSelectControl({
- choices: [],
- multi: true,
- placeholder: 'add something',
- });
- expect(screen.queryByRole('option')).not.toBeInTheDocument();
- });
- });
- // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from
describe blocks
- describe('withSingleChoice', () => {
- test('does not show a placeholder if there are no choices', async ()
=> {
- const container = renderSelectControl({
- choices: [],
- multi: false,
- placeholder: 'add something',
- });
- expect(
- container.querySelector('[role="option"]'),
- ).not.toBeInTheDocument();
- });
- });
- // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from
describe blocks
- describe('all choices selected', () => {
- test('does not show a placeholder', () => {
- const container = renderSelectControl({
- multi: true,
- value: ['today', '1 year ago'],
- });
- expect(
- container.querySelector('[role="option"]'),
- ).not.toBeInTheDocument();
- expect(screen.queryByText('Select ...')).not.toBeInTheDocument();
- });
+ test('multi: no choices -> no options', async () => {
+ renderSelectControl({ choices: [], multi: true });
+ expect(screen.queryByRole('option')).not.toBeInTheDocument();
});
- });
- // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from
describe blocks
- describe('when select is multi', () => {
- test('does not render the placeholder when a selection has been made',
() => {
- renderSelectControl({
- multi: true,
- value: ['today'],
- placeholder: 'add something',
- });
- expect(screen.queryByText('add something')).not.toBeInTheDocument();
+
+ test('single: no choices -> no options', async () => {
+ renderSelectControl({ choices: [], multi: false });
+ expect(screen.queryByRole('option')).not.toBeInTheDocument();
});
- });
- // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from
describe blocks
- describe('when select is single', () => {
- test('does not render the placeholder when a selection has been made',
() => {
+
+ test('all selected -> no placeholder', async () => {
renderSelectControl({
multi: true,
- value: 50,
- placeholder: 'add something',
+ value: ['today', '1 year ago'],
});
Review Comment:
**Suggestion:** The "empty placeholder" tests assert that no `role="option"`
elements exist without ever opening the Select dropdown, so they trivially pass
regardless of how the component behaves once opened. This means they do not
actually verify that there are no options rendered in the listbox for the "no
choices" and "all selected" cases. To correctly validate the behavior, each
test should first open the combobox via `userEvent.click` and then check for
the absence of options in the rendered listbox. [logic error]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ⚠️ Empty-state dropdown regressions not validated by current tests.
- ⚠️ Multi-select chart controls may show incorrect placeholder options.
```
</details>
```suggestion
const input = screen.getByRole('combobox', { name: /row limit/i });
await userEvent.click(input);
expect(screen.queryByRole('option')).not.toBeInTheDocument();
});
test('single: no choices -> no options', async () => {
renderSelectControl({ choices: [], multi: false });
const input = screen.getByRole('combobox', { name: /row limit/i });
await userEvent.click(input);
expect(screen.queryByRole('option')).not.toBeInTheDocument();
});
test('all selected -> no placeholder', async () => {
renderSelectControl({
multi: true,
value: ['today', '1 year ago'],
});
const input = screen.getByRole('combobox', { name: /row limit/i });
await userEvent.click(input);
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. In `superset-frontend/src/explore/components/controls/SelectControl.tsx`,
or in the
underlying Select implementation from `@superset-ui/core/components`,
introduce a bug that
causes at least one option element to be rendered in the dropdown even when
`choices` is
empty or all options are already selected (for example, by always rendering
a dummy
option).
2. Run the unit tests for
`superset-frontend/src/explore/components/controls/SelectControl.test.tsx`
so that the
`"empty placeholder"` suite at lines 143–164 executes.
3. Observe that each test (`'multi: no choices -> no options'`, `'single: no
choices -> no
options'`, and `'all selected -> no placeholder'`) only calls
`renderSelectControl(...)`
and immediately asserts `screen.queryByRole('option')` on the closed
component, never
clicking the combobox to open the listbox, so they continue to pass even
though the
dropdown would show options when opened.
4. In real chart configuration UIs where `SelectControl` is used for
multi-select and
time-shift controls, such as the `time_compare` control with `multi: true`
and `choices`
configured in
`plugins/legacy-plugin-chart-rose/src/controlPanel.tsx:189-195`, users would
see incorrect non-empty dropdowns for empty/all-selected states, but these
regressions
would not be caught by the existing tests.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset-frontend/src/explore/components/controls/SelectControl.test.tsx
**Line:** 146:158
**Comment:**
*Logic Error: The "empty placeholder" tests assert that no
`role="option"` elements exist without ever opening the Select dropdown, so
they trivially pass regardless of how the component behaves once opened. This
means they do not actually verify that there are no options rendered in the
listbox for the "no choices" and "all selected" cases. To correctly validate
the behavior, each test should first open the combobox via `userEvent.click`
and then check for the absence of options in the rendered listbox.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38792&comment_hash=59fd2e56549ee5928b3eadaf2d16b5b647a8d6148781c1eda7f4909d2ed980da&reaction=like'>👍</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38792&comment_hash=59fd2e56549ee5928b3eadaf2d16b5b647a8d6148781c1eda7f4909d2ed980da&reaction=dislike'>👎</a>
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]