Compare commits

..

5 Commits

Author SHA1 Message Date
Andrew Henry
7b88655373 Merge branch 'master' into 5211-tests-arent-using-source-maps-master 2022-07-28 11:19:28 -07:00
Scott Bell
9c54a9f862 Merge branch 'master' into 5211-tests-arent-using-source-maps-master 2022-07-28 08:13:17 -07:00
Scott Bell
029605ebdc simplify config 2022-07-27 07:09:29 +02:00
Scott Bell
60b59d6bd7 closer to what we want 2022-07-27 07:09:23 +02:00
Scott Bell
9de84df6a9 removed unused require 2022-07-27 07:08:33 +02:00
50 changed files with 592 additions and 1367 deletions

View File

@@ -5,7 +5,6 @@ executors:
- image: mcr.microsoft.com/playwright:v1.23.0-focal - image: mcr.microsoft.com/playwright:v1.23.0-focal
environment: environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
parameters: parameters:
BUST_CACHE: BUST_CACHE:
description: "Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!" description: "Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!"
@@ -65,8 +64,8 @@ commands:
suite: suite:
type: string type: string
steps: steps:
- run: npm run cov:e2e:report || true - run: npm run cov:e2e:report
- run: npm run cov:e2e:<<parameters.suite>>:publish - run: npm run cov:e2e:<<parameters.suite>>:publish
orbs: orbs:
node: circleci/node@4.9.0 node: circleci/node@4.9.0
browser-tools: circleci/browser-tools@1.3.0 browser-tools: circleci/browser-tools@1.3.0
@@ -154,22 +153,6 @@ jobs:
- store_artifacts: - store_artifacts:
path: html-test-results path: html-test-results
- generate_and_store_version_and_filesystem_artifacts - generate_and_store_version_and_filesystem_artifacts
visual-test:
parameters:
node-version:
type: string
executor: pw-focal-development
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- run: npm run test:e2e:visual
- store_test_results:
path: test-results/results.xml
- store_artifacts:
path: test-results
- store_artifacts:
path: html-test-results
- generate_and_store_version_and_filesystem_artifacts
workflows: workflows:
overall-circleci-commit-status: #These jobs run on every commit overall-circleci-commit-status: #These jobs run on every commit
jobs: jobs:
@@ -185,9 +168,6 @@ workflows:
suite: stable suite: stable
- perf-test: - perf-test:
node-version: lts/gallium node-version: lts/gallium
- visual-test:
node-version: lts/gallium
the-nightly: #These jobs do not run on PRs, but against master at night the-nightly: #These jobs do not run on PRs, but against master at night
jobs: jobs:
- unit-test: - unit-test:
@@ -205,10 +185,6 @@ workflows:
name: e2e-full-nightly name: e2e-full-nightly
node-version: lts/gallium node-version: lts/gallium
suite: full suite: full
- perf-test:
node-version: lts/gallium
- visual-test:
node-version: lts/gallium
triggers: triggers:
- schedule: - schedule:
cron: "0 0 * * *" cron: "0 0 * * *"

25
.github/workflows/e2e-visual.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: "e2e-visual"
on:
workflow_dispatch:
pull_request:
types:
- labeled
- opened
schedule:
- cron: '28 21 * * 1-5'
jobs:
e2e-visual:
if: ${{ github.event.label.name == 'pr:visual' }} || ${{ github.event.workflow_dispatch }} || ${{ github.event.schedule }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npx playwright@1.23.0 install
- run: npm install
- name: Run the e2e visual tests
run: npm run test:e2e:visual
env:
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}

View File

@@ -148,7 +148,6 @@ Current list of test tags:
- `@localStorage` - Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB). - `@localStorage` - Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB).
- `@snapshot` - Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container. - `@snapshot` - Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container.
- `@unstable` - A new test or test which is known to be flaky. - `@unstable` - A new test or test which is known to be flaky.
- `@2p` - Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity.
### Continuous Integration ### Continuous Integration

View File

@@ -30,80 +30,36 @@
*/ */
/** /**
* Defines parameters to be used in the creation of a domain object. * This common function creates a `domainObject` with default options. It is the preferred way of creating objects
* @typedef {Object} CreateObjectOptions
* @property {string} type the type of domain object to create (e.g.: "Sine Wave Generator").
* @property {string} [name] the desired name of the created domain object.
* @property {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the Identifier or uuid of the parent object.
*/
/**
* Contains information about the newly created domain object.
* @typedef {Object} CreatedObjectInfo
* @property {string} name the name of the created object
* @property {string} uuid the uuid of the created object
* @property {string} url the relative url to the object (for use with `page.goto()`)
*/
/**
* This common function creates a domain object with the default options. It is the preferred way of creating objects
* in the e2e suite when uninterested in properties of the objects themselves. * in the e2e suite when uninterested in properties of the objects themselves.
*
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {CreateObjectOptions} options * @param {string} type
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object. * @param {string | undefined} name
*/ */
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) { async function createDomainObjectWithDefaults(page, type, name) {
const parentUrl = await getHashUrlToDomainObject(page, parent);
// Navigate to the parent object. This is necessary to create the object
// in the correct location, such as a folder, layout, or plot.
await page.goto(`${parentUrl}?hideTree=true`);
await page.waitForLoadState('networkidle');
//Click the Create button //Click the Create button
await page.click('button:has-text("Create")'); await page.click('button:has-text("Create")');
// Click the object specified by 'type' // Click the object specified by 'type'
await page.click(`li:text("${type}")`); await page.click(`text=${type}`);
// Modify the name input field of the domain object to accept 'name' // Modify the name input field of the domain object to accept 'name'
if (name) { if (name) {
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]'); const nameInput = page.locator('input[type="text"]').nth(2);
await nameInput.fill(""); await nameInput.fill("");
await nameInput.fill(name); await nameInput.fill(name);
} }
// Click OK button and wait for Navigate event // Click OK button and wait for Navigate event
await Promise.all([ await Promise.all([
page.waitForLoadState(), page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('[aria-label="Save"]'), page.click('text=OK')
// Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]); ]);
// Wait until the URL is updated
await page.waitForURL(`**/${parent}/*`);
const uuid = await getFocusedObjectUuid(page);
const objectUrl = await getHashUrlToDomainObject(page, uuid);
if (await _isInEditMode(page, uuid)) {
// Save (exit edit mode)
await page.locator('button[title="Save"]').click();
await page.locator('li[title="Save and Finish Editing"]').click();
}
return {
name: name || `Unnamed ${type}`,
uuid: uuid,
url: objectUrl
};
} }
/** /**
* Open the given `domainObject`'s context menu from the object tree. * Open the given `domainObject`'s context menu from the object tree.
* Expands the 'My Items' folder if it is not already expanded. * Expands the 'My Items' folder if it is not already expanded.
*
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {string} myItemsFolderName the name of the "My Items" folder * @param {string} myItemsFolderName the name of the "My Items" folder
* @param {string} domainObjectName the display name of the `domainObject` * @param {string} domainObjectName the display name of the `domainObject`
@@ -120,154 +76,8 @@ async function openObjectTreeContextMenu(page, myItemsFolderName, domainObjectNa
}); });
} }
/**
* Gets the UUID of the currently focused object by parsing the current URL
* and returning the last UUID in the path.
* @param {import('@playwright/test').Page} page
* @returns {Promise<string>} the uuid of the focused object
*/
async function getFocusedObjectUuid(page) {
const UUIDv4Regexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi;
const focusedObjectUuid = await page.evaluate((regexp) => {
return window.location.href.match(regexp).at(-1);
}, UUIDv4Regexp);
return focusedObjectUuid;
}
/**
* Returns the hashUrl to the domainObject given its uuid.
* Useful for directly navigating to the given domainObject.
*
* URLs returned will be of the form `'./browse/#/mine/<uuid0>/<uuid1>/...'`
*
* @param {import('@playwright/test').Page} page
* @param {string} uuid the uuid of the object to get the url for
* @returns {Promise<string>} the url of the object
*/
async function getHashUrlToDomainObject(page, uuid) {
const hashUrl = await page.evaluate(async (objectUuid) => {
const path = await window.openmct.objects.getOriginalPath(objectUuid);
let url = './#/browse/' + [...path].reverse()
.map((object) => window.openmct.objects.makeKeyString(object.identifier))
.join('/');
// Drop the vestigial '/ROOT' if it exists
if (url.includes('/ROOT')) {
url = url.split('/ROOT').join('');
}
return url;
}, uuid);
return hashUrl;
}
/**
* Utilizes the OpenMCT API to detect if the given object has an active transaction (is in Edit mode).
* @private
* @param {import('@playwright/test').Page} page
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} identifier
* @return {Promise<boolean>} true if the object has an active transaction, false otherwise
*/
async function _isInEditMode(page, identifier) {
// eslint-disable-next-line no-return-await
return await page.evaluate((objectIdentifier) => window.openmct.objects.isTransactionActive(objectIdentifier), identifier);
}
/**
* Set the time conductor mode to either fixed timespan or realtime mode.
* @param {import('@playwright/test').Page} page
* @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
*/
async function setTimeConductorMode(page, isFixedTimespan = true) {
// Click 'mode' button
await page.locator('.c-mode-button').click();
// Switch time conductor mode
if (isFixedTimespan) {
await page.locator('data-testid=conductor-modeOption-fixed').click();
} else {
await page.locator('data-testid=conductor-modeOption-realtime').click();
}
}
/**
* Set the time conductor to fixed timespan mode
* @param {import('@playwright/test').Page} page
*/
async function setFixedTimeMode(page) {
await setTimeConductorMode(page, true);
}
/**
* Set the time conductor to realtime mode
* @param {import('@playwright/test').Page} page
*/
async function setRealTimeMode(page) {
await setTimeConductorMode(page, false);
}
/**
* @typedef {Object} OffsetValues
* @property {string | undefined} hours
* @property {string | undefined} mins
* @property {string | undefined} secs
*/
/**
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
* @param {import('@playwright/test').Locator} offsetButton
*/
async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) {
await offsetButton.click();
if (hours) {
await page.fill('.pr-time-controls__hrs', hours);
}
if (mins) {
await page.fill('.pr-time-controls__mins', mins);
}
if (secs) {
await page.fill('.pr-time-controls__secs', secs);
}
// Click the check button
await page.locator('.pr-time__buttons .icon-check').click();
}
/**
* Set the values (hours, mins, secs) for the start time offset when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
*/
async function setStartOffset(page, offset) {
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
await setTimeConductorOffset(page, offset, startOffsetButton);
}
/**
* Set the values (hours, mins, secs) for the end time offset when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
*/
async function setEndOffset(page, offset) {
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
await setTimeConductorOffset(page, offset, endOffsetButton);
}
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
module.exports = { module.exports = {
createDomainObjectWithDefaults, createDomainObjectWithDefaults,
openObjectTreeContextMenu, openObjectTreeContextMenu
getHashUrlToDomainObject,
getFocusedObjectUuid,
setFixedTimeMode,
setRealTimeMode,
setStartOffset,
setEndOffset
}; };

View File

@@ -1,30 +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 should be used to install the Snow theme for Open MCT. Espresso is the default
// e.g.
// await page.addInitScript({ path: path.join(__dirname, 'useSnowTheme.js') });
document.addEventListener('DOMContentLoaded', () => {
const openmct = window.openmct;
openmct.install(openmct.plugins.Snow());
});

View File

@@ -32,7 +32,6 @@ const config = {
projects: [ projects: [
{ {
name: 'chrome', name: 'chrome',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
use: { use: {
browserName: 'chromium' browserName: 'chromium'
} }

View File

@@ -2,13 +2,12 @@
// playwright.config.js // playwright.config.js
// @ts-check // @ts-check
/** @type {import('@playwright/test').PlaywrightTestConfig<{ theme: string }>} */ /** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = { const config = {
retries: 0, // visual tests should never retry due to snapshot comparison errors retries: 0, // visual tests should never retry due to snapshot comparison errors
testDir: 'tests/visual', testDir: 'tests/visual',
testMatch: '**/*.visual.spec.js', // only run visual tests timeout: 90 * 1000,
timeout: 60 * 1000, workers: 1, // visual tests should never run in parallel due to test pollution
workers: 2, //Limit to 2 for CircleCI Agent
webServer: { webServer: {
command: 'cross-env NODE_ENV=test npm run start', command: 'cross-env NODE_ENV=test npm run start',
url: 'http://localhost:8080/#', url: 'http://localhost:8080/#',
@@ -16,35 +15,17 @@ const config = {
reuseExistingServer: !process.env.CI reuseExistingServer: !process.env.CI
}, },
use: { use: {
browserName: "chromium",
baseURL: 'http://localhost:8080/', baseURL: 'http://localhost:8080/',
headless: true, // this needs to remain headless to avoid visual changes due to GPU rendering in headed browsers headless: true, // this needs to remain headless to avoid visual changes due to GPU
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
screenshot: 'on', screenshot: 'on',
trace: 'on', trace: 'off',
video: 'off' video: 'off'
}, },
projects: [
{
name: 'chrome',
use: {
browserName: 'chromium'
}
},
{
name: 'chrome-snow-theme',
use: {
browserName: 'chromium',
theme: 'snow'
}
}
],
reporter: [ reporter: [
['list'], ['list'],
['junit', { outputFile: 'test-results/results.xml' }], ['junit', { outputFile: 'test-results/results.xml' }]
['html', {
open: 'on-failure',
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
}]
] ]
}; };

View File

@@ -27,8 +27,7 @@
*/ */
const { test, expect } = require('./baseFixtures'); const { test, expect } = require('./baseFixtures');
// const { createDomainObjectWithDefaults } = require('./appActions'); const { createDomainObjectWithDefaults } = require('./appActions');
const path = require('path');
/** /**
* @typedef {Object} ObjectCreateOptions * @typedef {Object} ObjectCreateOptions
@@ -37,16 +36,12 @@ const path = require('path');
*/ */
/** /**
* **NOTE: This feature is a work-in-progress and should not currently be used.**
*
* Used to create a new domain object as a part of getOrCreateDomainObject. * Used to create a new domain object as a part of getOrCreateDomainObject.
* @type {Map<string, string>} * @type {Map<string, string>}
*/ */
// const createdObjects = new Map(); const createdObjects = new Map();
/** /**
* **NOTE: This feature is a work-in-progress and should not currently be used.**
*
* This action will create a domain object for the test to reference and return the uuid. If an object * This action will create a domain object for the test to reference and return the uuid. If an object
* of a given name already exists, it will return the uuid of that object to the test instead of creating * of a given name already exists, it will return the uuid of that object to the test instead of creating
* a new file. The intent is to move object creation out of test suites which are not explicitly worried * a new file. The intent is to move object creation out of test suites which are not explicitly worried
@@ -55,29 +50,27 @@ const path = require('path');
* @param {ObjectCreateOptions} options * @param {ObjectCreateOptions} options
* @returns {Promise<string>} uuid of the domain object * @returns {Promise<string>} uuid of the domain object
*/ */
// async function getOrCreateDomainObject(page, options) { async function getOrCreateDomainObject(page, options) {
// const { type, name } = options; const { type, name } = options;
// const objectName = name ? `${type}:${name}` : type; const objectName = name ? `${type}:${name}` : type;
// if (createdObjects.has(objectName)) { if (createdObjects.has(objectName)) {
// return createdObjects.get(objectName); return createdObjects.get(objectName);
// } }
// await createDomainObjectWithDefaults(page, type, name); await createDomainObjectWithDefaults(page, type, name);
// // Once object is created, get the uuid from the url // Once object is created, get the uuid from the url
// const uuid = await page.evaluate(() => { const uuid = await page.evaluate(() => {
// return window.location.href.match(/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/)[0]; return window.location.href.match(/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/)[0];
// }); });
// createdObjects.set(objectName, uuid); createdObjects.set(objectName, uuid);
// return uuid; return uuid;
// } }
/** /**
* **NOTE: This feature is a work-in-progress and should not currently be used.**
*
* If provided, these options will be used to get or create the desired domain object before * If provided, these options will be used to get or create the desired domain object before
* any tests or test hooks have run. * any tests or test hooks have run.
* The `uuid` of the `domainObject` will then be available to use within the scoped tests. * The `uuid` of the `domainObject` will then be available to use within the scoped tests.
@@ -94,24 +87,7 @@ const path = require('path');
* ``` * ```
* @type {ObjectCreateOptions} * @type {ObjectCreateOptions}
*/ */
// const objectCreateOptions = null; const objectCreateOptions = null;
/**
* The default theme for VIPER and Open MCT is the 'espresso' theme. Overriding this value with 'snow' in our playwright config.js
* will override the default theme by injecting the 'snow' theme on launch.
*
* ### Example:
* ```js
* projects: [
* {
* name: 'chrome-snow-theme',
* use: {
* browserName: 'chromium',
* theme: 'snow'
* ```
* @type {'snow' | 'espresso'}
*/
const theme = 'espresso';
/** /**
* The name of the "My Items" folder in the domain object tree. * The name of the "My Items" folder in the domain object tree.
@@ -123,39 +99,27 @@ const theme = 'espresso';
const myItemsFolderName = "My Items"; const myItemsFolderName = "My Items";
exports.test = test.extend({ 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) => {
// 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') });
}
await use(page);
},
myItemsFolderName: [myItemsFolderName, { option: true }], myItemsFolderName: [myItemsFolderName, { option: true }],
// eslint-disable-next-line no-shadow // eslint-disable-next-line no-shadow
openmctConfig: async ({ myItemsFolderName }, use) => { openmctConfig: async ({ myItemsFolderName }, use) => {
await use({ myItemsFolderName }); await use({ myItemsFolderName });
} },
// objectCreateOptions: [objectCreateOptions, {option: true}], objectCreateOptions: [objectCreateOptions, {option: true}],
// eslint-disable-next-line no-shadow // eslint-disable-next-line no-shadow
// domainObject: [async ({ page, objectCreateOptions }, use) => { domainObject: [async ({ page, objectCreateOptions }, use) => {
// // FIXME: This is a false-positive caused by a bug in the eslint-plugin-playwright rule. // 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 // eslint-disable-next-line playwright/no-conditional-in-test
// if (objectCreateOptions === null) { if (objectCreateOptions === null) {
// await use(page); await use(page);
// return; return;
// } }
// //Go to baseURL //Go to baseURL
// await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
// const uuid = await getOrCreateDomainObject(page, objectCreateOptions); const uuid = await getOrCreateDomainObject(page, objectCreateOptions);
// await use({ uuid }); await use({ uuid });
// }, { auto: true }] }, { auto: true }]
}); });
exports.expect = expect; exports.expect = expect;

View File

@@ -1,88 +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.
*****************************************************************************/
const { test, expect } = require('../../baseFixtures.js');
const { createDomainObjectWithDefaults } = require('../../appActions.js');
test.describe('AppActions', () => {
test('createDomainObjectsWithDefaults', async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
const e2eFolder = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'e2e folder'
});
await test.step('Create multiple flat objects in a row', async () => {
const timer1 = await createDomainObjectWithDefaults(page, {
type: 'Timer',
name: 'Timer Foo',
parent: e2eFolder.uuid
});
const timer2 = await createDomainObjectWithDefaults(page, {
type: 'Timer',
name: 'Timer Bar',
parent: e2eFolder.uuid
});
const timer3 = await createDomainObjectWithDefaults(page, {
type: 'Timer',
name: 'Timer Baz',
parent: e2eFolder.uuid
});
await page.goto(timer1.url, { waitUntil: 'networkidle' });
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('Timer Bar');
await page.goto(timer3.url, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Baz');
});
await test.step('Create multiple nested objects in a row', async () => {
const folder1 = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Folder Foo',
parent: e2eFolder.uuid
});
const folder2 = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Folder Bar',
parent: folder1.uuid
});
const folder3 = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Folder Baz',
parent: folder2.uuid
});
await page.goto(folder1.url, { waitUntil: 'networkidle' });
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('Folder Bar');
await page.goto(folder3.url, { waitUntil: 'networkidle' });
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}`);
expect(folder3.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}/${folder3.uuid}`);
});
});
});

