Typescriptify and shim kbn_tp_run_pipeline test plugin (#50645) (#51714)

* Typscriptify and shim kbn_tp_run_pipeline test plugin

* fix imports to not re-export ‘legacy’ from root of plugin
This commit is contained in:
Anton Dosov 2019-11-26 15:41:45 +01:00 committed by GitHub
parent f44a220324
commit e74a8066eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 593 additions and 276 deletions

View file

@ -22,6 +22,6 @@ require('@kbn/test').runTestsCli([
require.resolve('../test/functional/config.js'),
require.resolve('../test/api_integration/config.js'),
require.resolve('../test/plugin_functional/config.js'),
require.resolve('../test/interpreter_functional/config.js'),
require.resolve('../test/interpreter_functional/config.ts'),
require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'),
]);

View file

@ -20,10 +20,6 @@
import { PluginInitializerContext } from '../../../core/public';
import { ExpressionsPublicPlugin } from './plugin';
export function plugin(initializerContext: PluginInitializerContext) {
return new ExpressionsPublicPlugin(initializerContext);
}
export { ExpressionsPublicPlugin as Plugin };
export * from './plugin';
@ -31,3 +27,10 @@ export * from './types';
export * from '../common';
export { interpreterProvider, ExpressionInterpret } from './interpreter_provider';
export { ExpressionRenderer, ExpressionRendererProps } from './expression_renderer';
export { ExpressionDataHandler } from './execute';
export { RenderResult, ExpressionRenderHandler } from './render';
export function plugin(initializerContext: PluginInitializerContext) {
return new ExpressionsPublicPlugin(initializerContext);
}

View file

@ -30,15 +30,17 @@ interface RenderError {
export type IExpressionRendererExtraHandlers = Record<string, any>;
export type RenderResult = RenderId | RenderError;
export class ExpressionRenderHandler {
render$: Observable<RenderId | RenderError>;
render$: Observable<RenderResult>;
update$: Observable<any>;
events$: Observable<event>;
private element: HTMLElement;
private destroyFn?: any;
private renderCount: number = 0;
private renderSubject: Rx.BehaviorSubject<RenderId | RenderError | null>;
private renderSubject: Rx.BehaviorSubject<RenderResult | null>;
private eventsSubject: Rx.Subject<unknown>;
private updateSubject: Rx.Subject<unknown>;
private handlers: IInterpreterRenderHandlers;
@ -49,11 +51,11 @@ export class ExpressionRenderHandler {
this.eventsSubject = new Rx.Subject();
this.events$ = this.eventsSubject.asObservable().pipe(share());
this.renderSubject = new Rx.BehaviorSubject(null as RenderId | RenderError | null);
this.renderSubject = new Rx.BehaviorSubject(null as RenderResult | null);
this.render$ = this.renderSubject.asObservable().pipe(
share(),
filter(_ => _ !== null)
) as Observable<RenderId | RenderError>;
) as Observable<RenderResult>;
this.updateSubject = new Rx.Subject();
this.update$ = this.updateSubject.asObservable().pipe(share());

View file

@ -254,7 +254,7 @@ module.exports = function (grunt) {
cmd: NODE,
args: [
'scripts/functional_tests',
'--config', 'test/interpreter_functional/config.js',
'--config', 'test/interpreter_functional/config.ts',
'--bail',
'--debug',
'--kibana-install-dir', KIBANA_INSTALL_DIR,

View file

@ -470,7 +470,10 @@ export async function BrowserProvider({ getService }: FtrProviderContext) {
);
}
public async executeAsync<R>(fn: string | ((...args: any[]) => R), ...args: any[]): Promise<R> {
public async executeAsync<R>(
fn: string | ((...args: any[]) => Promise<R>),
...args: any[]
): Promise<R> {
return await driver.executeAsyncScript(
fn,
...cloneDeep<any>(args, arg => {

View file

@ -3,7 +3,7 @@
This folder contains interpreter functional tests.
Add new test suites into the `test_suites` folder and reference them from the
`config.js` file. These test suites work the same as regular functional test.
`config.ts` file. These test suites work the same as regular functional test.
## Run the test
@ -11,17 +11,17 @@ To run these tests during development you can use the following commands:
```
# Start the test server (can continue running)
node scripts/functional_tests_server.js --config test/interpreter_functional/config.js
node scripts/functional_tests_server.js --config test/interpreter_functional/config.ts
# Start a test run
node scripts/functional_test_runner.js --config test/interpreter_functional/config.js
node scripts/functional_test_runner.js --config test/interpreter_functional/config.ts
```
# Writing tests
Look into test_suites/run_pipeline/basic.js for examples
Look into test_suites/run_pipeline/basic.ts for examples
to update baseline screenshots and snapshots run with:
```
node scripts/functional_test_runner.js --config test/interpreter_functional/config.js --updateBaselines
node scripts/functional_test_runner.js --config test/interpreter_functional/config.ts --updateBaselines
```

View file

@ -19,25 +19,26 @@
import path from 'path';
import fs from 'fs';
import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
export default async function ({ readConfigFile }) {
export default async function({ readConfigFile }: FtrConfigProviderContext) {
const functionalConfig = await readConfigFile(require.resolve('../functional/config'));
// Find all folders in ./plugins since we treat all them as plugin folder
const allFiles = fs.readdirSync(path.resolve(__dirname, 'plugins'));
const plugins = allFiles.filter(file => fs.statSync(path.resolve(__dirname, 'plugins', file)).isDirectory());
const plugins = allFiles.filter(file =>
fs.statSync(path.resolve(__dirname, 'plugins', file)).isDirectory()
);
return {
testFiles: [
require.resolve('./test_suites/run_pipeline'),
],
testFiles: [require.resolve('./test_suites/run_pipeline')],
services: functionalConfig.get('services'),
pageObjects: functionalConfig.get('pageObjects'),
servers: functionalConfig.get('servers'),
esTestCluster: functionalConfig.get('esTestCluster'),
apps: functionalConfig.get('apps'),
esArchiver: {
directory: path.resolve(__dirname, '../es_archives')
directory: path.resolve(__dirname, '../es_archives'),
},
snapshots: {
directory: path.resolve(__dirname, 'snapshots'),
@ -49,7 +50,9 @@ export default async function ({ readConfigFile }) {
...functionalConfig.get('kbnTestServer'),
serverArgs: [
...functionalConfig.get('kbnTestServer.serverArgs'),
...plugins.map(pluginDir => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}`),
...plugins.map(
pluginDir => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}`
),
],
},
};

View file

@ -16,24 +16,34 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Legacy } from 'kibana';
import {
ArrayOrItem,
LegacyPluginApi,
LegacyPluginSpec,
LegacyPluginOptions,
} from 'src/legacy/plugin_discovery/types';
export default function (kibana) {
return new kibana.Plugin({
// eslint-disable-next-line import/no-default-export
export default function(kibana: LegacyPluginApi): ArrayOrItem<LegacyPluginSpec> {
const pluginSpec: Partial<LegacyPluginOptions> = {
id: 'kbn_tp_run_pipeline',
uiExports: {
app: {
title: 'Run Pipeline',
description: 'This is a sample plugin to test running pipeline expressions',
main: 'plugins/kbn_tp_run_pipeline/app',
}
main: 'plugins/kbn_tp_run_pipeline/legacy',
},
},
init(server) {
init(server: Legacy.Server) {
// The following lines copy over some configuration variables from Kibana
// to this plugin. This will be needed when embedding visualizations, so that e.g.
// region map is able to get its configuration.
server.injectUiAppVars('kbn_tp_run_pipeline', async () => {
return await server.getInjectedUiAppVars('kibana');
return server.getInjectedUiAppVars('kibana');
});
}
});
},
};
return new kibana.Plugin(pluginSpec);
}

View file

@ -1,76 +0,0 @@
/*
* 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 React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { uiModules } from 'ui/modules';
import chrome from 'ui/chrome';
import { RequestAdapter, DataAdapter } from 'ui/inspector/adapters';
import { registries } from 'plugins/interpreter/registries';
import { npStart } from 'ui/new_platform';
// This is required so some default styles and required scripts/Angular modules are loaded,
// or the timezone setting is correctly applied.
import 'ui/autoload/all';
// These are all the required uiExports you need to import in case you want to embed visualizations.
import 'uiExports/visTypes';
import 'uiExports/visResponseHandlers';
import 'uiExports/visRequestHandlers';
import 'uiExports/visEditorTypes';
import 'uiExports/visualize';
import 'uiExports/savedObjectTypes';
import 'uiExports/search';
import 'uiExports/interpreter';
import { Main } from './components/main';
const app = uiModules.get('apps/kbnRunPipelinePlugin', ['kibana']);
app.config($locationProvider => {
$locationProvider.html5Mode({
enabled: false,
requireBase: false,
rewriteLinks: false,
});
});
app.config(stateManagementConfigProvider =>
stateManagementConfigProvider.disable()
);
function RootController($scope, $element) {
const domNode = $element[0];
// render react to DOM
render(<Main
RequestAdapter={RequestAdapter}
DataAdapter={DataAdapter}
expressions={npStart.plugins.expressions}
registries={registries}
/>, domNode);
// unmount react on controller destroy
$scope.$on('$destroy', () => {
unmountComponentAtNode(domNode);
});
}
chrome.setRootController('kbnRunPipelinePlugin', RootController);

View file

@ -1,91 +0,0 @@
/*
* 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 React from 'react';
import {
EuiPage,
EuiPageBody,
EuiPageContent,
EuiPageContentHeader,
} from '@elastic/eui';
import { first } from 'rxjs/operators';
class Main extends React.Component {
chartDiv = React.createRef();
constructor(props) {
super(props);
this.state = {
expression: '',
};
window.runPipeline = async (expression, context = {}, initialContext = {}) => {
this.setState({ expression });
const adapters = {
requests: new props.RequestAdapter(),
data: new props.DataAdapter(),
};
return await props.expressions.execute(expression, {
inspectorAdapters: adapters,
context,
searchContext: initialContext,
}).getData();
};
let lastRenderHandler;
window.renderPipelineResponse = async (context = {}) => {
if (lastRenderHandler) {
lastRenderHandler.destroy();
}
lastRenderHandler = props.expressions.render(this.chartDiv, context);
const renderResult = await lastRenderHandler.render$.pipe(first()).toPromise();
if (typeof renderResult === 'object' && renderResult.type === 'error') {
return this.setState({ expression: 'Render error!\n\n' + JSON.stringify(renderResult.error) });
}
};
}
render() {
const pStyle = {
display: 'flex',
width: '100%',
height: '300px'
};
return (
<EuiPage>
<EuiPageBody>
<EuiPageContent data-test-subj="pluginContent">
<EuiPageContentHeader>
runPipeline tests are running ...
</EuiPageContentHeader>
<div data-test-subj="pluginChart" ref={ref => this.chartDiv = ref} style={pStyle}/>
<div>{this.state.expression}</div>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
}
}
export { Main };

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 * from './np_ready';

View file

@ -0,0 +1,41 @@
/*
* 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 { PluginInitializerContext } from 'src/core/public';
import { npSetup, npStart } from 'ui/new_platform';
import { plugin } from './np_ready';
// This is required so some default styles and required scripts/Angular modules are loaded,
// or the timezone setting is correctly applied.
import 'ui/autoload/all';
// Used to run esaggs queries
import 'uiExports/fieldFormats';
import 'uiExports/search';
import 'uiExports/visRequestHandlers';
import 'uiExports/visResponseHandlers';
// Used for kibana_context function
import 'uiExports/savedObjectTypes';
import 'uiExports/interpreter';
const pluginInstance = plugin({} as PluginInitializerContext);
export const setup = pluginInstance.setup(npSetup.core, npSetup.plugins);
export const start = pluginInstance.start(npStart.core, npStart.plugins);

View file

@ -0,0 +1,28 @@
/*
* 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 React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { AppMountContext, AppMountParameters } from 'kibana/public';
import { Main } from './components/main';
export const renderApp = (context: AppMountContext, { element }: AppMountParameters) => {
render(<Main />, element);
return () => unmountComponentAtNode(element);
};

View file

@ -0,0 +1,122 @@
/*
* 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 React from 'react';
import { EuiPage, EuiPageBody, EuiPageContent, EuiPageContentHeader } from '@elastic/eui';
import { first } from 'rxjs/operators';
import {
RequestAdapter,
DataAdapter,
} from '../../../../../../../../src/plugins/inspector/public/adapters';
import {
Adapters,
Context,
ExpressionRenderHandler,
ExpressionDataHandler,
RenderResult,
} from '../../types';
import { getExpressions } from '../../services';
declare global {
interface Window {
runPipeline: (
expressions: string,
context?: Context,
initialContext?: Context
) => ReturnType<ExpressionDataHandler['getData']>;
renderPipelineResponse: (context?: Context) => Promise<RenderResult>;
}
}
interface State {
expression: string;
}
class Main extends React.Component<{}, State> {
chartRef = React.createRef<HTMLDivElement>();
constructor(props: {}) {
super(props);
this.state = {
expression: '',
};
window.runPipeline = async (
expression: string,
context: Context = {},
initialContext: Context = {}
) => {
this.setState({ expression });
const adapters: Adapters = {
requests: new RequestAdapter(),
data: new DataAdapter(),
};
return getExpressions()
.execute(expression, {
inspectorAdapters: adapters,
context,
// TODO: naming / typing is confusing and doesn't match here
// searchContext is also a way to set initialContext and Context can't be set to SearchContext
searchContext: initialContext as any,
})
.getData();
};
let lastRenderHandler: ExpressionRenderHandler;
window.renderPipelineResponse = async (context = {}) => {
if (lastRenderHandler) {
lastRenderHandler.destroy();
}
lastRenderHandler = getExpressions().render(this.chartRef.current!, context);
const renderResult = await lastRenderHandler.render$.pipe(first()).toPromise();
if (typeof renderResult === 'object' && renderResult.type === 'error') {
this.setState({
expression: 'Render error!\n\n' + JSON.stringify(renderResult.error),
});
}
return renderResult;
};
}
render() {
const pStyle = {
display: 'flex',
width: '100%',
height: '300px',
};
return (
<EuiPage>
<EuiPageBody>
<EuiPageContent data-test-subj="pluginContent">
<EuiPageContentHeader>runPipeline tests are running ...</EuiPageContentHeader>
<div data-test-subj="pluginChart" ref={this.chartRef} style={pStyle} />
<div>{this.state.expression}</div>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
}
}
export { Main };

View file

@ -0,0 +1,28 @@
/*
* 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 { PluginInitializer, PluginInitializerContext } from 'src/core/public';
import { Plugin, StartDeps } from './plugin';
export { StartDeps };
export const plugin: PluginInitializer<void, void, {}, StartDeps> = (
initializerContext: PluginInitializerContext
) => {
return new Plugin(initializerContext);
};

View file

@ -0,0 +1,45 @@
/*
* 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 { CoreSetup, CoreStart, PluginInitializerContext } from 'src/core/public';
import { ExpressionsStart } from './types';
import { setExpressions } from './services';
export interface StartDeps {
expressions: ExpressionsStart;
}
export class Plugin {
constructor(initializerContext: PluginInitializerContext) {}
public setup({ application }: CoreSetup) {
application.register({
id: 'kbn_tp_run_pipeline',
title: 'Run Pipeline',
async mount(context, params) {
const { renderApp } = await import('./app/app');
return renderApp(context, params);
},
});
}
public start(start: CoreStart, { expressions }: StartDeps) {
setExpressions(expressions);
}
}

View file

@ -0,0 +1,23 @@
/*
* 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 { createGetterSetter } from '../../../../../../src/plugins/kibana_utils/public/core';
import { ExpressionsStart } from './types';
export const [getExpressions, setExpressions] = createGetterSetter<ExpressionsStart>('Expressions');

View file

@ -0,0 +1,37 @@
/*
* 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 {
ExpressionsStart,
Context,
ExpressionRenderHandler,
ExpressionDataHandler,
RenderResult,
} from 'src/plugins/expressions/public';
import { Adapters } from 'src/plugins/inspector/public';
export {
ExpressionsStart,
Context,
ExpressionRenderHandler,
ExpressionDataHandler,
RenderResult,
Adapters,
};

View file

@ -18,13 +18,16 @@
*/
import expect from '@kbn/expect';
import { expectExpressionProvider } from './helpers';
import { ExpectExpression, expectExpressionProvider } from './helpers';
import { FtrProviderContext } from '../../../functional/ftr_provider_context';
// this file showcases how to use testing utilities defined in helpers.js together with the kbn_tp_run_pipeline
// this file showcases how to use testing utilities defined in helpers.ts together with the kbn_tp_run_pipeline
// test plugin to write autmated tests for interprete
export default function ({ getService, updateBaselines }) {
let expectExpression;
export default function({
getService,
updateBaselines,
}: FtrProviderContext & { updateBaselines: boolean }) {
let expectExpression: ExpectExpression;
describe('basic visualize loader pipeline expression tests', () => {
before(() => {
expectExpression = expectExpressionProvider({ getService, updateBaselines });
@ -39,7 +42,12 @@ export default function ({ getService, updateBaselines }) {
});
it('correctly sets timeRange', async () => {
const result = await expectExpression('correctly_sets_timerange', 'kibana', {}, { timeRange: 'test' }).getResponse();
const result = await expectExpression(
'correctly_sets_timerange',
'kibana',
{},
{ timeRange: 'test' }
).getResponse();
expect(result).to.have.property('timeRange', 'test');
});
});
@ -60,30 +68,32 @@ export default function ({ getService, updateBaselines }) {
// we can also do snapshot comparison of result of our expression
// to update the snapshots run the tests with --updateBaselines
it ('runs the expression and compares final output', async () => {
it('runs the expression and compares final output', async () => {
await expectExpression('final_output_test', expression).toMatchSnapshot();
});
// its also possible to check snapshot at every step of expression (after execution of each function)
it ('runs the expression and compares output at every step', async () => {
it('runs the expression and compares output at every step', async () => {
await expectExpression('step_output_test', expression).steps.toMatchSnapshot();
});
// and we can do screenshot comparison of the rendered output of expression (if expression returns renderable)
it ('runs the expression and compares screenshots', async () => {
it('runs the expression and compares screenshots', async () => {
await expectExpression('final_screenshot_test', expression).toMatchScreenshot();
});
// it is also possible to combine different checks
it ('runs the expression and combines different checks', async () => {
await (await expectExpression('combined_test', expression).steps.toMatchSnapshot()).toMatchScreenshot();
it('runs the expression and combines different checks', async () => {
await (
await expectExpression('combined_test', expression).steps.toMatchSnapshot()
).toMatchScreenshot();
});
});
// if we want to do multiple different tests using the same data, or reusing a part of expression its
// possible to retrieve the intermediate result and reuse it in later expressions
describe('reusing partial results', () => {
it ('does some screenshot comparisons', async () => {
it('does some screenshot comparisons', async () => {
const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[
{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},
{"id":"2","enabled":true,"type":"terms","schema":"segment","params":
@ -93,17 +103,20 @@ export default function ({ getService, updateBaselines }) {
const context = await expectExpression('partial_test', expression).getResponse();
// we reuse that response to render 3 different charts and compare screenshots with baselines
const tagCloudExpr =
`tagcloud metric={visdimension 1 format="number"} bucket={visdimension 0}`;
await (await expectExpression('partial_test_1', tagCloudExpr, context).toMatchSnapshot()).toMatchScreenshot();
const tagCloudExpr = `tagcloud metric={visdimension 1 format="number"} bucket={visdimension 0}`;
await (
await expectExpression('partial_test_1', tagCloudExpr, context).toMatchSnapshot()
).toMatchScreenshot();
const metricExpr =
`metricVis metric={visdimension 1 format="number"} bucket={visdimension 0}`;
await (await expectExpression('partial_test_2', metricExpr, context).toMatchSnapshot()).toMatchScreenshot();
const metricExpr = `metricVis metric={visdimension 1 format="number"} bucket={visdimension 0}`;
await (
await expectExpression('partial_test_2', metricExpr, context).toMatchSnapshot()
).toMatchScreenshot();
const regionMapExpr =
`regionmap visConfig='{"metric":{"accessor":1,"format":{"id":"number"}},"bucket":{"accessor":0}}'`;
await (await expectExpression('partial_test_3', regionMapExpr, context).toMatchSnapshot()).toMatchScreenshot();
const regionMapExpr = `regionmap visConfig='{"metric":{"accessor":1,"format":{"id":"number"}},"bucket":{"accessor":0}}'`;
await (
await expectExpression('partial_test_3', regionMapExpr, context).toMatchSnapshot()
).toMatchScreenshot();
});
});
});

View file

@ -18,14 +18,45 @@
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../functional/ftr_provider_context';
import {
ExpressionDataHandler,
RenderResult,
Context,
} from '../../plugins/kbn_tp_run_pipeline/public/np_ready/types';
type UnWrapPromise<T> = T extends Promise<infer U> ? U : T;
export type ExpressionResult = UnWrapPromise<ReturnType<ExpressionDataHandler['getData']>>;
export type ExpectExpression = (
name: string,
expression: string,
context?: Context,
initialContext?: Context
) => ExpectExpressionHandler;
export interface ExpectExpressionHandler {
toReturn: (expectedResult: ExpressionResult) => Promise<void>;
getResponse: () => Promise<ExpressionResult>;
runExpression: (step?: string, stepContext?: Context) => Promise<ExpressionResult>;
steps: {
toMatchSnapshot: () => Promise<ExpectExpressionHandler>;
};
toMatchSnapshot: () => Promise<ExpectExpressionHandler>;
toMatchScreenshot: () => Promise<ExpectExpressionHandler>;
}
// helper for testing interpreter expressions
export const expectExpressionProvider = ({ getService, updateBaselines }) => {
export function expectExpressionProvider({
getService,
updateBaselines,
}: Pick<FtrProviderContext, 'getService'> & { updateBaselines: boolean }): ExpectExpression {
const browser = getService('browser');
const screenshot = getService('screenshots');
const snapshots = getService('snapshots');
const log = getService('log');
const testSubjects = getService('testSubjects');
/**
* returns a handler object to test a given expression
* @name: name of the test
@ -34,20 +65,25 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => {
* @initialContext: initialContext provided to the expression
* @returns handler object
*/
return (name, expression, context = {}, initialContext = {}) => {
return (
name: string,
expression: string,
context: Context = {},
initialContext: Context = {}
): ExpectExpressionHandler => {
log.debug(`executing expression ${expression}`);
const steps = expression.split('|'); // todo: we should actually use interpreter parser and get the ast
let responsePromise;
let responsePromise: Promise<ExpressionResult>;
const handler = {
const handler: ExpectExpressionHandler = {
/**
* checks if provided object matches expression result
* @param result: expected expression result
* @returns {Promise<void>}
*/
toReturn: async result => {
toReturn: async (expectedResult: ExpressionResult) => {
const pipelineResponse = await handler.getResponse();
expect(pipelineResponse).to.eql(result);
expect(pipelineResponse).to.eql(expectedResult);
},
/**
* returns expression response
@ -63,16 +99,31 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => {
* @param stepContext: context to provide to expression
* @returns {Promise<*>} result of running expression
*/
runExpression: async (step, stepContext) => {
runExpression: async (
step: string = expression,
stepContext: Context = context
): Promise<ExpressionResult> => {
log.debug(`running expression ${step || expression}`);
const promise = browser.executeAsync((expression, context, initialContext, done) => {
if (!context) context = {};
if (!context.type) context.type = 'null';
window.runPipeline(expression, context, initialContext).then(result => {
done(result);
});
}, step || expression, stepContext || context, initialContext);
return await promise;
return browser.executeAsync<ExpressionResult>(
(
_expression: string,
_currentContext: Context & { type: string },
_initialContext: Context,
done: (expressionResult: ExpressionResult) => void
) => {
if (!_currentContext) _currentContext = { type: 'null' };
if (!_currentContext.type) _currentContext.type = 'null';
return window
.runPipeline(_expression, _currentContext, _initialContext)
.then(expressionResult => {
done(expressionResult);
return expressionResult;
});
},
step,
stepContext,
initialContext
);
},
steps: {
/**
@ -80,17 +131,19 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => {
* @returns {Promise<void>}
*/
toMatchSnapshot: async () => {
let lastResponse;
let lastResponse: ExpressionResult;
for (let i = 0; i < steps.length; i++) {
const step = steps[i];
lastResponse = await handler.runExpression(step, lastResponse);
const diff = await snapshots.compareAgainstBaseline(name + i, toSerializable(lastResponse), updateBaselines);
lastResponse = await handler.runExpression(step, lastResponse!);
const diff = await snapshots.compareAgainstBaseline(
name + i,
toSerializable(lastResponse!),
updateBaselines
);
expect(diff).to.be.lessThan(0.05);
}
if (!responsePromise) {
responsePromise = new Promise(resolve => {
resolve(lastResponse);
});
responsePromise = Promise.resolve(lastResponse!);
}
return handler;
},
@ -101,7 +154,11 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => {
*/
toMatchSnapshot: async () => {
const pipelineResponse = await handler.getResponse();
await snapshots.compareAgainstBaseline(name, toSerializable(pipelineResponse), updateBaselines);
await snapshots.compareAgainstBaseline(
name,
toSerializable(pipelineResponse),
updateBaselines
);
return handler;
},
/**
@ -111,24 +168,31 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => {
toMatchScreenshot: async () => {
const pipelineResponse = await handler.getResponse();
log.debug('starting to render');
const result = await browser.executeAsync((context, done) => {
window.renderPipelineResponse(context).then(result => {
done(result);
});
}, pipelineResponse);
const result = await browser.executeAsync<RenderResult>(
(_context: ExpressionResult, done: (renderResult: RenderResult) => void) =>
window.renderPipelineResponse(_context).then(renderResult => {
done(renderResult);
return renderResult;
}),
pipelineResponse
);
log.debug('response of rendering: ', result);
const chartEl = await testSubjects.find('pluginChart');
const percentDifference = await screenshot.compareAgainstBaseline(name, updateBaselines, chartEl);
const percentDifference = await screenshot.compareAgainstBaseline(
name,
updateBaselines,
chartEl
);
expect(percentDifference).to.be.lessThan(0.1);
return handler;
}
},
};
return handler;
};
function toSerializable(response) {
function toSerializable(response: ExpressionResult) {
if (response.error) {
// in case of error, pass through only message to the snapshot
// as error could be expected and stack trace shouldn't be part of the snapshot
@ -136,4 +200,4 @@ export const expectExpressionProvider = ({ getService, updateBaselines }) => {
}
return response;
}
};
}

View file

@ -17,7 +17,9 @@
* under the License.
*/
export default function ({ getService, getPageObjects, loadTestFile }) {
import { FtrProviderContext } from '../../../functional/ftr_provider_context';
export default function({ getService, getPageObjects, loadTestFile }: FtrProviderContext) {
const browser = getService('browser');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
@ -25,13 +27,16 @@ export default function ({ getService, getPageObjects, loadTestFile }) {
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common', 'header']);
describe('runPipeline', function () {
describe('runPipeline', function() {
this.tags(['skipFirefox']);
before(async () => {
await esArchiver.loadIfNeeded('../functional/fixtures/es_archiver/logstash_functional');
await esArchiver.load('../functional/fixtures/es_archiver/visualize_embedding');
await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'Australia/North', 'defaultIndex': 'logstash-*' });
await kibanaServer.uiSettings.replace({
'dateFormat:tz': 'Australia/North',
defaultIndex: 'logstash-*',
});
await browser.setWindowSize(1300, 900);
await PageObjects.common.navigateToApp('settings');
await appsMenu.clickLink('Run Pipeline');

View file

@ -17,18 +17,21 @@
* under the License.
*/
import { expectExpressionProvider } from './helpers';
import { ExpectExpression, expectExpressionProvider, ExpressionResult } from './helpers';
import { FtrProviderContext } from '../../../functional/ftr_provider_context';
export default function ({ getService, updateBaselines }) {
let expectExpression;
export default function({
getService,
updateBaselines,
}: FtrProviderContext & { updateBaselines: boolean }) {
let expectExpression: ExpectExpression;
describe('metricVis pipeline expression tests', () => {
before(() => {
expectExpression = expectExpressionProvider({ getService, updateBaselines });
});
describe('correctly renders metric', () => {
let dataContext;
let dataContext: ExpressionResult;
before(async () => {
const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[
{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},
@ -44,27 +47,46 @@ export default function ({ getService, updateBaselines }) {
it('with invalid data', async () => {
const expression = 'metricVis metric={visdimension 0}';
await (await expectExpression('metric_invalid_data', expression).toMatchSnapshot()).toMatchScreenshot();
await (
await expectExpression('metric_invalid_data', expression).toMatchSnapshot()
).toMatchScreenshot();
});
it('with single metric data', async () => {
const expression = 'metricVis metric={visdimension 0}';
await (await expectExpression('metric_single_metric_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot();
await (
await expectExpression(
'metric_single_metric_data',
expression,
dataContext
).toMatchSnapshot()
).toMatchScreenshot();
});
it('with multiple metric data', async () => {
const expression = 'metricVis metric={visdimension 0} metric={visdimension 1}';
await (await expectExpression('metric_multi_metric_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot();
await (
await expectExpression(
'metric_multi_metric_data',
expression,
dataContext
).toMatchSnapshot()
).toMatchScreenshot();
});
it('with metric and bucket data', async () => {
const expression = 'metricVis metric={visdimension 0} bucket={visdimension 2}';
await (await expectExpression('metric_all_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot();
await (
await expectExpression('metric_all_data', expression, dataContext).toMatchSnapshot()
).toMatchScreenshot();
});
it('with percentage option', async () => {
const expression = 'metricVis metric={visdimension 0} percentage=true colorRange={range from=0 to=1000}';
await (await expectExpression('metric_percentage', expression, dataContext).toMatchSnapshot()).toMatchScreenshot();
const expression =
'metricVis metric={visdimension 0} percentage=true colorRange={range from=0 to=1000}';
await (
await expectExpression('metric_percentage', expression, dataContext).toMatchSnapshot()
).toMatchScreenshot();
});
});
});

View file

@ -17,18 +17,21 @@
* under the License.
*/
import { expectExpressionProvider } from './helpers';
import { ExpectExpression, expectExpressionProvider, ExpressionResult } from './helpers';
import { FtrProviderContext } from '../../../functional/ftr_provider_context';
export default function ({ getService, updateBaselines }) {
let expectExpression;
export default function({
getService,
updateBaselines,
}: FtrProviderContext & { updateBaselines: boolean }) {
let expectExpression: ExpectExpression;
describe('tag cloud pipeline expression tests', () => {
before(() => {
expectExpression = expectExpressionProvider({ getService, updateBaselines });
});
describe('correctly renders tagcloud', () => {
let dataContext;
let dataContext: ExpressionResult;
before(async () => {
const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[
{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},
@ -41,27 +44,39 @@ export default function ({ getService, updateBaselines }) {
it('with invalid data', async () => {
const expression = 'tagcloud metric={visdimension 0}';
await (await expectExpression('tagcloud_invalid_data', expression).toMatchSnapshot()).toMatchScreenshot();
await (
await expectExpression('tagcloud_invalid_data', expression).toMatchSnapshot()
).toMatchScreenshot();
});
it('with just metric data', async () => {
const expression = 'tagcloud metric={visdimension 0}';
await (await expectExpression('tagcloud_metric_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot();
await (
await expectExpression('tagcloud_metric_data', expression, dataContext).toMatchSnapshot()
).toMatchScreenshot();
});
it('with metric and bucket data', async () => {
const expression = 'tagcloud metric={visdimension 0} bucket={visdimension 1}';
await (await expectExpression('tagcloud_all_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot();
await (
await expectExpression('tagcloud_all_data', expression, dataContext).toMatchSnapshot()
).toMatchScreenshot();
});
it('with font size options', async () => {
const expression = 'tagcloud metric={visdimension 0} bucket={visdimension 1} minFontSize=20 maxFontSize=40';
await (await expectExpression('tagcloud_fontsize', expression, dataContext).toMatchSnapshot()).toMatchScreenshot();
const expression =
'tagcloud metric={visdimension 0} bucket={visdimension 1} minFontSize=20 maxFontSize=40';
await (
await expectExpression('tagcloud_fontsize', expression, dataContext).toMatchSnapshot()
).toMatchScreenshot();
});
it('with scale and orientation options', async () => {
const expression = 'tagcloud metric={visdimension 0} bucket={visdimension 1} scale="log" orientation="multiple"';
await (await expectExpression('tagcloud_options', expression, dataContext).toMatchSnapshot()).toMatchScreenshot();
const expression =
'tagcloud metric={visdimension 0} bucket={visdimension 1} scale="log" orientation="multiple"';
await (
await expectExpression('tagcloud_options', expression, dataContext).toMatchSnapshot()
).toMatchScreenshot();
});
});
});