diff --git a/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js new file mode 100644 index 0000000000..5d6c47f65c --- /dev/null +++ b/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js @@ -0,0 +1,79 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/* +Tests to verify log plot functionality. Note this test suite if very much under active development and should not +necessarily be used for reference when writing new tests in this area. +*/ + +const { test, expect } = require('../../../../pluginFixtures'); +const { createDomainObjectWithDefaults } = require('../../../../appActions'); + +test.describe('Stacked Plot', () => { + + test('Using the remove action removes the correct plot', async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' }); + const overlayPlot = await createDomainObjectWithDefaults(page, { + type: "Overlay Plot" + }); + + await createDomainObjectWithDefaults(page, { + type: "Sine Wave Generator", + name: 'swg a', + parent: overlayPlot.uuid + }); + await createDomainObjectWithDefaults(page, { + type: "Sine Wave Generator", + name: 'swg b', + parent: overlayPlot.uuid + }); + await createDomainObjectWithDefaults(page, { + type: "Sine Wave Generator", + name: 'swg c', + parent: overlayPlot.uuid + }); + await page.goto(overlayPlot.url); + await page.click('button[title="Edit"]'); + + // Expand the elements pool vertically + await page.locator('.l-pane__handle').nth(2).hover({ trial: true }); + await page.mouse.down(); + await page.mouse.move(0, 100); + await page.mouse.up(); + + await page.locator('.js-elements-pool__tree >> text=swg b').click({ button: 'right' }); + await page.locator('li[role="menuitem"]:has-text("Remove")').click(); + await page.locator('.js-overlay .js-overlay__button >> text=OK').click(); + + // Wait until the number of elements in the elements pool has changed, and then confirm that the correct children were retained + // await page.waitForFunction(() => { + // return Array.from(document.querySelectorAll('.js-elements-pool__tree .js-elements-pool__item')).length === 2; + // }); + // Wait until there are only two items in the elements pool (ie the remove action has completed) + await expect(page.locator('.js-elements-pool__tree .js-elements-pool__item')).toHaveCount(2); + + // Confirm that the elements pool contains the items we expect + await expect(page.locator('.js-elements-pool__tree >> text=swg a')).toHaveCount(1); + await expect(page.locator('.js-elements-pool__tree >> text=swg b')).toHaveCount(0); + await expect(page.locator('.js-elements-pool__tree >> text=swg c')).toHaveCount(1); + }); +}); diff --git a/src/api/composition/CompositionAPISpec.js b/src/api/composition/CompositionAPISpec.js index f8d33ed072..202a946773 100644 --- a/src/api/composition/CompositionAPISpec.js +++ b/src/api/composition/CompositionAPISpec.js @@ -1,47 +1,35 @@ -import CompositionAPI from './CompositionAPI'; +import { createOpenMct, resetApplicationState } from '../../utils/testing'; import CompositionCollection from './CompositionCollection'; describe('The Composition API', function () { let publicAPI; let compositionAPI; - let topicService; - let mutationTopic; - beforeEach(function () { + beforeEach(function (done) { + publicAPI = createOpenMct(); + compositionAPI = publicAPI.composition; - mutationTopic = jasmine.createSpyObj('mutationTopic', [ - 'listen' - ]); - topicService = jasmine.createSpy('topicService'); - topicService.and.returnValue(mutationTopic); - publicAPI = {}; - publicAPI.objects = jasmine.createSpyObj('ObjectAPI', [ - 'get', - 'mutate', - 'observe', - 'areIdsEqual' + const mockObjectProvider = jasmine.createSpyObj("mock provider", [ + "create", + "update", + "get" ]); - publicAPI.objects.areIdsEqual.and.callFake(function (id1, id2) { - return id1.namespace === id2.namespace && id1.key === id2.key; + mockObjectProvider.create.and.returnValue(Promise.resolve(true)); + mockObjectProvider.update.and.returnValue(Promise.resolve(true)); + mockObjectProvider.get.and.callFake((identifier) => { + return Promise.resolve({identifier}); }); - publicAPI.composition = jasmine.createSpyObj('CompositionAPI', [ - 'checkPolicy' - ]); - publicAPI.composition.checkPolicy.and.returnValue(true); + publicAPI.objects.addProvider('test', mockObjectProvider); + publicAPI.objects.addProvider('custom', mockObjectProvider); - publicAPI.objects.eventEmitter = jasmine.createSpyObj('eventemitter', [ - 'on' - ]); - publicAPI.objects.get.and.callFake(function (identifier) { - return Promise.resolve({identifier: identifier}); - }); - publicAPI.$injector = jasmine.createSpyObj('$injector', [ - 'get' - ]); - publicAPI.$injector.get.and.returnValue(topicService); - compositionAPI = new CompositionAPI(publicAPI); + publicAPI.on('start', done); + publicAPI.startHeadless(); + }); + + afterEach(() => { + return resetApplicationState(publicAPI); }); it('returns falsy if an object does not support composition', function () { @@ -106,6 +94,9 @@ describe('The Composition API', function () { let listener; beforeEach(function () { listener = jasmine.createSpy('reorderListener'); + spyOn(publicAPI.objects, 'mutate'); + publicAPI.objects.mutate.and.callThrough(); + composition.on('reorder', listener); return composition.load(); @@ -136,18 +127,20 @@ describe('The Composition API', function () { }); }); it('supports adding an object to composition', function () { - let addListener = jasmine.createSpy('addListener'); let mockChildObject = { identifier: { key: 'mock-key', namespace: '' } }; - composition.on('add', addListener); - composition.add(mockChildObject); - expect(domainObject.composition.length).toBe(4); - expect(domainObject.composition[3]).toEqual(mockChildObject.identifier); + return new Promise((resolve) => { + composition.on('add', resolve); + composition.add(mockChildObject); + }).then(() => { + expect(domainObject.composition.length).toBe(4); + expect(domainObject.composition[3]).toEqual(mockChildObject.identifier); + }); }); }); diff --git a/src/api/composition/CompositionProvider.js b/src/api/composition/CompositionProvider.js index 792a3db480..c2e49faa4c 100644 --- a/src/api/composition/CompositionProvider.js +++ b/src/api/composition/CompositionProvider.js @@ -224,7 +224,7 @@ export default class CompositionProvider { * @private * @param {DomainObject} oldDomainObject */ - #onMutation(oldDomainObject) { + #onMutation(newDomainObject, oldDomainObject) { const id = objectUtils.makeKeyString(oldDomainObject.identifier); const listeners = this.#listeningTo[id]; @@ -232,8 +232,8 @@ export default class CompositionProvider { return; } - const oldComposition = listeners.composition.map(objectUtils.makeKeyString); - const newComposition = oldDomainObject.composition.map(objectUtils.makeKeyString); + const oldComposition = oldDomainObject.composition.map(objectUtils.makeKeyString); + const newComposition = newDomainObject.composition.map(objectUtils.makeKeyString); const added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString); const removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString); @@ -248,8 +248,6 @@ export default class CompositionProvider { }; } - listeners.composition = newComposition.map(objectUtils.parseKeyString); - added.forEach(function (addedChild) { listeners.add.forEach(notify(addedChild)); }); diff --git a/src/api/composition/DefaultCompositionProvider.js b/src/api/composition/DefaultCompositionProvider.js index 6c242a25cd..93453b39ca 100644 --- a/src/api/composition/DefaultCompositionProvider.js +++ b/src/api/composition/DefaultCompositionProvider.js @@ -99,8 +99,7 @@ export default class DefaultCompositionProvider extends CompositionProvider { objectListeners = this.listeningTo[keyString] = { add: [], remove: [], - reorder: [], - composition: [].slice.apply(domainObject.composition) + reorder: [] }; } @@ -172,8 +171,9 @@ export default class DefaultCompositionProvider extends CompositionProvider { */ add(parent, childId) { if (!this.includes(parent, childId)) { - parent.composition.push(childId); - this.publicAPI.objects.mutate(parent, 'composition', parent.composition); + const composition = structuredClone(parent.composition); + composition.push(childId); + this.publicAPI.objects.mutate(parent, 'composition', composition); } } diff --git a/src/api/objects/MutableDomainObject.js b/src/api/objects/MutableDomainObject.js index df0965d3b5..8737d0d3ae 100644 --- a/src/api/objects/MutableDomainObject.js +++ b/src/api/objects/MutableDomainObject.js @@ -75,21 +75,23 @@ class MutableDomainObject { return eventOff; } $set(path, value) { + const oldModel = structuredClone(this); + const oldValue = _.get(oldModel, path); MutableDomainObject.mutateObject(this, path, value); //Emit secret synchronization event first, so that all objects are in sync before subsequent events fired. this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), this); //Emit a general "any object" event - this._globalEventEmitter.emit(ANY_OBJECT_EVENT, this); + this._globalEventEmitter.emit(ANY_OBJECT_EVENT, this, oldModel); //Emit wildcard event, with path so that callback knows what changed - this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this, path, value); + this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this, path, value, oldModel, oldValue); //Emit events specific to properties affected let parentPropertiesList = path.split('.'); for (let index = parentPropertiesList.length; index > 0; index--) { let parentPropertyPath = parentPropertiesList.slice(0, index).join('.'); - this._globalEventEmitter.emit(qualifiedEventName(this, parentPropertyPath), _.get(this, parentPropertyPath)); + this._globalEventEmitter.emit(qualifiedEventName(this, parentPropertyPath), _.get(this, parentPropertyPath), _.get(oldModel, parentPropertyPath)); } //TODO: Emit events for listeners of child properties when parent changes. @@ -124,7 +126,7 @@ class MutableDomainObject { Object.assign(mutable, object); mutable.$observe('$_synchronize_model', (updatedObject) => { - let clone = JSON.parse(JSON.stringify(updatedObject)); + let clone = structuredClone(updatedObject); utils.refresh(mutable, clone); }); diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js index 2edda6781b..052192db3d 100644 --- a/src/api/objects/ObjectAPI.js +++ b/src/api/objects/ObjectAPI.js @@ -648,7 +648,7 @@ export default class ObjectAPI { * @param {module:openmct.DomainObject} object the object to observe * @param {string} path the property to observe * @param {Function} callback a callback to invoke when new values for - * this property are observed + * this property are observed. * @method observe * @memberof module:openmct.ObjectAPI# */ diff --git a/src/api/objects/ObjectAPISpec.js b/src/api/objects/ObjectAPISpec.js index 6940b7426d..dfe0e42114 100644 --- a/src/api/objects/ObjectAPISpec.js +++ b/src/api/objects/ObjectAPISpec.js @@ -399,7 +399,7 @@ describe("The Object API", () => { unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback); objectAPI.mutate(mutable, 'otherAttribute', 'some-new-value'); }).then(function () { - expect(mutationCallback).toHaveBeenCalledWith('some-new-value'); + expect(mutationCallback).toHaveBeenCalledWith('some-new-value', 'other-attribute-value'); unlisten(); }); }); @@ -419,14 +419,20 @@ describe("The Object API", () => { objectAPI.mutate(mutable, 'objectAttribute.embeddedObject.embeddedKey', 'updated-embedded-value'); }).then(function () { - expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value'); + expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value', 'embedded-value'); expect(embeddedObjectCallback).toHaveBeenCalledWith({ embeddedKey: 'updated-embedded-value' + }, { + embeddedKey: 'embedded-value' }); expect(objectAttributeCallback).toHaveBeenCalledWith({ embeddedObject: { embeddedKey: 'updated-embedded-value' } + }, { + embeddedObject: { + embeddedKey: 'embedded-value' + } }); listeners.forEach(listener => listener()); diff --git a/src/api/overlays/components/OverlayComponent.vue b/src/api/overlays/components/OverlayComponent.vue index 8c84998c04..e92179c977 100644 --- a/src/api/overlays/components/OverlayComponent.vue +++ b/src/api/overlays/components/OverlayComponent.vue @@ -1,5 +1,5 @@