Compare commits
57 Commits
mutation-p
...
persistenc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5de5ff347c | ||
|
|
2bfe632e7e | ||
|
|
4ac39a3990 | ||
|
|
169d148c58 | ||
|
|
40d2f3295f | ||
|
|
0e707150e0 | ||
|
|
2540d96617 | ||
|
|
1c8784fec5 | ||
|
|
2943d2b6ec | ||
|
|
4246a597a9 | ||
|
|
0af7965021 | ||
|
|
e9c0909415 | ||
|
|
0f0a3dc48f | ||
|
|
4c82680b87 | ||
|
|
c4734b8ad6 | ||
|
|
9786ff5de4 | ||
|
|
437154a5c0 | ||
|
|
2bd38dab9f | ||
|
|
063df721ae | ||
|
|
a09db30b32 | ||
|
|
9d89bdd6d3 | ||
|
|
ed9ca2829b | ||
|
|
eacbac6aad | ||
|
|
69153fe8f0 | ||
|
|
51196530fd | ||
|
|
fefa46ce7e | ||
|
|
e08ab8ef24 | ||
|
|
7011877e64 | ||
|
|
34ecc08238 | ||
|
|
a07c043a29 | ||
|
|
2999a5135e | ||
|
|
2766452b38 | ||
|
|
f3cdf69288 | ||
|
|
a040bb30c2 | ||
|
|
0a2e0a4e65 | ||
|
|
e8df2bd437 | ||
|
|
ccd2a8b64c | ||
|
|
2bd35bb2a5 | ||
|
|
28dbd724d6 | ||
|
|
5a1c329c66 | ||
|
|
00a5cbd2fd | ||
|
|
a2d698d5c1 | ||
|
|
5685a5b393 | ||
|
|
164f39695e | ||
|
|
c384cf67da | ||
|
|
417b225505 | ||
|
|
e5e93f311c | ||
|
|
39e6d9c90c | ||
|
|
60d021ef82 | ||
|
|
59880955a2 | ||
|
|
b51ed7e844 | ||
|
|
7bbaec4006 | ||
|
|
c0f24b3925 | ||
|
|
4e79725897 | ||
|
|
0674c9fc33 | ||
|
|
de1b877954 | ||
|
|
4db2f547d9 |
2
.github/workflows/prcop-config.json
vendored
2
.github/workflows/prcop-config.json
vendored
@@ -3,7 +3,7 @@
|
||||
{
|
||||
"name": "descriptionRegexp",
|
||||
"config": {
|
||||
"regexp": "[x|X]] Testing instructions",
|
||||
"regexp": "x] Testing instructions",
|
||||
"errorMessage": ":police_officer: PR Description does not confirm that associated issue(s) contain Testing instructions"
|
||||
}
|
||||
},
|
||||
|
||||
3
app.js
3
app.js
@@ -12,7 +12,7 @@ const express = require('express');
|
||||
const app = express();
|
||||
const fs = require('fs');
|
||||
const request = require('request');
|
||||
const __DEV__ = !process.env.NODE_ENV || process.env.NODE_ENV === 'development';
|
||||
const __DEV__ = process.env.NODE_ENV === 'development';
|
||||
|
||||
// Defaults
|
||||
options.port = options.port || options.p || 8080;
|
||||
@@ -89,4 +89,3 @@ app.get('/', function (req, res) {
|
||||
app.listen(options.port, options.host, function () {
|
||||
console.log('Open MCT application running at %s:%s', options.host, options.port);
|
||||
});
|
||||
|
||||
|
||||
122
e2e/README.md
122
e2e/README.md
@@ -1,122 +0,0 @@
|
||||
# e2e testing
|
||||
|
||||
This document captures information specific to the e2e testing of Open MCT. For general information about testing, please see [the Open MCT README](https://github.com/nasa/openmct/blob/master/README.md#tests).
|
||||
|
||||
## Overview
|
||||
|
||||
This document is designed to capture on the What, Why, and How's of writing and running e2e tests in Open MCT.
|
||||
|
||||
### About e2e testing
|
||||
|
||||
e2e testing is an industry-standard approach to automating the testing of web-based UIs such as Open MCT. Broadly speaking, e2e tests differentiate themselves from unit tests by preferring replication of real user interactions over execution of raw JavaScript functions.
|
||||
|
||||
Historically, the abstraction necessary to replicate real user behavior meant that:
|
||||
|
||||
- e2e tests were "expensive" due to how much code each test executed. The closer a test replicates the user, the more code is needed run during test execution. Unit tests could run smaller units of code more effeciently.
|
||||
- e2e tests were flaky due to network conditions or the underlying protocols associated with testing a browser.
|
||||
- e2e frameworks relied on a browser communication standard which lacked the observability and controls necessary needed to reach the code paths possible with unit and integration tests.
|
||||
- e2e frameworks provided insufficient debug information on test failure
|
||||
|
||||
However, as the web ecosystem has matured to the point where mission-critical UIs can be written for the web (Open MCT), the e2e testing tools have matured as well. There are now fewer "trade-offs" when choosing to write an e2e test over any other type of test.
|
||||
|
||||
Modern e2e frameworks:
|
||||
|
||||
- Bypass the surface layer of the web-application-under-test and use a raw debugging protocol to observe and control application and browser state.
|
||||
- These new browser-internal protocols enable near-instant, bi-directional communication between test code and the browser, speeding up test execution and making the tests as reliable as the application itself.
|
||||
- Provide test debug tooling which enables developers to pinpoint failure
|
||||
|
||||
Furthermore, the abstraction necessary to run e2e tests as a user enables them to be extended to run within a variety of contexts. This matches the extensible design of Open MCT.
|
||||
|
||||
A single e2e test in Open MCT is extended to run:
|
||||
|
||||
- Against a matrix of browser versions.
|
||||
- Against a matrix of OS platforms.
|
||||
- Against a local development version of Open MCT.
|
||||
- A version of Open MCT loaded as a dependency (VIPER, VISTA, etc)
|
||||
- Against a variety of data sources or telemetry endpoints.
|
||||
|
||||
### Why Playwright?
|
||||
|
||||
[Playwright](https://playwright.dev/) was chosen as our e2e framework because it solves a few VIPER Mission needs:
|
||||
1. First-class support for Automated Performance Testing
|
||||
2. Official Chrome, Chrome Canary, and iPad Capabilities
|
||||
3. Support for Browserless.io
|
||||
4. Ability to generate code coverage reports
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Getting started with Playwright
|
||||
|
||||
### Getting started with Open MCT's implementation of Playwright
|
||||
|
||||
## Types of Testing
|
||||
|
||||
### (TBD) Visual Testing
|
||||
|
||||
- Visual tests leverage [Percy](https://percy.io/).
|
||||
- Visual tests should be written within the `./tests/visual` folder so that they can be ignored during git clones to avoid leaking credentials when executing percy cli
|
||||
|
||||
#### (TBD) How to write a good visual test
|
||||
|
||||
### (TBD) Snapshot Testing
|
||||
|
||||
<https://playwright.dev/docs/test-snapshots>
|
||||
|
||||
### (TBD) Mobile Testing
|
||||
|
||||
### (TBD) Performance Testing
|
||||
|
||||
### (FUTURE) Component Testing
|
||||
|
||||
- Component testing is currrently possible in Playwright but not enabled on this project. For more, please see: <https://playwright.dev/docs/test-components>
|
||||
|
||||
## Architecture, Test Design and Best Practices
|
||||
|
||||
### (TBD) Architecture
|
||||
|
||||
#### (TBD) Continuous Integration
|
||||
|
||||
- Test maturation
|
||||
- Difference between full and e2e-ci suites
|
||||
- Platforms
|
||||
|
||||
### (TBD) Multi-browser and Multi-operating system
|
||||
|
||||
- Where is it tested
|
||||
- What's supported
|
||||
|
||||
### (TBD) Test Design
|
||||
|
||||
- Re-usable tests for VISTA, VIPER, etc.
|
||||
|
||||
#### Annotations
|
||||
|
||||
- Annotations are a great way of organizing tests outside of a file structure.
|
||||
- Current list of annotations:
|
||||
- `@ipad` - Mobile execution possible with Playwright's iPad support.
|
||||
- `@gds` - Executes a GDS Test Case. Used to track in VIPER Mission.
|
||||
- `@addInit` - Initializes the browser with an injected and artificial state. Useful for non-default plugins.
|
||||
- `@localStorage` - Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB).
|
||||
- `@snapshot` - Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of a container.
|
||||
|
||||
### (TBD) Best Practices
|
||||
|
||||
### (TBD) Reporting
|
||||
|
||||
### (TBD) Code Coverage
|
||||
|
||||
Code coverage is collected during test execution and reported with [nyc](https://github.com/istanbuljs/nyc) and [codecov.io](https://about.codecov.io/)
|
||||
|
||||
## Other
|
||||
|
||||
### FAQ
|
||||
|
||||
- How does this help NASA missions?
|
||||
- When should I write an e2e test instead of a unit test?
|
||||
- When should I write a functional vs visual test?
|
||||
- How is Open MCT extending default Playwright functionality?
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- Why is my test failing on CI and not locally?
|
||||
- How can I view the failing tests on CI?
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Wait for all animations within the given element and subtrees to finish
|
||||
* See: https://github.com/microsoft/playwright/issues/15660#issuecomment-1184911658
|
||||
* @param {import('@playwright/test').Locator} locator
|
||||
*/
|
||||
function waitForAnimations(locator) {
|
||||
return locator
|
||||
.evaluate((element) =>
|
||||
Promise.all(
|
||||
element
|
||||
.getAnimations({ subtree: true })
|
||||
.map((animation) => animation.finished)));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
module.exports = {
|
||||
waitForAnimations
|
||||
};
|
||||
@@ -60,7 +60,6 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||
|
||||
getConditionSetIdentifierFromUrl = conditionSetUrl.split('/').pop().split('?')[0];
|
||||
console.debug('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
|
||||
await page.close();
|
||||
});
|
||||
|
||||
//Load localStorage for subsequent tests
|
||||
|
||||
@@ -28,7 +28,6 @@ but only assume that example imagery is present.
|
||||
|
||||
const { test } = require('../../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
const { waitForAnimations } = require('../../../commonActions.js');
|
||||
|
||||
const backgroundImageSelector = '.c-imagery__main-image__background-image';
|
||||
|
||||
@@ -82,16 +81,7 @@ test.describe('Example Imagery Object', () => {
|
||||
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
|
||||
expect(imageMouseZoomedOut.height).toBeLessThan(imageMouseZoomedIn.height);
|
||||
expect(imageMouseZoomedOut.width).toBeLessThan(imageMouseZoomedIn.width);
|
||||
});
|
||||
|
||||
test('Can adjust image brightness/contrast by dragging the sliders', async ({ page, browserName }) => {
|
||||
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
// Open the image filter menu
|
||||
await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click();
|
||||
|
||||
// Drag the brightness and contrast sliders around and assert filter values
|
||||
await dragBrightnessSliderAndAssertFilterValues(page);
|
||||
await dragContrastSliderAndAssertFilterValues(page);
|
||||
});
|
||||
|
||||
test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
|
||||
@@ -160,65 +150,78 @@ test.describe('Example Imagery Object', () => {
|
||||
});
|
||||
|
||||
test('Can use + - buttons to zoom on the image', async ({ page }) => {
|
||||
// Get initial image dimensions
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
|
||||
const zoomOutBtn = page.locator('.t-btn-zoom-out').nth(0);
|
||||
const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
|
||||
// Zoom in twice via button
|
||||
await zoomIntoImageryByButton(page);
|
||||
await zoomIntoImageryByButton(page);
|
||||
|
||||
// Get and assert zoomed in image dimensions
|
||||
await zoomInBtn.click();
|
||||
await zoomInBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
||||
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
||||
|
||||
// Zoom out once via button
|
||||
await zoomOutOfImageryByButton(page);
|
||||
|
||||
// Get and assert zoomed out image dimensions
|
||||
await zoomOutBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const zoomedOutBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
|
||||
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
|
||||
|
||||
// Zoom out again via button, assert against the initial image dimensions
|
||||
await zoomOutOfImageryByButton(page);
|
||||
const finalBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(finalBoundingBox).toEqual(initialBoundingBox);
|
||||
});
|
||||
|
||||
test('Can use the reset button to reset the image', async ({ page }, testInfo) => {
|
||||
test.slow(testInfo.project === 'chrome-beta', "This test is slow in chrome-beta");
|
||||
// Get initial image dimensions
|
||||
// wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
|
||||
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
|
||||
const zoomResetBtn = page.locator('.t-btn-zoom-reset').nth(0);
|
||||
const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
|
||||
// Zoom in twice via button
|
||||
await zoomIntoImageryByButton(page);
|
||||
await zoomIntoImageryByButton(page);
|
||||
await zoomInBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
await zoomInBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
|
||||
// Get and assert zoomed in image dimensions
|
||||
const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
||||
expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
||||
|
||||
// Reset pan and zoom and assert against initial image dimensions
|
||||
await resetImageryPanAndZoom(page);
|
||||
const finalBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(finalBoundingBox).toEqual(initialBoundingBox);
|
||||
// wait for zoom animation to finish
|
||||
// FIXME: The zoom is flakey, sometimes not returning to original dimensions
|
||||
// https://github.com/nasa/openmct/issues/5491
|
||||
await expect.poll(async () => {
|
||||
await zoomResetBtn.click();
|
||||
const boundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
|
||||
return boundingBox;
|
||||
}, {
|
||||
timeout: 10 * 1000
|
||||
}).toEqual(initialBoundingBox);
|
||||
});
|
||||
|
||||
test('Using the zoom features does not pause telemetry', async ({ page }) => {
|
||||
const pausePlayButton = page.locator('.c-button.pause-play');
|
||||
// wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
|
||||
// open the time conductor drop down
|
||||
await page.locator('button:has-text("Fixed Timespan")').click();
|
||||
|
||||
// Click local clock
|
||||
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
|
||||
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
|
||||
|
||||
// Zoom in via button
|
||||
await zoomIntoImageryByButton(page);
|
||||
await expect(pausePlayButton).not.toHaveClass(/is-paused/);
|
||||
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
|
||||
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
|
||||
await zoomInBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
|
||||
return expect(pausePlayButton).not.toHaveClass(/is-paused/);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -246,8 +249,10 @@ test('Example Imagery in Display layout', async ({ page, browserName }) => {
|
||||
await page.click('text=Example Imagery');
|
||||
|
||||
// Clear and set Image load delay to minimum value
|
||||
// FIXME: Update the value to 5000 ms when this bug is fixed.
|
||||
// See: https://github.com/nasa/openmct/issues/5265
|
||||
await page.locator('input[type="number"]').fill('');
|
||||
await page.locator('input[type="number"]').fill('5000');
|
||||
await page.locator('input[type="number"]').fill('0');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
@@ -331,19 +336,6 @@ test('Example Imagery in Display layout', async ({ page, browserName }) => {
|
||||
|
||||
// Verify selected image is still displayed
|
||||
await expect(selectedImage).toBeVisible();
|
||||
|
||||
// Unpause imagery
|
||||
await page.locator('.pause-play').click();
|
||||
|
||||
//Get background-image url from background-image css prop
|
||||
await assertBackgroundImageUrlFromBackgroundCss(page);
|
||||
|
||||
// Open the image filter menu
|
||||
await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click();
|
||||
|
||||
// Drag the brightness and contrast sliders around and assert filter values
|
||||
await dragBrightnessSliderAndAssertFilterValues(page);
|
||||
await dragContrastSliderAndAssertFilterValues(page);
|
||||
});
|
||||
|
||||
test.describe('Example imagery thumbnails resize in display layouts', () => {
|
||||
@@ -434,11 +426,6 @@ test.describe('Example imagery thumbnails resize in display layouts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
// test.fixme('Can use alt+drag to move around image once zoomed in');
|
||||
// test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
// test.fixme('If the imagery view is in pause mode, images still come in');
|
||||
// test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
test.describe('Example Imagery in Flexible layout', () => {
|
||||
test('Example Imagery in Flexible layout', async ({ page, browserName }) => {
|
||||
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
@@ -649,6 +636,22 @@ async function assertBackgroundImageBrightness(page, expected) {
|
||||
expect(actual).toBe(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the filter:contrast value of the current background-image and
|
||||
* asserts against an expected value
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {String} expected The expected contrast value
|
||||
*/
|
||||
async function assertBackgroundImageContrast(page, expected) {
|
||||
const backgroundImage = page.locator('.c-imagery__main-image__background-image');
|
||||
|
||||
// Get the contrast filter value (i.e: filter: contrast(500%) => "500")
|
||||
const actual = await backgroundImage.evaluate((el) => {
|
||||
return el.style.filter.match(/contrast\((\d{1,3})%\)/)[1];
|
||||
});
|
||||
expect(actual).toBe(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
@@ -748,70 +751,3 @@ async function mouseZoomIn(page) {
|
||||
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
|
||||
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the filter:contrast value of the current background-image and
|
||||
* asserts against an expected value
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {String} expected The expected contrast value
|
||||
*/
|
||||
async function assertBackgroundImageContrast(page, expected) {
|
||||
const backgroundImage = page.locator('.c-imagery__main-image__background-image');
|
||||
|
||||
// Get the contrast filter value (i.e: filter: contrast(500%) => "500")
|
||||
const actual = await backgroundImage.evaluate((el) => {
|
||||
return el.style.filter.match(/contrast\((\d{1,3})%\)/)[1];
|
||||
});
|
||||
expect(actual).toBe(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the '+' button to zoom in. Hovers first if the toolbar is not visible
|
||||
* and waits for the zoom animation to finish afterwards.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function zoomIntoImageryByButton(page) {
|
||||
// FIXME: There should only be one set of imagery buttons, but there are two?
|
||||
const zoomInBtn = page.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-in").nth(0);
|
||||
const backgroundImage = page.locator(backgroundImageSelector);
|
||||
if (!(await zoomInBtn.isVisible())) {
|
||||
await backgroundImage.hover({trial: true});
|
||||
}
|
||||
|
||||
await zoomInBtn.click();
|
||||
await waitForAnimations(backgroundImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the '-' button to zoom out. Hovers first if the toolbar is not visible
|
||||
* and waits for the zoom animation to finish afterwards.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function zoomOutOfImageryByButton(page) {
|
||||
// FIXME: There should only be one set of imagery buttons, but there are two?
|
||||
const zoomOutBtn = page.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-out").nth(0);
|
||||
const backgroundImage = page.locator(backgroundImageSelector);
|
||||
if (!(await zoomOutBtn.isVisible())) {
|
||||
await backgroundImage.hover({trial: true});
|
||||
}
|
||||
|
||||
await zoomOutBtn.click();
|
||||
await waitForAnimations(backgroundImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the reset button to reset image pan and zoom. Hovers first if the toolbar is not visible
|
||||
* and waits for the zoom animation to finish afterwards.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function resetImageryPanAndZoom(page) {
|
||||
// FIXME: There should only be one set of imagery buttons, but there are two?
|
||||
const panZoomResetBtn = page.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-reset").nth(0);
|
||||
const backgroundImage = page.locator(backgroundImageSelector);
|
||||
if (!(await panZoomResetBtn.isVisible())) {
|
||||
await backgroundImage.hover({trial: true});
|
||||
}
|
||||
|
||||
await panZoomResetBtn.click();
|
||||
await waitForAnimations(backgroundImage);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ to "fail" on assertions. Instead, they should be used to detect changes between
|
||||
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
|
||||
*/
|
||||
|
||||
const { test } = require('../../fixtures.js');
|
||||
const { test } = require('@playwright/test');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const path = require('path');
|
||||
const sinon = require('sinon');
|
||||
|
||||
@@ -32,8 +32,7 @@ to "fail" on assertions. Instead, they should be used to detect changes between
|
||||
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
|
||||
*/
|
||||
|
||||
const { test } = require('../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const path = require('path');
|
||||
const sinon = require('sinon');
|
||||
|
||||
@@ -24,8 +24,7 @@
|
||||
This test suite is dedicated to tests which verify search functionality.
|
||||
*/
|
||||
|
||||
const { test } = require('../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
|
||||
/**
|
||||
|
||||
24
package.json
24
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.1.0-SNAPSHOT",
|
||||
"version": "2.0.5",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.18.2",
|
||||
@@ -13,7 +13,7 @@
|
||||
"@types/karma": "^6.3.2",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"babel-loader": "8.2.5",
|
||||
"babel-loader": "8.2.3",
|
||||
"babel-plugin-istanbul": "6.1.1",
|
||||
"comma-separated-values": "3.6.4",
|
||||
"codecov":"3.8.3",
|
||||
@@ -23,10 +23,10 @@
|
||||
"d3-axis": "3.0.0",
|
||||
"d3-scale": "3.3.0",
|
||||
"d3-selection": "3.0.0",
|
||||
"eslint": "8.18.0",
|
||||
"eslint": "8.13.0",
|
||||
"eslint-plugin-compat": "4.0.2",
|
||||
"eslint-plugin-playwright": "0.9.0",
|
||||
"eslint-plugin-vue": "9.1.1",
|
||||
"eslint-plugin-vue": "9.1.0",
|
||||
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
|
||||
"eventemitter3": "1.2.0",
|
||||
"express": "4.13.1",
|
||||
@@ -34,7 +34,7 @@
|
||||
"git-rev-sync": "3.0.2",
|
||||
"html2canvas": "1.4.1",
|
||||
"imports-loader": "0.8.0",
|
||||
"jasmine-core": "4.2.0",
|
||||
"jasmine-core": "4.1.1",
|
||||
"jsdoc": "3.5.5",
|
||||
"karma": "6.3.20",
|
||||
"karma-chrome-launcher": "3.1.1",
|
||||
@@ -42,7 +42,7 @@
|
||||
"karma-coverage": "2.2.0",
|
||||
"karma-coverage-istanbul-reporter": "3.0.3",
|
||||
"karma-firefox-launcher": "2.1.2",
|
||||
"karma-jasmine": "5.1.0",
|
||||
"karma-jasmine": "4.0.1",
|
||||
"karma-junit-reporter": "2.0.1",
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-spec-reporter": "0.0.34",
|
||||
@@ -50,20 +50,20 @@
|
||||
"lighthouse": "9.6.1",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"mini-css-extract-plugin": "2.6.1",
|
||||
"moment": "2.29.4",
|
||||
"mini-css-extract-plugin": "2.6.0",
|
||||
"moment": "2.29.3",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"moment-timezone": "0.5.34",
|
||||
"node-bourbon": "4.2.3",
|
||||
"painterro": "1.2.56",
|
||||
"nyc":"15.1.0",
|
||||
"painterro": "1.2.78",
|
||||
"plotly.js-basic-dist": "2.12.0",
|
||||
"plotly.js-gl2d-dist": "2.12.0",
|
||||
"printj": "1.3.1",
|
||||
"request": "2.88.2",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sass": "1.52.2",
|
||||
"sass-loader": "13.0.2",
|
||||
"sass-loader": "12.6.0",
|
||||
"sinon": "14.0.0",
|
||||
"style-loader": "^1.0.1",
|
||||
"uuid": "8.3.2",
|
||||
@@ -72,7 +72,7 @@
|
||||
"vue-loader": "15.9.8",
|
||||
"vue-template-compiler": "2.6.14",
|
||||
"webpack": "5.68.0",
|
||||
"webpack-cli": "4.10.0",
|
||||
"webpack-cli": "4.9.2",
|
||||
"webpack-dev-middleware": "5.3.3",
|
||||
"webpack-hot-middleware": "2.25.1",
|
||||
"webpack-merge": "5.8.0"
|
||||
@@ -92,7 +92,7 @@
|
||||
"test:firefox": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
|
||||
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome visual smoke branding default condition timeConductor clock persistence performance grandsearch tags",
|
||||
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery persistence performance grandsearch notebook/tags",
|
||||
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
|
||||
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots",
|
||||
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js",
|
||||
|
||||
@@ -52,8 +52,8 @@ class MutableDomainObject {
|
||||
// Property should not be serialized
|
||||
enumerable: false
|
||||
},
|
||||
_callbacksForPaths: {
|
||||
value: {},
|
||||
_observers: {
|
||||
value: [],
|
||||
// Property should not be serialized
|
||||
enumerable: false
|
||||
},
|
||||
@@ -64,31 +64,15 @@ class MutableDomainObject {
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* BRAND new approach
|
||||
* - Register a listener on $_synchronize_model
|
||||
* - The $_synchronize_model event provides the path. Figure out whether the mutated path is equal to, or a parent of the observed path.
|
||||
* - If so, trigger callback with new value
|
||||
* - As an optimization, ONLY trigger if value has actually changed. Could be deferred until later?
|
||||
*/
|
||||
|
||||
$observe(path, callback) {
|
||||
let callbacksForPath = this._callbacksForPaths[path];
|
||||
if (callbacksForPath === undefined) {
|
||||
callbacksForPath = [];
|
||||
this._callbacksForPaths[path] = callbacksForPath;
|
||||
}
|
||||
let fullPath = qualifiedEventName(this, path);
|
||||
let eventOff =
|
||||
this._globalEventEmitter.off.bind(this._globalEventEmitter, fullPath, callback);
|
||||
|
||||
callbacksForPath.push(callback);
|
||||
|
||||
return function unlisten() {
|
||||
let index = callbacksForPath.indexOf(callback);
|
||||
callbacksForPath.splice(index, 1);
|
||||
if (callbacksForPath.length === 0) {
|
||||
delete this._callbacksForPaths[path];
|
||||
}
|
||||
}.bind(this);
|
||||
this._globalEventEmitter.on(fullPath, callback);
|
||||
this._observers.push(eventOff);
|
||||
|
||||
return eventOff;
|
||||
}
|
||||
$set(path, value) {
|
||||
_.set(this, path, value);
|
||||
@@ -104,14 +88,25 @@ class MutableDomainObject {
|
||||
this._globalEventEmitter.emit(ANY_OBJECT_EVENT, this);
|
||||
//Emit wildcard event, with path so that callback knows what changed
|
||||
this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this, path, value);
|
||||
|
||||
//Emit events specific to properties affected
|
||||
let parentPropertiesList = path.split('.');
|
||||
for (let index = parentPropertiesList.length; index > 0; index--) {
|
||||
let parentPropertyPath = parentPropertiesList.slice(0, index).join('.');
|
||||
this._globalEventEmitter.emit(qualifiedEventName(this, parentPropertyPath), _.get(this, parentPropertyPath));
|
||||
}
|
||||
|
||||
//TODO: Emit events for listeners of child properties when parent changes.
|
||||
// Do it at observer time - also register observers for parent attribute path.
|
||||
}
|
||||
|
||||
$refresh(model) {
|
||||
const clone = JSON.parse(JSON.stringify(this));
|
||||
//TODO: Currently we are updating the entire object.
|
||||
// In the future we could update a specific property of the object using the 'path' parameter.
|
||||
this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), model);
|
||||
|
||||
//Emit wildcard event
|
||||
this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this, '*', this, clone);
|
||||
//Emit wildcard event, with path so that callback knows what changed
|
||||
this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this);
|
||||
}
|
||||
|
||||
$on(event, callback) {
|
||||
@@ -119,53 +114,23 @@ class MutableDomainObject {
|
||||
|
||||
return () => this._instanceEventEmitter.off(event, callback);
|
||||
}
|
||||
$updateListenersOnPath(updatedModel, mutatedPath, newValue, oldModel) {
|
||||
const isRefresh = mutatedPath === '*';
|
||||
|
||||
Object.entries(this._callbacksForPaths).forEach(([observedPath, callbacks]) => {
|
||||
if (isChildOf(observedPath, mutatedPath)
|
||||
|| isParentOf(observedPath, mutatedPath)) {
|
||||
let newValueOfObservedPath;
|
||||
|
||||
if (observedPath === '*') {
|
||||
newValueOfObservedPath = updatedModel;
|
||||
|
||||
} else {
|
||||
newValueOfObservedPath = _.get(updatedModel, observedPath);
|
||||
}
|
||||
|
||||
if (isRefresh && observedPath !== '*') {
|
||||
const oldValueOfObservedPath = _.get(oldModel, observedPath);
|
||||
if (!_.isEqual(newValueOfObservedPath, oldValueOfObservedPath)) {
|
||||
callbacks.forEach(callback => callback(newValueOfObservedPath));
|
||||
}
|
||||
} else {
|
||||
//Assumed to be different if result of mutation.
|
||||
callbacks.forEach(callback => callback(newValueOfObservedPath));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
$synchronizeModel(updatedObject) {
|
||||
let clone = JSON.parse(JSON.stringify(updatedObject));
|
||||
utils.refresh(this, clone);
|
||||
}
|
||||
$destroy() {
|
||||
Object.keys(this._callbacksForPaths).forEach(key => delete this._callbacksForPaths[key]);
|
||||
while (this._observers.length > 0) {
|
||||
const observer = this._observers.pop();
|
||||
observer();
|
||||
}
|
||||
|
||||
this._instanceEventEmitter.emit('$_destroy');
|
||||
this._globalEventEmitter.off(qualifiedEventName(this, '$_synchronize_model'), this.$synchronizeModel);
|
||||
this._globalEventEmitter.off(qualifiedEventName(this, '*'), this.$updateListenersOnPath);
|
||||
}
|
||||
|
||||
static createMutable(object, mutationTopic) {
|
||||
let mutable = Object.create(new MutableDomainObject(mutationTopic));
|
||||
Object.assign(mutable, object);
|
||||
|
||||
mutable.$updateListenersOnPath = mutable.$updateListenersOnPath.bind(mutable);
|
||||
mutable.$synchronizeModel = mutable.$synchronizeModel.bind(mutable);
|
||||
|
||||
mutable._globalEventEmitter.on(qualifiedEventName(mutable, '$_synchronize_model'), mutable.$synchronizeModel);
|
||||
mutable._globalEventEmitter.on(qualifiedEventName(mutable, '*'), mutable.$updateListenersOnPath);
|
||||
mutable.$observe('$_synchronize_model', (updatedObject) => {
|
||||
let clone = JSON.parse(JSON.stringify(updatedObject));
|
||||
utils.refresh(mutable, clone);
|
||||
});
|
||||
|
||||
return mutable;
|
||||
}
|
||||
@@ -182,12 +147,4 @@ function qualifiedEventName(object, eventName) {
|
||||
return [keystring, eventName].join(':');
|
||||
}
|
||||
|
||||
function isChildOf(observedPath, mutatedPath) {
|
||||
return Boolean(mutatedPath === '*' || observedPath?.startsWith(mutatedPath));
|
||||
}
|
||||
|
||||
function isParentOf(observedPath, mutatedPath) {
|
||||
return Boolean(observedPath === '*' || mutatedPath?.startsWith(observedPath));
|
||||
}
|
||||
|
||||
export default MutableDomainObject;
|
||||
|
||||
@@ -230,6 +230,7 @@ export default class ObjectAPI {
|
||||
return result;
|
||||
}).catch((result) => {
|
||||
console.warn(`Failed to retrieve ${keystring}:`, result);
|
||||
this.openmct.notifications.error(`Failed to retrieve object ${keystring}`);
|
||||
|
||||
delete this.cache[keystring];
|
||||
|
||||
@@ -387,7 +388,13 @@ export default class ObjectAPI {
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return result.catch((error) => {
|
||||
if (error instanceof this.errors.Conflict) {
|
||||
this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`);
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ObjectAPI from './ObjectAPI.js';
|
||||
import { createOpenMct, resetApplicationState } from '../../utils/testing';
|
||||
|
||||
fdescribe("The Object API", () => {
|
||||
describe("The Object API", () => {
|
||||
let objectAPI;
|
||||
let typeRegistry;
|
||||
let openmct = {};
|
||||
@@ -287,167 +287,53 @@ fdescribe("The Object API", () => {
|
||||
mutableSecondInstance.$destroy();
|
||||
});
|
||||
|
||||
describe('on mutation', () => {
|
||||
it('to stay synchronized', function () {
|
||||
objectAPI.mutate(mutable, 'otherAttribute', 'new-attribute-value');
|
||||
expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value');
|
||||
});
|
||||
it('to stay synchronized when mutated', function () {
|
||||
objectAPI.mutate(mutable, 'otherAttribute', 'new-attribute-value');
|
||||
expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value');
|
||||
});
|
||||
|
||||
it('to indicate when a property changes', function () {
|
||||
let mutationCallback = jasmine.createSpy('mutation-callback');
|
||||
let unlisten;
|
||||
it('to indicate when a property changes', function () {
|
||||
let mutationCallback = jasmine.createSpy('mutation-callback');
|
||||
let unlisten;
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
mutationCallback.and.callFake(resolve);
|
||||
unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback);
|
||||
objectAPI.mutate(mutable, 'otherAttribute', 'some-new-value');
|
||||
}).then(function () {
|
||||
expect(mutationCallback).toHaveBeenCalledWith('some-new-value');
|
||||
unlisten();
|
||||
});
|
||||
});
|
||||
|
||||
it('to indicate when a child property has changed', function () {
|
||||
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
|
||||
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
|
||||
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
|
||||
let listeners = [];
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
objectAttributeCallback.and.callFake(resolve);
|
||||
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
|
||||
|
||||
objectAPI.mutate(mutable, 'objectAttribute.embeddedObject.embeddedKey', 'updated-embedded-value');
|
||||
}).then(function () {
|
||||
expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value');
|
||||
expect(embeddedObjectCallback).toHaveBeenCalledWith({
|
||||
embeddedKey: 'updated-embedded-value'
|
||||
});
|
||||
expect(objectAttributeCallback).toHaveBeenCalledWith({
|
||||
embeddedObject: {
|
||||
embeddedKey: 'updated-embedded-value'
|
||||
}
|
||||
});
|
||||
|
||||
listeners.forEach(listener => listener());
|
||||
});
|
||||
});
|
||||
|
||||
it('to indicate when a parent property has changed', function () {
|
||||
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
|
||||
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
|
||||
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
|
||||
let listeners = [];
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
objectAttributeCallback.and.callFake(resolve);
|
||||
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
|
||||
|
||||
objectAPI.mutate(mutable, 'objectAttribute.embeddedObject', 'updated-embedded-value');
|
||||
}).then(function () {
|
||||
expect(embeddedKeyCallback).toHaveBeenCalledWith(undefined);
|
||||
expect(embeddedObjectCallback).toHaveBeenCalledWith('updated-embedded-value');
|
||||
expect(objectAttributeCallback).toHaveBeenCalledWith({
|
||||
embeddedObject: 'updated-embedded-value'
|
||||
});
|
||||
|
||||
listeners.forEach(listener => listener());
|
||||
});
|
||||
return new Promise(function (resolve) {
|
||||
mutationCallback.and.callFake(resolve);
|
||||
unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback);
|
||||
objectAPI.mutate(mutable, 'otherAttribute', 'some-new-value');
|
||||
}).then(function () {
|
||||
expect(mutationCallback).toHaveBeenCalledWith('some-new-value');
|
||||
unlisten();
|
||||
});
|
||||
});
|
||||
|
||||
describe('on refresh', () => {
|
||||
let refreshModel;
|
||||
it('to indicate when a child property has changed', function () {
|
||||
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
|
||||
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
|
||||
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
|
||||
let listeners = [];
|
||||
|
||||
beforeEach(() => {
|
||||
refreshModel = JSON.parse(JSON.stringify(mutable));
|
||||
});
|
||||
return new Promise(function (resolve) {
|
||||
objectAttributeCallback.and.callFake(resolve);
|
||||
|
||||
it('to stay synchronized', function () {
|
||||
refreshModel.otherAttribute = 'new-attribute-value';
|
||||
mutable.$refresh(refreshModel);
|
||||
expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value');
|
||||
});
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
|
||||
|
||||
it('to indicate when a property changes', function () {
|
||||
let mutationCallback = jasmine.createSpy('mutation-callback');
|
||||
let unlisten;
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
mutationCallback.and.callFake(resolve);
|
||||
unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback);
|
||||
refreshModel.otherAttribute = 'some-new-value';
|
||||
mutable.$refresh(refreshModel);
|
||||
}).then(function () {
|
||||
expect(mutationCallback).toHaveBeenCalledWith('some-new-value');
|
||||
unlisten();
|
||||
objectAPI.mutate(mutable, 'objectAttribute.embeddedObject.embeddedKey', 'updated-embedded-value');
|
||||
}).then(function () {
|
||||
expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value');
|
||||
expect(embeddedObjectCallback).toHaveBeenCalledWith({
|
||||
embeddedKey: 'updated-embedded-value'
|
||||
});
|
||||
});
|
||||
|
||||
it('to indicate when a child property has changed', function () {
|
||||
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
|
||||
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
|
||||
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
|
||||
let listeners = [];
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
objectAttributeCallback.and.callFake(resolve);
|
||||
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
|
||||
|
||||
refreshModel.objectAttribute.embeddedObject.embeddedKey = 'updated-embedded-value';
|
||||
mutable.$refresh(refreshModel);
|
||||
}).then(function () {
|
||||
expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value');
|
||||
expect(embeddedObjectCallback).toHaveBeenCalledWith({
|
||||
expect(objectAttributeCallback).toHaveBeenCalledWith({
|
||||
embeddedObject: {
|
||||
embeddedKey: 'updated-embedded-value'
|
||||
});
|
||||
expect(objectAttributeCallback).toHaveBeenCalledWith({
|
||||
embeddedObject: {
|
||||
embeddedKey: 'updated-embedded-value'
|
||||
}
|
||||
});
|
||||
|
||||
listeners.forEach(listener => listener());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('to indicate when a parent property has changed', function () {
|
||||
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
|
||||
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
|
||||
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
|
||||
let listeners = [];
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
objectAttributeCallback.and.callFake(resolve);
|
||||
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
|
||||
|
||||
refreshModel.objectAttribute.embeddedObject = 'updated-embedded-value';
|
||||
|
||||
mutable.$refresh(refreshModel);
|
||||
}).then(function () {
|
||||
expect(embeddedKeyCallback).toHaveBeenCalledWith(undefined);
|
||||
expect(embeddedObjectCallback).toHaveBeenCalledWith('updated-embedded-value');
|
||||
expect(objectAttributeCallback).toHaveBeenCalledWith({
|
||||
embeddedObject: 'updated-embedded-value'
|
||||
});
|
||||
|
||||
listeners.forEach(listener => listener());
|
||||
});
|
||||
listeners.forEach(listener => listener());
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ describe("The LAD Table", () => {
|
||||
// add another telemetry object as composition in lad table to test multi rows
|
||||
mockObj.ladTable.composition.push(anotherTelemetryObj.identifier);
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(async (done) => {
|
||||
let telemetryRequestResolve;
|
||||
let telemetryObjectResolve;
|
||||
let anotherTelemetryObjectResolve;
|
||||
@@ -204,6 +204,8 @@ describe("The LAD Table", () => {
|
||||
|
||||
await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]);
|
||||
await Vue.nextTick();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it("should show one row per object in the composition", () => {
|
||||
|
||||
@@ -49,7 +49,6 @@ describe("the plugin", () => {
|
||||
let parentObject;
|
||||
let parentObjectPath;
|
||||
let changedParentObject;
|
||||
let unobserve;
|
||||
beforeEach((done) => {
|
||||
parentObject = {
|
||||
name: 'mock folder',
|
||||
@@ -74,7 +73,7 @@ describe("the plugin", () => {
|
||||
});
|
||||
});
|
||||
|
||||
unobserve = openmct.objects.observe(parentObject, '*', (newObject) => {
|
||||
openmct.objects.observe(parentObject, '*', (newObject) => {
|
||||
changedParentObject = newObject;
|
||||
|
||||
done();
|
||||
@@ -82,9 +81,6 @@ describe("the plugin", () => {
|
||||
|
||||
newFolderAction.invoke(parentObjectPath);
|
||||
});
|
||||
afterEach(() => {
|
||||
unobserve();
|
||||
});
|
||||
|
||||
it('creates a new folder object', () => {
|
||||
expect(openmct.objects.save).toHaveBeenCalled();
|
||||
|
||||
@@ -296,17 +296,12 @@ export default {
|
||||
window.addEventListener('orientationchange', this.formatSidebar);
|
||||
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||
this.filterAndSortEntries();
|
||||
this.unlistenToEntryChanges = this.openmct.objects.observe(this.domainObject, "configuration.entries", () => this.filterAndSortEntries());
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
|
||||
if (this.unlistenToEntryChanges) {
|
||||
this.unlistenToEntryChanges();
|
||||
}
|
||||
|
||||
window.removeEventListener('orientationchange', this.formatSidebar);
|
||||
window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||
},
|
||||
|
||||
@@ -233,13 +233,6 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.dropOnEntry = this.dropOnEntry.bind(this);
|
||||
this.$on('tags-updated', async () => {
|
||||
const user = await this.openmct.user.getCurrentUser();
|
||||
this.entry.modified = Date.now();
|
||||
this.entry.modifiedBy = user.getId();
|
||||
|
||||
this.$emit('updateEntry', this.entry);
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
async addNewEmbed(objectPath) {
|
||||
|
||||
@@ -217,9 +217,11 @@ class CouchObjectProvider {
|
||||
this.indicator.setIndicatorToState(DISCONNECTED);
|
||||
console.error(error.message);
|
||||
throw new Error(`CouchDB Error - No response"`);
|
||||
}
|
||||
} else {
|
||||
console.error(error.message);
|
||||
|
||||
console.error(error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,7 +655,6 @@ class CouchObjectProvider {
|
||||
let document = new CouchDocument(key, queued.model);
|
||||
document.metadata.created = Date.now();
|
||||
this.request(key, "PUT", document).then((response) => {
|
||||
console.log('create check response', key);
|
||||
this.#checkResponse(response, queued.intermediateResponse, key);
|
||||
}).catch(error => {
|
||||
queued.intermediateResponse.reject(error);
|
||||
|
||||
@@ -71,7 +71,7 @@ describe("the RemoteClock plugin", () => {
|
||||
parse: (datum) => datum.key
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach((done) => {
|
||||
openmct.install(openmct.plugins.RemoteClock(TIME_TELEMETRY_ID));
|
||||
|
||||
let clocks = openmct.time.getAllClocks();
|
||||
@@ -113,7 +113,9 @@ describe("the RemoteClock plugin", () => {
|
||||
end: OFFSET_END
|
||||
});
|
||||
|
||||
await Promise.all([objectPromiseResolve, requestPromise]);
|
||||
Promise.all([objectPromiseResolve, requestPromise])
|
||||
.then(done)
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('is available and sets up initial values and listeners', () => {
|
||||
|
||||
@@ -78,15 +78,13 @@ describe("the plugin", () => {
|
||||
|
||||
describe('when invoked', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach((done) => {
|
||||
openmct.overlays.overlay = function (options) {};
|
||||
|
||||
spyOn(openmct.overlays, 'overlay');
|
||||
|
||||
viewDatumAction.invoke(mockObjectPath, mockView);
|
||||
});
|
||||
|
||||
it('creates an overlay', () => {
|
||||
expect(openmct.overlays.overlay).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -133,11 +133,8 @@ export default {
|
||||
this.addedTags.push(newTagValue);
|
||||
this.userAddingTag = true;
|
||||
},
|
||||
async tagRemoved(tagToRemove) {
|
||||
const result = await this.openmct.annotation.removeAnnotationTag(this.annotation, tagToRemove);
|
||||
this.$emit('tags-updated');
|
||||
|
||||
return result;
|
||||
tagRemoved(tagToRemove) {
|
||||
return this.openmct.annotation.removeAnnotationTag(this.annotation, tagToRemove);
|
||||
},
|
||||
async tagAdded(newTag) {
|
||||
const annotationWasCreated = this.annotation === null || this.annotation === undefined;
|
||||
@@ -149,7 +146,6 @@ export default {
|
||||
|
||||
this.tagsChanged(this.annotation.tags);
|
||||
this.userAddingTag = false;
|
||||
this.$emit('tags-updated');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,17 +6,6 @@ const webpack = require('webpack');
|
||||
|
||||
module.exports = merge(common, {
|
||||
mode: 'development',
|
||||
watchOptions: {
|
||||
// Since we use require.context, webpack is watching the entire directory.
|
||||
// We need to exclude any files we don't want webpack to watch.
|
||||
// See: https://webpack.js.org/configuration/watch/#watchoptions-exclude
|
||||
ignored: [
|
||||
'**/{node_modules,dist,docs,e2e}', // All files in node_modules, dist, docs, e2e,
|
||||
'**/{*.yml,Procfile,webpack*.js,babel*.js,package*.json,tsconfig.json,jsdoc.json}', // Config files
|
||||
'**/*.{sh,md,png,ttf,woff,svg}', // Non source files
|
||||
'**/.*' // dotfiles and dotfolders
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"vue": path.join(__dirname, "node_modules/vue/dist/vue.js")
|
||||
|
||||
Reference in New Issue
Block a user