I am trying out Reason-React. I am facing a problem when I try to add a key to one of the components.
I have a TodoApp that takes a list of TodoItem as state. The app works fine when I don't have a key for the TodoItem. When I add it, however I am getting a compilation error. I am adding the files here for reference:
TodoItem.re:
type item = {
id: int,
title: string,
completed: bool
};
let lastId = ref(0);
let newItem = () => {
lastId := lastId^ + 1;
{id: lastId^, title: "Click a button", completed: false}
};
let toString = ReasonReact.stringToElement;
let component = ReasonReact.statelessComponent("TodoItem");
let make = (~item, children) => {
...component,
render: (self) =>
<div className="item">
<input _type="checkbox" checked=(Js.Boolean.to_js_boolean(item.completed)) />
(toString(item.title))
</div>
};
TodoApp.re:
let toString = ReasonReact.stringToElement;
type state = {items: list(TodoItem.item)};
type action =
| AddItem;
let component = ReasonReact.reducerComponent("TodoApp");
let currentItems = [TodoItem.{id: 0, title: "ToDo", completed: false}];
let make = (children) => {
...component,
initialState: () => {items: currentItems},
reducer: (action, {items}) =>
switch action {
| AddItem => ReasonReact.Update({items: [TodoItem.newItem(), ...items]})
},
render: ({state: {items}, reduce}) => {
let numOfItems = List.length(items);
<div className="app">
<div className="title">
(toString("What to do"))
<button onClick=(reduce((_evt) => AddItem))> (toString("Add Something")) </button>
</div>
<div className="items">
(
ReasonReact.arrayToElement(
Array.of_list(
(List.map((item) => <TodoItem key=(string_of_int(item.id)) item />, items))
/*List.map((item) => <TodoItem item />, items) This works but the above line of code with the key does not*/
)
)
)
</div>
<div className="footer"> (toString(string_of_int(numOfItems) ++ " items")) </div>
</div>
}
};
I've added a comment near the line where the error occurs.
The error reads as Unbound record field id, but I am not able to figure out how it is not bound. What am I missing here?
Type inference is unfortunately a bit limited when it comes to inferring the type of a record from another module based on the usage of record fields, so you need to give it some help. Two options that should work are:
Annotating the type of ìtem:
List.map((item: TodoItem.item) => <TodoItem key=(string_of_int(item.id)) item />)
or locally opening the module where the record field is used:
List.map((item) => <TodoItem key=(string_of_int(item.TodoItem.id)) item />)
Related
I'm trying to test my component that has the following conditional rendering:
const MyComponent = () => {
const [isVisible, setIsVisible] = (false);
if(selectedOption == 'optionOne')
setIsVisible(true);
else
setIsVisible(false);
return (
<div>
<Select data-testid="select1" selectedOption={selectedOption} />
{isVisible ? <Select data-testid="select2" selectedOption={anotherSelectedOption} /> : null }
</div>
)}
If selectedOption in select1 is 'optionOne', then select2 shows up.
Here is how I am testing it:
describe('Testing', () => {
let container: ElementWrapper<HTMLElement>;
const testState = {
userChoice1: {
selectedOption: ['optionOne'],
},
userChoice2: {
selectedOption: ['test1', 'test2'],
},
} as AppState;
beforeEach(() => {
container = render(<MyComponent/>, testState);
});
it('should show select2 if optionOne is selected', async () => {
const { getByTestId, getAllByTestId } = render(<MyComponent/>);
expect(container.find('span').getElement().textContent).toBe("optionOne"); // this successfully finds select1 with optionOne selected, all good
await screen.findAllByTestId('select2')
expect(screen.getAllByTestId('select2')).toBeInTheDocument();
});
In the testing above, as select1 has optionOne selected, I expect to select2 to show up. However, I am getting an error Unable to find an element by: [data-testid="select2"]. It also returns the whole HTML body, where I see select1 element with optionOne selected, but no select2 at all as it seems to still be hidden.
What am I missing here? How can I unhide select2 within the unit test?
Trying to figure working with data in reason. I have this graphql query returning data a logging it. Question is how do I access the data in the following component.
let component = ReasonReact.statelessComponent("Home");
let make = (_) => {
...component,
render: (_self) =>
<View>
<Hello message="Hello from home component" />
<FetchEpisodes>
(
(response) => {
Js.log(response);
/* let episodeItems =
Array.of_list(
List.map(
(response) =>
<Episode
key=response##data##allEpisodes##id
episode=response##data##allEpisodes##episode
/>,
)
); */
<div> <h1> (ReasonReact.stringToElement("Episodes!")) </h1> </div>
/* (ReasonReact.arrayToElement(episodeItems)) */
}
)
</FetchEpisodes>
</View>
};
This is the query response:
coming from JS i keep wanting to log allEpisodes with some like response.data...which doesn't work here, obviously
Gists to components: episode component, home.re component
If i uncomment and run, it produces the following error:
```
FAILED: src/pages/home.mlast
/usr/local/lib/node_modules/bs-platform/bin/bsc.exe -pp "/usr/local/lib/node_modules/bs-platform/bin/refmt3.exe --print binary" -ppx '/usr/local/lib/node_modules/bs-platform/bin/reactjs_jsx_ppx_2.exe' -w -30-40+6+7+27+32..39+44+45+101 -nostdlib -I '/Users/shingdev/code/REASON/with-reason-apollo-master/node_modules/bs-platform/lib/ocaml' -bs-super-errors -no-alias-deps -color always -c -o src/pages/home.mlast -bs-syntax-only -bs-binary-ast -impl /Users/shingdev/code/REASON/with-reason-apollo-master/src/pages/home.re
File "/Users/shingdev/code/REASON/with-reason-apollo-master/src/pages/home.re", line 53, characters 17-18:
Error: 2806: <UNKNOWN SYNTAX ERROR>
We've found a bug for you!
/Users/shingdev/code/REASON/with-reason-apollo-master/src/pages/home.re
There's been an error running Reason's refmt parser on a file.
This was the command:
/usr/local/lib/node_modules/bs-platform/bin/refmt3.exe --print binary '/Users/shingdev/code/REASON/with-reason-apollo-master/src/pages/home.re' > /var/folders/qx/xhwh5zfj7z36_bjh5187svx00000gn/T/ocamlpp530e68
```
I'm not understanding how to process the response object when an array is returned. Thank you.
UPDATE per #glennsl's suggestion:
```
let make = (_) => {
...component,
render: (_self) =>
<View>
<Hello message="Hello from home component" />
/* <Greeting name="Tony" /> */
<FetchEpisodes>
(
(response) => {
let episodeItems =
response##data##allEpisodes
|> Array.map((episode) => <Episode key=episode##id title=episode##title />);
<div> <h1> (ReasonReact.stringToElement("Episodes!")) </h1> </div>(
ReasonReact.arrayToElement(episodeItems)
)
}
)
</FetchEpisodes>
</View>
};
```
This produces the following error:
I'm figuring its coming because the types arent being passed to episode.re
```
let component = ReasonReact.statelessComponent("Episode");
let make = (~style=?, ~episode, _children) => {
...component,
render: (_self) => <View ?style> <h1> (ReasonReact.stringToElement(episode)) </h1> </View>
};
```
Am I supposed to pass list(episode) somewhere?
UPDATE 2: This code works as far as JSX thanks to #glennsl
```
let make = (_) => {
...component,
render: (_self) =>
<View>
<Hello message="Hello from home component" />
/* <Greeting name="Tony" /> */
<FetchEpisodes>
(
(response) => {
let episodeItems =
response##data##allEpisodes
|> Array.map((episode) => <Episode key=episode##id episode=episode##episode />);
<div>
<h1> (ReasonReact.stringToElement("Episodes!")) </h1>
(ReasonReact.arrayToElement(episodeItems))
</div>
}
)
</FetchEpisodes>
</View>
};
```
This should work, I think:
type episode = {. "id": string, "title": string, "episode": string};
type data = {. "allEpisodes": array(episode)};
...
(response) => {
let episodeItems =
response##data##allEpisodes
|> Array.map((episode) =>
<Episode key=episode##id
episode=episode##episode />);
<div>
<h1> (ReasonReact.stringToElement("Episodes!")) </h1>
(ReasonReact.arrayToElement(episodeItems))
</div>
}
Let me know if it doesn't, or if any of this confuses you I'll happily explain.
Edit: You've also typed data##allEpisodes as list(episode) when it's actually array(episode). Updated the code block above. A list is not the same as an array, the former is a linked list type while the latter is equivalent to a JavaScript array.
Edit 2: Fixed JSX
response##data##allEpisodes
|> Array.map((episode) =>
This won't work because response##data##allEpisodes returns undefined. I believe it's because in [reason-apollo]: https://github.com/Gregoirevda/reason-apollo/blob/master/src/ReasonApollo.re#L29 data is typed as a string.
This is very simple, I have a Multiselect and when one item is selected, I want the tag to represent the DataTextField. When multiple items are selected, I want one tag to represent the quantity of items selected. Here is my code:
#(Html.Kendo().MultiSelect()
.Placeholder("Select Employees...")
.Name("empSelect")
.DataTextField("Employee")
.DataValueField("PERSONNEL_KEY")
.HtmlAttributes(new { style = "width:100%;font-size:10px;", id = "empSelect" })
.AutoBind(false)
.AutoClose(false)
.Filter(FilterType.Contains)
.TagTemplateId("tagTemplate")
.DataSource(source => {
source.Read(read =>
{
read.Action("GetEmployees", "EmployeeTS");
})
.ServerFiltering(true);}))
and here is the tagTemplate script:
<script id="tagTemplate" type="text/x-kendo-template">
# if (data.length < 2) { #
<span>
#= data.Employee #
</span>
# } else { #
<span>
#= data.length # selected
</span>
# } #
All items come back from my Controller just fine. When I select an item(s), the tag displays "UNDEFINED selected". Apparently "data.length" is undefined, yet I know of no other way to grab the count of items selected.
I am currently on the 2016.3.1118 build of Telerik Kendo MVC.
"data" doesn't have property of length. Because of that, always works "else" and shows undefined.
<script>
function onChange(e) {
var multi = $("#empSelect").data("kendoMultiSelect");
var multi = $("#empSelect").data("kendoMultiSelect");
if (multi.listView._dataItems.length > 1) {
multi.setOptions({
tagMode: 'single'
});
} else {
multi.setOptions({
tagMode: 'multiple'
});
}
multi.refresh();
}
#(Html.Kendo().MultiSelect()
.Placeholder("Select Employees...")
.Name("empSelect")
.DataTextField("TANIM")
.DataValueField("URETIM_YERI")
.AutoBind(false)
.AutoClose(false)
.Filter(FilterType.Contains)
.TagMode(TagMode.Multiple)
.Events(e =>
{
e.Change("onChange");
})
.DataSource(source =>
{
source.Read(read =>
{
read.Action("GetFactories", "Factory");
})
.ServerFiltering(true);
}))
I have a search bar that I want to test for user input. I am rendering the view with React and writing the tests using Chai, Mocha and JSDom.
Here are the simple tests I have written so far
describe('Search', () => {
it('renders a search bar', () => {
const component = renderIntoDocument(
<Search />
);
const search_bar = scryRenderedDOMComponentsWithClass(component, 'input-group');
expect(search_bar.length).to.equal(1);
});
it('invokes a callback when the Add button is clicked', () => {
let search_term;
const clickHandler = (term) => search_term = term;
const component = renderIntoDocument(
<Search onClick={clickHandler}/>
)
const add_button = scryRenderedDOMComponentsWithTag(component, 'button');
Simulate.click(add_button[0]);
expect(search_term).to.equal('');
});
it('accepts user input from the search button and performs a regex check for invalid input', () => {
let search_term;
const clickHandler = (term) => search_term = term;
const component = renderIntoDocument(
<Search onClick={clickHandler} />
);
const add_button = scryRenderedDOMComponentsWithTag(component, 'button');
const search_bar = scryRenderedDOMComponentsWithClass(component, 'input-group');
Simulate.change(search_bar[0], {target: {value: 'FB'}});
expect(search_bar[0].state.value).to.equal('FB');
})
})
1) I want to test that the user inputs characters before hitting the Add button.
2) I want to test that the user does not input any numerals or special characters into the search term, the string needs to be only alphabets.
How do I simulate these tests?
Okay, so I have an answer after some debugging. But, it seems to be a little bit of a hack.
Here is the component I am rendering into the DOM
import React, {Component} from 'react';
class Search extends Component {
constructor(props){
super(props);
this.state = {
value: ''
}
}
handleChange(event){
this.setState({value: event.target.value})
}
render() {
return (
<div className='input-group'>
<input ref='input' type='text' className='form-control' placeholder='Enter stock ticker' value={this.state.value}
onChange={this.handleChange}/>
<span className='input-group-btn'>
<button className='btn btn-primary' type='submit'
onClick={() => this.props.onClick(this.state.value)}>
Add
</button>
</span>
</div>
)
}
}
Search.propTypes = {
onClick: React.PropTypes.func.isRequired
}
export default Search;
Here is the unit test to accept user input. Does not include validation
it('accepts user input from the search button', () => {
let search_term;
const clickHandler = (term) => search_term = term;
const component = renderIntoDocument(
<Search onClick={clickHandler} />
);
component.state.value = 'FB';
expect(component.state.value).to.equal('FB');
})
The reason I am not so sure about this is because I am explicitly setting component.state.value = 'FB' and then checking if it is, which it will be. But this is the closest I have gotten to validating that the value actually changes, because trying to run
Simulate.change(component, {state: {value: 'FB'}});
throws an error
TypeError: Cannot read property
'__reactInternalInstance$gvviufwbzvqrhtud00vbo6r' of undefined
Trying the following according to the React docs (https://facebook.github.io/react/docs/test-utils.html#simulate)
it('accepts user input from the search button', () => {
let search_term;
const clickHandler = (term) => search_term = term;
const component = renderIntoDocument(
<Search onClick={clickHandler} />
);
const input = component.refs.input;
input.value = 'FB';
Simulate.change(input);
expect(input.value).to.equal('FB');
// component.state.value = 'FB';
// expect(component.state.value).to.equal('FB');
})
returns an error
TypeError: Cannot read property 'setState' of undefined
presumably because the state has to be set when the Simulate function is called, and the input ref only reads the state.
If anyone has any answers to these various errors, or even how exactly the Simulate functionality works (the docs aren't a great help), do share.
So, I'm going to add another answer because i'd still like answers to the questions about the Simulate object I raised in my previous answer.
The issue I was having had nothing to do with the test I was writing, but the fact that I had not bound this to the handleChange method inside the Search container.
Here is the updated constructor for the Search container
constructor(props){
super(props);
this.state = {
value: ''
}
this.handleChange = this.handleChange.bind(this);
}
And here is the test that will now pass
it('accepts user input from the search button', () => {
let search_term;
const clickHandler = (term) => search_term = term;
const component = renderIntoDocument(
<Search onClick={clickHandler} />
);
const input = component.refs.input;
input.value = 'FB';
Simulate.change(component.refs.input);
expect(input.value).to.equal('FB');
})
I am using Chai, Sinon, and Mocha to test.
I am using Redux-Forms, along with ReactJS.
I want to test what happens after I click submit on a Forgot Password page.
Here's my code so far:
react file:
propTypes: {
fields: React.PropTypes.object.isRequired,
message: React.PropTypes.string.isRequired,
handleSubmit: React.PropTypes.func.isRequired,
},
renderForm: function() {
const { fields: {email}, handleSubmit, isFetching } = this.props;
const iconClass = "fa fa-star-o";
return(
<form form="forgotPassword" name="forgotPassword" className="stack-input">
<ReduxFormTextInput
{...email}
floatingLabelText="Email Address" />
<div className="clearfix" />
<div className="form-footer">
<ButtonSpinner spinner={isFetching} onClick={handleSubmit} ripple={true} raised={true} primary={true} >
<i className={iconClass} />Send
</ButtonSpinner>
</div>
</form>
);
},
renderMessage: function() {
return(
<div>
<i className="fa fa-exclamation-circle" style={{marginRight: '4px'}}/>
If your email is in the system, then it will be sent.
</div>
);
},
//Checks if user has submitted email.
emailSubmit: function(){
var locus = this, props = locus.props;
if(props.message === ''){
return null;
}
else if(props.message === 'SENT'){
return true;
}
return null;
},
render(){
var locus = this, props= locus.props;
return(
<div className="page-wrap">
<div className="page-column">
<h2>Forgot Your Password</h2>
{this.emailSubmit() ? this.renderMessage(): this.renderForm()}
</div>
</div>
);
}
unittest file:
describe('ForgotPasswordForm', () => {
const component = setup({
fields: {
email: {
onChange: spy(),
onBlur: spy(),
onFocus: spy()
}
}, // React.PropTypes.object.isRequired,
message: '', // React.PropTypes.string.isRequired,
handleSubmit: spy() // React.PropTypes.func.isRequired,
//onClearMessage:spy() //, React.PropTypes.func.isRequired
}),
domRoot = TestUtils.findRenderedDOMComponentWithClass(component, 'page-wrap'),
title = TestUtils.findRenderedDOMComponentWithTag(component, 'h2'),
submitButton = TestUtils.findRenderedDOMComponentWithClass(component, 'material-D-button'),
form = TestUtils.findRenderedDOMComponentWithTag(component, 'form'),
inputs = TestUtils.scryRenderedDOMComponentsWithTag(component, 'input'),
emailInput = inputs[0];
This test keeps failing, despite multiple attempts. I am not experienced with Spy(), so I'm not sure if I am suppose to be using calledWith.
it ('should display "If your email is in the system, then it will be sent." on submit', () => {
TestUtils.Simulate.change(emailInput, {target: {value: 'test#email.com'}});
TestUtils.Simulate.click(submitButton);
expect(domColumn.text).to.equal("Forgot Your Password");
});
This is the response I get.
+"If your email is in the system, then it will be sent."
- -"Forgot Your PasswordSend"
I used innerHTML to get a sense of what's being populated after the click, and I don't think the click is even registering.
When I try to do TestUtils.Simulate.change(emailInput, {target: {value: 'test#email.com'}});, it doesn't work. I have to populate the value of the email in the component.
You should be assigning your handleSubmit spy to a constant so you can at least be able to check whether it's getting called. (Probably the same for the other spies).
const handleSubmitSpy = spy()
const component = setup({
...
handleSubmit: handleSubmitSpy
Now you can check expect(handleSubmitSpy).toHaveBeenCalled().