Compare commits

..

43 Commits

Author SHA1 Message Date
Shefali
4d2891c35b Fix typo for event name 2023-02-13 12:01:37 -08:00
Shefali
fac2c233c1 Decouple removing the context for a view from refreshing the context of views to get their upstream contexts 2023-02-13 11:43:34 -08:00
dependabot[bot]
bf48a6e306 chore(deps-dev): bump eslint-plugin-compat from 4.0.2 to 4.1.1 (#6311)
Bumps [eslint-plugin-compat](https://github.com/amilajack/eslint-plugin-compat) from 4.0.2 to 4.1.1.
- [Release notes](https://github.com/amilajack/eslint-plugin-compat/releases)
- [Changelog](https://github.com/amilajack/eslint-plugin-compat/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amilajack/eslint-plugin-compat/compare/v4.0.2...v4.1.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-compat
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-13 10:34:36 -08:00
dependabot[bot]
00ad452930 chore(deps-dev): bump typescript from 4.9.4 to 4.9.5 (#6233)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.9.4 to 4.9.5.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.9.4...v4.9.5)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-13 10:16:33 -08:00
Jesse Mazzella
8df1f6406b docs: fix docker command and formatting (#6329)
- Fixes an inconsistency in the docs with regards to the docker command for spinning up a Playwright container

- Closes an unclosed markdown code block
2023-02-13 18:09:55 +01:00
Shefali Joshi
a50960d66c Remove console warn (#6320) 2023-02-10 13:47:46 -08:00
Jamie V
e3a69c8856 [Notebook] Fix link formatting on load, remove duplicate snapshot indicator (#6317)
* adding urlWhitelist to data so when/if it is updated it triggers link formatting

* removing dupe notebook and restricted notebook checks and just moving it to base notebook functionality install checks

* nullthing to see here
2023-02-10 12:08:58 -08:00
Charles Hacskaylo
672cb7e621 Prevent tabbing into Notebook entries (#6315)
Closes #6312
- Set entry inputs to `tabindex="-1"` to prevent tabbing into entry inputs.

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2023-02-09 23:51:38 +00:00
Khalid Adil
7dcccee1ae [TimeAPI] Fix independent time context check (#6308)
* Fix independent time context to check first object in path (self) for upstream content instead of last object in path

* Revert changes that don't allow unregistering independent time context

---------

Co-authored-by: Shefali <simplyrender@gmail.com>
2023-02-09 15:34:17 -08:00
Jesse Mazzella
302dbe7359 fix(#6268): show correct yAxis options upon series drag to another yAxis (#6301)
* fix: show yAxis properties when series is moved

- Shows the correct yAxis properties immediately when a series is moved to another yAxis

* test: check for correct yAxis properties

- refactor test to use unique autogenerated object names

- update Elements pool pane handle selector

* test: fix goto and move to beforeEach

* test: ☠️ disable GrandSearch and ApplicationRouterSpec ☠️

---------

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2023-02-09 11:58:03 -08:00
Scott Bell
b4df01965e Fix router test flakes (#6309)
* resolve conflicts
* disable flaky tests
2023-02-09 11:34:34 -08:00
Scott Bell
5a8f1d542e Allow tags files to define namespace to save annotations (#6274)
* allow tags files to define namespace to save annotations

* add tests

* typo in test name

* lint

* change param to objects object and remove debug
2023-02-08 14:48:03 +00:00
Shefali Joshi
10decda94e When auto scale is turned off, handle user specified range correctly (#6258)
* Fix typo when saving the user specified range
* Ensure range is specified when autoscale is turned off
* Don't redraw unless absolutely necessary
* Add 'stats' to the handled attributes for redrawing plots
* Handle x axis displayRange, marker shape and size to redraw
* If there are is no closest data point for a plot, skip annotation gathering
* Ensure that min and max user defined ranges are valid when autoscale is disabled. Otherwise, enable autoscale to true.
* Fix autoscale e2e test
* updated snapshot
* Update e2e/README.md
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-02-07 15:19:50 -08:00
Marcelo Arias
5b1f8d0eac Add cancel adding tag mechanism and fix persistent options visible (#6273)
* Add cancel adding tag mechanism and fix persistent options visible

* Add functional test for cancel adding a tag

* Create addtag.visual.spec.js and move createNotebookAndEntry to appActions.js

* Remove createDomainObjectWithDefaults function from tags.e2e.spec.js

* Integrate Percy snapshots and remove assertions

* Move createNotebookAndEntry function back to tags.e2e.spec.js

* Update locator of Annotations tab, split helper function and add test.beforeEach
2023-02-07 17:24:37 +00:00
Jamie V
2f6e1b703a [Staleness] Handle Overlay Plots in Stacked Plots and removing LAD Tables from LAD Table Sets (#6281)
* add handling for composition items (ex overlay plot) in stacked plots, fix swg staleness provider isStale method response

* typo

* removing staleness listeners when ladtable is remove

* addressing pr comments for this component

* address changes requested for lad table sets

* had to update is-stale for the row since we used combined keys in lad table sets now
2023-02-06 14:08:14 -08:00
Jesse Mazzella
5384022a59 fix: DisplayLayout shapes can be selected and manipulated again (#6289)
fix: handle case where parentObject is undefined

- Fixes manipulation of parentless display layout objects such as shapes and lines
2023-02-06 12:49:50 -08:00
Jamie V
b57974b462 [ObjectAPI] Cleanup code and remove possible memory leak (#6269)
* Destroy mutable after refresh
* Check if domainObject supports mutation before getting mutable
2023-02-06 18:53:06 +00:00
Marcelo Arias
3c36ba9a71 Fix keys duplication error (#6243)
* Update key value of notification-message

* Add 'Notifications can be dismissed individually' test
2023-02-06 17:59:26 +00:00
Jesse Mazzella
2ac463de90 test(e2e): Add tests for Recent Objects (#6270)
* test(e2e): add test for recent objects target

* test(e2e): Add RecentObjects tests

- Test for 'target button' scroll and animation

- Test for persistence on refresh

- Test for displaying objects and aliases uniquely

* test(e2e): add test for recent objects limit

* refactor: compress to a single line

* test(e2e): recents max limit test nests objects

- Do deep nesting of objects instead of flat objects

- Collapse the tree completely and then test the "target" button for the most deeply nested item

* test(e2e): update locator to not use `nth(i)`
2023-02-04 00:15:42 +00:00
Shefali Joshi
be38c3e654 Fix stacked plot child selection (#6275)
* Fix selections for different scenarios

* Ensure plot selection in stacked plots works when there are no selected or found annotations

* Adds e2e test for stacked plot selection and fixes the old e2e test which was testing overlay plots instead.

* Fix selection of plots while in Edit mode

* Improve tests for stacked plots

* refactor: remove unnecessary `await`s

* a11y: move aria-label to StackedPlotItem

* refactor(e2e): combine like tests, unique object names

- Use unique object names in `text=` selectors

- Combine like tests to reduce execution time

- Use `getByRole` selectors where able

* docs(e2e): add comments to test

* fix: add class back for unit test selector

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2023-02-03 23:56:50 +00:00
Jamie V
0f312a88bb [Notebook] Sanitize entries before save for extra protection (#6255)
* Sanitizing before save as well to be be doubly safe

---------

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2023-02-03 02:16:45 +00:00
Charles Hacskaylo
422b7f3e09 Compass rose rotation fixes (#6260) 2023-02-02 17:18:41 -08:00
Jesse Mazzella
800062d37e fix: remove 1px padding and re-enable autoscale snapshot test (#6271)
* style: remove 1px padding from plot legend item

* test: re-enable autoscale snapshot test
2023-02-02 15:50:37 -08:00
Jamie V
c1e8c7915c [Staleness] Fix removed object error and clean up (#6241)
* fixing error from plots when removing swg and making methods and props private for swg staleness provider

* removing unsubscribes from destroy hooks if the item has been removed already and reverting an unneccesary change

* checking for undefined staleness response

* removed un-neccesary code
2023-02-01 14:06:54 -08:00
Shefali Joshi
c1c1d87953 Fix multiple y axis issues (#6204)
* Ensure enabling log mode does not reset series that don't belong to that yaxis.
propagate both left and right y axes widths so that plots can adjust accordingly

* Revert code
Handle second axis resizing

* Fixes issue where logMode was getting initialized incorrectly for multiple y axes

* Get the yAxisId of the series from the model.

* Address review comments - rename params for readability

* Fix number of log ticks expected and the tick values since we reduced the number of secondary ticks

* Fix log plot test

* Add guard code during destroy

* Add missing remove callback
2023-02-01 21:46:15 +00:00
Jamie V
0382d22f7f [Notebook] Entry links tests (#6190)
* removing dupe nb install, adding whitelist nb init script, testing whitelist urls

* updating from copy

* addressing PR comments for cleaner tests

* removing .only

* added a secure url test and a subdomain url test and simplified some code

* not messin with protocols atm

* update variable name
2023-02-01 11:55:08 -08:00
Shefali Joshi
f570424357 Fix stacked plots legend (#6199)
* Add listeners to remove stacked plot series and make keys unique

* don't add overlay plots to stacked plot legends

* Ensure series colors are drawn correctly in the plot legend

* Remove legend from mct plot. Remove series reactivity from stackd plot and add them to the legend instead.

* Clean up stacked plots so that the plot legend needs fewer props
Also make sure that plot selection inside a stacked plot works - this had regressed due to plot annotations

* Fix console error in plot elements pool and plot legend - reset arrays to empty
* Ensure color in the y axis swatch updates correctly

* Fix small issues with removing objects from STacked plots

* Fix selection for annotations and also select stacked plot child items

* fix notebook tagging

* remove unused annotation editor and change selection to single object

* remove reference to deleted css

* fix e2e tests

* Fix small typos into the selection context for Notebooks.

* Add a typ that identifies that an annotation selection is coming from a search result

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
2023-02-01 10:14:02 -08:00
Charles Hacskaylo
393c801426 Closes #6215 (#6222)
- Markup cleanups, CSS placement improvements for tags.
- Better approach to tag layout.
- CSS prop migrated to _constants.scss.
- Style and layout improvements for `.c-autocomplete*` input.

Co-authored-by: Scott Bell <scott@traclabs.com>
2023-02-01 11:29:51 +01:00
Jesse Mazzella
6d63339b23 fix: Navigating from Recent Objects breadcrumb correctly updates the URL hash (#6234)
* fix: provide hashUrl for ObjectPath breadcrumbs

* a11y: add `navigation` role and aria-label to breadcrumb

* test(e2e): add regression test for breadcrumb nav

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
2023-02-01 11:11:44 +01:00
Andrew Henry
66d7c626e1 Replace structuredClone with JSON.parse (#6237) 2023-01-31 15:10:13 -08:00
Charles Hacskaylo
2246f33023 Color fix for Recently Viewed items (#6227)
- Normalized color styles.

Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
2023-01-31 19:42:15 +00:00
Andrew Henry
871362d469 Fix plot composition (#6206)
* Fixed composition

* Remove unnecessary guard code

* Removing deprecated code

* Use valid key for stacked plot v-for

* Fixed object API specs to expect old values as well as new values in mutation callbacks

* Fixed existing tests

* Added E2E test

* Fixed linting error

---------

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2023-01-31 06:01:00 +00:00
Jesse Mazzella
cc1bf47f5a fix(recents): fix vue warnings after target animation (#6203) 2023-01-31 01:45:08 +00:00
Jesse Mazzella
9c784398b3 fix(e2e): temp fix for appAction test flake (#6226) 2023-01-30 18:55:24 +00:00
John Hill
21ce013df2 path change (#6224) 2023-01-29 13:49:00 -08:00
Jesse Mazzella
d20c2a3e3c fix: ensure MoveAction always saves transaction (#6196)
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2023-01-26 22:18:46 +00:00
Jesse Mazzella
8d1a2e6716 Make tree items more actionable and add AppAction for expanding the object tree (#5997)
* style: add `visibility` to tree expand triangles

- The purpose of this is so that Playwright can perform actionability checks on the tree items. This will make operations involving expanding tree items much easier to perform in e2e.

* feat(e2e): Add AppAction to expand the entire tree

* fix: wait for loading indicator

* test: add test for `expandEntireTree`

* test: update `expandEntireTree` and tree selectors

- Use dynamic aria-label for different tree implementations

- Get rid of CSS ids which are only for testing

- Update percy tree scope selector

* chore(lint): remove unused variable

* refactor(e2e): update tree locators

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-01-26 17:25:15 +00:00
Shefali Joshi
01f724959d Ensure limit lines for both the old and new y axes are redrawn when a series moves from one y axis to another (#6181)
Optimize initialization of Plot configuration
Ensure the the y axis form correctly saves any changes to the configuration
Fix excluded limits test
2023-01-26 17:11:13 +00:00
Charles Hacskaylo
3ae6290ec3 Visual tweaks to Recently Viewed items (#6183)
- Reduced size of icon.
- Tightened spacing.
2023-01-25 14:15:50 -08:00
Jesse Mazzella
ba5ed27e74 fix: skip if no yAxisId exists on persistedConfig (#6188) 2023-01-25 19:18:26 +00:00
Jesse Mazzella
ca737d8afa fix(elementItemGroup): 🚫👶📜📊 (#6171)
- translation: remove the baby scroll bars from element item groups
2023-01-24 23:48:49 +00:00
Jesse Mazzella
33a275e8bc fix(multiYAxis): get yRange for yAxis of the series (#6170)
* fix: get yRange for the yAxis of the series

* refactor: use collection methods, define as vars
2023-01-24 14:22:48 -08:00
Shefali Joshi
60e808689c Mct6157 creating annotations for plots on multiple yaxes (#6161)
* First pass

* Get bounding box min and max values based on the y axis that a series belongs to.
Handle removal of telemetry from an overlay plot
Handle addition of telemetry after annotations have been saved to an overlay plot

* Fix showing the rectangle for a given target's bounding box.

* remove invalid comment

Co-authored-by: Scott Bell <scott@traclabs.com>
2023-01-24 11:00:45 +00:00
36 changed files with 591 additions and 331 deletions

View File

@@ -118,6 +118,7 @@ jobs:
suite: #stable or full
type: string
executor: pw-focal-development
parallelism: 4
steps:
- build_and_install:
node-version: <<parameters.node-version>>
@@ -173,10 +174,16 @@ jobs:
workflows:
overall-circleci-commit-status: #These jobs run on every commit
jobs:
- lint:
name: node14-lint
node-version: lts/fermium
- unit-test:
name: node18-chrome
node-version: "18"
- e2e-test:
name: e2e-full
name: e2e-stable
node-version: lts/gallium
suite: full
suite: stable
- perf-test:
node-version: lts/gallium
- visual-test:

View File

@@ -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

View File

@@ -12,13 +12,14 @@ const config = {
retries: 2, //Retries 2 times for a total of 3 runs. When running sharded and with maxFailures = 5, this should ensure that flake is managed without failing the full suite
testDir: 'tests',
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
timeout: 60 * 1000,
webServer: {
command: 'npm run start:coverage',
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: false
},
//maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste
maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste
workers: NUM_WORKERS, //Limit to 2 for CircleCI Agent
use: {
baseURL: 'http://localhost:8080/',
@@ -72,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']
]
};

View File

@@ -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' }]
]
};

View File

@@ -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

View File

@@ -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);
});
});

View File

@@ -231,4 +231,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();
});
});

View File

@@ -140,61 +140,4 @@ test.describe('Overlay Plot', () => {
expect(yAxis3Group.getByRole('listitem', { name: swgB.name })).toBeTruthy();
expect(yAxis3Group.getByRole('listitem').nth(0).getByText(swgB.name)).toBeTruthy();
});
test('Clicking on an item in the elements pool brings up the plot preview with data points', async ({ page }) => {
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: "Overlay Plot"
});
const swgA = await createDomainObjectWithDefaults(page, {
type: "Sine Wave Generator",
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
await page.click('button[title="Edit"]');
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
await page.locator('.js-overlay canvas').nth(1);
const plotPixelSize = await getCanvasPixelsWithData(page);
expect(plotPixelSize).toBeGreaterThan(0);
});
});
/**
* @param {import('@playwright/test').Page} page
*/
async function getCanvasPixelsWithData(page) {
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getCanvasValue', resolve));
await page.evaluate(() => {
// The document canvas is where the plot points and lines are drawn.
// The only way to access the canvas is using document (using page.evaluate)
let data;
let canvas;
let ctx;
canvas = document.querySelector('.js-overlay canvas');
ctx = canvas.getContext('2d');
data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
const imageDataValues = Object.values(data);
let plotPixels = [];
// Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four.
// The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order.
for (let i = 0; i < imageDataValues.length;) {
if (imageDataValues[i] > 0) {
plotPixels.push({
startIndex: i,
endIndex: i + 3,
value: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${imageDataValues[i + 2]}, ${imageDataValues[i + 3]})`
});
}
i = i + 4;
}
window.getCanvasValue(plotPixels.length);
});
return getTelemValuePromise;
}

View File

@@ -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();
}
});

View File

@@ -37,7 +37,7 @@ const { test, expect } = require('@playwright/test');
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
// eslint-disable-next-line playwright/no-skipped-test
test.describe('Memory Performance tests', () => {
test.describe.skip('Memory Performance tests', () => {
test.beforeEach(async ({ page, browser }, testInfo) => {
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });

View 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}')`);
});
});

View File

@@ -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);

View File

@@ -275,7 +275,7 @@ function pointForTimestamp(timestamp, name, imageSamples, delay) {
local: Math.floor(timestamp / delay) * delay,
url,
sunOrientation: getCompassValues(0, 360),
cameraAzimuth: getCompassValues(0, 360),
cameraPan: getCompassValues(0, 360),
heading: getCompassValues(0, 360),
transformations: navCamTransformations,
imageDownloadName

View File

@@ -1,6 +1,6 @@
{
"name": "openmct",
"version": "2.1.6",
"version": "2.1.6-SNAPSHOT",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.18.9",
@@ -21,7 +21,7 @@
"d3-scale": "3.3.0",
"d3-selection": "3.0.0",
"eslint": "8.32.0",
"eslint-plugin-compat": "4.0.2",
"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",
@@ -60,7 +60,7 @@
"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",

View File

@@ -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

View File

@@ -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();

View File

@@ -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;
}
},

View File

@@ -180,7 +180,6 @@ export default class TelemetryCollection extends EventEmitter {
let beforeStartOfBounds;
let afterEndOfBounds;
let added = [];
let addedIndices = [];
// loop through, sort and dedupe
for (let datum of data) {
@@ -213,7 +212,6 @@ export default class TelemetryCollection extends EventEmitter {
let index = endIndex || startIndex;
this.boundedTelemetry.splice(index, 0, datum);
addedIndices.push(index);
added.push(datum);
}
@@ -232,7 +230,7 @@ export default class TelemetryCollection extends EventEmitter {
this.emit('add', this.boundedTelemetry);
}
} else {
this.emit('add', added, addedIndices);
this.emit('add', added);
}
}
}
@@ -332,8 +330,7 @@ export default class TelemetryCollection extends EventEmitter {
this.boundedTelemetry = added;
}
// Assumption is that added will be of length 1 here, so just send the last index of the boundedTelemetry in the add event
this.emit('add', added, [this.boundedTelemetry.length]);
this.emit('add', added);
}
} else {
// user bounds change, reset

View File

@@ -117,58 +117,22 @@ describe("The Independent Time API", function () {
});
it("uses an object's independent time context if the parent doesn't have one", () => {
const domainObjectKey2 = `${domainObjectKey}-2`;
const domainObjectKey3 = `${domainObjectKey}-3`;
let timeContext = api.getContextForView([{
identifier: {
namespace: '',
key: domainObjectKey
}
}]);
let timeContext2 = api.getContextForView([{
}, {
identifier: {
namespace: '',
key: domainObjectKey2
key: 'blah'
}
}]);
let timeContext3 = api.getContextForView([{
identifier: {
namespace: '',
key: domainObjectKey3
}
}]);
// all bounds follow global time context
expect(timeContext.bounds()).toEqual(bounds);
expect(timeContext2.bounds()).toEqual(bounds);
expect(timeContext3.bounds()).toEqual(bounds);
// only first item has own context
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(timeContext.bounds()).toEqual(independentBounds);
expect(timeContext2.bounds()).toEqual(bounds);
expect(timeContext3.bounds()).toEqual(bounds);
// first and second item have own context
let destroyTimeContext2 = api.addIndependentContext(domainObjectKey2, independentBounds);
expect(timeContext.bounds()).toEqual(independentBounds);
expect(timeContext2.bounds()).toEqual(independentBounds);
expect(timeContext3.bounds()).toEqual(bounds);
// all items have own time context
let destroyTimeContext3 = api.addIndependentContext(domainObjectKey3, independentBounds);
expect(timeContext.bounds()).toEqual(independentBounds);
expect(timeContext2.bounds()).toEqual(independentBounds);
expect(timeContext3.bounds()).toEqual(independentBounds);
//remove own contexts one at a time - should revert to global time context
destroyTimeContext();
expect(timeContext.bounds()).toEqual(bounds);
expect(timeContext2.bounds()).toEqual(independentBounds);
expect(timeContext3.bounds()).toEqual(independentBounds);
destroyTimeContext2();
expect(timeContext.bounds()).toEqual(bounds);
expect(timeContext2.bounds()).toEqual(bounds);
expect(timeContext3.bounds()).toEqual(independentBounds);
destroyTimeContext3();
expect(timeContext.bounds()).toEqual(bounds);
expect(timeContext2.bounds()).toEqual(bounds);
expect(timeContext3.bounds()).toEqual(bounds);
});
it("Allows setting of valid bounds", function () {

View File

@@ -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);

View File

@@ -26,23 +26,18 @@
:style="`width: 100%; height: 100%`"
>
<CompassHUD
:camera-angle-of-view="cameraAngleOfView"
:heading="heading"
:camera-azimuth="cameraAzimuth"
:transformations="transformations"
:has-gimble="hasGimble"
:normalized-camera-azimuth="normalizedCameraAzimuth"
v-if="showCompassHUD"
:sun-heading="sunHeading"
:camera-angle-of-view="cameraAngleOfView"
:camera-pan="cameraPan"
/>
<CompassRose
:camera-angle-of-view="cameraAngleOfView"
v-if="showCompassRose"
:camera-pan="cameraPan"
:heading="heading"
:camera-azimuth="cameraAzimuth"
:transformations="transformations"
:has-gimble="hasGimble"
:normalized-camera-azimuth="normalizedCameraAzimuth"
:sun-heading="sunHeading"
:sized-image-dimensions="sizedImageDimensions"
:sun-heading="sunHeading"
:transformations="transformations"
/>
</div>
</template>
@@ -50,7 +45,6 @@
<script>
import CompassHUD from './CompassHUD.vue';
import CompassRose from './CompassRose.vue';
import { rotate } from './utils';
export default {
components: {
@@ -68,14 +62,11 @@ export default {
}
},
computed: {
hasGimble() {
return this.cameraAzimuth !== undefined;
showCompassHUD() {
return this.hasCameraPan && this.cameraAngleOfView > 0;
},
// compass ordinal orientation of camera
normalizedCameraAzimuth() {
return this.hasGimble
? rotate(this.cameraAzimuth)
: rotate(this.heading, -this.transformations.rotation || 0);
showCompassRose() {
return (this.hasCameraPan || this.hasHeading) && this.cameraAngleOfView > 0;
},
// horizontal rotation from north in degrees
heading() {
@@ -89,11 +80,14 @@ export default {
return this.image.sunOrientation;
},
// horizontal rotation from north in degrees
cameraAzimuth() {
cameraPan() {
return this.image.cameraPan;
},
hasCameraPan() {
return this.cameraPan !== undefined;
},
cameraAngleOfView() {
return this.transformations.cameraAngleOfView;
return this.transformations?.cameraAngleOfView;
},
transformations() {
return this.image.transformations;

View File

@@ -94,33 +94,17 @@ const COMPASS_POINTS = [
export default {
props: {
cameraAngleOfView: {
type: Number,
required: true
},
heading: {
type: Number,
required: true
},
cameraAzimuth: {
type: Number,
default: undefined
},
transformations: {
type: Object,
required: true
},
hasGimble: {
type: Boolean,
required: true
},
normalizedCameraAzimuth: {
type: Number,
required: true
},
sunHeading: {
type: Number,
default: undefined
},
cameraAngleOfView: {
type: Number,
default: undefined
},
cameraPan: {
type: Number,
required: true
}
},
computed: {
@@ -146,13 +130,10 @@ export default {
left: `${ percentage * 100 }%`
};
},
cameraRotation() {
return this.transformations?.rotation;
},
visibleRange() {
return [
rotate(this.normalizedCameraAzimuth, -this.cameraAngleOfView / 2),
rotate(this.normalizedCameraAzimuth, this.cameraAngleOfView / 2)
rotate(this.cameraPan, -this.cameraAngleOfView / 2),
rotate(this.cameraPan, this.cameraAngleOfView / 2)
];
}
}

View File

@@ -75,6 +75,7 @@
:style="sunHeadingStyle"
/>
<!-- Camera FOV -->
<mask
id="mask2"
class="c-cr__cam-fov-l-mask"
@@ -116,10 +117,10 @@
class="cr-vrover"
:style="camAngleAndPositionStyle"
>
<!-- Equipment body. Rotates relative to the camera pan value for cameras that gimble. -->
<!-- Equipment body. Rotates relative to the camera pan value for cams that gimbal. -->
<path
class="cr-vrover__body"
:style="gimbledCameraPanStyle"
:style="camGimbalAngleStyle"
x
fill-rule="evenodd"
clip-rule="evenodd"
@@ -127,7 +128,6 @@
/>
</g>
<!-- Camera FOV -->
<g
class="c-cr__cam-fov"
>
@@ -160,7 +160,7 @@
<!-- NSEW and ticks -->
<g
class="c-cr__nsew"
:style="compassDialStyle"
:style="compassRoseStyle"
>
<g class="c-cr__ticks-major">
<path d="M50 3L43 10H57L50 3Z" />
@@ -259,32 +259,23 @@ import { throttle } from 'lodash';
export default {
props: {
cameraAngleOfView: {
type: Number,
required: true
},
heading: {
type: Number,
required: true
required: true,
default() {
return 0;
}
},
cameraAzimuth: {
sunHeading: {
type: Number,
default: undefined
},
cameraPan: {
type: Number,
default: undefined
},
transformations: {
type: Object,
required: true
},
hasGimble: {
type: Boolean,
required: true
},
normalizedCameraAzimuth: {
type: Number,
required: true
},
sunHeading: {
type: Number,
default: undefined
},
sizedImageDimensions: {
@@ -298,6 +289,18 @@ export default {
};
},
computed: {
cameraHeading() {
return this.cameraPan ?? this.heading;
},
cameraAngleOfView() {
const cameraAngleOfView = this.transformations?.cameraAngleOfView;
if (!cameraAngleOfView) {
console.warn('No Camera Angle of View provided');
}
return cameraAngleOfView;
},
camAngleAndPositionStyle() {
const translateX = this.transformations?.translateX;
const translateY = this.transformations?.translateY;
@@ -306,22 +309,18 @@ export default {
return { transform: `translate(${translateX}%, ${translateY}%) rotate(${rotation}deg) scale(${scale})` };
},
gimbledCameraPanStyle() {
if (!this.hasGimble) {
return;
}
const gimbledCameraPan = rotate(this.normalizedCameraAzimuth, -this.heading);
camGimbalAngleStyle() {
const rotation = rotate(this.heading);
return {
transform: `rotate(${ -gimbledCameraPan }deg)`
transform: `rotate(${ rotation }deg)`
};
},
compassDialStyle() {
compassRoseStyle() {
return { transform: `rotate(${ this.north }deg)` };
},
north() {
return this.lockCompass ? rotate(-this.normalizedCameraAzimuth) : 0;
return this.lockCompass ? rotate(-this.cameraHeading) : 0;
},
cardinalTextRotateN() {
return { transform: `translateY(-27%) rotate(${ -this.north }deg)` };
@@ -349,7 +348,7 @@ export default {
};
},
cameraHeadingStyle() {
const rotation = rotate(this.north, this.normalizedCameraAzimuth);
const rotation = rotate(this.north, this.cameraHeading);
return {
transform: `rotate(${ rotation }deg)`

View File

@@ -35,15 +35,8 @@ describe("The Compass component", () => {
roll: 90,
pitch: 90,
cameraTilt: 100,
cameraAzimuth: 90,
sunAngle: 30,
transformations: {
translateX: 0,
translateY: 18,
rotation: 0,
scale: 0.3,
cameraAngleOfView: 70
}
cameraPan: 90,
sunAngle: 30
};
let propsData = {
naturalAspectRatio: 0.9,
@@ -51,7 +44,8 @@ describe("The Compass component", () => {
sizedImageDimensions: {
width: 100,
height: 100
}
},
compassRoseSizingClasses: '--rose-small --rose-min'
};
app = new Vue({
@@ -60,6 +54,7 @@ describe("The Compass component", () => {
return propsData;
},
template: `<Compass
:compass-rose-sizing-classes="compassRoseSizingClasses"
:image="image"
:natural-aspect-ratio="naturalAspectRatio"
:sized-image-dimensions="sizedImageDimensions"
@@ -72,7 +67,7 @@ describe("The Compass component", () => {
app.$destroy();
});
describe("when a heading value and cameraAngleOfView exists on the image", () => {
describe("when a heading value exists on the image", () => {
it("should display a compass rose", () => {
let compassRoseElement = instance.$el.querySelector(COMPASS_ROSE_CLASS

View File

@@ -94,6 +94,7 @@
<Compass
v-if="shouldDisplayCompass"
:image="focusedImage"
:natural-aspect-ratio="focusedImageNaturalAspectRatio"
:sized-image-dimensions="sizedImageDimensions"
/>
</div>
@@ -170,7 +171,7 @@
>
<ImageThumbnail
v-for="(image, index) in imageHistory"
:key="`${image.thumbnailUrl || image.url}-${image.time}-${index}`"
:key="`${image.thumbnailUrl || image.url}${image.time}`"
:image="image"
:active="focusedImageIndex === index"
:selected="focusedImageIndex === index && isPaused"
@@ -429,12 +430,9 @@ export default {
&& imageHeightAndWidth
&& this.zoomFactor === 1
&& this.imagePanned !== true;
const hasHeading = this.focusedImage?.heading !== undefined;
const hasCameraAngleOfView = this.focusedImage?.transformations?.cameraAngleOfView > 0;
const hasCameraConfigurations = this.focusedImage?.transformations !== undefined;
return display
&& hasCameraAngleOfView
&& hasHeading;
return display && hasCameraConfigurations;
},
isSpacecraftPositionFresh() {
let isFresh = undefined;
@@ -584,34 +582,11 @@ export default {
},
deep: true
},
focusedImage: {
handler(newImage, oldImage) {
const newTime = newImage?.time;
const oldTime = oldImage?.time;
const newUrl = newImage?.url;
const oldUrl = oldImage?.url;
// Skip if it's all falsy
if (!newTime && !oldTime && !newUrl && !oldUrl) {
return;
}
// Skip if it's the same image
if (newTime === oldTime && newUrl === oldUrl) {
return;
}
// Update image duration and reset age CSS
this.trackDuration();
this.resetAgeCSS();
// Reset image dimensions and calculate new dimensions
// on new image load
this.getImageNaturalDimensions();
// Get the related telemetry for the new image
this.updateRelatedTelemetryForFocusedImage();
}
focusedImageIndex() {
this.trackDuration();
this.resetAgeCSS();
this.updateRelatedTelemetryForFocusedImage();
this.getImageNaturalDimensions();
},
bounds() {
this.scrollHandler();
@@ -796,10 +771,6 @@ export default {
this.layers = layersMetadata;
if (this.domainObject.configuration) {
const persistedLayers = this.domainObject.configuration.layers;
if (!persistedLayers) {
return;
}
layersMetadata.forEach((layer) => {
const persistedLayer = persistedLayers.find(object => object.name === layer.name);
if (persistedLayer) {

View File

@@ -76,14 +76,9 @@ export default {
this.telemetryCollection.destroy();
},
methods: {
dataAdded(addedItems, addedItemIndices) {
const normalizedDataToAdd = addedItems.map(datum => this.normalizeDatum(datum));
let newImageHistory = this.imageHistory.slice();
normalizedDataToAdd.forEach(((datum, index) => {
newImageHistory.splice(addedItemIndices[index] ?? -1, 0, datum);
}));
//Assign just once so imageHistory watchers don't get called too often
this.imageHistory = newImageHistory;
dataAdded(dataToAdd) {
const normalizedDataToAdd = dataToAdd.map(datum => this.normalizeDatum(datum));
this.imageHistory = this.imageHistory.concat(normalizedDataToAdd);
},
dataCleared() {
this.imageHistory = [];
@@ -158,6 +153,9 @@ export default {
return;
}
// forcibly reset the imageContainer size to prevent an aspect ratio distortion
delete this.imageContainerWidth;
delete this.imageContainerHeight;
this.bounds = bounds; // setting bounds for ImageryView watcher
},
timeSystemChange() {

View File

@@ -64,7 +64,7 @@
tabindex="0"
>
<TextHighlight
:text="formatValidUrls(entry.text)"
:text="entryText"
:highlight="highlightText"
:highlight-class="'search-highlight'"
/>
@@ -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)"
@@ -94,7 +94,7 @@
class="c-ne__text"
contenteditable="false"
tabindex="0"
v-bind.prop="formattedText"
v-html="formattedText"
>
</div>
</template>
@@ -228,9 +228,7 @@ export default {
},
selectedEntryId: {
type: String,
default() {
return '';
}
required: true
}
},
data() {
@@ -238,7 +236,7 @@ export default {
editMode: false,
canEdit: true,
enableEmbedsWrapperScroll: false,
urlWhitelist: []
urlWhitelist: null
};
},
computed: {
@@ -250,15 +248,28 @@ export default {
},
formattedText() {
// remove ANY tags
const text = sanitizeHtml(this.entry.text, SANITIZATION_SCHEMA);
let text = sanitizeHtml(this.entry.text, SANITIZATION_SCHEMA);
if (this.editMode || this.urlWhitelist.length === 0) {
if (this.editMode || !this.urlWhitelist) {
return { innerText: text };
}
const html = this.formatValidUrls(text);
text = text.replace(URL_REGEX, (match) => {
const url = new URL(match);
const domain = url.hostname;
let result = match;
let isMatch = this.urlWhitelist.find((partialDomain) => {
return domain.endsWith(partialDomain);
});
return { innerHTML: html };
if (isMatch) {
result = `<a class="c-hyperlink" target="_blank" href="${match}">${match}</a>`;
}
return result;
});
return { innerHTML: text };
},
isSelectedEntry() {
return this.selectedEntryId === this.entry.id;
@@ -344,22 +355,6 @@ export default {
deleteEntry() {
this.$emit('deleteEntry', this.entry.id);
},
formatValidUrls(text) {
return text.replace(URL_REGEX, (match) => {
const url = new URL(match);
const domain = url.hostname;
let result = match;
let isMatch = this.urlWhitelist.find((partialDomain) => {
return domain.endsWith(partialDomain);
});
if (isMatch) {
result = `<a class="c-hyperlink" target="_blank" href="${match}">${match}</a>`;
}
return result;
});
},
manageEmbedLayout() {
if (this.$refs.embeds) {
const embedsWrapperLength = this.$refs.embedsWrapper.clientWidth;

View File

@@ -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"

View File

@@ -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);

View File

@@ -24,7 +24,7 @@
<ul
v-if="orderedPath.length"
class="c-location"
:aria-label="`${domainObject.name} Breadcrumb`"
:aria-label="`${domainObject.name}`"
role="navigation"
>
<li

View File

@@ -31,6 +31,7 @@
:added-tags="addedTags"
@tagRemoved="tagRemoved"
@tagAdded="tagAdded"
@tagBlurred="tagBlurred"
/>
<button
v-show="!userAddingTag && !maxTagsAdded"
@@ -165,6 +166,12 @@ export default {
}
}
},
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) => {

View File

@@ -31,6 +31,7 @@
class="c-tag-selection"
:item-css-class="'icon-circle'"
@onChange="tagSelected"
@autoCompleteBlur="autoCompleteBlur"
/>
</template>
<template v-else>
@@ -158,6 +159,9 @@ export default {
if (tagAdded) {
this.$emit('tagAdded', tagAdded.id);
}
},
autoCompleteBlur() {
this.$emit('tagBlurred');
}
}
};

View File

@@ -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 => {

View File

@@ -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"

View File

@@ -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>

View File

@@ -21,13 +21,24 @@
*****************************************************************************/
<template>
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="highlightedText"></span>
<span>
<span
v-for="segment in segments"
:key="segment.id"
:style="getStyles(segment)"
:class="{ [highlightClass] : segment.type === 'highlight' }"
>
{{ segment.text }}
</span>
</span>
</template>
<script>
import { v4 as uuid } from 'uuid';
export default {
props: {
text: {
@@ -47,11 +58,68 @@ export default {
}
}
},
computed: {
highlightedText() {
let regex = new RegExp(`(?<!<[^>]*)(${this.highlight})`, 'gi');
data() {
return {
segments: []
};
},
watch: {
highlight() {
this.highlightText();
},
text() {
this.highlightText();
}
},
mounted() {
this.highlightText();
},
methods: {
addHighlightSegment(segment) {
this.segments.push({
id: uuid(),
text: segment,
type: 'highlight',
spaceBefore: segment.startsWith(' '),
spaceAfter: segment.endsWith(' ')
});
},
addTextSegment(segment) {
this.segments.push({
id: uuid(),
text: segment,
type: 'text',
spaceBefore: segment.startsWith(' '),
spaceAfter: segment.endsWith(' ')
});
},
getStyles(segment) {
let styles = {
display: 'inline-block'
};
return this.text.replace(regex, `<span class="${this.highlightClass}">${this.highlight}</span>`);
if (segment.spaceBefore) {
styles.paddingLeft = '.33em';
}
if (segment.spaceAfter) {
styles.paddingRight = '.33em';
}
return styles;
},
highlightText() {
this.segments = [];
let regex = new RegExp('(' + this.highlight + ')', 'gi');
let textSegments = this.text.split(regex);
for (let i = 0; i < textSegments.length; i++) {
if (textSegments[i].toLowerCase() === this.highlight.toLowerCase()) {
this.addHighlightSegment(textSegments[i]);
} else {
this.addTextSegment(textSegments[i]);
}
}
}
}
};