View File

@@ -29,7 +29,7 @@ relates to how we've extended it (i.e. ./e2e/baseFixtures.js) and assumptions ma
const { test } = require('../../baseFixtures.js'); const { test } = require('../../baseFixtures.js');
test.describe('baseFixtures tests', () => { test.describe('baseFixtures tests', () => {
test('Verify that tests fail if console.error is thrown', async ({ page }) => { test('Verify that tests fail if console.error is thrown @framework', async ({ page }) => {
test.fail(); test.fail();
//Go to baseURL //Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
@@ -41,7 +41,7 @@ test.describe('baseFixtures tests', () => {
]); ]);
}); });
test('Verify that tests pass if console.warn is thrown', async ({ page }) => { test('Verify that tests pass if console.warn is thrown @framework', async ({ page }) => {
//Go to baseURL //Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });

View File

@@ -52,13 +52,13 @@ const { createDomainObjectWithDefaults } = require('../../appActions');
// Structure: Try to keep a single describe block per logical groups of tests. If your test runtime exceeds 5 minutes or 500 lines, it's likely that it will need to be split. // Structure: Try to keep a single describe block per logical groups of tests. If your test runtime exceeds 5 minutes or 500 lines, it's likely that it will need to be split.
// Annotations: Please use the @unstable tag so that our automation can pick it up as a part of our test promotion pipeline. // Annotations: Please use the @unstable tag so that our automation can pick it up as a part of our test promotion pipeline.
test.describe('Renaming Timer Object', () => { test.describe('Renaming Timer Object @unstable', () => {
//Create a testcase name which will be obvious when it fails in CI //Create a testcase name which will be obvious when it fails in CI
test('Can create a new Timer object and rename it from actions Menu', async ({ page }) => { test('Can create a new Timer object and rename it from actions Menu', async ({ page }) => {
//Open a browser, navigate to the main page, and wait until all networkevents to resolve //Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
//We provide some helper functions in appActions like createDomainObjectWithDefaults. This example will create a Timer object //We provide some helper functions in appActions like createDomainObjectWithDefaults. This example will create a Timer object
await createDomainObjectWithDefaults(page, { type: 'Timer' }); await createDomainObjectWithDefaults(page, 'Timer');
//Assert the object to be created and check it's name in the title //Assert the object to be created and check it's name in the title
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer'); await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');
@@ -68,12 +68,13 @@ test.describe('Renaming Timer Object', () => {
//Assert that the name has changed in the browser bar to the value we assigned above //Assert that the name has changed in the browser bar to the value we assigned above
await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName); await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName);
}); });
test('An existing Timer object can be renamed twice', async ({ page }) => { test('An existing Timer object can be renamed twice', async ({ page }) => {
//Open a browser, navigate to the main page, and wait until all networkevents to resolve //Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
//We provide some helper functions in appActions like createDomainObjectWithDefaults. This example will create a Timer object //We provide some helper functions in appActions like createDomainObjectWithDefaults. This example will create a Timer object
await createDomainObjectWithDefaults(page, { type: 'Timer' }); await createDomainObjectWithDefaults(page, 'Timer');
//Expect the object to be created and check it's name in the title //Expect the object to be created and check it's name in the title
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer'); await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');

View File

@@ -31,13 +31,29 @@ TODO: Provide additional validation of object properties as it grows.
*/ */
const { createDomainObjectWithDefaults } = require('../../appActions.js');
const { test, expect } = require('../../pluginFixtures.js'); const { test, expect } = require('../../pluginFixtures.js');
test('Generate Visual Test Data @localStorage', async ({ page, context }) => { test('Generate Visual Test Data @localStorage', async ({ page, context, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
//Go to baseURL //Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
const overlayPlot = await createDomainObjectWithDefaults(page, { type: 'Overlay Plot' });
await page.locator('button:has-text("Create")').click();
// add overlay plot with defaults
await page.locator('li:has-text("Overlay Plot")').click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message')
]);
// save (exit edit mode)
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
await page.locator('text=Save and Finish Editing').click();
// click create button // click create button
await page.locator('button:has-text("Create")').click(); await page.locator('button:has-text("Create")').click();
@@ -51,12 +67,16 @@ test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
await Promise.all([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
page.locator('text=OK').click(), page.locator('text=OK').click(),
//Wait for Save Banner to appear //Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message') page.waitForSelector('.c-message-banner__message')
]); ]);
// focus the overlay plot // focus the overlay plot
await page.goto(overlayPlot.url); await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Overlay Plot').first().click()
]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot'); await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
//Save localStorage for future test execution //Save localStorage for future test execution

View File

@@ -25,22 +25,21 @@ This test suite is dedicated to testing our use of our custom fixtures to verify
that they are working as expected. that they are working as expected.
*/ */
const { test } = require('../../pluginFixtures.js'); const { test, expect } = require('../../pluginFixtures.js');
// eslint-disable-next-line playwright/no-skipped-test test.describe('pluginFixtures tests', () => {
test.describe.skip('pluginFixtures tests', () => { test.use({ domainObjectName: 'Timer' });
// test.use({ domainObjectName: 'Timer' }); let timerUUID;
// let timerUUID;
// test('Creates a timer object @framework @unstable', ({ domainObject }) => { test('Creates a timer object @framework @unstable', ({ domainObject }) => {
// const { uuid } = domainObject; const { uuid } = domainObject;
// const uuidRegexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/; const uuidRegexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/;
// expect(uuid).toMatch(uuidRegexp); expect(uuid).toMatch(uuidRegexp);
// timerUUID = uuid; timerUUID = uuid;
// }); });
// test('Provides same uuid for subsequent uses of the same object @framework', ({ domainObject }) => { test('Provides same uuid for subsequent uses of the same object @framework', ({ domainObject }) => {
// const { uuid } = domainObject; const { uuid } = domainObject;
// expect(uuid).toEqual(timerUUID); expect(uuid).toEqual(timerUUID);
// }); });
}); });

View File

@@ -38,14 +38,14 @@ test.describe('Branding tests', () => {
await expect(page.locator('.c-about__image')).toBeVisible(); await expect(page.locator('.c-about__image')).toBeVisible();
// Modify the Build information in 'about' Modal // Modify the Build information in 'about' Modal
const versionInformationLocator = page.locator('ul.t-info.l-info.s-info').first(); const versionInformationLocator = page.locator('ul.t-info.l-info.s-info');
await expect(versionInformationLocator).toBeEnabled(); await expect(versionInformationLocator).toBeEnabled();
await expect.soft(versionInformationLocator).toContainText(/Version: \d/); await expect.soft(versionInformationLocator).toContainText(/Version: \d/);
await expect.soft(versionInformationLocator).toContainText(/Build Date: ((?:Mon|Tue|Wed|Thu|Fri|Sat|Sun))/); await expect.soft(versionInformationLocator).toContainText(/Build Date: ((?:Mon|Tue|Wed|Thu|Fri|Sat|Sun))/);
await expect.soft(versionInformationLocator).toContainText(/Revision: \b[0-9a-f]{5,40}\b/); await expect.soft(versionInformationLocator).toContainText(/Revision: \b[0-9a-f]{5,40}\b/);
await expect.soft(versionInformationLocator).toContainText(/Branch: ./); await expect.soft(versionInformationLocator).toContainText(/Branch: ./);
}); });
test('Verify Links in About Modal @2p', async ({ page }) => { test('Verify Links in About Modal', async ({ page }) => {
// Go to baseURL // Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });

View File

@@ -35,10 +35,7 @@ test.describe('Example Event Generator CRUD Operations', () => {
//Create a name for the object //Create a name for the object
const newObjectName = 'Test Event Generator'; const newObjectName = 'Test Event Generator';
await createDomainObjectWithDefaults(page, { await createDomainObjectWithDefaults(page, 'Event Message Generator', newObjectName);
type: 'Event Message Generator',
name: newObjectName
});
//Assertions against newly created object which define standard behavior //Assertions against newly created object which define standard behavior
await expect(page.waitForURL(/.*&view=table/)).toBeTruthy(); await expect(page.waitForURL(/.*&view=table/)).toBeTruthy();

View File

@@ -27,7 +27,6 @@ demonstrate some playwright for test developers. This pattern should not be re-u
*/ */
const { test, expect } = require('../../../../pluginFixtures.js'); const { test, expect } = require('../../../../pluginFixtures.js');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
let conditionSetUrl; let conditionSetUrl;
let getConditionSetIdentifierFromUrl; let getConditionSetIdentifierFromUrl;
@@ -179,24 +178,3 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
}); });
}); });
test.describe('Basic Condition Set Use', () => {
test('Can add a condition', async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
// Create a new condition set
await createDomainObjectWithDefaults(page, {
type: 'Condition Set',
name: "Test Condition Set"
});
// Change the object to edit mode
await page.locator('[title="Edit"]').click();
// Click Add Condition button
await page.locator('#addCondition').click();
// Check that the new Unnamed Condition section appears
const numOfUnnamedConditions = await page.locator('text=Unnamed Condition').count();
expect(numOfUnnamedConditions).toEqual(1);
});
});

View File

@@ -1,122 +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.
*****************************************************************************/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
test.describe('Testing Display Layout @unstable', () => {
let sineWaveObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await setRealTimeMode(page);
// Create Sine Wave Generator
sineWaveObject = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: "Test Sine Wave Generator"
});
});
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in real time', async ({ page }) => {
// Create a Display Layout
await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: "Test Display Layout"
});
// Edit Display 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 Display Layout and save changes
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
// Subscribe to the Sine Wave Generator data
// 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 = await getTelemValuePromise;
const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`);
const displayLayoutValue = await displayLayoutValuePromise.textContent();
const trimmedDisplayValue = displayLayoutValue.trim();
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
await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: "Test Display Layout"
});
// Edit Display 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 Display Layout and save changes
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
// Subscribe to the Sine Wave Generator data
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
// Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window
await setStartOffset(page, { mins: '1' });
await setFixedTimeMode(page);
// On getting data, check if the value found in the Display Layout is the most recent value
// from the Sine Wave Generator
const formattedTelemetryValue = await getTelemValuePromise;
const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`);
const displayLayoutValue = await displayLayoutValuePromise.textContent();
const trimmedDisplayValue = displayLayoutValue.trim();
await expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
});
});
/**
* Util for subscribing to a telemetry object by object identifier
* Limitations: Currently only works to return telemetry once to the node scope
* To Do: See if there's a way to await this multiple times to allow for multiple
* values to be returned over time
* @param {import('@playwright/test').Page} page
* @param {string} objectIdentifier identifier for object
* @returns {Promise<string>} the formatted sin telemetry value
*/
async function subscribeToTelemetry(page, objectIdentifier) {
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getTelemValue', resolve));
await page.evaluate(async (telemetryIdentifier) => {
const telemetryObject = await window.openmct.objects.get(telemetryIdentifier);
const metadata = window.openmct.telemetry.getMetadata(telemetryObject);
const formats = await window.openmct.telemetry.getFormatMap(metadata);
window.openmct.telemetry.subscribe(telemetryObject, (obj) => {
const sinVal = obj.sin;
const formattedSinVal = formats.sin.format(sinVal);
window.getTelemValue(formattedSinVal);
});
}, objectIdentifier);
return getTelemValuePromise;
}

View File

@@ -28,7 +28,6 @@ but only assume that example imagery is present.
const { waitForAnimations } = require('../../../../baseFixtures'); const { waitForAnimations } = require('../../../../baseFixtures');
const { test, expect } = require('../../../../pluginFixtures'); const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const backgroundImageSelector = '.c-imagery__main-image__background-image'; const backgroundImageSelector = '.c-imagery__main-image__background-image';
const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt']; const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan'; const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
@@ -40,17 +39,26 @@ test.describe('Example Imagery Object', () => {
//Go to baseURL //Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
// Create a default 'Example Imagery' object //Click the Create button
createDomainObjectWithDefaults(page, { type: 'Example Imagery' }); await page.click('button:has-text("Create")');
// Click text=Example Imagery
await page.click('text=Example Imagery');
// Click text=OK
await Promise.all([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation({waitUntil: 'networkidle'}),
page.locator(backgroundImageSelector).hover({trial: true}), page.click('text=OK'),
// eslint-disable-next-line playwright/missing-playwright-await //Wait for Save Banner to appear
expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery') page.waitForSelector('.c-message-banner__message')
]); ]);
// Close Banner
await page.locator('.c-message-banner__close-button').click();
// Verify that the created object is focused //Wait until Save Banner is gone
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
await page.locator(backgroundImageSelector).hover({trial: true});
}); });
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => { test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
@@ -200,7 +208,7 @@ test.describe('Example Imagery Object', () => {
const pausePlayButton = page.locator('.c-button.pause-play'); const pausePlayButton = page.locator('.c-button.pause-play');
// open the time conductor drop down // open the time conductor drop down
await page.locator('.c-mode-button').click(); await page.locator('button:has-text("Fixed Timespan")').click();
// Click local clock // Click local clock
await page.locator('[data-testid="conductor-modeOption-realtime"]').click(); await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
@@ -524,7 +532,7 @@ test.describe('Example Imagery in Flexible layout', () => {
await page.locator('.c-mode-button').click(); await page.locator('.c-mode-button').click();
// Select local clock mode // Select local clock mode
await page.locator('[data-testid=conductor-modeOption-realtime]').nth(0).click(); await page.locator('[data-testid=conductor-modeOption-realtime]').click();
// Zoom in on next image // Zoom in on next image
await mouseZoomIn(page); await mouseZoomIn(page);

View File

@@ -1,120 +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.
*****************************************************************************/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
test.describe('Testing LAD table @unstable', () => {
let sineWaveObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await setRealTimeMode(page);
// Create Sine Wave Generator
sineWaveObject = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: "Test Sine Wave Generator"
});
});
test('telemetry value exactly matches latest telemetry value received in real time', async ({ page }) => {
// Create LAD table
await createDomainObjectWithDefaults(page, {
type: 'LAD Table',
name: "Test LAD Table"
});
// Edit LAD table
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 LAD table and save changes
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
// Subscribe to the Sine Wave Generator data
// On getting data, check if the value found in the LAD table is the most recent value
// from the Sine Wave Generator
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
const subscribeTelemValue = await getTelemValuePromise;
const ladTableValuePromise = await page.waitForSelector(`text="${subscribeTelemValue}"`);
const ladTableValue = await ladTableValuePromise.textContent();
expect(ladTableValue).toBe(subscribeTelemValue);
});
test('telemetry value exactly matches latest telemetry value received in fixed time', async ({ page }) => {
// Create LAD table
await createDomainObjectWithDefaults(page, {
type: 'LAD Table',
name: "Test LAD Table"
});
// Edit LAD table
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 LAD table and save changes
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
// Subscribe to the Sine Wave Generator data
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
// Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window
await setStartOffset(page, { mins: '1' });
await setFixedTimeMode(page);
// On getting data, check if the value found in the LAD table is the most recent value
// from the Sine Wave Generator
const subscribeTelemValue = await getTelemValuePromise;
const ladTableValuePromise = await page.waitForSelector(`text="${subscribeTelemValue}"`);
const ladTableValue = await ladTableValuePromise.textContent();
expect(ladTableValue).toBe(subscribeTelemValue);
});
});
/**
* Util for subscribing to a telemetry object by object identifier
* Limitations: Currently only works to return telemetry once to the node scope
* To Do: See if there's a way to await this multiple times to allow for multiple
* values to be returned over time
* @param {import('@playwright/test').Page} page
* @param {string} objectIdentifier identifier for object
* @returns {Promise<string>} the formatted sin telemetry value
*/
async function subscribeToTelemetry(page, objectIdentifier) {
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getTelemValue', resolve));
await page.evaluate(async (telemetryIdentifier) => {
const telemetryObject = await window.openmct.objects.get(telemetryIdentifier);
const metadata = window.openmct.telemetry.getMetadata(telemetryObject);
const formats = await window.openmct.telemetry.getFormatMap(metadata);
window.openmct.telemetry.subscribe(telemetryObject, (obj) => {
const sinVal = obj.sin;
const formattedSinVal = formats.sin.format(sinVal);
window.getTelemValue(formattedSinVal);
});
}, objectIdentifier);
return getTelemValuePromise;
}

View File

@@ -25,18 +25,25 @@ This test suite is dedicated to tests which verify form functionality.
*/ */
const { test, expect } = require('../../../../pluginFixtures'); const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
/** /**
* Creates a notebook object and adds an entry. * Creates a notebook object and adds an entry.
* @param {import('@playwright/test').Page} - page to load * @param {import('@playwright/test').Page} - page to load
* @param {number} [iterations = 1] - the number of entries to create * @param {number} [iterations = 1] - the number of entries to create
*/ */
async function createNotebookAndEntry(page, iterations = 1) { async function createNotebookAndEntry(page, myItemsFolderName, iterations = 1) {
//Go to baseURL //Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
createDomainObjectWithDefaults(page, { type: 'Notebook' }); // Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
await page.locator('[title="Create and save timestamped notes with embedded object snapshots."]').click();
// Click button:has-text("OK")
await Promise.all([
page.waitForNavigation(),
page.locator(`[name="mctForm"] >> text=${myItemsFolderName}`).click(),
page.locator('button:has-text("OK")').click()
]);
for (let iteration = 0; iteration < iterations; iteration++) { for (let iteration = 0; iteration < iterations; iteration++) {
// Click text=To start a new entry, click here or drag and drop any object // Click text=To start a new entry, click here or drag and drop any object
@@ -45,6 +52,7 @@ async function createNotebookAndEntry(page, iterations = 1) {
await page.locator(entryLocator).click(); await page.locator(entryLocator).click();
await page.locator(entryLocator).fill(`Entry ${iteration}`); await page.locator(entryLocator).fill(`Entry ${iteration}`);
} }
} }
/** /**
@@ -52,8 +60,8 @@ async function createNotebookAndEntry(page, iterations = 1) {
* @param {import('@playwright/test').Page} page * @param {import('@playwright/test').Page} page
* @param {number} [iterations = 1] - the number of entries (and tags) to create * @param {number} [iterations = 1] - the number of entries (and tags) to create
*/ */
async function createNotebookEntryAndTags(page, iterations = 1) { async function createNotebookEntryAndTags(page, myItemsFolderName, iterations = 1) {
await createNotebookAndEntry(page, iterations); await createNotebookAndEntry(page, myItemsFolderName, iterations);
for (let iteration = 0; iteration < iterations; iteration++) { for (let iteration = 0; iteration < iterations; iteration++) {
// Click text=To start a new entry, click here or drag and drop any object // Click text=To start a new entry, click here or drag and drop any object
@@ -73,10 +81,11 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
} }
} }
test.describe('Tagging in Notebooks @addInit', () => { test.describe('Tagging in Notebooks', () => {
test('Can load tags', async ({ page }) => { test('Can load tags', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
await createNotebookAndEntry(page); await createNotebookAndEntry(page, myItemsFolderName);
// Click text=To start a new entry, click here or drag and drop any object // Click text=To start a new entry, click here or drag and drop any object
await page.locator('button:has-text("Add Tag")').click(); await page.locator('button:has-text("Add Tag")').click();
@@ -87,8 +96,10 @@ test.describe('Tagging in Notebooks @addInit', () => {
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling"); await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Driving"); await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Driving");
}); });
test('Can add tags', async ({ page }) => { test('Can add tags', async ({ page, openmctConfig }) => {
await createNotebookEntryAndTags(page); const { myItemsFolderName } = openmctConfig;
await createNotebookEntryAndTags(page, myItemsFolderName);
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science"); await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving"); await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving");
@@ -102,8 +113,10 @@ test.describe('Tagging in Notebooks @addInit', () => {
await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Driving"); await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Driving");
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling"); await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
}); });
test('Can search for tags', async ({ page }) => { test('Can search for tags', async ({ page, openmctConfig }) => {
await createNotebookEntryAndTags(page); const { myItemsFolderName } = openmctConfig;
await createNotebookEntryAndTags(page, myItemsFolderName);
// Click [aria-label="OpenMCT Search"] input[type="search"] // Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill [aria-label="OpenMCT Search"] input[type="search"] // Fill [aria-label="OpenMCT Search"] input[type="search"]
@@ -126,8 +139,10 @@ test.describe('Tagging in Notebooks @addInit', () => {
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
}); });
test('Can delete tags', async ({ page }) => { test('Can delete tags', async ({ page, openmctConfig }) => {
await createNotebookEntryAndTags(page); const { myItemsFolderName } = openmctConfig;
await createNotebookEntryAndTags(page, myItemsFolderName);
await page.locator('[aria-label="Notebook Entries"]').click(); await page.locator('[aria-label="Notebook Entries"]').click();
// Delete Driving // Delete Driving
await page.locator('text=Science Driving Add Tag >> button').nth(1).click(); await page.locator('text=Science Driving Add Tag >> button').nth(1).click();
@@ -139,31 +154,28 @@ test.describe('Tagging in Notebooks @addInit', () => {
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving"); await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
}); });
test('Tags persist across reload', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
test('Can delete objects with tags and neither return in search', async ({ page }) => {
await createNotebookEntryAndTags(page);
// Delete Notebook
await page.locator('button[title="More options"]').click();
await page.locator('li[title="Remove this object from its containing object."]').click();
await page.locator('button:has-text("OK")').click();
await page.goto('./', { waitUntil: 'networkidle' });
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed');
await expect(page.locator('text=No matching results.')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sci');
await expect(page.locator('text=No matching results.')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('dri');
await expect(page.locator('text=No matching results.')).toBeVisible();
});
test('Tags persist across reload', async ({ page }) => {
//Go to baseURL //Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
await createDomainObjectWithDefaults(page, { type: 'Clock' }); // Create a clock object we can navigate to
await page.click('button:has-text("Create")');
// Click Clock
await page.click('text=Clock');
// Click button:has-text("OK")
await Promise.all([
page.waitForNavigation(),
page.locator(`[name="mctForm"] >> text=${myItemsFolderName}`).click(),
page.locator('button:has-text("OK")').click()
]);
await page.click('.c-disclosure-triangle');
const ITERATIONS = 4; const ITERATIONS = 4;
await createNotebookEntryAndTags(page, ITERATIONS); await createNotebookEntryAndTags(page, myItemsFolderName, ITERATIONS);
for (let iteration = 0; iteration < ITERATIONS; iteration++) { for (let iteration = 0; iteration < ITERATIONS; iteration++) {
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`; const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
@@ -171,11 +183,6 @@ test.describe('Tagging in Notebooks @addInit', () => {
await expect(page.locator(entryLocator)).toContainText("Driving"); await expect(page.locator(entryLocator)).toContainText("Driving");
} }
await Promise.all([
page.waitForNavigation(),
page.goto('./#/browse/mine?hideTree=false'),
page.click('.c-disclosure-triangle')
]);
// Click Unnamed Clock // Click Unnamed Clock
await page.click('text="Unnamed Clock"'); await page.click('text="Unnamed Clock"');

View File

@@ -20,26 +20,55 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const { test, expect } = require('../../../../pluginFixtures'); const { test, expect } = require('../../../../pluginFixtures');
test.describe('Telemetry Table', () => { test.describe('Telemetry Table', () => {
test('unpauses and filters data when paused by button and user changes bounds', async ({ page }) => { test('unpauses and filters data when paused by button and user changes bounds', async ({ page, openmctConfig }) => {
test.info().annotations.push({ test.info().annotations.push({
type: 'issue', type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5113' description: 'https://github.com/nasa/openmct/issues/5113'
}); });
const { myItemsFolderName } = openmctConfig;
const bannerMessage = '.c-message-banner__message';
const createButton = 'button:has-text("Create")';
await page.goto('./', { waitUntil: 'networkidle' }); await page.goto('./', { waitUntil: 'networkidle' });
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' }); // Click create button
await createDomainObjectWithDefaults(page, { await page.locator(createButton).click();
type: 'Sine Wave Generator', await page.locator('li:has-text("Telemetry Table")').click();
parent: table.uuid
}); await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
// Wait for Save Banner to appear
page.waitForSelector(bannerMessage)
]);
// Save (exit edit mode)
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(3).click();
await page.locator('text=Save and Finish Editing').click();
// Click create button
await page.locator(createButton).click();
// add Sine Wave Generator with defaults
await page.locator('li:has-text("Sine Wave Generator")').click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
// Wait for Save Banner to appear
page.waitForSelector(bannerMessage)
]);
// focus the Telemetry Table // focus the Telemetry Table
page.goto(table.url); await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Telemetry Table').first().click()
]);
// Click pause button // Click pause button
const pauseButton = page.locator('button.c-button.icon-pause'); const pauseButton = page.locator('button.c-button.icon-pause');

View File

@@ -21,7 +21,6 @@
*****************************************************************************/ *****************************************************************************/
const { test, expect } = require('../../../../baseFixtures'); const { test, expect } = require('../../../../baseFixtures');
const { setFixedTimeMode, setRealTimeMode, setStartOffset, setEndOffset } = require('../../../../appActions');
test.describe('Time conductor operations', () => { test.describe('Time conductor operations', () => {
test('validate start time does not exceeds end time', async ({ page }) => { test('validate start time does not exceeds end time', async ({ page }) => {
@@ -143,8 +142,93 @@ test.describe('Time conductor input fields real-time mode', () => {
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01'); await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01');
// Verify url parameters persist after mode switch // Verify url parameters persist after mode switch
await page.waitForNavigation({ waitUntil: 'networkidle' }); await page.waitForNavigation();
expect(page.url()).toContain(`startDelta=${startDelta}`); expect(page.url()).toContain(`startDelta=${startDelta}`);
expect(page.url()).toContain(`endDelta=${endDelta}`); expect(page.url()).toContain(`endDelta=${endDelta}`);
}); });
}); });
/**
* @typedef {Object} OffsetValues
* @property {string | undefined} hours
* @property {string | undefined} mins
* @property {string | undefined} secs
*/
/**
* Set the values (hours, mins, secs) for the start time offset when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
*/
async function setStartOffset(page, offset) {
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
await setTimeConductorOffset(page, offset, startOffsetButton);
}
/**
* Set the values (hours, mins, secs) for the end time offset when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
*/
async function setEndOffset(page, offset) {
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
await setTimeConductorOffset(page, offset, endOffsetButton);
}
/**
* Set the time conductor to fixed timespan mode
* @param {import('@playwright/test').Page} page
*/
async function setFixedTimeMode(page) {
await setTimeConductorMode(page, true);
}
/**
* Set the time conductor to realtime mode
* @param {import('@playwright/test').Page} page
*/
async function setRealTimeMode(page) {
await setTimeConductorMode(page, false);
}
/**
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
* @param {import('@playwright/test').Locator} offsetButton
*/
async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) {
await offsetButton.click();
if (hours) {
await page.fill('.pr-time-controls__hrs', hours);
}
if (mins) {
await page.fill('.pr-time-controls__mins', mins);
}
if (secs) {
await page.fill('.pr-time-controls__secs', secs);
}
// Click the check button
await page.locator('.icon-check').click();
}
/**
* Set the time conductor mode to either fixed timespan or realtime mode.
* @param {import('@playwright/test').Page} page
* @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
*/
async function setTimeConductorMode(page, isFixedTimespan = true) {
// Click 'mode' button
await page.locator('.c-mode-button').click();
// Switch time conductor mode
if (isFixedTimespan) {
await page.locator('data-testid=conductor-modeOption-fixed').click();
} else {
await page.locator('data-testid=conductor-modeOption-realtime').click();
}
}

View File

@@ -21,14 +21,14 @@
*****************************************************************************/ *****************************************************************************/
const { test, expect } = require('../../../../pluginFixtures'); const { test, expect } = require('../../../../pluginFixtures');
const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions'); const { openObjectTreeContextMenu } = require('../../../../appActions');
const options = {
type: 'Timer'
};
test.describe('Timer', () => { test.describe('Timer', () => {
test.beforeEach(async ({ page }) => { test.use({ objectCreateOptions: options });
await page.goto('./', { waitUntil: 'networkidle' });
await createDomainObjectWithDefaults(page, { type: 'timer' });
});
test('Can perform actions on the Timer', async ({ page, openmctConfig }) => { test('Can perform actions on the Timer', async ({ page, openmctConfig }) => {
test.info().annotations.push({ test.info().annotations.push({
type: 'issue', type: 'issue',

View File

@@ -107,9 +107,6 @@ test.describe("Search Tests @unstable", () => {
// Verify that no results are found // Verify that no results are found
expect(await searchResults.count()).toBe(0); expect(await searchResults.count()).toBe(0);
// Verify proper message appears
await expect(page.locator('text=No matching results.')).toBeVisible();
}); });
test('Validate single object in search result', async ({ page }) => { test('Validate single object in search result', async ({ page }) => {

View File

@@ -32,31 +32,39 @@ Note: Larger testsuite sizes are OK due to the setup time associated with these
*/ */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const { test, expect } = require('../../pluginFixtures'); const { test, expect } = require('../../baseFixtures.js');
const { createDomainObjectWithDefaults } = require('../../appActions');
const percySnapshot = require('@percy/playwright'); const percySnapshot = require('@percy/playwright');
const path = require('path'); const path = require('path');
const VISUAL_GRACE_PERIOD = 5 * 1000; //Lets the application "simmer" before the snapshot is taken
const CUSTOM_NAME = 'CUSTOM_NAME'; const CUSTOM_NAME = 'CUSTOM_NAME';
test.describe('Visual - addInit', () => { test.describe('Visual - addInit', () => {
test.use({ test.use({
clockOptions: { clockOptions: {
now: 0, //Set browser clock to UNIX Epoch shouldAdvanceTime: true
shouldAdvanceTime: false //Don't advance the clock
} }
}); });
test('Restricted Notebook is visually correct @addInit @unstable', async ({ page, theme }) => { test('Restricted Notebook is visually correct @addInit', async ({ page }) => {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, '../../helper', './addInitRestrictedNotebook.js') }); await page.addInitScript({ path: path.join(__dirname, '../../helper', './addInitRestrictedNotebook.js') });
//Go to baseURL //Go to baseURL
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' }); await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
//Click the Create button
await createDomainObjectWithDefaults(page, { type: CUSTOM_NAME }); await page.click('button:has-text("Create")');
// Click text=CUSTOM_NAME
await page.click(`text=${CUSTOM_NAME}`);
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('text=OK')
]);
// Take a snapshot of the newly created CUSTOM_NAME notebook // Take a snapshot of the newly created CUSTOM_NAME notebook
await percySnapshot(page, `Restricted Notebook with CUSTOM_NAME (theme: '${theme}')`); await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'Restricted Notebook with CUSTOM_NAME');
}); });
}); });

View File

@@ -25,21 +25,27 @@ Collection of Visual Tests set to run in a default context. The tests within thi
are only meant to run against openmct's app.js started by `npm run start` within the are only meant to run against openmct's app.js started by `npm run start` within the
`./e2e/playwright-visual.config.js` file. `./e2e/playwright-visual.config.js` file.
These should only use functional expect statements to verify assumptions about the state
in a test and not for functional verification of correctness. Visual tests are not supposed
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
*/ */
const { test, expect } = require('../../pluginFixtures'); const { test, expect } = require('../../baseFixtures.js');
const percySnapshot = require('@percy/playwright'); const percySnapshot = require('@percy/playwright');
test.describe('Visual - Controlled Clock @localStorage', () => { test.describe('Visual - Controlled Clock', () => {
test.use({ test.use({
storageState: './e2e/test-data/VisualTestData_storage.json', storageState: './e2e/test-data/VisualTestData_storage.json',
clockOptions: { clockOptions: {
now: 0, //Set browser clock to UNIX Epoch now: 0, //Set browser clock to UNIX Epoch
shouldAdvanceTime: false //Don't advance the clock shouldAdvanceTime: false, //Don't advance the clock
toFake: ["setTimeout", "nextTick"]
} }
}); });
test('Overlay Plot Loading Indicator @localStorage', async ({ page, theme }) => { test('Overlay Plot Loading Indicator @localstorage', async ({ page }) => {
// Go to baseURL // Go to baseURL
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' }); await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
@@ -51,6 +57,6 @@ test.describe('Visual - Controlled Clock @localStorage', () => {
await page.locator('canvas >> nth=1').hover({trial: true}); await page.locator('canvas >> nth=1').hover({trial: true});
//Take snapshot of Sine Wave Generator within Overlay Plot //Take snapshot of Sine Wave Generator within Overlay Plot
await percySnapshot(page, `SineWaveInOverlayPlot (theme: '${theme}')`); await percySnapshot(page, 'SineWaveInOverlayPlot');
}); });
}); });

View File

@@ -32,62 +32,85 @@ to "fail" on assertions. Instead, they should be used to detect changes between
Note: Larger testsuite sizes are OK due to the setup time associated with these tests. Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
*/ */
const { test, expect } = require('../../pluginFixtures'); const { test, expect } = require('../../baseFixtures.js');
const percySnapshot = require('@percy/playwright'); const percySnapshot = require('@percy/playwright');
const { createDomainObjectWithDefaults } = require('../../appActions'); const VISUAL_GRACE_PERIOD = 5 * 1000; //Lets the application "simmer" before the snapshot is taken
test.describe('Visual - Default', () => { test.describe('Visual - Default', () => {
test.beforeEach(async ({ page }) => {
//Go to baseURL and Hide Tree
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
});
test.use({ test.use({
clockOptions: { clockOptions: {
now: 0, //Set browser clock to UNIX Epoch now: 0,
shouldAdvanceTime: false //Don't advance the clock shouldAdvanceTime: true
} }
}); });
test('Visual - Root and About', async ({ page, theme }) => { test('Visual - Root and About', async ({ page }) => {
// Go to baseURL
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
// Verify that Create button is actionable // Verify that Create button is actionable
await expect(page.locator('button:has-text("Create")')).toBeEnabled(); await expect(page.locator('button:has-text("Create")')).toBeEnabled();
// Take a snapshot of the Dashboard // Take a snapshot of the Dashboard
await percySnapshot(page, `Root (theme: '${theme}')`); await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'Root');
// Click About button // Click About button
await page.click('.l-shell__app-logo'); await page.click('.l-shell__app-logo');
// Modify the Build information in 'about' to be consistent run-over-run // Modify the Build information in 'about' to be consistent run-over-run
const versionInformationLocator = page.locator('ul.t-info.l-info.s-info').first(); const versionInformationLocator = page.locator('ul.t-info.l-info.s-info');
await expect(versionInformationLocator).toBeEnabled(); await expect(versionInformationLocator).toBeEnabled();
await versionInformationLocator.evaluate(node => node.innerHTML = '<li>Version: visual-snapshot</li> <li>Build Date: Mon Nov 15 2021 08:07:51 GMT-0800 (Pacific Standard Time)</li> <li>Revision: 93049cdbc6c047697ca204893db9603b864b8c9f</li> <li>Branch: master</li>'); await versionInformationLocator.evaluate(node => node.innerHTML = '<li>Version: visual-snapshot</li> <li>Build Date: Mon Nov 15 2021 08:07:51 GMT-0800 (Pacific Standard Time)</li> <li>Revision: 93049cdbc6c047697ca204893db9603b864b8c9f</li> <li>Branch: master</li>');
// Take a snapshot of the About modal // Take a snapshot of the About modal
await percySnapshot(page, `About (theme: '${theme}')`); await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'About');
}); });
test.fixme('Visual - Default Condition Set', async ({ page, theme }) => { test('Visual - Default Condition Set', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
await createDomainObjectWithDefaults(page, { type: 'Condition Set' }); //Click the Create button
await page.click('button:has-text("Create")');
// Click text=Condition Set
await page.click('text=Condition Set');
// Click text=OK
await page.click('text=OK');
// Take a snapshot of the newly created Condition Set object // Take a snapshot of the newly created Condition Set object
await percySnapshot(page, `Default Condition Set (theme: '${theme}')`); await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'Default Condition Set');
}); });
test.fixme('Visual - Default Condition Widget', async ({ page, theme }) => { test.fixme('Visual - Default Condition Widget', async ({ page }) => {
test.info().annotations.push({ test.info().annotations.push({
type: 'issue', type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5349' description: 'https://github.com/nasa/openmct/issues/5349'
}); });
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
await createDomainObjectWithDefaults(page, { type: 'Condition Widget' }); //Click the Create button
await page.click('button:has-text("Create")');
// Click text=Condition Widget
await page.click('text=Condition Widget');
// Click text=OK
await page.click('text=OK');
// Take a snapshot of the newly created Condition Widget object // Take a snapshot of the newly created Condition Widget object
await percySnapshot(page, `Default Condition Widget (theme: '${theme}')`); await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'Default Condition Widget');
}); });
test('Visual - Time Conductor start time is less than end time', async ({ page, theme }) => { test('Visual - Time Conductor start time is less than end time', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
const year = new Date().getFullYear(); const year = new Date().getFullYear();
let startDate = 'xxxx-01-01 01:00:00.000Z'; let startDate = 'xxxx-01-01 01:00:00.000Z';
@@ -100,14 +123,16 @@ test.describe('Visual - Default', () => {
await page.locator('input[type="text"]').first().fill(startDate.toString()); await page.locator('input[type="text"]').first().fill(startDate.toString());
// verify no error msg // verify no error msg
await percySnapshot(page, `Default Time conductor (theme: '${theme}')`); await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'Default Time conductor');
startDate = (year + 1) + startDate.substring(4); startDate = (year + 1) + startDate.substring(4);
await page.locator('input[type="text"]').first().fill(startDate.toString()); await page.locator('input[type="text"]').first().fill(startDate.toString());
await page.locator('input[type="text"]').nth(1).click(); await page.locator('input[type="text"]').nth(1).click();
// verify error msg for start time (unable to capture snapshot of popup) // verify error msg for start time (unable to capture snapshot of popup)
await percySnapshot(page, `Start time error (theme: '${theme}')`); await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'Start time error');
startDate = (year - 1) + startDate.substring(4); startDate = (year - 1) + startDate.substring(4);
await page.locator('input[type="text"]').first().fill(startDate.toString()); await page.locator('input[type="text"]').first().fill(startDate.toString());
@@ -118,51 +143,79 @@ test.describe('Visual - Default', () => {
await page.locator('input[type="text"]').first().click(); await page.locator('input[type="text"]').first().click();
// verify error msg for end time (unable to capture snapshot of popup) // verify error msg for end time (unable to capture snapshot of popup)
await percySnapshot(page, `End time error (theme: '${theme}')`); await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'End time error');
}); });
test('Visual - Sine Wave Generator Form', async ({ page, theme }) => { test('Visual - Sine Wave Generator Form', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button //Click the Create button
await page.click('button:has-text("Create")'); await page.click('button:has-text("Create")');
// Click text=Sine Wave Generator // Click text=Sine Wave Generator
await page.click('text=Sine Wave Generator'); await page.click('text=Sine Wave Generator');
await percySnapshot(page, `Default Sine Wave Generator Form (theme: '${theme}')`); await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'Default Sine Wave Generator Form');
await page.locator('.field.control.l-input-sm input').first().click(); await page.locator('.field.control.l-input-sm input').first().click();
await page.locator('.field.control.l-input-sm input').first().fill(''); await page.locator('.field.control.l-input-sm input').first().fill('');
// Validate red x mark // Validate red x mark
await percySnapshot(page, `removed amplitude property value (theme: '${theme}')`); await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'removed amplitude property value');
}); });
test.fixme('Visual - Save Successful Banner', async ({ page, theme }) => { test('Visual - Save Successful Banner', async ({ page }) => {
await createDomainObjectWithDefaults(page, { type: 'Timer' }); //Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
//NOTE Something other than example imagery
await page.click('text=Timer');
// Click text=OK
await page.click('text=OK');
await page.locator('.c-message-banner__message').hover({ trial: true }); await page.locator('.c-message-banner__message').hover({ trial: true });
await percySnapshot(page, `Banner message shown (theme: '${theme}')`); await percySnapshot(page, 'Banner message shown');
//Wait until Save Banner is gone //Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await percySnapshot(page, `Banner message gone (theme: '${theme}')`); await percySnapshot(page, 'Banner message gone');
}); });
test('Visual - Display Layout Icon is correct', async ({ page, theme }) => { test('Visual - Display Layout Icon is correct', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button //Click the Create button
await page.click('button:has-text("Create")'); await page.click('button:has-text("Create")');
//Hover on Display Layout option. //Hover on Display Layout option.
await page.locator('text=Display Layout').hover(); await page.locator('text=Display Layout').hover();
await percySnapshot(page, `Display Layout Create Menu (theme: '${theme}')`); await percySnapshot(page, 'Display Layout Create Menu');
}); });
test.fixme('Visual - Default Gauge is correct', async ({ page, theme }) => { test('Visual - Default Gauge is correct', async ({ page }) => {
await createDomainObjectWithDefaults(page, { type: 'Gauge' });
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
await page.click('text=Gauge');
await page.click('text=OK');
// Take a snapshot of the newly created Gauge object // Take a snapshot of the newly created Gauge object
await percySnapshot(page, `Default Gauge (theme: '${theme}')`); await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'Default Gauge');
}); });
}); });

View File

@@ -24,61 +24,81 @@
This test suite is dedicated to tests which verify search functionality. This test suite is dedicated to tests which verify search functionality.
*/ */
const { test, expect } = require('../../pluginFixtures'); const { test, expect } = require('../../baseFixtures.js');
const { createDomainObjectWithDefaults } = require('../../appActions');
const percySnapshot = require('@percy/playwright'); const percySnapshot = require('@percy/playwright');
/**
* Creates a notebook object and adds an entry.
* @param {import('@playwright/test').Page} page
*/
async function createClockAndDisplayLayout(page) {
//Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Notebook")
await page.locator('li:has-text("Clock")').click();
// Click button:has-text("OK")
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click()
]);
// Click a:has-text("My Items")
await Promise.all([
page.waitForNavigation(),
page.locator('a:has-text("My Items") >> 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()
]);
}
test.describe('Grand Search', () => { test.describe('Grand Search', () => {
test.beforeEach(async ({ page, theme }) => { test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page }) => {
//Go to baseURL and Hide Tree await createClockAndDisplayLayout(page);
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
});
test.use({
clockOptions: {
now: 0, //Set browser clock to UNIX Epoch
shouldAdvanceTime: false //Don't advance the clock
}
});
//This needs to be rewritten to use a non clock or non display layout object
test('Can search for objects, and subsequent search dropdown behaves properly @unstable', async ({ page, theme }) => {
// await createDomainObjectWithDefaults(page, 'Display Layout');
// await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
// await page.locator('text=Save and Finish Editing').click();
const folder1 = 'Folder1';
await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: folder1
});
// Click [aria-label="OpenMCT Search"] input[type="search"] // Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill [aria-label="OpenMCT Search"] input[type="search"] // Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill(folder1); await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl');
await expect(page.locator('[aria-label="Search Result"]')).toContainText(folder1); await expect(page.locator('[aria-label="Search Result"]')).toContainText('Clock');
await percySnapshot(page, 'Searching for Folder Object'); await percySnapshot(page, 'Searching for Clocks');
// Click text=Elements >> nth=0
await page.locator('text=Elements').first().click();
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
// Click [aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock
await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click(); await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click();
await percySnapshot(page, 'Preview for clock should display when editing enabled and search item clicked'); await percySnapshot(page, 'Preview for clock should display when editing enabled and search item clicked');
// Click [aria-label="Close"]
await page.locator('[aria-label="Close"]').click(); await page.locator('[aria-label="Close"]').click();
await percySnapshot(page, 'Search should still be showing after preview closed'); await percySnapshot(page, 'Search should still be showing after preview closed');
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
// Click text=Save and Finish Editing
await page.locator('text=Save and Finish Editing').click(); await page.locator('text=Save and Finish Editing').click();
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
// Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"]
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl'); await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
// Click text=Unnamed Clock
await Promise.all([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
page.locator('text=Unnamed Clock').click() page.locator('text=Unnamed Clock').click()
]); ]);
await percySnapshot(page, `Clicking on search results should navigate to them if not editing (theme: '${theme}')`); await percySnapshot(page, 'Clicking on search results should navigate to them if not editing');
}); });
}); });

View File

@@ -1,11 +1,11 @@
{ {
"name": "openmct", "name": "openmct",
"version": "2.0.7", "version": "2.1.0-SNAPSHOT",
"description": "The Open MCT core platform", "description": "The Open MCT core platform",
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "7.18.9", "@babel/eslint-parser": "7.18.2",
"@braintree/sanitize-url": "6.0.0", "@braintree/sanitize-url": "6.0.0",
"@percy/cli": "1.7.2", "@percy/cli": "1.2.1",
"@percy/playwright": "1.0.4", "@percy/playwright": "1.0.4",
"@playwright/test": "1.23.0", "@playwright/test": "1.23.0",
"@types/eventemitter3": "^1.0.0", "@types/eventemitter3": "^1.0.0",
@@ -26,7 +26,7 @@
"eslint": "8.18.0", "eslint": "8.18.0",
"eslint-plugin-compat": "4.0.2", "eslint-plugin-compat": "4.0.2",
"eslint-plugin-playwright": "0.10.0", "eslint-plugin-playwright": "0.10.0",
"eslint-plugin-vue": "9.3.0", "eslint-plugin-vue": "9.1.1",
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0", "eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
"eventemitter3": "1.2.0", "eventemitter3": "1.2.0",
"express": "4.13.1", "express": "4.13.1",
@@ -34,7 +34,7 @@
"git-rev-sync": "3.0.2", "git-rev-sync": "3.0.2",
"html2canvas": "1.4.1", "html2canvas": "1.4.1",
"imports-loader": "0.8.0", "imports-loader": "0.8.0",
"jasmine-core": "4.3.0", "jasmine-core": "4.2.0",
"jsdoc": "3.6.11", "jsdoc": "3.6.11",
"karma": "6.3.20", "karma": "6.3.20",
"karma-chrome-launcher": "3.1.1", "karma-chrome-launcher": "3.1.1",
@@ -94,7 +94,7 @@
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable", "test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome", "test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots", "test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots",
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js --grep-invert @unstable", "test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js",
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js", "test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js",
"test:perf": "npx playwright test --config=e2e/playwright-performance.config.js", "test:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
"test:watch": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run", "test:watch": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",

View File

@@ -40,8 +40,6 @@ const ANNOTATION_TYPES = Object.freeze({
PLOT_SPATIAL: 'PLOT_SPATIAL' PLOT_SPATIAL: 'PLOT_SPATIAL'
}); });
const ANNOTATION_TYPE = 'annotation';
/** /**
* @typedef {Object} Tag * @typedef {Object} Tag
* @property {String} key a unique identifier for the tag * @property {String} key a unique identifier for the tag
@@ -56,7 +54,7 @@ export default class AnnotationAPI extends EventEmitter {
this.ANNOTATION_TYPES = ANNOTATION_TYPES; this.ANNOTATION_TYPES = ANNOTATION_TYPES;
this.openmct.types.addType(ANNOTATION_TYPE, { this.openmct.types.addType('annotation', {
name: 'Annotation', name: 'Annotation',
description: 'A user created note or comment about time ranges, pixel space, and geospatial features.', description: 'A user created note or comment about time ranges, pixel space, and geospatial features.',
creatable: false, creatable: false,
@@ -138,10 +136,6 @@ export default class AnnotationAPI extends EventEmitter {
this.availableTags[tagKey] = tagsDefinition; this.availableTags[tagKey] = tagsDefinition;
} }
isAnnotation(domainObject) {
return domainObject && (domainObject.type === ANNOTATION_TYPE);
}
getAvailableTags() { getAvailableTags() {
if (this.availableTags) { if (this.availableTags) {
const rearrangedToArray = Object.keys(this.availableTags).map(tagKey => { const rearrangedToArray = Object.keys(this.availableTags).map(tagKey => {
@@ -277,10 +271,7 @@ export default class AnnotationAPI extends EventEmitter {
const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat(); const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat();
const appliedTagSearchResults = this.#addTagMetaInformationToResults(searchResults, matchingTagKeys); const appliedTagSearchResults = this.#addTagMetaInformationToResults(searchResults, matchingTagKeys);
const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults); const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults);
const resultsWithValidPath = appliedTargetsModels.filter(result => {
return this.openmct.objects.isReachable(result.targetModels?.[0]?.originalPath);
});
return resultsWithValidPath; return appliedTargetsModels;
} }
} }

View File

@@ -27,26 +27,15 @@ describe("The Annotation API", () => {
let openmct; let openmct;
let mockObjectProvider; let mockObjectProvider;
let mockDomainObject; let mockDomainObject;
let mockFolderObject;
let mockAnnotationObject; let mockAnnotationObject;
beforeEach((done) => { beforeEach((done) => {
openmct = createOpenMct(); openmct = createOpenMct();
openmct.install(new ExampleTagsPlugin()); openmct.install(new ExampleTagsPlugin());
const availableTags = openmct.annotation.getAvailableTags(); const availableTags = openmct.annotation.getAvailableTags();
mockFolderObject = {
type: 'root',
name: 'folderFoo',
location: '',
identifier: {
key: 'someParent',
namespace: 'fooNameSpace'
}
};
mockDomainObject = { mockDomainObject = {
type: 'notebook', type: 'notebook',
name: 'fooRabbitNotebook', name: 'fooRabbitNotebook',
location: 'fooNameSpace:someParent',
identifier: { identifier: {
key: 'some-object', key: 'some-object',
namespace: 'fooNameSpace' namespace: 'fooNameSpace'
@@ -79,8 +68,6 @@ describe("The Annotation API", () => {
return mockDomainObject; return mockDomainObject;
} else if (identifier.key === mockAnnotationObject.identifier.key) { } else if (identifier.key === mockAnnotationObject.identifier.key) {
return mockAnnotationObject; return mockAnnotationObject;
} else if (identifier.key === mockFolderObject.identifier.key) {
return mockFolderObject;
} else { } else {
return null; return null;
} }
@@ -163,7 +150,6 @@ describe("The Annotation API", () => {
// use local worker // use local worker
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker; sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
openmct.objects.inMemorySearchProvider.worker = null; openmct.objects.inMemorySearchProvider.worker = null;
await openmct.objects.inMemorySearchProvider.index(mockFolderObject);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject); await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject); await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject);
}); });

View File

@@ -34,11 +34,11 @@ import InMemorySearchProvider from './InMemorySearchProvider';
* Uniquely identifies a domain object. * Uniquely identifies a domain object.
* *
* @typedef Identifier * @typedef Identifier
* @memberof module:openmct.ObjectAPI~
* @property {string} namespace the namespace to/from which this domain * @property {string} namespace the namespace to/from which this domain
* object should be loaded/stored. * object should be loaded/stored.
* @property {string} key a unique identifier for the domain object * @property {string} key a unique identifier for the domain object
* within that namespace * within that namespace
* @memberof module:openmct.ObjectAPI~
*/ */
/** /**
@@ -88,7 +88,7 @@ export default class ObjectAPI {
this.cache = {}; this.cache = {};
this.interceptorRegistry = new InterceptorRegistry(); this.interceptorRegistry = new InterceptorRegistry();
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan', 'annotation']; this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan'];
this.errors = { this.errors = {
Conflict: ConflictError Conflict: ConflictError
@@ -230,7 +230,6 @@ export default class ObjectAPI {
return result; return result;
}).catch((result) => { }).catch((result) => {
console.warn(`Failed to retrieve ${keystring}:`, result); console.warn(`Failed to retrieve ${keystring}:`, result);
this.openmct.notifications.error(`Failed to retrieve object ${keystring}`);
delete this.cache[keystring]; delete this.cache[keystring];
@@ -388,13 +387,7 @@ export default class ObjectAPI {
} }
} }
return result.catch((error) => { return result;
if (error instanceof this.errors.Conflict) {
this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`);
}
throw error;
});
} }
/** /**
@@ -615,60 +608,27 @@ export default class ObjectAPI {
* @param {module:openmct.ObjectAPI~Identifier[]} identifiers * @param {module:openmct.ObjectAPI~Identifier[]} identifiers
*/ */
areIdsEqual(...identifiers) { areIdsEqual(...identifiers) {
const firstIdentifier = utils.parseKeyString(identifiers[0]);
return identifiers.map(utils.parseKeyString) return identifiers.map(utils.parseKeyString)
.every(identifier => { .every(identifier => {
return identifier === firstIdentifier return identifier === identifiers[0]
|| (identifier.namespace === firstIdentifier.namespace || (identifier.namespace === identifiers[0].namespace
&& identifier.key === firstIdentifier.key); && identifier.key === identifiers[0].key);
}); });
} }
/** getOriginalPath(identifier, path = []) {
* Given an original path check if the path is reachable via root return this.get(identifier).then((domainObject) => {
* @param {Array<Object>} originalPath an array of path objects to check path.push(domainObject);
* @returns {boolean} whether the domain object is reachable let location = domainObject.location;
*/
isReachable(originalPath) {
if (originalPath && originalPath.length) {
return (originalPath[originalPath.length - 1].type === 'root');
}
return false; if (location) {
} return this.getOriginalPath(utils.parseKeyString(location), path);
} else {
#pathContainsDomainObject(keyStringToCheck, path) { return path;
if (!keyStringToCheck) { }
return false;
}
return path.some(pathElement => {
const identifierToCheck = utils.parseKeyString(keyStringToCheck);
return this.areIdsEqual(identifierToCheck, pathElement.identifier);
}); });
} }
/**
* Given an identifier, constructs the original path by walking up its parents
* @param {module:openmct.ObjectAPI~Identifier} identifier
* @param {Array<module:openmct.DomainObject>} path an array of path objects
* @returns {Promise<Array<module:openmct.DomainObject>>} a promise containing an array of domain objects
*/
async getOriginalPath(identifier, path = []) {
const domainObject = await this.get(identifier);
path.push(domainObject);
const { location } = domainObject;
if (location && (!this.#pathContainsDomainObject(location, path))) {
// if we have a location, and we don't already have this in our constructed path,
// then keep walking up the path
return this.getOriginalPath(utils.parseKeyString(location), path);
} else {
return path;
}
}
isObjectPathToALink(domainObject, objectPath) { isObjectPathToALink(domainObject, objectPath) {
return objectPath !== undefined return objectPath !== undefined
&& objectPath.length > 1 && objectPath.length > 1

View File

@@ -7,7 +7,6 @@ describe("The Object API", () => {
let openmct = {}; let openmct = {};
let mockDomainObject; let mockDomainObject;
const TEST_NAMESPACE = "test-namespace"; const TEST_NAMESPACE = "test-namespace";
const TEST_KEY = "test-key";
const FIFTEEN_MINUTES = 15 * 60 * 1000; const FIFTEEN_MINUTES = 15 * 60 * 1000;
beforeEach((done) => { beforeEach((done) => {
@@ -23,7 +22,7 @@ describe("The Object API", () => {
mockDomainObject = { mockDomainObject = {
identifier: { identifier: {
namespace: TEST_NAMESPACE, namespace: TEST_NAMESPACE,
key: TEST_KEY key: "test-key"
}, },
name: "test object", name: "test object",
type: "test-type" type: "test-type"
@@ -85,31 +84,6 @@ describe("The Object API", () => {
expect(mockProvider.create).not.toHaveBeenCalled(); expect(mockProvider.create).not.toHaveBeenCalled();
expect(mockProvider.update).not.toHaveBeenCalled(); expect(mockProvider.update).not.toHaveBeenCalled();
}); });
describe("Shows a notification on persistence conflict", () => {
beforeEach(() => {
openmct.notifications.error = jasmine.createSpy('error');
});
it("on create", () => {
mockProvider.create.and.returnValue(Promise.reject(new openmct.objects.errors.Conflict("Test Conflict error")));
return objectAPI.save(mockDomainObject).catch(() => {
expect(openmct.notifications.error).toHaveBeenCalledWith(`Conflict detected while saving ${TEST_NAMESPACE}:${TEST_KEY}`);
});
});
it("on update", () => {
mockProvider.update.and.returnValue(Promise.reject(new openmct.objects.errors.Conflict("Test Conflict error")));
mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;
mockDomainObject.modified = Date.now();
return objectAPI.save(mockDomainObject).catch(() => {
expect(openmct.notifications.error).toHaveBeenCalledWith(`Conflict detected while saving ${TEST_NAMESPACE}:${TEST_KEY}`);
});
});
});
}); });
}); });
@@ -164,33 +138,21 @@ describe("The Object API", () => {
}); });
it("Caches multiple requests for the same object", () => { it("Caches multiple requests for the same object", () => {
const promises = [];
expect(mockProvider.get.calls.count()).toBe(0); expect(mockProvider.get.calls.count()).toBe(0);
promises.push(objectAPI.get(mockDomainObject.identifier)); objectAPI.get(mockDomainObject.identifier);
expect(mockProvider.get.calls.count()).toBe(1); expect(mockProvider.get.calls.count()).toBe(1);
promises.push(objectAPI.get(mockDomainObject.identifier)); objectAPI.get(mockDomainObject.identifier);
expect(mockProvider.get.calls.count()).toBe(1); expect(mockProvider.get.calls.count()).toBe(1);
return Promise.all(promises);
}); });
it("applies any applicable interceptors", () => { it("applies any applicable interceptors", () => {
expect(mockDomainObject.changed).toBeUndefined(); expect(mockDomainObject.changed).toBeUndefined();
objectAPI.get(mockDomainObject.identifier).then((object) => {
return objectAPI.get(mockDomainObject.identifier).then((object) => {
expect(object.changed).toBeTrue(); expect(object.changed).toBeTrue();
expect(object.alsoChanged).toBeTrue(); expect(object.alsoChanged).toBeTrue();
expect(object.shouldNotBeChanged).toBeUndefined(); expect(object.shouldNotBeChanged).toBeUndefined();
}); });
}); });
it("displays a notification in the event of an error", () => {
mockProvider.get.and.returnValue(Promise.reject());
return objectAPI.get(mockDomainObject.identifier).catch(() => {
expect(openmct.notifications.error).toHaveBeenCalledWith(`Failed to retrieve object ${TEST_NAMESPACE}:${TEST_KEY}`);
});
});
}); });
}); });
@@ -206,7 +168,7 @@ describe("The Object API", () => {
testObject = { testObject = {
identifier: { identifier: {
namespace: TEST_NAMESPACE, namespace: TEST_NAMESPACE,
key: TEST_KEY key: 'test-key'
}, },
name: 'test object', name: 'test object',
type: 'notebook', type: 'notebook',
@@ -233,8 +195,6 @@ describe("The Object API", () => {
"observeObjectChanges" "observeObjectChanges"
]); ]);
mockProvider.get.and.returnValue(Promise.resolve(testObject)); mockProvider.get.and.returnValue(Promise.resolve(testObject));
mockProvider.create.and.returnValue(Promise.resolve(true));
mockProvider.update.and.returnValue(Promise.resolve(true));
mockProvider.observeObjectChanges.and.callFake(() => { mockProvider.observeObjectChanges.and.callFake(() => {
callbacks[0](updatedTestObject); callbacks[0](updatedTestObject);
callbacks.splice(0, 1); callbacks.splice(0, 1);
@@ -377,73 +337,6 @@ describe("The Object API", () => {
}); });
}); });
describe("getOriginalPath", () => {
let mockGrandParentObject;
let mockParentObject;
let mockChildObject;
beforeEach(() => {
const mockObjectProvider = jasmine.createSpyObj("mock object provider", [
"create",
"update",
"get"
]);
mockGrandParentObject = {
type: 'folder',
name: 'Grand Parent Folder',
location: 'fooNameSpace:child',
identifier: {
key: 'grandParent',
namespace: 'fooNameSpace'
}
};
mockParentObject = {
type: 'folder',
name: 'Parent Folder',
location: 'fooNameSpace:grandParent',
identifier: {
key: 'parent',
namespace: 'fooNameSpace'
}
};
mockChildObject = {
type: 'folder',
name: 'Child Folder',
location: 'fooNameSpace:parent',
identifier: {
key: 'child',
namespace: 'fooNameSpace'
}
};
// eslint-disable-next-line require-await
mockObjectProvider.get = async (identifier) => {
if (identifier.key === mockGrandParentObject.identifier.key) {
return mockGrandParentObject;
} else if (identifier.key === mockParentObject.identifier.key) {
return mockParentObject;
} else if (identifier.key === mockChildObject.identifier.key) {
return mockChildObject;
} else {
return null;
}
};
openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
mockObjectProvider.create.and.returnValue(Promise.resolve(true));
mockObjectProvider.update.and.returnValue(Promise.resolve(true));
openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
});
it('can construct paths even with cycles', async () => {
const objectPath = await objectAPI.getOriginalPath(mockChildObject.identifier);
expect(objectPath.length).toEqual(3);
});
});
describe("transactions", () => { describe("transactions", () => {
beforeEach(() => { beforeEach(() => {
spyOn(openmct.editor, 'isEditing').and.returnValue(true); spyOn(openmct.editor, 'isEditing').and.returnValue(true);

View File

@@ -91,10 +91,6 @@ define([
* @returns keyString * @returns keyString
*/ */
function makeKeyString(identifier) { function makeKeyString(identifier) {
if (!identifier) {
throw new Error("Cannot make key string from null identifier");
}
if (isKeyString(identifier)) { if (isKeyString(identifier)) {
return identifier; return identifier;
} }

View File

@@ -83,11 +83,9 @@ class UserAPI extends EventEmitter {
* @throws Will throw an error if no user provider is set * @throws Will throw an error if no user provider is set
*/ */
getCurrentUser() { getCurrentUser() {
if (!this.hasProvider()) { this.noProviderCheck();
return Promise.resolve(undefined);
} else { return this._provider.getCurrentUser();
return this._provider.getCurrentUser();
}
} }
/** /**

View File

@@ -51,11 +51,7 @@ export default class TelemetryCriterion extends EventEmitter {
} }
initialize() { initialize() {
this.telemetryObjectIdAsString = ""; this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
if (![undefined, null, ""].includes(this.telemetryDomainObjectDefinition?.telemetry)) {
this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
}
this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects); this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects);
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) { if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData(); this.subscribeForStaleData();

View File

@@ -1,68 +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.
-->
<template>
<div
class="c-imagery__thumb c-thumb"
:class="{
'active': active,
'selected': selected,
'real-time': realTime
}"
:title="image.formattedTime"
>
<a
href=""
:download="image.imageDownloadName"
@click.prevent
>
<img
class="c-thumb__image"
:src="image.url"
>
</a>
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
</div>
</template>
<script>
export default {
props: {
image: {
type: Object,
required: true
},
active: {
type: Boolean,
required: true
},
selected: {
type: Boolean,
required: true
},
realTime: {
type: Boolean,
required: true
}
}
};
</script>

View File

@@ -166,15 +166,26 @@
class="c-imagery__thumbs-scroll-area" class="c-imagery__thumbs-scroll-area"
@scroll="handleScroll" @scroll="handleScroll"
> >
<ImageThumbnail <div
v-for="(image, index) in imageHistory" v-for="(image, index) in imageHistory"
:key="image.url + image.time" :key="image.url + image.time"
:image="image" class="c-imagery__thumb c-thumb"
:active="focusedImageIndex === index" :class="{ selected: focusedImageIndex === index && isPaused }"
:selected="focusedImageIndex === index && isPaused" :title="image.formattedTime"
:real-time="!isFixed" @click="thumbnailClicked(index)"
@click.native="thumbnailClicked(index)" >
/> <a
href=""
:download="image.imageDownloadName"
@click.prevent
>
<img
class="c-thumb__image"
:src="image.url"
>
</a>
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
</div>
</div> </div>
<button <button
@@ -194,7 +205,6 @@ import moment from 'moment';
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry'; import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
import Compass from './Compass/Compass.vue'; import Compass from './Compass/Compass.vue';
import ImageControls from './ImageControls.vue'; import ImageControls from './ImageControls.vue';
import ImageThumbnail from './ImageThumbnail.vue';
import imageryData from "../../imagery/mixins/imageryData"; import imageryData from "../../imagery/mixins/imageryData";
const REFRESH_CSS_MS = 500; const REFRESH_CSS_MS = 500;
@@ -219,11 +229,9 @@ const SHOW_THUMBS_THRESHOLD_HEIGHT = 200;
const SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600; const SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600;
export default { export default {
name: 'ImageryView',
components: { components: {
Compass, Compass,
ImageControls, ImageControls
ImageThumbnail
}, },
mixins: [imageryData], mixins: [imageryData],
inject: ['openmct', 'domainObject', 'objectPath', 'currentView', 'imageFreshnessOptions'], inject: ['openmct', 'domainObject', 'objectPath', 'currentView', 'imageFreshnessOptions'],
@@ -246,7 +254,6 @@ export default {
visibleLayers: [], visibleLayers: [],
durationFormatter: undefined, durationFormatter: undefined,
imageHistory: [], imageHistory: [],
bounds: {},
timeSystem: timeSystem, timeSystem: timeSystem,
keyString: undefined, keyString: undefined,
autoScroll: true, autoScroll: true,
@@ -562,16 +569,6 @@ export default {
this.resetAgeCSS(); this.resetAgeCSS();
this.updateRelatedTelemetryForFocusedImage(); this.updateRelatedTelemetryForFocusedImage();
this.getImageNaturalDimensions(); this.getImageNaturalDimensions();
},
bounds() {
this.scrollToFocused();
},
isFixed(newValue) {
const isRealTime = !newValue;
// if realtime unpause which will focus on latest image
if (isRealTime) {
this.paused(false);
}
} }
}, },
async mounted() { async mounted() {
@@ -613,7 +610,6 @@ export default {
this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY); this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY);
this.handleThumbWindowResizeEnded = _.debounce(this.handleThumbWindowResizeEnded, SCROLL_LATENCY); this.handleThumbWindowResizeEnded = _.debounce(this.handleThumbWindowResizeEnded, SCROLL_LATENCY);
this.handleThumbWindowResizeStart = _.debounce(this.handleThumbWindowResizeStart, SCROLL_LATENCY); this.handleThumbWindowResizeStart = _.debounce(this.handleThumbWindowResizeStart, SCROLL_LATENCY);
this.scrollToFocused = _.debounce(this.scrollToFocused, 400);
if (this.$refs.thumbsWrapper) { if (this.$refs.thumbsWrapper) {
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart); this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
@@ -849,8 +845,7 @@ export default {
if (domThumb) { if (domThumb) {
domThumb.scrollIntoView({ domThumb.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
block: 'center', block: 'center'
inline: 'center'
}); });
} }
}, },

View File

@@ -258,22 +258,13 @@
min-width: $w; min-width: $w;
width: $w; width: $w;
&.active {
background: $colorSelectedBg;
color: $colorSelectedFg;
}
&:hover { &:hover {
background: $colorThumbHoverBg; background: $colorThumbHoverBg;
} }
&.selected { &.selected {
// fixed time - selected bg will match active bg color background: $colorPausedBg !important;
background: $colorSelectedBg; color: $colorPausedFg !important;
color: $colorSelectedFg;
&.real-time {
// real time - bg orange when selected
background: $colorPausedBg !important;
color: $colorPausedFg !important;
}
} }
&__image { &__image {

View File

@@ -139,7 +139,6 @@ export default {
// forcibly reset the imageContainer size to prevent an aspect ratio distortion // forcibly reset the imageContainer size to prevent an aspect ratio distortion
delete this.imageContainerWidth; delete this.imageContainerWidth;
delete this.imageContainerHeight; delete this.imageContainerHeight;
this.bounds = bounds; // setting bounds for ImageryView watcher
}, },
timeSystemChange() { timeSystemChange() {
this.timeSystem = this.timeContext.timeSystem(); this.timeSystem = this.timeContext.timeSystem();

View File

@@ -296,17 +296,12 @@ export default {
window.addEventListener('orientationchange', this.formatSidebar); window.addEventListener('orientationchange', this.formatSidebar);
window.addEventListener('hashchange', this.setSectionAndPageFromUrl); window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
this.filterAndSortEntries(); this.filterAndSortEntries();
this.unobserveEntries = this.openmct.objects.observe(this.domainObject, '*', this.filterAndSortEntries);
}, },
beforeDestroy() { beforeDestroy() {
if (this.unlisten) { if (this.unlisten) {
this.unlisten(); this.unlisten();
} }
if (this.unobserveEntries) {
this.unobserveEntries();
}
window.removeEventListener('orientationchange', this.formatSidebar); window.removeEventListener('orientationchange', this.formatSidebar);
window.removeEventListener('hashchange', this.setSectionAndPageFromUrl); window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
}, },

View File

@@ -88,7 +88,6 @@
:annotation-type="openmct.annotation.ANNOTATION_TYPES.NOTEBOOK" :annotation-type="openmct.annotation.ANNOTATION_TYPES.NOTEBOOK"
:annotation-search-type="openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS" :annotation-search-type="openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS"
:target-specific-details="{entryId: entry.id}" :target-specific-details="{entryId: entry.id}"
@tags-updated="timestampAndUpdate"
/> />
<div class="c-snapshots c-ne__embeds"> <div class="c-snapshots c-ne__embeds">
@@ -147,8 +146,6 @@ import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '..
import Moment from 'moment'; import Moment from 'moment';
const UNKNOWN_USER = 'Unknown';
export default { export default {
components: { components: {
NotebookEmbed, NotebookEmbed,
@@ -209,8 +206,7 @@ export default {
return { return {
targetKeyString, targetKeyString,
entryId: this.entry.id, entryId: this.entry.id
modified: this.entry.modified
}; };
}, },
createdOnTime() { createdOnTime() {
@@ -287,7 +283,7 @@ export default {
await this.addNewEmbed(objectPath); await this.addNewEmbed(objectPath);
} }
this.timestampAndUpdate(); this.$emit('updateEntry', this.entry);
}, },
findPositionInArray(array, id) { findPositionInArray(array, id) {
let position = -1; let position = -1;
@@ -325,7 +321,7 @@ export default {
// TODO: remove notebook snapshot object using object remove API // TODO: remove notebook snapshot object using object remove API
this.entry.embeds.splice(embedPosition, 1); this.entry.embeds.splice(embedPosition, 1);
this.timestampAndUpdate(); this.$emit('updateEntry', this.entry);
}, },
updateEmbed(newEmbed) { updateEmbed(newEmbed) {
this.entry.embeds.some(e => { this.entry.embeds.some(e => {
@@ -337,17 +333,6 @@ export default {
return found; return found;
}); });
this.timestampAndUpdate();
},
async timestampAndUpdate() {
const user = await this.openmct.user.getCurrentUser();
if (user === undefined) {
this.entry.modifiedBy = UNKNOWN_USER;
}
this.entry.modified = Date.now();
this.$emit('updateEntry', this.entry); this.$emit('updateEntry', this.entry);
}, },
editingEntry() { editingEntry() {
@@ -357,7 +342,7 @@ export default {
const value = $event.target.innerText; const value = $event.target.innerText;
if (value !== this.entry.text && value.match(/\S/)) { if (value !== this.entry.text && value.match(/\S/)) {
this.entry.text = value; this.entry.text = value;
this.timestampAndUpdate(); this.$emit('updateEntry', this.entry);
} else { } else {
this.$emit('cancelEdit'); this.$emit('cancelEdit');
} }

View File

@@ -211,17 +211,10 @@ describe("Notebook plugin:", () => {
describe("synchronization", () => { describe("synchronization", () => {
let objectCloneToSyncFrom;
beforeEach(() => {
objectCloneToSyncFrom = structuredClone(notebookViewObject);
objectCloneToSyncFrom.persisted = notebookViewObject.modified + 1;
});
it("updates an entry when another user modifies it", () => { it("updates an entry when another user modifies it", () => {
expect(getEntryText(0).innerText).toBe("First Test Entry"); expect(getEntryText(0).innerText).toBe("First Test Entry");
objectCloneToSyncFrom.configuration.entries["test-section-1"]["test-page-1"][0].text = "Modified entry text"; notebookViewObject.configuration.entries["test-section-1"]["test-page-1"][0].text = "Modified entry text";
objectProviderObserver(objectCloneToSyncFrom); objectProviderObserver(notebookViewObject);
return Vue.nextTick().then(() => { return Vue.nextTick().then(() => {
expect(getEntryText(0).innerText).toBe("Modified entry text"); expect(getEntryText(0).innerText).toBe("Modified entry text");
@@ -230,13 +223,13 @@ describe("Notebook plugin:", () => {
it("shows new entry when another user adds one", () => { it("shows new entry when another user adds one", () => {
expect(allNotebookEntryElements().length).toBe(2); expect(allNotebookEntryElements().length).toBe(2);
objectCloneToSyncFrom.configuration.entries["test-section-1"]["test-page-1"].push({ notebookViewObject.configuration.entries["test-section-1"]["test-page-1"].push({
"id": "entry-3", "id": "entry-3",
"createdOn": 0, "createdOn": 0,
"text": "Third Test Entry", "text": "Third Test Entry",
"embeds": [] "embeds": []
}); });
objectProviderObserver(objectCloneToSyncFrom); objectProviderObserver(notebookViewObject);
return Vue.nextTick().then(() => { return Vue.nextTick().then(() => {
expect(allNotebookEntryElements().length).toBe(3); expect(allNotebookEntryElements().length).toBe(3);
@@ -244,9 +237,9 @@ describe("Notebook plugin:", () => {
}); });
it("removes an entry when another user removes one", () => { it("removes an entry when another user removes one", () => {
expect(allNotebookEntryElements().length).toBe(2); expect(allNotebookEntryElements().length).toBe(2);
let entries = objectCloneToSyncFrom.configuration.entries["test-section-1"]["test-page-1"]; let entries = notebookViewObject.configuration.entries["test-section-1"]["test-page-1"];
objectCloneToSyncFrom.configuration.entries["test-section-1"]["test-page-1"] = entries.splice(0, 1); notebookViewObject.configuration.entries["test-section-1"]["test-page-1"] = entries.splice(0, 1);
objectProviderObserver(objectCloneToSyncFrom); objectProviderObserver(notebookViewObject);
return Vue.nextTick().then(() => { return Vue.nextTick().then(() => {
expect(allNotebookEntryElements().length).toBe(1); expect(allNotebookEntryElements().length).toBe(1);
@@ -263,8 +256,8 @@ describe("Notebook plugin:", () => {
}; };
expect(allNotebookPageElements().length).toBe(2); expect(allNotebookPageElements().length).toBe(2);
objectCloneToSyncFrom.configuration.sections[0].pages.push(newPage); notebookViewObject.configuration.sections[0].pages.push(newPage);
objectProviderObserver(objectCloneToSyncFrom); objectProviderObserver(notebookViewObject);
return Vue.nextTick().then(() => { return Vue.nextTick().then(() => {
expect(allNotebookPageElements().length).toBe(3); expect(allNotebookPageElements().length).toBe(3);
@@ -274,8 +267,8 @@ describe("Notebook plugin:", () => {
it("updates the notebook when a user removes a page", () => { it("updates the notebook when a user removes a page", () => {
expect(allNotebookPageElements().length).toBe(2); expect(allNotebookPageElements().length).toBe(2);
objectCloneToSyncFrom.configuration.sections[0].pages.splice(0, 1); notebookViewObject.configuration.sections[0].pages.splice(0, 1);
objectProviderObserver(objectCloneToSyncFrom); objectProviderObserver(notebookViewObject);
return Vue.nextTick().then(() => { return Vue.nextTick().then(() => {
expect(allNotebookPageElements().length).toBe(1); expect(allNotebookPageElements().length).toBe(1);
@@ -298,8 +291,8 @@ describe("Notebook plugin:", () => {
}; };
expect(allNotebookSectionElements().length).toBe(2); expect(allNotebookSectionElements().length).toBe(2);
objectCloneToSyncFrom.configuration.sections.push(newSection); notebookViewObject.configuration.sections.push(newSection);
objectProviderObserver(objectCloneToSyncFrom); objectProviderObserver(notebookViewObject);
return Vue.nextTick().then(() => { return Vue.nextTick().then(() => {
expect(allNotebookSectionElements().length).toBe(3); expect(allNotebookSectionElements().length).toBe(3);
@@ -308,8 +301,8 @@ describe("Notebook plugin:", () => {
it("updates the notebook when a user removes a section", () => { it("updates the notebook when a user removes a section", () => {
expect(allNotebookSectionElements().length).toBe(2); expect(allNotebookSectionElements().length).toBe(2);
objectCloneToSyncFrom.configuration.sections.splice(0, 1); notebookViewObject.configuration.sections.splice(0, 1);
objectProviderObserver(objectCloneToSyncFrom); objectProviderObserver(notebookViewObject);
return Vue.nextTick().then(() => { return Vue.nextTick().then(() => {
expect(allNotebookSectionElements().length).toBe(1); expect(allNotebookSectionElements().length).toBe(1);

View File

@@ -217,11 +217,9 @@ class CouchObjectProvider {
this.indicator.setIndicatorToState(DISCONNECTED); this.indicator.setIndicatorToState(DISCONNECTED);
console.error(error.message); console.error(error.message);
throw new Error(`CouchDB Error - No response"`); throw new Error(`CouchDB Error - No response"`);
} else {
console.error(error.message);
throw error;
} }
console.error(error.message);
} }
} }
@@ -289,7 +287,7 @@ class CouchObjectProvider {
this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]); this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]);
} }
if (isNotebookType(object) || object.type === 'annotation') { if (isNotebookType(object)) {
//Temporary measure until object sync is supported for all object types //Temporary measure until object sync is supported for all object types
//Always update notebook revision number because we have realtime sync, so always assume it's the latest. //Always update notebook revision number because we have realtime sync, so always assume it's the latest.
this.objectQueue[key].updateRevision(response[REV]); this.objectQueue[key].updateRevision(response[REV]);
@@ -655,6 +653,7 @@ class CouchObjectProvider {
let document = new CouchDocument(key, queued.model); let document = new CouchDocument(key, queued.model);
document.metadata.created = Date.now(); document.metadata.created = Date.now();
this.request(key, "PUT", document).then((response) => { this.request(key, "PUT", document).then((response) => {
console.log('create check response', key);
this.#checkResponse(response, queued.intermediateResponse, key); this.#checkResponse(response, queued.intermediateResponse, key);
}).catch(error => { }).catch(error => {
queued.intermediateResponse.reject(error); queued.intermediateResponse.reject(error);

View File

@@ -188,8 +188,7 @@ export default {
if (domainObject.type === 'plan') { if (domainObject.type === 'plan') {
this.getPlanDataAndSetConfig({ this.getPlanDataAndSetConfig({
...this.domainObject, ...this.domainObject,
selectFile: domainObject.selectFile, selectFile: domainObject.selectFile
sourceMap: domainObject.sourceMap
}); });
} }
}, },

View File

@@ -25,14 +25,13 @@
/******************************************************** CONTROL-SPECIFIC MIXINS */ /******************************************************** CONTROL-SPECIFIC MIXINS */
@mixin menuOuter() { @mixin menuOuter() {
border-radius: $basicCr; border-radius: $basicCr;
box-shadow: $shdwMenu; box-shadow: $shdwMenuInner, $shdwMenu;
@if $shdwMenuInner != none {
box-shadow: $shdwMenuInner, $shdwMenu;
}
background: $colorMenuBg; background: $colorMenuBg;
color: $colorMenuFg; color: $colorMenuFg;
//filter: $filterMenu; // 2022: causing all kinds of weird visual bugs in Chrome
text-shadow: $shdwMenuText; text-shadow: $shdwMenuText;
padding: $interiorMarginSm; padding: $interiorMarginSm;
//box-shadow: $shdwMenu;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: absolute; position: absolute;
@@ -61,13 +60,14 @@
cursor: pointer; cursor: pointer;
display: flex; display: flex;
padding: nth($menuItemPad, 1) nth($menuItemPad, 2); padding: nth($menuItemPad, 1) nth($menuItemPad, 2);
transition: $transIn;
white-space: nowrap; white-space: nowrap;
@include hover { @include hover {
background: $colorMenuHovBg; background: $colorMenuHovBg;
color: $colorMenuHovFg; color: $colorMenuHovFg;
&:before { &:before {
color: $colorMenuHovIc !important; color: $colorMenuHovIc;
} }
} }

View File

@@ -97,17 +97,14 @@ export default {
this.tagsChanged(this.annotation.tags); this.tagsChanged(this.annotation.tags);
}, },
deep: true deep: true
},
annotationQuery: {
handler() {
this.unloadAnnotation();
this.loadAnnotation();
},
deep: true
} }
}, },
mounted() { async mounted() {
this.loadAnnotation(); this.annotation = await this.openmct.annotation.getAnnotation(this.annotationQuery, this.annotationSearchType);
this.addAnnotationListener(this.annotation);
if (this.annotation && this.annotation.tags) {
this.tagsChanged(this.annotation.tags);
}
}, },
destroyed() { destroyed() {
if (this.removeTagsListener) { if (this.removeTagsListener) {
@@ -117,23 +114,7 @@ export default {
methods: { methods: {
addAnnotationListener(annotation) { addAnnotationListener(annotation) {
if (annotation && !this.removeTagsListener) { if (annotation && !this.removeTagsListener) {
this.removeTagsListener = this.openmct.objects.observe(annotation, '*', (newAnnotation) => { this.removeTagsListener = this.openmct.objects.observe(annotation, 'tags', this.tagsChanged);
this.tagsChanged(newAnnotation.tags);
this.annotation = newAnnotation;
});
}
},
async loadAnnotation() {
this.annotation = await this.openmct.annotation.getAnnotation(this.annotationQuery, this.annotationSearchType);
this.addAnnotationListener(this.annotation);
if (this.annotation && this.annotation.tags) {
this.tagsChanged(this.annotation.tags);
}
},
unloadAnnotation() {
if (this.removeTagsListener) {
this.removeTagsListener();
this.removeTagsListener = undefined;
} }
}, },
tagsChanged(newTags) { tagsChanged(newTags) {
@@ -152,11 +133,8 @@ export default {
this.addedTags.push(newTagValue); this.addedTags.push(newTagValue);
this.userAddingTag = true; this.userAddingTag = true;
}, },
async tagRemoved(tagToRemove) { tagRemoved(tagToRemove) {
const result = await this.openmct.annotation.removeAnnotationTag(this.annotation, tagToRemove); return this.openmct.annotation.removeAnnotationTag(this.annotation, tagToRemove);
this.$emit('tags-updated');
return result;
}, },
async tagAdded(newTag) { async tagAdded(newTag) {
const annotationWasCreated = this.annotation === null || this.annotation === undefined; const annotationWasCreated = this.annotation === null || this.annotation === undefined;
@@ -168,8 +146,6 @@ export default {
this.tagsChanged(this.annotation.tags); this.tagsChanged(this.annotation.tags);
this.userAddingTag = false; this.userAddingTag = false;
this.$emit('tags-updated');
} }
} }
}; };

View File

@@ -77,6 +77,7 @@ export default {
} }
this.searchValue = value; this.searchValue = value;
this.searchLoading = true;
// clear any previous search results // clear any previous search results
this.annotationSearchResults = []; this.annotationSearchResults = [];
this.objectSearchResults = []; this.objectSearchResults = [];
@@ -84,13 +85,8 @@ export default {
if (this.searchValue) { if (this.searchValue) {
await this.getSearchResults(); await this.getSearchResults();
} else { } else {
const dropdownOptions = { this.searchLoading = false;
searchLoading: this.searchLoading, this.$refs.searchResultsDropDown.showResults(this.annotationSearchResults, this.objectSearchResults);
searchValue: this.searchValue,
annotationSearchResults: this.annotationSearchResults,
objectSearchResults: this.objectSearchResults
};
this.$refs.searchResultsDropDown.showResults(dropdownOptions);
} }
}, },
getPathsForObjects(objectsNeedingPaths) { getPathsForObjects(objectsNeedingPaths) {
@@ -107,8 +103,6 @@ export default {
async getSearchResults() { async getSearchResults() {
// an abort controller will be passed in that will be used // an abort controller will be passed in that will be used
// to cancel an active searches if necessary // to cancel an active searches if necessary
this.searchLoading = true;
this.$refs.searchResultsDropDown.showSearchStarted();
this.abortSearchController = new AbortController(); this.abortSearchController = new AbortController();
const abortSignal = this.abortSearchController.signal; const abortSignal = this.abortSearchController.signal;
try { try {
@@ -116,15 +110,10 @@ export default {
const fullObjectSearchResults = await Promise.all(this.openmct.objects.search(this.searchValue, abortSignal)); const fullObjectSearchResults = await Promise.all(this.openmct.objects.search(this.searchValue, abortSignal));
const aggregatedObjectSearchResults = fullObjectSearchResults.flat(); const aggregatedObjectSearchResults = fullObjectSearchResults.flat();
const aggregatedObjectSearchResultsWithPaths = await this.getPathsForObjects(aggregatedObjectSearchResults); const aggregatedObjectSearchResultsWithPaths = await this.getPathsForObjects(aggregatedObjectSearchResults);
const filterAnnotationsAndValidPaths = aggregatedObjectSearchResultsWithPaths.filter(result => { const filterAnnotations = aggregatedObjectSearchResultsWithPaths.filter(result => {
if (this.openmct.annotation.isAnnotation(result)) { return result.type !== 'annotation';
return false;
}
return this.openmct.objects.isReachable(result?.originalPath);
}); });
this.objectSearchResults = filterAnnotationsAndValidPaths; this.objectSearchResults = filterAnnotations;
this.searchLoading = false;
this.showSearchResults(); this.showSearchResults();
} catch (error) { } catch (error) {
console.error(`😞 Error searching`, error); console.error(`😞 Error searching`, error);
@@ -136,13 +125,7 @@ export default {
} }
}, },
showSearchResults() { showSearchResults() {
const dropdownOptions = { this.$refs.searchResultsDropDown.showResults(this.annotationSearchResults, this.objectSearchResults);
searchLoading: this.searchLoading,
searchValue: this.searchValue,
annotationSearchResults: this.annotationSearchResults,
objectSearchResults: this.objectSearchResults
};
this.$refs.searchResultsDropDown.showResults(dropdownOptions);
document.body.addEventListener('click', this.handleOutsideClick); document.body.addEventListener('click', this.handleOutsideClick);
}, },
handleOutsideClick(event) { handleOutsideClick(event) {

View File

@@ -39,8 +39,6 @@ describe("GrandSearch", () => {
let mockAnnotationObject; let mockAnnotationObject;
let mockDisplayLayout; let mockDisplayLayout;
let mockFolderObject; let mockFolderObject;
let mockAnotherFolderObject;
let mockTopObject;
let originalRouterPath; let originalRouterPath;
beforeEach((done) => { beforeEach((done) => {
@@ -72,29 +70,11 @@ describe("GrandSearch", () => {
} }
} }
}; };
mockTopObject = {
type: 'root',
name: 'Top Folder',
identifier: {
key: 'topObject',
namespace: 'fooNameSpace'
}
};
mockAnotherFolderObject = {
type: 'folder',
name: 'Another Test Folder',
location: 'fooNameSpace:topObject',
identifier: {
key: 'someParent',
namespace: 'fooNameSpace'
}
};
mockFolderObject = { mockFolderObject = {
type: 'folder', type: 'folder',
name: 'Test Folder', name: 'Test Folder',
location: 'fooNameSpace:someParent',
identifier: { identifier: {
key: 'someFolder', key: 'some-folder',
namespace: 'fooNameSpace' namespace: 'fooNameSpace'
} }
}; };
@@ -142,10 +122,6 @@ describe("GrandSearch", () => {
return mockDisplayLayout; return mockDisplayLayout;
} else if (identifier.key === mockFolderObject.identifier.key) { } else if (identifier.key === mockFolderObject.identifier.key) {
return mockFolderObject; return mockFolderObject;
} else if (identifier.key === mockAnotherFolderObject.identifier.key) {
return mockAnotherFolderObject;
} else if (identifier.key === mockTopObject.identifier.key) {
return mockTopObject;
} else { } else {
return null; return null;
} }

View File

@@ -22,6 +22,8 @@
<template> <template>
<div <div
v-if="(annotationResults && annotationResults.length) ||
(objectResults && objectResults.length)"
class="c-gsearch__dropdown" class="c-gsearch__dropdown"
> >
<div <div
@@ -56,40 +58,25 @@
@click.native="selectedResult" @click.native="selectedResult"
/> />
</div> </div>
<div
v-if="searchLoading"
> <progress-bar
:model="{progressText: 'Searching...',
progressPerc: undefined
}"
/>
</div>
<div
v-if="!searchLoading && (!annotationResults || !annotationResults.length) &&
(!objectResults || !objectResults.length)"
>No matching results.
</div>
</div> </div>
</div> </div>
</div></template> </div>
</template>
<script> <script>
import AnnotationSearchResult from './AnnotationSearchResult.vue'; import AnnotationSearchResult from './AnnotationSearchResult.vue';
import ObjectSearchResult from './ObjectSearchResult.vue'; import ObjectSearchResult from './ObjectSearchResult.vue';
import ProgressBar from '@/ui/components/ProgressBar.vue';
export default { export default {
name: 'SearchResultsDropDown', name: 'SearchResultsDropDown',
components: { components: {
AnnotationSearchResult, AnnotationSearchResult,
ObjectSearchResult, ObjectSearchResult
ProgressBar
}, },
inject: ['openmct'], inject: ['openmct'],
data() { data() {
return { return {
resultsShown: false, resultsShown: false,
searchLoading: false,
annotationResults: [], annotationResults: [],
objectResults: [], objectResults: [],
previewVisible: false previewVisible: false
@@ -104,18 +91,12 @@ export default {
previewChanged(changedPreviewState) { previewChanged(changedPreviewState) {
this.previewVisible = changedPreviewState; this.previewVisible = changedPreviewState;
}, },
showSearchStarted() { showResults(passedAnnotationResults, passedObjectResults) {
this.searchLoading = true; if ((passedAnnotationResults && passedAnnotationResults.length)
this.resultsShown = true; || (passedObjectResults && passedObjectResults.length)) {
this.annotationResults = [];
this.objectResults = [];
},
showResults({searchLoading, searchValue, annotationSearchResults, objectSearchResults}) {
this.searchLoading = searchLoading;
this.annotationResults = annotationSearchResults;
this.objectResults = objectSearchResults;
if (searchValue?.length) {
this.resultsShown = true; this.resultsShown = true;
this.annotationResults = passedAnnotationResults;
this.objectResults = passedObjectResults;
} else { } else {
this.resultsShown = false; this.resultsShown = false;
} }