Compare commits

..

5 Commits

Author SHA1 Message Date
John Hill
9f8ba345e7 Add e2e tests to npm package (#5930) 2022-10-31 13:49:15 -07:00
Shefali Joshi
52d2f2dfae Fix typo for publish (#5936) 2022-10-31 20:31:54 +00:00
Andrew Henry
0cf9bc9cf4 Fix persistence timestamp (#5916)
* Calculate persisted timestamp last
* Added regression tests
* Correct transaction handling code
* Code cleanup
2022-10-27 09:18:43 -07:00
Shefali Joshi
8e3146ee32 Ensure properties stay in sync and are committed only once (#5717)
* Ensure form properties stay in sync
* Separate out overlay based forms and custom forms
* Use a transaction to save properties
* Fix GaugeController to not depend on event emitted from FormsAPI
* refactor showForms to call showCustomForm

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-10-25 11:42:37 -07:00
Jesse Mazzella
62fe9a94dc Bump version to 2.1.2 2022-10-21 18:10:50 -07:00
90 changed files with 590 additions and 1573 deletions

View File

@@ -13,17 +13,11 @@ updates:
- "pr:daveit"
- "pr:platform"
ignore:
#We have to source the playwright container which is not detected by Dependabot
- dependency-name: "@playwright/test"
- dependency-name: "playwright-core"
#Lots of noise in these type patch releases.
- dependency-name: "@babel/eslint-parser"
- dependency-name: "@playwright/test" #We have to source the playwright container which is not detected by Dependabot
- dependency-name: "playwright-core" #We have to source the playwright container which is not detected by Dependabot
- dependency-name: "@babel/eslint-parser" #Lots of noise in these type patch releases.
update-types: ["version-update:semver-patch"]
- dependency-name: "eslint-plugin-vue"
update-types: ["version-update:semver-patch"]
- dependency-name: "babel-loader"
update-types: ["version-update:semver-patch"]
- dependency-name: "sinon"
- dependency-name: "eslint-plugin-vue" #Lots of noise in these type patch releases.
update-types: ["version-update:semver-patch"]
- package-ecosystem: "github-actions"
directory: "/"

View File

@@ -16,11 +16,7 @@ jobs:
with:
node-version: 16
- run: npm install
- run: |
echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" >> ~/.npmrc
npm whoami
npm publish --access=public --tag unstable openmct
# - run: npm test
- run: npm test
publish-npm-prerelease:
needs: build

View File

@@ -10,7 +10,7 @@ accept changes from external contributors.
The short version:
1. Write your contribution or describe your idea in the form of a [GitHub issue](https://github.com/nasa/openmct/issues/new/choose) or [start a GitHub discussion](https://github.com/nasa/openmct/discussions).
1. Write your contribution or describe your idea in the form of an [GitHub issue](https://github.com/nasa/openmct/issues/new/choose) or [Starting a GitHub Discussion](https://github.com/nasa/openmct/discussions)
2. Make sure your contribution meets code, test, and commit message
standards as described below.
3. Submit a pull request from a topic branch back to `master`. Include a check

View File

@@ -6,8 +6,10 @@ Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting S
Once you've created something amazing with Open MCT, showcase your work in our GitHub Discussions [Show and Tell](https://github.com/nasa/openmct/discussions/categories/show-and-tell) section. We love seeing unique and wonderful implementations of Open MCT!
![Screen Shot 2022-11-23 at 9 51 36 AM](https://user-images.githubusercontent.com/4215777/203617422-4d912bfc-766f-4074-8324-409d9bbe7c05.png)
## See Open MCT in Action
Try Open MCT now with our [live demo](https://openmct-demo.herokuapp.com/).
![Demo](https://nasa.github.io/openmct/static/res/images/Open-MCT.Browse.Layout.Mars-Weather-1.jpg)
## Building and Running Open MCT Locally

View File

@@ -276,36 +276,14 @@ Skipping based on browser version (Rarely used): <https://github.com/microsoft/p
- Leverage `await page.goto('./', { waitUntil: 'networkidle' });`
- Avoid repeated setup to test to test a single assertion. Write longer tests with multiple soft assertions.
### How to write a great test (WIP)
- Use our [App Actions](./appActions.js) for performing common actions whenever applicable.
- If you create an object outside of using the `createDomainObjectWithDefaults` App Action, make sure to fill in the 'Notes' section of your object with `page.testNotes`:
```js
// Fill the "Notes" section with information about the
// currently running test and its project.
const { testNotes } = page;
const notesInput = page.locator('form[name="mctForm"] #notes-textarea');
await notesInput.fill(testNotes);
```
### How to write a great test (TODO)
#### How to write a great visual test (TODO)
#### How to write a great network test
- Where possible, it is best to mock out third-party network activity to ensure we are testing application behavior of Open MCT.
- It is best to be as specific as possible about the expected network request/response structures in creating your mocks.
- Make sure to only mock requests which are relevant to the specific behavior being tested.
- Where possible, network requests and responses should be treated in an order-agnostic manner, as the order in which certain requests/responses happen is dynamic and subject to change.
Some examples of mocking network responses in regards to CouchDB can be found in our [couchdb.e2e.spec.js](./tests/functional/couchdb.e2e.spec.js) test file.
### Best Practices
For now, our best practices exist as self-tested, living documentation in our [exampleTemplate.e2e.spec.js](./tests/framework/exampleTemplate.e2e.spec.js) file.
For best practices with regards to mocking network responses, see our [couchdb.e2e.spec.js](./tests/functional/couchdb.e2e.spec.js) file.
### Tips & Tricks (TODO)
The following contains a list of tips and tricks which don't exactly fit into a FAQ or Best Practices doc.

View File

@@ -46,7 +46,6 @@
*/
const Buffer = require('buffer').Buffer;
const genUuid = require('uuid').v4;
/**
* This common function creates a domain object with the default options. It is the preferred way of creating objects
@@ -57,10 +56,6 @@ const genUuid = require('uuid').v4;
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
*/
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
if (!name) {
name = `${type}:${genUuid()}`;
}
const parentUrl = await getHashUrlToDomainObject(page, parent);
// Navigate to the parent object. This is necessary to create the object
@@ -72,18 +67,13 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
await page.click('button:has-text("Create")');
// Click the object specified by 'type'
await page.click(`li[role='menuitem']:text("${type}")`);
await page.click(`li:text("${type}")`);
// Modify the name input field of the domain object to accept 'name'
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
await nameInput.fill("");
await nameInput.fill(name);
if (page.testNotes) {
// Fill the "Notes" section with information about the
// currently running test and its project.
const notesInput = page.locator('form[name="mctForm"] #notes-textarea');
await notesInput.fill(page.testNotes);
if (name) {
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
await nameInput.fill("");
await nameInput.fill(name);
}
// Click OK button and wait for Navigate event
@@ -106,8 +96,8 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
}
return {
name,
uuid,
name: name || `Unnamed ${type}`,
uuid: uuid,
url: objectUrl
};
}

View File

@@ -20,8 +20,6 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { createDomainObjectWithDefaults } = require('../appActions');
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
/**
@@ -40,17 +38,24 @@ async function enterTextEntry(page, text) {
/**
* @param {import('@playwright/test').Page} page
*/
async function dragAndDropEmbed(page, notebookObject) {
// Create example telemetry object
const swg = await createDomainObjectWithDefaults(page, {
type: "Sine Wave Generator"
});
// Navigate to notebook
await page.goto(notebookObject.url);
// Expand the tree to reveal the notebook
await page.click('button[title="Show selected item in tree"]');
// Drag and drop the SWG into the notebook
await page.dragAndDrop(`text=${swg.name}`, NOTEBOOK_DROP_AREA);
async function dragAndDropEmbed(page, myItemsFolderName) {
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Sine Wave Generator")
await page.locator('li:has-text("Sine Wave Generator")').click();
// Click form[name="mctForm"] >> text=My Items
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
// Click text=OK
await page.locator('text=OK').click();
// Click text=Open MCT My Items >> span >> nth=3
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
// Click text=Unnamed CUSTOM_NAME
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed CUSTOM_NAME').click()
]);
await page.dragAndDrop('text=UNNAMED SINE WAVE GENERATOR', NOTEBOOK_DROP_AREA);
}
// eslint-disable-next-line no-undef

View File

@@ -126,21 +126,13 @@ exports.test = test.extend({
// This should follow in the Project's configuration. Can be set to 'snow' in playwright config.js
theme: [theme, { option: true }],
// eslint-disable-next-line no-shadow
page: async ({ page, theme }, use, testInfo) => {
page: async ({ page, theme }, use) => {
// eslint-disable-next-line playwright/no-conditional-in-test
if (theme === 'snow') {
//inject snow theme
await page.addInitScript({ path: path.join(__dirname, './helper', './useSnowTheme.js') });
}
// Attach info about the currently running test and its project.
// This will be used by appActions to fill in the created
// domain object's notes.
page.testNotes = [
`${testInfo.titlePath.join('\n')}`,
`${testInfo.project.name}`
].join('\n');
await use(page);
},
myItemsFolderName: [myItemsFolderName, { option: true }],
@@ -148,5 +140,22 @@ exports.test = test.extend({
openmctConfig: async ({ myItemsFolderName }, use) => {
await use({ myItemsFolderName });
}
// objectCreateOptions: [objectCreateOptions, {option: true}],
// eslint-disable-next-line no-shadow
// domainObject: [async ({ page, objectCreateOptions }, use) => {
// // FIXME: This is a false-positive caused by a bug in the eslint-plugin-playwright rule.
// // eslint-disable-next-line playwright/no-conditional-in-test
// if (objectCreateOptions === null) {
// await use(page);
// return;
// }
// //Go to baseURL
// await page.goto('./', { waitUntil: 'networkidle' });
// const uuid = await getOrCreateDomainObject(page, objectCreateOptions);
// await use({ uuid });
// }, { auto: true }]
});
exports.expect = expect;

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test, expect } = require('../../pluginFixtures.js');
const { test, expect } = require('../../baseFixtures.js');
const { createDomainObjectWithDefaults } = require('../../appActions.js');
test.describe('AppActions', () => {
@@ -50,11 +50,11 @@ test.describe('AppActions', () => {
});
await page.goto(timer1.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer1.name);
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Foo');
await page.goto(timer2.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer2.name);
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Bar');
await page.goto(timer3.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer3.name);
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Baz');
});
await test.step('Create multiple nested objects in a row', async () => {
@@ -74,11 +74,11 @@ test.describe('AppActions', () => {
parent: folder2.uuid
});
await page.goto(folder1.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder1.name);
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Foo');
await page.goto(folder2.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder2.name);
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Bar');
await page.goto(folder3.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder3.name);
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Baz');
expect(folder1.url).toBe(`${e2eFolder.url}/${folder1.uuid}`);
expect(folder2.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}`);

View File

@@ -45,7 +45,7 @@
*/
// Structure: Some standard Imports. Please update the required pathing.
const { test, expect } = require('../../pluginFixtures');
const { test, expect } = require('../../baseFixtures');
const { createDomainObjectWithDefaults } = require('../../appActions');
/**
@@ -144,5 +144,5 @@ async function renameTimerFrom3DotMenu(page, timerUrl, newNameForTimer) {
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(newNameForTimer);
// Click Ok button to Save
await page.locator('button:has-text("OK")').click();
await page.locator('text=OK').click();
}

View File

@@ -43,14 +43,14 @@ test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
await page.locator('button:has-text("Create")').click();
// add sine wave generator with defaults
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
await page.locator('li:has-text("Sine Wave Generator")').click();
//Add a 5000 ms Delay
await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000');
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
@@ -58,7 +58,7 @@ test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
// focus the overlay plot
await page.goto(overlayPlot.url);
await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlot.name);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
//Save localStorage for future test execution
await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' });
});

View File

@@ -25,9 +25,9 @@
*
*/
const { test, expect } = require('../../pluginFixtures');
const { test, expect } = require('../../baseFixtures');
test.describe("CouchDB Status Indicator with mocked responses @couchdb", () => {
test.describe("CouchDB Status Indicator @couchdb", () => {
test.use({ failOnConsoleError: false });
//TODO BeforeAll Verify CouchDB Connectivity with APIContext
test('Shows green if connected', async ({ page }) => {
@@ -71,41 +71,38 @@ test.describe("CouchDB Status Indicator with mocked responses @couchdb", () => {
});
});
test.describe("CouchDB initialization with mocked responses @couchdb", () => {
test.describe("CouchDB initialization @couchdb", () => {
test.use({ failOnConsoleError: false });
test("'My Items' folder is created if it doesn't exist", async ({ page }) => {
const mockedMissingObjectResponsefromCouchDB = {
status: 404,
contentType: 'application/json',
body: JSON.stringify({})
};
// Store any relevant PUT requests that happen on the page
const createMineFolderRequests = [];
page.on('request', req => {
// eslint-disable-next-line playwright/no-conditional-in-test
if (req.method() === 'PUT' && req.url().endsWith('openmct/mine')) {
createMineFolderRequests.push(req);
}
});
// Override the first request to GET openmct/mine to return a 404.
// This simulates the case of starting Open MCT with a fresh database
// and no "My Items" folder created yet.
await page.route('**/mine', route => {
route.fulfill(mockedMissingObjectResponsefromCouchDB);
// Override the first request to GET openmct/mine to return a 404
await page.route('**/openmct/mine', route => {
route.fulfill({
status: 404,
contentType: 'application/json',
body: JSON.stringify({})
});
}, { times: 1 });
// Set up promise to verify that a PUT request to create "My Items"
// folder was made.
const putMineFolderRequest = page.waitForRequest(req =>
req.url().endsWith('/mine')
&& req.method() === 'PUT');
// Set up promise to verify that a GET request to retrieve "My Items"
// folder was made.
const getMineFolderRequest = page.waitForRequest(req =>
req.url().endsWith('/mine')
&& req.method() === 'GET');
// Go to baseURL.
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
// Wait for both requests to resolve.
await Promise.all([
putMineFolderRequest,
getMineFolderRequest
]);
// Verify that error banner is displayed
const bannerMessage = await page.locator('.c-message-banner__message').innerText();
expect(bannerMessage).toEqual('Failed to retrieve object mine');
// Verify that a PUT request to create "My Items" folder was made
await expect.poll(() => createMineFolderRequests.length, {
message: 'Verify that PUT request to create "mine" folder was made',
timeout: 1000
}).toBeGreaterThanOrEqual(1);
});
});

View File

@@ -24,7 +24,7 @@
This test suite is dedicated to tests which verify the basic operations surrounding the example event generator.
*/
const { test, expect } = require('../../../pluginFixtures');
const { test, expect } = require('../../../baseFixtures');
const { createDomainObjectWithDefaults } = require('../../../appActions');
test.describe('Example Event Generator CRUD Operations', () => {

View File

@@ -96,7 +96,7 @@ test.describe('Sine Wave Generator', () => {
//Click text=OK
await Promise.all([
page.waitForNavigation(),
page.click('button:has-text("OK")')
page.click('text=OK')
]);
// Verify that the Sine Wave Generator is displayed and correct

View File

@@ -24,9 +24,7 @@
This test suite is dedicated to tests which verify form functionality in isolation
*/
const { test, expect } = require('../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../appActions');
const genUuid = require('uuid').v4;
const { test, expect } = require('../../baseFixtures');
const path = require('path');
const TEST_FOLDER = 'test folder';
@@ -45,7 +43,7 @@ test.describe('Form Validation Behavior', () => {
await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
//Required Field Form Validation
await expect(page.locator('button:has-text("OK")')).toBeDisabled();
await expect(page.locator('text=OK')).toBeDisabled();
await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/);
//Correct Form Validation for missing title and trigger validation with 'Tab'
@@ -54,13 +52,13 @@ test.describe('Form Validation Behavior', () => {
await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
//Required Field Form Validation is corrected
await expect(page.locator('button:has-text("OK")')).toBeEnabled();
await expect(page.locator('text=OK')).toBeEnabled();
await expect(page.locator('.c-form-row__state-indicator').first()).not.toHaveClass(/invalid/);
//Finish Creating Domain Object
await Promise.all([
page.waitForNavigation(),
page.click('button:has-text("OK")')
page.click('text=OK')
]);
//Verify that the Domain Object has been created with the corrected title property
@@ -93,146 +91,6 @@ test.describe('Persistence operations @addInit', () => {
});
});
test.describe('Persistence operations @couchdb', () => {
test.use({ failOnConsoleError: false });
test('Editing object properties should generate a single persistence operation', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5616'
});
await page.goto('./', { waitUntil: 'networkidle' });
// Create a new 'Clock' object with default settings
const clock = await createDomainObjectWithDefaults(page, {
type: 'Clock'
});
// Count all persistence operations (PUT requests) for this specific object
let putRequestCount = 0;
page.on('request', req => {
if (req.method() === 'PUT' && req.url().endsWith(clock.uuid)) {
putRequestCount += 1;
}
});
// Open the edit form for the clock object
await page.click('button[title="More options"]');
await page.click('li[title="Edit properties of this object."]');
// Modify the display format from default 12hr -> 24hr and click 'Save'
await page.locator('select[aria-label="12 or 24 hour clock"]').selectOption({ value: 'clock24' });
await page.click('button[aria-label="Save"]');
await expect.poll(() => putRequestCount, {
message: 'Verify a single PUT request was made to persist the object',
timeout: 1000
}).toEqual(1);
});
test('Can create an object after a conflict error @couchdb @2p', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5982'
});
const page2 = await page.context().newPage();
// Both pages: Go to baseURL
await Promise.all([
page.goto('./', { waitUntil: 'networkidle' }),
page2.goto('./', { waitUntil: 'networkidle' })
]);
// Both pages: Click the Create button
await Promise.all([
page.click('button:has-text("Create")'),
page2.click('button:has-text("Create")')
]);
// Both pages: Click "Clock" in the Create menu
await Promise.all([
page.click(`li[role='menuitem']:text("Clock")`),
page2.click(`li[role='menuitem']:text("Clock")`)
]);
// Generate unique names for both objects
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
const nameInput2 = page2.locator('form[name="mctForm"] .first input[type="text"]');
// Both pages: Fill in the 'Name' form field.
await Promise.all([
nameInput.fill(""),
nameInput.fill(`Clock:${genUuid()}`),
nameInput2.fill(""),
nameInput2.fill(`Clock:${genUuid()}`)
]);
// Both pages: Fill the "Notes" section with information about the
// currently running test and its project.
const testNotes = page.testNotes;
const notesInput = page.locator('form[name="mctForm"] #notes-textarea');
const notesInput2 = page2.locator('form[name="mctForm"] #notes-textarea');
await Promise.all([
notesInput.fill(testNotes),
notesInput2.fill(testNotes)
]);
// Page 2: Click "OK" to create the domain object and wait for navigation.
// This will update the composition of the parent folder, setting the
// conditions for a conflict error from the first page.
await Promise.all([
page2.waitForLoadState(),
page2.click('[aria-label="Save"]'),
// Wait for Save Banner to appear
page2.waitForSelector('.c-message-banner__message')
]);
// Close Page 2, we're done with it.
await page2.close();
// Page 1: Click "OK" to create the domain object and wait for navigation.
// This will trigger a conflict error upon attempting to update
// the composition of the parent folder.
await Promise.all([
page.waitForLoadState(),
page.click('[aria-label="Save"]'),
// Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// Page 1: Verify that the conflict has occurred and an error notification is displayed.
await expect(page.locator('.c-message-banner__message', {
hasText: "Conflict detected while saving mine"
})).toBeVisible();
// Page 1: Start logging console errors from this point on
let errors = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});
// Page 1: Try to create a clock with the page that received the conflict.
const clockAfterConflict = await createDomainObjectWithDefaults(page, {
type: 'Clock'
});
// Page 1: Wait for save progress dialog to appear/disappear
await page.locator('.c-message-banner__message', {
hasText: 'Do not navigate away from this page or close this browser tab while this message is displayed.',
state: 'visible'
}).waitFor({ state: 'hidden' });
// Page 1: Navigate to 'My Items' and verify that the second clock was created
await page.goto('./#/browse/mine');
await expect(page.locator(`.c-grid-item__name[title="${clockAfterConflict.name}"]`)).toBeVisible();
// Verify no console errors occurred
expect(errors).toHaveLength(0);
});
});
test.describe('Form Correctness by Object Type', () => {
test.fixme('Verify correct behavior of number object (SWG)', async ({page}) => {});
test.fixme('Verify correct behavior of number object Timer', async ({page}) => {});

View File

@@ -81,7 +81,7 @@ test.describe('Move & link item tests', () => {
await page.locator('li.icon-move').click();
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
await page.locator('button:has-text("OK")').click();
await page.locator('text=OK').click();
// Expect that Child Folder is in My Items, the root folder
expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=Child Folder)`)).toBeTruthy();
@@ -95,11 +95,11 @@ test.describe('Move & link item tests', () => {
// Create Telemetry Table
let telemetryTable = 'Test Telemetry Table';
await page.locator('button:has-text("Create")').click();
await page.locator('li[role="menuitem"]:has-text("Telemetry Table")').click();
await page.locator('li:has-text("Telemetry Table")').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable);
await page.locator('button:has-text("OK")').click();
await page.locator('text=OK').click();
// Finish editing and save Telemetry Table
await page.locator('.c-button--menu.c-button--major.icon-save').click();
@@ -108,7 +108,7 @@ test.describe('Move & link item tests', () => {
// Create New Folder Basic Domain Object
let folder = 'Test Folder';
await page.locator('button:has-text("Create")').click();
await page.locator('li[role="menuitem"]:has-text("Folder")').click();
await page.locator('li:has-text("Folder")').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder);
@@ -120,7 +120,7 @@ test.describe('Move & link item tests', () => {
// Continue test regardless of assertion and create it in My Items
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
await page.locator('button:has-text("OK")').click();
await page.locator('text=OK').click();
// Open My Items
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
@@ -196,7 +196,7 @@ test.describe('Move & link item tests', () => {
await page.locator('li.icon-link').click();
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
await page.locator('button:has-text("OK")').click();
await page.locator('text=OK').click();
// Expect that Child Folder is in My Items, the root folder
expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=Child Folder)`)).toBeTruthy();

View File

@@ -40,11 +40,11 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.click('button:has-text("Create")');
await page.locator('li[role="menuitem"]:has-text("Condition Set")').click();
await page.locator('li:has-text("Condition Set")').click();
await Promise.all([
page.waitForNavigation(),
page.click('button:has-text("OK")')
page.click('text=OK')
]);
//Save localStorage for future test execution
@@ -163,9 +163,9 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
// Click hamburger button
await page.locator('[title="More options"]').click();
// Click 'Remove' and press OK
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();
// Click text=Remove
await page.locator('text=Remove').click();
await page.locator('text=OK').click();
//Expect Unnamed Condition Set to be removed in Main View
const numberOfConditionSetsAtEnd = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count();

View File

@@ -23,7 +23,7 @@
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
test.describe('Display Layout', () => {
test.describe('Testing Display Layout @unstable', () => {
let sineWaveObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
@@ -55,12 +55,12 @@ test.describe('Display Layout', () => {
// On getting data, check if the value found in the Display Layout is the most recent value
// from the Sine Wave Generator
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
const formattedTelemetryValue = getTelemValuePromise;
const formattedTelemetryValue = await getTelemValuePromise;
const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`);
const displayLayoutValue = await displayLayoutValuePromise.textContent();
const trimmedDisplayValue = displayLayoutValue.trim();
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
await expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
});
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in fixed time', async ({ page }) => {
// Create a Display Layout
@@ -86,12 +86,12 @@ test.describe('Display Layout', () => {
// On getting data, check if the value found in the Display Layout is the most recent value
// from the Sine Wave Generator
const formattedTelemetryValue = getTelemValuePromise;
const formattedTelemetryValue = await getTelemValuePromise;
const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`);
const displayLayoutValue = await displayLayoutValuePromise.textContent();
const trimmedDisplayValue = displayLayoutValue.trim();
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
await expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
});
test('items in a display layout can be removed with object tree context menu when viewing the display layout', async ({ page }) => {
// Create a Display Layout
@@ -116,20 +116,16 @@ test.describe('Display Layout', () => {
// Bring up context menu and remove
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').first().click({ button: 'right' });
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();
await page.locator('text=Remove').click();
await page.locator('text=OK').click();
// delete
expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
});
test('items in a display layout can be removed with object tree context menu when viewing another item', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/3117'
});
// Create a Display Layout
const displayLayout = await createDomainObjectWithDefaults(page, {
await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: "Test Display Layout"
});
@@ -148,18 +144,18 @@ test.describe('Display Layout', () => {
// Expand the Display Layout so we can remove the sine wave generator
await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
// Go to the original Sine Wave Generator to navigate away from the Display Layout
await page.goto(sineWaveObject.url);
// Click the original Sine Wave Generator to navigate away from the Display Layout
await page.locator('.c-tree__item .c-tree__item__name:text("Test Sine Wave Generator")').click();
// Bring up context menu and remove
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').click({ button: 'right' });
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();
await page.locator('text=Remove').click();
await page.locator('text=OK').click();
// navigate back to the display layout to confirm it has been removed
await page.goto(displayLayout.url);
await page.locator('.c-tree__item .c-tree__item__name:text("Test Display Layout")').click();
expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
});
});

View File

@@ -23,13 +23,12 @@
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Flexible Layout', () => {
let sineWaveObject;
test.describe('Testing Flexible Layout @unstable', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
// Create Sine Wave Generator
sineWaveObject = await createDomainObjectWithDefaults(page, {
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: "Test Sine Wave Generator"
});
@@ -55,81 +54,13 @@ test.describe('Flexible Layout', () => {
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
await page.dragAndDrop('text=Test Clock', '.c-fl__container.is-empty');
// Check that panes can be dragged while Flexible Layout is in Edit mode
let dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
let dragWrapper = await page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
await expect(dragWrapper).toHaveAttribute('draggable', 'true');
// Save Flexible Layout
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
// Check that panes are not draggable while Flexible Layout is in Browse mode
dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
dragWrapper = await page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
await expect(dragWrapper).toHaveAttribute('draggable', 'false');
});
test('items in a flexible layout can be removed with object tree context menu when viewing the flexible layout', async ({ page }) => {
// Create a Display Layout
await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout',
name: "Test Flexible Layout"
});
// Edit Flexible Layout
await page.locator('[title="Edit"]').click();
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator to the Flexible Layout and save changes
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1);
// Expand the Flexible Layout so we can remove the sine wave generator
await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
// Bring up context menu and remove
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').first().click({ button: 'right' });
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();
// Verify that the item has been removed from the layout
expect(await page.locator('.c-fl-container__frame').count()).toEqual(0);
});
test('items in a flexible layout can be removed with object tree context menu when viewing another item', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/3117'
});
// Create a Flexible Layout
const flexibleLayout = await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout',
name: "Test Flexible Layout"
});
// Edit Flexible Layout
await page.locator('[title="Edit"]').click();
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the Flexible Layout and save changes
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1);
// Expand the Flexible Layout so we can remove the sine wave generator
await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
// Go to the original Sine Wave Generator to navigate away from the Flexible Layout
await page.goto(sineWaveObject.url);
// Bring up context menu and remove
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').click({ button: 'right' });
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();
// navigate back to the display layout to confirm it has been removed
await page.goto(flexibleLayout.url);
// Verify that the item has been removed from the layout
expect(await page.locator('.c-fl-container__frame').count()).toEqual(0);
});
});

View File

@@ -1,124 +0,0 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
* This test suite is dedicated to testing the Gauge component.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const uuid = require('uuid').v4;
test.describe('Gauge', () => {
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'networkidle' });
});
test('Can add and remove telemetry sources @unstable', async ({ page }) => {
// Create the gauge with defaults
const gauge = await createDomainObjectWithDefaults(page, { type: 'Gauge' });
const editButtonLocator = page.locator('button[title="Edit"]');
const saveButtonLocator = page.locator('button[title="Save"]');
// Create a sine wave generator within the gauge
const swg1 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: `swg-${uuid()}`,
parent: gauge.uuid
});
// Navigate to the gauge and verify that
// the SWG appears in the elements pool
await page.goto(gauge.url);
await editButtonLocator.click();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible();
await saveButtonLocator.click();
await page.locator('li[title="Save and Finish Editing"]').click();
// Create another sine wave generator within the gauge
const swg2 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: `swg-${uuid()}`,
parent: gauge.uuid
});
// Verify that the 'Replace telemetry source' modal appears and accept it
await expect.soft(page.locator('text=This action will replace the current telemetry source. Do you want to continue?')).toBeVisible();
await page.click('text=Ok');
// Navigate to the gauge and verify that the new SWG
// appears in the elements pool and the old one is gone
await page.goto(gauge.url);
await editButtonLocator.click();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible();
await saveButtonLocator.click();
// Right click on the new SWG in the elements pool and delete it
await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({
button: 'right'
});
await page.locator('li[title="Remove this object from its containing object."]').click();
// Verify that the 'Remove object' confirmation modal appears and accept it
await expect.soft(page.locator('text=Warning! This action will remove this object. Are you sure you want to continue?')).toBeVisible();
await page.click('text=Ok');
// Verify that the elements pool shows no elements
await expect(page.locator('text="No contained elements"')).toBeVisible();
});
test('Can create a non-default Gauge', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5356'
});
//Click the Create button
await page.click('button:has-text("Create")');
// Click the object specified by 'type'
await page.click(`li[role='menuitem']:text("Gauge")`);
// FIXME: We need better selectors for these custom form controls
const displayCurrentValueSwitch = page.locator('.c-toggle-switch__slider >> nth=0');
await displayCurrentValueSwitch.setChecked(false);
await page.click('button[aria-label="Save"]');
// TODO: Verify changes in the UI
});
test('Can edit a single Gauge-specific property', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5985'
});
// Create the gauge with defaults
await createDomainObjectWithDefaults(page, { type: 'Gauge' });
await page.click('button[title="More options"]');
await page.click('li[role="menuitem"]:has-text("Edit Properties")');
// FIXME: We need better selectors for these custom form controls
const displayCurrentValueSwitch = page.locator('.c-toggle-switch__slider >> nth=0');
await displayCurrentValueSwitch.setChecked(false);
await page.click('button[aria-label="Save"]');
// TODO: Verify changes in the UI
});
});

View File

@@ -40,10 +40,10 @@ test.describe('Example Imagery Object', () => {
await page.goto('./', { waitUntil: 'networkidle' });
// Create a default 'Example Imagery' object
const exampleImagery = await createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
await createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
// Verify that the created object is focused
await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
await page.locator(backgroundImageSelector).hover({trial: true});
});
@@ -188,7 +188,7 @@ test.describe('Example Imagery in Display Layout', () => {
await page.click('button:has-text("Create")');
// Click text=Example Imagery
await page.click('li[role="menuitem"]:has-text("Example Imagery")');
await page.click('text=Example Imagery');
// Clear and set Image load delay to minimum value
await page.locator('input[type="number"]').fill('');
@@ -197,7 +197,7 @@ test.describe('Example Imagery in Display Layout', () => {
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('button:has-text("OK")'),
page.click('text=OK'),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
@@ -275,7 +275,7 @@ test.describe('Example Imagery in Flexible layout', () => {
await page.click('button:has-text("Create")');
// Click text=Example Imagery
await page.click('li[role="menuitem"]:has-text("Example Imagery")');
await page.click('text=Example Imagery');
// Clear and set Image load delay to minimum value
await page.locator('input[type="number"]').fill('');
@@ -284,7 +284,7 @@ test.describe('Example Imagery in Flexible layout', () => {
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('button:has-text("OK")'),
page.click('text=OK'),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
@@ -317,7 +317,7 @@ test.describe('Example Imagery in Tabs View', () => {
await page.click('button:has-text("Create")');
// Click text=Example Imagery
await page.click('li[role="menuitem"]:has-text("Example Imagery")');
await page.click('text=Example Imagery');
// Clear and set Image load delay to minimum value
await page.locator('input[type="number"]').fill('');
@@ -326,7 +326,7 @@ test.describe('Example Imagery in Tabs View', () => {
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('button:has-text("OK")'),
page.click('text=OK'),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);

View File

@@ -26,7 +26,7 @@ This test suite is dedicated to tests which verify the basic operations surround
// FIXME: Remove this eslint exception once tests are implemented
// eslint-disable-next-line no-unused-vars
const { test, expect } = require('../../../../pluginFixtures');
const { test, expect } = require('../../../../baseFixtures');
const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../../../appActions');
const nbUtils = require('../../../../helper/notebookUtils');

View File

@@ -24,7 +24,7 @@
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks with CouchDB.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { test, expect } = require('../../../../baseFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Notebook Tests with CouchDB @couchdb', () => {

View File

@@ -36,27 +36,27 @@ test.describe('Restricted Notebook', () => {
});
test('Can be renamed @addInit', async ({ page }) => {
await expect(page.locator('.l-browse-bar__object-name')).toContainText(`${notebook.name}`);
await expect(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`);
});
test('Can be deleted if there are no locked pages @addInit', async ({ page }) => {
test('Can be deleted if there are no locked pages @addInit', async ({ page, openmctConfig }) => {
await openObjectTreeContextMenu(page, notebook.url);
const menuOptions = page.locator('.c-menu ul');
await expect.soft(menuOptions).toContainText('Remove');
const restrictedNotebookTreeObject = page.locator(`a:has-text("${notebook.name}")`);
const restrictedNotebookTreeObject = page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`);
// notebook tree object exists
expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1);
// Click Remove Text
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('text=Remove').click();
// Click 'OK' on confirmation window and wait for save banner to appear
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click(),
page.locator('text=OK').click(),
page.waitForSelector('.c-message-banner__message')
]);
@@ -134,7 +134,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
// Click text=Ok
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click()
page.locator('text=Ok').click()
]);
// deleted page, should no longer exist
@@ -145,9 +145,10 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
test.describe('Restricted Notebook with a page locked and with an embed @addInit', () => {
test.beforeEach(async ({ page }) => {
const notebook = await startAndAddRestrictedNotebookObject(page);
await nbUtils.dragAndDropEmbed(page, notebook);
test.beforeEach(async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
await startAndAddRestrictedNotebookObject(page);
await nbUtils.dragAndDropEmbed(page, myItemsFolderName);
});
test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => {

View File

@@ -36,7 +36,7 @@ async function createNotebookAndEntry(page, iterations = 1) {
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
const notebook = createDomainObjectWithDefaults(page, { type: 'Notebook' });
createDomainObjectWithDefaults(page, { type: 'Notebook' });
for (let iteration = 0; iteration < iterations; iteration++) {
// Create an entry
@@ -45,8 +45,6 @@ async function createNotebookAndEntry(page, iterations = 1) {
await page.locator(entryLocator).click();
await page.locator(entryLocator).fill(`Entry ${iteration}`);
}
return notebook;
}
/**
@@ -55,7 +53,7 @@ async function createNotebookAndEntry(page, iterations = 1) {
* @param {number} [iterations = 1] - the number of entries (and tags) to create
*/
async function createNotebookEntryAndTags(page, iterations = 1) {
const notebook = await createNotebookAndEntry(page, iterations);
await createNotebookAndEntry(page, iterations);
for (let iteration = 0; iteration < iterations; iteration++) {
// Hover and click "Add Tag" button
@@ -77,8 +75,6 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
// Select the "Science" tag
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
}
return notebook;
}
test.describe('Tagging in Notebooks @addInit', () => {
@@ -177,10 +173,10 @@ test.describe('Tagging in Notebooks @addInit', () => {
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
const clock = await createDomainObjectWithDefaults(page, { type: 'Clock' });
await createDomainObjectWithDefaults(page, { type: 'Clock' });
const ITERATIONS = 4;
const notebook = await createNotebookEntryAndTags(page, ITERATIONS);
await createNotebookEntryAndTags(page, ITERATIONS);
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
@@ -193,11 +189,11 @@ test.describe('Tagging in Notebooks @addInit', () => {
page.goto('./#/browse/mine?hideTree=false'),
page.click('.c-disclosure-triangle')
]);
// Click Clock
await page.click(`text=${clock.name}`);
// Click Unnamed Clock
await page.click('text="Unnamed Clock"');
// Click Notebook
await page.click(`text=${notebook.name}`);
// Click Unnamed Notebook
await page.click('text="Unnamed Notebook"');
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
@@ -211,13 +207,14 @@ test.describe('Tagging in Notebooks @addInit', () => {
page.waitForLoadState('networkidle')
]);
// Click Notebook
await page.click(`text="${notebook.name}"`);
// Click Unnamed Notebook
await page.click('text="Unnamed Notebook"');
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
await expect(page.locator(entryLocator)).toContainText("Science");
await expect(page.locator(entryLocator)).toContainText("Driving");
}
});
});

View File

@@ -110,10 +110,10 @@ async function createSinewaveOverlayPlot(page, myItemsFolderName) {
await page.locator('button:has-text("Create")').click();
// add overlay plot with defaults
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
await page.locator('li:has-text("Overlay Plot")').click();
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message')
]);
@@ -129,10 +129,10 @@ async function createSinewaveOverlayPlot(page, myItemsFolderName) {
await page.locator('button:has-text("Create")').click();
// add sine wave generator with defaults
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
await page.locator('li:has-text("Sine Wave Generator")').click();
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message')
]);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -88,10 +88,10 @@ async function makeOverlayPlot(page, myItemsFolderName) {
// create overlay plot
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
await page.locator('li:has-text("Overlay Plot")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('button:has-text("OK")').click(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
@@ -106,7 +106,7 @@ async function makeOverlayPlot(page, myItemsFolderName) {
// create a sinewave generator
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
await page.locator('li:has-text("Sine Wave Generator")').click();
// set amplitude to 6, offset 4, period 2
@@ -123,7 +123,7 @@ async function makeOverlayPlot(page, myItemsFolderName) {
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('button:has-text("OK")').click(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);

View File

@@ -88,11 +88,11 @@ async function makeStackedPlot(page, myItemsFolderName) {
// create stacked plot
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Stacked Plot")').click();
await page.locator('li:has-text("Stacked Plot")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('button:has-text("OK")').click(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
@@ -146,11 +146,11 @@ async function saveStackedPlot(page) {
async function createSineWaveGenerator(page) {
//Create sine wave generator
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
await page.locator('li:has-text("Sine Wave Generator")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('button:has-text("OK")').click(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);

View File

@@ -68,10 +68,10 @@ async function makeOverlayPlot(page) {
// create overlay plot
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
await page.locator('li:has-text("Overlay Plot")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('button:has-text("OK")').click(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
@@ -86,13 +86,13 @@ async function makeOverlayPlot(page) {
// create a sinewave generator
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
await page.locator('li:has-text("Sine Wave Generator")').click();
// Click OK to make generator
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('button:has-text("OK")').click(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);

View File

@@ -25,8 +25,8 @@
*
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults} = require('../../../../appActions');
const { test, expect } = require('../../../../baseFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Plot Integrity Testing @unstable', () => {
let sineWaveGeneratorObject;
@@ -40,6 +40,7 @@ test.describe('Plot Integrity Testing @unstable', () => {
test('Plots do not re-request data when a plot is clicked', async ({ page }) => {
//Navigate to Sine Wave Generator
await page.goto(sineWaveGeneratorObject.url);
//Capture the number of plots points and store as const name numberOfPlotPoints
//Click on the plot canvas
await page.locator('canvas').nth(1).click();
//No request was made to get historical data
@@ -50,90 +51,4 @@ test.describe('Plot Integrity Testing @unstable', () => {
});
expect(createMineFolderRequests.length).toEqual(0);
});
test('Plot is rendered when infinity values exist', async ({ page }) => {
// Edit Plot
await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject);
//Get pixel data from Canvas
const plotPixelSize = await getCanvasPixelsWithData(page);
expect(plotPixelSize).toBeGreaterThan(0);
});
});
/**
* This function edits a sine wave generator with the default options and enables the infinity values option.
*
* @param {import('@playwright/test').Page} page
* @param {import('../../../../appActions').CreateObjectInfo} sineWaveGeneratorObject
* @returns {Promise<CreatedObjectInfo>} An object containing information about the edited domain object.
*/
async function editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject) {
await page.goto(sineWaveGeneratorObject.url);
// Edit LAD table
await page.locator('[title="More options"]').click();
await page.locator('[title="Edit properties of this object."]').click();
// Modify the infinity option to true
const infinityInput = page.locator('[aria-label="Include Infinity Values"]');
await infinityInput.click();
// Click OK button and wait for Navigate event
await Promise.all([
page.waitForLoadState(),
page.click('[aria-label="Save"]'),
// Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// FIXME: Changes to SWG properties should be reflected on save, but they're not?
// Thus, navigate away and back to the object.
await page.goto('./#/browse/mine');
await page.goto(sineWaveGeneratorObject.url);
await page.locator('c-progress-bar c-telemetry-table__progress-bar').waitFor({
state: 'hidden'
});
// FIXME: The progress bar disappears on series data load, not on plot render,
// so wait for a half a second before evaluating the canvas.
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(500);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function getCanvasPixelsWithData(page) {
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getCanvasValue', resolve));
await page.evaluate(() => {
// The document canvas is where the plot points and lines are drawn.
// The only way to access the canvas is using document (using page.evaluate)
let data;
let canvas;
let ctx;
canvas = document.querySelector('canvas');
ctx = canvas.getContext('2d');
data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
const imageDataValues = Object.values(data);
let plotPixels = [];
// Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four.
// The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order.
for (let i = 0; i < imageDataValues.length;) {
if (imageDataValues[i] > 0) {
plotPixels.push({
startIndex: i,
endIndex: i + 3,
value: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${imageDataValues[i + 2]}, ${imageDataValues[i + 3]})`
});
}
i = i + 4;
}
window.getCanvasValue(plotPixels.length);
});
return getTelemValuePromise;
}

View File

@@ -1,93 +0,0 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
* This test suite is dedicated to testing the Scatter Plot component.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const uuid = require('uuid').v4;
test.describe('Scatter Plot', () => {
let scatterPlot;
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'networkidle' });
// Create the Scatter Plot
scatterPlot = await createDomainObjectWithDefaults(page, { type: 'Scatter Plot' });
});
test('Can add and remove telemetry sources', async ({ page }) => {
const editButtonLocator = page.locator('button[title="Edit"]');
const saveButtonLocator = page.locator('button[title="Save"]');
// Create a sine wave generator within the scatter plot
const swg1 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: `swg-${uuid()}`,
parent: scatterPlot.uuid
});
// Navigate to the scatter plot and verify that
// the SWG appears in the elements pool
await page.goto(scatterPlot.url);
await editButtonLocator.click();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible();
await saveButtonLocator.click();
await page.locator('li[title="Save and Finish Editing"]').click();
// Create another sine wave generator within the scatter plot
const swg2 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: `swg-${uuid()}`,
parent: scatterPlot.uuid
});
// Verify that the 'Replace telemetry source' modal appears and accept it
await expect.soft(page.locator('text=This action will replace the current telemetry source. Do you want to continue?')).toBeVisible();
await page.click('text=Ok');
// Navigate to the scatter plot and verify that the new SWG
// appears in the elements pool and the old one is gone
await page.goto(scatterPlot.url);
await editButtonLocator.click();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden();
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible();
await saveButtonLocator.click();
// Right click on the new SWG in the elements pool and delete it
await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({
button: 'right'
});
await page.locator('li[title="Remove this object from its containing object."]').click();
// Verify that the 'Remove object' confirmation modal appears and accept it
await expect.soft(page.locator('text=Warning! This action will remove this object. Are you sure you want to continue?')).toBeVisible();
await page.click('text=Ok');
// Verify that the elements pool shows no elements
await expect(page.locator('text="No contained elements"')).toBeVisible();
});
});

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test, expect } = require('../../../../pluginFixtures');
const { test, expect } = require('../../../../baseFixtures');
const { setFixedTimeMode, setRealTimeMode, setStartOffset, setEndOffset } = require('../../../../appActions');
test.describe('Time conductor operations', () => {

View File

@@ -30,7 +30,7 @@ test.describe('Timer', () => {
timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
});
test('Can perform actions on the Timer', async ({ page }) => {
test('Can perform actions on the Timer', async ({ page, openmctConfig }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/4313'

View File

@@ -31,7 +31,7 @@ test.describe('Grand Search', () => {
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
const createdObjects = await createObjectsForSearch(page);
await createObjectsForSearch(page, myItemsFolderName);
// Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
@@ -41,8 +41,8 @@ test.describe('Grand Search', () => {
await expect(page.locator('[aria-label="Search Result"] >> nth=1')).toContainText(`Clock B ${myItemsFolderName} Red Folder Blue Folder`);
await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText(`Clock C ${myItemsFolderName} Red Folder Blue Folder`);
await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText(`Clock D ${myItemsFolderName} Red Folder Blue Folder`);
// Click the Elements pool to dismiss the search menu
await page.locator('.l-pane__label:has-text("Elements")').click();
// Click text=Elements >> nth=0
await page.locator('text=Elements').first().click();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
@@ -77,7 +77,7 @@ test.describe('Grand Search', () => {
await expect(page.locator('.is-object-type-clock')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Disp');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(createdObjects.displayLayout.name);
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText('Unnamed Display Layout');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toContainText('Folder');
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Clock C');
@@ -185,7 +185,7 @@ async function createFolderObject(page, folderName) {
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folderName);
// Create folder object
await page.locator('button:has-text("OK")').click();
await page.locator('text=OK').click();
}
async function waitForSearchCompletion(page) {
@@ -197,56 +197,75 @@ async function waitForSearchCompletion(page) {
* Creates some domain objects for searching
* @param {import('@playwright/test').Page} page
*/
async function createObjectsForSearch(page) {
async function createObjectsForSearch(page, myItemsFolderName) {
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
const redFolder = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Red Folder'
});
await page.locator('button:has-text("Create")').click();
await page.locator('li:has-text("Folder") >> nth=1').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"]').fill('Red Folder'),
await page.locator(`text=Save In Open MCT ${myItemsFolderName} >> span`).nth(3).click(),
page.locator('button:has-text("OK")').click()
]);
const blueFolder = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Blue Folder',
parent: redFolder.uuid
});
await page.locator('button:has-text("Create")').click();
await page.locator('li:has-text("Folder") >> nth=2').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"]').fill('Blue Folder'),
await page.locator('form[name="mctForm"] >> text=Red Folder').click(),
page.locator('button:has-text("OK")').click()
]);
const clockA = await createDomainObjectWithDefaults(page, {
type: 'Clock',
name: 'Clock A',
parent: blueFolder.uuid
});
const clockB = await createDomainObjectWithDefaults(page, {
type: 'Clock',
name: 'Clock B',
parent: blueFolder.uuid
});
const clockC = await createDomainObjectWithDefaults(page, {
type: 'Clock',
name: 'Clock C',
parent: blueFolder.uuid
});
const clockD = await createDomainObjectWithDefaults(page, {
type: 'Clock',
name: 'Clock D',
parent: blueFolder.uuid
});
await page.locator('button:has-text("Create")').click();
await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock A'),
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
page.locator('button:has-text("OK")').click()
]);
const displayLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout'
});
await page.locator('button:has-text("Create")').click();
await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock B'),
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
page.locator('button:has-text("OK")').click()
]);
// Go back into edit mode for the display layout
await page.locator('button[title="Edit"]').click();
await page.locator('button:has-text("Create")').click();
await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock C'),
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
page.locator('button:has-text("OK")').click()
]);
return {
redFolder,
blueFolder,
clockA,
clockB,
clockC,
clockD,
displayLayout
};
await page.locator('button:has-text("Create")').click();
await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock D'),
await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
page.locator('button:has-text("OK")').click()
]);
await Promise.all([
page.waitForNavigation(),
page.locator(`a:has-text("${myItemsFolderName}") >> nth=0`).click()
]);
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Notebook")
await page.locator('li:has-text("Display Layout")').click();
// Click button:has-text("OK")
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click()
]);
}

