Working with tables #2190
-
Hi, I am working with table, there are no identifiers on neither rows nor columns. To work with the data and table item positions our steps look like this:
To implement this step i find index of Ping column ColumnIndex: (columnHeaderText) => Question.about('magic', async (actor) => {
const before = await PageElements.located(By.xpath(`//th[text()="${columnHeaderText}"]/preceding-sibling::th`)).answeredBy(actor);
return before.length + 1;
}); same for the row just with different selector. Then I can select input in the correct place with const columnIndex = await ColumnIndex(columnName).answeredBy(actorInTheSpotlight());
const rowIndex = await RowIndex(rowName).answeredBy(actorInTheSpotlight());
return actorInTheSpotlight().attemptsTo(
Enter.theValue(text).into(input.of(column(columnIndex)).of(row(rowIndex))),
); where example column selector is like this: column: (columnIndex) => PageElement.located(By.css(`td:nth-child(${columnIndex})`)) This works, but I believe it can be done better. And I loose clarity in reports for the values I resolved manually. How could I make this better? |
Beta Was this translation helpful? Give feedback.
Replies: 5 comments
-
Hi @eyesopen, I don't know how your original table and use case looks like, but,
I would normally go for something like this to get a readable report in domain specific language: import { Ensure, equals } from '@serenity-js/assertions';
import { Log } from '@serenity-js/core';
import { describe, it } from '@serenity-js/playwright-test';
import { By, Navigate, PageElements, Text } from '@serenity-js/web';
const CustomersTable = () => PageElement.located(By.css('#customers'))
.describedAs('customer table')
const TableRows = () => PageElements.located(By.css('tr'))
.describedAs('all table rows').of(CustomersTable());
const TableColumns = () => PageElements.located(By.css('td'))
.describedAs('all table columns')
.of(CustomersTable())
const TableColumnsInRow = (index: number) => TableColumns().of(TableRows().nth(index));
const CompanyInRow = (index: number) => TableColumnsInRow(index).nth(0)
.describedAs(`company column in row ${index}`);
const ContactInRow = (index: number) => TableColumnsInRow(index).nth(1)
.describedAs(`contact column in row ${index}`);
const CountryInRow = (index: number) => TableColumnsInRow(index).nth(2)
.describedAs(`country column in row ${index}`);
describe('table selector', () => (
it('ensures text of columns in a row', async ({actor}) => (
actor.attemptsTo(
Navigate.to('https://www.w3schools.com/html/html_tables.asp'),
Log.the(Text.ofAll(TableColumnsInRow(1))),
Ensure.that(Text.of(CompanyInRow(1)), equals('Alfreds Futterkiste')),
Ensure.that(Text.of(ContactInRow(1)), equals('Maria Anders')),
Ensure.that(Text.of(CountryInRow(1)), equals('Germany'))
))
)
));
/*
✓ Alice ensures that the text of company column in row 1 does equal 'Alfreds Futterkiste' (7ms)
✓ Alice ensures that the text of contact column in row 1 does equal 'Maria Anders' (7ms)
✓ Alice ensures that the text of country column in row 1 does equal 'Germany' (6ms)
*/ Another use case we're sometimes have to cope with: We do not even now the index of the table row under test because sort order in table is not defined and may be different at any refresh. So, in the example of our customers table we can just access a row by some expected content. Let's say we want to ensure that contact name for the Centro comercial Moctezuma in Mexico equals Francisco Chang. import { contain, Ensure, equals } from '@serenity-js/assertions';
import { describe, it } from '@serenity-js/playwright-test';
import { By, Navigate, PageElement, PageElements, Text } from '@serenity-js/web';
const CustomersTable = () => PageElement.located(By.css('#customers'))
.describedAs('customer table')
const TableRows = () => PageElements.located(By.css('tr'))
.describedAs('all table rows').of(CustomersTable());
const CompanyColumn = () => PageElements.located(By.css('td:nth-child(1)'))
.describedAs('company column');
const ContactColumn = () => PageElements.located(By.css('td:nth-child(2)'))
.describedAs('contact column');
const CountryColumn = () => PageElements.located(By.css('td:nth-child(3)'))
.describedAs('country column');
const ContactColumByCountryAndCompany = (country: string, company: string) =>
ContactColumn().of(
TableRows()
.where(Text.ofAll(CountryColumn()), contain(country))
.where(Text.ofAll(CompanyColumn()), contain(company))
.first())
.first();
describe('table selector', () => (
it('ensures text of columns in a row', async ({actor}) => (
actor.attemptsTo(
Navigate.to('https://www.w3schools.com/html/html_tables.asp'),
Ensure.that(Text.of(ContactColumByCountryAndCompany(
'Mexico','Centro comercial Moctezuma')),
equals('Francisco Chang'))
))
)
));
/*
✓ Alice ensures that the text of the first of contact column
of the first of all table rows of customer table
where the text of country column does contain 'Mexico'
and the text of company column does contain 'Centro comercial Moctezuma'
does equal 'Francisco Chang' (66ms)
*/ Be aware, that in this use case we have to make sure to take always the first result of our matching row of country & company, as we assume there is just one result with this combination. |
Beta Was this translation helpful? Give feedback.
-
what would be your approach if you do not know in which column is which field? For example if every time the table is drawn its columns are set in random order. Then you can no longer hardcode column index. |
Beta Was this translation helpful? Give feedback.
-
You mean, if our example table would be so unpredictable, that with each reload the 'Contact' column could be the first, the middle or the last column? I assume, that at least the the company, the country and the contact - so to say 'Mexico', 'Centro comercial Moctezuma' and 'Francisco Chang' - stays together in one row, and get not mixed up as well? 🥲 Can you then at least predict in which row such a combination of country, contact end company ends up? Or is this random, as well? There would be the strategy to ensure that there is a row that contains a combination of county, company and contact in any order, I guess. Can imagine a strategy, as well, where we could try to get the index of a column at first, then reuse this index. But never tried this. May I ask if your question is hypothetical or is this a real situation you're currently facing? |
Beta Was this translation helpful? Give feedback.
-
My site has many tables. In my opinion, solution where i do not bind to index but rather human readable column header provided in the step makes sense. Example i was referring to could be:
Table 2:
|
Beta Was this translation helpful? Give feedback.
-
I was trying to find a better way, limited to my knowledge, and I have tweaked my method to the point where I like how it feels and reports. const rowByText= (text) => PageElement.located(By.xpath(`//td[text()="${text}"]/ancestor::tr`)).describedAs(`${text} row`);
const columnByHeader = (text) => Question.about(`${columnText} column`, async (actor) => {
const header = PageElement.located(By.xpath(`//th[text()="${text}"]`));
const headersBefore = PageElements.located(By.xpath('.//preceding-sibling::th'));
const count= await actor.answer(headersBefore.of(header).count());
return actor.answer(PageElement.located(By.css(`td:nth-child(${count + 1})`)));
}); Here I separated header to separate PageElement, that in case the header with required text does not exist, it would fail instead of finding 0 preceding th. Now I can find elements of interest with // When user enters Test in the Ping column of the Pong row
Enter.theValue('Test').into(input.of(columnByHeader('Ping').of(rowByText('Pong')))), This reports nicely:
🌞 |
Beta Was this translation helpful? Give feedback.
Hi @eyesopen, I don't know how your original table and use case looks like, but,
I would normally go for something like this to get a readable report in domain specific language: