openmct/src/api/objects/ObjectAPISpec.js
Jesse Mazzella 16e1ac2529
Fixes for e2e tests following the Vue 3 compat upgrade (#6837)
* clock, timeConductor and appActions fixes

* Ensure realtime uses upstream context when available
Eliminate ambiguity when looking for time conductor locator

* Fix log plot e2e tests

* Fix displayLayout e2e tests

* Specify global time conductor to fix issues with duplicate selectors with independent time contexts

* a11y: ARIA for conductor and independent time conductor

* a11y: fix label collisions, specify 'Menu' in label

* Add watch mode

* fix(e2e): update appActions and tests to use a11y locators for ITC

* Don't remove the itc popup from the DOM. Just show/hide it once it's added the first time.

* test(e2e): disable one imagery test due to known bug

* Add fixme to tagging tests, issue described in 6822

* Fix locator for time conductor popups

* Improve how time bounds are set in independent time conductor.
Fix tests for flexible layout and timestrip

* Fix some tests for itc for display layouts

* Fix Inspector tabs remounting on change

* fix autoscale test and snapshot

* Fix telemetry table test

* Fix timestrip test

* e2e: move test info annotations to within test

* 6826: Fixes padStart error due to using it on a number rather than a string

* fix(e2e): update snapshots

* fix(e2e): fix restricted notebook locator

* fix(restrictedNotebook): fix issue causing sections not to update on lock

* fix(restrictedNotebook): fix issue causing snapshots to not be able to be deleted from a locked page

- Using `this.$delete(arr, index)` does not update the `length` property on the underlying target object, so it can lead to bizarre issues where your array is of length 4 but it has 3 objects in it.

* fix: replace all instances of `$delete` with `Array.splice()` or `delete`

* fix(e2e): fix grand search test

* fix(#3117): can remove item from displayLayout via tree context menu while viewing another item

* fix: remove typo 

* Wait for background image to load

* fix(#6832): timelist events can tick down

* fix: ensure that menuitems have the raw objects so emits work

* fix: assign new arrays instead of editing state in-place

* refactor(timelist): use `getClock()` instead of `clock()`

* Revert "refactor(timelist): use `getClock()` instead of `clock()`"

This reverts commit d88855311289bcf9e0d94799cdeee25c8628f65d.

* refactor(timelist): use new timeAPI

* Stop ticking when the independent time context is disabled (#6833)

* Turn off the clock ticket for independent time conductor when it is disabled

* Fix linting issues

---------

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>

* test: update couchdb notebook test

* fix: codeQL warnings

* fix(tree-item): infinite spinner issue

- Using `indexOf()` with an object was failing due to some items in the tree being Proxy-wrapped and others not. So instead, use `findIndex()` with a predicate that compares the navigationPaths of both objects

* [Timer] Remove "refresh" call, it is not needed (#6841)

* removing an unneccessary refresh that waas causing many get requests
* lets just pretend this never happened

* fix(mct-tree): maintain reactivity of all tree items

* Hide change role button in the indicator in cases where there is only… (#6840)

Hide change role button in the indicator in cases where there is only a single role available for the current user

---------

Co-authored-by: Shefali <simplyrender@gmail.com>
Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: David Tsay <david.e.tsay@nasa.gov>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2023-07-28 02:06:41 +00:00

604 lines
19 KiB
JavaScript

import ObjectAPI from './ObjectAPI.js';
import { createOpenMct, resetApplicationState } from '../../utils/testing';
describe('The Object API', () => {
let objectAPI;
let typeRegistry;
let openmct = {};
let mockDomainObject;
const TEST_NAMESPACE = 'test-namespace';
const TEST_KEY = 'test-key';
const USERNAME = 'Joan Q Public';
const FIFTEEN_MINUTES = 15 * 60 * 1000;
beforeEach((done) => {
typeRegistry = jasmine.createSpyObj('typeRegistry', ['get']);
const userProvider = {
isLoggedIn() {
return true;
},
getCurrentUser() {
return Promise.resolve({
getName() {
return USERNAME;
}
});
},
getPossibleRoles() {
return Promise.resolve([]);
}
};
openmct = createOpenMct();
openmct.user.setProvider(userProvider);
objectAPI = openmct.objects;
openmct.editor = {};
openmct.editor.isEditing = () => false;
mockDomainObject = {
identifier: {
namespace: TEST_NAMESPACE,
key: TEST_KEY
},
name: 'test object',
type: 'test-type'
};
openmct.on('start', () => {
done();
});
openmct.startHeadless();
});
afterEach(async () => {
await resetApplicationState(openmct);
});
describe('The save function', () => {
it('Rejects if no provider available', async () => {
let rejected = false;
objectAPI.providers = {};
objectAPI.fallbackProvider = null;
try {
await objectAPI.save(mockDomainObject);
} catch (error) {
rejected = true;
}
expect(rejected).toBe(true);
});
describe('when a provider is available', () => {
let mockProvider;
beforeEach(() => {
mockProvider = jasmine.createSpyObj('mock provider', ['create', 'update']);
mockProvider.create.and.returnValue(Promise.resolve(true));
mockProvider.update.and.returnValue(Promise.resolve(true));
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
});
it("Adds a 'created' timestamp to new objects", async () => {
await objectAPI.save(mockDomainObject);
expect(mockDomainObject.created).not.toBeUndefined();
});
it("Calls 'create' on provider if object is new", async () => {
await objectAPI.save(mockDomainObject);
expect(mockProvider.create).toHaveBeenCalled();
expect(mockProvider.update).not.toHaveBeenCalled();
});
it("Calls 'update' on provider if object is not new", async () => {
mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;
mockDomainObject.modified = Date.now();
await objectAPI.save(mockDomainObject);
expect(mockProvider.create).not.toHaveBeenCalled();
expect(mockProvider.update).toHaveBeenCalled();
});
describe('the persisted timestamp for existing objects', () => {
let persistedTimestamp;
beforeEach(() => {
persistedTimestamp = Date.now() - FIFTEEN_MINUTES;
mockDomainObject.persisted = persistedTimestamp;
mockDomainObject.modified = Date.now();
});
it('is updated', async () => {
await objectAPI.save(mockDomainObject);
expect(mockDomainObject.persisted).toBeDefined();
expect(mockDomainObject.persisted > persistedTimestamp).toBe(true);
});
it('is >= modified timestamp', async () => {
await objectAPI.save(mockDomainObject);
expect(mockDomainObject.persisted >= mockDomainObject.modified).toBe(true);
});
});
describe('the persisted timestamp for new objects', () => {
it('is updated', async () => {
await objectAPI.save(mockDomainObject);
expect(mockDomainObject.persisted).toBeDefined();
});
it('is >= modified timestamp', async () => {
await objectAPI.save(mockDomainObject);
expect(mockDomainObject.persisted >= mockDomainObject.modified).toBe(true);
});
});
it("Sets the current user for 'createdBy' on new objects", async () => {
await objectAPI.save(mockDomainObject);
expect(mockDomainObject.createdBy).toBe(USERNAME);
});
it("Sets the current user for 'modifedBy' on existing objects", async () => {
mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;
mockDomainObject.modified = Date.now();
await objectAPI.save(mockDomainObject);
expect(mockDomainObject.modifiedBy).toBe(USERNAME);
});
it('Does not persist if the object is unchanged', () => {
mockDomainObject.persisted = mockDomainObject.modified = Date.now();
objectAPI.save(mockDomainObject);
expect(mockProvider.create).not.toHaveBeenCalled();
expect(mockProvider.update).not.toHaveBeenCalled();
});
describe('Shows a notification on persistence conflict', () => {
beforeEach(() => {
openmct.notifications.error = jasmine.createSpy('error');
});
it('on create', () => {
mockProvider.create.and.returnValue(
Promise.reject(new openmct.objects.errors.Conflict('Test Conflict error'))
);
return objectAPI.save(mockDomainObject).catch(() => {
expect(openmct.notifications.error).toHaveBeenCalledWith(
`Conflict detected while saving ${TEST_NAMESPACE}:${TEST_KEY}`
);
});
});
it('on update', () => {
mockProvider.update.and.returnValue(
Promise.reject(new openmct.objects.errors.Conflict('Test Conflict error'))
);
mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;
mockDomainObject.modified = Date.now();
return objectAPI.save(mockDomainObject).catch(() => {
expect(openmct.notifications.error).toHaveBeenCalledWith(
`Conflict detected while saving ${TEST_NAMESPACE}:${TEST_KEY}`
);
});
});
});
});
});
describe('The get function', () => {
describe('when a provider is available', () => {
let mockProvider;
let mockInterceptor;
let anotherMockInterceptor;
let notApplicableMockInterceptor;
beforeEach(() => {
mockProvider = jasmine.createSpyObj('mock provider', ['get']);
mockProvider.get.and.returnValue(Promise.resolve(mockDomainObject));
mockInterceptor = jasmine.createSpyObj('mock interceptor', ['appliesTo', 'invoke']);
mockInterceptor.appliesTo.and.returnValue(true);
mockInterceptor.invoke.and.callFake((identifier, object) => {
return Object.assign(
{
changed: true
},
object
);
});
anotherMockInterceptor = jasmine.createSpyObj('another mock interceptor', [
'appliesTo',
'invoke'
]);
anotherMockInterceptor.appliesTo.and.returnValue(true);
anotherMockInterceptor.invoke.and.callFake((identifier, object) => {
return Object.assign(
{
alsoChanged: true
},
object
);
});
notApplicableMockInterceptor = jasmine.createSpyObj('not applicable mock interceptor', [
'appliesTo',
'invoke'
]);
notApplicableMockInterceptor.appliesTo.and.returnValue(false);
notApplicableMockInterceptor.invoke.and.callFake((identifier, object) => {
return Object.assign(
{
shouldNotBeChanged: true
},
object
);
});
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
objectAPI.addGetInterceptor(mockInterceptor);
objectAPI.addGetInterceptor(anotherMockInterceptor);
objectAPI.addGetInterceptor(notApplicableMockInterceptor);
});
it('Caches multiple requests for the same object', () => {
const promises = [];
expect(mockProvider.get.calls.count()).toBe(0);
promises.push(objectAPI.get(mockDomainObject.identifier));
expect(mockProvider.get.calls.count()).toBe(1);
promises.push(objectAPI.get(mockDomainObject.identifier));
expect(mockProvider.get.calls.count()).toBe(1);
return Promise.all(promises);
});
it('applies any applicable interceptors', () => {
expect(mockDomainObject.changed).toBeUndefined();
return objectAPI.get(mockDomainObject.identifier).then((object) => {
expect(object.changed).toBeTrue();
expect(object.alsoChanged).toBeTrue();
expect(object.shouldNotBeChanged).toBeUndefined();
});
});
it('displays a notification in the event of an error', () => {
openmct.notifications.warn = jasmine.createSpy('warn');
mockProvider.get.and.returnValue(
Promise.reject({
name: 'Error',
status: 404,
statusText: 'Not Found'
})
);
return objectAPI.get(mockDomainObject.identifier).catch(() => {
expect(openmct.notifications.warn).toHaveBeenCalledWith(
`Failed to retrieve object ${TEST_NAMESPACE}:${TEST_KEY}`
);
});
});
});
});
describe('the mutation API', () => {
let testObject;
let updatedTestObject;
let mutable;
let mockProvider;
let callbacks = [];
beforeEach(function () {
objectAPI = new ObjectAPI(typeRegistry, openmct);
testObject = {
identifier: {
namespace: TEST_NAMESPACE,
key: TEST_KEY
},
name: 'test object',
type: 'notebook',
otherAttribute: 'other-attribute-value',
modified: 0,
persisted: 0,
objectAttribute: {
embeddedObject: {
embeddedKey: 'embedded-value'
}
}
};
updatedTestObject = Object.assign(
{
otherAttribute: 'changed-attribute-value'
},
testObject
);
updatedTestObject.modified = 1;
updatedTestObject.persisted = 1;
mockProvider = jasmine.createSpyObj('mock provider', [
'get',
'create',
'update',
'observe',
'observeObjectChanges'
]);
mockProvider.get.and.returnValue(Promise.resolve(testObject));
mockProvider.create.and.returnValue(Promise.resolve(true));
mockProvider.update.and.returnValue(Promise.resolve(true));
mockProvider.observeObjectChanges.and.callFake(() => {
callbacks[0](updatedTestObject);
callbacks.splice(0, 1);
return () => {};
});
mockProvider.observe.and.callFake((id, callback) => {
if (callbacks.length === 0) {
callbacks.push(callback);
} else {
callbacks[0] = callback;
}
return () => {};
});
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
return objectAPI.getMutable(testObject.identifier).then((object) => {
mutable = object;
return mutable;
});
});
afterEach(() => {
mutable.$destroy();
});
it('mutates the original object', () => {
const MUTATED_NAME = 'mutated name';
objectAPI.mutate(testObject, 'name', MUTATED_NAME);
expect(testObject.name).toBe(MUTATED_NAME);
});
it('Provides a way of refreshing an object from the persistence store', () => {
const modifiedTestObject = JSON.parse(JSON.stringify(testObject));
const OTHER_ATTRIBUTE_VALUE = 'Modified value';
const NEW_ATTRIBUTE_VALUE = 'A new attribute';
modifiedTestObject.otherAttribute = OTHER_ATTRIBUTE_VALUE;
modifiedTestObject.newAttribute = NEW_ATTRIBUTE_VALUE;
delete modifiedTestObject.objectAttribute;
spyOn(objectAPI, 'get');
objectAPI.get.and.returnValue(Promise.resolve(modifiedTestObject));
expect(objectAPI.get).not.toHaveBeenCalled();
return objectAPI.refresh(testObject).then(() => {
expect(objectAPI.get).toHaveBeenCalledWith(testObject.identifier);
expect(testObject.otherAttribute).toEqual(OTHER_ATTRIBUTE_VALUE);
expect(testObject.newAttribute).toEqual(NEW_ATTRIBUTE_VALUE);
expect(testObject.objectAttribute).not.toBeDefined();
});
});
describe('uses a MutableDomainObject', () => {
it('and retains properties of original object ', function () {
expect(hasOwnProperty(mutable, 'identifier')).toBe(true);
expect(hasOwnProperty(mutable, 'otherAttribute')).toBe(true);
expect(mutable.identifier).toEqual(testObject.identifier);
expect(mutable.otherAttribute).toEqual(testObject.otherAttribute);
});
it('that is identical to original object when serialized', function () {
expect(JSON.stringify(mutable)).toEqual(JSON.stringify(testObject));
});
it('that observes for object changes', function () {
let mockListener = jasmine.createSpy('mockListener');
objectAPI.observe(testObject, '*', mockListener);
mockProvider.observeObjectChanges();
expect(mockListener).toHaveBeenCalled();
});
});
describe('uses events', function () {
let testObjectDuplicate;
let mutableSecondInstance;
beforeEach(function () {
// Duplicate object to guarantee we are not sharing object instance, which would invalidate test
testObjectDuplicate = JSON.parse(JSON.stringify(testObject));
mutableSecondInstance = objectAPI.toMutable(testObjectDuplicate);
});
afterEach(() => {
mutableSecondInstance.$destroy();
});
it('to stay synchronized when mutated', function () {
objectAPI.mutate(mutable, 'otherAttribute', 'new-attribute-value');
expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value');
});
it('to indicate when a property changes', function () {
let mutationCallback = jasmine.createSpy('mutation-callback');
let unlisten;
return new Promise(function (resolve) {
mutationCallback.and.callFake(resolve);
unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback);
objectAPI.mutate(mutable, 'otherAttribute', 'some-new-value');
}).then(function () {
expect(mutationCallback).toHaveBeenCalledWith('some-new-value', 'other-attribute-value');
unlisten();
});
});
it('to indicate when a child property has changed', function () {
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
let listeners = [];
return new Promise(function (resolve) {
objectAttributeCallback.and.callFake(resolve);
listeners.push(
objectAPI.observe(
mutableSecondInstance,
'objectAttribute.embeddedObject.embeddedKey',
embeddedKeyCallback
)
);
listeners.push(
objectAPI.observe(
mutableSecondInstance,
'objectAttribute.embeddedObject',
embeddedObjectCallback
)
);
listeners.push(
objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback)
);
objectAPI.mutate(
mutable,
'objectAttribute.embeddedObject.embeddedKey',
'updated-embedded-value'
);
}).then(function () {
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());
});
});
});
});
describe('getOriginalPath', () => {
let mockGrandParentObject;
let mockParentObject;
let mockChildObject;
beforeEach(() => {
const mockObjectProvider = jasmine.createSpyObj('mock object provider', [
'create',
'update',
'get'
]);
mockGrandParentObject = {
type: 'folder',
name: 'Grand Parent Folder',
location: 'fooNameSpace:child',
identifier: {
key: 'grandParent',
namespace: 'fooNameSpace'
}
};
mockParentObject = {
type: 'folder',
name: 'Parent Folder',
location: 'fooNameSpace:grandParent',
identifier: {
key: 'parent',
namespace: 'fooNameSpace'
}
};
mockChildObject = {
type: 'folder',
name: 'Child Folder',
location: 'fooNameSpace:parent',
identifier: {
key: 'child',
namespace: 'fooNameSpace'
}
};
// eslint-disable-next-line require-await
mockObjectProvider.get = async (identifier) => {
if (identifier.key === mockGrandParentObject.identifier.key) {
return mockGrandParentObject;
} else if (identifier.key === mockParentObject.identifier.key) {
return mockParentObject;
} else if (identifier.key === mockChildObject.identifier.key) {
return mockChildObject;
} else {
return null;
}
};
openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
mockObjectProvider.create.and.returnValue(Promise.resolve(true));
mockObjectProvider.update.and.returnValue(Promise.resolve(true));
openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
});
it('can construct paths even with cycles', async () => {
const objectPath = await objectAPI.getOriginalPath(mockChildObject.identifier);
expect(objectPath.length).toEqual(3);
});
});
describe('transactions', () => {
beforeEach(() => {
spyOn(openmct.editor, 'isEditing').and.returnValue(true);
});
it('there is no active transaction', () => {
expect(objectAPI.isTransactionActive()).toBe(false);
});
it('start a transaction', () => {
objectAPI.startTransaction();
expect(objectAPI.isTransactionActive()).toBe(true);
});
it('has active transaction', () => {
objectAPI.startTransaction();
const activeTransaction = objectAPI.getActiveTransaction();
expect(activeTransaction).not.toBe(null);
});
it('end a transaction', () => {
objectAPI.endTransaction();
expect(objectAPI.isTransactionActive()).toBe(false);
});
it('returns dirty object on get', (done) => {
spyOn(objectAPI, 'supportsMutation').and.returnValue(true);
objectAPI.startTransaction();
objectAPI.mutate(mockDomainObject, 'name', 'dirty object');
const dirtyObject = objectAPI.transaction.getDirtyObject(mockDomainObject.identifier);
objectAPI
.get(mockDomainObject.identifier)
.then((object) => {
const areEqual = JSON.stringify(object) === JSON.stringify(dirtyObject);
expect(areEqual).toBe(true);
})
.finally(done);
});
});
});
function hasOwnProperty(object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
}