View File

@@ -53,7 +53,7 @@ test.describe('Performance tests', () => {
await page.setInputFiles('#fileElem', filePath);
// Click text=OK
await page.locator('button:has-text("OK")').click();
await page.locator('text=OK').click();
await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();

View File

@@ -33,8 +33,7 @@ define([
dataRateInHz: 1,
randomness: 0,
phase: 0,
loadDelay: 0,
infinityValues: false
loadDelay: 0
};
function GeneratorProvider(openmct) {
@@ -57,8 +56,7 @@ define([
'dataRateInHz',
'randomness',
'phase',
'loadDelay',
'infinityValues'
'loadDelay'
];
request = request || {};

View File

@@ -76,10 +76,10 @@
name: data.name,
utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness, data.infinityValues),
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness),
wavelengths: wavelengths(),
intensities: intensities(),
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness, data.infinityValues)
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
}
});
nextStep += step;
@@ -117,7 +117,6 @@
var phase = request.phase;
var randomness = request.randomness;
var loadDelay = Math.max(request.loadDelay, 0);
var infinityValues = request.infinityValues;
var step = 1000 / dataRateInHz;
var nextStep = start - (start % step) + step;
@@ -128,10 +127,10 @@
data.push({
utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, period, amplitude, offset, phase, randomness, infinityValues),
sin: sin(nextStep, period, amplitude, offset, phase, randomness),
wavelengths: wavelengths(),
intensities: intensities(),
cos: cos(nextStep, period, amplitude, offset, phase, randomness, infinityValues)
cos: cos(nextStep, period, amplitude, offset, phase, randomness)
});
}
@@ -156,20 +155,12 @@
});
}
function cos(timestamp, period, amplitude, offset, phase, randomness, infinityValues) {
if (infinityValues && Math.random() > 0.5) {
return Number.POSITIVE_INFINITY;
}
function cos(timestamp, period, amplitude, offset, phase, randomness) {
return amplitude
* Math.cos(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
}
function sin(timestamp, period, amplitude, offset, phase, randomness, infinityValues) {
if (infinityValues && Math.random() > 0.5) {
return Number.POSITIVE_INFINITY;
}
function sin(timestamp, period, amplitude, offset, phase, randomness) {
return amplitude
* Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
}

View File

@@ -143,16 +143,6 @@ define([
"telemetry",
"loadDelay"
]
},
{
name: "Include Infinity Values",
control: "toggleSwitch",
cssClass: "l-input",
key: "infinityValues",
property: [
"telemetry",
"infinityValues"
]
}
],
initialize: function (object) {
@@ -163,8 +153,7 @@ define([
dataRateInHz: 1,
phase: 0,
randomness: 0,
loadDelay: 0,
infinityValues: false
loadDelay: 0
};
}
});

View File

@@ -1,16 +1,16 @@
{
"name": "openmct",
"version": "2.1.5-SNAPSHOT",
"version": "2.1.2",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.18.9",
"@braintree/sanitize-url": "6.0.2",
"@percy/cli": "1.16.0",
"@braintree/sanitize-url": "6.0.0",
"@percy/cli": "1.11.0",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.25.2",
"@types/jasmine": "4.3.1",
"@types/lodash": "4.14.191",
"babel-loader": "9.1.0",
"@types/jasmine": "4.3.0",
"@types/lodash": "4.14.186",
"babel-loader": "8.2.5",
"babel-plugin-istanbul": "6.1.1",
"codecov": "3.8.3",
"comma-separated-values": "3.6.4",
@@ -19,17 +19,17 @@
"d3-axis": "3.0.0",
"d3-scale": "3.3.0",
"d3-selection": "3.0.0",
"eslint": "8.30.0",
"eslint": "8.25.0",
"eslint-plugin-compat": "4.0.2",
"eslint-plugin-playwright": "0.11.2",
"eslint-plugin-vue": "9.8.0",
"eslint-plugin-vue": "9.6.0",
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
"eventemitter3": "1.2.0",
"file-saver": "2.0.5",
"git-rev-sync": "3.0.2",
"html2canvas": "1.4.1",
"imports-loader": "4.0.1",
"jasmine-core": "4.5.0",
"jasmine-core": "4.4.0",
"karma": "6.3.20",
"karma-chrome-launcher": "3.1.1",
"karma-cli": "2.0.0",
@@ -42,10 +42,10 @@
"karma-webpack": "5.0.0",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"mini-css-extract-plugin": "2.7.2",
"mini-css-extract-plugin": "2.6.1",
"moment": "2.29.4",
"moment-duration-format": "2.3.2",
"moment-timezone": "0.5.40",
"moment-timezone": "0.5.37",
"nyc": "15.1.0",
"painterro": "1.2.78",
"playwright-core": "1.25.2",
@@ -53,18 +53,18 @@
"plotly.js-gl2d-dist": "2.14.0",
"printj": "1.3.1",
"resolve-url-loader": "5.0.0",
"sass": "1.56.1",
"sass": "1.55.0",
"sass-loader": "13.0.2",
"sinon": "15.0.1",
"sinon": "14.0.0",
"style-loader": "^3.3.1",
"typescript": "4.9.4",
"typescript": "4.8.4",
"uuid": "9.0.0",
"vue": "2.6.14",
"vue-eslint-parser": "9.1.0",
"vue-loader": "15.9.8",
"vue-template-compiler": "2.6.14",
"webpack": "5.74.0",
"webpack-cli": "5.0.0",
"webpack-cli": "4.10.0",
"webpack-dev-server": "4.11.1",
"webpack-merge": "5.8.0"
},
@@ -114,5 +114,6 @@
"ios_saf > 15"
],
"author": "",
"license": "Apache-2.0"
"license": "Apache-2.0",
"private": true
}

