[Vega] add functional tests for Vega visualization (#74097)

* [Vega] [Inspector] add functional tests for Request tab

* add some tests for Vega Debug tab

* add clipboard permissions for webdriver

* add smoke tests for data grid

* fix CI

* add some tests for vega expression funcitons

* change order

* Update _vega_chart.ts

* Rename dagta_grid.ts to data_grid.ts

* Update index.ts

* Update data_grid.ts

* stabilize tests

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Alexey Antonov 2020-08-12 14:05:23 +03:00 committed by GitHub
parent a81059b14b
commit 21b9b36c94
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 423 additions and 19 deletions

View file

@ -70,7 +70,7 @@ function VegaActionsMenu({ formatHJson, formatJson }: VegaActionsMenuProps) {
return (
<EuiPopover
id="helpMenu"
id="actionsMenu"
button={button}
isOpen={isPopoverOpen}
closePopover={closePopover}

View file

@ -66,7 +66,13 @@ export const SpecViewer = ({ vegaAdapter, ...rest }: SpecViewerProps) => {
<div className="eui-textRight">
<EuiCopy textToCopy={spec}>
{(copy) => (
<EuiButtonEmpty size="xs" flush="right" iconType="copyClipboard" onClick={copy}>
<EuiButtonEmpty
size="xs"
flush="right"
iconType="copyClipboard"
onClick={copy}
data-test-subj="vegaDataInspectorCopyClipboardButton"
>
{copyToClipboardLabel}
</EuiButtonEmpty>
)}

View file

@ -47,11 +47,13 @@ export const VegaDataInspector = ({ adapters }: VegaDataInspectorProps) => {
id: 'data-viewer--id',
name: dataSetsLabel,
content: <DataViewer vegaAdapter={adapters.vega} />,
'data-test-subj': 'vegaDataInspectorDataViewerButton',
},
{
id: 'signal-viewer--id',
name: signalValuesLabel,
content: <SignalViewer vegaAdapter={adapters.vega} />,
'data-test-subj': 'vegaDataInspectorSignalViewerButton',
},
{
id: 'spec-viewer--id',
@ -59,6 +61,7 @@ export const VegaDataInspector = ({ adapters }: VegaDataInspectorProps) => {
content: (
<SpecViewer className="vgaVegaDataInspector__specViewer" vegaAdapter={adapters.vega} />
),
'data-test-subj': 'vegaDataInspectorSpecViewerButton',
},
];

View file

@ -16,10 +16,25 @@
* specific language governing permissions and limitations
* under the License.
*/
import { unzip } from 'lodash';
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
const getTestSpec = (expression: string) => `
{
config: { "kibana": {"renderer": "svg"} }
$schema: https://vega.github.io/schema/vega/v5.json
marks: [{
type: text
encode: { update: { text: { value: "Test" } } }
}]
signals: [ {
on: [{
events: click
update: ${expression}
}]
}]}`;
export default function ({ getPageObjects, getService }: FtrProviderContext) {
const PageObjects = getPageObjects([
'timePicker',
@ -29,7 +44,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
'vegaChart',
]);
const filterBar = getService('filterBar');
const inspector = getService('inspector');
const vegaDebugInspectorView = getService('vegaDebugInspector');
const log = getService('log');
const retry = getService('retry');
const browser = getService('browser');
describe('vega chart in visualize app', () => {
before(async () => {
@ -88,5 +107,177 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
});
});
describe('Inspector Panel', () => {
it('should have inspector enabled', async () => {
await inspector.expectIsEnabled();
});
describe('Request Tab', () => {
beforeEach(async () => {
await inspector.open();
});
afterEach(async () => {
await inspector.close();
});
it('should contain "Statistics", "Request", "Response" tabs', async () => {
await inspector.openInspectorRequestsView();
for (const getFn of [
inspector.getOpenRequestDetailRequestButton,
inspector.getOpenRequestDetailResponseButton,
inspector.getOpenRequestStatisticButton,
]) {
await retry.try(async () => {
const requestStatisticTab = await getFn();
expect(await requestStatisticTab.isEnabled()).to.be(true);
});
}
});
it('should set the default query name if not given in the schema', async () => {
const requests = await inspector.getRequestNames();
expect(requests).to.be('Unnamed request #0');
});
it('should log the request statistic', async () => {
await inspector.openInspectorRequestsView();
const rawTableData = await inspector.getTableData();
expect(unzip(rawTableData)[0].join(', ')).to.be(
'Hits, Hits (total), Query time, Request timestamp'
);
});
});
describe('Debug Tab', () => {
beforeEach(async () => {
await inspector.open();
});
afterEach(async () => {
await inspector.close();
});
it('should contain "Data Sets", "Signal Values", "Spec" tabs', async () => {
await vegaDebugInspectorView.openVegaDebugInspectorView();
for (const getFn of [
vegaDebugInspectorView.getOpenDataViewerButton,
vegaDebugInspectorView.getOpenSignalViewerButton,
vegaDebugInspectorView.getOpenSpecViewerButton,
]) {
await retry.try(async () => {
const requestStatisticTab = await getFn();
expect(await requestStatisticTab.isEnabled()).to.be(true);
});
}
});
it('should contain data on "Signal Values" tab', async () => {
await vegaDebugInspectorView.openVegaDebugInspectorView();
await vegaDebugInspectorView.navigateToSignalViewerTab();
const { rows, columns } = await vegaDebugInspectorView.getGridTableData();
expect(columns.join(', ')).to.be('Signal, Value');
expect(rows.length).to.be.greaterThan(0);
expect(rows[0].length).to.be(2);
});
it('should contain data on "Signal Values" tab', async () => {
await vegaDebugInspectorView.openVegaDebugInspectorView();
await vegaDebugInspectorView.navigateToDataViewerTab();
const { rows, columns } = await vegaDebugInspectorView.getGridTableData();
expect(columns.length).to.be.greaterThan(0);
expect(rows.length).to.be.greaterThan(0);
});
it('should be able to copy vega spec to clipboard', async () => {
await vegaDebugInspectorView.openVegaDebugInspectorView();
await vegaDebugInspectorView.navigateToSpecViewerTab();
const copyCopyToClipboardButton = await vegaDebugInspectorView.getCopyClipboardButton();
expect(await copyCopyToClipboardButton.isEnabled()).to.be(true);
// The "clipboard-read" permission of the Permissions API must be granted
if (!(await browser.checkBrowserPermission('clipboard-read'))) {
return;
}
await copyCopyToClipboardButton.click();
expect(
(await browser.getClipboardValue()).includes(
'"$schema": "https://vega.github.io/schema/vega-lite/'
)
).to.be(true);
});
});
});
describe('Vega extension functions', () => {
beforeEach(async () => {
await filterBar.removeAllFilters();
});
const fillSpecAndGo = async (newSpec: string) => {
await PageObjects.vegaChart.fillSpec(newSpec);
await PageObjects.visEditor.clickGo();
const viewContainer = await PageObjects.vegaChart.getViewContainer();
const textElement = await viewContainer.findByTagName('text');
await textElement.click();
};
it('should update global time range by calling "kibanaSetTimeFilter" expression', async () => {
await fillSpecAndGo(getTestSpec('kibanaSetTimeFilter("2019", "2020")'));
const currentTimeRange = await PageObjects.timePicker.getTimeConfig();
expect(currentTimeRange.start).to.be('Jan 1, 2019 @ 00:00:00.000');
expect(currentTimeRange.end).to.be('Jan 1, 2020 @ 00:00:00.000');
});
it('should set filter by calling "kibanaAddFilter" expression', async () => {
await fillSpecAndGo(
getTestSpec('kibanaAddFilter({ query_string: { query: "response:200" }})')
);
expect(await filterBar.getFilterCount()).to.be(1);
});
it('should remove filter by calling "kibanaRemoveFilter" expression', async () => {
await filterBar.addFilter('response', 'is', '200');
expect(await filterBar.getFilterCount()).to.be(1);
await fillSpecAndGo(
getTestSpec('kibanaRemoveFilter({ match_phrase: { response: "200" }})')
);
expect(await filterBar.getFilterCount()).to.be(0);
});
it('should remove all filters by calling "kibanaRemoveAllFilters" expression', async () => {
await filterBar.addFilter('response', 'is', '200');
await filterBar.addFilter('response', 'is', '500');
expect(await filterBar.getFilterCount()).to.be(2);
await fillSpecAndGo(getTestSpec('kibanaRemoveAllFilters()'));
expect(await filterBar.getFilterCount()).to.be(0);
});
});
});
}

View file

@ -18,8 +18,14 @@
*/
import { Key } from 'selenium-webdriver';
import expect from '@kbn/expect';
import { FtrProviderContext } from '../ftr_provider_context';
const compareSpecs = (first: string, second: string) => {
const normalizeSpec = (spec: string) => spec.replace(/[\n ]/g, '');
return normalizeSpec(first) === normalizeSpec(second);
};
export function VegaChartPageProvider({
getService,
getPageObjects,
@ -28,24 +34,57 @@ export function VegaChartPageProvider({
const testSubjects = getService('testSubjects');
const browser = getService('browser');
const { common } = getPageObjects(['common']);
const retry = getService('retry');
class VegaChartPage {
public async getSpec() {
public getEditor() {
return testSubjects.find('vega-editor');
}
public getViewContainer() {
return find.byCssSelector('div.vgaVis__view');
}
public getControlContainer() {
return find.byCssSelector('div.vgaVis__controls');
}
public async getRawSpec() {
// Adapted from console_page.js:getVisibleTextFromAceEditor(). Is there a common utilities file?
const editor = await testSubjects.find('vega-editor');
const editor = await this.getEditor();
const lines = await editor.findAllByClassName('ace_line_group');
const linesText = await Promise.all(
return await Promise.all(
lines.map(async (line) => {
return await line.getVisibleText();
})
);
return linesText.join('\n');
}
public async getSpec() {
return (await this.getRawSpec()).join('\n');
}
public async focusEditor() {
const editor = await this.getEditor();
const textarea = await editor.findByClassName('ace_content');
await textarea.click();
}
public async fillSpec(newSpec: string) {
await retry.try(async () => {
await this.cleanSpec();
await this.focusEditor();
await browser.pressKeys(newSpec);
expect(compareSpecs(await this.getSpec(), newSpec)).to.be(true);
});
}
public async typeInSpec(text: string) {
const editor = await testSubjects.find('vega-editor');
const textarea = await editor.findByClassName('ace_content');
await textarea.click();
await this.focusEditor();
let repeats = 20;
while (--repeats > 0) {
await browser.pressKeys(Key.ARROW_UP);
@ -55,12 +94,16 @@ export function VegaChartPageProvider({
await browser.pressKeys(text);
}
public async getViewContainer() {
return await find.byCssSelector('div.vgaVis__view');
}
public async cleanSpec() {
const editor = await this.getEditor();
const aceGutter = await editor.findByClassName('ace_gutter');
public async getControlContainer() {
return await find.byCssSelector('div.vgaVis__controls');
await retry.try(async () => {
await aceGutter.doubleClick();
await browser.pressKeys(Key.BACK_SPACE);
expect(await this.getSpec()).to.be('');
});
}
public async getYAxisLabels() {

View file

@ -489,5 +489,17 @@ export async function BrowserProvider({ getService }: FtrProviderContext) {
const _id = idOrElement instanceof WebElementWrapper ? idOrElement._webElement : idOrElement;
await driver.switchTo().frame(_id);
}
public async checkBrowserPermission(permission: string): Promise<boolean> {
const result: any = await driver.executeAsyncScript(
`navigator.permissions.query({name:'${permission}'}).then(arguments[0])`
);
return Boolean(result?.state === 'granted');
}
public getClipboardValue(): Promise<string> {
return driver.executeAsyncScript('navigator.clipboard.readText().then(arguments[0])');
}
})();
}

View file

@ -0,0 +1,55 @@
/*
* 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.
*/
import { FtrProviderContext } from '../ftr_provider_context';
interface TabbedGridData {
columns: string[];
rows: string[][];
}
export function DataGridProvider({ getService }: FtrProviderContext) {
const find = getService('find');
class DataGrid {
async getDataGridTableData(): Promise<TabbedGridData> {
const table = await find.byCssSelector('.euiDataGrid');
const $ = await table.parseDomContent();
const columns = $('.euiDataGridHeaderCell__content')
.toArray()
.map((cell) => $(cell).text());
const rows = $.findTestSubjects('dataGridRow')
.toArray()
.map((row) =>
$(row)
.find('.euiDataGridRowCell__truncate')
.toArray()
.map((cell) => $(cell).text())
);
return {
columns,
rows,
};
}
}
return new DataGrid();
}

View file

@ -47,7 +47,12 @@ import { RemoteProvider } from './remote';
import { RenderableProvider } from './renderable';
import { TableProvider } from './table';
import { ToastsProvider } from './toasts';
import { PieChartProvider, ElasticChartProvider } from './visualizations';
import { DataGridProvider } from './data_grid';
import {
PieChartProvider,
ElasticChartProvider,
VegaDebugInspectorViewProvider,
} from './visualizations';
import { ListingTableProvider } from './listing_table';
import { SavedQueryManagementComponentProvider } from './saved_query_management_component';
import { KibanaSupertestProvider } from './supertest';
@ -72,12 +77,14 @@ export const services = {
dashboardPanelActions: DashboardPanelActionsProvider,
flyout: FlyoutProvider,
comboBox: ComboBoxProvider,
dataGrid: DataGridProvider,
embedding: EmbeddingProvider,
renderable: RenderableProvider,
table: TableProvider,
browser: BrowserProvider,
pieChart: PieChartProvider,
inspector: InspectorProvider,
vegaDebugInspector: VegaDebugInspectorViewProvider,
appsMenu: AppsMenuProvider,
globalNav: GlobalNavProvider,
toasts: ToastsProvider,

View file

@ -233,6 +233,18 @@ export function InspectorProvider({ getService }: FtrProviderContext) {
const singleRequest = await testSubjects.find('inspectorRequestName');
return await singleRequest.getVisibleText();
}
public getOpenRequestStatisticButton() {
return testSubjects.find('inspectorRequestDetailStatistics');
}
public getOpenRequestDetailRequestButton() {
return testSubjects.find('inspectorRequestDetailRequest');
}
public getOpenRequestDetailResponseButton() {
return testSubjects.find('inspectorRequestDetailResponse');
}
}
return new Inspector();

View file

@ -53,9 +53,15 @@ const SECOND = 1000;
const MINUTE = 60 * SECOND;
const NO_QUEUE_COMMANDS = ['getLog', 'getStatus', 'newSession', 'quit'];
const downloadDir = resolve(REPO_ROOT, 'target/functional-tests/downloads');
const chromiumDownloadPrefs = {
const chromiumUserPrefs = {
'download.default_directory': downloadDir,
'download.prompt_for_download': false,
'profile.content_settings.exceptions.clipboard': {
'[*.],*': {
last_modified: Date.now(),
setting: 1,
},
},
};
/**
@ -135,7 +141,7 @@ async function attemptToCreateCommand(
const prefs = new logging.Preferences();
prefs.setLevel(logging.Type.BROWSER, logging.Level.ALL);
chromeOptions.setUserPreferences(chromiumDownloadPrefs);
chromeOptions.setUserPreferences(chromiumUserPrefs);
chromeOptions.setLoggingPrefs(prefs);
chromeOptions.set('unexpectedAlertBehaviour', 'accept');
chromeOptions.setAcceptInsecureCerts(config.acceptInsecureCerts);
@ -185,7 +191,7 @@ async function attemptToCreateCommand(
edgeOptions.setBinaryPath(edgePaths.browserPath);
const options = edgeOptions.get('ms:edgeOptions');
// overriding options to include preferences
Object.assign(options, { prefs: chromiumDownloadPrefs });
Object.assign(options, { prefs: chromiumUserPrefs });
edgeOptions.set('ms:edgeOptions', options);
const session = await new Builder()
.forBrowser('MicrosoftEdge')

View file

@ -19,3 +19,4 @@
export { PieChartProvider } from './pie_chart';
export { ElasticChartProvider } from './elastic_chart';
export { VegaDebugInspectorViewProvider } from './vega_debug_inspector';

View file

@ -0,0 +1,68 @@
/*
* 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.
*/
import { FtrProviderContext } from '../../ftr_provider_context';
export function VegaDebugInspectorViewProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const inspector = getService('inspector');
const dataGrid = getService('dataGrid');
class VegaDebugInspectorView {
async openVegaDebugInspectorView() {
await inspector.openInspectorView('inspectorViewChooserVega debug');
}
public getOpenDataViewerButton() {
return testSubjects.find('vegaDataInspectorDataViewerButton');
}
public getOpenSignalViewerButton() {
return testSubjects.find('vegaDataInspectorSignalViewerButton');
}
public getOpenSpecViewerButton() {
return testSubjects.find('vegaDataInspectorSpecViewerButton');
}
public getCopyClipboardButton() {
return testSubjects.find('vegaDataInspectorCopyClipboardButton');
}
public getGridTableData() {
return dataGrid.getDataGridTableData();
}
public async navigateToDataViewerTab() {
const dataViewerButton = await this.getOpenDataViewerButton();
await dataViewerButton.click();
}
public async navigateToSignalViewerTab() {
const signalViewerButton = await this.getOpenSignalViewerButton();
await signalViewerButton.click();
}
public async navigateToSpecViewerTab() {
const specViewerButton = await this.getOpenSpecViewerButton();
await specViewerButton.click();
}
}
return new VegaDebugInspectorView();
}