mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
parent
ed98a87115
commit
239f604f94
26 changed files with 489 additions and 441 deletions
|
@ -38,7 +38,7 @@ export class ToolingLog {
|
|||
public warning(...args: any[]): void;
|
||||
public error(errOrMsg: string | Error): void;
|
||||
public write(...args: any[]): void;
|
||||
public indent(spaces: number): void;
|
||||
public indent(spaces?: number): void;
|
||||
public getWriters(): ToolingLogWriter[];
|
||||
public setWriters(reporters: ToolingLogWriter[]): void;
|
||||
public getWritten$(): Rx.Observable<LogMessage>;
|
||||
|
|
|
@ -17,21 +17,17 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import * as FunctionalTestRunner from '../../../../../src/functional_test_runner';
|
||||
import { FunctionalTestRunner } from '../../../../../src/functional_test_runner';
|
||||
import { CliError } from './run_cli';
|
||||
|
||||
function createFtr({ configPath, options: { log, bail, grep, updateBaselines, suiteTags } }) {
|
||||
return FunctionalTestRunner.createFunctionalTestRunner({
|
||||
log,
|
||||
configFile: configPath,
|
||||
configOverrides: {
|
||||
mochaOpts: {
|
||||
bail: !!bail,
|
||||
grep,
|
||||
},
|
||||
updateBaselines,
|
||||
suiteTags,
|
||||
return new FunctionalTestRunner(log, configPath, {
|
||||
mochaOpts: {
|
||||
bail: !!bail,
|
||||
grep,
|
||||
},
|
||||
updateBaselines,
|
||||
suiteTags,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,128 +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 { resolve } from 'path';
|
||||
|
||||
import { Command } from 'commander';
|
||||
|
||||
import { ToolingLog } from '@kbn/dev-utils';
|
||||
import { createFunctionalTestRunner } from './functional_test_runner';
|
||||
|
||||
const cmd = new Command('node scripts/functional_test_runner');
|
||||
const resolveConfigPath = v => resolve(process.cwd(), v);
|
||||
const defaultConfigPath = resolveConfigPath('test/functional/config.js');
|
||||
|
||||
const createMultiArgCollector = (map) => () => {
|
||||
const paths = [];
|
||||
return (arg) => {
|
||||
paths.push(map ? map(arg) : arg);
|
||||
return paths;
|
||||
};
|
||||
};
|
||||
|
||||
const collectExcludePaths = createMultiArgCollector(a => resolve(a));
|
||||
const collectIncludeTags = createMultiArgCollector();
|
||||
const collectExcludeTags = createMultiArgCollector();
|
||||
|
||||
cmd
|
||||
.option('--config [path]', 'Path to a config file', resolveConfigPath, defaultConfigPath)
|
||||
.option('--bail', 'stop tests after the first failure', false)
|
||||
.option('--grep <pattern>', 'pattern used to select which tests to run')
|
||||
.option('--invert', 'invert grep to exclude tests', false)
|
||||
.option('--exclude [file]', 'Path to a test file that should not be loaded', collectExcludePaths(), [])
|
||||
.option('--include-tag [tag]', 'A tag to be included, pass multiple times for multiple tags', collectIncludeTags(), [])
|
||||
.option('--exclude-tag [tag]', 'A tag to be excluded, pass multiple times for multiple tags', collectExcludeTags(), [])
|
||||
.option('--test-stats', 'Print the number of tests (included and excluded) to STDERR', false)
|
||||
.option('--verbose', 'Log everything', false)
|
||||
.option('--quiet', 'Only log errors', false)
|
||||
.option('--silent', 'Log nothing', false)
|
||||
.option('--updateBaselines', 'Replace baseline screenshots with whatever is generated from the test', false)
|
||||
.option('--debug', 'Run in debug mode', false)
|
||||
.parse(process.argv);
|
||||
|
||||
let logLevel = 'info';
|
||||
if (cmd.silent) logLevel = 'silent';
|
||||
if (cmd.quiet) logLevel = 'error';
|
||||
if (cmd.debug) logLevel = 'debug';
|
||||
if (cmd.verbose) logLevel = 'verbose';
|
||||
|
||||
const log = new ToolingLog({
|
||||
level: logLevel,
|
||||
writeTo: process.stdout
|
||||
});
|
||||
|
||||
const functionalTestRunner = createFunctionalTestRunner({
|
||||
log,
|
||||
configFile: cmd.config,
|
||||
configOverrides: {
|
||||
mochaOpts: {
|
||||
bail: cmd.bail,
|
||||
grep: cmd.grep,
|
||||
invert: cmd.invert,
|
||||
},
|
||||
suiteTags: {
|
||||
include: cmd.includeTag,
|
||||
exclude: cmd.excludeTag,
|
||||
},
|
||||
updateBaselines: cmd.updateBaselines,
|
||||
excludeTestFiles: cmd.exclude
|
||||
}
|
||||
});
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
if (cmd.testStats) {
|
||||
process.stderr.write(JSON.stringify(
|
||||
await functionalTestRunner.getTestStats(),
|
||||
null,
|
||||
2
|
||||
) + '\n');
|
||||
} else {
|
||||
const failureCount = await functionalTestRunner.run();
|
||||
process.exitCode = failureCount ? 1 : 0;
|
||||
}
|
||||
} catch (err) {
|
||||
await teardown(err);
|
||||
} finally {
|
||||
await teardown();
|
||||
}
|
||||
}
|
||||
|
||||
let teardownRun = false;
|
||||
async function teardown(err) {
|
||||
if (teardownRun) return;
|
||||
|
||||
teardownRun = true;
|
||||
if (err) {
|
||||
log.indent(-log.indent());
|
||||
log.error(err);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
try {
|
||||
await functionalTestRunner.close();
|
||||
} finally {
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
process.on('unhandledRejection', err => teardown(err));
|
||||
process.on('SIGTERM', () => teardown());
|
||||
process.on('SIGINT', () => teardown());
|
||||
run();
|
106
src/functional_test_runner/cli.ts
Normal file
106
src/functional_test_runner/cli.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* 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 { resolve } from 'path';
|
||||
|
||||
import { run } from '../dev/run';
|
||||
import { FunctionalTestRunner } from './functional_test_runner';
|
||||
|
||||
run(
|
||||
async ({ flags, log }) => {
|
||||
const resolveConfigPath = (v: string) => resolve(process.cwd(), v);
|
||||
const toArray = (v: string | string[]) => ([] as string[]).concat(v || []);
|
||||
|
||||
const functionalTestRunner = new FunctionalTestRunner(
|
||||
log,
|
||||
resolveConfigPath(flags.config as string),
|
||||
{
|
||||
mochaOpts: {
|
||||
bail: flags.bail,
|
||||
grep: flags.grep || undefined,
|
||||
invert: flags.invert,
|
||||
},
|
||||
suiteTags: {
|
||||
include: toArray(flags['include-tag'] as string | string[]),
|
||||
exclude: toArray(flags['exclude-tag'] as string | string[]),
|
||||
},
|
||||
updateBaselines: flags.updateBaselines,
|
||||
excludeTestFiles: flags.exclude || undefined,
|
||||
}
|
||||
);
|
||||
|
||||
let teardownRun = false;
|
||||
const teardown = async (err?: Error) => {
|
||||
if (teardownRun) return;
|
||||
|
||||
teardownRun = true;
|
||||
if (err) {
|
||||
log.indent(-log.indent());
|
||||
log.error(err);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
try {
|
||||
await functionalTestRunner.close();
|
||||
} finally {
|
||||
process.exit();
|
||||
}
|
||||
};
|
||||
|
||||
process.on('unhandledRejection', err => teardown(err));
|
||||
process.on('SIGTERM', () => teardown());
|
||||
process.on('SIGINT', () => teardown());
|
||||
|
||||
try {
|
||||
if (flags['test-stats']) {
|
||||
process.stderr.write(
|
||||
JSON.stringify(await functionalTestRunner.getTestStats(), null, 2) + '\n'
|
||||
);
|
||||
} else {
|
||||
const failureCount = await functionalTestRunner.run();
|
||||
process.exitCode = failureCount ? 1 : 0;
|
||||
}
|
||||
} catch (err) {
|
||||
await teardown(err);
|
||||
} finally {
|
||||
await teardown();
|
||||
}
|
||||
},
|
||||
{
|
||||
flags: {
|
||||
string: ['config', 'grep', 'exclude', 'include-tag', 'exclude-tag'],
|
||||
boolean: ['bail', 'invert', 'test-stats', 'updateBaselines'],
|
||||
default: {
|
||||
config: 'test/functional/config.js',
|
||||
debug: true,
|
||||
},
|
||||
help: `
|
||||
--config=path path to a config file
|
||||
--bail stop tests after the first failure
|
||||
--grep <pattern> pattern used to select which tests to run
|
||||
--invert invert grep to exclude tests
|
||||
--exclude=file path to a test file that should not be loaded
|
||||
--include-tag=tag a tag to be included, pass multiple times for multiple tags
|
||||
--exclude-tag=tag a tag to be excluded, pass multiple times for multiple tags
|
||||
--test-stats print the number of tests (included and excluded) to STDERR
|
||||
--updateBaselines replace baseline screenshots with whatever is generated from the test
|
||||
`,
|
||||
},
|
||||
}
|
||||
);
|
44
src/functional_test_runner/fake_mocha_types.d.ts
vendored
Normal file
44
src/functional_test_runner/fake_mocha_types.d.ts
vendored
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The real mocha types conflict with the global jest types, because
|
||||
* globals are terrible. So instead of using any for everything this
|
||||
* tries to mock out simple versions of the Mocha types
|
||||
*/
|
||||
|
||||
import EventEmitter from 'events';
|
||||
|
||||
export interface Suite {
|
||||
suites: Suite[];
|
||||
tests: Test[];
|
||||
}
|
||||
|
||||
export interface Test {
|
||||
fullTitle(): string;
|
||||
}
|
||||
|
||||
export interface Runner extends EventEmitter {
|
||||
abort(): void;
|
||||
failures: any[];
|
||||
}
|
||||
|
||||
export interface Mocha {
|
||||
run(cb: () => void): Runner;
|
||||
}
|
|
@ -1,146 +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 {
|
||||
createLifecycle,
|
||||
readConfigFile,
|
||||
ProviderCollection,
|
||||
readProviderSpec,
|
||||
setupMocha,
|
||||
runTests,
|
||||
} from './lib';
|
||||
|
||||
export function createFunctionalTestRunner({ log, configFile, configOverrides }) {
|
||||
const lifecycle = createLifecycle();
|
||||
|
||||
lifecycle.on('phaseStart', name => {
|
||||
log.verbose('starting %j lifecycle phase', name);
|
||||
});
|
||||
|
||||
lifecycle.on('phaseEnd', name => {
|
||||
log.verbose('ending %j lifecycle phase', name);
|
||||
});
|
||||
|
||||
|
||||
class FunctionalTestRunner {
|
||||
async run() {
|
||||
return await this._run(async (config, coreProviders) => {
|
||||
const providers = new ProviderCollection(log, [
|
||||
...coreProviders,
|
||||
...readProviderSpec('Service', config.get('services')),
|
||||
...readProviderSpec('PageObject', config.get('pageObjects'))
|
||||
]);
|
||||
|
||||
await providers.loadAll();
|
||||
|
||||
const mocha = await setupMocha(lifecycle, log, config, providers);
|
||||
await lifecycle.trigger('beforeTests');
|
||||
log.info('Starting tests');
|
||||
|
||||
return await runTests(lifecycle, log, mocha);
|
||||
});
|
||||
}
|
||||
|
||||
async getTestStats() {
|
||||
return await this._run(async (config, coreProviders) => {
|
||||
// replace the function of custom service providers so that they return
|
||||
// promise-like objects which never resolve, essentially disabling them
|
||||
// allowing us to load the test files and populate the mocha suites
|
||||
const stubProvider = provider => (
|
||||
coreProviders.includes(provider)
|
||||
? provider
|
||||
: {
|
||||
...provider,
|
||||
fn: () => ({
|
||||
then: () => {}
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
const providers = new ProviderCollection(log, [
|
||||
...coreProviders,
|
||||
...readProviderSpec('Service', config.get('services')),
|
||||
...readProviderSpec('PageObject', config.get('pageObjects'))
|
||||
].map(stubProvider));
|
||||
|
||||
const mocha = await setupMocha(lifecycle, log, config, providers);
|
||||
|
||||
const countTests = suite => (
|
||||
suite.suites.reduce(
|
||||
(sum, suite) => sum + countTests(suite),
|
||||
suite.tests.length
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
testCount: countTests(mocha.suite),
|
||||
excludedTests: mocha.excludedTests.map(t => t.fullTitle())
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async _run(handler) {
|
||||
let runErrorOccurred = false;
|
||||
|
||||
try {
|
||||
const config = await readConfigFile(log, configFile, configOverrides);
|
||||
log.info('Config loaded');
|
||||
|
||||
if (config.get('testFiles').length === 0) {
|
||||
log.warning('No test files defined.');
|
||||
return;
|
||||
}
|
||||
|
||||
// base level services that functional_test_runner exposes
|
||||
const coreProviders = readProviderSpec('Service', {
|
||||
lifecycle: () => lifecycle,
|
||||
log: () => log,
|
||||
config: () => config,
|
||||
});
|
||||
|
||||
return await handler(config, coreProviders);
|
||||
} catch (runError) {
|
||||
runErrorOccurred = true;
|
||||
throw runError;
|
||||
|
||||
} finally {
|
||||
try {
|
||||
await this.close();
|
||||
|
||||
} catch (closeError) {
|
||||
if (runErrorOccurred) {
|
||||
log.error('failed to close functional_test_runner');
|
||||
log.error(closeError);
|
||||
} else {
|
||||
throw closeError;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (this._closed) return;
|
||||
|
||||
this._closed = true;
|
||||
await lifecycle.trigger('cleanup');
|
||||
}
|
||||
}
|
||||
|
||||
return new FunctionalTestRunner();
|
||||
}
|
145
src/functional_test_runner/functional_test_runner.ts
Normal file
145
src/functional_test_runner/functional_test_runner.ts
Normal file
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* 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 { ToolingLog } from '@kbn/dev-utils';
|
||||
import { Suite, Test } from './fake_mocha_types';
|
||||
|
||||
import {
|
||||
createLifecycle,
|
||||
readConfigFile,
|
||||
ProviderCollection,
|
||||
readProviderSpec,
|
||||
setupMocha,
|
||||
runTests,
|
||||
Config,
|
||||
} from './lib';
|
||||
|
||||
export class FunctionalTestRunner {
|
||||
public readonly lifecycle = createLifecycle();
|
||||
private closed = false;
|
||||
|
||||
constructor(
|
||||
private readonly log: ToolingLog,
|
||||
private readonly configFile: string,
|
||||
private readonly configOverrides: any
|
||||
) {
|
||||
this.lifecycle.on('phaseStart', name => {
|
||||
log.verbose('starting %j lifecycle phase', name);
|
||||
});
|
||||
|
||||
this.lifecycle.on('phaseEnd', name => {
|
||||
log.verbose('ending %j lifecycle phase', name);
|
||||
});
|
||||
}
|
||||
|
||||
async run() {
|
||||
return await this._run(async (config, coreProviders) => {
|
||||
const providers = new ProviderCollection(this.log, [
|
||||
...coreProviders,
|
||||
...readProviderSpec('Service', config.get('services')),
|
||||
...readProviderSpec('PageObject', config.get('pageObjects')),
|
||||
]);
|
||||
|
||||
await providers.loadAll();
|
||||
|
||||
const mocha = await setupMocha(this.lifecycle, this.log, config, providers);
|
||||
await this.lifecycle.trigger('beforeTests');
|
||||
this.log.info('Starting tests');
|
||||
|
||||
return await runTests(this.lifecycle, mocha);
|
||||
});
|
||||
}
|
||||
|
||||
async getTestStats() {
|
||||
return await this._run(async (config, coreProviders) => {
|
||||
// replace the function of custom service providers so that they return
|
||||
// promise-like objects which never resolve, essentially disabling them
|
||||
// allowing us to load the test files and populate the mocha suites
|
||||
const readStubbedProviderSpec = (type: string, providers: any) =>
|
||||
readProviderSpec(type, providers).map(p => ({
|
||||
...p,
|
||||
fn: () => ({
|
||||
then: () => {},
|
||||
}),
|
||||
}));
|
||||
|
||||
const providers = new ProviderCollection(this.log, [
|
||||
...coreProviders,
|
||||
...readStubbedProviderSpec('Service', config.get('services')),
|
||||
...readStubbedProviderSpec('PageObject', config.get('pageObjects')),
|
||||
]);
|
||||
|
||||
const mocha = await setupMocha(this.lifecycle, this.log, config, providers);
|
||||
|
||||
const countTests = (suite: Suite): number =>
|
||||
suite.suites.reduce((sum, s) => sum + countTests(s), suite.tests.length);
|
||||
|
||||
return {
|
||||
testCount: countTests(mocha.suite),
|
||||
excludedTests: mocha.excludedTests.map((t: Test) => t.fullTitle()),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async _run<T = any>(
|
||||
handler: (config: Config, coreProvider: ReturnType<typeof readProviderSpec>) => Promise<T>
|
||||
): Promise<T> {
|
||||
let runErrorOccurred = false;
|
||||
|
||||
try {
|
||||
const config = await readConfigFile(this.log, this.configFile, this.configOverrides);
|
||||
this.log.info('Config loaded');
|
||||
|
||||
if (config.get('testFiles').length === 0) {
|
||||
throw new Error('No test files defined.');
|
||||
}
|
||||
|
||||
// base level services that functional_test_runner exposes
|
||||
const coreProviders = readProviderSpec('Service', {
|
||||
lifecycle: () => this.lifecycle,
|
||||
log: () => this.log,
|
||||
config: () => config,
|
||||
});
|
||||
|
||||
return await handler(config, coreProviders);
|
||||
} catch (runError) {
|
||||
runErrorOccurred = true;
|
||||
throw runError;
|
||||
} finally {
|
||||
try {
|
||||
await this.close();
|
||||
} catch (closeError) {
|
||||
if (runErrorOccurred) {
|
||||
this.log.error('failed to close functional_test_runner');
|
||||
this.log.error(closeError);
|
||||
} else {
|
||||
// eslint-disable-next-line no-unsafe-finally
|
||||
throw closeError;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (this.closed) return;
|
||||
|
||||
this.closed = true;
|
||||
await this.lifecycle.trigger('cleanup');
|
||||
}
|
||||
}
|
|
@ -17,5 +17,5 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { createFunctionalTestRunner } from './functional_test_runner';
|
||||
export { FunctionalTestRunner } from './functional_test_runner';
|
||||
export { readConfigFile } from './lib';
|
|
@ -17,4 +17,5 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { Config } from './config';
|
||||
export { readConfigFile } from './read_config_file';
|
|
@ -17,6 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { ToolingLog } from '@kbn/dev-utils';
|
||||
import { defaultsDeep } from 'lodash';
|
||||
|
||||
import { Config } from './config';
|
||||
|
@ -24,37 +25,34 @@ import { transformDeprecations } from './transform_deprecations';
|
|||
|
||||
const cache = new WeakMap();
|
||||
|
||||
async function getSettingsFromFile(log, path, settingOverrides) {
|
||||
const configModule = require(path); // eslint-disable-line import/no-dynamic-require
|
||||
const configProvider = configModule.__esModule
|
||||
? configModule.default
|
||||
: configModule;
|
||||
async function getSettingsFromFile(log: ToolingLog, path: string, settingOverrides: any) {
|
||||
const configModule = require(path); // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
const configProvider = configModule.__esModule ? configModule.default : configModule;
|
||||
|
||||
if (!cache.has(configProvider)) {
|
||||
log.debug('Loading config file from %j', path);
|
||||
cache.set(configProvider, configProvider({
|
||||
log,
|
||||
async readConfigFile(...args) {
|
||||
return new Config({
|
||||
settings: await getSettingsFromFile(log, ...args),
|
||||
primary: false,
|
||||
path,
|
||||
});
|
||||
}
|
||||
}));
|
||||
cache.set(
|
||||
configProvider,
|
||||
configProvider({
|
||||
log,
|
||||
async readConfigFile(p: string, o: any) {
|
||||
return new Config({
|
||||
settings: await getSettingsFromFile(log, p, o),
|
||||
primary: false,
|
||||
path: p,
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const settingsWithDefaults = defaultsDeep(
|
||||
{},
|
||||
settingOverrides,
|
||||
await cache.get(configProvider)
|
||||
);
|
||||
const settingsWithDefaults = defaultsDeep({}, settingOverrides, await cache.get(configProvider)!);
|
||||
|
||||
const logDeprecation = (...args) => log.error(...args);
|
||||
const logDeprecation = (error: string | Error) => log.error(error);
|
||||
return transformDeprecations(settingsWithDefaults, logDeprecation);
|
||||
}
|
||||
|
||||
export async function readConfigFile(log, path, settingOverrides) {
|
||||
export async function readConfigFile(log: ToolingLog, path: string, settingOverrides: any) {
|
||||
return new Config({
|
||||
settings: await getSettingsFromFile(log, path, settingOverrides),
|
||||
primary: true,
|
|
@ -17,8 +17,16 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import { createTransform, Deprecations } from '../../../legacy/deprecation';
|
||||
|
||||
export const transformDeprecations = createTransform([
|
||||
Deprecations.unused('servers.webdriver')
|
||||
type DeprecationTransformer = (
|
||||
settings: object,
|
||||
log: (msg: string) => void
|
||||
) => {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export const transformDeprecations: DeprecationTransformer = createTransform([
|
||||
Deprecations.unused('servers.webdriver'),
|
||||
]);
|
|
@ -1,23 +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.
|
||||
*/
|
||||
|
||||
export { createLifecycle } from './lifecycle';
|
||||
export { readConfigFile } from './config';
|
||||
export { setupMocha, runTests } from './mocha';
|
||||
export { readProviderSpec, ProviderCollection } from './providers';
|
|
@ -17,5 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { Config } from './config/config';
|
||||
export { Lifecycle } from './lifecycle';
|
||||
export { createLifecycle, Lifecycle } from './lifecycle';
|
||||
export { readConfigFile, Config } from './config';
|
||||
export { readProviderSpec, ProviderCollection, Provider } from './providers';
|
||||
export { runTests, setupMocha } from './mocha';
|
|
@ -17,7 +17,8 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
const globalLoadPath = [];
|
||||
const globalLoadPath: Array<{ ident: string; description: string }> = [];
|
||||
|
||||
function getPath(startAt = 0) {
|
||||
return globalLoadPath
|
||||
.slice(startAt)
|
||||
|
@ -25,9 +26,15 @@ function getPath(startAt = 0) {
|
|||
.join(' -> ');
|
||||
}
|
||||
|
||||
function addPathToMessage(message, startAt) {
|
||||
const errorsFromLoadTracer = new WeakSet();
|
||||
|
||||
function addPathToMessage(message: string, startAt?: number) {
|
||||
const path = getPath(startAt);
|
||||
if (!path) return message;
|
||||
|
||||
if (!path) {
|
||||
return message;
|
||||
}
|
||||
|
||||
return `${message} -- from ${path}`;
|
||||
}
|
||||
|
||||
|
@ -41,7 +48,7 @@ function addPathToMessage(message, startAt) {
|
|||
* @param {Function} load function that executes this step
|
||||
* @return {Any} the value produced by load()
|
||||
*/
|
||||
export function loadTracer(ident, description, load) {
|
||||
export function loadTracer(ident: any, description: string, load: () => Promise<void> | void) {
|
||||
const isCircular = globalLoadPath.find(step => step.ident === ident);
|
||||
if (isCircular) {
|
||||
throw new Error(addPathToMessage(`Circular reference to "${description}"`));
|
||||
|
@ -51,13 +58,13 @@ export function loadTracer(ident, description, load) {
|
|||
globalLoadPath.unshift({ ident, description });
|
||||
return load();
|
||||
} catch (err) {
|
||||
if (err.__fromLoadTracer) {
|
||||
if (errorsFromLoadTracer.has(err)) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
const wrapped = new Error(addPathToMessage(`Failure to load ${description}`, 1));
|
||||
wrapped.stack = `${wrapped.message}\n\n Original Error: ${err.stack}`;
|
||||
wrapped.__fromLoadTracer = true;
|
||||
errorsFromLoadTracer.add(wrapped);
|
||||
throw wrapped;
|
||||
} finally {
|
||||
globalLoadPath.shift();
|
|
@ -17,5 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
// @ts-ignore will be replaced shortly
|
||||
export { setupMocha } from './setup_mocha';
|
||||
export { runTests } from './run_tests';
|
|
@ -17,6 +17,9 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { Lifecycle } from '../lifecycle';
|
||||
import { Mocha } from '../../fake_mocha_types';
|
||||
|
||||
/**
|
||||
* Run the tests that have already been loaded into
|
||||
* mocha. aborts tests on 'cleanup' lifecycle runs
|
||||
|
@ -26,7 +29,7 @@
|
|||
* @param {Mocha} mocha
|
||||
* @return {Promise<Number>} resolves to the number of test failures
|
||||
*/
|
||||
export async function runTests(lifecycle, log, mocha) {
|
||||
export async function runTests(lifecycle: Lifecycle, mocha: Mocha) {
|
||||
let runComplete = false;
|
||||
const runner = mocha.run(() => {
|
||||
runComplete = true;
|
||||
|
@ -36,7 +39,7 @@ export async function runTests(lifecycle, log, mocha) {
|
|||
if (!runComplete) runner.abort();
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
const respond = () => resolve(runner.failures);
|
||||
|
||||
// if there are no tests, mocha.run() is sync
|
|
@ -20,22 +20,29 @@
|
|||
const INITIALIZING = Symbol('async instance initializing');
|
||||
const asyncInitFns = new WeakSet();
|
||||
|
||||
export const isAsyncInstance = val => (
|
||||
val && asyncInitFns.has(val.init)
|
||||
);
|
||||
type AsyncInstance<T> = {
|
||||
init: () => Promise<T>;
|
||||
} & T;
|
||||
|
||||
export const createAsyncInstance = (type, name, promiseForValue) => {
|
||||
let instance = INITIALIZING;
|
||||
export const isAsyncInstance = <T = unknown>(val: any): val is AsyncInstance<T> =>
|
||||
val && asyncInitFns.has(val.init);
|
||||
|
||||
const initPromise = promiseForValue.then(v => instance = v);
|
||||
export const createAsyncInstance = <T>(
|
||||
type: string,
|
||||
name: string,
|
||||
promiseForValue: Promise<T>
|
||||
): AsyncInstance<T> => {
|
||||
let instance: T | symbol = INITIALIZING;
|
||||
|
||||
const initPromise = promiseForValue.then(v => (instance = v));
|
||||
const loadingTarget = {
|
||||
init() {
|
||||
return initPromise;
|
||||
}
|
||||
},
|
||||
};
|
||||
asyncInitFns.add(loadingTarget.init);
|
||||
|
||||
const assertReady = desc => {
|
||||
const assertReady = (desc: string) => {
|
||||
if (instance === INITIALIZING) {
|
||||
throw new Error(`
|
||||
${type} \`${desc}\` is loaded asynchronously but isn't available yet. Either await the
|
||||
|
@ -52,81 +59,93 @@ export const createAsyncInstance = (type, name, promiseForValue) => {
|
|||
};
|
||||
|
||||
return new Proxy(loadingTarget, {
|
||||
apply(target, context, args) {
|
||||
apply(_, context, args) {
|
||||
assertReady(`${name}()`);
|
||||
return Reflect.apply(instance, context, args);
|
||||
return Reflect.apply(instance as any, context, args);
|
||||
},
|
||||
|
||||
construct(target, args, newTarget) {
|
||||
construct(_, args, newTarget) {
|
||||
assertReady(`new ${name}()`);
|
||||
return Reflect.construct(instance, args, newTarget);
|
||||
return Reflect.construct(instance as any, args, newTarget);
|
||||
},
|
||||
|
||||
defineProperty(target, prop, descriptor) {
|
||||
assertReady(`${name}.${prop}`);
|
||||
return Reflect.defineProperty(instance, prop, descriptor);
|
||||
defineProperty(_, prop, descriptor) {
|
||||
if (typeof prop !== 'symbol') {
|
||||
assertReady(`${name}.${prop}`);
|
||||
}
|
||||
return Reflect.defineProperty(instance as any, prop, descriptor);
|
||||
},
|
||||
|
||||
deleteProperty(target, prop) {
|
||||
assertReady(`${name}.${prop}`);
|
||||
return Reflect.deleteProperty(instance, prop);
|
||||
deleteProperty(_, prop) {
|
||||
if (typeof prop !== 'symbol') {
|
||||
assertReady(`${name}.${prop}`);
|
||||
}
|
||||
return Reflect.deleteProperty(instance as any, prop);
|
||||
},
|
||||
|
||||
get(target, prop, receiver) {
|
||||
get(_, prop, receiver) {
|
||||
if (loadingTarget.hasOwnProperty(prop)) {
|
||||
return Reflect.get(loadingTarget, prop, receiver);
|
||||
return Reflect.get(loadingTarget as any, prop, receiver);
|
||||
}
|
||||
|
||||
assertReady(`${name}.${prop}`);
|
||||
return Reflect.get(instance, prop, receiver);
|
||||
if (typeof prop !== 'symbol') {
|
||||
assertReady(`${name}.${prop}`);
|
||||
}
|
||||
return Reflect.get(instance as any, prop, receiver);
|
||||
},
|
||||
|
||||
getOwnPropertyDescriptor(target, prop) {
|
||||
getOwnPropertyDescriptor(_, prop) {
|
||||
if (loadingTarget.hasOwnProperty(prop)) {
|
||||
return Reflect.getOwnPropertyDescriptor(loadingTarget, prop);
|
||||
}
|
||||
|
||||
assertReady(`${name}.${prop}`);
|
||||
return Reflect.getOwnPropertyDescriptor(instance, prop);
|
||||
if (typeof prop !== 'symbol') {
|
||||
assertReady(`${name}.${prop}`);
|
||||
}
|
||||
return Reflect.getOwnPropertyDescriptor(instance as any, prop);
|
||||
},
|
||||
|
||||
getPrototypeOf() {
|
||||
assertReady(`${name}`);
|
||||
return Reflect.getPrototypeOf(instance);
|
||||
return Reflect.getPrototypeOf(instance as any);
|
||||
},
|
||||
|
||||
has(target, prop) {
|
||||
has(_, prop) {
|
||||
if (!loadingTarget.hasOwnProperty(prop)) {
|
||||
return Reflect.has(loadingTarget, prop);
|
||||
}
|
||||
|
||||
assertReady(`${name}.${prop}`);
|
||||
return Reflect.has(instance, prop);
|
||||
if (typeof prop !== 'symbol') {
|
||||
assertReady(`${name}.${prop}`);
|
||||
}
|
||||
return Reflect.has(instance as any, prop);
|
||||
},
|
||||
|
||||
isExtensible() {
|
||||
assertReady(`${name}`);
|
||||
return Reflect.isExtensible(instance);
|
||||
return Reflect.isExtensible(instance as any);
|
||||
},
|
||||
|
||||
ownKeys() {
|
||||
assertReady(`${name}`);
|
||||
return Reflect.ownKeys(instance);
|
||||
return Reflect.ownKeys(instance as any);
|
||||
},
|
||||
|
||||
preventExtensions() {
|
||||
assertReady(`${name}`);
|
||||
return Reflect.preventExtensions(instance);
|
||||
return Reflect.preventExtensions(instance as any);
|
||||
},
|
||||
|
||||
set(target, prop, value, receiver) {
|
||||
assertReady(`${name}.${prop}`);
|
||||
return Reflect.set(instance, prop, value, receiver);
|
||||
set(_, prop, value, receiver) {
|
||||
if (typeof prop !== 'symbol') {
|
||||
assertReady(`${name}.${prop}`);
|
||||
}
|
||||
return Reflect.set(instance as any, prop, value, receiver);
|
||||
},
|
||||
|
||||
setPrototypeOf(target, prototype) {
|
||||
setPrototypeOf(_, prototype) {
|
||||
assertReady(`${name}`);
|
||||
return Reflect.setPrototypeOf(instance, prototype);
|
||||
}
|
||||
});
|
||||
return Reflect.setPrototypeOf(instance as any, prototype);
|
||||
},
|
||||
}) as AsyncInstance<T>;
|
||||
};
|
|
@ -18,4 +18,4 @@
|
|||
*/
|
||||
|
||||
export { ProviderCollection } from './provider_collection';
|
||||
export { readProviderSpec } from './read_provider_spec';
|
||||
export { Provider, readProviderSpec } from './read_provider_spec';
|
|
@ -17,52 +17,47 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { ToolingLog } from '@kbn/dev-utils';
|
||||
|
||||
import { loadTracer } from '../load_tracer';
|
||||
import { createAsyncInstance, isAsyncInstance } from './async_instance';
|
||||
import { Providers } from './read_provider_spec';
|
||||
import { createVerboseInstance } from './verbose_instance';
|
||||
|
||||
export class ProviderCollection {
|
||||
constructor(log, providers) {
|
||||
this._log = log;
|
||||
this._instances = new Map();
|
||||
this._providers = providers;
|
||||
}
|
||||
private readonly instances = new Map();
|
||||
|
||||
getService = name => (
|
||||
this._getInstance('Service', name)
|
||||
)
|
||||
constructor(private readonly log: ToolingLog, private readonly providers: Providers) {}
|
||||
|
||||
hasService = name => (
|
||||
Boolean(this._findProvider('Service', name))
|
||||
)
|
||||
public getService = (name: string) => this.getInstance('Service', name);
|
||||
|
||||
getPageObject = name => (
|
||||
this._getInstance('PageObject', name)
|
||||
)
|
||||
public hasService = (name: string) => Boolean(this.findProvider('Service', name));
|
||||
|
||||
getPageObjects = names => {
|
||||
const pageObjects = {};
|
||||
names.forEach(name => pageObjects[name] = this.getPageObject(name));
|
||||
public getPageObject = (name: string) => this.getInstance('PageObject', name);
|
||||
|
||||
public getPageObjects = (names: string[]) => {
|
||||
const pageObjects: Record<string, any> = {};
|
||||
names.forEach(name => (pageObjects[name] = this.getPageObject(name)));
|
||||
return pageObjects;
|
||||
};
|
||||
|
||||
public loadExternalService(name: string, provider: (...args: any) => any) {
|
||||
return this.getInstance('Service', name, provider);
|
||||
}
|
||||
|
||||
loadExternalService(name, provider) {
|
||||
return this._getInstance('Service', name, provider);
|
||||
}
|
||||
|
||||
async loadAll() {
|
||||
public async loadAll() {
|
||||
const asyncInitFailures = [];
|
||||
|
||||
await Promise.all(
|
||||
this._providers.map(async ({ type, name }) => {
|
||||
this.providers.map(async ({ type, name }) => {
|
||||
try {
|
||||
const instance = this._getInstance(type, name);
|
||||
const instance = this.getInstance(type, name);
|
||||
if (isAsyncInstance(instance)) {
|
||||
await instance.init();
|
||||
}
|
||||
} catch (err) {
|
||||
this._log.warning('Failure loading service %j', name);
|
||||
this._log.error(err);
|
||||
this.log.warning('Failure loading service %j', name);
|
||||
this.log.error(err);
|
||||
asyncInitFailures.push(name);
|
||||
}
|
||||
})
|
||||
|
@ -73,20 +68,20 @@ export class ProviderCollection {
|
|||
}
|
||||
}
|
||||
|
||||
_findProvider(type, name) {
|
||||
return this._providers.find(p => p.type === type && p.name === name);
|
||||
private findProvider(type: string, name: string) {
|
||||
return this.providers.find(p => p.type === type && p.name === name);
|
||||
}
|
||||
|
||||
_getProvider(type, name) {
|
||||
const providerDef = this._findProvider(type, name);
|
||||
private getProvider(type: string, name: string) {
|
||||
const providerDef = this.findProvider(type, name);
|
||||
if (!providerDef) {
|
||||
throw new Error(`Unknown ${type} "${name}"`);
|
||||
}
|
||||
return providerDef.fn;
|
||||
}
|
||||
|
||||
_getInstance(type, name, provider = this._getProvider(type, name)) {
|
||||
const instances = this._instances;
|
||||
private getInstance(type: string, name: string, provider = this.getProvider(type, name)) {
|
||||
const instances = this.instances;
|
||||
|
||||
return loadTracer(provider, `${type}(${name})`, () => {
|
||||
if (!provider) {
|
||||
|
@ -105,9 +100,15 @@ export class ProviderCollection {
|
|||
instance = createAsyncInstance(type, name, instance);
|
||||
}
|
||||
|
||||
if (name !== '__webdriver__' && name !== 'log' && name !== 'config' && instance && typeof instance === 'object') {
|
||||
if (
|
||||
name !== '__webdriver__' &&
|
||||
name !== 'log' &&
|
||||
name !== 'config' &&
|
||||
instance &&
|
||||
typeof instance === 'object'
|
||||
) {
|
||||
instance = createVerboseInstance(
|
||||
this._log,
|
||||
this.log,
|
||||
type === 'PageObject' ? `PageObjects.${name}` : name,
|
||||
instance
|
||||
);
|
|
@ -17,7 +17,10 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export function readProviderSpec(type, providers) {
|
||||
export type Providers = ReturnType<typeof readProviderSpec>;
|
||||
export type Provider = Providers extends Array<infer X> ? X : unknown;
|
||||
|
||||
export function readProviderSpec(type: string, providers: Record<string, (...args: any[]) => any>) {
|
||||
return Object.keys(providers).map(name => {
|
||||
return {
|
||||
type,
|
|
@ -19,45 +19,54 @@
|
|||
|
||||
import { inspect } from 'util';
|
||||
|
||||
function printArgs(args) {
|
||||
return args.map((arg) => {
|
||||
if (typeof arg === 'string' || typeof arg === 'number' || arg instanceof Date) {
|
||||
return inspect(arg);
|
||||
}
|
||||
import { ToolingLog } from '@kbn/dev-utils';
|
||||
|
||||
if (Array.isArray(arg)) {
|
||||
return `[${printArgs(arg)}]`;
|
||||
}
|
||||
function printArgs(args: any[]): string {
|
||||
return args
|
||||
.map(arg => {
|
||||
if (typeof arg === 'string' || typeof arg === 'number' || arg instanceof Date) {
|
||||
return inspect(arg);
|
||||
}
|
||||
|
||||
return Object.prototype.toString.call(arg);
|
||||
}).join(', ');
|
||||
if (Array.isArray(arg)) {
|
||||
return `[${printArgs(arg)}]`;
|
||||
}
|
||||
|
||||
return Object.prototype.toString.call(arg);
|
||||
})
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
export function createVerboseInstance(log, name, instance) {
|
||||
if (!log.getWriters().some(l => l.level.flags.verbose)) {
|
||||
export function createVerboseInstance(
|
||||
log: ToolingLog,
|
||||
name: string,
|
||||
instance: { [k: string]: any; [i: number]: any }
|
||||
) {
|
||||
if (!log.getWriters().some(l => (l as any).level.flags.verbose)) {
|
||||
return instance;
|
||||
}
|
||||
|
||||
return new Proxy(instance, {
|
||||
get(_, prop) {
|
||||
const value = instance[prop];
|
||||
const value = (instance as any)[prop];
|
||||
|
||||
if (typeof value !== 'function' || prop === 'init') {
|
||||
if (typeof value !== 'function' || prop === 'init' || typeof prop === 'symbol') {
|
||||
return value;
|
||||
}
|
||||
|
||||
return function (...args) {
|
||||
return function(this: any, ...args: any[]) {
|
||||
log.verbose(`${name}.${prop}(${printArgs(args)})`);
|
||||
log.indent(2);
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = {
|
||||
returned: value.apply(this, args)
|
||||
returned: value.apply(this, args),
|
||||
};
|
||||
} catch (error) {
|
||||
result = {
|
||||
thrown: error
|
||||
returned: undefined,
|
||||
thrown: error,
|
||||
};
|
||||
}
|
||||
|
|
@ -39,7 +39,6 @@ export default async function ({ readConfigFile }) {
|
|||
esArchiver: {
|
||||
directory: path.resolve(__dirname, '../es_archives')
|
||||
},
|
||||
screenshots: functionalConfig.get('screenshots'),
|
||||
snapshots: {
|
||||
directory: path.resolve(__dirname, 'snapshots'),
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"extends": "../../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target",
|
||||
"skipLibCheck": true
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"extends": "../../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target",
|
||||
"skipLibCheck": true
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"extends": "../../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target",
|
||||
"skipLibCheck": true
|
||||
|
@ -11,4 +11,4 @@
|
|||
"../../../../typings/**/*",
|
||||
],
|
||||
"exclude": []
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
"types": [
|
||||
"node",
|
||||
"mocha"
|
||||
],
|
||||
"lib": [
|
||||
"esnext"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue