Wait for visualizations to render in tests (#21258)

* Wait for visualizations to render

* Use Visualize.waitForRender on dashboard

* Enable previously flaky dashboard tests again

* Add data-loading for initial render

* Remove unnecessary timeout
This commit is contained in:
Tim Roes 2018-07-27 13:10:25 +02:00 committed by GitHub
parent 6c46e3b1d9
commit bd03506736
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 130 additions and 24 deletions

View file

@ -38,6 +38,7 @@ interface EmbeddedVisualizeHandlerParams extends VisualizeLoaderParams {
}
const RENDER_COMPLETE_EVENT = 'render_complete';
const LOADING_ATTRIBUTE = 'data-loading';
/**
* A handler to the embedded visualization. It offers several methods to interact
@ -51,7 +52,6 @@ export class EmbeddedVisualizeHandler {
private listeners = new EventEmitter();
private firstRenderComplete: Promise<void>;
private renderCompleteHelper: RenderCompleteHelper;
private onRenderCompleteListener: () => void;
private shouldForceNextFetch: boolean = false;
private debouncedFetchAndRender = debounce(() => {
if (this.destroyed) {
@ -93,10 +93,7 @@ export class EmbeddedVisualizeHandler {
this.listeners.once(RENDER_COMPLETE_EVENT, resolve);
});
this.onRenderCompleteListener = () => {
this.listeners.emit(RENDER_COMPLETE_EVENT);
};
element.setAttribute(LOADING_ATTRIBUTE, '');
element.addEventListener('renderComplete', this.onRenderCompleteListener);
this.appState = appState;
@ -224,6 +221,11 @@ export class EmbeddedVisualizeHandler {
this.listeners.removeListener(RENDER_COMPLETE_EVENT, listener);
}
private onRenderCompleteListener = () => {
this.listeners.emit(RENDER_COMPLETE_EVENT);
this.element.removeAttribute(LOADING_ATTRIBUTE);
};
/**
* Fetches new data and renders the chart. This will happen debounced for a couple
* of milliseconds, to bundle fast successive calls into one fetch and render,
@ -236,6 +238,7 @@ export class EmbeddedVisualizeHandler {
*/
private fetchAndRender = (forceFetch = false): void => {
this.shouldForceNextFetch = forceFetch || this.shouldForceNextFetch;
this.element.setAttribute(LOADING_ATTRIBUTE, '');
this.debouncedFetchAndRender();
};

View file

@ -37,10 +37,7 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.dashboard.gotoDashboardLandingPage();
});
// Disabling flaky test
// Failing in PageObjects.dashboard.waitForRenderComplete() with the error
// "tryForTime timeout: Error: Still waiting on more visualizations to finish rendering, expecting: 17, received: 16"
describe.skip('adding a filter that excludes all data', async () => {
describe('adding a filter that excludes all data', async () => {
before(async () => {
await PageObjects.dashboard.clickNewDashboard();
await PageObjects.dashboard.setTimepickerInDataRange();
@ -104,8 +101,7 @@ export default function ({ getService, getPageObjects }) {
});
});
// Skipped because it depends on filter applied by disabled test
describe.skip('disabling a filter unfilters the data on', async () => {
describe('disabling a filter unfilters the data on', async () => {
before(async () => {
await testSubjects.click('disableFilter-bytes');
await PageObjects.header.waitUntilLoadingHasFinished();

View file

@ -49,6 +49,7 @@ import {
DashboardAddPanelProvider,
DashboardPanelActionsProvider,
FlyoutProvider,
VisualizationProvider,
} from './services';
export default async function ({ readConfigFile }) {
@ -103,6 +104,7 @@ export default async function ({ readConfigFile }) {
dashboardAddPanel: DashboardAddPanelProvider,
dashboardPanelActions: DashboardPanelActionsProvider,
flyout: FlyoutProvider,
visualization: VisualizationProvider,
},
servers: commonConfig.get('servers'),

View file

@ -34,6 +34,7 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
const dashboardAddPanel = getService('dashboardAddPanel');
const visualization = getService('visualization');
const PageObjects = getPageObjects(['common', 'header', 'settings', 'visualize']);
const defaultFindTimeout = config.get('timeouts.find');
@ -550,20 +551,10 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
async waitForRenderComplete() {
await retry.try(async () => {
const sharedItems = await find.allByCssSelector('[data-shared-item]');
const renderComplete = await Promise.all(sharedItems.map(async sharedItem => {
return await sharedItem.getAttribute('data-render-complete');
const sharedItems = await this.getPanelSharedItemData();
await Promise.all(sharedItems.map(async sharedItem => {
return await visualization.waitForRender(sharedItem.element, sharedItem.title, { ignoreNonVisualization: true });
}));
if (renderComplete.length !== sharedItems.length) {
const expecting = `expecting: ${sharedItems.length}, received: ${renderComplete.length}`;
throw new Error(
`Some shared items dont have data-render-complete attribute, ${expecting}`);
}
const totalCount = renderComplete.filter(value => value === 'true' || value === 'disabled').length;
if (totalCount < sharedItems.length) {
const expecting = `${sharedItems.length}, received: ${totalCount}`;
throw new Error(`Still waiting on more visualizations to finish rendering, expecting: ${expecting}`);
}
});
}
@ -581,6 +572,7 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
const sharedItems = await find.allByCssSelector('[data-shared-item]');
return await Promise.all(sharedItems.map(async sharedItem => {
return {
element: sharedItem,
title: await sharedItem.getAttribute('data-title'),
description: await sharedItem.getAttribute('data-description')
};

View file

@ -29,6 +29,7 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
const find = getService('find');
const log = getService('log');
const flyout = getService('flyout');
const visualization = getService('visualization');
const PageObjects = getPageObjects(['common', 'header']);
const defaultFindTimeout = config.get('timeouts.find');
@ -636,6 +637,7 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
async clickGo() {
await testSubjects.click('visualizeEditorRenderButton');
await PageObjects.header.waitUntilLoadingHasFinished();
await visualization.waitForRender();
}
async toggleAutoMode() {

View file

@ -118,6 +118,14 @@ export function FindProvider({ getService }) {
return await this._ensureElement(async () => await parentElement.findDisplayedByCssSelector(selector));
}
async allDescendantDisplayedByCssSelector(selector, parentElement) {
log.debug(`Find.allDescendantDisplayedByCssSelector(${selector})`);
const allElements = await parentElement.findAllByCssSelector(selector);
return await Promise.all(
allElements.map((element) => this._ensureElement(async () => element))
);
}
async displayedByCssSelector(selector, timeout = defaultFindTimeout, parentElement) {
log.debug('in displayedByCssSelector: ' + selector);
return await this._ensureElementWithTimeout(timeout, async remote => {

View file

@ -29,3 +29,4 @@ export { VisualizeListingTableProvider } from './visualize_listing_table';
export { FlyoutProvider } from './flyout';
export * from './dashboard';
export * from './visualize';

View file

@ -70,6 +70,10 @@ export function TestSubjectsProvider({ getService }) {
return await find.descendantDisplayedByCssSelector(testSubjSelector(selector), parentElement);
}
async findAllDescendant(selector, parentElement) {
return await find.allDescendantDisplayedByCssSelector(testSubjSelector(selector), parentElement);
}
async find(selector, timeout = 1000) {
log.debug(`TestSubjects.find(${selector})`);
return await find.byCssSelector(testSubjSelector(selector), timeout);

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { VisualizationProvider } from './visualization';

View file

@ -0,0 +1,78 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
const TEST_SUBJECT_VISUALIZE = 'visualizationLoader';
export function VisualizationProvider({ getService }) {
const log = getService('log');
const retry = getService('retry');
const find = getService('find');
const testSubjects = getService('testSubjects');
return new class Visualization {
/**
* This method waits for a visualization to finish rendering in case its currently rendering.
* Every visualization indicates a loading state via an attribute while data is currently fetched
* and then rendered. This method waits for that attribute to be gone.
* If you call this method before a visualization started fetching its data, it might return immediately,
* i.e. it does not wait for the next fetch and render to start.
*
* You can pass in a parent element, in which the visualization should be located. The parent element can also be
* the visualization element itself. If you don't specify a parent element, this method looks for a visualization
* in body. In case you specify a parent (or use the default) and multiple visualizations are found within that element,
* this method will throw an error.
*
* This method will wait an absolute of 10 seconds for the visualization to finish rendering.
*/
async waitForRender(parentElement, title = '', { ignoreNonVisualization } = {}) {
log.debug(`Visualization.waitForRender(${title})`);
const tag = `waitForRender(${title}):`;
if (!parentElement) {
parentElement = await find.byCssSelector('body');
}
const testSubj = await parentElement.getAttribute('data-test-subj');
let vis;
if (testSubj === TEST_SUBJECT_VISUALIZE) {
vis = parentElement;
} else {
const visualizations = await testSubjects.findAllDescendant(TEST_SUBJECT_VISUALIZE, parentElement);
if (visualizations.length === 0 && ignoreNonVisualization) {
log.info(`${tag} element does not contain a visualization, ignoring it`);
return;
}
if (visualizations.length !== 1) {
throw new Error(`${tag} expects exactly 1 visualization in the specified parent, but found ${visualizations.length}`);
}
vis = visualizations[0];
}
await retry.try(async () => {
const renderComplete = await vis.getAttribute('data-render-complete');
if (renderComplete !== 'disabled' && renderComplete !== 'true') {
throw new Error(`${tag} visualization has not finished first render`);
}
const isLoading = await vis.getAttribute('data-loading');
if (isLoading !== null) {
throw new Error(`${tag} visualization is still loading/rendering`);
}
});
}
};
}