Skip to content

Commit 8f445e3

Browse files
authored
feat(gui): add ability to use db connection on client side (#402)
* fix(gui): serve static files from "reportPath" in config * fix: show notification if something fail on open report * feat(gui): add ability to use db connection on client side
1 parent fb52a0a commit 8f445e3

File tree

10 files changed

+151
-63
lines changed

10 files changed

+151
-63
lines changed

lib/gui/server.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ const {INTERNAL_SERVER_ERROR, OK} = require('http-codes');
99

1010
const App = require('./app');
1111
const {MAX_REQUEST_SIZE, KEEP_ALIVE_TIMEOUT, HEADERS_TIMEOUT} = require('./constants/server');
12-
const {IMAGES_PATH, ERROR_DETAILS_PATH} = require('../constants/paths');
1312
const {logger, initializeCustomGui, runCustomGuiAction} = require('../server-utils');
1413
const initPluginsRoutes = require('./routes/plugins');
1514

@@ -26,9 +25,7 @@ exports.start = async ({paths, hermione, guiApi, configs}) => {
2625
server.use(initPluginsRoutes(express.Router(), pluginConfig));
2726

2827
server.use(express.static(path.join(__dirname, '../static'), {index: 'gui.html'}));
29-
server.use(express.static(process.cwd()));
30-
server.use(`/${IMAGES_PATH}`, express.static(path.join(process.cwd(), pluginConfig.path, IMAGES_PATH)));
31-
server.use(`/${ERROR_DETAILS_PATH}`, express.static(path.join(process.cwd(), pluginConfig.path, ERROR_DETAILS_PATH)));
28+
server.use(express.static(path.join(process.cwd(), pluginConfig.path)));
3229

3330
server.get('/', (req, res) => res.sendFile(path.join(__dirname, '../static', 'gui.html')));
3431

lib/static/components/gui.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ class Gui extends Component {
3030
this._subscribeToEvents();
3131
}
3232

33+
componentWillUnmount() {
34+
this.props.actions.finGuiReport();
35+
}
36+
3337
_subscribeToEvents() {
3438
const {actions} = this.props;
3539
const eventSource = new EventSource('/events');
@@ -60,7 +64,12 @@ class Gui extends Component {
6064
const {allRootSuiteIds, loading, customScripts} = this.props;
6165

6266
if (!allRootSuiteIds.length) {
63-
return (<Loading active={true} />);
67+
return (
68+
<Fragment>
69+
<Notifications theme={wybo}/>
70+
<Loading active={true} />
71+
</Fragment>
72+
);
6473
}
6574

6675
return (

lib/static/components/report.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ class Report extends Component {
3434
const {allRootSuiteIds, fetchDbDetails} = this.props;
3535

3636
if (!allRootSuiteIds.length && !fetchDbDetails.length) {
37-
return (<Loading active={true} />);
37+
return (
38+
<Fragment>
39+
<Notifications theme={wybo}/>
40+
<Loading active={true} />
41+
</Fragment>
42+
);
3843
}
3944

4045
return (

lib/static/modules/action-names.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
export default {
44
INIT_GUI_REPORT: 'INIT_GUI_REPORT',
55
INIT_STATIC_REPORT: 'INIT_STATIC_REPORT',
6+
FIN_GUI_REPORT: 'FIN_GUI_REPORT',
67
FIN_STATIC_REPORT: 'FIN_STATIC_REPORT',
78
RUN_ALL_TESTS: 'RUN_ALL_TESTS',
89
RUN_FAILED_TESTS: 'RUN_FAILED_TESTS',

lib/static/modules/actions.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import StaticTestsTreeBuilder from '../../tests-tree-builder/static';
55
import actionNames from './action-names';
66
import {types as modalTypes} from '../components/modals';
77
import {QUEUED} from '../../constants/test-statuses';
8+
import {LOCAL_DATABASE_NAME} from '../../constants/file-names';
89
import {getHttpErrorMessage} from './utils';
9-
import {fetchDatabases, mergeDatabases} from './sqlite';
10+
import {fetchDatabases, mergeDatabases, connectToDatabase} from './sqlite';
1011
import {getSuitesTableRows} from './database-utils';
1112
import {setFilteredBrowsers} from './query-params';
1213
import plugins from './plugins';
@@ -22,11 +23,14 @@ export const initGuiReport = () => {
2223
try {
2324
const appState = await axios.get('/init');
2425

26+
const mainDatabaseUrl = new URL(LOCAL_DATABASE_NAME, window.location.href);
27+
const db = await connectToDatabase(mainDatabaseUrl.href);
28+
2529
await plugins.loadAll(appState.data.config);
2630

2731
dispatch({
2832
type: actionNames.INIT_GUI_REPORT,
29-
payload: appState.data
33+
payload: {...appState.data, db}
3034
});
3135

3236
const {customGuiError} = appState.data;
@@ -80,6 +84,7 @@ export const initStaticReport = () => {
8084
};
8185
};
8286

87+
export const finGuiReport = () => ({type: actionNames.FIN_GUI_REPORT});
8388
export const finStaticReport = () => ({type: actionNames.FIN_STATIC_REPORT});
8489

8590
const runTests = ({tests = [], action = {}} = {}) => {

lib/static/modules/reducers/db.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import actionNames from '../action-names';
33

44
export default (state, action) => {
55
switch (action.type) {
6-
case actionNames.INIT_STATIC_REPORT: {
6+
case actionNames.INIT_STATIC_REPORT:
7+
case actionNames.INIT_GUI_REPORT: {
78
const {db} = action.payload;
89
return {...state, db};
910
}
1011

11-
case actionNames.FIN_STATIC_REPORT: {
12+
case actionNames.FIN_STATIC_REPORT:
13+
case actionNames.FIN_GUI_REPORT: {
1214
closeDatabase(state.db);
1315

1416
return state;

lib/static/modules/sqlite.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,18 @@ async function mergeDatabases(dataForDbs) {
135135
return mergedDbConnection;
136136
}
137137

138+
async function connectToDatabase(dbUrl) {
139+
const mainDatabaseUrl = new URL(dbUrl);
140+
const {data} = await fetchFile(mainDatabaseUrl.href, {
141+
responseType: 'arraybuffer'
142+
});
143+
144+
const SQL = await window.initSqlJs();
145+
return new SQL.Database(new Uint8Array(data));
146+
}
147+
138148
module.exports = {
139149
fetchDatabases,
140-
mergeDatabases
150+
mergeDatabases,
151+
connectToDatabase
141152
};

test/unit/lib/static/modules/actions.js

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@ import proxyquire from 'proxyquire';
33
import {acceptOpened, retryTest, runFailedTests} from 'lib/static/modules/actions';
44
import actionNames from 'lib/static/modules/action-names';
55
import StaticTestsTreeBuilder from 'lib/tests-tree-builder/static';
6+
import {LOCAL_DATABASE_NAME} from 'lib/constants/file-names';
67

78
describe('lib/static/modules/actions', () => {
89
const sandbox = sinon.sandbox.create();
9-
let dispatch, actions, addNotification, getSuitesTableRows, pluginsStub;
10+
let dispatch, actions, addNotification, getSuitesTableRows, connectToDatabaseStub, pluginsStub;
1011

1112
beforeEach(() => {
1213
dispatch = sandbox.stub();
1314
sandbox.stub(axios, 'post').resolves({data: {}});
1415
addNotification = sandbox.stub();
1516
getSuitesTableRows = sandbox.stub();
17+
connectToDatabaseStub = sandbox.stub().resolves({});
1618
pluginsStub = {loadAll: sandbox.stub()};
1719

1820
sandbox.stub(StaticTestsTreeBuilder, 'create').returns(Object.create(StaticTestsTreeBuilder.prototype));
@@ -21,31 +23,57 @@ describe('lib/static/modules/actions', () => {
2123
actions = proxyquire('lib/static/modules/actions', {
2224
'reapop': {addNotification},
2325
'./database-utils': {getSuitesTableRows},
26+
'./sqlite': {connectToDatabase: connectToDatabaseStub},
2427
'./plugins': pluginsStub
2528
});
2629
});
2730

2831
afterEach(() => sandbox.restore());
2932

3033
describe('initGuiReport', () => {
31-
it('should run init action on server', async () => {
34+
beforeEach(() => {
3235
sandbox.stub(axios, 'get').resolves({data: {}});
3336

37+
global.window = {
38+
location: {
39+
href: 'http://localhost/random/path.html'
40+
}
41+
};
42+
});
43+
44+
afterEach(() => {
45+
global.window = undefined;
46+
});
47+
48+
it('should run init action on server', async () => {
3449
await actions.initGuiReport()(dispatch);
3550

3651
assert.calledOnceWith(axios.get, '/init');
3752
});
3853

39-
it('should dispatch "INIT_GUI_REPORT" action', async () => {
40-
sandbox.stub(axios, 'get').resolves({data: 'some-data'});
54+
it('should fetch database from default html page', async () => {
55+
global.window.location.href = 'http://127.0.0.1:8080/';
4156

4257
await actions.initGuiReport()(dispatch);
4358

44-
assert.calledOnceWith(dispatch, {type: actionNames.INIT_GUI_REPORT, payload: 'some-data'});
59+
assert.calledOnceWith(connectToDatabaseStub, `http://127.0.0.1:8080/${LOCAL_DATABASE_NAME}`);
60+
});
61+
62+
it('should dispatch "INIT_GUI_REPORT" action with data from "/init" route and connection to db', async () => {
63+
const db = {};
64+
connectToDatabaseStub.resolves(db);
65+
axios.get.resolves({data: {some: 'data'}});
66+
67+
await actions.initGuiReport()(dispatch);
68+
69+
assert.calledOnceWith(dispatch, {
70+
type: actionNames.INIT_GUI_REPORT,
71+
payload: {some: 'data', db}
72+
});
4573
});
4674

4775
it('should show notification if error in initialization on the server is happened', async () => {
48-
sandbox.stub(axios, 'get').throws(new Error('failed to initialize custom gui'));
76+
axios.get.rejects(new Error('failed to initialize custom gui'));
4977

5078
await actions.initGuiReport()(dispatch);
5179

@@ -62,7 +90,7 @@ describe('lib/static/modules/actions', () => {
6290

6391
it('should init plugins with the config from /init route', async () => {
6492
const config = {pluginsEnabled: true, plugins: []};
65-
sandbox.stub(axios, 'get').withArgs('/init').resolves({data: {config}});
93+
axios.get.withArgs('/init').resolves({data: {config}});
6694

6795
await actions.initGuiReport()(dispatch);
6896

test/unit/lib/static/modules/sqlite.js

Lines changed: 70 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,20 @@ import * as _ from 'lodash';
55

66
import {
77
fetchDatabases,
8-
mergeDatabases
8+
mergeDatabases,
9+
connectToDatabase
910
} from 'lib/static/modules/sqlite';
1011

1112
describe('lib/static/modules/sqlite', () => {
1213
const sandbox = sinon.sandbox.create();
1314

14-
describe('fetchDatabases', () => {
15-
beforeEach(() => {
16-
sandbox.stub(axios, 'get').resolves();
17-
});
15+
beforeEach(() => {
16+
sandbox.stub(axios, 'get').resolves();
17+
});
1818

19-
afterEach(() => sandbox.restore());
19+
afterEach(() => sandbox.restore());
2020

21+
describe('fetchDatabases', () => {
2122
it('should return empty arrays if dbUrls.json not contain useful data', async () => {
2223
axios.get.resolves({
2324
status: 200,
@@ -182,56 +183,80 @@ describe('lib/static/modules/sqlite', () => {
182183

183184
afterEach(() => {
184185
global.window = undefined;
185-
sandbox.restore();
186186
});
187187

188-
describe('mergeDatabases', () => {
189-
it('should return null if dataForDbs is empty', async () => {
190-
const mergedDbConnection = await mergeDatabases([]);
188+
it('should return null if dataForDbs is empty', async () => {
189+
const mergedDbConnection = await mergeDatabases([]);
191190

192-
assert.equal(mergedDbConnection, null);
193-
});
191+
assert.equal(mergedDbConnection, null);
192+
});
194193

195-
it('should not create unnecessary databases if dataForDbs contain data for single db', async () => {
196-
const data = new ArrayBuffer(1);
194+
it('should not create unnecessary databases if dataForDbs contain data for single db', async () => {
195+
const data = new ArrayBuffer(1);
197196

198-
const mergedDbConnection = await mergeDatabases([data]);
197+
const mergedDbConnection = await mergeDatabases([data]);
199198

200-
assert.instanceOf(mergedDbConnection, SQL.Database);
201-
assert.calledOnceWith(DatabaseConstructorSpy, new Uint8Array(1));
202-
assert.notCalled(SQL.Database.prototype.run);
203-
assert.notCalled(SQL.Database.prototype.close);
204-
});
199+
assert.instanceOf(mergedDbConnection, SQL.Database);
200+
assert.calledOnceWith(DatabaseConstructorSpy, new Uint8Array(1));
201+
assert.notCalled(SQL.Database.prototype.run);
202+
assert.notCalled(SQL.Database.prototype.close);
203+
});
205204

206-
it('should merge several chunk databases into one', async () => {
207-
const chunkSize1 = 1;
208-
const chunkSize2 = 2;
209-
const sumOfChunkSizes = chunkSize1 + chunkSize2;
210-
const data1 = new ArrayBuffer(chunkSize1);
211-
const data2 = new ArrayBuffer(chunkSize2);
212-
213-
const mergedDbConnection = await mergeDatabases([data1, data2]);
214-
215-
assert.instanceOf(mergedDbConnection, SQL.Database);
216-
assert.calledThrice(DatabaseConstructorSpy);
217-
assert.calledWith(DatabaseConstructorSpy, new Uint8Array(chunkSize1));
218-
assert.calledWith(DatabaseConstructorSpy, new Uint8Array(chunkSize2));
219-
assert.calledWith(DatabaseConstructorSpy, undefined, sumOfChunkSizes);
220-
assert.calledTwice(SQL.Database.prototype.close);
221-
});
205+
it('should merge several chunk databases into one', async () => {
206+
const chunkSize1 = 1;
207+
const chunkSize2 = 2;
208+
const sumOfChunkSizes = chunkSize1 + chunkSize2;
209+
const data1 = new ArrayBuffer(chunkSize1);
210+
const data2 = new ArrayBuffer(chunkSize2);
211+
212+
const mergedDbConnection = await mergeDatabases([data1, data2]);
213+
214+
assert.instanceOf(mergedDbConnection, SQL.Database);
215+
assert.calledThrice(DatabaseConstructorSpy);
216+
assert.calledWith(DatabaseConstructorSpy, new Uint8Array(chunkSize1));
217+
assert.calledWith(DatabaseConstructorSpy, new Uint8Array(chunkSize2));
218+
assert.calledWith(DatabaseConstructorSpy, undefined, sumOfChunkSizes);
219+
assert.calledTwice(SQL.Database.prototype.close);
220+
});
222221

223-
it('should merge both "suites" tables', async () => {
224-
const data1 = new ArrayBuffer(1);
225-
const data2 = new ArrayBuffer(1);
222+
it('should merge both "suites" tables', async () => {
223+
const data1 = new ArrayBuffer(1);
224+
const data2 = new ArrayBuffer(1);
226225

227-
await mergeDatabases([data1, data2]);
226+
await mergeDatabases([data1, data2]);
228227

229-
const rawQueries = _
230-
.flatten(SQL.Database.prototype.run.args)
231-
.join(' ');
228+
const rawQueries = _
229+
.flatten(SQL.Database.prototype.run.args)
230+
.join(' ');
232231

233-
assert.include(rawQueries, 'suites');
234-
});
232+
assert.include(rawQueries, 'suites');
233+
});
234+
});
235+
236+
describe('connectToDatabase', () => {
237+
let SQL, Database;
238+
239+
beforeEach(() => {
240+
Database = function Database() {};
241+
SQL = {Database};
242+
243+
global.window = {
244+
initSqlJs: sandbox.stub().resolves(SQL)
245+
};
246+
});
247+
248+
afterEach(() => {
249+
global.window = undefined;
250+
});
251+
252+
it('should return connection to database', async () => {
253+
axios.get
254+
.withArgs('http://127.0.0.1:8080/sqlite.db', {responseType: 'arraybuffer'})
255+
.resolves({status: 200, data: 'stub buffer'});
256+
257+
const db = await connectToDatabase('http://127.0.0.1:8080/sqlite.db');
258+
259+
assert.instanceOf(db, Database);
235260
});
236261
});
237262
});

webpack.common.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ module.exports = {
6767
filename: 'gui.html',
6868
template: 'template.html',
6969
chunks: ['gui']
70+
}),
71+
new HtmlWebpackIncludeAssetsPlugin({
72+
files: ['gui.html'],
73+
assets: ['sql-wasm.js'],
74+
append: false
7075
})
7176
]
7277
};

0 commit comments

Comments
 (0)