I have a component that lists a bunch of items. In this, they're cryptocurrency assets. When I click the header labelled Name, it sorts the items in alphabetical order. I would like to test this functionality by clicking the Name button in order to fire the event, and then asserting that the values in the name column are sorted alphabetically:
it("Sorts by name on click", () => {
const sortedByName = spotData
.sort((a, b) => {
return a.name < b.name ? -1 : a.name === b.name ? 0 : 1;
})
.map((ticker) => ticker.name);
const { getByText, getAllByTestId } = renderWithProviders(
<MarketSelectorPanel marketsList={spotData} />
);
fireEvent.click(getByText("Name"));
expect(getAllByTestId("market-selector-row-name")).toEqual(
sortedByName
);
});
The above doesn't work because expect(getAllByTestId("market-selector-row-name")) grabs the entire HTML element:
● MarketSelectorPanel tables › Sorts by name on click
expect(received).toEqual(expected) // deep equality
- Expected
+ Received
Array [
- "BCH/USD",
- "BTC/USD",
- "ETH/USD",
+ <span
+ class="market-selector-row-val"
+ data-testid="market-selector-row-name"
+ >
+ BCH/USD
+ </span>,
+ <span
+ class="market-selector-row-val"
+ data-testid="market-selector-row-name"
+ >
+ BTC/USD
+ </span>,
+ <span
+ class="market-selector-row-val"
+ data-testid="market-selector-row-name"
+ >
+ ETH/USD
+ </span>
]
Please try passing the XPath of the component for testing rather than the id
Solved using getNodeText:
const columnAfterClick = getAllByTestId("market-selector-row-name").map(getNodeText); // Contains array of text strings only now
expect(columnAfterClick).toEqual(sortedByName);
Related
In my application I am currently getting the react warning:
Warning: Each child in a list should have a unique "key" prop.
Check the render method of GetReplies.
This is the GetReplies method:
export function GetReplies(props) {
const Id = props.Id;
const replies = allComments.filter(obj => obj.ParentCommentId === Id).
sort((objA, objB) => new Date(objB.Time) - new Date(objA.Time));
console.log(generateKey(Id));
if (Object.keys(replies).length !== 0) {
return (
<div key = {"replies-container-" + generateKey(Id)} id={"replies-container-" + Id} className="replies-container">
<div key ={"panel-heading replies-title" + generateKey(Id)} className="panel-heading replies-title">
<a key = {"accordion-toggle replies-" + generateKey(Id)} className="accordion-toggle replies-a collapsed" data-parent={"#replies-container-" + Id} data-toggle="collapse" data-target={"#replies-for-" + Id}>Replies</a>
</div>
<div key = {"replies-for-" + Id} id={"replies-for-" + generateKey(Id)} className="replies-list collapse">
{
<React.Fragment>
{ Object.entries(replies).reverse().map(([key, arr]) => {
return (
<GetComments commentsArray = {replies}/>
)
}) }
</React.Fragment>
}
</div>
</div>
);
}
}
and this is the GetComments Method it calls:
export function GetComments({ commentsArray }) {
return (
<React.Fragment>
{commentsArray.map((comment) => {
const localId = comment.LocalId;
const parentCommentId = comment.ParentCommentId;
const parentLocalId = allComments.filter(obj => obj.Id === parentCommentId);
const recipients = comment.Recipients;
let recipientsArray = [];
let recipientsList;
recipients.forEach(function (arrayItem) {
recipientsArray.push(arrayItem.Name);
recipientsList = recipientsArray.join(', ');
});
console.log(generateKey(localId));
const date = new Date(comment.Time);
const formattedDate = date.toLocaleDateString() + " " + ("0" + date.getHours()).slice(-2) + ":" + ("0" + date.getMinutes()).slice(-2);
return (
<div key={generateKey(localId)} className="comment-container">
<div key={generateKey(comment.Commenter.ItemId)} className="commenter">
<span className="id-label">{localId}</span>
{parentCommentId && (
<span className="reply" title={`in reply to ${parentLocalId[0].LocalId}`}>
<a className="reply" href={"#c" + parentLocalId[0].LocalId}>⤵</a> </span>
)}
<span><a id={"c" + localId} name={"c" + localId}>{comment.Commenter.Name}</a></span>
<div key={generateKey(localId) + "-comment-actions-container "} className="comment-actions-container">
<button type="button" className="btn-reply" data-value={comment.Id} title="Reply to comment" data-toggle="modal" data-target="#dlg-new-comment">⥅</button>
</div>
</div>
<div key={generateKey(localId) + "-recipients "} className="recipients">{recipientsList}</div>
<div key={generateKey(localId) + "-comment "} className="comment">{comment.Comment}</div>
<div key={generateKey(localId) + "-comment-footer "} className="comment-footer">{formattedDate}</div>
<GetReplies Id = {comment.Id}/>
</div>
);
})}
</React.Fragment>
);
}
To help make sure each key is unique I made this generate key method:
const generateKey = (pre) => {
return `${ pre }_${ new Date().getTime() }`;
}
However I am still getting that unique key error and I have no idea what it is I could be missing?
I am even tried to expand upon it by doing:
const generateKey = (pre) => {
let index =0;
return `${ pre }_${ new Date().getTime()}_${index++}`;
}
but index would always equal 0? So I'm not sure why that wasn't incrementing either, any advice would be appreciated
Even though your generateKey might work (not sure though since it will get called quite quickly when mapping over an array) you are adding the key to the wrong part of your code.
React expects a key on every item in a for-loop. In your case you have added a key to every JSX element, except for those within the Object.entries(replies).reverse().map
You could probably fix your problem by adding keys in there, like so:
{Object.entries(replies).reverse().map(([key, arr]) => {
return (
<GetComments key={key} commentsArray = {replies}/>
)
})}
To add a note though, React uses the key value to recognize changes on re-renders. In case your array gets re-ordered it can use the keys to prevent enormous re-renders. The best practice here would be to add a key that is related to the data itself, not the location in an array or a random number.
In addition of the previous answer :
const generateKey = (pre) => {
let index =0;
return `${ pre }_${ new Date().getTime()}_${index++}`;
}
will always have a 0 index, because each function call will create his own scope, with his own "index" variable. To increment the index on each call, you must extract the "index" variable outside of the function, so each function call will share the same index variable, from the 'outside' scope (but using the item's index remains the best idea, if you have no unique key you can use from your data).
Using "new Date().getTime()" to generate keys is also a bad idea, because your code will be ran so quick that a few components could (and will) share the same timestamp.
An alternative is to use a third party library, like 'uuid', to generate unique ids, but it must be used carefully.
You should use the index of the commentsArray.map
like :
{commentsArray.map((comment, index) => {
...
key={generateKey(Id, index)}
and then :
const generateKey = (pre, index) => `${pre}_${index}`;
You are filtering with Id values, so your key will be the same for each childs, that's why you need the commentsArray index.
I have a simple roll the dice code and I want to update the score based on the dice roll.
This is the states of the dices and the scores:
const [Dice1, setDice1] = useState(0);
const [Dice2, setDice2] = useState(0);
const [Score1, setScore1] = useState(0);
const [Score2, setScore2] = useState(0);
function randomNum() {
setDice2(Math.floor(Math.random() * 6) + 1);
setDice1(Math.floor(Math.random() * 6) + 1);
Dice1 > Dice2 && Dice1 !== Dice2 ? setScore1(Score1 + 1) : Dice2 > Dice1 && Dice1 !== Dice2 ? setScore2(Score2 + 1) : setScore2(Score2 + 0);
}
The function randomNum is triggered on click.
return (
<div className="App">
<header className="App-header">
<p>Player 1 rolls {Dice1}</p>
<p>Player 2 rolls {Dice2}</p>
<button onClick={randomNum}>Roll the dice</button>
{Dice1 > Dice2 ? <p>Player 1 win</p> : Dice1 === Dice2 ? <p>Draw</p> : <p>Player 2 win</p>}
<p>Player 1 score is {Score1} points</p>
<p>Player 2 score is {Score2} points</p>
<button onClick={resetScore}>Reset score</button>
</header>
</div>
);
Everything works well, except that the score if updated with 1 round lag.
Everytime I roll, it is added the point from the last round.
What am I'm doing wrong here?
Update
One Score state would look like
const [score, setScore] = useState({
score1: 0,
score2: 0
});
You can update like
setScore(prev=>{...prev, score1: prev.score1 + 1})
setScore(prev=>{...prev, score2: prev.score2 + 1})
Try this
Use one state for dice instead
const [dice, setDice] = useState({
dice1: 0,
dice2: 0,
})
Update the state
function randomNum() {
const dice1Val = Math.floor(Math.random() * 6) + 1;
const dice2val = Math.floor(Math.random() * 6) + 1;
setDice({
dice1: dice1Val,
dice2val: dice2Val
})
And since setting score is an effect of setting dice, update the score in a useEffect
useEffect(() => {
dice.dice1 > dice.dice2 && dice.dice1 !== dice.dice2 ? setScore1(prev => prev + 1); : dice.dice2 > dice.dice1 && dice.dice1 !== dice.dice2 ? setScore2(prev => prev + 1) : setScore2(prev => prev + 1)
},[dice])
}
You can also put the scores under one state instead.
The most easiest way I can think of is to implement the resetScore function that will set the both states to 0.
function resetScore() { setScore1(0); setScore2(0); }
or call this function at the start of the randomNum.
You are not using the previous state values correctly. It is not recommended to use previous state values directly while updating state variables.
So in your code instead of :
setScore1(Score1 + 1)
you should do like this :
setScore1(prevScore => prevScore + 1);
and similarly for other state variables.
I'm usually used to play with the Object.keys() function.
But this time, I'm having trouble to read the value of the properties by their name, for each object within the following JSON :
The JSON
var myData = {
"customNotes":{
"2017/04/17":{
"concernedDate":"April, 17, 2017",
"notesList":[
{
"id":25,
"title":"Note 25 Title"
},
{
"id":51,
"title":"Note 51 Title"
}
]
},
"2017/04/14":{
"concernedDate":"April, 14, 2017",
"notesList":[
{
"id":53,
"title":"Note 53 Title"
}
]
}
}
}
The OUTPUT I Need
What I Need is the following OUTPUT :
2017/04/17
concernedDate: April, 17, 2017
Number of notes: 2
Notes list:
- id: 25
- title: Note 25 Title
- id: 51
- title: Note 51 Title
- - - - - - - -
2017/04/14
concernedDate: April, 14, 2017
Number of notes: 1
Notes list:
- id: 53
- title: Note 53 Title
My buggy JS Code
$(Object.keys(myData.customNotes)).each(function(iGroup){
//Get Key/Val of each Group of notes
var keyGroup = Object.keys(myData.customNotes)[iGroup];
var valGroup = myData.customNotes[keyGroup];
//Print Key (the date as a string)
console.log(valGroup[0]);
//Print Key Property ('concernedDate')
console.log('concernedDate: ' + valGroup[0].concernedDate);
//Print Key Property Length ('notesList')
console.log('Number of notes: ' + valGroup[0].notesList.length);
//Print List of notes
console.log('Notes list:');
//If the property exists
if(valGroup[0].notesList){
//For each item in 'notesList'
$(Object.keys(valGroup[0].notesList)).each(function(iNote){
//Get Key/Val For each note
var keyNote = Object.keys(valGroup[0].notesList)[iNote];
var valNote = valGroup[0].notesList[keyNote];
//Print the properties of each note
console.log('- id: ' + valNote.id);
console.log('- title: ' + valNote.title);
});
}
});
es6
Object.keys(myData.customNotes).reduce((prev, curr) => {
const date = curr;
const concernedDate = myData.customNotes[curr].concernedDate;
const numberNotes = myData.customNotes[curr].notesList.length;
const notesList = myData.customNotes[curr].notesList.map(note => `- id: ${note.id} \n - title: ${note.title} \n\n`);
return prev + `${date} \n ${concernedDate} \n Number of notes: ${numberNotes} \n Notes list: \n ${notesList} \n - - - - - - - - \n`;
}, ''));
es5
Object.keys(myData.customNotes).reduce(function (prev, curr) {
const date = curr;
const concernedDate = myData.customNotes[curr].concernedDate;
const numberNotes = myData.customNotes[curr].notesList.length;
const notesList = myData.customNotes[curr].notesList.map(function(note) { return ‘- id: ‘ + note.id + ‘\n’ + ‘- title: ‘ + note.title + ‘\n\n’;});
return prev + 'date' + '\n' + concernedDate + '\n Number of notes: ' + numberNotes + '\n Notes list: \n' + notesList + ' \n - - - - - - - - \n';
}, ''));
First, I want to thank #cheesenthusiast for its answer: It works like a charm, and you should definitely rely on it, wether it is in ES6 or ES5 !
In the same time, I've experimented by myself, and found another working solution, in a more "classic loopy-style" way of achieving the same result.
Another working method (more classical)
So for those of you that want to have more control at every step of the script, Here is another working way to do it :
//WORKING
console.log('---START LIST---');
//For each GROUP OF NOTES
for (var item in myData.customNotes) {
//Print GROUP OF NOTES key
console.log(item);
//Print GROUP OF NOTES properties
console.log('concernedDate: ' + myData.customNotes[item].concernedDate);
console.log('Number of notes: ' + myData.customNotes[item].notesList.length);
console.log('Notes list: ');
//For each NOTE
$(Object.keys(myData.customNotes[item].notesList)).each(function(iGroup){
//Get this Array item
var keyGroup = Object.keys(myData.customNotes[item].notesList)[iGroup];
var valGroup = myData.customNotes[item].notesList[keyGroup];
//Print NOTE properties
console.log('- id: ' + valGroup.id);
console.log('- title: ' + valGroup.title);
});
console.log('- - - - - -');
}
console.log('---END LIST---');
SORTING
After having added some elements to my Object, I need to sort it by KEY (the date : example 2017/04/17).
But it always returns the same items in the same order, regardless of what I try :
//Creating an Array of SORTED values (Nothing works :-/)
var tmpArr = Object.keys(myData.customNotes).sort((a, b) => a > b ? a : b);
var tmpArr = Object.keys(myData.customNotes).sort((a, b) => Date(a).valueOf() > Date(b).valueOf() ? a : b);
var tmpArr = Object.keys(myData.customNotes).sort((a, b) => a.valueOf() > b.valueOf() ? a : b);
var tmpArr = Object.keys(myData.customNotes).sort((a, b) => a-b);
//Convert back the Array to an Object
var myData.customNotes = tmpArr.reduce(function(acc, cur, i) {
acc[i] = cur;
return acc;
}, {});
I'm working for iPhone application using phonegap. In the application there are few dropdowns whose values are more the 10000. Now we are trying to replace the dropdown with Autocomplete.
We are maintaining those 10000 records in SQLite DB and fetch the records from the DB as user enters the string.
CODE:
<input type ="text" class="inputFormText" ng-model="Location.location" id="location" list ="locValues" placeholder="{{'lSummary_Location_text'|translate}}" autocomplete = "on" maxlength="80" ng-keyup="populateLocations($event)"/>
<div class="aListCon">
<datalist id="locValues">
</datalist>
</div>
$scope.populateLocations = function($event){
if ($event.target.value.length > 2) {
try
{
db.transaction(function(tx){
tx.executeSql("SELECT ID, VALUE FROM tbl_Location WHERE VALUE LIKE '%" + $event.target.value + "%'", [],
function(tx, res){
if(res.rows.length > 0)
{
var template = "";
for (var i = 0; i < res.rows.length; i++) {
var id = res.rows.item(i).value + ' / ID: ' + res.rows.item(i).id;
template += '<option class="aCon" id="' + id + '" value="' + res.rows.item(i).value + '"></option>';
};
document.getElementById('locValues').innerHTML = template;
}
else
console.log("No records found")
},
function(ex){
console.log("Populate Location Error: " + ex.message);
});
});
}
catch(ex)
{
console.log("Populate Location Error: " + ex.message);
}
};
};
I was able to fetch the records form the SQLite and append to the datalist, but Autocomplete is not displayed in the UI.
Any idea where I'm going wrong?
Thanks in advance
This code should be working:
var divtoappend=angular.element( document.querySelector( '#locValues' ) );
divtoappend.append("<option class="aCon" id="' + id + '" value="' + res.rows.item(i).value + '"></option>");
Is it possible to turn off certain dropdown filters in the footer? This is the API I'm using: https://datatables.net/examples/api/multi_filter_select.html
I don't want all columns to be filterable. Also, is it possible to have the header labels be the default in the dropdown instead of a blank?
Here is my live example: http://live.datatables.net/cufawibi/3/
A usual approach is to use a css class to filter which columns can be filterable.
You could also add the column name as selected and disabled in order to display it as the default (included an all values options to disable the filter).
initComplete: function () {
var api = this.api();
api.columns('.filtersearch').indexes().flatten().each( function ( i ) {
var column = api.column( i );
var select = $('<select></select>')
.appendTo( $(column.footer()).empty() )
.on( 'change', function () {
var val = $.fn.dataTable.util.escapeRegex(
$(this).val()
);
column
.search( val ? '^'+val+'$' : '', true, false )
.draw();
} );
select.append('<option selected disabled>"'+$(column.header()).text()+'"</option>');
select.append('<option value="">All values</option>');
column.data().unique().sort().each( function ( d, j ) {
select.append( '<option value="'+d+'">'+d+'</option>' );
} );
} );
}
UPDATE:
In order to have the class added from the controller, changed also the table head definition to
<th ng-repeat="(i, th) in head" value="{{th.id}}" class="{{th.class}}"><span>{{th.name}}</span></th>
Live example (filter only for the "Payload" column, add filtersearch class to other columns to enable filtering)
I don't think there's a clean way to do this via the API. So this solution will be hacky.
So you'd wrap your logic around an IF block to filter out the columns dropdown filters you don't want to display.
api.columns().indexes().flatten().each(function (index) {
var column, select;
// You don't want to display the first and the fourth dropdown filter
if (index !== 0 || index !== 3) {
column = api.column(i);
select = $('<select><option value=""></option></select>')
.appendTo($(column.footer()).empty())
.on('change', function () {
var val = $.fn.dataTable.util.escapeRegex($(this).val());
column.search(val ? '^' + val + '$' : '', true, false).draw();
});
column.data().unique().sort().each(function (d, j) {
select.append('<option value="' + d + '">' + d + '</option>')
});
}
});