View File

@@ -56,12 +56,17 @@ export default class Editor extends EventEmitter {
* Save any unsaved changes from this editing session. This will
* end the current transaction.
*/
async save() {
save() {
const transaction = this.openmct.objects.getActiveTransaction();
await transaction.commit();
this.editing = false;
this.emit('isEditing', false);
this.openmct.objects.endTransaction();
return transaction.commit()
.then(() => {
this.editing = false;
this.emit('isEditing', false);
this.openmct.objects.endTransaction();
}).catch(error => {
throw error;
});
}
/**
@@ -73,10 +78,6 @@ export default class Editor extends EventEmitter {
return new Promise((resolve, reject) => {
const transaction = this.openmct.objects.getActiveTransaction();
if (!transaction) {
return resolve();
}
transaction.cancel()
.then(resolve)
.catch(reject)

View File

@@ -26,7 +26,6 @@
v-model="selected"
required="model.required"
name="mctControl"
:aria-label="model.ariaLabel || model.name"
@change="onChange($event)"
>
<option

View File

@@ -27,7 +27,6 @@
:class="model.cssClass"
>
<textarea
:id="`${model.key}-textarea`"
v-model="field"
type="text"
:size="model.size"

View File

@@ -29,7 +29,6 @@
<ToggleSwitch
id="switchId"
:checked="isChecked"
:name="model.name"
@change="toggleCheckBox"
/>
</span>

View File

@@ -3,52 +3,39 @@
class="c-menu"
:class="options.menuClass"
>
<ul
v-if="options.actions.length && options.actions[0].length"
role="menu"
>
<ul v-if="options.actions.length && options.actions[0].length">
<template
v-for="(actionGroups, index) in options.actions"
>
<div
:key="index"
role="group"
<li
v-for="action in actionGroups"
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
>
<li
v-for="action in actionGroups"
:key="action.name"
role="menuitem"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
>
{{ action.name }}
</li>
<div
v-if="index !== options.actions.length - 1"
:key="index"
role="separator"
class="c-menu__section-separator"
>
</div>
<li
v-if="actionGroups.length === 0"
:key="index"
>
No actions defined.
</li>
</div></template>
{{ action.name }}
</li>
<div
v-if="index !== options.actions.length - 1"
:key="index"
class="c-menu__section-separator"
>
</div>
<li
v-if="actionGroups.length === 0"
:key="index"
>
No actions defined.
</li>
</template>
</ul>
<ul
v-else
role="menu"
>
<ul v-else>
<li
v-for="action in options.actions"
:key="action.name"
role="menuitem"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"

View File

@@ -5,54 +5,45 @@
>
<ul
v-if="options.actions.length && options.actions[0].length"
role="menu"
class="c-super-menu__menu"
>
<template
v-for="(actionGroups, index) in options.actions"
>
<div
:key="index"
role="group"
<li
v-for="action in actionGroups"
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
>
<li
v-for="action in actionGroups"
:key="action.name"
role="menuitem"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
>
{{ action.name }}
</li>
<div
v-if="index !== options.actions.length - 1"
:key="index"
role="separator"
class="c-menu__section-separator"
>
</div>
<li
v-if="actionGroups.length === 0"
:key="index"
>
No actions defined.
</li>
</div></template>
{{ action.name }}
</li>
<div
v-if="index !== options.actions.length - 1"
:key="index"
class="c-menu__section-separator"
>
</div>
<li
v-if="actionGroups.length === 0"
:key="index"
>
No actions defined.
</li>
</template>
</ul>
<ul
v-else
class="c-super-menu__menu"
role="menu"
>
<li
v-for="action in options.actions"
:key="action.name"
role="menuitem"
:class="action.cssClass"
:title="action.description"
:data-testid="action.testId || false"

View File

@@ -19,7 +19,6 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
const DEFAULT_INTERCEPTOR_PRIORITY = 0;
export default class InterceptorRegistry {
/**
* A InterceptorRegistry maintains the definitions for different interceptors that may be invoked on domain objects.
@@ -46,6 +45,7 @@ export default class InterceptorRegistry {
* @memberof module:openmct.InterceptorRegistry#
*/
addInterceptor(interceptorDef) {
//TODO: sort by priority
this.interceptors.push(interceptorDef);
}
@@ -56,18 +56,10 @@ export default class InterceptorRegistry {
* @memberof module:openmct.InterceptorRegistry#
*/
getInterceptors(identifier, object) {
function byPriority(interceptorA, interceptorB) {
let priorityA = interceptorA.priority ?? DEFAULT_INTERCEPTOR_PRIORITY;
let priorityB = interceptorB.priority ?? DEFAULT_INTERCEPTOR_PRIORITY;
return priorityB - priorityA;
}
return this.interceptors.filter(interceptor => {
return typeof interceptor.appliesTo === 'function'
&& interceptor.appliesTo(identifier, object);
}).sort(byPriority);
});
}
}

