Compare commits
61 Commits
release/2.
...
playwright
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98fd496c1f | ||
|
|
0dd12bce85 | ||
|
|
9c9e0442f1 | ||
|
|
d49f057698 | ||
|
|
c74ad1279c | ||
|
|
470a451956 | ||
|
|
fa6cbb6f4d | ||
|
|
52c00cfaef | ||
|
|
96d723a424 | ||
|
|
fb4b80862e | ||
|
|
bb2c8cfa63 | ||
|
|
ceffee9f22 | ||
|
|
a08ccd80dc | ||
|
|
3509eacdec | ||
|
|
d4496cba41 | ||
|
|
64f300d466 | ||
|
|
8de24a109a | ||
|
|
6d62e0e73c | ||
|
|
5da1c9c0d7 | ||
|
|
4fa9a9697b | ||
|
|
bf48a6e306 | ||
|
|
00ad452930 | ||
|
|
8df1f6406b | ||
|
|
a50960d66c | ||
|
|
e3a69c8856 | ||
|
|
672cb7e621 | ||
|
|
7dcccee1ae | ||
|
|
302dbe7359 | ||
|
|
b4df01965e | ||
|
|
5a8f1d542e | ||
|
|
10decda94e | ||
|
|
5b1f8d0eac | ||
|
|
2f6e1b703a | ||
|
|
5384022a59 | ||
|
|
b57974b462 | ||
|
|
3c36ba9a71 | ||
|
|
2ac463de90 | ||
|
|
be38c3e654 | ||
|
|
0f312a88bb | ||
|
|
422b7f3e09 | ||
|
|
800062d37e | ||
|
|
c1e8c7915c | ||
|
|
c1c1d87953 | ||
|
|
0382d22f7f | ||
|
|
f570424357 | ||
|
|
393c801426 | ||
|
|
6d63339b23 | ||
|
|
66d7c626e1 | ||
|
|
2246f33023 | ||
|
|
871362d469 | ||
|
|
cc1bf47f5a | ||
|
|
9c784398b3 | ||
|
|
21ce013df2 | ||
|
|
d20c2a3e3c | ||
|
|
8d1a2e6716 | ||
|
|
01f724959d | ||
|
|
3ae6290ec3 | ||
|
|
ba5ed27e74 | ||
|
|
ca737d8afa | ||
|
|
33a275e8bc | ||
|
|
60e808689c |
@@ -1,4 +1,4 @@
|
||||
# Open MCT [](http://www.apache.org/licenses/LICENSE-2.0) [](https://lgtm.com/projects/g/nasa/openmct/context:javascript) [](https://codecov.io/gh/nasa/openmct) [](https://percy.io/b2e34b17/openmct) [](https://www.npmjs.com/package/openmct)
|
||||
# Open MCT [](http://www.apache.org/licenses/LICENSE-2.0) [](https://codecov.io/gh/nasa/openmct) [](https://percy.io/b2e34b17/openmct) [](https://www.npmjs.com/package/openmct)
|
||||
|
||||
Open MCT (Open Mission Control Technologies) is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data.
|
||||
|
||||
@@ -98,7 +98,7 @@ To run the performance tests:
|
||||
The test suite is configured to all tests localed in `e2e/tests/` ending in `*.e2e.spec.js`. For more about the e2e test suite, please see the [README](./e2e/README.md)
|
||||
|
||||
### Security Tests
|
||||
Each commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/) and our overall security report is available on [LGTM](https://lgtm.com/projects/g/nasa/openmct/). The list of CWE coverage items is avaiable in the [CodeQL docs](https://codeql.github.com/codeql-query-help/javascript-cwe/). The CodeQL workflow is specified in the [CodeQL analysis file](./.github/workflows/codeql-analysis.yml) and the custom [CodeQL config](./.github/codeql/codeql-config.yml).
|
||||
Each commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/). The list of CWE coverage items is avaiable in the [CodeQL docs](https://codeql.github.com/codeql-query-help/javascript-cwe/). The CodeQL workflow is specified in the [CodeQL analysis file](./.github/workflows/codeql-analysis.yml) and the custom [CodeQL config](./.github/codeql/codeql-config.yml).
|
||||
|
||||
### Test Reporting and Code Coverage
|
||||
|
||||
|
||||
@@ -92,7 +92,9 @@ Read more about [Playwright Snapshots](https://playwright.dev/docs/test-snapshot
|
||||
- Snapshots need to be executed within the official Playwright container to ensure we're using the exact rendering platform in CI and locally. To do a valid comparison locally:
|
||||
|
||||
```sh
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:[GET THIS VERSION FROM OUR CIRCLECI CONFIG FILE]-focal /bin/bash
|
||||
// Replace {X.X.X} with the current Playwright version
|
||||
// from our package.json or circleCI configuration file
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-focal /bin/bash
|
||||
npm install
|
||||
npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot
|
||||
```
|
||||
@@ -104,17 +106,20 @@ When the `@snapshot` tests fail, they will need to be evaluated to determine if
|
||||
To compare a snapshot, run a test and open the html report with the 'Expected' vs 'Actual' screenshot. If the actual screenshot is preferred, then the source-controlled 'Expected' snapshots will need to be updated with the following scripts.
|
||||
|
||||
MacOS
|
||||
|
||||
```
|
||||
npm run test:e2e:updatesnapshots
|
||||
```
|
||||
|
||||
Linux/CI
|
||||
|
||||
```sh
|
||||
// Replace {X.X.X} with the current Playwright version
|
||||
// from our package.json or circleCI configuration file
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-focal /bin/bash
|
||||
npm install
|
||||
npm run test:e2e:updatesnapshots
|
||||
```
|
||||
|
||||
## Performance Testing
|
||||
|
||||
|
||||
51
e2e/playwright-a11y.config.js
Normal file
51
e2e/playwright-a11y.config.js
Normal file
@@ -0,0 +1,51 @@
|
||||
/* eslint-disable no-undef */
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig<{ theme: string }>} */
|
||||
const config = {
|
||||
retries: 0, // Visual tests should never retry due to snapshot comparison errors. Leaving as a shim
|
||||
testDir: 'tests/a11y',
|
||||
testMatch: '**/*.a11y.spec.js', // only run visual tests
|
||||
timeout: 60 * 1000,
|
||||
workers: 1, //Lower stress on Circle CI Agent for Visual tests https://github.com/percy/cli/discussions/1067
|
||||
webServer: {
|
||||
command: 'npm run start:coverage',
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: !process.env.CI
|
||||
},
|
||||
use: {
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: true, // this needs to remain headless to avoid visual changes due to GPU rendering in headed browsers
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'only-on-failure',
|
||||
trace: 'on-first-retry',
|
||||
video: 'off'
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome',
|
||||
use: {
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'chrome-snow-theme', //Runs the same visual tests but with snow-theme enabled
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
theme: 'snow'
|
||||
}
|
||||
}
|
||||
],
|
||||
reporter: [
|
||||
['list'],
|
||||
['junit', { outputFile: '../test-results/results.xml' }],
|
||||
['html', {
|
||||
open: 'never',
|
||||
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
|
||||
}]
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
@@ -73,7 +73,7 @@ const config = {
|
||||
open: 'never',
|
||||
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
|
||||
}],
|
||||
['junit', { outputFile: 'test-results/results.xml' }],
|
||||
['junit', { outputFile: '../test-results/results.xml' }],
|
||||
['github']
|
||||
]
|
||||
};
|
||||
|
||||
@@ -35,8 +35,8 @@ const config = {
|
||||
],
|
||||
reporter: [
|
||||
['list'],
|
||||
['junit', { outputFile: 'test-results/results.xml' }],
|
||||
['json', { outputFile: 'test-results/results.json' }]
|
||||
['junit', { outputFile: '../test-results/results.xml' }],
|
||||
['json', { outputFile: '../test-results/results.json' }]
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ const config = {
|
||||
],
|
||||
reporter: [
|
||||
['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
|
||||
|
||||
129
e2e/tests/a11y/default.a11y.spec.js
Normal file
129
e2e/tests/a11y/default.a11y.spec.js
Normal file
@@ -0,0 +1,129 @@
|
||||
/*****************************************************************************
|
||||
* 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, expandTreePaneItemByName } = require('../../appActions');
|
||||
const { injectAxe, checkA11y, getViolations, reportViolations } = require('axe-playwright');
|
||||
const { createHtmlReport } = require('axe-html-reporter');
|
||||
const AxeBuilder = require('@axe-core/playwright').default; // 1
|
||||
|
||||
test.describe('Visual - Default', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Go to baseURL and Hide Tree
|
||||
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
||||
await injectAxe(page);
|
||||
});
|
||||
|
||||
test.only('axe-playwright - basic reporting', async ({ page, theme }) => {
|
||||
// Verify that Create button is actionable
|
||||
await expect(page.locator('button:has-text("Create")')).toBeEnabled();
|
||||
|
||||
await checkA11y(page, null, {
|
||||
detailedReport: true,
|
||||
detailedReportOptions: { html: true }
|
||||
});
|
||||
});
|
||||
|
||||
test('axe-playwright - notebook', async ({ page, theme, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
// Create Notebook
|
||||
const notebook = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Notebook',
|
||||
name: "Embed Test Notebook"
|
||||
});
|
||||
// Create Overlay Plot
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot',
|
||||
name: "Dropped Overlay Plot"
|
||||
});
|
||||
|
||||
await expandTreePaneItemByName(page, myItemsFolderName);
|
||||
|
||||
await page.goto(notebook.url);
|
||||
await page.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', '.c-notebook__drag-area');
|
||||
|
||||
await percySnapshot(page, `Notebook w/ dropped embed (theme: ${theme})`);
|
||||
|
||||
await checkA11y(page, null, {
|
||||
detailedReport: true,
|
||||
detailedReportOptions: { html: true }
|
||||
});
|
||||
});
|
||||
|
||||
test('axe-playwright - html reporting with wcag2aa rules 2', async ({ page, theme }) => {
|
||||
// Verify that Create button is actionable
|
||||
await expect(page.locator('button:has-text("Create")')).toBeEnabled();
|
||||
|
||||
await checkA11y(page, null,
|
||||
{
|
||||
axeOptions: {
|
||||
runOnly: {
|
||||
type: 'tag',
|
||||
values: ['wcag2aa']
|
||||
}
|
||||
}
|
||||
},
|
||||
true, 'default',
|
||||
{
|
||||
outputDirPath: './results',
|
||||
outputDir: 'accessibility',
|
||||
reportFileName: 'accessibility-audit.html'
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('axe-playwright - html reporting with wcag2aa rules', async ({ page, theme }) => {
|
||||
// Verify that Create button is actionable
|
||||
await expect(page.locator('button:has-text("Create")')).toBeEnabled();
|
||||
|
||||
await checkA11y(page, null,
|
||||
{
|
||||
axeOptions: {
|
||||
runOnly: {
|
||||
type: 'tag',
|
||||
values: ['wcag2aa']
|
||||
}
|
||||
}
|
||||
},
|
||||
true, 'default',
|
||||
{ reporter: 'html' }
|
||||
);
|
||||
await createHtmlReport(
|
||||
{ results: { violations: args.violations } }
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
test('axeBuilder - should not have any automatically detectable accessibility issues', async ({ page }, testInfo) => {
|
||||
|
||||
const accessibilityScanResults = await new AxeBuilder({ page }).withTags(['wcag2aa']).analyze(); // 4
|
||||
|
||||
await testInfo.attach('accessibility-scan-results', {
|
||||
body: JSON.stringify(accessibilityScanResults, null, 2),
|
||||
contentType: 'application/json'
|
||||
});
|
||||
|
||||
expect(accessibilityScanResults.violations).toEqual([]); // 5
|
||||
});
|
||||
|
||||
});
|
||||
@@ -24,18 +24,51 @@
|
||||
This test suite is dedicated to tests which verify Open MCT's Notification functionality
|
||||
*/
|
||||
|
||||
// FIXME: Remove this eslint exception once tests are implemented
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
const { createDomainObjectWithDefaults, createNotification } = require('../../appActions');
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
|
||||
test.describe('Notifications List', () => {
|
||||
test.fixme('Notifications can be dismissed individually', async ({ page }) => {
|
||||
// Create some persistent notifications
|
||||
// Verify that they are present in the notifications list
|
||||
// Dismiss one of the notifications
|
||||
// Verify that it is no longer present in the notifications list
|
||||
// Verify that the other notifications are still present in the notifications list
|
||||
test('Notifications can be dismissed individually', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/6122'
|
||||
});
|
||||
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create an error notification with the message "Error message"
|
||||
await createNotification(page, {
|
||||
severity: 'error',
|
||||
message: 'Error message'
|
||||
});
|
||||
|
||||
// Create an alert notification with the message "Alert message"
|
||||
await createNotification(page, {
|
||||
severity: 'alert',
|
||||
message: 'Alert message'
|
||||
});
|
||||
|
||||
// Verify that there is a button with aria-label "Review 2 Notifications"
|
||||
expect(await page.locator('button[aria-label="Review 2 Notifications"]').count()).toBe(1);
|
||||
|
||||
// Click on button with aria-label "Review 2 Notifications"
|
||||
await page.click('button[aria-label="Review 2 Notifications"]');
|
||||
|
||||
// Click on button with aria-label="Dismiss notification of Error message"
|
||||
await page.click('button[aria-label="Dismiss notification of Error message"]');
|
||||
|
||||
// Verify there is no a notification (listitem) with the text "Error message" since it was dismissed
|
||||
expect(await page.locator('div[role="dialog"] div[role="listitem"]').innerText()).not.toContain('Error message');
|
||||
|
||||
// Verify there is still a notification (listitem) with the text "Alert message"
|
||||
expect(await page.locator('div[role="dialog"] div[role="listitem"]').innerText()).toContain('Alert message');
|
||||
|
||||
// Click on button with aria-label="Dismiss notification of Alert message"
|
||||
await page.click('button[aria-label="Dismiss notification of Alert message"]');
|
||||
|
||||
// Verify that there is no dialog since the notification overlay was closed automatically after all notifications were dismissed
|
||||
expect(await page.locator('div[role="dialog"]').count()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -181,10 +181,11 @@ 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
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Open a browser, navigate to the main page, and wait until all network events to resolve
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
});
|
||||
test('Can add a condition', async ({ page }) => {
|
||||
// Create a new condition set
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Condition Set',
|
||||
@@ -199,4 +200,50 @@ test.describe('Basic Condition Set Use', () => {
|
||||
const numOfUnnamedConditions = await page.locator('text=Unnamed Condition').count();
|
||||
expect(numOfUnnamedConditions).toEqual(1);
|
||||
});
|
||||
test('ConditionSet should display appropriate view options', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5924'
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: "Alpha Sine Wave Generator"
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: "Beta Sine Wave Generator"
|
||||
});
|
||||
const conditionSet1 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Condition Set',
|
||||
name: "Test Condition Set"
|
||||
});
|
||||
|
||||
// Change the object to edit mode
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.goto(conditionSet1.url);
|
||||
page.click('button[title="Show selected item in tree"]');
|
||||
// Add the Alpha & Beta Sine Wave Generator to the Condition Set and save changes
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const alphaGeneratorTreeItem = treePane.getByRole('treeitem', { name: "Alpha Sine Wave Generator"});
|
||||
const betaGeneratorTreeItem = treePane.getByRole('treeitem', { name: "Beta Sine Wave Generator"});
|
||||
const conditionCollection = page.locator('#conditionCollection');
|
||||
|
||||
await alphaGeneratorTreeItem.dragTo(conditionCollection);
|
||||
await betaGeneratorTreeItem.dragTo(conditionCollection);
|
||||
|
||||
const saveButtonLocator = page.locator('button[title="Save"]');
|
||||
await saveButtonLocator.click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.click('button[title="Change the current view"]');
|
||||
|
||||
await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden();
|
||||
await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible();
|
||||
await expect(page.getByRole('menuitem', { name: /Plot/ })).toBeVisible();
|
||||
await expect(page.getByRole('menuitem', { name: /Telemetry Table/ })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -131,13 +131,13 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
// This happens for 3 tags so 12 requests
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.hover('[aria-label="Tag"]:has-text("Driving")');
|
||||
await page.locator('[aria-label="Tag"]:has-text("Driving") ~ .c-completed-tag-deletion').click();
|
||||
await page.locator('[aria-label="Remove tag Driving"]').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")', {state: 'hidden'});
|
||||
await page.hover('[aria-label="Tag"]:has-text("Drilling")');
|
||||
await page.locator('[aria-label="Tag"]:has-text("Drilling") ~ .c-completed-tag-deletion').click();
|
||||
await page.locator('[aria-label="Remove tag Drilling"]').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")', {state: 'hidden'});
|
||||
page.hover('[aria-label="Tag"]:has-text("Science")');
|
||||
await page.locator('[aria-label="Tag"]:has-text("Science") ~ .c-completed-tag-deletion').click();
|
||||
await page.locator('[aria-label="Remove tag Science"]').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")', {state: 'hidden'});
|
||||
page.waitForLoadState('networkidle');
|
||||
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(12);
|
||||
|
||||
@@ -111,7 +111,7 @@ 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"]')).toContainText("Drilling");
|
||||
});
|
||||
test('Can search for tags', async ({ page }) => {
|
||||
test('Can search for tags and preview works properly', async ({ page }) => {
|
||||
await createNotebookEntryAndTags(page);
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
|
||||
@@ -126,13 +126,26 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq');
|
||||
await expect(page.locator('text=No results found')).toBeVisible();
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout'
|
||||
});
|
||||
|
||||
// Go back into edit mode for the display layout
|
||||
await page.locator('button[title="Edit"]').click();
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science");
|
||||
await page.getByText('Entry 0').click();
|
||||
await expect(page.locator('.js-preview-window')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Can delete tags', async ({ page }) => {
|
||||
await createNotebookEntryAndTags(page);
|
||||
// Delete Driving
|
||||
await page.hover('[aria-label="Tag"]:has-text("Driving")');
|
||||
await page.locator('[aria-label="Tag"]:has-text("Driving") ~ .c-completed-tag-deletion').click();
|
||||
await page.locator('[aria-label="Remove tag Driving"]').click();
|
||||
|
||||
await expect(page.locator('[aria-label="Tags Inspector"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Tags Inspector"]')).not.toContainText("Driving");
|
||||
@@ -231,4 +244,25 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
await expect(page.locator(entryLocator)).toContainText("Driving");
|
||||
}
|
||||
});
|
||||
test('Can cancel adding a tag', async ({ page }) => {
|
||||
await createNotebookAndEntry(page);
|
||||
|
||||
// Click on Annotations tab
|
||||
await page.locator('.c-inspector__tab', { hasText: "Annotations" }).click();
|
||||
|
||||
// Click on the "Add Tag" button
|
||||
await page.locator('button:has-text("Add Tag")').click();
|
||||
|
||||
// Click inside the AutoComplete field
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
|
||||
// Click on the "Tags" header (simulating a click outside the autocomplete)
|
||||
await page.locator('div.c-inspect-properties__header:has-text("Tags")').click();
|
||||
|
||||
// Verify there is a button with text "Add Tag"
|
||||
await expect(page.locator('button:has-text("Add Tag")')).toBeVisible();
|
||||
|
||||
// Verify the AutoComplete field is hidden
|
||||
await expect(page.locator('[placeholder="Type to select tag"]')).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,8 +35,9 @@ test.describe('Stacked Plot', () => {
|
||||
let swgC;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
stackedPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: "Stacked Plot"
|
||||
});
|
||||
@@ -82,6 +83,56 @@ test.describe('Stacked Plot', () => {
|
||||
await expect(swgCElementsPoolItem).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('Can reorder Stacked Plot items', async ({ page }) => {
|
||||
const swgAElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgA.name });
|
||||
const swgBElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgB.name });
|
||||
const swgCElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgC.name });
|
||||
|
||||
await page.goto(stackedPlot.url);
|
||||
|
||||
await page.click('button[title="Edit"]');
|
||||
|
||||
// Expand the elements pool vertically
|
||||
await page.locator('.l-pane__handle').nth(2).hover({ trial: true });
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(0, 100);
|
||||
await page.mouse.up();
|
||||
|
||||
const stackedPlotItem1 = page.locator('.c-plot--stacked-container').nth(0);
|
||||
const stackedPlotItem2 = page.locator('.c-plot--stacked-container').nth(1);
|
||||
const stackedPlotItem3 = page.locator('.c-plot--stacked-container').nth(2);
|
||||
|
||||
// assert initial plot order - [swgA, swgB, swgC]
|
||||
await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`);
|
||||
await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`);
|
||||
await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`);
|
||||
|
||||
// Drag and drop to reorder - [swgB, swgA, swgC]
|
||||
await swgBElementsPoolItem.dragTo(swgAElementsPoolItem);
|
||||
|
||||
// assert plot order after reorder - [swgB, swgA, swgC]
|
||||
await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`);
|
||||
await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`);
|
||||
await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`);
|
||||
|
||||
// Drag and drop to reorder - [swgB, swgC, swgA]
|
||||
await swgCElementsPoolItem.dragTo(swgAElementsPoolItem);
|
||||
|
||||
// assert plot order after second reorder - [swgB, swgC, swgA]
|
||||
await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`);
|
||||
await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`);
|
||||
await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`);
|
||||
|
||||
// Save (exit edit mode)
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('li[title="Save and Finish Editing"]').click();
|
||||
|
||||
// assert plot order persists after save - [swgB, swgC, swgA]
|
||||
await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`);
|
||||
await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`);
|
||||
await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`);
|
||||
});
|
||||
|
||||
test('Selecting a child plot while in browse and edit modes shows its properties in the inspector', async ({ page }) => {
|
||||
await page.goto(stackedPlot.url);
|
||||
|
||||
|
||||
223
e2e/tests/functional/plugins/plot/tagging.e2e.spec.js
Normal file
223
e2e/tests/functional/plugins/plot/tagging.e2e.spec.js
Normal file
@@ -0,0 +1,223 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Tests to verify plot tagging functionality.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults, setRealTimeMode, setFixedTimeMode } = require('../../../../appActions');
|
||||
|
||||
test.describe('Plot Tagging', () => {
|
||||
async function createTags({page, canvas, xEnd, yEnd}) {
|
||||
await canvas.hover({trial: true});
|
||||
|
||||
//Alt+Shift Drag Start to select some points to tag
|
||||
await page.keyboard.down('Alt');
|
||||
await page.keyboard.down('Shift');
|
||||
|
||||
await canvas.dragTo(canvas, {
|
||||
sourcePosition: {
|
||||
x: 1,
|
||||
y: 1
|
||||
},
|
||||
targetPosition: {
|
||||
x: xEnd,
|
||||
y: yEnd
|
||||
}
|
||||
});
|
||||
|
||||
//Alt Drag End
|
||||
await page.keyboard.up('Alt');
|
||||
await page.keyboard.up('Shift');
|
||||
|
||||
//Wait for canvas to stablize.
|
||||
await canvas.hover({trial: true});
|
||||
|
||||
// add some tags
|
||||
await page.getByText('Annotations').click();
|
||||
await page.getByRole('button', { name: /Add Tag/ }).click();
|
||||
await page.getByPlaceholder('Type to select tag').click();
|
||||
await page.getByText('Driving').click();
|
||||
|
||||
await page.getByRole('button', { name: /Add Tag/ }).click();
|
||||
await page.getByPlaceholder('Type to select tag').click();
|
||||
await page.getByText('Science').click();
|
||||
}
|
||||
|
||||
async function testTelemetryItem(page, canvas, telemetryItem) {
|
||||
// Check that telemetry item also received the tag
|
||||
await page.goto(telemetryItem.url);
|
||||
|
||||
await expect(page.getByText('No tags to display for this item')).toBeVisible();
|
||||
|
||||
//Wait for canvas to stablize.
|
||||
await canvas.hover({trial: true});
|
||||
|
||||
// click on the tagged plot point
|
||||
await canvas.click({
|
||||
position: {
|
||||
x: 325,
|
||||
y: 377
|
||||
}
|
||||
});
|
||||
|
||||
await expect(page.getByText('Science')).toBeVisible();
|
||||
await expect(page.getByText('Driving')).toBeHidden();
|
||||
}
|
||||
|
||||
async function basicTagsTests(page, canvas) {
|
||||
// Search for Science
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
|
||||
await expect(page.locator('[aria-label="Search Result"]').nth(0)).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Search Result"]').nth(0)).not.toContainText("Drilling");
|
||||
|
||||
// Delete Driving
|
||||
await page.hover('[aria-label="Tag"]:has-text("Driving")');
|
||||
await page.locator('[aria-label="Remove tag Driving"]').click();
|
||||
|
||||
await expect(page.locator('[aria-label="Tags Inspector"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Tags Inspector"]')).not.toContainText("Driving");
|
||||
|
||||
// Search for Driving
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv');
|
||||
await expect(page.getByText('No results found')).toBeVisible();
|
||||
|
||||
//Reload Page
|
||||
await Promise.all([
|
||||
page.reload(),
|
||||
page.waitForLoadState('networkidle')
|
||||
]);
|
||||
// wait for plot progress bar to disappear
|
||||
await page.locator('.l-view-section.c-progress-bar').waitFor({ state: 'detached' });
|
||||
|
||||
await page.getByText('Annotations').click();
|
||||
await expect(page.getByText('No tags to display for this item')).toBeVisible();
|
||||
|
||||
// click on the tagged plot point
|
||||
await canvas.click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100
|
||||
}
|
||||
});
|
||||
|
||||
await expect(page.getByText('Science')).toBeVisible();
|
||||
await expect(page.getByText('Driving')).toBeHidden();
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
});
|
||||
|
||||
test('Tags work with Overlay Plots', async ({ page }) => {
|
||||
//Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
|
||||
test.slow();
|
||||
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: "Overlay Plot"
|
||||
});
|
||||
|
||||
const alphaSineWave = await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: "Alpha Sine Wave",
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: "Beta Sine Wave",
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
await page.goto(overlayPlot.url);
|
||||
|
||||
let canvas = page.locator('canvas').nth(1);
|
||||
|
||||
// Switch to real-time mode
|
||||
// Adding tags should pause the plot
|
||||
await setRealTimeMode(page);
|
||||
|
||||
await createTags({
|
||||
page,
|
||||
canvas,
|
||||
xEnd: 700,
|
||||
yEnd: 480
|
||||
});
|
||||
|
||||
await setFixedTimeMode(page);
|
||||
|
||||
// changing to fixed time mode rebuilds canvas?
|
||||
canvas = page.locator('canvas').nth(1);
|
||||
|
||||
await basicTagsTests(page, canvas);
|
||||
await testTelemetryItem(page, canvas, alphaSineWave);
|
||||
});
|
||||
|
||||
test('Tags work with Plot View of telemetry items', async ({ page }) => {
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator"
|
||||
});
|
||||
|
||||
const canvas = page.locator('canvas').nth(1);
|
||||
await createTags({
|
||||
page,
|
||||
canvas,
|
||||
xEnd: 700,
|
||||
yEnd: 480
|
||||
});
|
||||
await basicTagsTests(page, canvas);
|
||||
});
|
||||
|
||||
test('Tags work with Stacked Plots', async ({ page }) => {
|
||||
const stackedPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: "Stacked Plot"
|
||||
});
|
||||
|
||||
const alphaSineWave = await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: "Alpha Sine Wave",
|
||||
parent: stackedPlot.uuid
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: "Sine Wave Generator",
|
||||
name: "Beta Sine Wave",
|
||||
parent: stackedPlot.uuid
|
||||
});
|
||||
|
||||
await page.goto(stackedPlot.url);
|
||||
|
||||
const canvas = page.locator('canvas').nth(1);
|
||||
|
||||
await createTags({
|
||||
page,
|
||||
canvas,
|
||||
xEnd: 700,
|
||||
yEnd: 215
|
||||
});
|
||||
await basicTagsTests(page, canvas);
|
||||
await testTelemetryItem(page, canvas, alphaSineWave);
|
||||
});
|
||||
});
|
||||
@@ -22,10 +22,14 @@
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures.js');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions.js');
|
||||
const { waitForAnimations } = require('../../baseFixtures.js');
|
||||
|
||||
test.describe('Recent Objects', () => {
|
||||
/** @type {import('@playwright/test').Locator} */
|
||||
let recentObjectsList;
|
||||
/** @type {import('@playwright/test').Locator} */
|
||||
let clock;
|
||||
/** @type {import('@playwright/test').Locator} */
|
||||
let folderA;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
@@ -45,19 +49,16 @@ test.describe('Recent Objects', () => {
|
||||
});
|
||||
|
||||
// Drag the Recent Objects panel up a bit
|
||||
await page.locator('div:nth-child(2) > .l-pane__handle').hover();
|
||||
await page.locator('.l-pane.l-pane--vertical-handle-before', {
|
||||
hasText: 'Recently Viewed'
|
||||
}).locator('.l-pane__handle').hover();
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(0, 100);
|
||||
await page.mouse.up();
|
||||
});
|
||||
test('Recent Objects CRUD operations', async ({ page }) => {
|
||||
test('Navigated objects show up in recents, object renames and deletions are reflected', async ({ page }) => {
|
||||
// Verify that both created objects appear in the list and are in the correct order
|
||||
expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem').nth(0).getByText(clock.name)).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem').nth(1).getByText(folderA.name)).toBeTruthy();
|
||||
assertInitialRecentObjectsListState();
|
||||
|
||||
// Navigate to the folder by clicking on the main object name in the recent objects list item
|
||||
await page.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click();
|
||||
@@ -72,7 +73,7 @@ test.describe('Recent Objects', () => {
|
||||
|
||||
// Verify rename has been applied in recent objects list item and objects paths
|
||||
expect(await page.getByRole('navigation', {
|
||||
name: `${clock.name} Breadcrumb`
|
||||
name: clock.name
|
||||
}).locator('a').filter({
|
||||
hasText: folderA.name
|
||||
}).count()).toBeGreaterThan(0);
|
||||
@@ -102,31 +103,153 @@ test.describe('Recent Objects', () => {
|
||||
// Navigate to the folder by clicking on its entry in the Clock's breadcrumb
|
||||
const waitForFolderNavigation = page.waitForURL(`**/${folderA.uuid}?*`);
|
||||
await page.getByRole('navigation', {
|
||||
name: `${clock.name} Breadcrumb`
|
||||
name: clock.name
|
||||
}).locator('a').filter({
|
||||
hasText: folderA.name
|
||||
}).click();
|
||||
|
||||
// Verify that the hash URL updates correctly
|
||||
await waitForFolderNavigation;
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
expect(page.url()).toMatch(new RegExp(`.*${folderA.uuid}\?.*`));
|
||||
expect(page.url()).toMatch(new RegExp(`.*${folderA.uuid}?.*`));
|
||||
|
||||
// Navigate to My Items by clicking on its entry in the Clock's breadcrumb
|
||||
const waitForMyItemsNavigation = page.waitForURL(`**/mine?*`);
|
||||
await page.getByRole('navigation', {
|
||||
name: `${clock.name} Breadcrumb`
|
||||
name: clock.name
|
||||
}).locator('a').filter({
|
||||
hasText: myItemsFolderName
|
||||
}).click();
|
||||
|
||||
// Verify that the hash URL updates correctly
|
||||
await waitForMyItemsNavigation;
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
expect(page.url()).toMatch(new RegExp(`.*mine\?.*`));
|
||||
expect(page.url()).toMatch(new RegExp(`.*mine?.*`));
|
||||
});
|
||||
test.fixme("Clicking on the 'target button' scrolls the object into view in the tree and highlights it", async ({ page }) => {
|
||||
test("Clicking on the 'target button' scrolls the object into view in the tree and highlights it", async ({ page }) => {
|
||||
const clockTreeItem = page.getByRole('tree', { name: 'Main Tree'}).getByRole('treeitem', { name: clock.name });
|
||||
const folderTreeItem = page.getByRole('tree', { name: 'Main Tree'})
|
||||
.getByRole('treeitem', {
|
||||
name: folderA.name,
|
||||
expanded: true
|
||||
});
|
||||
|
||||
// Click the "Target" button for the Clock which is nested in a folder
|
||||
await page.getByRole('button', { name: `Open and scroll to ${clock.name}`}).click();
|
||||
|
||||
// Assert that the Clock parent folder has expanded and the Clock is visible)
|
||||
await expect(folderTreeItem.locator('.c-disclosure-triangle')).toHaveClass(/--expanded/);
|
||||
await expect(clockTreeItem).toBeVisible();
|
||||
|
||||
// Assert that the Clock treeitem is highlighted
|
||||
await expect(clockTreeItem.locator('.c-tree__item')).toHaveClass(/is-targeted-item/);
|
||||
|
||||
// Wait for highlight animation to end
|
||||
await waitForAnimations(clockTreeItem.locator('.c-tree__item'));
|
||||
|
||||
// Assert that the Clock treeitem is no longer highlighted
|
||||
await expect(clockTreeItem.locator('.c-tree__item')).not.toHaveClass(/is-targeted-item/);
|
||||
});
|
||||
test.fixme("Tests for context menu actions from recent objects", async ({ page }) => {
|
||||
test("Persists on refresh", async ({ page }) => {
|
||||
assertInitialRecentObjectsListState();
|
||||
await page.reload();
|
||||
assertInitialRecentObjectsListState();
|
||||
});
|
||||
test("Displays objects and aliases uniquely", async ({ page }) => {
|
||||
const mainTree = page.getByRole('tree', { name: 'Main Tree'});
|
||||
|
||||
// Navigate to the clock and reveal it in the tree
|
||||
await page.goto(clock.url);
|
||||
await page.getByTitle('Show selected item in tree').click();
|
||||
|
||||
// Right click the clock and create an alias using the "link" context menu action
|
||||
const clockTreeItem = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
}).getByRole('treeitem', {
|
||||
name: clock.name
|
||||
});
|
||||
await clockTreeItem.click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.getByRole('menuitem', {
|
||||
name: /Create Link/
|
||||
}).click();
|
||||
await page.getByRole('tree', { name: 'Create Modal Tree'}).getByRole('treeitem').first().click();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
// Click the newly created object alias in the tree
|
||||
await mainTree.getByRole('treeitem', {
|
||||
name: new RegExp(clock.name)
|
||||
}).filter({
|
||||
has: page.locator('.is-alias')
|
||||
}).click();
|
||||
|
||||
// Assert that two recent objects are displayed and one of them is an alias
|
||||
expect(await recentObjectsList.getByRole('listitem', { name: clock.name }).count()).toBe(2);
|
||||
expect(await recentObjectsList.locator('.is-alias').count()).toBe(1);
|
||||
|
||||
// Assert that the alias and the original's breadcrumbs are different
|
||||
const clockBreadcrumbs = recentObjectsList.getByRole('listitem', {name: clock.name}).getByRole('navigation');
|
||||
expect(await clockBreadcrumbs.count()).toBe(2);
|
||||
expect(await clockBreadcrumbs.nth(0).innerText()).not.toEqual(await clockBreadcrumbs.nth(1).innerText());
|
||||
});
|
||||
test("Enforces a limit of 20 recent objects", async ({ page }) => {
|
||||
// Creating 21 objects takes a while, so increase the timeout
|
||||
test.slow();
|
||||
|
||||
// Assert that the list initially contains 3 objects (clock, folder, my items)
|
||||
expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(3);
|
||||
|
||||
let lastFolder;
|
||||
let lastClock;
|
||||
// Create 19 more objects (3 in beforeEach() + 18 new = 21 total)
|
||||
for (let i = 0; i < 9; i++) {
|
||||
lastFolder = await createDomainObjectWithDefaults(page, {
|
||||
type: "Folder",
|
||||
parent: lastFolder?.uuid
|
||||
});
|
||||
lastClock = await createDomainObjectWithDefaults(page, {
|
||||
type: "Clock",
|
||||
parent: lastFolder?.uuid
|
||||
});
|
||||
}
|
||||
|
||||
// Assert that the list contains 20 objects
|
||||
expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(20);
|
||||
|
||||
// Collapse the tree
|
||||
await page.getByTitle("Collapse all tree items").click();
|
||||
const lastFolderTreeItem = page.getByRole('tree', { name: 'Main Tree'})
|
||||
.getByRole('treeitem', {
|
||||
name: lastFolder.name,
|
||||
expanded: true
|
||||
});
|
||||
const lastClockTreeItem = page.getByRole('tree', { name: 'Main Tree'})
|
||||
.getByRole('treeitem', {
|
||||
name: lastClock.name
|
||||
});
|
||||
|
||||
// Test "Open and Scroll To" in a deeply nested tree, while we're here
|
||||
await page.getByRole('button', { name: `Open and scroll to ${lastClock.name}`}).click();
|
||||
|
||||
// Assert that the Clock parent folder has expanded and the Clock is visible)
|
||||
await expect(lastFolderTreeItem.locator('.c-disclosure-triangle')).toHaveClass(/--expanded/);
|
||||
await expect(lastClockTreeItem).toBeVisible();
|
||||
|
||||
// Assert that the Clock treeitem is highlighted
|
||||
await expect(lastClockTreeItem.locator('.c-tree__item')).toHaveClass(/is-targeted-item/);
|
||||
|
||||
// Wait for highlight animation to end
|
||||
await waitForAnimations(lastClockTreeItem.locator('.c-tree__item'));
|
||||
|
||||
// Assert that the Clock treeitem is no longer highlighted
|
||||
await expect(lastClockTreeItem.locator('.c-tree__item')).not.toHaveClass(/is-targeted-item/);
|
||||
});
|
||||
|
||||
function assertInitialRecentObjectsListState() {
|
||||
expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem').nth(0).getByText(clock.name)).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem').nth(1).getByText(folderA.name)).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -28,6 +28,14 @@ const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
const { v4: uuid } = require('uuid');
|
||||
|
||||
test.describe('Grand Search', () => {
|
||||
const searchResultSelector = '.c-gsearch-result__title';
|
||||
const searchResultDropDownSelector = '.c-gsearch__results';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto("./", { waitUntil: "networkidle" });
|
||||
});
|
||||
|
||||
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
@@ -89,15 +97,8 @@ test.describe('Grand Search', () => {
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText(`Clock C ${myItemsFolderName} Red Folder Blue Folder`);
|
||||
await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText(`Clock D ${myItemsFolderName} Red Folder Blue Folder`);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Search Tests @unstable", () => {
|
||||
const searchResultSelector = '.c-gsearch-result__title';
|
||||
|
||||
test('Validate empty search result', async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto("./", { waitUntil: "networkidle" });
|
||||
|
||||
// Invalid search for objects
|
||||
await page.type("input[type=search]", 'not found');
|
||||
|
||||
@@ -105,7 +106,7 @@ test.describe("Search Tests @unstable", () => {
|
||||
await waitForSearchCompletion(page);
|
||||
|
||||
// Get the search results
|
||||
const searchResults = await page.locator(searchResultSelector);
|
||||
const searchResults = page.locator(searchResultSelector);
|
||||
|
||||
// Verify that no results are found
|
||||
expect(await searchResults.count()).toBe(0);
|
||||
@@ -115,9 +116,6 @@ test.describe("Search Tests @unstable", () => {
|
||||
});
|
||||
|
||||
test('Validate single object in search result @couchdb', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto("./", { waitUntil: "networkidle" });
|
||||
|
||||
// Create a folder object
|
||||
const folderName = uuid();
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
@@ -139,21 +137,56 @@ test.describe("Search Tests @unstable", () => {
|
||||
await expect(searchResults).toHaveText(folderName);
|
||||
});
|
||||
|
||||
test('Search results are debounced @couchdb', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/6179'
|
||||
});
|
||||
await createObjectsForSearch(page);
|
||||
|
||||
let networkRequests = [];
|
||||
page.on('request', (request) => {
|
||||
const searchRequest = request.url().endsWith('_find');
|
||||
const fetchRequest = request.resourceType() === 'fetch';
|
||||
if (searchRequest && fetchRequest) {
|
||||
networkRequests.push(request);
|
||||
}
|
||||
});
|
||||
|
||||
// Full search for object
|
||||
await page.type("input[type=search]", 'Clock', { delay: 100 });
|
||||
|
||||
// Wait for search to finish
|
||||
await waitForSearchCompletion(page);
|
||||
|
||||
// Network requests for the composite telemetry with multiple items should be:
|
||||
// 1. batched request for latest telemetry using the bulk API
|
||||
expect(networkRequests.length).toBe(1);
|
||||
|
||||
const searchResultDropDown = await page.locator(searchResultDropDownSelector);
|
||||
|
||||
await expect(searchResultDropDown).toContainText('Clock A');
|
||||
});
|
||||
|
||||
test("Validate multiple objects in search results return partial matches", async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/4667'
|
||||
});
|
||||
|
||||
// Go to baseURL
|
||||
await page.goto("/", { waitUntil: "networkidle" });
|
||||
|
||||
// Create folder objects
|
||||
const folderName = "e928a26e-e924-4ea0";
|
||||
const folderName1 = "e928a26e-e924-4ea0";
|
||||
const folderName2 = "e928a26e-e924-4001";
|
||||
|
||||
await createFolderObject(page, folderName);
|
||||
await createFolderObject(page, folderName2);
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: folderName1
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: folderName2
|
||||
});
|
||||
|
||||
// Partial search for objects
|
||||
await page.type("input[type=search]", 'e928a26e');
|
||||
@@ -161,36 +194,22 @@ test.describe("Search Tests @unstable", () => {
|
||||
// Wait for search to finish
|
||||
await waitForSearchCompletion(page);
|
||||
|
||||
// Get the search results
|
||||
const searchResults = await page.locator(searchResultSelector);
|
||||
const searchResultDropDown = await page.locator(searchResultDropDownSelector);
|
||||
|
||||
// Verify that the search result/s correctly match the search query
|
||||
await expect(searchResultDropDown).toContainText(folderName1);
|
||||
await expect(searchResultDropDown).toContainText(folderName2);
|
||||
|
||||
// Get the search results
|
||||
const searchResults = page.locator(searchResultSelector);
|
||||
// Verify that two results are found
|
||||
expect(await searchResults.count()).toBe(2);
|
||||
await expect(await searchResults.first()).toHaveText(folderName);
|
||||
await expect(await searchResults.last()).toHaveText(folderName2);
|
||||
});
|
||||
});
|
||||
|
||||
async function createFolderObject(page, folderName) {
|
||||
// Open Create menu
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// Select Folder object
|
||||
await page.locator('text=Folder').nth(1).click();
|
||||
|
||||
// Click folder title to enter edit mode
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
||||
|
||||
// Enter folder name
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folderName);
|
||||
|
||||
// Create folder object
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
}
|
||||
|
||||
async function waitForSearchCompletion(page) {
|
||||
// Wait loading spinner to disappear
|
||||
await page.waitForSelector('.c-tree-and-search__loading', { state: 'detached' });
|
||||
await page.waitForSelector('.search-finished');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,9 +217,6 @@ async function waitForSearchCompletion(page) {
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function createObjectsForSearch(page) {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
const redFolder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Red Folder'
|
||||
|
||||
68
e2e/tests/visual/addTag.visual.spec.js
Normal file
68
e2e/tests/visual/addTag.visual.spec.js
Normal file
@@ -0,0 +1,68 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* This test is dedicated to test the blur behavior of the add tag button.
|
||||
*/
|
||||
|
||||
const { test } = require("../../pluginFixtures");
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
|
||||
test.describe("Visual - Check blur of Add Tag button", () => {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Open a browser, navigate to the main page, and wait until all network events to resolve
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
});
|
||||
|
||||
test("Blur 'Add tag'", async ({ page, theme }) => {
|
||||
createDomainObjectWithDefaults(page, { type: 'Notebook' });
|
||||
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = 0`;
|
||||
await page.locator(entryLocator).click();
|
||||
await page.locator(entryLocator).fill(`Entry 0`);
|
||||
await page.locator(entryLocator).press('Enter');
|
||||
|
||||
// Click on Annotations tab
|
||||
await page.locator('.c-inspector__tab', { hasText: "Annotations" }).click();
|
||||
|
||||
// Take snapshot of the notebook with the Annotations tab opened
|
||||
await percySnapshot(page, `Notebook Annotation (theme: '${theme}')`);
|
||||
|
||||
// Click on the "Add Tag" button
|
||||
await page.locator('button:has-text("Add Tag")').click();
|
||||
|
||||
// Take snapshot of the notebook with the AutoComplete field visible
|
||||
await percySnapshot(page, `Notebook Add Tag (theme: '${theme}')`);
|
||||
|
||||
// Click inside the AutoComplete field
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
|
||||
// Click on the "Tags" header (simulating a click outside the autocomplete field)
|
||||
await page.locator('div.c-inspect-properties__header:has-text("Tags")').click();
|
||||
|
||||
// Take snapshot of the notebook with the AutoComplete field hidden and with the "Add Tag" button visible
|
||||
await percySnapshot(page, `Notebook Annotation de-select blur (theme: '${theme}')`);
|
||||
});
|
||||
});
|
||||
@@ -20,11 +20,23 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import availableTags from './tags.json';
|
||||
|
||||
/**
|
||||
@typedef {{
|
||||
namespaceToSaveAnnotations: string
|
||||
}} TagsPluginOptions
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {TagsPluginOptions} options
|
||||
* @returns {function} The plugin install function
|
||||
*/
|
||||
export default function exampleTagsPlugin() {
|
||||
export default function exampleTagsPlugin(options) {
|
||||
return function install(openmct) {
|
||||
if (options?.namespaceToSaveAnnotations) {
|
||||
openmct.annotation.setNamespaceToSaveAnnotations(options?.namespaceToSaveAnnotations);
|
||||
}
|
||||
|
||||
Object.keys(availableTags.tags).forEach(tagKey => {
|
||||
const tagDefinition = availableTags.tags[tagKey];
|
||||
openmct.annotation.defineTag(tagKey, tagDefinition);
|
||||
|
||||
16
package.json
16
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.1.6",
|
||||
"version": "2.2.0-SNAPSHOT",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.18.9",
|
||||
@@ -20,8 +20,8 @@
|
||||
"d3-axis": "3.0.0",
|
||||
"d3-scale": "3.3.0",
|
||||
"d3-selection": "3.0.0",
|
||||
"eslint": "8.32.0",
|
||||
"eslint-plugin-compat": "4.0.2",
|
||||
"eslint": "8.35.0",
|
||||
"eslint-plugin-compat": "4.1.1",
|
||||
"eslint-plugin-playwright": "0.12.0",
|
||||
"eslint-plugin-vue": "9.9.0",
|
||||
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
|
||||
@@ -47,20 +47,23 @@
|
||||
"mini-css-extract-plugin": "2.7.2",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"moment-timezone": "0.5.40",
|
||||
"moment-timezone": "0.5.41",
|
||||
"nyc": "15.1.0",
|
||||
"painterro": "1.2.78",
|
||||
"playwright-core": "1.29.0",
|
||||
"axe-playwright": "1.2.3",
|
||||
"axe-html-reporter": "2.2.3",
|
||||
"@axe-core/playwright": "4.6.0",
|
||||
"plotly.js-basic-dist": "2.17.0",
|
||||
"plotly.js-gl2d-dist": "2.17.1",
|
||||
"printj": "1.3.1",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sanitize-html": "2.8.1",
|
||||
"sanitize-html": "2.10.0",
|
||||
"sass": "1.57.1",
|
||||
"sass-loader": "13.2.0",
|
||||
"sinon": "15.0.1",
|
||||
"style-loader": "^3.3.1",
|
||||
"typescript": "4.9.4",
|
||||
"typescript": "4.9.5",
|
||||
"uuid": "9.0.0",
|
||||
"vue": "2.6.14",
|
||||
"vue-eslint-parser": "9.1.0",
|
||||
@@ -86,6 +89,7 @@
|
||||
"test": "karma start",
|
||||
"test:debug": "KARMA_DEBUG=true karma start",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:e2e:a11y": "npx playwright test --config=e2e/playwright-a11y.config.js --project=chrome --grep-invert @unstable",
|
||||
"test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb",
|
||||
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb\"",
|
||||
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",
|
||||
|
||||
@@ -84,6 +84,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
super();
|
||||
this.openmct = openmct;
|
||||
this.availableTags = {};
|
||||
this.namespaceToSaveAnnotations = '';
|
||||
|
||||
this.ANNOTATION_TYPES = ANNOTATION_TYPES;
|
||||
this.ANNOTATION_TYPE = ANNOTATION_TYPE;
|
||||
@@ -139,7 +140,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
const domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
const originalPathObjects = await this.openmct.objects.getOriginalPath(domainObjectKeyString);
|
||||
const originalContextPath = this.openmct.objects.getRelativePath(originalPathObjects);
|
||||
const namespace = domainObject.identifier.namespace;
|
||||
const namespace = this.namespaceToSaveAnnotations;
|
||||
const type = 'annotation';
|
||||
const typeDefinition = this.openmct.types.get(type);
|
||||
const definition = typeDefinition.definition;
|
||||
@@ -198,6 +199,14 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
this.availableTags[tagKey] = tagsDefinition;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method setNamespaceToSaveAnnotations
|
||||
* @param {String} namespace the namespace to save new annotations to
|
||||
*/
|
||||
setNamespaceToSaveAnnotations(namespace) {
|
||||
this.namespaceToSaveAnnotations = namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method isAnnotation
|
||||
* @param {DomainObject} domainObject the domainObject in question
|
||||
|
||||
@@ -26,6 +26,7 @@ import ExampleTagsPlugin from "../../../example/exampleTags/plugin";
|
||||
describe("The Annotation API", () => {
|
||||
let openmct;
|
||||
let mockObjectProvider;
|
||||
let mockImmutableObjectProvider;
|
||||
let mockDomainObject;
|
||||
let mockFolderObject;
|
||||
let mockAnnotationObject;
|
||||
@@ -89,6 +90,23 @@ describe("The Annotation API", () => {
|
||||
mockObjectProvider.create.and.returnValue(Promise.resolve(true));
|
||||
mockObjectProvider.update.and.returnValue(Promise.resolve(true));
|
||||
|
||||
mockImmutableObjectProvider = jasmine.createSpyObj("mock immutable provider", [
|
||||
"get"
|
||||
]);
|
||||
// eslint-disable-next-line require-await
|
||||
mockImmutableObjectProvider.get = async (identifier) => {
|
||||
if (identifier.key === mockDomainObject.identifier.key) {
|
||||
return mockDomainObject;
|
||||
} else if (identifier.key === mockAnnotationObject.identifier.key) {
|
||||
return mockAnnotationObject;
|
||||
} else if (identifier.key === mockFolderObject.identifier.key) {
|
||||
return mockFolderObject;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
openmct.objects.addProvider('immutableProvider', mockImmutableObjectProvider);
|
||||
openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
@@ -115,6 +133,22 @@ describe("The Annotation API", () => {
|
||||
expect(annotationObject).toBeDefined();
|
||||
expect(annotationObject.type).toEqual('annotation');
|
||||
});
|
||||
it("can create annotations if domain object is immutable", async () => {
|
||||
mockDomainObject.identifier.namespace = 'immutableProvider';
|
||||
const annotationCreationArguments = {
|
||||
name: 'Test Annotation',
|
||||
domainObject: mockDomainObject,
|
||||
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
|
||||
tags: ['sometag'],
|
||||
contentText: "fooContext",
|
||||
targetDomainObjects: [mockDomainObject],
|
||||
targets: {'fooTarget': {}}
|
||||
};
|
||||
openmct.annotation.setNamespaceToSaveAnnotations('fooNameSpace');
|
||||
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
|
||||
expect(annotationObject).toBeDefined();
|
||||
expect(annotationObject.type).toEqual('annotation');
|
||||
});
|
||||
it("fails if annotation is an unknown type", async () => {
|
||||
try {
|
||||
await openmct.annotation.create('Garbage Annotation', mockDomainObject, 'garbageAnnotation', ['sometag'], "fooContext", {'fooTarget': {}});
|
||||
@@ -122,6 +156,40 @@ describe("The Annotation API", () => {
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
});
|
||||
it("fails if annotation if given an immutable namespace to save to", async () => {
|
||||
try {
|
||||
const annotationCreationArguments = {
|
||||
name: 'Test Annotation',
|
||||
domainObject: mockDomainObject,
|
||||
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
|
||||
tags: ['sometag'],
|
||||
contentText: "fooContext",
|
||||
targetDomainObjects: [mockDomainObject],
|
||||
targets: {'fooTarget': {}}
|
||||
};
|
||||
openmct.annotation.setNamespaceToSaveAnnotations('nameespaceThatDoesNotExist');
|
||||
await openmct.annotation.create(annotationCreationArguments);
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
});
|
||||
it("fails if annotation if given an undefined namespace to save to", async () => {
|
||||
try {
|
||||
const annotationCreationArguments = {
|
||||
name: 'Test Annotation',
|
||||
domainObject: mockDomainObject,
|
||||
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
|
||||
tags: ['sometag'],
|
||||
contentText: "fooContext",
|
||||
targetDomainObjects: [mockDomainObject],
|
||||
targets: {'fooTarget': {}}
|
||||
};
|
||||
openmct.annotation.setNamespaceToSaveAnnotations('immutableProvider');
|
||||
await openmct.annotation.create(annotationCreationArguments);
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tagging", () => {
|
||||
@@ -149,13 +217,6 @@ describe("The Annotation API", () => {
|
||||
openmct.annotation.deleteAnnotations([annotationObject]);
|
||||
expect(annotationObject._deleted).toBeTrue();
|
||||
});
|
||||
it("throws an error if deleting non-existent tag", async () => {
|
||||
const annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||
expect(annotationObject).toBeDefined();
|
||||
expect(() => {
|
||||
openmct.annotation.removeAnnotationTag(annotationObject, 'ThisTagShouldNotExist');
|
||||
}).toThrow();
|
||||
});
|
||||
it("can remove all tags", async () => {
|
||||
const annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||
expect(annotationObject).toBeDefined();
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!hideOptions"
|
||||
v-if="!hideOptions && filteredOptions.length > 0"
|
||||
class="c-menu c-input--autocomplete__options"
|
||||
aria-label="Autocomplete Options"
|
||||
@blur="hideOptions = true"
|
||||
@@ -230,10 +230,10 @@ export default {
|
||||
this.showFilteredOptions = false;
|
||||
this.autocompleteInputElement.select();
|
||||
|
||||
if (this.hideOptions) {
|
||||
this.showOptions();
|
||||
} else {
|
||||
if (!this.hideOptions && this.filteredOptions.length > 0) {
|
||||
this.hideOptions = true;
|
||||
} else {
|
||||
this.showOptions();
|
||||
}
|
||||
|
||||
},
|
||||
@@ -242,6 +242,7 @@ export default {
|
||||
// dropdown is visible, this will collapse the dropdown.
|
||||
const clickedInsideAutocomplete = this.autocompleteInputAndArrow.contains(event.target);
|
||||
if (!clickedInsideAutocomplete && !this.hideOptions) {
|
||||
this.$emit('autoCompleteBlur');
|
||||
this.hideOptions = true;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -33,9 +33,12 @@ export default class LADTableViewProvider {
|
||||
canView(domainObject) {
|
||||
const supportsComposition = this.openmct.composition.supportsComposition(domainObject);
|
||||
const providesTelemetry = this.openmct.telemetry.isTelemetryObject(domainObject);
|
||||
const isLadTable = domainObject.type === 'LadTable';
|
||||
const isConditionSet = domainObject.type === 'conditionSet';
|
||||
|
||||
return domainObject.type === 'LadTable'
|
||||
|| (providesTelemetry && supportsComposition);
|
||||
return !isConditionSet
|
||||
&& (isLadTable
|
||||
|| (providesTelemetry && supportsComposition));
|
||||
}
|
||||
|
||||
canEdit(domainObject) {
|
||||
|
||||
@@ -121,7 +121,8 @@ describe("The URLTimeSettingsSynchronizer", () => {
|
||||
openmct.router.on('change:hash', resolveFunction);
|
||||
});
|
||||
|
||||
it("reset hash", (done) => {
|
||||
// disabling due to test flakiness
|
||||
xit("reset hash", (done) => {
|
||||
window.location.hash = oldHash;
|
||||
resolveFunction = () => {
|
||||
expect(window.location.hash).toBe(oldHash);
|
||||
|
||||
@@ -885,6 +885,10 @@ export default {
|
||||
});
|
||||
},
|
||||
focusElement() {
|
||||
if (this.isComposedInLayout) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.$el.focus();
|
||||
},
|
||||
|
||||
|
||||
@@ -246,7 +246,7 @@
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
opacity: 0.5;
|
||||
opacity: 1;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
:id="entry.id"
|
||||
class="c-ne__text c-ne__input"
|
||||
aria-label="Notebook Entry Input"
|
||||
tabindex="0"
|
||||
tabindex="-1"
|
||||
:contenteditable="canEdit"
|
||||
v-bind.prop="formattedText"
|
||||
@mouseover="checkEditability($event)"
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
class="w-messages c-overlay__messages"
|
||||
>
|
||||
<notification-message
|
||||
v-for="notification in notifications"
|
||||
:key="notification.model.timestamp"
|
||||
v-for="(notification, notificationIndex) in notifications"
|
||||
:key="notificationIndex"
|
||||
:close-overlay="closeOverlay"
|
||||
:notification="notification"
|
||||
:notifications-count="notifications.length"
|
||||
|
||||
@@ -327,7 +327,7 @@ export default {
|
||||
return this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
|
||||
},
|
||||
annotationViewingAndEditingAllowed() {
|
||||
// only allow annotations viewing/editing if plot is paused or in fixed time mode
|
||||
// only allow annotations viewing/editing if plot is paused or in fixed time mode
|
||||
return this.isFrozen || !this.isRealTime;
|
||||
},
|
||||
plotFirstLeftTickWidth() {
|
||||
@@ -1110,7 +1110,9 @@ export default {
|
||||
|
||||
if (event.altKey && !event.shiftKey) {
|
||||
return this.startPan(event);
|
||||
} else if (this.annotationViewingAndEditingAllowed && event.altKey && event.shiftKey) {
|
||||
} else if (event.altKey && event.shiftKey) {
|
||||
this.freeze();
|
||||
|
||||
return this.startMarquee(event, true);
|
||||
} else {
|
||||
return this.startMarquee(event, false);
|
||||
|
||||
@@ -241,7 +241,7 @@ export default {
|
||||
let oldComposition = this.compositionObjects.slice();
|
||||
|
||||
reorderPlan.forEach((reorder) => {
|
||||
this.compositionObjects[reorder.newIndex] = oldComposition[reorder.oldIndex];
|
||||
this.$set(this.compositionObjects, reorder.newIndex, oldComposition[reorder.oldIndex]);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -340,7 +340,8 @@ describe("the plugin", function () {
|
||||
expect(legend.length).toBe(6);
|
||||
});
|
||||
|
||||
it("Renders X-axis ticks for the telemetry object", () => {
|
||||
// disable due to flakiness
|
||||
xit("Renders X-axis ticks for the telemetry object", () => {
|
||||
let xAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-x .gl-plot-tick-wrapper");
|
||||
expect(xAxisElement.length).toBe(1);
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<ul
|
||||
v-if="orderedPath.length"
|
||||
class="c-location"
|
||||
:aria-label="`${domainObject.name} Breadcrumb`"
|
||||
:aria-label="`${domainObject.name}`"
|
||||
role="navigation"
|
||||
>
|
||||
<li
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div class="c-tag-applier has-tag-applier">
|
||||
<div class="c-tag-applier">
|
||||
<TagSelection
|
||||
v-for="(addedTag, index) in addedTags"
|
||||
:key="index"
|
||||
@@ -31,6 +31,7 @@
|
||||
:added-tags="addedTags"
|
||||
@tagRemoved="tagRemoved"
|
||||
@tagAdded="tagAdded"
|
||||
@tagBlurred="tagBlurred"
|
||||
/>
|
||||
<button
|
||||
v-show="!userAddingTag && !maxTagsAdded"
|
||||
@@ -161,10 +162,17 @@ export default {
|
||||
await this.openmct.annotation.deleteAnnotations(annotationsToDelete);
|
||||
this.$emit('tags-updated', annotationsToDelete);
|
||||
if (this.onTagChange) {
|
||||
this.userAddingTag = false;
|
||||
this.onTagChange(this.annotations);
|
||||
}
|
||||
}
|
||||
},
|
||||
tagBlurred() {
|
||||
// Remove last tag when user clicks outside of TagSelection
|
||||
this.addedTags.pop();
|
||||
// Hide TagSelection and show "Add Tag" button
|
||||
this.userAddingTag = false;
|
||||
},
|
||||
async tagAdded(newTag) {
|
||||
// Either undelete an annotation, or create one (1) new annotation
|
||||
let existingAnnotation = this.annotations.find((annotation) => {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div class="w-tag-wrapper">
|
||||
<div class="w-tag-wrapper has-tag-applier">
|
||||
<template v-if="newTag">
|
||||
<AutoCompleteField
|
||||
v-if="newTag"
|
||||
@@ -31,6 +31,7 @@
|
||||
class="c-tag-selection"
|
||||
:item-css-class="'icon-circle'"
|
||||
@onChange="tagSelected"
|
||||
@autoCompleteBlur="autoCompleteBlur"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -39,15 +40,17 @@
|
||||
:class="{'c-tag-edit': !readOnly}"
|
||||
:style="{ background: selectedBackgroundColor, color: selectedForegroundColor }"
|
||||
>
|
||||
<button
|
||||
v-show="!readOnly"
|
||||
class="c-completed-tag-deletion c-tag__remove-btn icon-x-in-circle"
|
||||
:style="{ textShadow: selectedBackgroundColor + ' 0 0 4px' }"
|
||||
:aria-label="`Remove tag ${selectedTagLabel}`"
|
||||
@click="removeTag"
|
||||
></button>
|
||||
<div
|
||||
class="c-tag__label"
|
||||
aria-label="Tag"
|
||||
>{{ selectedTagLabel }} </div>
|
||||
<button
|
||||
v-show="!readOnly"
|
||||
class="c-completed-tag-deletion c-tag__remove-btn icon-x-in-circle"
|
||||
@click="removeTag"
|
||||
></button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -158,6 +161,9 @@ export default {
|
||||
if (tagAdded) {
|
||||
this.$emit('tagAdded', tagAdded.id);
|
||||
}
|
||||
},
|
||||
autoCompleteBlur() {
|
||||
this.$emit('tagBlurred');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -93,17 +93,30 @@
|
||||
// Apply this class to all components that should trigger tag removal btn on hover
|
||||
&:hover {
|
||||
.c-tag {
|
||||
padding-right: 17px !important;
|
||||
@include userSelectNone();
|
||||
transition: $transOut;
|
||||
}
|
||||
|
||||
.c-tag__label {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.c-tag__remove-btn {
|
||||
//display: block;
|
||||
//margin-left: $interiorMarginSm;
|
||||
width: 1em;
|
||||
width: 1.3em;
|
||||
opacity: 0.8;
|
||||
padding: 2px !important;
|
||||
transition: $transOut;
|
||||
//transition-delay: 250ms;
|
||||
right: 5%;
|
||||
text-align: center;
|
||||
z-index: 2;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
|
||||
& ~ * {
|
||||
// This sibling selector further dims the label
|
||||
// to make the remove button stand out
|
||||
opacity: 0.4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +201,6 @@ export default {
|
||||
}
|
||||
},
|
||||
async loadAnnotationForTargetObject(target) {
|
||||
console.debug(`📝 Loading annotations for target`, target);
|
||||
const targetID = this.openmct.objects.makeKeyString(target.identifier);
|
||||
const allAnnotationsForTarget = await this.openmct.annotation.getAnnotations(target.identifier);
|
||||
const filteredAnnotationsForSelection = allAnnotationsForTarget.filter(annotation => {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
>
|
||||
<ul
|
||||
class="c-tree-and-search__tree c-tree c-tree__scrollable"
|
||||
aria-label="Recent Objects"
|
||||
>
|
||||
<recent-objects-list-item
|
||||
v-for="(recentObject) in recentObjects"
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
<div class="c-recentobjects-listitem__target-button">
|
||||
<button
|
||||
class="c-icon-button icon-target"
|
||||
:aria-label="`Open and scroll to ${domainObject.name}`"
|
||||
@click="openAndScrollTo(navigationPath)"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
|
||||
<script>
|
||||
import ObjectPath from '../../components/ObjectPath.vue';
|
||||
import PreviewAction from '../../preview/PreviewAction';
|
||||
import { identifierToString } from '../../../../src/tools/url';
|
||||
|
||||
export default {
|
||||
@@ -125,12 +126,25 @@ export default {
|
||||
return this.result.fullTagModels[0].foregroundColor;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.previewAction = new PreviewAction(this.openmct);
|
||||
this.previewAction.on('isVisible', this.togglePreviewState);
|
||||
},
|
||||
destroyed() {
|
||||
this.previewAction.off('isVisible', this.togglePreviewState);
|
||||
},
|
||||
methods: {
|
||||
clickedResult() {
|
||||
clickedResult(event) {
|
||||
const objectPath = this.domainObject.originalPath;
|
||||
let resultUrl = identifierToString(this.openmct, objectPath);
|
||||
if (this.openmct.editor.isEditing()) {
|
||||
event.preventDefault();
|
||||
this.preview(objectPath);
|
||||
} else {
|
||||
const resultUrl = identifierToString(this.openmct, objectPath);
|
||||
|
||||
this.openmct.router.navigate(resultUrl);
|
||||
}
|
||||
|
||||
this.openmct.router.navigate(resultUrl);
|
||||
if (this.result.annotationType === this.openmct.annotation.ANNOTATION_TYPES.PLOT_SPATIAL) {
|
||||
//wait a beat for the navigation
|
||||
setTimeout(() => {
|
||||
@@ -138,6 +152,11 @@ export default {
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
preview(objectPath) {
|
||||
if (this.previewAction.appliesTo(objectPath)) {
|
||||
this.previewAction.invoke(objectPath);
|
||||
}
|
||||
},
|
||||
clickedPlotAnnotation() {
|
||||
const targetDetails = {};
|
||||
const targetDomainObjects = {};
|
||||
|
||||
@@ -47,6 +47,8 @@
|
||||
import search from '../../components/search.vue';
|
||||
import SearchResultsDropDown from './SearchResultsDropDown.vue';
|
||||
|
||||
const SEARCH_DEBOUNCE_TIME = 200;
|
||||
|
||||
export default {
|
||||
name: 'GrandSearch',
|
||||
components: {
|
||||
@@ -59,11 +61,15 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
searchValue: '',
|
||||
debouncedSearchTimeoutID: null,
|
||||
searchLoading: false,
|
||||
annotationSearchResults: [],
|
||||
objectSearchResults: []
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.getSearchResults = this.debounceAsyncFunction(this.getSearchResults, SEARCH_DEBOUNCE_TIME);
|
||||
},
|
||||
destroyed() {
|
||||
document.body.removeEventListener('click', this.handleOutsideClick);
|
||||
},
|
||||
@@ -84,6 +90,7 @@ export default {
|
||||
if (this.searchValue) {
|
||||
await this.getSearchResults();
|
||||
} else {
|
||||
clearTimeout(this.debouncedSearchTimeoutID);
|
||||
const dropdownOptions = {
|
||||
searchLoading: this.searchLoading,
|
||||
searchValue: this.searchValue,
|
||||
@@ -93,6 +100,19 @@ export default {
|
||||
this.$refs.searchResultsDropDown.showResults(dropdownOptions);
|
||||
}
|
||||
},
|
||||
debounceAsyncFunction(functionToDebounce, debounceTime) {
|
||||
return (...args) => {
|
||||
clearTimeout(this.debouncedSearchTimeoutID);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.debouncedSearchTimeoutID = setTimeout(() => {
|
||||
functionToDebounce(...args)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}, debounceTime);
|
||||
});
|
||||
};
|
||||
},
|
||||
getPathsForObjects(objectsNeedingPaths) {
|
||||
return Promise.all(objectsNeedingPaths.map(async (domainObject) => {
|
||||
if (!domainObject) {
|
||||
|
||||
@@ -291,4 +291,16 @@ xdescribe("GrandSearch", () => {
|
||||
const previewWindow = document.querySelector('.js-preview-window');
|
||||
expect(previewWindow.innerText).toContain('Snapshot');
|
||||
});
|
||||
|
||||
it("should preview annotation search results in edit mode if annotation clicked", async () => {
|
||||
await grandSearchComponent.$children[0].searchEverything('Dri');
|
||||
grandSearchComponent._provided.openmct.router.path = [mockDisplayLayout];
|
||||
await Vue.nextTick();
|
||||
const annotationResults = document.querySelectorAll('[aria-label="Search Result"]');
|
||||
expect(annotationResults.length).toBe(1);
|
||||
expect(annotationResults[0].innerText).toContain('Driving');
|
||||
annotationResults[0].click();
|
||||
const previewWindow = document.querySelector('.js-preview-window');
|
||||
expect(previewWindow.innerText).toContain('Snapshot');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,11 +96,11 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
clickedResult(event) {
|
||||
const { objectPath } = this.result;
|
||||
if (this.openmct.editor.isEditing()) {
|
||||
event.preventDefault();
|
||||
this.preview();
|
||||
this.preview(objectPath);
|
||||
} else {
|
||||
const { objectPath } = this.result;
|
||||
let resultUrl = identifierToString(this.openmct, objectPath);
|
||||
|
||||
// Remove the vestigial 'ROOT' identifier from url if it exists
|
||||
@@ -114,8 +114,7 @@ export default {
|
||||
togglePreviewState(previewState) {
|
||||
this.$emit('preview-changed', previewState);
|
||||
},
|
||||
preview() {
|
||||
const { objectPath } = this.result;
|
||||
preview(objectPath) {
|
||||
if (this.previewAction.appliesTo(objectPath)) {
|
||||
this.previewAction.invoke(objectPath);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,10 @@
|
||||
v-show="resultsShown"
|
||||
class="c-gsearch__results-wrapper"
|
||||
>
|
||||
<div class="c-gsearch__results">
|
||||
<div
|
||||
class="c-gsearch__results"
|
||||
:class="{ 'search-finished' : !searchLoading }"
|
||||
>
|
||||
<div
|
||||
v-if="objectResults && objectResults.length"
|
||||
ref="objectResults"
|
||||
|
||||
Reference in New Issue
Block a user