diff --git a/client/karma.config.js b/client/karma.config.js index 2bc798a61c..0df702d3c0 100644 --- a/client/karma.config.js +++ b/client/karma.config.js @@ -118,6 +118,7 @@ module.exports = function(karma) { ], alias: { 'bpmn-js/lib/Modeler': 'test/mocks/bpmn-js/Modeler', + 'cmmn-js/lib/Modeler': 'test/mocks/cmmn-js/Modeler', 'dmn-js/lib/Modeler': 'test/mocks/dmn-js/Modeler' } } diff --git a/client/src/app/App.js b/client/src/app/App.js index a5ecfc8762..6dafa8f06d 100644 --- a/client/src/app/App.js +++ b/client/src/app/App.js @@ -382,6 +382,16 @@ export class App extends Component { this.handleError(error, tab); } + /** + * Handle tab warning. + * + * @param {Object} tab descriptor + * + * @return {Function} tab warning callback + */ + handleTabWarning = (tab) => (warning) => { + this.handleWarning(warning, tab); + } /** * Handle tab changed. @@ -542,16 +552,58 @@ export class App extends Component { } handleError(error, ...args) { - const { onError } = this.props; + const { + message + } = error; + if (typeof onError === 'function') { onError(error, ...args); } - // TODO: show error in log + this.logEntry(message, 'error'); + } + + handleWarning(warning, ...args) { + const { + onWarning + } = this.props; + + const { + message + } = warning; + + if (typeof onWarning === 'function') { + onWarning(warning, ...args); + } + + this.logEntry(message, 'warning'); + } + + /** + * + * @param {String} message - Message to be logged. + * @param {String} category - Category of message. + */ + logEntry(message, category) { + const { + logEntries + } = this.state; + + this.toggleLog(true); + + this.setState({ + logEntries: [ + ...logEntries, + { + category, + message + } + ] + }); } setLayout(layout) { @@ -688,7 +740,22 @@ export class App extends Component { } askExportAs = (file, filters) => { - return this.props.globals.dialog.askExportAs(file, filters); + const { + globals + } = this.props; + + return globals.dialog.askExportAs(file, filters); + } + + /** + * Show a generic dialog. + */ + showDialog(options = {}) { + const { + globals + } = this.props; + + return globals.dialog.show(options); } triggerAction = (action, options) => { @@ -915,6 +982,7 @@ export class App extends Component { layout={ layout } onChanged={ this.handleTabChanged(activeTab) } onError={ this.handleTabError(activeTab) } + onWarning={ this.handleTabWarning(activeTab) } onShown={ this.handleTabShown(activeTab) } onLayoutChanged={ this.handleLayoutChanged } onContextMenu={ this.openTabMenu } diff --git a/client/src/app/AppParent.js b/client/src/app/AppParent.js index 8babbfc15f..9d4234ab3d 100644 --- a/client/src/app/AppParent.js +++ b/client/src/app/AppParent.js @@ -123,6 +123,14 @@ export default class AppParent extends Component { return log('app error', error); } + handleWarning = (warning, tab) => { + if (tab) { + return log('tab warning', warning, tab); + } + + return log('app warning', warning); + } + handleReady = async () => { try { @@ -199,6 +207,7 @@ export default class AppParent extends Component { onWorkspaceChanged={ this.handleWorkspaceChanged } onReady={ this.handleReady } onError={ this.handleError } + onWarning={ this.handleWarning } /> ); } diff --git a/client/src/app/__tests__/AppSpec.js b/client/src/app/__tests__/AppSpec.js index 3bb8672928..fee89068e0 100644 --- a/client/src/app/__tests__/AppSpec.js +++ b/client/src/app/__tests__/AppSpec.js @@ -700,6 +700,28 @@ describe('', function() { }); + it('should show in log', async function() { + + // given + const { + app + } = createApp(mount); + + await app.createDiagram(); + + const tabInstance = app.tabRef.current; + + const error = new Error('YZO!'); + + // when + tabInstance.triggerAction('error', error); + + // then + expect(app.state.layout.log.open).to.be.true; + expect(app.state.logEntries).to.have.length(1); + }); + + describe('should catch', function() { const errorHandler = window.onerror; @@ -750,6 +772,59 @@ describe('', function() { }); + describe('tab warnings', function() { + + it('should propagate', async function() { + + // given + const warningSpy = spy(); + + const { + app + } = createApp({ onWarning: warningSpy }, mount); + + const tab = await app.createDiagram(); + + const tabInstance = app.tabRef.current; + + // when + const warning = { + message: 'warning' + }; + + tabInstance.triggerAction('warning', warning); + + // then + expect(warningSpy).to.have.been.calledWith(warning, tab); + }); + + + it('should show in log', async function() { + + // given + const { + app + } = createApp(mount); + + await app.createDiagram(); + + const tabInstance = app.tabRef.current; + + // when + const warning = { + message: 'warning' + }; + + tabInstance.triggerAction('warning', warning); + + // then + expect(app.state.layout.log.open).to.be.true; + expect(app.state.logEntries).to.have.length(1); + }); + + }); + + describe('workspace integration', function() { describe('should notify #onWorkspaceChanged', function() { @@ -859,6 +934,28 @@ describe('', function() { expect(log.props().expanded).to.be.true; }); + + it('#logEntry', function() { + + // given + const { tree, app } = createApp(); + + app.setLayout({ log: { open: false } }); + + // when + app.logEntry('foo', 'bar'); + + // then + const log = tree.find(Log).first(); + + expect(app.state.logEntries).to.eql([{ + message: 'foo', + category: 'bar' + }]); + + expect(log.props().expanded).to.be.true; + }); + }); @@ -945,6 +1042,7 @@ function createApp(options = {}, mountFn=shallow) { const onReady = options.onReady; const onError = options.onError; + const onWarning = options.onWarning; const tree = mountFn( { + const { + onWarning + } = this.props; + + onWarning(warning); + } + + handleImport = (error, warnings) => { + + if (error) { + // TODO: open fallback + + return this.handleError(error); + } + + if (warnings && warnings.length) { + warnings.forEach(warning => { + this.handleWarning(warning); + }); + } + } + handleContextMenu = (event, context) => { const { @@ -238,6 +261,7 @@ class MultiSheetTab extends CachedComponent { onContextMenu={ this.handleContextMenu } onChanged={ this.handleChanged } onError={ this.handleError } + onImport={ this.handleImport } onLayoutChanged={ this.handleLayoutChanged } /> diff --git a/client/src/app/tabs/__tests__/MultiSheetTabSpec.js b/client/src/app/tabs/__tests__/MultiSheetTabSpec.js new file mode 100644 index 0000000000..34bd539338 --- /dev/null +++ b/client/src/app/tabs/__tests__/MultiSheetTabSpec.js @@ -0,0 +1,162 @@ +/* global sinon */ + +import React from 'react'; + +import { MultiSheetTab } from '../MultiSheetTab'; + +import { mount } from 'enzyme'; + +import { + Cache, + WithCachedState +} from '../../cached'; + +import { + providers as defaultProviders +} from './mocks'; + +const { spy } = sinon; + + +describe('', function() { + + it('should render', function() { + const { + instance + } = renderTab(); + + expect(instance).to.exist; + }); + + + describe('#onImport', function() { + + it('should import without errors', function() { + + // given + const errorSpy = spy(), + warningSpy = spy(); + + const { + instance + } = renderTab({ + onError: errorSpy, + onWarning: warningSpy + }); + + // when + instance.handleImport(); + + // then + expect(errorSpy).not.to.have.been.called; + expect(warningSpy).not.to.have.been.called; + }); + + + it('should import with warnings', function() { + + // given + const errorSpy = spy(), + warningSpy = spy(); + + const { + instance + } = renderTab({ + onError: errorSpy, + onWarning: warningSpy + }); + + // when + const warnings = [ 'warning', 'warning' ]; + + instance.handleImport(null, warnings); + + // then + expect(errorSpy).not.to.have.been.called; + expect(warningSpy).to.have.been.calledTwice; + expect(warningSpy.alwaysCalledWith('warning')).to.be.true; + }); + + + it('should import with error', function() { + + // given + const errorSpy = spy(), + warningSpy = spy(); + + const { + instance + } = renderTab({ + onError: errorSpy, + onWarning: warningSpy + }); + + // when + const error = new Error('error'); + + instance.handleImport(error); + + // then + expect(errorSpy).to.have.been.calledWith(error); + expect(warningSpy).not.to.have.been.called; + }); + + }); + +}); + + +// helpers ////////////////////////////// + +function noop() {} + +const TestTab = WithCachedState(MultiSheetTab); + +function renderTab(options = {}) { + const { + id, + xml, + layout, + onChanged, + onError, + onWarning, + onShown, + onLayoutChanged, + onContextMenu, + onAction, + providers + } = options; + + const withCachedState = mount( + + ); + + const wrapper = withCachedState.find(MultiSheetTab); + + const instance = wrapper.instance(); + + return { + instance, + wrapper + }; +} \ No newline at end of file diff --git a/client/src/app/tabs/__tests__/mocks/index.js b/client/src/app/tabs/__tests__/mocks/index.js new file mode 100644 index 0000000000..5777f7844a --- /dev/null +++ b/client/src/app/tabs/__tests__/mocks/index.js @@ -0,0 +1,13 @@ +import React, { Component } from 'react'; + +export class Editor extends Component { + render() { + return
; + } +} + +export const providers = [{ + type: 'editor', + editor: Editor, + defaultName: 'Editor' +}]; \ No newline at end of file diff --git a/client/src/app/tabs/bpmn/BpmnEditor.js b/client/src/app/tabs/bpmn/BpmnEditor.js index b12e9e5c57..48c0f13b16 100644 --- a/client/src/app/tabs/bpmn/BpmnEditor.js +++ b/client/src/app/tabs/bpmn/BpmnEditor.js @@ -180,7 +180,6 @@ export class BpmnEditor extends CachedComponent { } handleError = (event) => { - const { error } = event; @@ -192,6 +191,23 @@ export class BpmnEditor extends CachedComponent { onError(error); } + + handleImport = (error, warnings) => { + + const { + onImport + } = this.props; + + onImport(error, warnings); + + if (!error) { + this.setState({ + loading: false + }); + } + } + + updateState = (event) => { const { modeler @@ -264,22 +280,7 @@ export class BpmnEditor extends CachedComponent { }); // TODO(nikku): apply default element templates to initial diagram - modeler.importXML(xml, (err, warnings) => { - - if (warnings.length) { - console.log('WARNINGS', warnings); - } - - if (err) { - return this.handleError({ - error: err - }); - } - - this.setState({ - loading: false - }); - }); + modeler.importXML(xml, this.handleImport); } } diff --git a/client/src/app/tabs/bpmn/__tests__/BpmnEditorSpec.js b/client/src/app/tabs/bpmn/__tests__/BpmnEditorSpec.js index be0c238400..1904f9114f 100644 --- a/client/src/app/tabs/bpmn/__tests__/BpmnEditorSpec.js +++ b/client/src/app/tabs/bpmn/__tests__/BpmnEditorSpec.js @@ -222,22 +222,8 @@ describe('', function() { it('should handle template error'); - it('should handle import error', function() { - - // given - const errorSpy = spy(); - - // when - renderEditor('import-error', { - onError: errorSpy - }); - - // then - expect(errorSpy).to.have.been.called; - }); - - it('should handle XML export', async function() { + // given const errorSpy = spy(); @@ -263,6 +249,7 @@ describe('', function() { it('should handle image export error', async function() { + // given const errorSpy = spy(); @@ -288,6 +275,65 @@ describe('', function() { }); + + describe('import', function() { + + it('should import without errors and warnings', function() { + + // given + const importSpy = spy(); + + // when + renderEditor(diagramXML, { + onImport: importSpy + }); + + // then + expect(importSpy).to.have.been.calledWith(null, []); + }); + + + it('should import with warnings', function() { + + // given + const importSpy = (error, warnings) => { + + // then + expect(error).not.to.exist; + + expect(warnings).to.exist; + expect(warnings).to.have.length(1); + expect(warnings[0]).to.equal('warning'); + }; + + // when + renderEditor('import-warnings', { + onImport: importSpy + }); + }); + + + it('should import with error', function() { + + // given + const importSpy = (error, warnings) => { + + // then + expect(error).to.exist; + expect(error.message).to.equal('error'); + + expect(warnings).to.exist; + expect(warnings).to.have.length(0); + }; + + // when + renderEditor('import-error', { + onImport: importSpy + }); + }); + + }); + }); @@ -299,18 +345,21 @@ const TestEditor = WithCachedState(BpmnEditor); function renderEditor(xml, options = {}) { const { + id, layout, onError, + onImport, onLayoutChanged } = options; const slotFillRoot = mount( { + + const { + onImport + } = this.props; + + onImport(error, warnings); + + if (!error) { + this.setState({ + loading: false + }); + } + } + updateState = (event) => { const { modeler @@ -180,13 +199,11 @@ export class CmmnEditor extends CachedComponent { modeler.lastXML = xml; - modeler.importXML(xml, (err) => { - if (err) { - this.handleError({ - error: err - }); - } + this.setState({ + loading: true }); + + modeler.importXML(xml, this.handleImport); } } @@ -300,9 +317,15 @@ export class CmmnEditor extends CachedComponent { onLayoutChanged } = this.props; + const { + loading, + } = this.state; + return (
+