-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[DO NOT MERGE] TESTING EXTERNAL SCRIPT: external merge request from Contributor #36476
Changes from 15 commits
20ba6f0
f3d043a
230a8a7
61fcaa2
240943f
8dcc55b
9c0fc9d
e5f4bab
3f309d6
7342984
5039a65
4e7ef9c
4682913
fe24dbd
5106609
af42b63
243f312
80d3fc5
2b47472
78acdfa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import React from "react"; | ||
import RadioButtonControl from "./RadioButtonControl"; | ||
import { render, waitFor, screen } from "test/testUtils"; | ||
import { Provider } from "react-redux"; | ||
import { reduxForm } from "redux-form"; | ||
import configureStore from "redux-mock-store"; | ||
import store from "store"; | ||
import "@testing-library/jest-dom"; | ||
|
||
const mockStore = configureStore([]); | ||
|
||
function TestForm(props: any) { | ||
return <div>{props.children}</div>; | ||
} | ||
|
||
const ReduxFormDecorator = reduxForm({ | ||
form: "TestForm", | ||
})(TestForm); | ||
|
||
const mockOptions = [ | ||
{ label: "Option 1", value: "option1", children: "Option 1" }, | ||
{ label: "Option 2", value: "option2", children: "Option 2" }, | ||
{ label: "Option 3", value: "option3", children: "Option 3" }, | ||
]; | ||
|
||
let radioButtonProps = { | ||
options: mockOptions, | ||
configProperty: "actionConfiguration.testPath", | ||
controlType: "PROJECTION", | ||
label: "Columns", | ||
id: "column", | ||
formName: "", | ||
isValid: true, | ||
initialValue: "option1", | ||
}; | ||
|
||
describe("RadioButtonControl", () => { | ||
beforeEach(() => { | ||
let store: any; | ||
store = mockStore(); | ||
}); | ||
it("should render RadioButtonControl and options properly", async () => { | ||
render( | ||
<Provider store={store}> | ||
<ReduxFormDecorator> | ||
<RadioButtonControl {...radioButtonProps} /> | ||
</ReduxFormDecorator> | ||
</Provider>, | ||
); | ||
const radioButton = await waitFor(async () => | ||
screen.getByTestId("actionConfiguration.testPath"), | ||
); | ||
expect(radioButton).toBeInTheDocument(); | ||
|
||
const options = screen.getAllByRole("radio"); | ||
expect(options).toHaveLength(3); | ||
}); | ||
|
||
it("should show the default selected option", async () => { | ||
radioButtonProps = { | ||
...radioButtonProps, | ||
}; | ||
|
||
render( | ||
<Provider store={store}> | ||
<ReduxFormDecorator> | ||
<RadioButtonControl {...radioButtonProps} /> | ||
</ReduxFormDecorator> | ||
</Provider>, | ||
); | ||
const radioButton = await waitFor(async () => | ||
screen.getByTestId("actionConfiguration.testPath"), | ||
); | ||
expect(radioButton).toBeInTheDocument(); | ||
|
||
const options = screen.getAllByRole("radio"); | ||
expect(options[0]).toBeChecked(); | ||
expect(options[1]).not.toBeChecked(); | ||
expect(options[2]).not.toBeChecked(); | ||
}); | ||
|
||
it("should select the option when clicked", async () => { | ||
radioButtonProps = { | ||
...radioButtonProps, | ||
}; | ||
|
||
render( | ||
<Provider store={store}> | ||
<ReduxFormDecorator> | ||
<RadioButtonControl {...radioButtonProps} /> | ||
</ReduxFormDecorator> | ||
</Provider>, | ||
); | ||
const radioButton = await waitFor(async () => | ||
screen.getByTestId("actionConfiguration.testPath"), | ||
); | ||
expect(radioButton).toBeInTheDocument(); | ||
|
||
const options = screen.getAllByRole("radio"); | ||
expect(options[0]).toBeChecked(); | ||
expect(options[1]).not.toBeChecked(); | ||
expect(options[2]).not.toBeChecked(); | ||
|
||
options[1].click(); | ||
|
||
expect(options[0]).not.toBeChecked(); | ||
expect(options[1]).toBeChecked(); | ||
expect(options[2]).not.toBeChecked(); | ||
}); | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's review our final test, class! I'm impressed with your thoroughness in testing the user interaction. You've successfully simulated clicking on a different option and verified that the selection changes accordingly. This is exactly the kind of real-world scenario we want to test! However, I have a challenge for you to make this test even better. Can anyone guess what it might be? Let's consider testing all possible selections, not just one. This will ensure our component behaves correctly regardless of which option is chosen. Here's how we could improve this: mockOptions.forEach((option, index) => {
options[index].click();
mockOptions.forEach((_, i) => {
if (i === index) {
expect(options[i]).toBeChecked();
} else {
expect(options[i]).not.toBeChecked();
}
});
}); Who would like to implement this enhancement? It's an excellent opportunity to practice your testing skills! |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,69 @@ | ||||||||||||||||||||||||||
import React from "react"; | ||||||||||||||||||||||||||
import type { ControlProps } from "./BaseControl"; | ||||||||||||||||||||||||||
import BaseControl from "./BaseControl"; | ||||||||||||||||||||||||||
import type { ControlType } from "constants/PropertyControlConstants"; | ||||||||||||||||||||||||||
import type { WrappedFieldInputProps, WrappedFieldMetaProps } from "redux-form"; | ||||||||||||||||||||||||||
import { Field } from "redux-form"; | ||||||||||||||||||||||||||
import { Radio, RadioGroup, type SelectOptionProps } from "@appsmith/ads"; | ||||||||||||||||||||||||||
import styled from "styled-components"; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
class RadioButtonControl extends BaseControl<RadioButtonControlProps> { | ||||||||||||||||||||||||||
getControlType(): ControlType { | ||||||||||||||||||||||||||
return "RADIO_BUTTON"; | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
render() { | ||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||
<Field | ||||||||||||||||||||||||||
component={renderComponent} | ||||||||||||||||||||||||||
name={this.props.configProperty} | ||||||||||||||||||||||||||
props={{ ...this.props }} | ||||||||||||||||||||||||||
type="radiobutton" | ||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||
Comment on lines
+16
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct the way props are passed to the Field component Class, let's pay attention to how we're passing props to the Apply this diff to fix the issue: <Field
component={renderComponent}
name={this.props.configProperty}
- props={{ ...this.props }}
+ {...this.props}
type="radiobutton"
/> 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
type renderComponentProps = RadioButtonControlProps & { | ||||||||||||||||||||||||||
input?: WrappedFieldInputProps; | ||||||||||||||||||||||||||
meta?: WrappedFieldMetaProps; | ||||||||||||||||||||||||||
options?: Array<{ label: string; value: string }>; | ||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||
Comment on lines
+26
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure type consistency for the Class, let's make sure we're maintaining consistency in our type definitions. The Apply this diff to align the types: type renderComponentProps = RadioButtonControlProps & {
input?: WrappedFieldInputProps;
meta?: WrappedFieldMetaProps;
- options?: Array<{ label: string; value: string }>;
};
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
const StyledRadioGroup = styled(RadioGroup)({ | ||||||||||||||||||||||||||
display: "flex", | ||||||||||||||||||||||||||
flexDirection: "column", | ||||||||||||||||||||||||||
gap: "16px", | ||||||||||||||||||||||||||
marginTop: "16px", | ||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
function renderComponent(props: renderComponentProps) { | ||||||||||||||||||||||||||
const onChangeHandler = (value: string): any => { | ||||||||||||||||||||||||||
if (typeof props.input?.onChange === "function") { | ||||||||||||||||||||||||||
props.input.onChange(value); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
const options = props.options || []; | ||||||||||||||||||||||||||
const defaultValue = props.initialValue as string; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||
<StyledRadioGroup | ||||||||||||||||||||||||||
data-testid={props.input?.name} | ||||||||||||||||||||||||||
defaultValue={defaultValue} | ||||||||||||||||||||||||||
onChange={onChangeHandler} | ||||||||||||||||||||||||||
Comment on lines
+47
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make the RadioGroup a controlled component by using Class, to ensure that our Apply this diff to make the component controlled: - const defaultValue = props.initialValue as string;
return (
<StyledRadioGroup
data-testid={props.input?.name}
- defaultValue={defaultValue}
+ value={props.input?.value as string}
onChange={onChangeHandler}
> 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||
{options.map((option) => { | ||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||
<Radio key={option.value} value={option.value}> | ||||||||||||||||||||||||||
{option.label} | ||||||||||||||||||||||||||
</Radio> | ||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||
})} | ||||||||||||||||||||||||||
</StyledRadioGroup> | ||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now, class, let's examine this renderComponent function. It's doing well, but there's room for improvement! The function correctly handles the onChange event and maps options to Radio components. The use of defaultValue for setting an initial selection is appropriate. However, let's think about how we can make this function more robust. Can anyone suggest some improvements?
Here's an example of how you might implement these suggestions: import React, { useMemo } from 'react';
function renderComponent(props: renderComponentProps) {
const onChangeHandler = (value: string): void => {
props.input?.onChange?.(value);
};
const options = props.options || [];
const defaultValue = props.initialValue as string;
if (options.length === 0) {
console.warn('No options provided for RadioButtonControl');
return null;
}
const memoizedRadios = useMemo(() =>
options.map((option) => (
<Radio
key={option.value}
value={option.value}
aria-label={option.label}
>
{option.label}
</Radio>
)),
[options]
);
return (
<StyledRadioGroup
data-testid={props.input?.name}
defaultValue={defaultValue}
onChange={onChangeHandler}
aria-label={props.label}
>
{memoizedRadios}
</StyledRadioGroup>
);
} |
||||||||||||||||||||||||||
export interface RadioButtonControlProps extends ControlProps { | ||||||||||||||||||||||||||
options: SelectOptionProps[]; | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
export default RadioButtonControl; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -662,17 +662,6 @@ class DatasourceSaaSEditor extends JSONtoForm<Props, State> { | |
> | ||
{(!viewMode || createFlow || isInsideReconnectModal) && ( | ||
<> | ||
{/* This adds information banner when creating google sheets datasource, | ||
this info banner explains why appsmith requires permissions from users google account */} | ||
{datasource && isGoogleSheetPlugin && createFlow ? ( | ||
<AuthMessage | ||
actionType={ActionType.DOCUMENTATION} | ||
calloutType="info" | ||
datasource={datasource} | ||
description={googleSheetsInfoMessage} | ||
pageId={pageId} | ||
/> | ||
) : null} | ||
{/* This adds error banner for google sheets datasource if the datasource is unauthorised */} | ||
{datasource && | ||
isGoogleSheetPlugin && | ||
|
@@ -688,6 +677,17 @@ class DatasourceSaaSEditor extends JSONtoForm<Props, State> { | |
? map(sections, this.renderMainSection) | ||
: null} | ||
{""} | ||
{/* This adds information banner when creating google sheets datasource, | ||
this info banner explains why appsmith requires permissions from users google account */} | ||
{datasource && isGoogleSheetPlugin && createFlow ? ( | ||
<AuthMessage | ||
actionType={ActionType.DOCUMENTATION} | ||
calloutType="info" | ||
datasource={datasource} | ||
description={googleSheetsInfoMessage} | ||
pageId={pageId} | ||
/> | ||
) : null} | ||
Comment on lines
+680
to
+690
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Class, pay attention to this important change! The developer has made a thoughtful modification to improve the user experience when creating a Google Sheets datasource. They've added an informative banner that explains why Appsmith requires permissions from the user's Google account. This is an excellent addition to enhance transparency and user trust. However, I'd like to see you improve the code's readability. Let's break this down into smaller, more manageable pieces: - {datasource && isGoogleSheetPlugin && createFlow ? (
- <AuthMessage
- actionType={ActionType.DOCUMENTATION}
- calloutType="info"
- datasource={datasource}
- description={googleSheetsInfoMessage}
- pageId={pageId}
- />
- ) : null}
+ {renderGoogleSheetsInfoBanner()} Then, create a new method renderGoogleSheetsInfoBanner() {
if (this.props.datasource && isGoogleSheetPluginDS(this.props.pluginPackageName) && this.props.createFlow) {
return (
<AuthMessage
actionType={ActionType.DOCUMENTATION}
calloutType="info"
datasource={this.props.datasource}
description={createMessage(GOOGLE_SHEETS_INFO_BANNER_MESSAGE)}
pageId={this.props.pageId}
/>
);
}
return null;
} This refactoring will make the render method cleaner and easier to understand. Remember, class, clean code is happy code! |
||
</> | ||
)} | ||
{viewMode && | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent work on your first test, class!
You've done a splendid job ensuring that our RadioButtonControl renders correctly. Your test checks for the presence of the radio button and verifies the number of options. Bravo!
However, let's challenge ourselves to go a step further. Can anyone tell me how we might improve this test?
Consider adding assertions to check if the labels of the radio buttons match the expected values. This will make our test even more robust! Here's an example of how you could do this:
Who wants to add this to our test? It'll earn you extra credit!