mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Reporting] Remove Phantom (#27142)
* remove some phantom stuff and tests * remove phantom * remove phantom * remove phantom * todo comments * remove from yarn.lock * edit fix * use constant in init * readme edit * update migration guide * remove refs to non-existing docs
This commit is contained in:
parent
241949b350
commit
237e446ba3
31 changed files with 52 additions and 775 deletions
|
@ -23,6 +23,30 @@ Kibana 7.0 will only use the Node.js distribution included in the package.
|
|||
|
||||
*Impact:* There is no expected impact unless Kibana is installed in a non-standard way.
|
||||
|
||||
[float]
|
||||
=== Removed support for using PhantomJS browser for screenshots in Reporting
|
||||
*Details:* Since the first release of Kibana Reporting, PhantomJS was used as
|
||||
the headless browser to capture screenshots of Kibana dashboards and
|
||||
visualizations. In that short time, Chromium has started offering a new
|
||||
headless browser library and the PhantomJS maintainers abandoned their project.
|
||||
We started planning for a transition in 6.5.0, when we made Chromium the
|
||||
default option, but allowed users to continue using Phantom with the
|
||||
`xpack.reporting.capture.browser.type: phantom` setting. In 7.0, that setting
|
||||
will still exist for compatibility, but the only valid option will be
|
||||
`chromium`.
|
||||
|
||||
*Impact:* Before upgrading to 7.0, if you have `xpack.reporting.capture.browser.type`
|
||||
set in kibana.yml, make sure it is set to `chromium`.
|
||||
|
||||
[NOTE]
|
||||
============
|
||||
Reporting 7.0 uses a version of the Chromium headless browser that RHEL 6,
|
||||
CentOS 6.x, and other old versions of Linux derived from RHEL 6. This change
|
||||
effectively removes RHEL 6 OS server support from Kibana Reporting. Users with
|
||||
RHEL 6 must upgrade to RHEL 7 to use Kibana Reporting starting with version
|
||||
7.0.0 of the Elastic stack.
|
||||
============
|
||||
|
||||
[float]
|
||||
=== Advanced setting query:queryString:options no longer applies to filters
|
||||
*Details:* In previous versions of Kibana the Advanced Setting `query:queryString:options` was applied to both queries
|
||||
|
|
|
@ -22,8 +22,9 @@ There is currently a known limitation with the Data Table visualization that onl
|
|||
|
||||
[float]
|
||||
==== `You must install fontconfig and freetype for Reporting to work'`
|
||||
Reporting using PhantomJS, the default browser, relies on system packages. Install the appropriate fontconfig and freetype
|
||||
packages for your distribution.
|
||||
Reporting uses a headless browser on the Kibana server, which relies on some
|
||||
system packages. Install the appropriate fontconfig and freetype packages for
|
||||
your distribution.
|
||||
|
||||
[float]
|
||||
==== `Max attempts reached (3)`
|
||||
|
@ -54,11 +55,6 @@ the CAP_SYS_ADMIN capability.
|
|||
Elastic recommends that you research the feasibility of enabling unprivileged user namespaces before disabling the sandbox. An exception
|
||||
is if you are running Kibana in Docker because the container runs in a user namespace with the built-in seccomp/bpf filters.
|
||||
|
||||
[float]
|
||||
==== `spawn EACCES`
|
||||
Ensure that the `phantomjs` binary in your Kibana data directory is owned by the user who is running Kibana, that the user has the execute permission,
|
||||
and if applicable, that the filesystem is mounted with the `exec` option.
|
||||
|
||||
[float]
|
||||
==== `Caught error spawning Chromium`
|
||||
Ensure that the `headless_shell` binary located in your Kibana data directory is owned by the user who is running Kibana, that the user has the execute permission,
|
||||
|
|
|
@ -91,16 +91,8 @@ visualizations, try increasing this value.
|
|||
Defaults to `3000` (3 seconds).
|
||||
|
||||
[[xpack-reporting-browser]]`xpack.reporting.capture.browser.type`::
|
||||
Specifies the browser to use to capture screenshots. Valid options are `phantom`
|
||||
and `chromium`. When `chromium` is set, the settings specified in the <<reporting-chromium-settings, Chromium settings>>
|
||||
are respected. Defaults to `chromium`.
|
||||
|
||||
[NOTE]
|
||||
============
|
||||
Starting in 7.0, Phantom support will be removed from Kibana, and `chromium`
|
||||
will be the only valid option for the `xpack.reporting.capture.browser.type` setting.
|
||||
============
|
||||
|
||||
Specifies the browser to use to capture screenshots. This setting exists for
|
||||
backward compatibility. The only valid option is `chromium`.
|
||||
|
||||
[float]
|
||||
[[reporting-chromium-settings]]
|
||||
|
|
|
@ -26,7 +26,7 @@ module.exports = {
|
|||
browsers: [
|
||||
'last 2 versions',
|
||||
'> 5%',
|
||||
'Safari 7', // for PhantomJS support
|
||||
'Safari 7', // for PhantomJS support: https://github.com/elastic/kibana/issues/27136
|
||||
],
|
||||
},
|
||||
useBuiltIns: true,
|
||||
|
|
|
@ -195,27 +195,21 @@ export const CleanExtraBrowsersTask = {
|
|||
async run(config, log, build) {
|
||||
const getBrowserPathsForPlatform = platform => {
|
||||
const reportingDir = 'node_modules/x-pack/plugins/reporting';
|
||||
const phantomDir = '.phantom';
|
||||
const chromiumDir = '.chromium';
|
||||
const phantomPath = p =>
|
||||
build.resolvePathForPlatform(platform, reportingDir, phantomDir, p);
|
||||
const chromiumPath = p =>
|
||||
build.resolvePathForPlatform(platform, reportingDir, chromiumDir, p);
|
||||
return platforms => {
|
||||
const paths = [];
|
||||
if (platforms.windows) {
|
||||
paths.push(phantomPath('phantomjs-*-windows.zip'));
|
||||
paths.push(chromiumPath('chromium-*-win32.zip'));
|
||||
paths.push(chromiumPath('chromium-*-windows.zip'));
|
||||
}
|
||||
|
||||
if (platforms.darwin) {
|
||||
paths.push(phantomPath('phantomjs-*-macosx.zip'));
|
||||
paths.push(chromiumPath('chromium-*-darwin.zip'));
|
||||
}
|
||||
|
||||
if (platforms.linux) {
|
||||
paths.push(phantomPath('phantomjs-*-linux-x86_64.tar.bz2'));
|
||||
paths.push(chromiumPath('chromium-*-linux.zip'));
|
||||
}
|
||||
return paths;
|
||||
|
|
|
@ -23,7 +23,7 @@ module.exports = {
|
|||
browsers: [
|
||||
'last 2 versions',
|
||||
'> 5%',
|
||||
'Safari 7' // for PhantomJS support
|
||||
'Safari 7' // for PhantomJS support: https://github.com/elastic/kibana/issues/27136
|
||||
]
|
||||
})
|
||||
]
|
||||
|
|
|
@ -93,6 +93,7 @@ ObjDefine.prototype.create = function () {
|
|||
// clone the object on serialization and choose which properties
|
||||
// to include or trim manually. This is currently only in use in PhantomJS
|
||||
// due to https://github.com/ariya/phantomjs/issues/11856
|
||||
// TODO: remove this: https://github.com/elastic/kibana/issues/27136
|
||||
self.obj.toJSON = function () {
|
||||
return _.transform(self.obj, function (json, val, key) {
|
||||
const desc = self.descs[key];
|
||||
|
|
|
@ -127,18 +127,3 @@ For both of the above commands, it's crucial that you pass in `--config` to spec
|
|||
Read more about how the scripts work [here](scripts/README.md).
|
||||
|
||||
For a deeper dive, read more about the way functional tests and servers work [here](packages/kbn-test/README.md).
|
||||
|
||||
### Issues starting dev more of creating builds
|
||||
|
||||
You may see an error like this when you are getting started:
|
||||
|
||||
```
|
||||
[14:08:15] Error: Linux x86 checksum failed
|
||||
at download_phantom.js:42:15
|
||||
at process._tickDomainCallback (node.js:407:9)
|
||||
```
|
||||
|
||||
That's thanks to the binary Phantom downloads that have to happen, and Bitbucket being annoying with throttling and redirecting or... something. The real issue eludes me, but you have 2 options to resolve it.
|
||||
|
||||
1. Just keep re-running the command until it passes. Eventually the downloads will work, and since they are cached, it won't ever be an issue again.
|
||||
1. Download them by hand [from Bitbucket](https://bitbucket.org/ariya/phantomjs/downloads) and copy them into the `.phantom` path. We're currently using 1.9.8, and you'll need the Window, Mac, and Linux builds.
|
||||
|
|
|
@ -121,7 +121,6 @@
|
|||
"@elastic/datemath": "5.0.2",
|
||||
"@elastic/eui": "6.0.1",
|
||||
"@elastic/node-crypto": "0.1.2",
|
||||
"@elastic/node-phantom-simple": "2.2.4",
|
||||
"@elastic/numeral": "2.3.2",
|
||||
"@kbn/babel-preset": "1.0.0",
|
||||
"@kbn/es-query": "1.0.0",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
******
|
||||
****** This is a collection of CSS overrides that make Kibana look better for
|
||||
****** generating PDF reports with Phantom.
|
||||
****** generating PDF reports with headless browser
|
||||
******
|
||||
*/
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
******
|
||||
****** This is a collection of CSS overrides that make Kibana look better for
|
||||
****** generating PDF reports with Phantom.
|
||||
****** generating PDF reports with headless browser
|
||||
******
|
||||
*/
|
||||
|
||||
|
@ -117,7 +117,7 @@ visualize-app .visEditor__canvas {
|
|||
* reporting/export_types/printable_pdf/server/lib/screenshot.js or this will also hide the visualization
|
||||
* we want to capture.
|
||||
* 2. React grid item's transform affects the visualizations, even when they are using fixed positioning. Chrome seems
|
||||
* to handle this fine, but firefox moves the visualizations around, as I suspect Phantom since it had a problem.
|
||||
* to handle this fine, but firefox moves the visualizations around.
|
||||
*/
|
||||
dashboard-app .react-grid-item {
|
||||
height: 0 !important; /* 1. */
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { resolve } from 'path';
|
||||
import { UI_SETTINGS_CUSTOM_PDF_LOGO } from './common/constants';
|
||||
import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status';
|
||||
|
@ -17,7 +16,7 @@ import { checkLicenseFactory } from './server/lib/check_license';
|
|||
import { validateConfig } from './server/lib/validate_config';
|
||||
import { validateMaxContentLength } from './server/lib/validate_max_content_length';
|
||||
import { exportTypesRegistryFactory } from './server/lib/export_types_registry';
|
||||
import { PHANTOM, createBrowserDriverFactory, getDefaultBrowser, getDefaultChromiumSandboxDisabled } from './server/browsers';
|
||||
import { CHROMIUM, createBrowserDriverFactory, getDefaultChromiumSandboxDisabled } from './server/browsers';
|
||||
import { logConfiguration } from './log_configuration';
|
||||
|
||||
import { getReportingUsageCollector } from './server/usage';
|
||||
|
@ -94,7 +93,7 @@ export const reporting = (kibana) => {
|
|||
settleTime: Joi.number().integer().default(1000), //deprecated
|
||||
concurrency: Joi.number().integer().default(appConfig.concurrency), //deprecated
|
||||
browser: Joi.object({
|
||||
type: Joi.any().valid('phantom', 'chromium').default(await getDefaultBrowser()), // TODO: make chromium the only valid option in 7.0
|
||||
type: Joi.any().valid(CHROMIUM).default(CHROMIUM),
|
||||
autoDownload: Joi.boolean().when('$dev', {
|
||||
is: true,
|
||||
then: Joi.default(true),
|
||||
|
@ -178,15 +177,6 @@ export const reporting = (kibana) => {
|
|||
|
||||
deprecations: function ({ unused }) {
|
||||
return [
|
||||
(settings, log) => {
|
||||
const isPhantom = get(settings, 'capture.browser.type') === PHANTOM;
|
||||
if (isPhantom) {
|
||||
log(
|
||||
'Phantom browser support for Reporting will be removed and Chromium will be the only valid option starting in 7.0.0. ' +
|
||||
'Use the default `chromium` value for `xpack.reporting.capture.browser.type` to dismiss this warning.'
|
||||
);
|
||||
}
|
||||
},
|
||||
unused("capture.concurrency"),
|
||||
unused("capture.timeout"),
|
||||
unused("capture.settleTime"),
|
||||
|
|
|
@ -4,5 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const PHANTOM = 'phantom';
|
||||
export const CHROMIUM = 'chromium';
|
||||
export const CHROMIUM = 'chromium';
|
||||
|
|
|
@ -5,9 +5,7 @@
|
|||
*/
|
||||
|
||||
import * as chromium from './chromium';
|
||||
import * as phantom from './phantom';
|
||||
|
||||
export const BROWSERS_BY_TYPE = {
|
||||
chromium,
|
||||
phantom,
|
||||
};
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import getosSync from 'getos';
|
||||
import { promisify } from 'bluebird';
|
||||
import { PHANTOM, CHROMIUM } from './browser_types';
|
||||
|
||||
const getos = promisify(getosSync);
|
||||
|
||||
// Chromium is unsupported on RHEL/CentOS before 7.0.
|
||||
const distroSupportsChromium = (distro, release) => {
|
||||
if (distro.toLowerCase() !== 'centos' && distro.toLowerCase () !== 'red hat linux') {
|
||||
return true;
|
||||
}
|
||||
const releaseNumber = parseInt(release, 10);
|
||||
return releaseNumber >= 7;
|
||||
};
|
||||
|
||||
|
||||
export async function getDefaultBrowser() {
|
||||
const os = await getos();
|
||||
|
||||
if (os.os === 'linux' && !distroSupportsChromium(os.dist, os.release)) {
|
||||
return PHANTOM;
|
||||
} else {
|
||||
return CHROMIUM;
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
export class ExtractError extends Error {
|
||||
constructor(cause, message = 'Failed to extract the phantom.js archive') {
|
||||
constructor(cause, message = 'Failed to extract the browser archive') {
|
||||
super(message);
|
||||
this.message = message;
|
||||
this.name = this.constructor.name;
|
||||
|
|
|
@ -6,6 +6,5 @@
|
|||
|
||||
export { ensureAllBrowsersDownloaded } from './download';
|
||||
export { createBrowserDriverFactory } from './create_browser_driver_factory';
|
||||
export { getDefaultBrowser } from './default_browser';
|
||||
export { getDefaultChromiumSandboxDisabled } from './default_chromium_sandbox_disabled';
|
||||
export { PHANTOM, CHROMIUM } from './browser_types';
|
||||
export { CHROMIUM } from './browser_types';
|
||||
|
|
|
@ -1,360 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { randomBytes } from 'crypto';
|
||||
import { fromCallback } from 'bluebird';
|
||||
import { transformFn } from './transform_fn';
|
||||
|
||||
export function PhantomDriver({ page, browser, zoom, logger }) {
|
||||
this.browser = browser;
|
||||
this.page = page;
|
||||
this.logger = logger;
|
||||
|
||||
const validateInstance = () => {
|
||||
if (page === false || browser === false) {
|
||||
throw new Error('Phantom instance is closed: ' + JSON.stringify({ page, browser }));
|
||||
}
|
||||
};
|
||||
|
||||
const configurePage = (pageOptions) => {
|
||||
const RESOURCE_TIMEOUT = 5000;
|
||||
return fromCallback(cb => page.set('resourceTimeout', RESOURCE_TIMEOUT, cb))
|
||||
.then(() => {
|
||||
if (zoom) return fromCallback(cb => page.set('zoomFactor', zoom, cb));
|
||||
})
|
||||
.then(() => {
|
||||
if (pageOptions.conditionalHeaders) {
|
||||
const headers = pageOptions.conditionalHeaders.headers;
|
||||
const conditions = pageOptions.conditionalHeaders.conditions;
|
||||
|
||||
const escape = (str) => {
|
||||
return str
|
||||
.replace(/'/g, `\\'`)
|
||||
.replace(/\\/g, `\\\\`)
|
||||
.replace(/\r?\n/g, '\\n');
|
||||
};
|
||||
|
||||
// we're using base64 encoding for any user generated values that we need to eval
|
||||
// to be sure that we're handling these properly
|
||||
const btoa = (str) => {
|
||||
return Buffer.from(str).toString('base64');
|
||||
};
|
||||
|
||||
const fn = `function (requestData, networkRequest) {
|
||||
var log = function (msg) {
|
||||
if (!page.onConsoleMessage) {
|
||||
return;
|
||||
}
|
||||
page.onConsoleMessage(msg);
|
||||
};
|
||||
|
||||
var parseUrl = function (url) {
|
||||
var link = document.createElement('a');
|
||||
link.href = url;
|
||||
return {
|
||||
protocol: link.protocol,
|
||||
port: link.port,
|
||||
hostname: link.hostname,
|
||||
pathname: link.pathname,
|
||||
};
|
||||
};
|
||||
|
||||
var shouldUseCustomHeadersForPort = function (port) {
|
||||
if ('${escape(conditions.protocol)}' === 'http' && ${conditions.port} === 80) {
|
||||
return port === undefined || port === null || port === '' || port === '${conditions.port}';
|
||||
}
|
||||
|
||||
if ('${escape(conditions.protocol)}' === 'https' && ${conditions.port} === 443) {
|
||||
return port === undefined || port === null || port === '' || port === '${conditions.port}';
|
||||
}
|
||||
|
||||
return port === '${conditions.port}';
|
||||
};
|
||||
|
||||
var url = parseUrl(requestData.url);
|
||||
if (
|
||||
url.hostname === '${escape(conditions.hostname)}' &&
|
||||
url.protocol === '${escape(conditions.protocol)}:' &&
|
||||
shouldUseCustomHeadersForPort(url.port) &&
|
||||
url.pathname.indexOf('${escape(conditions.basePath)}/') === 0
|
||||
) {
|
||||
log('Using custom headers for ' + requestData.url);
|
||||
${Object.keys(headers).map(key => `networkRequest.setHeader(atob('${btoa(key)}'), atob('${btoa(headers[key])}'));`)
|
||||
.join('\n')}
|
||||
} else {
|
||||
log('No custom headers for ' + requestData.url);
|
||||
}
|
||||
}`;
|
||||
return fromCallback(cb => page.setFn('onResourceRequested', fn, cb));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
open(url, pageOptions) {
|
||||
validateInstance();
|
||||
|
||||
return configurePage(pageOptions)
|
||||
.then(() => logger.debug('Configured page'))
|
||||
.then(() => fromCallback(cb => page.open(url, cb)))
|
||||
.then(status => {
|
||||
logger.debug(`Page opened with status ${status}`);
|
||||
if (status !== 'success') {
|
||||
throw new Error(`URL open failed with status [${status}]. Is the server running?`);
|
||||
}
|
||||
if (pageOptions.waitForSelector) {
|
||||
return this.waitForSelector(pageOptions.waitForSelector);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setScrollPosition(position) {
|
||||
return fromCallback(cb => page.set('scrollPosition', position, cb));
|
||||
},
|
||||
|
||||
setViewport(size) {
|
||||
return fromCallback(cb => page.set('viewportSize', size, cb));
|
||||
},
|
||||
|
||||
evaluate({ fn, args }) {
|
||||
validateInstance();
|
||||
|
||||
const uniqId = [
|
||||
randomBytes(6).toString('base64'),
|
||||
randomBytes(9).toString('base64'),
|
||||
randomBytes(6).toString('base64'),
|
||||
].join('-');
|
||||
|
||||
const intlPath = require.resolve('intl/dist/Intl.min.js');
|
||||
const promisePath = require.resolve('bluebird/js/browser/bluebird.js');
|
||||
|
||||
return injectPolyfill(
|
||||
page,
|
||||
intlPath,
|
||||
function hasIntl() {
|
||||
return (window.Intl !== undefined);
|
||||
}
|
||||
)
|
||||
.then(() =>
|
||||
injectPolyfill(
|
||||
page,
|
||||
promisePath,
|
||||
function hasPromise() {
|
||||
return (window.Promise !== undefined);
|
||||
}
|
||||
))
|
||||
.then(() => {
|
||||
return fromCallback(cb => {
|
||||
page.evaluate(transformFn(evaluateWrapper), transformFn(fn).toString(), uniqId, args, cb);
|
||||
|
||||
// The original function is passed here as a string, and eval'd in phantom's context.
|
||||
// It's then executed in phantom's context and the result is attached to a __reporting
|
||||
// property on window. Promises can be used, and the result will be handled in the next
|
||||
// block. If the original function does not return a promise, its result is passed on.
|
||||
function evaluateWrapper(userFnStr, cbIndex, origArgs) {
|
||||
// you can't pass a function to phantom, so we pass the string and eval back into a function
|
||||
let userFn;
|
||||
eval('userFn = ' + userFnStr); // eslint-disable-line no-eval
|
||||
|
||||
// keep a record of the resulting execution for future calls (used when async)
|
||||
window.__reporting = window.__reporting || {};
|
||||
window.__reporting[cbIndex] = undefined;
|
||||
|
||||
// used to format the response consistently
|
||||
function done(err, res) {
|
||||
if (window.__reporting[cbIndex]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isErr = err instanceof Error;
|
||||
if (isErr) {
|
||||
const keys = Object.getOwnPropertyNames(err);
|
||||
err = keys.reduce(function copyErr(obj, key) {
|
||||
obj[key] = err[key];
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
||||
return window.__reporting[cbIndex] = {
|
||||
err: err,
|
||||
res: res,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// execute the original function
|
||||
const res = userFn.apply(this, origArgs);
|
||||
|
||||
if (res && typeof res.then === 'function') {
|
||||
// handle async resolution via Promises
|
||||
res.then((val) => {
|
||||
done(null, val);
|
||||
}, (err) => {
|
||||
if (!(err instanceof Error)) {
|
||||
err = new Error(err || 'Unspecified error');
|
||||
}
|
||||
done(err);
|
||||
});
|
||||
return '__promise__';
|
||||
} else {
|
||||
// if not given a promise, execute as sync
|
||||
return done(null, res);
|
||||
}
|
||||
} catch (err) {
|
||||
// any error during execution should be dealt with
|
||||
return done(err);
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((res) => {
|
||||
// if the response is not a promise, pass it along
|
||||
if (res !== '__promise__') {
|
||||
return res;
|
||||
}
|
||||
|
||||
// promise response means async, so wait for its resolution
|
||||
return this.waitFor({
|
||||
fn: function (cbIndex) {
|
||||
// resolves when the result object is no longer undefined
|
||||
return !!window.__reporting[cbIndex];
|
||||
},
|
||||
args: [uniqId],
|
||||
toEqual: true,
|
||||
})
|
||||
.then(() => {
|
||||
// once the original promise is resolved, pass along its value
|
||||
return fromCallback(cb => {
|
||||
page.evaluate(function (cbIndex) {
|
||||
return window.__reporting[cbIndex];
|
||||
}, uniqId, cb);
|
||||
});
|
||||
});
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.err) {
|
||||
// Make long/normal stack traces work
|
||||
res.err.name = res.err.name || 'Error';
|
||||
|
||||
if (!res.err.stack) {
|
||||
res.err.stack = res.err.toString();
|
||||
}
|
||||
|
||||
res.err.stack.replace(/\n*$/g, '\n');
|
||||
|
||||
if (res.err.stack) {
|
||||
res.err.toString = function () {
|
||||
return this.name + ': ' + this.message;
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.reject(res.err);
|
||||
}
|
||||
|
||||
return res.res;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
wait(timeout) {
|
||||
validateInstance();
|
||||
|
||||
return new Promise(resolve => setTimeout(resolve, timeout));
|
||||
},
|
||||
|
||||
waitFor({ fn, args, toEqual }) {
|
||||
const INTERVAL_TIME = 250;
|
||||
|
||||
if (typeof toEqual === 'undefined') return Promise.resolve();
|
||||
|
||||
validateInstance();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const self = this;
|
||||
|
||||
(function waitForCheck() {
|
||||
if (self.killed) {
|
||||
return;
|
||||
}
|
||||
|
||||
return self.evaluate({ fn, args })
|
||||
.then(res => {
|
||||
if (res === toEqual) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
setTimeout(waitForCheck, INTERVAL_TIME);
|
||||
})
|
||||
.catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
}());
|
||||
});
|
||||
},
|
||||
|
||||
waitForSelector(selector) {
|
||||
logger.debug(`PhantomDriver: waitForSelector ${selector}`);
|
||||
|
||||
validateInstance();
|
||||
|
||||
return this.waitFor({
|
||||
fn: function (cssSelector) {
|
||||
return !!document.querySelector(cssSelector);
|
||||
},
|
||||
args: [selector],
|
||||
toEqual: true,
|
||||
})
|
||||
.then(() => {
|
||||
logger.debug(`Finished waiting for selector ${selector}`);
|
||||
});
|
||||
},
|
||||
|
||||
async screenshot(position) {
|
||||
const { boundingClientRect, scroll = { x: 0, y: 0 } } = position;
|
||||
|
||||
validateInstance();
|
||||
|
||||
const zoomFactor = await fromCallback(cb => page.get('zoomFactor', cb));
|
||||
const previousClipRect = await fromCallback(cb => page.get('clipRect', cb));
|
||||
|
||||
const clipRect = {
|
||||
top: (boundingClientRect.top * zoomFactor) + scroll.y,
|
||||
left: (boundingClientRect.left * zoomFactor) + scroll.x,
|
||||
height: boundingClientRect.height * zoomFactor,
|
||||
width: boundingClientRect.width * zoomFactor
|
||||
};
|
||||
|
||||
await fromCallback(cb => page.set('clipRect', clipRect, cb));
|
||||
const data = await fromCallback(cb => page.renderBase64('PNG', cb));
|
||||
|
||||
await fromCallback(cb => page.set('clipRect', previousClipRect, cb));
|
||||
return data;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function injectPolyfill(page, pathToPolyfillFile, checkFunction) {
|
||||
const hasPolyfill = await fromCallback(cb => {
|
||||
page.evaluate(checkFunction, cb);
|
||||
});
|
||||
|
||||
if (hasPolyfill) {
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await fromCallback(cb => page.injectJs(pathToPolyfillFile, cb));
|
||||
|
||||
if (!status) {
|
||||
return Promise.reject(`Failed to load ${pathToPolyfillFile} library`);
|
||||
}
|
||||
|
||||
const hasPolyfillLoaded = await fromCallback(cb => {
|
||||
page.evaluate(checkFunction, cb);
|
||||
});
|
||||
|
||||
if (!hasPolyfillLoaded) {
|
||||
return Promise.reject(`Failed to inject ${pathToPolyfillFile}`);
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { transform as babelTransform } from 'babel-core';
|
||||
import { memoize } from 'lodash';
|
||||
|
||||
const safeWrap = (obj) => {
|
||||
const code = obj.toString();
|
||||
return new Function(`return (${code}).apply(null, arguments);`);
|
||||
};
|
||||
|
||||
const transform = (code) => {
|
||||
const result = babelTransform(code, {
|
||||
ast: false,
|
||||
babelrc: false,
|
||||
presets: [
|
||||
[ require.resolve('babel-preset-es2015'), { 'modules': false } ]
|
||||
]
|
||||
});
|
||||
return result.code;
|
||||
};
|
||||
|
||||
export const transformFn = memoize((fn) => {
|
||||
const code = transform(safeWrap(fn));
|
||||
return safeWrap(code);
|
||||
});
|
|
@ -1,101 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as Rx from 'rxjs';
|
||||
import { mergeMap } from 'rxjs/operators';
|
||||
import phantom from '@elastic/node-phantom-simple';
|
||||
import { getPhantomOptions } from './phantom_options';
|
||||
import { PhantomDriver } from '../driver';
|
||||
import { promisify } from 'bluebird';
|
||||
import { safeChildProcess, exitCodeSuggestion } from '../../safe_child_process';
|
||||
|
||||
export class PhantomDriverFactory {
|
||||
constructor(binaryPath) {
|
||||
this.binaryPath = binaryPath;
|
||||
}
|
||||
|
||||
type = 'phantom';
|
||||
|
||||
create({ bridgePort, viewport, zoom, logger }) {
|
||||
return Rx.Observable.create(observer => {
|
||||
let killed = false;
|
||||
let browser;
|
||||
let page;
|
||||
|
||||
(async () => {
|
||||
const phantomOptions = getPhantomOptions({
|
||||
phantomPath: this.binaryPath,
|
||||
bridgePort
|
||||
});
|
||||
|
||||
try {
|
||||
browser = await promisify(phantom.create)(phantomOptions);
|
||||
if (killed) {
|
||||
return;
|
||||
}
|
||||
|
||||
safeChildProcess(browser.process, observer);
|
||||
|
||||
page = await promisify(browser.createPage)();
|
||||
if (killed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await promisify(page.set)('viewportSize', viewport);
|
||||
if (killed) {
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err.toString();
|
||||
|
||||
if (message.includes('Phantom immediately exited with: 126')) {
|
||||
observer.error(new Error('Cannot execute phantom binary, incorrect format'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.includes('Phantom immediately exited with: 127')) {
|
||||
observer.error(Error('You must install fontconfig and freetype for Reporting to work'));
|
||||
return;
|
||||
}
|
||||
|
||||
observer.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const exit$ = Rx.fromEvent(browser.process, 'exit').pipe(
|
||||
mergeMap(([code]) => Rx.throwError(new Error(`Phantom exited with code: ${code}. ${exitCodeSuggestion(code)}`)))
|
||||
);
|
||||
|
||||
const driver = new PhantomDriver({
|
||||
page,
|
||||
browser,
|
||||
zoom,
|
||||
logger,
|
||||
});
|
||||
const driver$ = Rx.of(driver);
|
||||
|
||||
const consoleMessage$ = Rx.fromEventPattern(handler => {
|
||||
page.onConsoleMessage = handler;
|
||||
}, () => {
|
||||
page.onConsoleMessage = null;
|
||||
});
|
||||
|
||||
const message$ = Rx.never();
|
||||
|
||||
observer.next({
|
||||
driver$,
|
||||
message$,
|
||||
consoleMessage$,
|
||||
exit$
|
||||
});
|
||||
})();
|
||||
|
||||
return () => {
|
||||
killed = true;
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export function getPhantomOptions({ bridgePort, phantomPath }) {
|
||||
return {
|
||||
parameters: {
|
||||
'load-images': true,
|
||||
'ssl-protocol': 'any',
|
||||
'ignore-ssl-errors': true,
|
||||
},
|
||||
path: phantomPath,
|
||||
bridge: {
|
||||
port: bridgePort
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { PhantomDriverFactory } from './driver_factory';
|
||||
|
||||
export { paths } from './paths';
|
||||
|
||||
export async function createDriverFactory(binaryPath) {
|
||||
return new PhantomDriverFactory(binaryPath);
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
|
||||
export const paths = {
|
||||
archivesPath: path.resolve(__dirname, '../../../.phantom'),
|
||||
baseUrl: 'https://github.com/Medium/phantomjs/releases/download/v2.1.1/',
|
||||
packages: [{
|
||||
platforms: ['darwin', 'freebsd', 'openbsd'],
|
||||
archiveFilename: 'phantomjs-2.1.1-macosx.zip',
|
||||
archiveChecksum: 'b0c038bd139b9ecaad8fd321070c1651',
|
||||
rawChecksum: 'bbebe2381435309431c9d4e989aefdeb',
|
||||
binaryRelativePath: 'phantomjs-2.1.1-macosx/bin/phantomjs',
|
||||
}, {
|
||||
platforms: ['linux'],
|
||||
archiveFilename: 'phantomjs-2.1.1-linux-x86_64.tar.bz2',
|
||||
archiveChecksum: '1c947d57fce2f21ce0b43fe2ed7cd361',
|
||||
rawChecksum: '3f4bbbe5acd45494d8e52941936235f2',
|
||||
binaryRelativePath: 'phantomjs-2.1.1-linux-x86_64/bin/phantomjs'
|
||||
}, {
|
||||
platforms: ['win32'],
|
||||
archiveFilename: 'phantomjs-2.1.1-windows.zip',
|
||||
archiveChecksum: '4104470d43ddf2a195e8869deef0aa69',
|
||||
rawChecksum: '339f74c735e683502c43512a508e53d6',
|
||||
binaryRelativePath: 'phantomjs-2.1.1-windows\\bin\\phantomjs.exe'
|
||||
}]
|
||||
};
|
|
@ -40,6 +40,7 @@ export function getOrdinalValue(number) {
|
|||
// defaultMessage: '{number, selectordinal, one{#st} two{#nd} few{#rd} other{#th}}',
|
||||
// values: { number },
|
||||
// });
|
||||
// TODO: https://github.com/elastic/kibana/issues/27136
|
||||
|
||||
// Protects against falsey (including 0) values
|
||||
const num = number && number.toString();
|
||||
|
|
|
@ -8,8 +8,6 @@ require('@kbn/plugin-helpers').babelRegister();
|
|||
require('@kbn/test').runTestsCli([
|
||||
require.resolve('../test/reporting/configs/chromium_api.js'),
|
||||
require.resolve('../test/reporting/configs/chromium_functional.js'),
|
||||
// require.resolve('../test/reporting/configs/phantom_api.js'),
|
||||
// require.resolve('../test/reporting/configs/phantom_functional.js'),
|
||||
require.resolve('../test/functional/config.js'),
|
||||
require.resolve('../test/api_integration/config.js'),
|
||||
require.resolve('../test/saml_api_integration/config.js'),
|
||||
|
|
|
@ -3,19 +3,14 @@
|
|||
### Overview
|
||||
|
||||
Reporting tests have their own top level test folder because:
|
||||
- We run the same tests with different kibana.yml settings for your browser choice - Chromium or Phantom.
|
||||
- Current API tests run with `optimize.enabled=false` flag for performance reasons, but reporting actually requires UI assets.
|
||||
- Reporting tests take a lot longer than other test types. This separation allows developers to run them in isolation, or to run other functional or API tests without them.
|
||||
|
||||
Starting in 7.0 Phantom support will be removed and we can remove the phantom test versions.
|
||||
|
||||
### Running the tests
|
||||
|
||||
There is more information on running x-pack tests here: https://github.com/elastic/kibana/blob/master/x-pack/README.md#running-functional-tests. Similar to running the API tests, you need to specify a reporting configuration file. Reporting currently has four configuration files you can point to:
|
||||
There is more information on running x-pack tests here: https://github.com/elastic/kibana/blob/master/x-pack/README.md#running-functional-tests. Similar to running the API tests, you need to specify a reporting configuration file. Reporting currently has two configuration files you can point to:
|
||||
- test/reporting/configs/chromium_api.js
|
||||
- test/reporting/configs/phantom_api.js
|
||||
- test/reporting/configs/chromium_functional.js
|
||||
- test/reporting/configs/phantom_functional.js
|
||||
|
||||
The `api` versions hit the reporting api and ensure report generation completes successfully, but does not verify the output of the reports. This is done in the `functional` test versions, which does a snapshot comparison of the generated URL against a baseline to determine success.
|
||||
|
||||
|
@ -69,8 +64,6 @@ Install with all default options
|
|||
|
||||
The functional version of the reporting tests create a few pdf reports and do a snapshot comparison against a couple baselines. The baseline images are stored in `./functional/reports/baseline`.
|
||||
|
||||
**Note:** The snapshot comparisons use a threshold due to expected visual difference when running on different browsers and different Operating Systems. This threshold is currently very high because of differences between Chromium and Phantom versions, and also because of some slight bugs with the Phantom version. The bug is [here](https://github.com/elastic/kibana/issues/21485), the issue tracking this very high threshold is [here](https://github.com/elastic/kibana/issues/21486). Once we remove Phantom support in 7.0, we can drop this threshold and hopefully catch more bugs!
|
||||
|
||||
#### Updating the baselines
|
||||
|
||||
Every now and then visual changes will be made that will require the snapshots to be updated. This is how you go about updating it. I will discuss generating snapshots from chromium since that is the way of the future.
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { OSS_KIBANA_ARCHIVE_PATH, OSS_DATA_ARCHIVE_PATH } from './constants';
|
||||
|
||||
export default function ({ loadTestFile, getService }) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
describe('phantom', function () {
|
||||
this.tags('ciGroup7');
|
||||
|
||||
before(async () => {
|
||||
await esArchiver.load(OSS_KIBANA_ARCHIVE_PATH);
|
||||
await esArchiver.load(OSS_DATA_ARCHIVE_PATH);
|
||||
|
||||
await kibanaServer.uiSettings.update({
|
||||
'dateFormat:tz': 'UTC',
|
||||
'defaultIndex': '0bf35f60-3dc9-11e8-8660-4d65aa086b3c'
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload(OSS_KIBANA_ARCHIVE_PATH);
|
||||
await esArchiver.unload(OSS_DATA_ARCHIVE_PATH);
|
||||
});
|
||||
|
||||
loadTestFile(require.resolve('./bwc_existing_indexes'));
|
||||
loadTestFile(require.resolve('./bwc_generation_urls'));
|
||||
loadTestFile(require.resolve('./usage'));
|
||||
});
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getReportingApiConfig } from './api';
|
||||
|
||||
export default async function ({ readConfigFile }) {
|
||||
|
||||
const reportingApiConfig = await getReportingApiConfig({ readConfigFile });
|
||||
|
||||
return {
|
||||
...reportingApiConfig,
|
||||
junit: {
|
||||
reportName: 'X-Pack Phantom API Reporting Tests',
|
||||
},
|
||||
testFiles: [
|
||||
require.resolve('../api/phantom_tests'),
|
||||
],
|
||||
kbnTestServer: {
|
||||
...reportingApiConfig.kbnTestServer,
|
||||
serverArgs: [
|
||||
...reportingApiConfig.kbnTestServer.serverArgs,
|
||||
`--xpack.reporting.capture.browser.type=phantom`,
|
||||
`--xpack.spaces.enabled=false`,
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getFunctionalConfig } from './functional';
|
||||
|
||||
export default async function ({ readConfigFile }) {
|
||||
|
||||
const functionalConfig = await getFunctionalConfig({ readConfigFile });
|
||||
|
||||
return {
|
||||
...functionalConfig,
|
||||
junit: {
|
||||
reportName: 'X-Pack Phantom Functional Reporting Tests',
|
||||
},
|
||||
testFiles: [require.resolve('../functional')],
|
||||
kbnTestServer: {
|
||||
...functionalConfig.kbnTestServer,
|
||||
serverArgs: [
|
||||
...functionalConfig.kbnTestServer.serverArgs,
|
||||
`--xpack.reporting.capture.browser.type=phantom`,
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
|
@ -85,8 +85,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
describe.skip('Print Layout', () => {
|
||||
it('matches baseline report', async function () {
|
||||
// Generating and then comparing reports can take longer than the default 60s timeout because the comparePngs
|
||||
// function is taking about 15 seconds per comparison in jenkins. Also Chromium takes a lot longer to generate a
|
||||
// report than phantom.
|
||||
// function is taking about 15 seconds per comparison in jenkins.
|
||||
this.timeout(360000);
|
||||
|
||||
await PageObjects.dashboard.switchToEditMode();
|
||||
|
@ -125,8 +124,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
|
||||
it('matches same baseline report with margins turned on', async function () {
|
||||
// Generating and then comparing reports can take longer than the default 60s timeout because the comparePngs
|
||||
// function is taking about 15 seconds per comparison in jenkins. Also Chromium takes a lot longer to generate a
|
||||
// report than phantom.
|
||||
// function is taking about 15 seconds per comparison in jenkins.
|
||||
this.timeout(360000);
|
||||
|
||||
await PageObjects.dashboard.switchToEditMode();
|
||||
|
@ -156,12 +154,13 @@ export default function ({ getService, getPageObjects }) {
|
|||
});
|
||||
});
|
||||
|
||||
// TODO Re-enable the tests after removing Phantom:
|
||||
// https://github.com/elastic/kibana/issues/21485
|
||||
describe.skip('Preserve Layout', () => {
|
||||
it('matches baseline report', async function () {
|
||||
|
||||
// Generating and then comparing reports can take longer than the default 60s timeout because the comparePngs
|
||||
// function is taking about 15 seconds per comparison in jenkins. Also Chromium takes a lot longer to generate a
|
||||
// report than phantom.
|
||||
// function is taking about 15 seconds per comparison in jenkins.
|
||||
this.timeout(360000);
|
||||
|
||||
await PageObjects.reporting.openPdfReportingPanel();
|
||||
|
@ -185,9 +184,6 @@ export default function ({ getService, getPageObjects }) {
|
|||
config.get('screenshots.directory'),
|
||||
log
|
||||
);
|
||||
// After expected OS differences, the diff count came to be around 350k. Due to
|
||||
// https://github.com/elastic/kibana/issues/21485 this jumped up to something like 368 when
|
||||
// comparing the same baseline for chromium and phantom.
|
||||
expect(percentSimilar).to.be.lessThan(0.05);
|
||||
|
||||
});
|
||||
|
@ -208,12 +204,13 @@ export default function ({ getService, getPageObjects }) {
|
|||
});
|
||||
});
|
||||
|
||||
// TODO Re-enable the tests after removing Phantom:
|
||||
// https://github.com/elastic/kibana/issues/21485
|
||||
describe.skip('Preserve Layout', () => {
|
||||
it('matches baseline report', async function () {
|
||||
|
||||
// Generating and then comparing reports can take longer than the default 60s timeout because the comparePngs
|
||||
// function is taking about 15 seconds per comparison in jenkins. Also Chromium takes a lot longer to generate a
|
||||
// report than phantom.
|
||||
// function is taking about 15 seconds per comparison in jenkins.
|
||||
this.timeout(360000);
|
||||
|
||||
await PageObjects.dashboard.switchToEditMode();
|
||||
|
@ -249,9 +246,6 @@ export default function ({ getService, getPageObjects }) {
|
|||
config.get('screenshots.directory'),
|
||||
log
|
||||
);
|
||||
// After expected OS differences, the diff count came to be around 350k. Due to
|
||||
// https://github.com/elastic/kibana/issues/21485 this jumped up to something like 368 when
|
||||
// comparing the same baseline for chromium and phantom.
|
||||
expect(percentSimilar).to.be.lessThan(0.05);
|
||||
|
||||
});
|
||||
|
@ -310,6 +304,8 @@ export default function ({ getService, getPageObjects }) {
|
|||
await expectEnabledGenerateReportButton();
|
||||
});
|
||||
|
||||
// TODO Re-enable the tests after removing Phantom:
|
||||
// https://github.com/elastic/kibana/issues/21485
|
||||
it.skip('matches baseline report', async function () {
|
||||
// Generating and then comparing reports can take longer than the default 60s timeout because the comparePngs
|
||||
// function is taking about 15 seconds per comparison in jenkins.
|
||||
|
@ -332,11 +328,6 @@ export default function ({ getService, getPageObjects }) {
|
|||
config.get('screenshots.directory'),
|
||||
log
|
||||
);
|
||||
// After expected OS and browser differences, the diff count came up to max 800564 that I saw.
|
||||
// This is pretty bad. https://github.com/elastic/kibana/issues/21486 is filed to lower this
|
||||
// which will be much easier when we only support one browser type (chromium instead of phantom).
|
||||
// The reason this is so high currently is because of a phantom bug:
|
||||
// https://github.com/elastic/kibana/issues/21485
|
||||
expect(percentSimilar).to.be.lessThan(0.05);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -847,13 +847,6 @@
|
|||
resolved "https://registry.yarnpkg.com/@elastic/node-crypto/-/node-crypto-0.1.2.tgz#c18ac282f635e88f041cc1555d806e492ca8f3b1"
|
||||
integrity sha1-wYrCgvY16I8EHMFVXYBuSSyo87E=
|
||||
|
||||
"@elastic/node-phantom-simple@2.2.4":
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@elastic/node-phantom-simple/-/node-phantom-simple-2.2.4.tgz#edca5c0001313a8a18b8663169c3a1b812f2251a"
|
||||
integrity sha1-7cpcAAExOooYuGYxacOhuBLyJRo=
|
||||
dependencies:
|
||||
debug "^2.2.0"
|
||||
|
||||
"@elastic/numeral@2.3.2":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@elastic/numeral/-/numeral-2.3.2.tgz#06c9ef22f18dd8c2b39ffe353868d4d0c13ea4f9"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue