Cypress: Get JQuery value without needing `then` or `each` - cypress

I'm hoping someone can help, but I've posted this as a Cypress discussion as well, although it might just be my understanding that's wrong
I need to get the Cypress.Chainable<JQuery<HTMLElement>> of the cell of a table using the column header and another cell's value in the same row.
Here's a working example JQuery TS Fiddle: https://jsfiddle.net/6w1r7ha9/
My current implementation looks as follows:
static findCellByRowTextColumnHeaderText(
rowText: string,
columnName: string,
) {
const row = cy.get(`tr:contains(${rowText})`);
const column = cy.get(`th:contains(${columnName})`)
const columnIndex = ???
return row.find(`td:eq(${columnIndex})`)
}
This function is required because I want to write DRY code to find cells easily for content verification, clicking elements inside of it etc.
The only example I've seen is this https://stackoverflow.com/a/70686525/1321908, but the following doesn't work:
const columns = cy.get('th')
let columnIndex = -1
columns.each((el, index) => {
if (el.text().includes(columnName) {
columnIndex = index
}
cy.log('columnIndex', columnIndex) // Outputs 2 as expected
})
cy.log('finalColumnIndex', columnIndex) // Outputs -1
My current thinking is something like:
const columnIndex: number = column.then((el) => el.index())
This however returns a Chainable<number> How to turn it into number, I have no idea. I'm using this answer to guide my thinking in this regard.

Using .then() in a Cypress test is almost mandatory to avoid flaky tests.
To avoid problems with test code getting ahead of web page updating, Cypress uses Chainable to retry the DOM query until success, or time out.
But the Chainable interface isn't a promise, so you can't await it. You can only then() it.
It would be nice if you could substitute another keyword like unchain
const column = unchain cy.get(`th:contains(${columnName})`)
but unfortunately Javascript can't be extended with new keywords. You can only add methods like .then() onto objects like Chainable.
Having said that, there are code patterns that allow extracting a Chainable value and using it like a plain Javascript variable.
But they are limited to specific scenarios, for example assigning to a global in a before() and using it in an it().
If you give up the core feature of Cypress, the automatic retry feature, then it's just jQuery exactly as you have in the fiddle (but using Cypress.$() instead of $()).
But even Mikhail's thenify relys on the structure of the test when you add a small amount of asynchronicity
Example app
<foo>abc</foo>
<script>
setTimeout(() => {
const foo = document.querySelector('foo')
foo.innerText = 'def'
}, 1000)
</script>
Test
let a = cy.get("foo").thenify()
// expect(a.text()).to.eq('def') // fails
// cy.wrap(a.text()).should('eq', 'def') // fails
cy.wrap(a).should('have.text', 'def') // passes
let b = cy.get("foo") // no thenify
b.should('have.text', 'def') // passes

Based on your working example, you will need to get the headers first, map out the text, then find the index of the column (I've chosen 'Col B'). Afterwards you will find the row containing the other cell value, then get all the cells in row and use .eq() with the column index found earlier.
// get headers, map text, filter to Col B index
cy.get("th")
.then(($headers) => Cypress._.map($headers, "innerText"))
.then(cy.log)
.invoke("indexOf", "Col B")
.then((headerIndex) => {
// find row containing Val A
cy.contains("tbody tr", "Val A")
.find("td")
// get cell containing Val B
.eq(headerIndex)
.should("have.text", "Val B");
});
Here is the example.

Related

Cypress number of elements in a table

Is there an easier way to count the number of elements in a table via Cypress?
I have a 3 column table and I want to count, how many times is "India" in my table.
I tried it this way:
cy.get('ag-grid-table').contains(country).its('length').as("countrylength");
cy.get('#countrylength').then((ccc) => {
cy.log(ccc)
})
but this gives me always 1, as it finds only first element.
Then I have this solution, that may probably work:
let count = 0;
cy.get('ag-grid-table div.ag-row').each(($el, index, $list) => {
if($el.text().includes(country))
{ count ++; }
}
but can't we just find that country with one line command using length()?
You could just move the contains() inside the selector, it gets around the restriction of only returning the first result.
You need to merge the country variable into the selector as well, either with a string template as below, or by concatenating strings (+ operator).
cy.get(`ag-grid-table div.ag-row:contains("${country}")`)
.its('length').as("countrylength");
cy.get('#countrylength').should('eq', 3)
You can use the filter command for this:
cy.get('ag-grid-table div.ag-row')
.filter(':contains("India")')
.should('have.length', 3)
In case you want to use the length of different tests in your project you can use Cypress.env(). As aliases as are removed after every test.
cy.get('ag-grid-table div.ag-row')
.filter(':contains("India")')
.its('length')
.then((len) => {
Cypress.env('length', len)
})
//To use it in other test
cy.get('selector').type(Cypress.env('length'))
cy.get('selector').should('have.length', Cypress.env('length'))
If you want to use the alias value later in the test, or in another test you could access it directly from the "Mocha context" by using a function callback
if('tests the ag-grid', function() { // now alias value is available on "this"
cy.get('ag-grid-table div.ag-row')
.invoke('text')
.then(text => text.match(/India/g).length) // number of occurrences in table
.as("countrylength");
cy.get('.flags').should('have.length', this.countrylength)
})

vuetify v-data-table custom-filter questions

I wanted to build a custom-filter for my v-data-table (containing books) which
allows to narrow down search results by entering multiple space separated search terms
separates compound words with hyphens into separate search terms
all search terms should appear in the item, but they can appear in different columns/props, so searching for "tolkien hobbit" would give you back a book with title "The Hobbit" and author "J.R.R. Tolkien"
I figured that using "value" in the customFilter function would require all of the search terms to appear in the same prop, so I used item instead, but vuetify gives the complete item to the filter function, not only the filterable headers of the data table, which will yield unwanted search results, so I'm re-initializing the item to get rid of unwanted props.
methods: {
customFilter(value, search, item) {
item = {
number: item.number,
title: item.title,
author: item.author
};
const haystack = Object.values(item).join().toLowerCase();
let s = search.toString().toLowerCase();
let needles = s.replace('-',' ').split(' ');
return needles.filter(needle => haystack.indexOf(needle) >= 0).length == needles.length
}
}
My questions
Is there a way to tell vuetify to only give the filterable header props to the custom-filter as item?
if not, is there a better way to get rid of unwanted props than re-initializing? (destructuring with spread operator also works, but is longer and eslint nags about unused vars)
Is there maybe even a way to realize my requirements with "value" instead of "item"? (I don't see how)
Answering my own questions:
No, you can't tell vuetify to pass only specific props of the item to the custom filter, you just pass the item object
I get rid of unwanted props by filtering / reducing now
value only gives you one value, presumably it defaults to the first column, but I'm not 100% sure
My solution now:
methods: {
itemFilterMultipleSearchTermsAnd(value, search, item) {
const nonSearchableColumns = ["excludedPropName1", "excludedPropName2"];
const reducedObject = Object.keys(item)
.filter((key) => !nonSearchableColumns.includes(key))
.reduce((res, key) => ((res[key] = item[key]), res), {});
const haystack = Object.values(reducedObject)
.join()
.toLowerCase();
let s = search.toString().toLowerCase();
let needles = s.replace("-", " ").split(" ");
return (
needles.filter((needle) => haystack.indexOf(needle) >= 0).length ==
needles.length
);
}
}

Horizon/RethinkDB api

So I have one error and two questions about Horizon. (http://horizon.io/docs/)
I have a simple table and 1 record inside, this is the row:
id: "o34242-43251-462...",
user_id: "3lw5-6232s2...",
other_id: "531h51-51351..."
When I run hz serve I'm getting this error:
Unexpected index name (invalid field): "hz_[["user_id"],[["other_id","0"]]]".
Okay, okay, invalid field... But I didn't find any informations about "valid" fields. Anybody knows the answer? What can I do?
My questions:
How to run Horizon "forever" on ubuntu? Now I'm using just hz serve and "&",
If I have few queries, for example:
let table = this.horizon('firstTable');
let table2 = this.horizon('secondTable');
table.find(someId).fetch().subscribe((item) => {
//and then I want to run other query, e.g:
table2.find(item.id).fetch().subscribe((value) => {
//and here again run other query... <-- how to avoid this?
});
});
How to e.g return a value from horizon's query, and then use this value inside other query? I don't want to write it all in one function...
Thanks for any help.
Since 'fetch' returns an RxJS observable, and it yield one result only, you can use 'toPromise' to consume it in a convenient fashion.
let table = this.horizon('firstTable');
let table2 = this.horizon('secondTable');
let item1 = await table.find(someId).fetch().toPromise();
let item2 = await table2.find(item1.id).fetch().toPromise();
Or without ES7 await, just using Promise.then:
table.find(someId).fetch().toPromise().then((item1) => {
table2.find(item1.id).fetch().toPromise().then((item2) => {
// take over the world from here
});
});

group.all() call required for data to populate correctly

So I've encountered a weird issue when dealing with making Groups based on a variable when the crossfilter is using an array, instead of a literal number.
I currently have an output array of a date, then 4 values, that I then map into a composite graph. The problem is that the 4 values can fluctuate depending on the input given to the page. What I mean is that based on what it receives, I can have 3 values, or 10, and there's no way to know in advance. They're placed into an array which is then given to a crossfilter. When in testing, I was accessing using
dimension.group.reduceSum(function(d) { return d[0]; });
Where 0 was changed to whatever I needed. But I've finished testing, for the most part, and began to adapt it into a dynamic system where it can change, but there's always at least the first two. To do this I created an integer that keeps track of what index I'm at, and then increases it after the group has been created. The following code is being used:
var range = crossfilter(results);
var dLen = 0;
var curIndex = 0;
var dateDimension = range.dimension(function(d) { dLen = d.length; return d[curIndex]; });
curIndex++;
var aGroup = dateDimension.group().reduceSum(function(d) { return d[curIndex]; });
curIndex++;
var bGroup = dateDimension.group().reduceSum(function(d) { return d[curIndex]; });
curIndex++;
var otherGroups = [];
for(var h = 0; h < dLen-3; h++) {
otherGroups[h] = dateDimension.group().reduceSum(function(d) { return d[curIndex]; });
curIndex++;
}
var charts = [];
for(var x = 0; x < dLen - 3; x++) {
charts[x] = dc.barChart(dataGraph)
.group(otherGroups[x], "Extra Group " + (x+1))
.hidableStacks(true)
}
charts[charts.length] = dc.lineChart(dataGraph)
.group(aGroup, "Group A")
.hidableStacks(true)
charts[charts.length] = dc.lineChart(dataGraph)
.group(aGroup, "Group B")
.hidableStacks(true)
The issue is this:
The graph gets built empty. I checked the curIndex variable multiple times and it was always correct. I finally decided to instead check the actual group's resulting data using the .all() method.
The weird thing is that AFTER I used .all(), now the data works. Without a .all() call, the graph cannot determine the data and outputs absolutely nothing, however if I call .all() immediately after the group has been created, it populates correctly.
Each Group needs to call .all(), or only the ones that do will work. For example, when I first was debugging, I used .all() only on aGroup, and only aGroup populated into the graph. When I added it to bGroup, then both aGroup and bGroup populated. So in the current build, every group has .all() called directly after it is created.
Technically there's no issue, but I'm really confused on why this is required. I have absolutely no idea what the cause of this is, and I was wondering if there was any insight into it. When I was using literals, there was no issue, it only happens when I'm using a variable to create the groups. I tried to get output later, and when I do I received NaN for all the values. I'm not really sure why .all() is changing values into what they should be especially when it only occurs if I do it immediately after the group has been created.
Below is a screenshot of the graph. The top is when everything has a .all() call after being created, while the bottom is when the Extra Groups (the ones defined in the for loop) do not have the .all() call anymore. The data is just not there at all, I'm not really sure why. Any thoughts would be great.
http://i.stack.imgur.com/0j1ey.jpg
It looks like you may have run into the classic "generating lambdas from loops" JavaScript problem.
You are creating a whole bunch of functions that reference curIndex but unless you call those functions immediately, they will refer to the same instance of curIndex in the global environment. So if you call them after initialization, they will probably all try to use a value which is past the end.
Instead, you might create a function which generates your lambdas, like so:
function accessor(curIndex) {
return function(d) { return d[curIndex]; };
}
And then each time call .reduceSum(accessor(curIndex))
This will cause the value of curIndex to get copied each time you call the accessor function (or you can think of each generated function as having its own environment with its own curIndex).

Get all rows not filtered from jqGrid

I have local data in a grid. How can I get all of the rows or IDs that are not removed after a user uses the filter toolbar? I need to get all filtered rows, regardless of pagination.
For example, say I begin with 50 rows in the grid. The user uses the filter toolbar and the set of rows decreases to 10 rows. How can I get those ten rows?
There are no direct way to get the information which you need. Internally jqGrid uses $.jgrid.from to filter local data. The main code which uses $.jgrid.from in inside of addLocalData. To get results which you need without studying all the code I suggest to use the fact that all filtered data will be returned by select method of $.jgrid.from (see the line of code). My suggestion is to catch the data before the data will be cut to the page size.
To do this I suggest to use sub-classing: overwriting of the method select method of $.jgrid.from. I demonstrate the technique in the examples created for the answer and this one.
In your case the code will be
var oldFrom = $.jgrid.from,
lastSelected;
$.jgrid.from = function (source, initalQuery) {
var result = oldFrom.call(this, source, initalQuery),
old_select = result.select;
result.select = function (f) {
lastSelected = old_select.call(this, f);
return lastSelected;
};
return result;
};
Now the variable lastSelected will save the array of elements which are results of the last sorting or filtering operation. Because $.jgrid.from is global the data are not connected to the grid. If you have more as one grid on the page it will be uncomfortable. One can fix the small disadvantage with the following line in the code of loadComplate of every grid:
loadComplete: function () {
this.p.lastSelected = lastSelected; // set this.p.lastSelected
}
In the way we introduce new jqGrid parameter lastSelected which will have close structure as data parameter, but will hold only last filtered data.
The following code will display the ids of filtered data in alert message
$("#getIds").click(function () {
var filteredData = $grid.jqGrid('getGridParam', 'lastSelected'), i, n, ids = [],
idName = $grid.jqGrid('getGridParam', 'localReader').id;
if (filteredData) {
for (i = 0, n = filteredData.length; i < n; i++) {
ids.push(filteredData[i][idName]);
}
alert("tolal number of filtered data: " + n + "\n" +
"ids of filtered data:\n" + ids.join(', '));
}
});
I used localReader.id parameter because property name used for local data are typically id or _id_. The _id_ will be used in case of data loaded from the server if one uses loadonce: true option.
The demo demonstrate the approach. If one filter for example only the data from FedEx and then clicks on "Show Ids" button one will see information about all filtered and not only about the data displayed on the current page:
UPDATED: free jqGrid provides new lastSelectedData option. See the demo in the list of demos.
You colud use afterSearch option of the search toolbar:
var filteredIDs = new Array(); //Global variable
$("#"+gridId).jqGrid("filterToolbar", { stringResult:true, searchOnEnter:false,
afterSearch:function(){
filteredIDs = $("#"+gridId).getDataIDs();
}
});
If you want to get the filtered rows instead the filtered IDs, use getRowData() instead of getDataIDs().
All, I found another answer which is far easier to include
loadComplete: function (gridData) {
var isSearchPerformed = $grid.getGridParam("postData")._search;
if (isSearchPerformed) {
$("#spanFilterTotal").text(gridData.records);
}
All you want is below:
$.each($grid.getRowData(), function( index, value ) {
a.push(value["COLUMN_NAME"]); //Get the selected data you want
});

Resources