View File

@@ -357,7 +357,6 @@ export default class ObjectAPI {
async save(domainObject) {
const provider = this.getProvider(domainObject.identifier);
let result;
let lastPersistedTime;
if (!this.isPersistable(domainObject.identifier)) {
result = Promise.reject('Object provider does not support saving');
@@ -378,17 +377,14 @@ export default class ObjectAPI {
this.#mutate(domainObject, 'modifiedBy', username);
if (isNewObject) {
this.#mutate(domainObject, 'createdBy', username);
const createdTime = Date.now();
this.#mutate(domainObject, 'created', createdTime);
const persistedTime = Date.now();
this.#mutate(domainObject, 'persisted', persistedTime);
this.#mutate(domainObject, 'created', persistedTime);
this.#mutate(domainObject, 'createdBy', username);
savedObjectPromise = provider.create(domainObject);
} else {
lastPersistedTime = domainObject.persisted;
const persistedTime = Date.now();
this.#mutate(domainObject, 'persisted', persistedTime);
@@ -399,10 +395,6 @@ export default class ObjectAPI {
savedObjectPromise.then(response => {
savedResolve(response);
}).catch((error) => {
if (lastPersistedTime !== undefined) {
this.#mutate(domainObject, 'persisted', lastPersistedTime);
}
savedReject(error);
});
} else {
@@ -410,19 +402,9 @@ export default class ObjectAPI {
}
}
return result.catch(async (error) => {
return result.catch((error) => {
if (error instanceof this.errors.Conflict) {
this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`);
// Synchronized objects will resolve their own conflicts, so
// bypass the refresh here and throw the error.
if (!this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) {
if (this.isTransactionActive()) {
this.endTransaction();
}
await this.refresh(domainObject);
}
}
throw error;

View File

@@ -202,13 +202,8 @@ class IndependentTimeContext extends TimeContext {
}
getUpstreamContext() {
const objectKey = this.openmct.objects.makeKeyString(this.objectPath[this.objectPath.length - 1].identifier);
const doesObjectHaveTimeContext = this.globalTimeContext.independentContexts.get(objectKey);
if (doesObjectHaveTimeContext) {
return undefined;
}
let timeContext = this.globalTimeContext;
this.objectPath.some((item, index) => {
const key = this.openmct.objects.makeKeyString(item.identifier);
//last index is the view object itself

View File

@@ -112,7 +112,11 @@ export default {
}
},
removeFromComposition(telemetryObject) {
this.composition.remove(telemetryObject);
let composition = this.domainObject.composition.filter(id =>
!this.openmct.objects.areIdsEqual(id, telemetryObject.identifier)
);
this.openmct.objects.mutate(this.domainObject, 'composition', composition);
},
addTelemetryObject(telemetryObject) {
// grab information we need from the added telmetry object

View File

@@ -104,14 +104,10 @@ export default {
this.$set(this.plotSeries, this.plotSeries.length, series);
this.setAxesLabels();
},
removeSeries(seriesKey) {
const seriesIndex = this.plotSeries.findIndex(
plotSeries => this.openmct.objects.areIdsEqual(seriesKey, plotSeries.identifier)
);
const foundSeries = seriesIndex > -1;
if (foundSeries) {
this.$delete(this.plotSeries, seriesIndex);
removeSeries(series) {
const index = this.plotSeries.findIndex(plotSeries => this.openmct.objects.areIdsEqual(series.identifier, plotSeries.identifier));
if (index !== undefined) {
this.$delete(this.plotSeries, index);
this.setAxesLabels();
}
},

View File

@@ -68,7 +68,6 @@ export default function ClockPlugin(options) {
]
},
{
ariaLabel: "12 or 24 hour clock",
control: 'select',
options: [
{

View File

@@ -583,7 +583,6 @@ define(['lodash'], function (_) {
domainObject: selectedParent,
icon: "icon-object",
title: "Merge into a telemetry table or plot",
label: "View type",
options: APPLICABLE_VIEWS['telemetry-view-multi'],
method: function (option) {
displayLayoutContext.mergeMultipleTelemetryViews(selection, option.value);

View File

@@ -245,9 +245,6 @@ export default {
});
this.gridDimensions = [wMax * this.gridSize[0], hMax * this.gridSize[1]];
},
clearSelection() {
this.$el.click();
},
watchDisplayResize() {
const resizeObserver = new ResizeObserver(() => this.updateGrid());
@@ -481,7 +478,7 @@ export default {
});
_.pullAt(this.layoutItems, indices);
this.mutate("configuration.items", this.layoutItems);
this.clearSelection();
this.$el.click();
},
untrackItem(item) {
if (!item.identifier) {
@@ -507,11 +504,15 @@ export default {
}
if (!telemetryViewCount && !objectViewCount) {
this.removeFromComposition(item);
this.removeFromComposition(keyString);
}
},
removeFromComposition(item) {
this.composition.remove(item);
removeFromComposition(keyString) {
let composition = this.domainObject.composition ? this.domainObject.composition : [];
composition = composition.filter(identifier => {
return this.openmct.objects.makeKeyString(identifier) !== keyString;
});
this.mutate("composition", composition);
},
initializeItems() {
this.telemetryViewMap = {};
@@ -528,10 +529,7 @@ export default {
}
});
this.startTransaction();
removedItems.forEach(this.removeFromConfiguration);
return this.endTransaction();
},
isItemAlreadyTracked(child) {
let found = false;
@@ -592,7 +590,7 @@ export default {
}
});
this.mutate("configuration.items", layoutItems);
this.clearSelection();
this.$el.click();
},
orderItem(position, selectedItems) {
let delta = ORDERS[position];
@@ -775,7 +773,7 @@ export default {
this.$nextTick(() => {
this.openmct.objects.mutate(this.domainObject, "configuration.items", this.layoutItems);
this.openmct.objects.mutate(this.domainObject, "configuration.objectStyles", objectStyles);
this.clearSelection();
this.$el.click(); //clear selection;
newDomainObjectsArray.forEach(domainObject => {
this.composition.add(domainObject);
@@ -869,20 +867,6 @@ export default {
this.removeItem(selection);
this.initSelectIndex = this.layoutItems.length - 1; //restore selection
},
startTransaction() {
if (!this.openmct.objects.isTransactionActive()) {
this.transaction = this.openmct.objects.startTransaction();
}
},
async endTransaction() {
if (!this.transaction) {
return;
}
await this.transaction.commit();
this.openmct.objects.endTransaction();
this.transaction = null;
},
toggleGrid() {
this.showGrid = !this.showGrid;
},

View File

@@ -185,24 +185,10 @@ export default {
this.composition.off('add', this.addFrame);
},
methods: {
containsObject(identifier) {
if ('composition' in this.domainObject) {
return this.domainObject.composition
.some(childId => this.openmct.objects.areIdsEqual(childId, identifier));
}
return false;
},
buildIdentifierMap() {
this.containers.forEach(container => {
container.frames.forEach(frame => {
if (!this.containsObject(frame.domainObjectIdentifier)) {
this.removeChildObject(frame.domainObjectIdentifier);
return;
}
const keystring = this.openmct.objects.makeKeyString(frame.domainObjectIdentifier);
let keystring = this.openmct.objects.makeKeyString(frame.domainObjectIdentifier);
this.identifierMap[keystring] = true;
});
});
@@ -310,14 +296,11 @@ export default {
}
},
persist(index) {
this.startTransaction();
if (index) {
this.openmct.objects.mutate(this.domainObject, `configuration.containers[${index}]`, this.containers[index]);
} else {
this.openmct.objects.mutate(this.domainObject, 'configuration.containers', this.containers);
}
return this.endTransaction();
},
startContainerResizing(index) {
let beforeContainer = this.containers[index];
@@ -383,20 +366,6 @@ export default {
});
this.persist();
},
startTransaction() {
if (!this.openmct.objects.isTransactionActive()) {
this.transaction = this.openmct.objects.startTransaction();
}
},
async endTransaction() {
if (!this.transaction) {
return;
}
await this.transaction.commit();
this.openmct.objects.endTransaction();
this.transaction = null;
}
}
};

View File

@@ -53,7 +53,10 @@ export default class CreateAction extends PropertiesAction {
const existingValue = this.domainObject[key];
if (!(existingValue instanceof Array) && (typeof existingValue === 'object')) {
value = _.merge(existingValue, value);
value = {
...existingValue,
...value
};
}
_.set(this.domainObject, key, value);
@@ -73,21 +76,19 @@ export default class CreateAction extends PropertiesAction {
title: 'Saving'
});
try {
await this.openmct.objects.save(this.domainObject);
const success = await this.openmct.objects.save(this.domainObject);
if (success) {
const compositionCollection = await this.openmct.composition.get(parentDomainObject);
compositionCollection.add(this.domainObject);
this._navigateAndEdit(this.domainObject, parentDomainObjectPath);
this.openmct.notifications.info('Save successful');
} catch (err) {
console.error(err);
this.openmct.notifications.error(`Error saving objects: ${err}`);
} finally {
dialog.dismiss();
} else {
this.openmct.notifications.error('Error saving objects');
}
dialog.dismiss();
}
/**

View File

@@ -22,7 +22,6 @@
import PropertiesAction from './PropertiesAction';
import CreateWizard from './CreateWizard';
import _ from 'lodash';
export default class EditPropertiesAction extends PropertiesAction {
constructor(openmct) {
@@ -53,7 +52,7 @@ export default class EditPropertiesAction extends PropertiesAction {
/**
* @private
*/
async _onSave(changes) {
_onSave(changes) {
if (!this.openmct.objects.isTransactionActive()) {
this.openmct.objects.startTransaction();
}
@@ -62,14 +61,23 @@ export default class EditPropertiesAction extends PropertiesAction {
Object.entries(changes).forEach(([key, value]) => {
const existingValue = this.domainObject[key];
if (!(Array.isArray(existingValue)) && (typeof existingValue === 'object')) {
value = _.merge(existingValue, value);
value = {
...existingValue,
...value
};
}
this.openmct.objects.mutate(this.domainObject, key, value);
});
const transaction = this.openmct.objects.getActiveTransaction();
await transaction.commit();
this.openmct.objects.endTransaction();
return transaction.commit()
.catch(error => {
throw error;
}).finally(() => {
this.openmct.objects.endTransaction();
});
} catch (error) {
this.openmct.notifications.error('Error saving objects');
console.error(error);

View File

@@ -598,7 +598,11 @@ export default {
return this.round(((vPercent / 100) * 270) + DIAL_VALUE_DEG_OFFSET, 2);
},
removeFromComposition(telemetryObject = this.telemetryObject) {
this.composition.remove(telemetryObject);
let composition = this.domainObject.composition.filter(id =>
!this.openmct.objects.areIdsEqual(id, telemetryObject.identifier)
);
this.openmct.objects.mutate(this.domainObject, 'composition', composition);
},
refreshData(bounds, isTick) {
if (!isTick) {

View File

@@ -45,10 +45,6 @@ export default class GoToOriginalAction {
});
}
appliesTo(objectPath) {
if (this._openmct.editor.isEditing()) {
return false;
}
let parentKeystring = objectPath[1] && this._openmct.objects.makeKeyString(objectPath[1].identifier);
if (!parentKeystring) {

View File

@@ -31,32 +31,21 @@
:title="image.formattedTime"
>
<a
class="c-thumb__image-wrapper"
href=""
:download="image.imageDownloadName"
@click.prevent
>
<img
ref="img"
class="c-thumb__image"
:src="image.url"
fetchpriority="low"
@load="imageLoadCompleted"
>
</a>
<div
v-if="viewableArea"
class="c-thumb__viewable-area"
:style="viewableAreaStyle"
></div>
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
</div>
</template>
<script>
const THUMB_PADDING = 4;
const BORDER_WIDTH = 2;
export default {
props: {
image: {
@@ -74,77 +63,6 @@ export default {
realTime: {
type: Boolean,
required: true
},
viewableArea: {
type: Object,
default: function () {
return null;
}
}
},
data() {
return {
imgWidth: 0,
imgHeight: 0
};
},
computed: {
viewableAreaStyle() {
if (!this.viewableArea || !this.imgWidth || !this.imgHeight) {
return null;
}
const { widthRatio, heightRatio, xOffsetRatio, yOffsetRatio } = this.viewableArea;
const imgWidth = this.imgWidth;
const imgHeight = this.imgHeight;
let translateX = imgWidth * xOffsetRatio;
let translateY = imgHeight * yOffsetRatio;
let width = imgWidth * widthRatio;
let height = imgHeight * heightRatio;
if (translateX < 0) {
width += translateX;
translateX = 0;
}
if (translateX + width > imgWidth) {
width = imgWidth - translateX;
}
if (translateX + 2 * BORDER_WIDTH > imgWidth) {
translateX = imgWidth - 2 * BORDER_WIDTH;
}
if (translateY < 0) {
height += translateY;
translateY = 0;
}
if (translateY + height > imgHeight) {
height = imgHeight - translateY;
}
if (translateY + 2 * BORDER_WIDTH > imgHeight) {
translateY = imgHeight - 2 * BORDER_WIDTH;
}
return {
'transform': `translate(${translateX + THUMB_PADDING}px, ${translateY + THUMB_PADDING}px)`,
'width': `${width}px`,
'height': `${height}px`
};
}
},
methods: {
imageLoadCompleted() {
if (!this.$refs.img) {
return;
}
const { width: imgWidth, height: imgHeight } = this.$refs.img;
this.imgWidth = imgWidth;
this.imgHeight = imgHeight;
}
}
};

View File

@@ -25,7 +25,7 @@
tabindex="0"
class="c-imagery"
@keyup="arrowUpHandler"
@keydown.prevent="arrowDownHandler"
@keydown="arrowDownHandler"
@mouseover="focusElement"
>
<div
@@ -147,7 +147,7 @@
v-if="!isFixed"
class="c-button icon-pause pause-play"
:class="{'is-paused': isPaused}"
@click="handlePauseButton(!isPaused)"
@click="paused(!isPaused)"
></button>
</div>
</div>
@@ -165,9 +165,6 @@
<div
ref="thumbsWrapper"
class="c-imagery__thumbs-scroll-area"
:class="[{
'animate-scroll': animateThumbScroll
}]"
@scroll="handleScroll"
>
<ImageThumbnail
@@ -177,7 +174,6 @@
:active="focusedImageIndex === index"
:selected="focusedImageIndex === index && isPaused"
:real-time="!isFixed"
:viewable-area="focusedImageIndex === index ? viewableArea : null"
@click.native="thumbnailClicked(index)"
/>
</div>
@@ -185,7 +181,7 @@
<button
class="c-imagery__auto-scroll-resume-button c-icon-button icon-play"
title="Resume automatic scrolling of image thumbnails"
@click="scrollToRight"
@click="scrollToRight('reset')"
></button>
</div>
</div>
@@ -195,7 +191,6 @@
import eventHelpers from '../lib/eventHelpers';
import _ from 'lodash';
import moment from 'moment';
import Vue from 'vue';
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
import Compass from './Compass/Compass.vue';
@@ -224,8 +219,6 @@ const ZOOM_SCALE_DEFAULT = 1;
const SHOW_THUMBS_THRESHOLD_HEIGHT = 200;
const SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600;
const IMAGE_CONTAINER_BORDER_WIDTH = 1;
export default {
name: 'ImageryView',
components: {
@@ -288,13 +281,10 @@ export default {
},
imageTranslateX: 0,
imageTranslateY: 0,
imageViewportWidth: 0,
imageViewportHeight: 0,
pan: undefined,
animateZoom: true,
imagePanned: false,
forceShowThumbnails: false,
animateThumbScroll: false
forceShowThumbnails: false
};
},
computed: {
@@ -398,12 +388,6 @@ export default {
return disabled;
},
isComposedInLayout() {
return (
this.currentView?.objectPath
&& !this.openmct.router.isNavigatedObject(this.currentView.objectPath)
);
},
focusedImage() {
return this.imageHistory[this.focusedImageIndex];
},
@@ -532,28 +516,11 @@ export default {
}
return 'Alt drag to pan';
},
viewableArea() {
if (this.zoomFactor === 1) {
return null;
}
const imageWidth = this.sizedImageWidth * this.zoomFactor;
const imageHeight = this.sizedImageHeight * this.zoomFactor;
const xOffset = (imageWidth - this.imageViewportWidth) / 2;
const yOffset = (imageHeight - this.imageViewportHeight) / 2;
return {
widthRatio: this.imageViewportWidth / imageWidth,
heightRatio: this.imageViewportHeight / imageHeight,
xOffsetRatio: (xOffset - this.imageTranslateX * this.zoomFactor) / imageWidth,
yOffsetRatio: (yOffset - this.imageTranslateY * this.zoomFactor) / imageHeight
};
}
},
watch: {
imageHistory: {
async handler(newHistory, oldHistory) {
handler(newHistory, _oldHistory) {
const newSize = newHistory.length;
let imageIndex = newSize > 0 ? newSize - 1 : undefined;
if (this.focusedImageTimestamp !== undefined) {
@@ -581,13 +548,10 @@ export default {
if (!this.isPaused) {
this.setFocusedImage(imageIndex);
this.scrollToRight();
} else {
this.scrollToFocused();
}
await this.scrollHandler();
if (oldHistory?.length > 0) {
this.animateThumbScroll = true;
}
},
deep: true
},
@@ -598,7 +562,7 @@ export default {
this.getImageNaturalDimensions();
},
bounds() {
this.scrollHandler();
this.scrollToFocused();
},
isFixed(newValue) {
const isRealTime = !newValue;
@@ -862,13 +826,6 @@ export default {
const disableScroll = scrollWidth > Math.ceil(scrollLeft + clientWidth);
this.autoScroll = !disableScroll;
},
handlePauseButton(newState) {
this.paused(newState);
if (newState) {
// need to set the focused index or the paused focus will drift
this.thumbnailClicked(this.focusedImageIndex);
}
},
paused(state) {
this.isPaused = Boolean(state);
@@ -876,7 +833,7 @@ export default {
this.previousFocusedImage = null;
this.setFocusedImage(this.nextImageIndex);
this.autoScroll = true;
this.scrollHandler();
this.scrollToRight();
}
},
scrollToFocused() {
@@ -886,43 +843,28 @@ export default {
}
let domThumb = thumbsWrapper.children[this.focusedImageIndex];
if (!domThumb) {
return;
if (domThumb) {
domThumb.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
});
}
// separate scrollTo function had to be implemented since scrollIntoView
// caused undesirable behavior in layouts
// and could not simply be scoped to the parent element
if (this.isComposedInLayout) {
const wrapperWidth = this.$refs.thumbsWrapper.clientWidth ?? 0;
this.$refs.thumbsWrapper.scrollLeft = (
domThumb.offsetLeft - (wrapperWidth - domThumb.clientWidth) / 2);
return;
}
domThumb.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
});
},
async scrollToRight() {
scrollToRight(type) {
if (type !== 'reset' && (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll)) {
return;
}
const scrollWidth = this.$refs?.thumbsWrapper?.scrollWidth ?? 0;
const scrollWidth = this.$refs.thumbsWrapper.scrollWidth || 0;
if (!scrollWidth) {
return;
}
await Vue.nextTick();
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
},
scrollHandler() {
if (this.isPaused) {
return this.scrollToFocused();
} else if (this.autoScroll) {
return this.scrollToRight();
}
this.$nextTick(() => {
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
});
},
matchIndexOfPreviousImage(previous, imageHistory) {
// match logic uses a composite of url and time to account
@@ -1121,12 +1063,12 @@ export default {
}
this.setSizedImageDimensions();
this.setImageViewport();
this.calculateViewHeight();
this.scrollHandler();
this.scrollToFocused();
},
setSizedImageDimensions() {
this.focusedImageNaturalAspectRatio = this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight;
if ((this.imageContainerWidth / this.imageContainerHeight) > this.focusedImageNaturalAspectRatio) {
// container is wider than image
this.sizedImageWidth = this.imageContainerHeight * this.focusedImageNaturalAspectRatio;
@@ -1137,17 +1079,6 @@ export default {
this.sizedImageHeight = this.imageContainerWidth / this.focusedImageNaturalAspectRatio;
}
},
setImageViewport() {
if (this.imageContainerHeight > this.sizedImageHeight + IMAGE_CONTAINER_BORDER_WIDTH) {
// container is taller than wrapper
this.imageViewportWidth = this.sizedImageWidth;
this.imageViewportHeight = this.sizedImageHeight;
} else {
// container is wider than wrapper
this.imageViewportWidth = this.imageContainerWidth;
this.imageViewportHeight = this.imageContainerHeight;
}
},
handleThumbWindowResizeStart() {
if (!this.autoScroll) {
return;
@@ -1158,7 +1089,9 @@ export default {
this.handleThumbWindowResizeEnded();
},
handleThumbWindowResizeEnded() {
this.scrollHandler();
if (!this.isPaused) {
this.scrollToRight('reset');
}
this.calculateViewHeight();
@@ -1171,6 +1104,7 @@ export default {
},
wheelZoom(e) {
e.preventDefault();
this.$refs.imageControls.wheelZoom(e);
},
startPan(e) {

View File

@@ -194,9 +194,6 @@
overflow-y: hidden;
margin-bottom: 1px;
padding-bottom: $interiorMarginSm;
&.animate-scroll {
scroll-behavior: smooth;
}
}
&__auto-scroll-resume-button {
@@ -288,13 +285,6 @@
flex: 0 0 auto;
padding: 2px 3px;
}
&__viewable-area {
position: absolute;
border: 2px yellow solid;
left: 0;
top: 0;
}
}
.is-small-thumbs {

View File

@@ -481,16 +481,19 @@ describe("The Imagery View Layouts", () => {
});
});
});
it ('scrollToRight is called when clicking on auto scroll button', async () => {
await Vue.nextTick();
// use spyon to spy the scroll function
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollHandler');
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
await Vue.nextTick();
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryContainer.scrollHandler);
it ('scrollToRight is called when clicking on auto scroll button', (done) => {
Vue.nextTick(() => {
// use spyon to spy the scroll function
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight');
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
Vue.nextTick(() => {
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset');
done();
});
});
});
xit('should change the image zoom factor when using the zoom buttons', async () => {
xit('should change the image zoom factor when using the zoom buttons', async (done) => {
await Vue.nextTick();
let imageSizeBefore;
let imageSizeAfter;
@@ -509,6 +512,7 @@ describe("The Imagery View Layouts", () => {
imageSizeAfter = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect();
expect(imageSizeAfter.height).toBeLessThan(imageSizeBefore.height);
expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width);
done();
});
xit('should reset the zoom factor on the image when clicking the zoom button', async (done) => {
await Vue.nextTick();
@@ -525,19 +529,6 @@ describe("The Imagery View Layouts", () => {
done();
});
it('should display the viewable area when zoom factor is greater than 1', async () => {
await Vue.nextTick();
expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(0);
parent.querySelector('.t-btn-zoom-in').click();
await Vue.nextTick();
expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(1);
parent.querySelector('.t-btn-zoom-reset').click();
await Vue.nextTick();
expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(0);
});
it('should reset the brightness and contrast when clicking the reset button', async () => {
const viewInstance = imageryView._getInstance();
await Vue.nextTick();

View File

@@ -37,15 +37,14 @@ function myItemsInterceptor(openmct, identifierObject, name) {
return identifier.key === MY_ITEMS_KEY;
},
invoke: (identifier, object) => {
if (!object || openmct.objects.isMissing(object)) {
if (openmct.objects.isMissing(object)) {
openmct.objects.save(myItemsModel);
return myItemsModel;
}
return object;
},
priority: openmct.priority.HIGH
}
};
}

View File

@@ -894,16 +894,24 @@ export default {
this.transaction = this.openmct.objects.startTransaction();
}
},
async saveTransaction() {
saveTransaction() {
if (this.transaction !== undefined) {
await this.transaction.commit();
this.openmct.objects.endTransaction();
this.transaction.commit()
.catch(error => {
throw error;
}).finally(() => {
this.openmct.objects.endTransaction();
});
}
},
async cancelTransaction() {
cancelTransaction() {
if (this.transaction !== undefined) {
await this.transaction.cancel();
this.openmct.objects.endTransaction();
this.transaction.cancel()
.catch(error => {
throw error;
}).finally(() => {
this.openmct.objects.endTransaction();
});
}
}
}

View File

@@ -31,8 +31,8 @@ export default class OpenInNewTab {
this._openmct = openmct;
}
invoke(objectPath, urlParams = undefined) {
let url = objectPathToUrl(this._openmct, objectPath, urlParams);
invoke(objectPath) {
let url = objectPathToUrl(this._openmct, objectPath);
window.open(url);
}
}

View File

@@ -234,8 +234,7 @@ class CouchObjectProvider {
#handleResponseCode(status, json, fetchOptions) {
this.indicator.setIndicatorToState(this.#statusCodeToIndicatorState(status));
if (status === CouchObjectProvider.HTTP_CONFLICT) {
const objectName = JSON.parse(fetchOptions.body)?.model?.name;
throw new this.openmct.objects.errors.Conflict(`Conflict persisting "${objectName}"`);
throw new this.openmct.objects.errors.Conflict(`Conflict persisting ${fetchOptions.body.name}`);
} else if (status >= CouchObjectProvider.HTTP_BAD_REQUEST) {
if (!json.error || !json.reason) {
throw new Error(`CouchDB Error ${status}`);

View File

@@ -129,13 +129,11 @@ export default {
this.timeContext.on("timeSystem", this.setScaleAndPlotActivities);
this.timeContext.on("bounds", this.updateViewBounds);
this.timeContext.on("clock", this.updateBounds);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off("timeSystem", this.setScaleAndPlotActivities);
this.timeContext.off("bounds", this.updateViewBounds);
this.timeContext.off("clock", this.updateBounds);
}
},
observeForChanges(mutatedObject) {
@@ -144,15 +142,10 @@ export default {
},
resize() {
let clientWidth = this.getClientWidth();
let clientHeight = this.getClientHeight();
if (clientWidth !== this.width) {
this.setDimensions();
this.updateViewBounds();
}
if (clientHeight !== this.height) {
this.setDimensions();
}
},
getClientWidth() {
let clientWidth = this.$refs.plan.clientWidth;
@@ -167,27 +160,9 @@ export default {
return clientWidth - 200;
},
getClientHeight() {
let clientHeight = this.$refs.plan.clientHeight;
if (!clientHeight) {
//this is a hack - need a better way to find the parent of this component
let parent = this.openmct.layout.$refs.browseObject.$el;
if (parent) {
clientHeight = parent.getBoundingClientRect().height;
}
}
return clientHeight;
},
getPlanData(domainObject) {
this.planData = getValidatedData(domainObject);
},
updateBounds(clock) {
if (clock === undefined) {
this.viewBounds = Object.create(this.timeContext.bounds());
}
},
updateViewBounds(bounds) {
if (bounds) {
this.viewBounds = Object.create(bounds);
@@ -216,8 +191,10 @@ export default {
activities.forEach(activity => activity.remove());
},
setDimensions() {
const planHolder = this.$refs.plan;
this.width = this.getClientWidth();
this.height = this.getClientHeight();
this.height = Math.round(planHolder.getBoundingClientRect().height);
},
setScale(timeSystem) {
if (!this.width) {

View File

@@ -383,8 +383,10 @@ export default {
},
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.path);
this.followTimeContext();
},
followTimeContext() {
this.updateDisplayBounds(this.timeContext.bounds());
@@ -1190,15 +1192,11 @@ export default {
this.$emit('statusUpdated', status);
},
handleWindowResize() {
const { plotWrapper } = this.$parent.$refs;
if (!plotWrapper) {
return;
}
const newOffsetWidth = plotWrapper.offsetWidth;
const newOffsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;
//we ignore when width gets smaller
const offsetChange = newOffsetWidth - this.offsetWidth;
if (offsetChange > OFFSET_THRESHOLD) {
if (this.$parent.$refs.plotWrapper
&& offsetChange > OFFSET_THRESHOLD) {
this.offsetWidth = newOffsetWidth;
this.config.series.models.forEach(this.loadSeriesData, this);
}

View File

@@ -83,8 +83,6 @@ export default class PlotSeries extends Model {
// Model.apply(this, arguments);
this.onXKeyChange(this.get('xKey'));
this.onYKeyChange(this.get('yKey'));
this.unPlottableValues = [undefined, Infinity, -Infinity];
}
/**
@@ -344,10 +342,6 @@ export default class PlotSeries extends Model {
let stats = this.get('stats');
let changed = false;
if (!stats) {
if ([Infinity, -Infinity].includes(value)) {
return;
}
stats = {
minValue: value,
minPoint: point,
@@ -356,13 +350,13 @@ export default class PlotSeries extends Model {
};
changed = true;
} else {
if (stats.maxValue < value && value !== Infinity) {
if (stats.maxValue < value) {
stats.maxValue = value;
stats.maxPoint = point;
changed = true;
}
if (stats.minValue > value && value !== -Infinity) {
if (stats.minValue > value) {
stats.minValue = value;
stats.minPoint = point;
changed = true;
@@ -425,7 +419,7 @@ export default class PlotSeries extends Model {
* @private
*/
isValueInvalid(val) {
return Number.isNaN(val) || this.unPlottableValues.includes(val);
return Number.isNaN(val) || val === undefined;
}
/**

View File

@@ -62,8 +62,8 @@ export default class RemoteClock extends DefaultClock {
}
start() {
this.openmct.time.on('timeSystem', this._timeSystemChange);
this.openmct.objects.get(this.identifier).then((domainObject) => {
this.openmct.time.on('timeSystem', this._timeSystemChange);
this.timeTelemetryObject = domainObject;
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
this._timeSystemChange();

View File

@@ -71,10 +71,7 @@ describe("the RemoteClock plugin", () => {
parse: (datum) => datum.key
};
let objectPromise;
let requestPromise;
beforeEach(() => {
beforeEach(async () => {
openmct.install(openmct.plugins.RemoteClock(TIME_TELEMETRY_ID));
let clocks = openmct.time.getAllClocks();
@@ -92,9 +89,7 @@ describe("the RemoteClock plugin", () => {
spyOn(metadata, 'value').and.callThrough();
let requestPromiseResolve;
let objectPromiseResolve;
requestPromise = new Promise((resolve) => {
let requestPromise = new Promise((resolve) => {
requestPromiseResolve = resolve;
});
spyOn(openmct.telemetry, 'request').and.callFake(() => {
@@ -103,7 +98,8 @@ describe("the RemoteClock plugin", () => {
return requestPromise;
});
objectPromise = new Promise((resolve) => {
let objectPromiseResolve;
let objectPromise = new Promise((resolve) => {
objectPromiseResolve = resolve;
});
spyOn(openmct.objects, 'get').and.callFake(() => {
@@ -116,48 +112,39 @@ describe("the RemoteClock plugin", () => {
start: OFFSET_START,
end: OFFSET_END
});
await Promise.all([objectPromiseResolve, requestPromise]);
});
it("Does not throw error if time system is changed before remote clock initialized", () => {
expect(() => openmct.time.timeSystem('utc')).not.toThrow();
it('is available and sets up initial values and listeners', () => {
expect(remoteClock.key).toEqual(REMOTE_CLOCK_KEY);
expect(remoteClock.identifier).toEqual(TIME_TELEMETRY_ID);
expect(openmct.time.on).toHaveBeenCalledWith('timeSystem', remoteClock._timeSystemChange);
expect(remoteClock._timeSystemChange).toHaveBeenCalled();
});
describe('once resolved', () => {
beforeEach(async () => {
await Promise.all([objectPromise, requestPromise]);
});
it('is available and sets up initial values and listeners', () => {
expect(remoteClock.key).toEqual(REMOTE_CLOCK_KEY);
expect(remoteClock.identifier).toEqual(TIME_TELEMETRY_ID);
expect(openmct.time.on).toHaveBeenCalledWith('timeSystem', remoteClock._timeSystemChange);
expect(remoteClock._timeSystemChange).toHaveBeenCalled();
});
it('will request/store the object based on the identifier passed in', () => {
expect(remoteClock.timeTelemetryObject).toEqual(object);
});
it('will request metadata and set up formatters', () => {
expect(remoteClock.metadata).toEqual(metadata);
expect(metadata.value).toHaveBeenCalled();
expect(openmct.telemetry.getValueFormatter).toHaveBeenCalledWith(metadataValue);
});
it('will request the latest datum for the object it received and process the datum returned', () => {
expect(openmct.telemetry.request).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, REQ_OPTIONS);
expect(boundsCallback).toHaveBeenCalledWith({
start: TIME_VALUE + OFFSET_START,
end: TIME_VALUE + OFFSET_END
}, true);
});
it('will set up subscriptions correctly', () => {
expect(remoteClock._unsubscribe).toBeDefined();
expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, remoteClock._processDatum);
});
it('will request/store the object based on the identifier passed in', () => {
expect(remoteClock.timeTelemetryObject).toEqual(object);
});
it('will request metadata and set up formatters', () => {
expect(remoteClock.metadata).toEqual(metadata);
expect(metadata.value).toHaveBeenCalled();
expect(openmct.telemetry.getValueFormatter).toHaveBeenCalledWith(metadataValue);
});
it('will request the latest datum for the object it received and process the datum returned', () => {
expect(openmct.telemetry.request).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, REQ_OPTIONS);
expect(boundsCallback).toHaveBeenCalledWith({
start: TIME_VALUE + OFFSET_START,
end: TIME_VALUE + OFFSET_END
}, true);
});
it('will set up subscriptions correctly', () => {
expect(remoteClock._unsubscribe).toBeDefined();
expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, remoteClock._processDatum);
});
});
});

View File

@@ -35,7 +35,6 @@ export default class RemoveAction {
this.openmct = openmct;
this.removeFromComposition = this.removeFromComposition.bind(this); // for access to private transaction variable
this.#transaction = null;
}
async invoke(objectPath) {
@@ -153,13 +152,16 @@ export default class RemoveAction {
}
}
async saveTransaction() {
saveTransaction() {
if (!this.#transaction) {
return;
}
await this.#transaction.commit();
this.openmct.objects.endTransaction();
this.#transaction = null;
return this.#transaction.commit()
.catch(error => {
throw error;
}).finally(() => {
this.openmct.objects.endTransaction();
});
}
}

View File

@@ -78,8 +78,6 @@ describe("The Remove Action plugin", () => {
spyOn(removeAction, 'removeFromComposition').and.callThrough();
spyOn(removeAction, 'inNavigationPath').and.returnValue(false);
spyOn(openmct.objects, 'mutate').and.callThrough();
spyOn(openmct.objects, 'startTransaction').and.callThrough();
spyOn(openmct.objects, 'endTransaction').and.callThrough();
removeAction.removeFromComposition(parentObject, childObject);
});
@@ -92,17 +90,6 @@ describe("The Remove Action plugin", () => {
expect(openmct.objects.mutate).toHaveBeenCalled();
expect(openmct.objects.mutate.calls.argsFor(0)[0]).toEqual(parentObject);
});
it("it should start a transaction", () => {
expect(openmct.objects.startTransaction).toHaveBeenCalled();
});
it("it should end the transaction", (done) => {
setTimeout(() => {
expect(openmct.objects.endTransaction).toHaveBeenCalled();
done();
}, 100);
});
});
describe("when determining the object is applicable", () => {

View File

@@ -178,7 +178,7 @@ define([
if (this.paused) {
this.delayedActions.push(this.tableRows.addRows.bind(this, telemetryRows, 'add'));
} else {
this.tableRows.addRows(telemetryRows);
this.tableRows.addRows(telemetryRows, 'add');
}
};
}
@@ -229,7 +229,7 @@ define([
});
});
this.tableRows.clearRowsFromTableAndFilter(allRows);
this.tableRows.addRows(allRows, 'filter');
}
updateFilters(updatedFilters) {

View File

@@ -61,39 +61,30 @@ define(
this.emit('remove', removed);
}
addRows(rows) {
let rowsToAdd = this.filterRows(rows);
addRows(rows, type = 'add') {
if (this.sortOptions === undefined) {
throw 'Please specify sort options';
}
let isFilterTriggeredReset = type === 'filter';
let anyActiveFilters = Object.keys(this.columnFilters).length > 0;
let rowsToAdd = !anyActiveFilters ? rows : rows.filter(this.matchesFilters, this);
// if type is filter, then it's a reset of all rows,
// need to wipe current rows
if (isFilterTriggeredReset) {
this.rows = [];
}
this.sortAndMergeRows(rowsToAdd);
// we emit filter no matter what to trigger
// an update of visible rows
if (rowsToAdd.length > 0) {
this.emit('add', rowsToAdd);
if (rowsToAdd.length > 0 || isFilterTriggeredReset) {
this.emit(type, rowsToAdd);
}
}
clearRowsFromTableAndFilter(rows) {
let rowsToAdd = this.filterRows(rows);
// Reset of all rows, need to wipe current rows
this.rows = [];
this.sortAndMergeRows(rowsToAdd);
// We emit filter and update of visible rows
this.emit('filter', rowsToAdd);
}
filterRows(rows) {
if (Object.keys(this.columnFilters).length > 0) {
return rows.filter(this.matchesFilters, this);
}
return rows;
}
sortAndMergeRows(rows) {
const sortedRowsToAdd = this.sortCollection(rows);

View File

@@ -124,10 +124,12 @@ export default {
};
this.items.push(item);
this.updateContentHeight();
},
removeItem(identifier) {
let index = this.items.findIndex(item => this.openmct.objects.areIdsEqual(identifier, item.domainObject.identifier));
this.items.splice(index, 1);
this.updateContentHeight();
},
reorder(reorderPlan) {
let oldItems = this.items.slice();
@@ -136,23 +138,7 @@ export default {
});
},
updateContentHeight() {
const clientHeight = this.getClientHeight();
if (this.height !== clientHeight) {
this.height = clientHeight;
}
},
getClientHeight() {
let clientHeight = this.$refs.contentHolder.getBoundingClientRect().height;
if (!clientHeight) {
//this is a hack - need a better way to find the parent of this component
let parent = this.openmct.layout.$refs.browseObject.$el;
if (parent) {
clientHeight = parent.getBoundingClientRect().height;
}
}
return clientHeight;
this.height = Math.round(this.$refs.contentHolder.getBoundingClientRect().height);
},
getTimeSystems() {
const timeSystems = this.openmct.time.getAllTimeSystems();
@@ -169,9 +155,7 @@ export default {
//TODO: Some kind of translation via an offset? of current bounds to target timeSystem
return currentBounds;
},
updateViewBounds() {
const bounds = this.timeContext.bounds();
this.updateContentHeight();
updateViewBounds(bounds) {
let currentTimeSystem = this.timeSystems.find(item => item.timeSystem.key === this.openmct.time.timeSystem().key);
if (currentTimeSystem) {
currentTimeSystem.bounds = bounds;
@@ -182,14 +166,12 @@ export default {
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.getTimeSystems();
this.updateViewBounds();
this.updateViewBounds(this.timeContext.bounds());
this.timeContext.on('bounds', this.updateViewBounds);
this.timeContext.on('clock', this.updateViewBounds);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.updateViewBounds);
this.timeContext.off('clock', this.updateViewBounds);
}
}
}

View File

@@ -27,7 +27,6 @@ import EventEmitter from "EventEmitter";
describe('the plugin', function () {
let objectDef;
let appHolder;
let element;
let child;
let openmct;
@@ -93,10 +92,6 @@ describe('the plugin', function () {
};
beforeEach((done) => {
appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
mockObjectPath = [
{
name: 'mock folder',
@@ -138,7 +133,7 @@ describe('the plugin', function () {
element.appendChild(child);
openmct.on('start', done);
openmct.start(appHolder);
openmct.startHeadless();
});
afterEach(() => {
@@ -172,7 +167,7 @@ describe('the plugin', function () {
const applicableViews = openmct.objectViews.get(testViewObject, mockObjectPath);
timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view');
let view = timelineView.view(testViewObject, mockObjectPath);
let view = timelineView.view(testViewObject, element);
view.show(child, true);
return Vue.nextTick();
@@ -250,7 +245,7 @@ describe('the plugin', function () {
beforeEach(done => {
const applicableViews = openmct.objectViews.get(testViewObject, mockObjectPath);
timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view');
let view = timelineView.view(testViewObject, mockObjectPath);
let view = timelineView.view(testViewObject, element);
view.show(child, true);
Vue.nextTick(done);
@@ -286,7 +281,7 @@ describe('the plugin', function () {
beforeEach((done) => {
const applicableViews = openmct.objectViews.get(testViewObject2, mockObjectPath);
timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view');
let view = timelineView.view(testViewObject2, mockObjectPath);
let view = timelineView.view(testViewObject2, element);
view.show(child, true);
Vue.nextTick(done);

View File

@@ -24,27 +24,9 @@
* Module defining url handling.
*/
function getUrlParams(openmct, customUrlParams = {}) {
let urlParams = openmct.router.getParams();
Object.entries(customUrlParams).forEach((urlParam) => {
const [key, value] = urlParam;
urlParams[key] = value;
});
if (urlParams['tc.mode'] === 'fixed') {
delete urlParams['tc.startDelta'];
delete urlParams['tc.endDelta'];
} else if (urlParams['tc.mode'] === 'local') {
delete urlParams['tc.startBound'];
delete urlParams['tc.endBound'];
}
return urlParams;
}
export function paramsToArray(openmct, customUrlParams = {}) {
export function paramsToArray(openmct) {
// parse urlParams from an object to an array.
let urlParams = getUrlParams(openmct, customUrlParams);
let urlParams = openmct.router.getParams();
let newTabParams = [];
for (let key in urlParams) {
if ({}.hasOwnProperty.call(urlParams, key)) {
@@ -60,9 +42,9 @@ export function identifierToString(openmct, objectPath) {
return '#/browse/' + openmct.objects.getRelativePath(objectPath);
}
export default function objectPathToUrl(openmct, objectPath, customUrlParams = {}) {
export default function objectPathToUrl(openmct, objectPath) {
let url = identifierToString(openmct, objectPath);
let urlParams = paramsToArray(openmct, customUrlParams);
let urlParams = paramsToArray(openmct);
if (urlParams.length) {
url += '?' + urlParams.join('&');
}

View File

@@ -66,14 +66,5 @@ describe('the url tool', function () {
const constructedURL = objectPathToUrl(openmct, mockObjectPath);
expect(constructedURL).toContain('#/browse/mock-parent-folder/mock-folder');
});
it('can take params to set a custom url', () => {
const customParams = {
'tc.startBound': 1669911059,
'tc.endBound': 1669911082,
'tc.mode': 'fixed'
};
const constructedURL = objectPathToUrl(openmct, mockObjectPath, customParams);
expect(constructedURL).toContain('tc.startBound=1669911059&tc.endBound=1669911082&tc.mode=fixed');
});
});
});

View File

@@ -94,13 +94,15 @@ export default {
if (this.openmct.time.clock() === undefined) {
let nowMarker = document.querySelector('.nowMarker');
if (nowMarker) {
nowMarker.classList.add('hidden');
nowMarker.parentNode.removeChild(nowMarker);
}
} else {
let nowMarker = document.querySelector('.nowMarker');
if (nowMarker) {
nowMarker.classList.remove('hidden');
nowMarker.style.height = this.contentHeight + 'px';
const svgEl = d3Selection.select(this.svgElement).node();
let height = svgEl.style('height').replace('px', '');
height = Number(height) + this.contentHeight;
nowMarker.style.height = height + 'px';
const now = this.xScale(Date.now());
nowMarker.style.left = now + this.offset + 'px';
}

View File

@@ -7,11 +7,7 @@
:checked="checked"
@change="onUserSelect($event)"
>
<span
class="c-toggle-switch__slider"
role="switch"
:aria-label="name"
></span>
<span class="c-toggle-switch__slider"></span>
</label>
<div
v-if="label && label.length"
@@ -36,11 +32,6 @@ export default {
required: false,
default: ''
},
name: {
type: String,
required: false,
default: ''
},
checked: Boolean
},
methods: {

View File

@@ -22,10 +22,8 @@
import ObjectView from './ObjectView.vue';
import StackedPlot from '../../plugins/plot/stackedPlot/StackedPlot.vue';
import Plot from '../../plugins/plot/Plot.vue';
export default {
ObjectView,
StackedPlot,
Plot
StackedPlot
};

View File

@@ -32,10 +32,6 @@
z-index: 10;
background: gray;
&.hidden {
display: none;
}
& .icon-arrow-down {
font-size: large;
position: absolute;

View File

@@ -335,7 +335,6 @@ export default {
dialog.dismiss();
this.openmct.notifications.error('Error saving objects');
console.error(error);
this.openmct.editor.cancel();
});
},
saveAndContinueEditing() {

View File

@@ -76,7 +76,7 @@
<div :style="childrenHeightStyles">
<tree-item
v-for="(treeItem, index) in visibleItems"
:key="`${treeItem.navigationPath}-${index}`"
:key="treeItem.navigationPath"
:node="treeItem"
:is-selector-tree="isSelectorTree"
:selected-item="selectedItem"
@@ -174,7 +174,8 @@ export default {
itemOffset: 0,
activeSearch: false,
mainTreeTopMargin: undefined,
selectedItem: {}
selectedItem: {},
observers: {}
};
},
computed: {
@@ -276,13 +277,10 @@ export default {
this.treeResizeObserver.disconnect();
}
this.destroyObservers();
this.destroyMutables();
this.destroyObservers(this.observers);
},
methods: {
async initialize() {
this.observers = {};
this.mutables = {};
this.isLoading = true;
this.getSavedOpenItems();
this.treeResizeObserver = new ResizeObserver(this.handleTreeResize);
@@ -357,15 +355,8 @@ export default {
}
this.treeItems = this.treeItems.filter((checkItem) => {
if (checkItem.navigationPath !== path
&& checkItem.navigationPath.includes(path)) {
this.destroyObserverByPath(checkItem.navigationPath);
this.destroyMutableByPath(checkItem.navigationPath);
return false;
}
return true;
return checkItem.navigationPath === path
|| !checkItem.navigationPath.includes(path);
});
this.openTreeItems.splice(pathIndex, 1);
this.removeCompositionListenerFor(path);
@@ -445,17 +436,7 @@ export default {
}, Promise.resolve()).then(() => {
if (this.isSelectorTree) {
// If item is missing due to error in object creation,
// walk up the navigationPath until we find an item
let item = this.getTreeItemByPath(navigationPath);
while (!item) {
const startIndex = 0;
const endIndex = navigationPath.lastIndexOf('/');
navigationPath = navigationPath.substring(startIndex, endIndex);
item = this.getTreeItemByPath(navigationPath);
}
this.treeItemSelection(item);
this.treeItemSelection(this.getTreeItemByPath(navigationPath));
}
});
},
@@ -556,7 +537,7 @@ export default {
composition = sortedComposition;
}
if (parentObjectPath.length && !this.isSelectorTree) {
if (parentObjectPath.length) {
let navigationPath = this.buildNavigationPath(parentObjectPath);
if (this.compositionCollections[navigationPath]) {
@@ -575,15 +556,7 @@ export default {
}
return composition.map((object) => {
// Only add observers and mutables if this is NOT a selector tree
if (!this.isSelectorTree) {
if (this.openmct.objects.supportsMutation(object.identifier)) {
object = this.openmct.objects.toMutable(object);
this.addMutable(object, parentObjectPath);
}
this.addTreeItemObserver(object, parentObjectPath);
}
this.addTreeItemObserver(object, parentObjectPath);
return this.buildTreeItem(object, parentObjectPath);
});
@@ -601,15 +574,6 @@ export default {
navigationPath
};
},
addMutable(mutableDomainObject, parentObjectPath) {
const objectPath = [mutableDomainObject].concat(parentObjectPath);
const navigationPath = this.buildNavigationPath(objectPath);
// If the mutable already exists, destroy it.
this.destroyMutableByPath(navigationPath);
this.mutables[navigationPath] = () => this.openmct.objects.destroyMutable(mutableDomainObject);
},
addTreeItemObserver(domainObject, parentObjectPath) {
const objectPath = [domainObject].concat(parentObjectPath);
const navigationPath = this.buildNavigationPath(objectPath);
@@ -624,6 +588,30 @@ export default {
this.sortTreeItems.bind(this, parentObjectPath)
);
},
async updateTreeItems(parentObjectPath) {
let children;
if (parentObjectPath.length) {
const parentItem = this.treeItems.find(item => item.objectPath === parentObjectPath);
const descendants = this.getChildrenInTreeFor(parentItem, true);
const parentIndex = this.treeItems.map(e => e.object).indexOf(parentObjectPath[0]);
children = await this.loadAndBuildTreeItemsFor(parentItem.object, parentItem.objectPath);
this.treeItems.splice(parentIndex + 1, descendants.length, ...children);
} else {
const root = await this.openmct.objects.get('ROOT');
children = await this.loadAndBuildTreeItemsFor(root, []);
this.treeItems = [...children];
}
for (let item of children) {
if (this.isTreeItemOpen(item)) {
this.openTreeItem(item);
}
}
},
sortTreeItems(parentObjectPath) {
const navigationPath = this.buildNavigationPath(parentObjectPath);
const parentItem = this.getTreeItemByPath(navigationPath);
@@ -674,10 +662,6 @@ export default {
const descendants = this.getChildrenInTreeFor(parentItem, true);
const directDescendants = this.getChildrenInTreeFor(parentItem);
if (domainObject.isMutable) {
this.addMutable(domainObject, parentItem.objectPath);
}
this.addTreeItemObserver(domainObject, parentItem.objectPath);
if (directDescendants.length === 0) {
@@ -708,15 +692,13 @@ export default {
},
compositionRemoveHandler(navigationPath) {
return (identifier) => {
const removeKeyString = this.openmct.objects.makeKeyString(identifier);
const parentItem = this.getTreeItemByPath(navigationPath);
const directDescendants = this.getChildrenInTreeFor(parentItem);
const removeItem = directDescendants.find(item => item.id === removeKeyString);
let removeKeyString = this.openmct.objects.makeKeyString(identifier);
let parentItem = this.getTreeItemByPath(navigationPath);
let directDescendants = this.getChildrenInTreeFor(parentItem);
let removeItem = directDescendants.find(item => item.id === removeKeyString);
// Remove the item from the tree, unobserve it, and clean up any mutables
this.removeItemFromTree(removeItem);
this.destroyObserverByPath(removeItem.navigationPath);
this.destroyMutableByPath(removeItem.navigationPath);
this.removeItemFromObservers(removeItem);
};
},
removeCompositionListenerFor(navigationPath) {
@@ -738,6 +720,13 @@ export default {
const removeIndex = this.getTreeItemIndex(item.navigationPath);
this.treeItems.splice(removeIndex, 1);
},
removeItemFromObservers(item) {
if (this.observers[item.id]) {
this.observers[item.id]();
delete this.observers[item.id];
}
},
addItemToTreeBefore(addItem, beforeItem) {
const addIndex = this.getTreeItemIndex(beforeItem.navigationPath);
@@ -803,17 +792,12 @@ export default {
for (const result of results) {
if (!abortSignal.aborted) {
// Don't show deleted objects in search results
if (result.location === null) {
continue;
}
resultPromises.push(this.openmct.objects.getOriginalPath(result.identifier).then((objectPath) => {
// removing the item itself, as the path we pass to buildTreeItem is a parent path
objectPath.shift();
// if root, remove, we're not using in object path for tree
const lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;
let lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;
if (lastObject && lastObject.type === 'root') {
objectPath.pop();
}
@@ -975,46 +959,13 @@ export default {
handleTreeResize() {
this.calculateHeights();
},
/**
* Destroy an observer for the given navigationPath.
*/
destroyObserverByPath(navigationPath) {
if (this.observers[navigationPath]) {
this.observers[navigationPath]();
delete this.observers[navigationPath];
}
},
/**
* Destroy all observers.
*/
destroyObservers() {
Object.entries(this.observers).forEach(([key, unobserve]) => {
if (unobserve) {
destroyObservers(observers) {
Object.entries(observers).forEach(([keyString, unobserve]) => {
if (typeof unobserve === 'function') {
unobserve();
}
delete this.observers[key];
});
},
/**
* Destroy a mutable for the given navigationPath.
*/
destroyMutableByPath(navigationPath) {
if (this.mutables[navigationPath]) {
this.mutables[navigationPath]();
delete this.mutables[navigationPath];
}
},
/**
* Destroy all mutables.
*/
destroyMutables() {
Object.entries(this.mutables).forEach(([key, destroyMutable]) => {
if (destroyMutable) {
destroyMutable();
}
delete this.mutables[key];
delete observers[keyString];
});
}
}

View File

@@ -22,5 +22,5 @@ module.exports = merge(common, {
__OPENMCT_ROOT_RELATIVE__: '""'
})
],
devtool: 'source-map'
devtool: 'eval-source-map'
});