Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nasa/openmct.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorScott Bell <scott@traclabs.com>2022-08-17 20:05:29 +0300
committerGitHub <noreply@github.com>2022-08-17 20:05:29 +0300
commit4787827705e7b4219b8a5c18b64cdcb07192cefc (patch)
treeb3e90091699537b1351f89f8dfd4039213957374
parentfd389cfa4d36b0296a6f0e6573c4708241f5bea8 (diff)
parentb47712a0f42dc753c4e52bfc8a1dff72cd214223 (diff)
Merge branch 'release/2.0.8' into mct5549-fix-composition-errormct5549-fix-composition-error
-rw-r--r--e2e/appActions.js19
-rw-r--r--e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js27
-rw-r--r--e2e/tests/functional/plugins/timer/timer.e2e.spec.js17
-rw-r--r--e2e/tests/functional/tree.e2e.spec.js138
-rw-r--r--e2e/tests/visual/components/tree.visual.spec.js101
-rw-r--r--package.json2
-rw-r--r--src/ui/layout/Layout.vue1
-rw-r--r--src/ui/layout/mct-tree.vue53
-rw-r--r--src/ui/layout/tree-item.vue4
9 files changed, 318 insertions, 44 deletions
diff --git a/e2e/appActions.js b/e2e/appActions.js
index e4fbf7589..c7a4428d5 100644
--- a/e2e/appActions.js
+++ b/e2e/appActions.js
@@ -102,20 +102,15 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
/**
* Open the given `domainObject`'s context menu from the object tree.
-* Expands the 'My Items' folder if it is not already expanded.
+* Expands the path to the object and scrolls to it if necessary.
*
* @param {import('@playwright/test').Page} page
-* @param {string} myItemsFolderName the name of the "My Items" folder
-* @param {string} domainObjectName the display name of the `domainObject`
+* @param {string} url the url to the object
*/
-async function openObjectTreeContextMenu(page, myItemsFolderName, domainObjectName) {
- const myItemsFolder = page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3);
- const className = await myItemsFolder.getAttribute('class');
- if (!className.includes('c-disclosure-triangle--expanded')) {
- await myItemsFolder.click();
- }
-
- await page.locator(`a:has-text("${domainObjectName}")`).click({
+async function openObjectTreeContextMenu(page, url) {
+ await page.goto(url);
+ await page.click('button[title="Show selected item in tree"]');
+ await page.locator('.is-navigated-object').click({
button: 'right'
});
}
@@ -129,7 +124,7 @@ async function openObjectTreeContextMenu(page, myItemsFolderName, domainObjectNa
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);
+ return window.location.href.split('?')[0].match(regexp).at(-1);
}, UUIDv4Regexp);
return focusedObjectUuid;
diff --git a/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js b/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js
index c076329d4..568061c89 100644
--- a/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js
+++ b/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js
@@ -21,7 +21,7 @@
*****************************************************************************/
const { test, expect } = require('../../../../pluginFixtures');
-const { openObjectTreeContextMenu } = require('../../../../appActions');
+const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
const path = require('path');
const TEST_TEXT = 'Testing text for entries.';
@@ -30,8 +30,9 @@ const CUSTOM_NAME = 'CUSTOM_NAME';
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
test.describe('Restricted Notebook', () => {
+ let notebook;
test.beforeEach(async ({ page }) => {
- await startAndAddRestrictedNotebookObject(page);
+ notebook = await startAndAddRestrictedNotebookObject(page);
});
test('Can be renamed @addInit', async ({ page }) => {
@@ -39,9 +40,7 @@ test.describe('Restricted Notebook', () => {
});
test('Can be deleted if there are no locked pages @addInit', async ({ page, openmctConfig }) => {
- const { myItemsFolderName } = openmctConfig;
-
- await openObjectTreeContextMenu(page, myItemsFolderName, `Unnamed ${CUSTOM_NAME}`);
+ await openObjectTreeContextMenu(page, notebook.url);
const menuOptions = page.locator('.c-menu ul');
await expect.soft(menuOptions).toContainText('Remove');
@@ -76,9 +75,9 @@ test.describe('Restricted Notebook', () => {
});
test.describe('Restricted Notebook with at least one entry and with the page locked @addInit', () => {
-
+ let notebook;
test.beforeEach(async ({ page }) => {
- await startAndAddRestrictedNotebookObject(page);
+ notebook = await startAndAddRestrictedNotebookObject(page);
await enterTextEntry(page);
await lockPage(page);
@@ -86,9 +85,8 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
await page.locator('button.c-notebook__toggle-nav-button').click();
});
- test('Locked page should now be in a locked state @addInit @unstable', async ({ page, openmctConfig }, testInfo) => {
+ test('Locked page should now be in a locked state @addInit @unstable', async ({ page }, testInfo) => {
test.fixme(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta");
- const { myItemsFolderName } = openmctConfig;
// main lock message on page
const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed');
expect.soft(await lockMessage.count()).toEqual(1);
@@ -98,7 +96,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
expect.soft(await pageLockIcon.count()).toEqual(1);
// no way to remove a restricted notebook with a locked page
- await openObjectTreeContextMenu(page, myItemsFolderName, `Unnamed ${CUSTOM_NAME}`);
+ await openObjectTreeContextMenu(page, notebook.url);
const menuOptions = page.locator('.c-menu ul');
await expect(menuOptions).not.toContainText('Remove');
@@ -178,13 +176,8 @@ async function startAndAddRestrictedNotebookObject(page) {
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitRestrictedNotebook.js') });
await page.goto('./', { waitUntil: 'networkidle' });
- await page.click('button:has-text("Create")');
- await page.click(`text=${CUSTOM_NAME}`); // secondarily tests renamability also
- // Click text=OK
- await Promise.all([
- page.waitForNavigation({waitUntil: 'networkidle'}),
- page.click('text=OK')
- ]);
+
+ return createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });
}
/**
diff --git a/e2e/tests/functional/plugins/timer/timer.e2e.spec.js b/e2e/tests/functional/plugins/timer/timer.e2e.spec.js
index 0235a1d2c..16c6b5def 100644
--- a/e2e/tests/functional/plugins/timer/timer.e2e.spec.js
+++ b/e2e/tests/functional/plugins/timer/timer.e2e.spec.js
@@ -24,9 +24,10 @@ const { test, expect } = require('../../../../pluginFixtures');
const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Timer', () => {
+ let timer;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
- await createDomainObjectWithDefaults(page, { type: 'timer' });
+ timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
});
test('Can perform actions on the Timer', async ({ page, openmctConfig }) => {
@@ -35,13 +36,13 @@ test.describe('Timer', () => {
description: 'https://github.com/nasa/openmct/issues/4313'
});
- const { myItemsFolderName } = await openmctConfig;
+ const timerUrl = timer.url;
await test.step("From the tree context menu", async () => {
- await triggerTimerContextMenuAction(page, myItemsFolderName, 'Start');
- await triggerTimerContextMenuAction(page, myItemsFolderName, 'Pause');
- await triggerTimerContextMenuAction(page, myItemsFolderName, 'Restart at 0');
- await triggerTimerContextMenuAction(page, myItemsFolderName, 'Stop');
+ await triggerTimerContextMenuAction(page, timerUrl, 'Start');
+ await triggerTimerContextMenuAction(page, timerUrl, 'Pause');
+ await triggerTimerContextMenuAction(page, timerUrl, 'Restart at 0');
+ await triggerTimerContextMenuAction(page, timerUrl, 'Stop');
});
await test.step("From the 3dot menu", async () => {
@@ -74,9 +75,9 @@ test.describe('Timer', () => {
* @param {import('@playwright/test').Page} page
* @param {TimerAction} action
*/
-async function triggerTimerContextMenuAction(page, myItemsFolderName, action) {
+async function triggerTimerContextMenuAction(page, timerUrl, action) {
const menuAction = `.c-menu ul li >> text="${action}"`;
- await openObjectTreeContextMenu(page, myItemsFolderName, "Unnamed Timer");
+ await openObjectTreeContextMenu(page, timerUrl);
await page.locator(menuAction).click();
assertTimerStateAfterAction(page, action);
}
diff --git a/e2e/tests/functional/tree.e2e.spec.js b/e2e/tests/functional/tree.e2e.spec.js
new file mode 100644
index 000000000..691f7f127
--- /dev/null
+++ b/e2e/tests/functional/tree.e2e.spec.js
@@ -0,0 +1,138 @@
+/*****************************************************************************
+ * 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.js');
+const {
+ createDomainObjectWithDefaults,
+ openObjectTreeContextMenu
+} = require('../../appActions.js');
+
+test.describe('Tree operations', () => {
+ test('Renaming an object reorders the tree @unstable', async ({ page, openmctConfig }) => {
+ const { myItemsFolderName } = openmctConfig;
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: 'Foo'
+ });
+
+ await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: 'Bar'
+ });
+
+ await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: 'Baz'
+ });
+
+ const clock1 = await createDomainObjectWithDefaults(page, {
+ type: 'Clock',
+ name: 'aaa'
+ });
+
+ await createDomainObjectWithDefaults(page, {
+ type: 'Clock',
+ name: 'www'
+ });
+
+ // Expand the root folder
+ await expandTreePaneItemByName(page, myItemsFolderName);
+
+ await test.step("Reorders objects with the same tree depth", async () => {
+ await getAndAssertTreeItems(page, ['aaa', 'Bar', 'Baz', 'Foo', 'www']);
+ await renameObjectFromContextMenu(page, clock1.url, 'zzz');
+ await getAndAssertTreeItems(page, ['Bar', 'Baz', 'Foo', 'www', 'zzz']);
+ });
+
+ await test.step("Reorders links to objects as well as original objects", async () => {
+ await page.click('role=treeitem[name=/Bar/]');
+ await page.dragAndDrop('role=treeitem[name=/www/]', '.c-object-view');
+ await page.dragAndDrop('role=treeitem[name=/zzz/]', '.c-object-view');
+ await page.click('role=treeitem[name=/Baz/]');
+ await page.dragAndDrop('role=treeitem[name=/www/]', '.c-object-view');
+ await page.dragAndDrop('role=treeitem[name=/zzz/]', '.c-object-view');
+ await page.click('role=treeitem[name=/Foo/]');
+ await page.dragAndDrop('role=treeitem[name=/www/]', '.c-object-view');
+ await page.dragAndDrop('role=treeitem[name=/zzz/]', '.c-object-view');
+ // Expand the unopened folders
+ await expandTreePaneItemByName(page, 'Bar');
+ await expandTreePaneItemByName(page, 'Baz');
+ await expandTreePaneItemByName(page, 'Foo');
+
+ await renameObjectFromContextMenu(page, clock1.url, '___');
+ await getAndAssertTreeItems(page,
+ [
+ "___",
+ "Bar",
+ "___",
+ "www",
+ "Baz",
+ "___",
+ "www",
+ "Foo",
+ "___",
+ "www",
+ "www"
+ ]);
+ });
+ });
+});
+
+/**
+ * @param {import('@playwright/test').Page} page
+ * @param {Array<string>} expected
+ */
+async function getAndAssertTreeItems(page, expected) {
+ const treeItems = page.locator('[role="treeitem"]');
+ const allTexts = await treeItems.allInnerTexts();
+ // Get rid of root folder ('My Items') as its position will not change
+ allTexts.shift();
+ expect(allTexts).toEqual(expected);
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ * @param {string} name
+ */
+async function expandTreePaneItemByName(page, name) {
+ const treePane = page.locator('#tree-pane');
+ const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
+ const expandTriangle = treeItem.locator('.c-disclosure-triangle');
+ await expandTriangle.click();
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ * @param {string} myItemsFolderName
+ * @param {string} url
+ * @param {string} newName
+ */
+async function renameObjectFromContextMenu(page, url, newName) {
+ await openObjectTreeContextMenu(page, url);
+ await page.click('li:text("Edit Properties")');
+ const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
+ await nameInput.fill("");
+ await nameInput.fill(newName);
+ await page.click('[aria-label="Save"]');
+}
diff --git a/e2e/tests/visual/components/tree.visual.spec.js b/e2e/tests/visual/components/tree.visual.spec.js
new file mode 100644
index 000000000..0ad2aca75
--- /dev/null
+++ b/e2e/tests/visual/components/tree.visual.spec.js
@@ -0,0 +1,101 @@
+/*****************************************************************************
+ * 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 } = require('../../../pluginFixtures.js');
+const { createDomainObjectWithDefaults } = require('../../../appActions.js');
+
+const percySnapshot = require('@percy/playwright');
+
+test.describe('Visual - Tree Pane', () => {
+ test('Tree pane in various states @unstable', async ({ page, theme, openmctConfig }) => {
+ const { myItemsFolderName } = openmctConfig;
+ await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
+
+ const foo = await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: "Foo Folder"
+ });
+
+ const bar = await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: "Bar Folder",
+ parent: foo.uuid
+ });
+
+ const baz = await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: "Baz Folder",
+ parent: bar.uuid
+ });
+
+ await createDomainObjectWithDefaults(page, {
+ type: 'Clock',
+ name: 'A Clock'
+ });
+
+ await createDomainObjectWithDefaults(page, {
+ type: 'Clock',
+ name: 'Z Clock'
+ });
+
+ const treePane = "#tree-pane";
+
+ await percySnapshot(page, `Tree Pane w/ collapsed tree (theme: ${theme})`, {
+ scope: treePane
+ });
+
+ await expandTreePaneItemByName(page, myItemsFolderName);
+
+ await page.goto(foo.url);
+ await page.dragAndDrop('role=treeitem[name=/A Clock/]', '.c-object-view');
+ await page.dragAndDrop('role=treeitem[name=/Z Clock/]', '.c-object-view');
+ await page.goto(bar.url);
+ await page.dragAndDrop('role=treeitem[name=/A Clock/]', '.c-object-view');
+ await page.dragAndDrop('role=treeitem[name=/Z Clock/]', '.c-object-view');
+ await page.goto(baz.url);
+ await page.dragAndDrop('role=treeitem[name=/A Clock/]', '.c-object-view');
+ await page.dragAndDrop('role=treeitem[name=/Z Clock/]', '.c-object-view');
+
+ await percySnapshot(page, `Tree Pane w/ single level expanded (theme: ${theme})`, {
+ scope: treePane
+ });
+
+ await expandTreePaneItemByName(page, foo.name);
+ await expandTreePaneItemByName(page, bar.name);
+ await expandTreePaneItemByName(page, baz.name);
+
+ await percySnapshot(page, `Tree Pane w/ multiple levels expanded (theme: ${theme})`, {
+ scope: treePane
+ });
+ });
+});
+
+/**
+ * @param {import('@playwright/test').Page} page
+ * @param {string} name
+ */
+async function expandTreePaneItemByName(page, name) {
+ const treePane = page.locator('#tree-pane');
+ const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
+ const expandTriangle = treeItem.locator('.c-disclosure-triangle');
+ await expandTriangle.click();
+}
diff --git a/package.json b/package.json
index d1c9ceebb..9148462bd 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"devDependencies": {
"@babel/eslint-parser": "7.18.9",
"@braintree/sanitize-url": "6.0.0",
- "@percy/cli": "1.7.2",
+ "@percy/cli": "1.8.1",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.23.0",
"@types/eventemitter3": "^1.0.0",
diff --git a/src/ui/layout/Layout.vue b/src/ui/layout/Layout.vue
index ee5296436..87db0617c 100644
--- a/src/ui/layout/Layout.vue
+++ b/src/ui/layout/Layout.vue
@@ -53,6 +53,7 @@
type="horizontal"
>
<pane
+ id="tree-pane"
class="l-shell__pane-tree"
handle="after"
label="Browse"
diff --git a/src/ui/layout/mct-tree.vue b/src/ui/layout/mct-tree.vue
index 220f82b42..9bf1b87a4 100644
--- a/src/ui/layout/mct-tree.vue
+++ b/src/ui/layout/mct-tree.vue
@@ -41,6 +41,8 @@
<div
ref="mainTree"
class="c-tree-and-search__tree c-tree"
+ role="tree"
+ aria-expanded="true"
>
<div>
@@ -467,7 +469,7 @@ export default {
}
},
scrollEndEvent() {
- if (!this.$refs.srcrollable) {
+ if (!this.$refs.scrollable) {
return;
}
@@ -576,14 +578,17 @@ export default {
};
},
addTreeItemObserver(domainObject, parentObjectPath) {
- if (this.observers[domainObject.identifier.key]) {
- this.observers[domainObject.identifier.key]();
+ const objectPath = [domainObject].concat(parentObjectPath);
+ const navigationPath = this.buildNavigationPath(objectPath);
+
+ if (this.observers[navigationPath]) {
+ this.observers[navigationPath]();
}
- this.observers[domainObject.identifier.key] = this.openmct.objects.observe(
+ this.observers[navigationPath] = this.openmct.objects.observe(
domainObject,
'name',
- this.updateTreeItems.bind(this, parentObjectPath)
+ this.sortTreeItems.bind(this, parentObjectPath)
);
},
async updateTreeItems(parentObjectPath) {
@@ -610,6 +615,44 @@ export default {
}
}
},
+ sortTreeItems(parentObjectPath) {
+ const navigationPath = this.buildNavigationPath(parentObjectPath);
+ const parentItem = this.getTreeItemByPath(navigationPath);
+
+ // If the parent is not sortable, skip sorting
+ if (!this.isSortable(parentObjectPath)) {
+ return;
+ }
+
+ // Sort the renamed object and its siblings (direct descendants of the parent)
+ const directDescendants = this.getChildrenInTreeFor(parentItem, false);
+ directDescendants.sort(this.sortNameAscending);
+
+ // Take a copy of the sorted descendants array
+ const sortedTreeItems = directDescendants.slice();
+
+ directDescendants.forEach(descendant => {
+ const parent = this.getTreeItemByPath(descendant.navigationPath);
+
+ // If descendant is not open, skip
+ if (!this.isTreeItemOpen(parent)) {
+ return;
+ }
+
+ // If descendant is open but has no children, skip
+ const children = this.getChildrenInTreeFor(parent, true);
+ if (children.length === 0) {
+ return;
+ }
+
+ // Splice in the children of the descendant
+ const parentIndex = sortedTreeItems.map(item => item.navigationPath).indexOf(parent.navigationPath);
+ sortedTreeItems.splice(parentIndex + 1, 0, ...children);
+ });
+
+ // Splice in all of the sorted descendants
+ this.treeItems.splice(this.treeItems.indexOf(parentItem) + 1, sortedTreeItems.length, ...sortedTreeItems);
+ },
buildNavigationPath(objectPath) {
return '/browse/' + [...objectPath].reverse()
.map((object) => this.openmct.objects.makeKeyString(object.identifier))
diff --git a/src/ui/layout/tree-item.vue b/src/ui/layout/tree-item.vue
index 7283e3bd3..5c0712f95 100644
--- a/src/ui/layout/tree-item.vue
+++ b/src/ui/layout/tree-item.vue
@@ -1,7 +1,9 @@
<template>
<div
- :style="treeItemStyles"
class="c-tree__item-h"
+ role="treeitem"
+ :style="treeItemStyles"
+ :aria-expanded="(!activeSearch && hasComposition) ? (isOpen || isLoading) ? 'true' : 'false' : undefined"
>
<div
class="c-tree__item"