mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
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:
parent
6c46e3b1d9
commit
bd03506736
10 changed files with 130 additions and 24 deletions
|
@ -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();
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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'),
|
||||
|
||||
|
|
|
@ -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')
|
||||
};
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -29,3 +29,4 @@ export { VisualizeListingTableProvider } from './visualize_listing_table';
|
|||
export { FlyoutProvider } from './flyout';
|
||||
|
||||
export * from './dashboard';
|
||||
export * from './visualize';
|
||||
|
|
|
@ -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);
|
||||
|
|
20
test/functional/services/visualize/index.js
Normal file
20
test/functional/services/visualize/index.js
Normal 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';
|
78
test/functional/services/visualize/visualization.js
Normal file
78
test/functional/services/visualize/visualization.js
Normal 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`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue