diff --git a/README.md b/README.md index 0a911da84..a847da60d 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,18 @@ directory. ] } ``` +* **customScripts** (optional) `function[]` - allows to add any scripts on the report html-page. Script will be executed immediately on page render. It can be helpful for adding some metrics or own extra functionality. + +```js +customScripts: [ + function() {console.log('something')}, + () => { + const div = document.createElement('div'); + div.innerHTML = 'hello'; + document.body.prepend(div); + } +] +``` Also there is ability to override plugin parameters by CLI options or environment variables (see [configparser](https://github.com/gemini-testing/configparser)). diff --git a/lib/config/index.js b/lib/config/index.js index 2f214057c..228f74265 100644 --- a/lib/config/index.js +++ b/lib/config/index.js @@ -66,6 +66,19 @@ const assertMetaInfoBaseUrls = (metaInfoBaseUrls) => { } }; +const assertArrayOfFunctions = (name) => { + return (value) => { + if (!_.isArray(value)) { + throw new Error(`"${name}" option must be an array, but got ${typeof value}`); + } + for (const item of value) { + if (!_.isFunction(item)) { + throw new Error(`"${name}" option must be an array of functions but got ${typeof item} for one of items`); + } + } + }; +}; + const mapErrorPatterns = (errorPatterns) => { return errorPatterns.map(patternInfo => { return _.isString(patternInfo) @@ -130,6 +143,10 @@ const getParser = () => { customGui: option({ defaultValue: configDefaults.customGui, validate: assertCustomGui + }), + customScripts: option({ + defaultValue: configDefaults.customScripts, + validate: assertArrayOfFunctions('customScripts') }) }), {envPrefix: ENV_PREFIX, cliPrefix: CLI_PREFIX}); }; diff --git a/lib/constants/defaults.js b/lib/constants/defaults.js index f7c987bda..e5bffac2e 100644 --- a/lib/constants/defaults.js +++ b/lib/constants/defaults.js @@ -10,6 +10,7 @@ module.exports = { lazyLoadOffset: 800, errorPatterns: [], metaInfoBaseUrls: {}, - customGui: {} + customGui: {}, + customScripts: [] } }; diff --git a/lib/report-builder/report-builder.js b/lib/report-builder/report-builder.js index cdafb28d6..31cc96148 100644 --- a/lib/report-builder/report-builder.js +++ b/lib/report-builder/report-builder.js @@ -230,7 +230,8 @@ module.exports = class ReportBuilder { getResult() { const { - defaultView, baseHost, scaleImages, lazyLoadOffset, errorPatterns, metaInfoBaseUrls, customGui + defaultView, baseHost, scaleImages, lazyLoadOffset, + errorPatterns, metaInfoBaseUrls, customGui, customScripts } = this._pluginConfig; this._sortTree(); @@ -238,7 +239,8 @@ module.exports = class ReportBuilder { skips: _.uniq(this._skips, JSON.stringify), suites: this._tree.children, config: { - defaultView, baseHost, scaleImages, lazyLoadOffset, errorPatterns, metaInfoBaseUrls, customGui + defaultView, baseHost, scaleImages, lazyLoadOffset, + errorPatterns, metaInfoBaseUrls, customGui, customScripts }, apiValues: this._apiValues, date: new Date().toString(), diff --git a/lib/static/components/custom-scripts.js b/lib/static/components/custom-scripts.js new file mode 100644 index 000000000..3d933fd28 --- /dev/null +++ b/lib/static/components/custom-scripts.js @@ -0,0 +1,25 @@ +'use strict'; + +import React, {useEffect, useRef} from 'react'; +import {isEmpty} from 'lodash'; + +export default function CustomScripts(props) { + const {scripts} = props; + + if (isEmpty(scripts)) { + return null; + } + + const ref = useRef(null); + + useEffect(() => { + scripts.forEach((script) => { + const s = document.createElement('script'); + s.type = 'text/javascript'; + s.innerHTML = `(${script})();`; + ref.current.appendChild(s); + }); + }, [scripts]); + + return
; +} diff --git a/lib/static/components/gui.js b/lib/static/components/gui.js index 6159b514d..0cd77e516 100644 --- a/lib/static/components/gui.js +++ b/lib/static/components/gui.js @@ -11,6 +11,7 @@ import SkippedList from './skipped-list'; import Loading from './loading'; import ModalContainer from '../containers/modal'; import MainTree from './main-tree'; +import CustomScripts from './custom-scripts'; class Gui extends Component { componentDidMount() { @@ -18,10 +19,11 @@ class Gui extends Component { } render() { - const {loading} = this.props; + const {loading, customScripts} = this.props; return ( + @@ -33,4 +35,6 @@ class Gui extends Component { } } -export default connect(({reporter: {gui, loading}}) => ({gui, loading}), {initial})(Gui); +export default connect(({reporter: {gui, loading, config: {customScripts}}}) => { + return {gui, loading, customScripts}; +}, {initial})(Gui); diff --git a/lib/static/components/report.js b/lib/static/components/report.js index cdedd5e0d..5a2818685 100644 --- a/lib/static/components/report.js +++ b/lib/static/components/report.js @@ -1,15 +1,18 @@ 'use strict'; import React, {Component, Fragment} from 'react'; +import {connect} from 'react-redux'; import Header from './header'; import ControlButtons from './controls/report-controls'; import SkippedList from './skipped-list'; import MainTree from './main-tree'; +import CustomScripts from './custom-scripts'; -export default class Report extends Component { +class Report extends Component { render() { return ( +
@@ -18,3 +21,5 @@ export default class Report extends Component { ); } } + +export default connect(({reporter: {config}}) => ({customScripts: config.customScripts}))(Report); diff --git a/test/unit/lib/config/index.js b/test/unit/lib/config/index.js index 43fe8fa10..3695e02ba 100644 --- a/test/unit/lib/config/index.js +++ b/test/unit/lib/config/index.js @@ -384,4 +384,32 @@ describe('config', () => { )); }); }); + + describe('customScripts', () => { + it('should have default value', () => { + assert.deepEqual(parseConfig({}).customScripts, configDefaults.customScripts); + }); + + it('should validate for Array type', () => { + assert.throws( + () => parseConfig({customScripts: 'foo'}), + /"customScripts" option must be an array, but got string/ + ); + }); + + it('should validate for Array items', () => { + assert.throws( + () => parseConfig({customScripts: ['foo']}), + /"customScripts" option must be an array of functions but got string for one of items/ + ); + }); + + it('should not throw with correct values', () => { + const scripts = [function() {}]; + + const config = parseConfig({customScripts: scripts}); + + assert.deepEqual(config.customScripts, scripts); + }); + }); }); diff --git a/test/unit/lib/static/components/custom-scripts.js b/test/unit/lib/static/components/custom-scripts.js new file mode 100644 index 000000000..6bd2c2766 --- /dev/null +++ b/test/unit/lib/static/components/custom-scripts.js @@ -0,0 +1,51 @@ +import React from 'react'; +import CustomScripts from 'lib/static/components/custom-scripts'; + +describe('', () => { + it('should render component for scripts', () => { + const props = { + scripts: [function() {}] + }; + + const component = mount(); + const node = component.find('.custom-scripts'); + + assert.lengthOf(node, 1); + }); + + it('should not render component if no scripts to inject', () => { + const props = { + scripts: [] + }; + + const component = mount(); + const node = component.find('.custom-scripts'); + + assert.lengthOf(node, 0); + }); + + it('should wrap function with IIFE', () => { + const props = { + scripts: [function foo() {}] + }; + + const component = mount(); + const script = component.text(); + + assert.equal(script, '(function foo() {})();'); + }); + + it('should split each function with ";"', () => { + const props = { + scripts: [ + function foo() {}, + function bar() {} + ] + }; + + const component = mount(); + const script = component.text(); + + assert.equal(script, '(function foo() {})();(function bar() {})();'); + }); +});