diff --git a/apis/nucleus/src/object/get-object.js b/apis/nucleus/src/object/get-object.js index 7bc3f15ee..f187761cc 100644 --- a/apis/nucleus/src/object/get-object.js +++ b/apis/nucleus/src/object/get-object.js @@ -1,19 +1,6 @@ import vizualizationAPI from '../viz'; import ObjectAPI from './object-api'; - -export function observe(model, objectAPI) { - const onChanged = () => - model.getLayout().then(layout => { - objectAPI.setLayout(layout); - }); - model.on('changed', onChanged); - model.once('closed', () => { - model.removeListener('changed', onChanged); - objectAPI.close(); - }); - - onChanged(); -} +import { observe } from './observer'; export default function initiate(getCfg, optional, context) { return context.app.getObject(getCfg.id).then(model => { @@ -24,7 +11,7 @@ export default function initiate(getCfg, optional, context) { const objectAPI = new ObjectAPI(model, context, viz); - observe(model, objectAPI); + observe(model, layout => objectAPI.setLayout(layout)); // TODO - call unobserve when viz is destroyed const api = objectAPI.getPublicAPI(); diff --git a/apis/nucleus/src/object/observer.js b/apis/nucleus/src/object/observer.js index 0287d9523..c14d5fcd1 100644 --- a/apis/nucleus/src/object/observer.js +++ b/apis/nucleus/src/object/observer.js @@ -46,15 +46,23 @@ export function observe(model, callback, property = 'layout') { affected.forEach(key => { c.props[key].state = STATES.VALIDATING; const method = OBSERVABLE[key].filter(m => model[m])[0]; - model[method]().then(value => { - if (cache[model.id] && cache[model.id].props[key]) { - if (cache[model.id].props[key].state < STATES.CLOSED && cache[model.id].props[key].state !== STATES.VALID) { - cache[model.id].props[key].state = STATES.VALID; - cache[model.id].props[key].value = value; - cache[model.id].props[key].callbacks.forEach(cb => cb(value)); + model[method]() + .then(value => { + if (cache[model.id] && cache[model.id].props[key]) { + if ( + cache[model.id].props[key].state < STATES.CLOSED && + cache[model.id].props[key].state !== STATES.VALID + ) { + cache[model.id].props[key].state = STATES.VALID; + cache[model.id].props[key].value = value; + cache[model.id].props[key].callbacks.forEach(cb => cb(value)); + } } - } - }); + }) + .catch(() => { + // TODO - retry? + cache[model.id].props[key].state = STATES.INVALID; + }); }); }; diff --git a/apis/nucleus/src/selections/app-selections.js b/apis/nucleus/src/selections/app-selections.js index 9d7cbabdd..8c4fc4585 100644 --- a/apis/nucleus/src/selections/app-selections.js +++ b/apis/nucleus/src/selections/app-selections.js @@ -15,11 +15,13 @@ const create = app => { let modalObject; // let mounted; let lyt; + let currentSelectionsModel; + let prom; const api = { model: app, switchModal(object, path, accept = true) { if (object === modalObject) { - return Promise.resolve(); + return prom || Promise.resolve(); } if (modalObject) { modalObject.endSelections(accept); @@ -30,17 +32,22 @@ const create = app => { // TODO check model state modalObject = object; api.emit('modal', modalObject._selections); - return modalObject.beginSelections(Array.isArray(path) ? path : [path]).catch(err => { - if (err.code === 6003) { - // If another object already is in modal -> abort and take over - return api.abortModal().then(() => object.beginSelections(Array.isArray(path) ? path : [path])); - } - throw err; + prom = currentSelectionsModel.then(() => { + // do not return the call to beginSelection to avoid waiting for it's response + modalObject.beginSelections(Array.isArray(path) ? path : [path]).catch(err => { + if (err.code === 6003) { + // If another object already is in modal -> abort and take over + return api.abortModal().then(() => object.beginSelections(Array.isArray(path) ? path : [path])); + } + throw err; + }); }); + return prom; } modalObject = null; api.emit('modal-unset'); - return Promise.resolve(); + prom = Promise.resolve(); + return prom; }, isModal(objectModel) { // TODO check model state @@ -83,7 +90,7 @@ const create = app => { eventmixin(api); - modelCache( + currentSelectionsModel = modelCache( { qInfo: { qType: 'current-selections', @@ -94,47 +101,51 @@ const create = app => { alternateStates: [], }, app - ).then(model => { - observe(app, appLayout => { - const states = [...appLayout.qStateNames].map(s => ({ - stateName: s, // need this as reference in selection toolbar since qSelectionObject.qStateName is not in the layout - qSelectionObjectDef: { - qStateName: s, - }, - })); - const existingStates = (lyt ? lyt.alternateStates.map(s => s.stateName) : []).join('::'); - const newStates = appLayout.qStateNames.map(s => s).join('::'); - if (existingStates !== newStates) { - model.applyPatches( - [ - { - qOp: 'replace', - qPath: '/alternateStates', - qValue: JSON.stringify(states), - }, - ], - true - ); - } - }); - - observe(model, layout => { - canGoBack = false; - canGoForward = false; - canClear = false; - [layout, ...layout.alternateStates].forEach(state => { - canGoBack = canGoBack || state.qSelectionObject.qBackCount > 0; - canGoForward = canGoForward || state.qSelectionObject.qForwardCount > 0; - canClear = canClear || state.qSelectionObject.qSelections.filter(s => s.qLocked !== true).length > 0; + ) + .then(model => { + observe(app, appLayout => { + const states = [...appLayout.qStateNames].map(s => ({ + stateName: s, // need this as reference in selection toolbar since qSelectionObject.qStateName is not in the layout + qSelectionObjectDef: { + qStateName: s, + }, + })); + const existingStates = (lyt ? lyt.alternateStates.map(s => s.stateName) : []).join('::'); + const newStates = appLayout.qStateNames.map(s => s).join('::'); + if (existingStates !== newStates) { + model.applyPatches( + [ + { + qOp: 'replace', + qPath: '/alternateStates', + qValue: JSON.stringify(states), + }, + ], + true + ); + } }); - lyt = layout; - api.emit('changed'); + + observe(model, layout => { + canGoBack = false; + canGoForward = false; + canClear = false; + [layout, ...layout.alternateStates].forEach(state => { + canGoBack = canGoBack || state.qSelectionObject.qBackCount > 0; + canGoForward = canGoForward || state.qSelectionObject.qForwardCount > 0; + canClear = canClear || state.qSelectionObject.qSelections.filter(s => s.qLocked !== true).length > 0; + }); + lyt = layout; + api.emit('changed'); + }); + model.once('closed', () => { + app._selections = null; // eslint-disable-line no-param-reassign + cache[app.id] = null; + }); + }) + .catch(() => { + // do something }); - model.once('closed', () => { - app._selections = null; // eslint-disable-line no-param-reassign - cache[app.id] = null; - }); - }); return api; }; diff --git a/apis/nucleus/src/selections/object-selections.js b/apis/nucleus/src/selections/object-selections.js index 1494edcec..36aa50d2d 100644 --- a/apis/nucleus/src/selections/object-selections.js +++ b/apis/nucleus/src/selections/object-selections.js @@ -55,16 +55,18 @@ export default function(model, app) { return appAPI().switchModal(null, null, false, false); }, select(s) { - this.begin([s.params[0]]); + const b = this.begin([s.params[0]]); if (!appAPI().isModal()) { return; } hasSelected = true; - model[s.method](...s.params).then(qSuccess => { - if (!qSuccess) { - this.clear(); - } - }); + b.then(() => + model[s.method](...s.params).then(qSuccess => { + if (!qSuccess) { + this.clear(); + } + }) + ); }, canClear() { return hasSelected && layout.qSelectionInfo.qMadeSelections; diff --git a/commands/create/templates/none/_package.json b/commands/create/templates/none/_package.json index a2abc0183..8c587bce4 100644 --- a/commands/create/templates/none/_package.json +++ b/commands/create/templates/none/_package.json @@ -18,7 +18,7 @@ "build": "nebula build", "lint": "eslint src", "start": "nebula serve", - "test:integration": "aw puppet --testExt '*.int.js' --glob 'test/integration/**/*.int.js' --chrome.headless true --mocha.timeout 15000" + "test:integration": "aw puppet --testExt '*.int.js' --glob 'test/integration/**/*.int.js'" }, "devDependencies": { "@after-work.js/aw": "^6.0.3", diff --git a/commands/create/templates/none/test/integration/setup.int.js b/commands/create/templates/none/test/integration/setup.int.js index 9916015e1..57b1e5751 100644 --- a/commands/create/templates/none/test/integration/setup.int.js +++ b/commands/create/templates/none/test/integration/setup.int.js @@ -2,7 +2,8 @@ const serve = require('@nebula.js/cli-serve'); // eslint-disable-line let s; -before(async () => { +before(async function setup() { + this.timeout(15000); s = await serve({ open: false, }); diff --git a/commands/create/templates/picasso/barchart/test/integration/interaction.int.js b/commands/create/templates/picasso/barchart/test/integration/interaction.int.js index c20e92566..c540388e1 100644 --- a/commands/create/templates/picasso/barchart/test/integration/interaction.int.js +++ b/commands/create/templates/picasso/barchart/test/integration/interaction.int.js @@ -9,8 +9,12 @@ describe('interaction', () => { await page.click('rect[data-label="K"]'); await page.click('rect[data-label="S"]'); + + await page.waitForSelector('button[title="Confirm selection"]'); await page.click('button[title="Confirm selection"]'); + await page.waitFor(100); // wait a bit to make sure websocket traffic has gone through + const rects = await page.$$eval('rect[data-label]', sel => sel.map(r => r.getAttribute('data-label'))); expect(rects).to.eql(['K', 'S']); }); diff --git a/commands/create/templates/picasso/common/_package.json b/commands/create/templates/picasso/common/_package.json index f7e77f395..fab2151a4 100644 --- a/commands/create/templates/picasso/common/_package.json +++ b/commands/create/templates/picasso/common/_package.json @@ -18,7 +18,7 @@ "build": "nebula build", "lint": "eslint src", "start": "nebula serve", - "test:integration": "aw puppet --testExt '*.int.js' --glob 'test/integration/**/*.int.js' --chrome.headless true --chrome.slowMo 10" + "test:integration": "aw puppet --testExt '*.int.js' --glob 'test/integration/**/*.int.js'" }, "devDependencies": { "@after-work.js/aw": "^6.0.3", diff --git a/commands/create/templates/picasso/common/test/integration/setup.int.js b/commands/create/templates/picasso/common/test/integration/setup.int.js index 66977c678..0fbf8bd04 100644 --- a/commands/create/templates/picasso/common/test/integration/setup.int.js +++ b/commands/create/templates/picasso/common/test/integration/setup.int.js @@ -2,7 +2,8 @@ const serve = require('@nebula.js/cli-serve'); // eslint-disable-line let s; -before(async () => { +before(async function setup() { + this.timeout(15000); // to allow time for the server to start s = await serve({ build: false, });