diff --git a/README.md b/README.md index 48a2ff21c..a1a3ff8ba 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ directory. The routes then can be called from the plugin React components defined in the `plugin.js`. For convenience the plugin name is always passed with options when function- or array-returning form is used to export plugin as the function options property `pluginName`: ```js - export default ['react', 'axios', function(React, axios, {pluginName, pluginConfig, actions, selectors}) { + export default ['react', 'axios', function(React, axios, {pluginName, pluginConfig, actions, actionNames, selectors}) { class PluginComponent extends React.Component { // ... somewhere inside the component ... const result = await axios.get(`/plugin-routes/${pluginName}/plugin-route`); @@ -271,9 +271,11 @@ directory. In the example you can also see another convenient properties: - `pluginName` - plugin name; + - `pluginConfig` - plugin configuration; - `actions` - the html-reporter **Redux** actions; - - `selectors` - the memoized html-reporter selectors which created using **reselect** library - - `pluginConfig` - plugin configuration. + - `actionNames` - the html-reporter action names, that used in **Redux** actions. To be able to subscribe on html-reporter events; + - `selectors` - the memoized html-reporter selectors which created using **reselect** library. + Available dependencies: - `react` diff --git a/lib/static/components/details.js b/lib/static/components/details.js index fc0d8dd4e..4f1e8ffb7 100644 --- a/lib/static/components/details.js +++ b/lib/static/components/details.js @@ -16,11 +16,15 @@ export default class Details extends Component { state = {isOpened: false}; handleClick = () => { - this.setState({isOpened: !this.state.isOpened}); + this.setState((state, props) => { + const newState = {isOpened: !state.isOpened}; - if (this.props.onClick) { - this.props.onClick(); - } + if (props.onClick) { + props.onClick(newState); + } + + return newState; + }); } render() { diff --git a/lib/static/modules/action-names.js b/lib/static/modules/action-names.js index 753b71ed8..78f4b6b16 100644 --- a/lib/static/modules/action-names.js +++ b/lib/static/modules/action-names.js @@ -13,7 +13,7 @@ export default { SUITE_BEGIN: 'SUITE_BEGIN', TEST_BEGIN: 'TEST_BEGIN', TEST_RESULT: 'TEST_RESULT', - TESTS_END: 'TEST_END', + TESTS_END: 'TESTS_END', ACCEPT_SCREENSHOT: 'ACCEPT_SCREENSHOT', ACCEPT_OPENED_SCREENSHOTS: 'ACCEPT_OPENED_SCREENSHOTS', CLOSE_SECTIONS: 'CLOSE_SECTIONS', diff --git a/lib/static/modules/actions.js b/lib/static/modules/actions.js index 01f64e857..c1df58c1c 100644 --- a/lib/static/modules/actions.js +++ b/lib/static/modules/actions.js @@ -5,9 +5,8 @@ import StaticTestsTreeBuilder from '../../tests-tree-builder/static'; import actionNames from './action-names'; import {types as modalTypes} from '../components/modals'; import {QUEUED} from '../../constants/test-statuses'; -import {LOCAL_DATABASE_NAME} from '../../constants/file-names'; import {getHttpErrorMessage} from './utils'; -import {fetchDatabases, mergeDatabases, connectToDatabase} from './sqlite'; +import {fetchDatabases, mergeDatabases, connectToDatabase, getMainDatabaseUrl} from './sqlite'; import {getSuitesTableRows} from './database-utils'; import {setFilteredBrowsers} from './query-params'; import plugins from './plugins'; @@ -23,7 +22,7 @@ export const initGuiReport = () => { try { const appState = await axios.get('/init'); - const mainDatabaseUrl = new URL(LOCAL_DATABASE_NAME, window.location.href); + const mainDatabaseUrl = getMainDatabaseUrl(); const db = await connectToDatabase(mainDatabaseUrl.href); await plugins.loadAll(appState.data.config); @@ -148,6 +147,20 @@ export const stopTests = () => async dispatch => { } }; +export const testsEnd = () => async dispatch => { + try { + const mainDatabaseUrl = getMainDatabaseUrl(); + const db = await connectToDatabase(mainDatabaseUrl.href); + + dispatch({ + type: actionNames.TESTS_END, + payload: {db} + }); + } catch (e) { + dispatch(createNotificationError('testsEnd', e)); + } +}; + export const suiteBegin = (suite) => ({type: actionNames.SUITE_BEGIN, payload: suite}); export const testBegin = (test) => ({type: actionNames.TEST_BEGIN, payload: test}); export const testResult = (result) => ({type: actionNames.TEST_RESULT, payload: result}); @@ -157,7 +170,6 @@ export const toggleLoading = (payload) => ({type: actionNames.TOGGLE_LOADING, pa export const closeSections = (payload) => ({type: actionNames.CLOSE_SECTIONS, payload}); export const openModal = (payload) => ({type: actionNames.OPEN_MODAL, payload}); export const closeModal = (payload) => ({type: actionNames.CLOSE_MODAL, payload}); -export const testsEnd = () => ({type: actionNames.TESTS_END}); export const runFailed = () => ({type: actionNames.RUN_FAILED_TESTS}); export const expandAll = () => ({type: actionNames.VIEW_EXPAND_ALL}); export const expandErrors = () => ({type: actionNames.VIEW_EXPAND_ERRORS}); diff --git a/lib/static/modules/load-plugin.js b/lib/static/modules/load-plugin.js index 26a211add..d7500cabd 100644 --- a/lib/static/modules/load-plugin.js +++ b/lib/static/modules/load-plugin.js @@ -11,6 +11,7 @@ import immer from 'immer'; import * as reselect from 'reselect'; import axios from 'axios'; import * as selectors from './selectors'; +import actionNames from './action-names'; import Details from '../components/details'; const whitelistedDeps = { @@ -85,7 +86,7 @@ async function initPlugin(plugin, pluginName, pluginConfig) { const depArgs = deps.map(dep => whitelistedDeps[dep]); // cyclic dep, resolve it dynamically const actions = await import('./actions'); - return plugin(...depArgs, {pluginName, pluginConfig, actions, selectors}); + return plugin(...depArgs, {pluginName, pluginConfig, actions, actionNames, selectors}); } return plugin; diff --git a/lib/static/modules/reducers/db.js b/lib/static/modules/reducers/db.js index 9abd42129..10fa41411 100644 --- a/lib/static/modules/reducers/db.js +++ b/lib/static/modules/reducers/db.js @@ -6,6 +6,14 @@ export default (state, action) => { case actionNames.INIT_STATIC_REPORT: case actionNames.INIT_GUI_REPORT: { const {db} = action.payload; + + return {...state, db}; + } + + case actionNames.TESTS_END: { + closeDatabase(state.db); // close previous connection in order to free memory + const {db} = action.payload; + return {...state, db}; } diff --git a/lib/static/modules/sqlite.js b/lib/static/modules/sqlite.js index da94c6fd0..1d503969b 100644 --- a/lib/static/modules/sqlite.js +++ b/lib/static/modules/sqlite.js @@ -3,6 +3,7 @@ import axios from 'axios'; import {flattenDeep} from 'lodash'; import {mergeTables} from '../../common-utils'; +import {LOCAL_DATABASE_NAME} from '../../constants/file-names'; function isRelativeUrl(url) { try { @@ -139,8 +140,13 @@ async function connectToDatabase(dbUrl) { return new SQL.Database(new Uint8Array(data)); } +function getMainDatabaseUrl() { + return new URL(LOCAL_DATABASE_NAME, window.location.href); +} + module.exports = { fetchDatabases, mergeDatabases, - connectToDatabase + connectToDatabase, + getMainDatabaseUrl }; diff --git a/test/unit/lib/static/components/details.js b/test/unit/lib/static/components/details.js index 2d7280b2f..017b16f9f 100644 --- a/test/unit/lib/static/components/details.js +++ b/test/unit/lib/static/components/details.js @@ -30,16 +30,34 @@ describe('
', () => { assert.equal(text, 'some-title'); }); - it('should call "onClick" handler on click in title', () => { - const props = { - title: 'some-title', - content: 'foo bar', - onClick: sinon.stub() - }; + describe('"onClick" handler', () => { + let props; - const component = mount(
); - component.find('.details__summary').simulate('click'); + beforeEach(() => { + props = { + title: 'some-title', + content: 'foo bar', + onClick: sinon.stub() + }; + }); + + it('should call on click in title', () => { + const component = mount(
); + + component.find('.details__summary').simulate('click'); + + assert.calledOnce(props.onClick); + }); + + it('should call with changed state on each call', () => { + const component = mount(
); + + component.find('.details__summary') + .simulate('click') + .simulate('click'); - assert.calledOnceWith(props.onClick); + assert.calledWith(props.onClick.firstCall, {isOpened: true}); + assert.calledWith(props.onClick.secondCall, {isOpened: false}); + }); }); }); diff --git a/test/unit/lib/static/modules/actions.js b/test/unit/lib/static/modules/actions.js index 4e06542cc..33f6f119a 100644 --- a/test/unit/lib/static/modules/actions.js +++ b/test/unit/lib/static/modules/actions.js @@ -7,13 +7,14 @@ import {LOCAL_DATABASE_NAME} from 'lib/constants/file-names'; describe('lib/static/modules/actions', () => { const sandbox = sinon.sandbox.create(); - let dispatch, actions, addNotification, getSuitesTableRows, connectToDatabaseStub, pluginsStub; + let dispatch, actions, addNotification, getSuitesTableRows, getMainDatabaseUrl, connectToDatabaseStub, pluginsStub; beforeEach(() => { dispatch = sandbox.stub(); sandbox.stub(axios, 'post').resolves({data: {}}); addNotification = sandbox.stub(); getSuitesTableRows = sandbox.stub(); + getMainDatabaseUrl = sandbox.stub().returns({href: 'http://localhost/default/sqlite.db'}); connectToDatabaseStub = sandbox.stub().resolves({}); pluginsStub = {loadAll: sandbox.stub()}; @@ -23,7 +24,7 @@ describe('lib/static/modules/actions', () => { actions = proxyquire('lib/static/modules/actions', { 'reapop': {addNotification}, './database-utils': {getSuitesTableRows}, - './sqlite': {connectToDatabase: connectToDatabaseStub}, + './sqlite': {getMainDatabaseUrl, connectToDatabase: connectToDatabaseStub}, './plugins': pluginsStub }); }); @@ -33,16 +34,6 @@ describe('lib/static/modules/actions', () => { describe('initGuiReport', () => { beforeEach(() => { sandbox.stub(axios, 'get').resolves({data: {}}); - - global.window = { - location: { - href: 'http://localhost/random/path.html' - } - }; - }); - - afterEach(() => { - global.window = undefined; }); it('should run init action on server', async () => { @@ -52,7 +43,8 @@ describe('lib/static/modules/actions', () => { }); it('should fetch database from default html page', async () => { - global.window.location.href = 'http://127.0.0.1:8080/'; + const href = 'http://127.0.0.1:8080/sqlite.db'; + getMainDatabaseUrl.returns({href}); await actions.initGuiReport()(dispatch); @@ -330,4 +322,43 @@ describe('lib/static/modules/actions', () => { assert.deepEqual(actions.closeModal(modal), {type: actionNames.CLOSE_MODAL, payload: modal}); }); }); + + describe('testsEnd', () => { + it('should connect to database', async () => { + const href = 'http://127.0.0.1:8080/sqlite.db'; + getMainDatabaseUrl.returns({href}); + + await actions.testsEnd()(dispatch); + + assert.calledOnceWith(connectToDatabaseStub, href); + }); + + it('should dispatch "TESTS_END" action with db connection', async () => { + const db = {}; + connectToDatabaseStub.resolves(db); + + await actions.testsEnd()(dispatch); + + assert.calledOnceWith(dispatch, { + type: actionNames.TESTS_END, + payload: {db} + }); + }); + + it('should show notification if error appears', async () => { + connectToDatabaseStub.rejects(new Error('failed to connect to database')); + + await actions.testsEnd()(dispatch); + + assert.calledOnceWith( + addNotification, + { + dismissAfter: 0, + id: 'testsEnd', + message: 'failed to connect to database', + status: 'error' + } + ); + }); + }); }); diff --git a/test/unit/lib/static/modules/load-plugin.js b/test/unit/lib/static/modules/load-plugin.js index bd4261286..8b97191fa 100644 --- a/test/unit/lib/static/modules/load-plugin.js +++ b/test/unit/lib/static/modules/load-plugin.js @@ -2,6 +2,7 @@ import axios from 'axios'; import loadPlugin from 'lib/static/modules/load-plugin'; +import actionNames from 'lib/static/modules/action-names'; import * as actions from 'lib/static/modules/actions'; import * as selectors from 'lib/static/modules/selectors'; @@ -32,7 +33,7 @@ describe('static/modules/load-plugin', () => { await loadPlugin('plugin-a'); assert.deepStrictEqual(plugin.args, [ - [{actions, selectors, pluginName: 'plugin-a', pluginConfig: undefined}] + [{actions, actionNames, selectors, pluginName: 'plugin-a', pluginConfig: undefined}] ]); }); @@ -41,7 +42,7 @@ describe('static/modules/load-plugin', () => { await loadPlugin('plugin-b', config); assert.deepStrictEqual(plugin.args, [ - [{actions, selectors, pluginName: 'plugin-b', pluginConfig: config}] + [{actions, actionNames, selectors, pluginName: 'plugin-b', pluginConfig: config}] ]); }); @@ -50,7 +51,7 @@ describe('static/modules/load-plugin', () => { await loadPlugin('plugin-c'); assert.deepStrictEqual(plugin.args, [ - [axios, {actions, selectors, pluginName: 'plugin-c', pluginConfig: undefined}] + [axios, {actions, actionNames, selectors, pluginName: 'plugin-c', pluginConfig: undefined}] ]); }); @@ -59,7 +60,7 @@ describe('static/modules/load-plugin', () => { await loadPlugin('plugin-d'); assert.deepStrictEqual(plugin.args, [ - [undefined, {actions, selectors, pluginName: 'plugin-d', pluginConfig: undefined}] + [undefined, {actions, actionNames, selectors, pluginName: 'plugin-d', pluginConfig: undefined}] ]); }); }); diff --git a/test/unit/lib/static/modules/sqlite.js b/test/unit/lib/static/modules/sqlite.js index cba539025..97800c392 100644 --- a/test/unit/lib/static/modules/sqlite.js +++ b/test/unit/lib/static/modules/sqlite.js @@ -6,8 +6,10 @@ import proxyquire from 'proxyquire'; import { fetchDatabases, mergeDatabases, - connectToDatabase + connectToDatabase, + getMainDatabaseUrl } from 'lib/static/modules/sqlite'; +import {LOCAL_DATABASE_NAME} from 'lib/constants/file-names'; describe('lib/static/modules/sqlite', () => { const sandbox = sinon.sandbox.create(); @@ -16,7 +18,10 @@ describe('lib/static/modules/sqlite', () => { sandbox.stub(axios, 'get').resolves(); }); - afterEach(() => sandbox.restore()); + afterEach(() => { + sandbox.restore(); + global.window = undefined; + }); describe('fetchDatabases', () => { it('should return empty arrays if dbUrls.json not contain useful data', async () => { @@ -190,10 +195,6 @@ describe('lib/static/modules/sqlite', () => { }; }); - afterEach(() => { - global.window = undefined; - }); - it('should return null if dataForDbs is empty', async () => { const mergedDbConnection = await mergeDatabases([]); @@ -267,10 +268,6 @@ describe('lib/static/modules/sqlite', () => { }; }); - afterEach(() => { - global.window = undefined; - }); - it('should return connection to database', async () => { axios.get .withArgs('http://127.0.0.1:8080/sqlite.db', {responseType: 'arraybuffer'}) @@ -281,4 +278,22 @@ describe('lib/static/modules/sqlite', () => { assert.instanceOf(db, Database); }); }); + + describe('getMainDatabaseUrl', () => { + beforeEach(() => { + global.window = { + location: { + href: 'http://localhost/default/path.html' + } + }; + }); + + it('should return url to main database', () => { + global.window.location.href = 'http://127.0.0.1:8080/'; + + const url = getMainDatabaseUrl(); + + assert.equal(url.href, `http://127.0.0.1:8080/${LOCAL_DATABASE_NAME}`); + }); + }); });