[7.0] [ftr/services/pipelineList] reduce calls to the browser to avoid flakiness (#31366) (#31373)

Backports the following commits to 7.0:
 - [ftr/services/pipelineList] reduce calls to the browser to avoid flakiness  (#31366)
This commit is contained in:
Spencer 2019-02-17 23:11:50 -08:00 committed by GitHub
parent 03eeb6b025
commit d565777054
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 90 additions and 83 deletions

View file

@ -209,6 +209,7 @@ module.exports = {
{
files: [
'test/functional/services/lib/leadfoot_element_wrapper/scroll_into_view_if_necessary.js',
'**/browser_exec_scripts/**/*',
],
rules: {
'prefer-object-spread/prefer-object-spread': 'off',
@ -220,6 +221,7 @@ module.exports = {
'ArrowFunctionExpression',
'AwaitExpression',
'ClassDeclaration',
'ImportDeclaration',
'RestElement',
'SpreadElement',
'YieldExpression',

View file

@ -17,9 +17,13 @@
* under the License.
*/
import { cloneDeep } from 'lodash';
import { modifyUrl } from '../../../src/core/utils';
import Keys from 'leadfoot/keys';
import { LeadfootElementWrapper } from './lib/leadfoot_element_wrapper';
export function BrowserProvider({ getService }) {
const leadfoot = getService('__leadfoot__');
@ -260,8 +264,12 @@ export function BrowserProvider({ getService }) {
* @param {string|function} function
* @param {...any[]} args
*/
async execute(...args) {
return await leadfoot.execute(...args);
async execute(fn, ...args) {
return await leadfoot.execute(fn, cloneDeep(args, arg => {
if (arg instanceof LeadfootElementWrapper) {
return arg._leadfootElement;
}
}));
}
}

View file

@ -214,6 +214,10 @@ export function TestSubjectsProvider({ getService }) {
async waitForAttributeToChange(selector, attribute, value) {
await find.waitForAttributeToChange(testSubjSelector(selector), attribute, value);
}
getCssSelector(selector) {
return testSubjSelector(selector);
}
}
return new TestSubjects();

View file

@ -80,6 +80,11 @@ exports[`PipelinesTable component renders component as expected 1`] = `
}
}
responsive={true}
rowProps={
Object {
"data-test-subj": "row",
}
}
search={
Object {
"box": Object {

View file

@ -206,6 +206,9 @@ function PipelinesTableUi({
search={search}
selection={selectionOptions}
sorting={true}
rowProps={{
'data-test-subj': 'row'
}}
/>
);
}

View file

@ -67,7 +67,7 @@ export default function ({ getService, getPageObjects }) {
await pipelineList.assertExists();
await pipelineList.setFilter(id);
const rows = await pipelineList.getRowsFromTable();
const rows = await pipelineList.readRows();
const newRow = rows.find(row => row.id === id);
expect(newRow)
@ -78,13 +78,13 @@ export default function ({ getService, getPageObjects }) {
describe('cancel button', () => {
it('discards the pipeline and redirects to the list', async () => {
await PageObjects.logstash.gotoPipelineList();
const originalRows = await pipelineList.getRowsFromTable();
const originalRows = await pipelineList.readRows();
await PageObjects.logstash.gotoNewPipelineEditor();
await pipelineEditor.clickCancel();
await pipelineList.assertExists();
const currentRows = await pipelineList.getRowsFromTable();
const currentRows = await pipelineList.readRows();
expect(originalRows).to.eql(currentRows);
});
});

View file

@ -31,7 +31,7 @@ export default function ({ getService, getPageObjects }) {
});
it('shows example pipelines', async () => {
const rows = await pipelineList.getRowsFromTable();
const rows = await pipelineList.readRows();
const rowsWithoutTime = rows.map(row => omit(row, 'lastModified'));
for (const time of rows.map(row => row.lastModified)) {
@ -67,14 +67,14 @@ export default function ({ getService, getPageObjects }) {
// select all
await pipelineList.clickSelectAll();
for (const row of await pipelineList.getRowsFromTable()) {
for (const row of await pipelineList.readRows()) {
expect(row).to.have.property('selected', true);
}
// unselect all
await pipelineList.clickSelectAll();
for (const row of await pipelineList.getRowsFromTable()) {
for (const row of await pipelineList.readRows()) {
expect(row).to.have.property('selected', false);
}
});
@ -113,7 +113,7 @@ export default function ({ getService, getPageObjects }) {
describe('filter', () => {
it('filters the pipeline list', async () => {
await pipelineList.setFilter('tweets');
const rows = await pipelineList.getRowsFromTable();
const rows = await pipelineList.readRows();
expect(rows).to.have.length(1);
expect(rows[0]).to.have.property('id', 'tweets_and_beats');
@ -140,7 +140,7 @@ export default function ({ getService, getPageObjects }) {
it('takes user to the second page', async () => {
await pipelineList.clickNextPage();
const rows = await pipelineList.getRowsFromTable();
const rows = await pipelineList.readRows();
const rowsWithoutTime = rows.map(row => omit(row, 'lastModified'));
for (const time of rows.map(row => row.lastModified)) {

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export function readPipelineListRows(container, cssSelectors) {
return Array.prototype.map.call(
container.querySelectorAll(cssSelectors.ROW),
function (row) {
return {
selected: row.querySelector('input[type=checkbox]').checked,
id: row.querySelector(cssSelectors.CELL_ID).innerText.trim(),
description: row.querySelector(cssSelectors.CELL_DESCRIPTION).innerText.trim(),
lastModified: row.querySelector(cssSelectors.CELL_LAST_MODIFIED).innerText.trim(),
username: row.querySelector(cssSelectors.CELL_USERNAME).innerText.trim(),
};
}
);
}

View file

@ -4,24 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import expect from 'expect.js';
import { times, mapValues } from 'lodash';
import { readPipelineListRows } from './browser_exec_scripts/read_pipeline_list_rows';
export function PipelineListProvider({ getService }) {
const testSubjects = getService('testSubjects');
const retry = getService('retry');
const browser = getService('browser');
const random = getService('random');
function assertLengthsMatch(arrays) {
const lengths = arrays.map(array => array.length);
try {
expect(Math.min(...lengths)).to.be(Math.max(...lengths));
} catch (err) {
throw new Error(`Expected lengths of arrays to match, ${JSON.stringify(arrays)}`);
}
}
// test subject selectors
const SUBJ_CONTAINER = `pipelineList`;
const SUBJ_BTN_ADD = `pipelineList btnAdd`;
@ -30,12 +20,16 @@ export function PipelineListProvider({ getService }) {
const SUBJ_FILTER = `pipelineList filter`;
const SUBJ_SELECT_ALL = `pipelineList pipelineTable checkboxSelectAll`;
const getSelectCheckbox = id => `pipelineList pipelineTable checkboxSelectRow-${id}`;
const SUBJ_CELL_ID = `pipelineList pipelineTable cellId`;
const SUBJ_CELL_DESCRIPTION = `pipelineList pipelineTable cellDescription`;
const SUBJ_CELL_LAST_MODIFIED = `pipelineList pipelineTable cellLastModified`;
const SUBJ_CELL_USERNAME = `pipelineList pipelineTable cellUsername`;
const SUBJ_BTN_NEXT_PAGE = `pipelineList pagination-button-next`;
const INNER_SUBJ_ROW = `row`;
const INNER_SUBJ_CELL_ID = `cellId`;
const INNER_SUBJ_CELL_DESCRIPTION = `cellDescription`;
const INNER_SUBJ_CELL_LAST_MODIFIED = `cellLastModified`;
const INNER_SUBJ_CELL_USERNAME = `cellUsername`;
const SUBJ_CELL_ID = `${SUBJ_CONTAINER} ${INNER_SUBJ_ROW} ${INNER_SUBJ_CELL_ID}`;
return new class PipelineList {
/**
* Set the text of the pipeline list filter
@ -52,38 +46,13 @@ export function PipelineListProvider({ getService }) {
* @return {Promise<Object>}
*/
async getRowCounts() {
const ids = await this.getRowIds();
const isSelecteds = await Promise.all(
ids.map(async (id) => await testSubjects.isSelected(getSelectCheckbox(id)))
);
const total = isSelecteds.length;
const isSelected = isSelecteds.filter(Boolean).length;
const rows = await this.readRows();
const total = rows.length;
const isSelected = rows.reduce((acc, row) => acc + (row.selected ? 1 : 0), 0);
const isUnselected = total - isSelected;
return { total, isSelected, isUnselected };
}
/**
* Read the rows from the table, mapping the cell values to key names
* in an array of objects
* @return {Promise<Array<Object>>}
*/
async getRowsFromTable() {
const ids = await this.getRowIds();
const selected = await Promise.all(ids.map(async (id) => await testSubjects.isSelected(getSelectCheckbox(id))));
const description = await testSubjects.getVisibleTextAll(SUBJ_CELL_DESCRIPTION);
const lastModified = await testSubjects.getVisibleTextAll(SUBJ_CELL_LAST_MODIFIED);
const username = await testSubjects.getVisibleTextAll(SUBJ_CELL_USERNAME);
const valuesByKey = { selected, id: ids, description, lastModified, username };
// ensure that we got values for every row, otherwise we can't
// recombine these into a list of rows
assertLengthsMatch(Object.values(valuesByKey));
return times(valuesByKey.id.length, i => {
return mapValues(valuesByKey, values => values[i]);
});
}
/**
* Click the selectAll checkbox until all rows are selected
* @return {Promise<undefined>}
@ -128,35 +97,33 @@ export function PipelineListProvider({ getService }) {
throw new Error('pipelineList.selectRandomRow() requires at least one unselected row');
}
// get pick an unselected selectbox and click it
await retry.try(async () => {
const ids = await this.getRowIds();
const rowToClick = await random.pickOne(ids);
const checkboxId = getSelectCheckbox(rowToClick);
const isSelected = await testSubjects.isSelected(checkboxId);
// pick an unselected selectbox and select it
const rows = await this.readRows();
const rowToClick = await random.pickOne(rows.filter(r => !r.selected));
await testSubjects.click(getSelectCheckbox(rowToClick.id));
if (isSelected) {
throw new Error('randomly chosen row was already selected');
}
await testSubjects.click(checkboxId);
});
// wait for the selected count to grow
await retry.try(async () => {
const now = await this.getRowCounts();
if (initial.isSelected >= now.isSelected) {
throw new Error(`randomly selected row still not selected`);
}
});
await retry.waitFor('selected count to grow', async () => (
(await this.getRowCounts()).isSelected > initial.isSelected
));
}
/**
* Get a list of all pipeline IDs in the current table
* @return {Promise<any>}
* Read the rows from the table, mapping the cell values to key names
* in an array of objects
* @return {Promise<Array<Object>>}
*/
async getRowIds() {
return await testSubjects.getVisibleTextAll(SUBJ_CELL_ID);
async readRows() {
return await browser.execute(
readPipelineListRows,
await testSubjects.find(SUBJ_CONTAINER),
{
ROW: testSubjects.getCssSelector(INNER_SUBJ_ROW),
CELL_ID: testSubjects.getCssSelector(INNER_SUBJ_CELL_ID),
CELL_DESCRIPTION: testSubjects.getCssSelector(INNER_SUBJ_CELL_DESCRIPTION),
CELL_LAST_MODIFIED: testSubjects.getCssSelector(INNER_SUBJ_CELL_LAST_MODIFIED),
CELL_USERNAME: testSubjects.getCssSelector(INNER_SUBJ_CELL_USERNAME),
},
);
}
/**
@ -196,11 +163,9 @@ export function PipelineListProvider({ getService }) {
* @return {Promise<undefined>}
*/
async assertExists() {
await retry.try(async () => {
if (!(await testSubjects.exists(SUBJ_CONTAINER))) {
throw new Error('Expected to find the pipeline list');
}
});
await retry.waitFor('pipline list visible on screen', async () => (
await testSubjects.exists(SUBJ_CONTAINER)
));
}
/**