Compare commits
12 Commits
5211-tests
...
infinite-v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a60e5167dd | ||
|
|
10ff4e1781 | ||
|
|
f1c85933c3 | ||
|
|
f2ed518300 | ||
|
|
bf8d1561ae | ||
|
|
2e1ede1427 | ||
|
|
fc3614dfbd | ||
|
|
22924f18fc | ||
|
|
60f20c64d5 | ||
|
|
8dc8a1c0a9 | ||
|
|
710259b5f0 | ||
|
|
e774eb01f3 |
@@ -5,6 +5,7 @@ executors:
|
||||
- image: mcr.microsoft.com/playwright:v1.23.0-focal
|
||||
environment:
|
||||
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
|
||||
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
|
||||
parameters:
|
||||
BUST_CACHE:
|
||||
description: "Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!"
|
||||
@@ -64,8 +65,8 @@ commands:
|
||||
suite:
|
||||
type: string
|
||||
steps:
|
||||
- run: npm run cov:e2e:report
|
||||
- run: npm run cov:e2e:<<parameters.suite>>:publish
|
||||
- run: npm run cov:e2e:report || true
|
||||
- run: npm run cov:e2e:<<parameters.suite>>:publish
|
||||
orbs:
|
||||
node: circleci/node@4.9.0
|
||||
browser-tools: circleci/browser-tools@1.3.0
|
||||
@@ -153,6 +154,22 @@ jobs:
|
||||
- store_artifacts:
|
||||
path: html-test-results
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
visual-test:
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
executor: pw-focal-development
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- run: npm run test:e2e:visual
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: html-test-results
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
workflows:
|
||||
overall-circleci-commit-status: #These jobs run on every commit
|
||||
jobs:
|
||||
@@ -168,6 +185,9 @@ workflows:
|
||||
suite: stable
|
||||
- perf-test:
|
||||
node-version: lts/gallium
|
||||
- visual-test:
|
||||
node-version: lts/gallium
|
||||
|
||||
the-nightly: #These jobs do not run on PRs, but against master at night
|
||||
jobs:
|
||||
- unit-test:
|
||||
@@ -185,6 +205,10 @@ workflows:
|
||||
name: e2e-full-nightly
|
||||
node-version: lts/gallium
|
||||
suite: full
|
||||
- perf-test:
|
||||
node-version: lts/gallium
|
||||
- visual-test:
|
||||
node-version: lts/gallium
|
||||
triggers:
|
||||
- schedule:
|
||||
cron: "0 0 * * *"
|
||||
|
||||
25
.github/workflows/e2e-visual.yml
vendored
25
.github/workflows/e2e-visual.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: "e2e-visual"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- opened
|
||||
schedule:
|
||||
- cron: '28 21 * * 1-5'
|
||||
|
||||
jobs:
|
||||
e2e-visual:
|
||||
if: ${{ github.event.label.name == 'pr:visual' }} || ${{ github.event.workflow_dispatch }} || ${{ github.event.schedule }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npx playwright@1.23.0 install
|
||||
- run: npm install
|
||||
- name: Run the e2e visual tests
|
||||
run: npm run test:e2e:visual
|
||||
env:
|
||||
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
|
||||
@@ -148,6 +148,7 @@ Current list of test tags:
|
||||
- `@localStorage` - Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB).
|
||||
- `@snapshot` - Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container.
|
||||
- `@unstable` - A new test or test which is known to be flaky.
|
||||
- `@2p` - Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity.
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
|
||||
@@ -37,11 +37,16 @@
|
||||
* @param {string | undefined} name
|
||||
*/
|
||||
async function createDomainObjectWithDefaults(page, type, name) {
|
||||
// Navigate to focus the 'My Items' folder, and hide the object tree
|
||||
// This is necessary so that subsequent objects can be created without a parent
|
||||
// TODO: Ideally this would navigate to a common `e2e` folder
|
||||
await page.goto('./#/browse/mine?hideTree=true');
|
||||
await page.waitForLoadState('networkidle');
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click the object specified by 'type'
|
||||
await page.click(`text=${type}`);
|
||||
await page.click(`li:text("${type}")`);
|
||||
|
||||
// Modify the name input field of the domain object to accept 'name'
|
||||
if (name) {
|
||||
@@ -52,9 +57,13 @@ async function createDomainObjectWithDefaults(page, type, name) {
|
||||
|
||||
// Click OK button and wait for Navigate event
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK')
|
||||
page.waitForLoadState(),
|
||||
page.click('[aria-label="Save"]'),
|
||||
// Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
return name || `Unnamed ${type}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
30
e2e/helper/useSnowTheme.js
Normal file
30
e2e/helper/useSnowTheme.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
// This should be used to install the Snow theme for Open MCT. Espresso is the default
|
||||
// e.g.
|
||||
// await page.addInitScript({ path: path.join(__dirname, 'useSnowTheme.js') });
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const openmct = window.openmct;
|
||||
openmct.install(openmct.plugins.Snow());
|
||||
});
|
||||
@@ -32,6 +32,7 @@ const config = {
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
use: {
|
||||
browserName: 'chromium'
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig<{ theme: string }>} */
|
||||
const config = {
|
||||
retries: 0, // visual tests should never retry due to snapshot comparison errors
|
||||
testDir: 'tests/visual',
|
||||
timeout: 90 * 1000,
|
||||
workers: 1, // visual tests should never run in parallel due to test pollution
|
||||
testMatch: '**/*.visual.spec.js', // only run visual tests
|
||||
timeout: 60 * 1000,
|
||||
workers: 2, //Limit to 2 for CircleCI Agent
|
||||
webServer: {
|
||||
command: 'cross-env NODE_ENV=test npm run start',
|
||||
url: 'http://localhost:8080/#',
|
||||
@@ -15,17 +16,35 @@ const config = {
|
||||
reuseExistingServer: !process.env.CI
|
||||
},
|
||||
use: {
|
||||
browserName: "chromium",
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: true, // this needs to remain headless to avoid visual changes due to GPU
|
||||
headless: true, // this needs to remain headless to avoid visual changes due to GPU rendering in headed browsers
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'on',
|
||||
trace: 'off',
|
||||
trace: 'on',
|
||||
video: 'off'
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome',
|
||||
use: {
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'chrome-snow-theme',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
theme: 'snow'
|
||||
}
|
||||
}
|
||||
],
|
||||
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
|
||||
}]
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
*/
|
||||
|
||||
const { test, expect } = require('./baseFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('./appActions');
|
||||
// const { createDomainObjectWithDefaults } = require('./appActions');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* @typedef {Object} ObjectCreateOptions
|
||||
@@ -36,12 +37,16 @@ const { createDomainObjectWithDefaults } = require('./appActions');
|
||||
*/
|
||||
|
||||
/**
|
||||
* **NOTE: This feature is a work-in-progress and should not currently be used.**
|
||||
*
|
||||
* Used to create a new domain object as a part of getOrCreateDomainObject.
|
||||
* @type {Map<string, string>}
|
||||
*/
|
||||
const createdObjects = new Map();
|
||||
// const createdObjects = new Map();
|
||||
|
||||
/**
|
||||
* **NOTE: This feature is a work-in-progress and should not currently be used.**
|
||||
*
|
||||
* This action will create a domain object for the test to reference and return the uuid. If an object
|
||||
* of a given name already exists, it will return the uuid of that object to the test instead of creating
|
||||
* a new file. The intent is to move object creation out of test suites which are not explicitly worried
|
||||
@@ -50,27 +55,29 @@ const createdObjects = new Map();
|
||||
* @param {ObjectCreateOptions} options
|
||||
* @returns {Promise<string>} uuid of the domain object
|
||||
*/
|
||||
async function getOrCreateDomainObject(page, options) {
|
||||
const { type, name } = options;
|
||||
const objectName = name ? `${type}:${name}` : type;
|
||||
// async function getOrCreateDomainObject(page, options) {
|
||||
// const { type, name } = options;
|
||||
// const objectName = name ? `${type}:${name}` : type;
|
||||
|
||||
if (createdObjects.has(objectName)) {
|
||||
return createdObjects.get(objectName);
|
||||
}
|
||||
// if (createdObjects.has(objectName)) {
|
||||
// return createdObjects.get(objectName);
|
||||
// }
|
||||
|
||||
await createDomainObjectWithDefaults(page, type, name);
|
||||
// await createDomainObjectWithDefaults(page, type, name);
|
||||
|
||||
// Once object is created, get the uuid from the url
|
||||
const uuid = await page.evaluate(() => {
|
||||
return window.location.href.match(/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/)[0];
|
||||
});
|
||||
// // Once object is created, get the uuid from the url
|
||||
// const uuid = await page.evaluate(() => {
|
||||
// return window.location.href.match(/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/)[0];
|
||||
// });
|
||||
|
||||
createdObjects.set(objectName, uuid);
|
||||
// createdObjects.set(objectName, uuid);
|
||||
|
||||
return uuid;
|
||||
}
|
||||
// return uuid;
|
||||
// }
|
||||
|
||||
/**
|
||||
* **NOTE: This feature is a work-in-progress and should not currently be used.**
|
||||
*
|
||||
* If provided, these options will be used to get or create the desired domain object before
|
||||
* any tests or test hooks have run.
|
||||
* The `uuid` of the `domainObject` will then be available to use within the scoped tests.
|
||||
@@ -87,7 +94,24 @@ async function getOrCreateDomainObject(page, options) {
|
||||
* ```
|
||||
* @type {ObjectCreateOptions}
|
||||
*/
|
||||
const objectCreateOptions = null;
|
||||
// const objectCreateOptions = null;
|
||||
|
||||
/**
|
||||
* The default theme for VIPER and Open MCT is the 'espresso' theme. Overriding this value with 'snow' in our playwright config.js
|
||||
* will override the default theme by injecting the 'snow' theme on launch.
|
||||
*
|
||||
* ### Example:
|
||||
* ```js
|
||||
* projects: [
|
||||
* {
|
||||
* name: 'chrome-snow-theme',
|
||||
* use: {
|
||||
* browserName: 'chromium',
|
||||
* theme: 'snow'
|
||||
* ```
|
||||
* @type {'snow' | 'espresso'}
|
||||
*/
|
||||
const theme = 'espresso';
|
||||
|
||||
/**
|
||||
* The name of the "My Items" folder in the domain object tree.
|
||||
@@ -99,27 +123,39 @@ const objectCreateOptions = null;
|
||||
const myItemsFolderName = "My Items";
|
||||
|
||||
exports.test = test.extend({
|
||||
// This should follow in the Project's configuration. Can be set to 'snow' in playwright config.js
|
||||
theme: [theme, { option: true }],
|
||||
// eslint-disable-next-line no-shadow
|
||||
page: async ({ page, theme }, use) => {
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||
if (theme === 'snow') {
|
||||
//inject snow theme
|
||||
await page.addInitScript({ path: path.join(__dirname, './helper', './useSnowTheme.js') });
|
||||
}
|
||||
|
||||
await use(page);
|
||||
},
|
||||
myItemsFolderName: [myItemsFolderName, { option: true }],
|
||||
// eslint-disable-next-line no-shadow
|
||||
openmctConfig: async ({ myItemsFolderName }, use) => {
|
||||
await use({ myItemsFolderName });
|
||||
},
|
||||
objectCreateOptions: [objectCreateOptions, {option: true}],
|
||||
}
|
||||
// objectCreateOptions: [objectCreateOptions, {option: true}],
|
||||
// eslint-disable-next-line no-shadow
|
||||
domainObject: [async ({ page, objectCreateOptions }, use) => {
|
||||
// FIXME: This is a false-positive caused by a bug in the eslint-plugin-playwright rule.
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||
if (objectCreateOptions === null) {
|
||||
await use(page);
|
||||
// domainObject: [async ({ page, objectCreateOptions }, use) => {
|
||||
// // FIXME: This is a false-positive caused by a bug in the eslint-plugin-playwright rule.
|
||||
// // eslint-disable-next-line playwright/no-conditional-in-test
|
||||
// if (objectCreateOptions === null) {
|
||||
// await use(page);
|
||||
|
||||
return;
|
||||
}
|
||||
// return;
|
||||
// }
|
||||
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
// //Go to baseURL
|
||||
// await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
const uuid = await getOrCreateDomainObject(page, objectCreateOptions);
|
||||
await use({ uuid });
|
||||
}, { auto: true }]
|
||||
// const uuid = await getOrCreateDomainObject(page, objectCreateOptions);
|
||||
// await use({ uuid });
|
||||
// }, { auto: true }]
|
||||
});
|
||||
exports.expect = expect;
|
||||
|
||||
41
e2e/tests/framework/appActions.e2e.spec.js
Normal file
41
e2e/tests/framework/appActions.e2e.spec.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../baseFixtures.js');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions.js');
|
||||
|
||||
test.describe('appActions tests', () => {
|
||||
test('createDomainObjectsWithDefaults can create multiple objects in a row', async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await createDomainObjectWithDefaults(page, 'Timer', 'Timer Foo');
|
||||
await createDomainObjectWithDefaults(page, 'Timer', 'Timer Bar');
|
||||
await createDomainObjectWithDefaults(page, 'Timer', 'Timer Baz');
|
||||
|
||||
// Expand the tree
|
||||
await page.click('.c-disclosure-triangle');
|
||||
|
||||
// Verify the objects were created
|
||||
await expect(page.locator('a :text("Timer Foo")')).toBeVisible();
|
||||
await expect(page.locator('a :text("Timer Bar")')).toBeVisible();
|
||||
await expect(page.locator('a :text("Timer Baz")')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -29,7 +29,7 @@ relates to how we've extended it (i.e. ./e2e/baseFixtures.js) and assumptions ma
|
||||
const { test } = require('../../baseFixtures.js');
|
||||
|
||||
test.describe('baseFixtures tests', () => {
|
||||
test('Verify that tests fail if console.error is thrown @framework', async ({ page }) => {
|
||||
test('Verify that tests fail if console.error is thrown', async ({ page }) => {
|
||||
test.fail();
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
@@ -41,7 +41,7 @@ test.describe('baseFixtures tests', () => {
|
||||
]);
|
||||
|
||||
});
|
||||
test('Verify that tests pass if console.warn is thrown @framework', async ({ page }) => {
|
||||
test('Verify that tests pass if console.warn is thrown', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
|
||||
// Structure: Try to keep a single describe block per logical groups of tests. If your test runtime exceeds 5 minutes or 500 lines, it's likely that it will need to be split.
|
||||
// Annotations: Please use the @unstable tag so that our automation can pick it up as a part of our test promotion pipeline.
|
||||
test.describe('Renaming Timer Object @unstable', () => {
|
||||
test.describe('Renaming Timer Object', () => {
|
||||
//Create a testcase name which will be obvious when it fails in CI
|
||||
test('Can create a new Timer object and rename it from actions Menu', async ({ page }) => {
|
||||
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
@@ -68,7 +68,6 @@ test.describe('Renaming Timer Object @unstable', () => {
|
||||
|
||||
//Assert that the name has changed in the browser bar to the value we assigned above
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName);
|
||||
|
||||
});
|
||||
test('An existing Timer object can be renamed twice', async ({ page }) => {
|
||||
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
|
||||
@@ -25,21 +25,22 @@ This test suite is dedicated to testing our use of our custom fixtures to verify
|
||||
that they are working as expected.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures.js');
|
||||
const { test } = require('../../pluginFixtures.js');
|
||||
|
||||
test.describe('pluginFixtures tests', () => {
|
||||
test.use({ domainObjectName: 'Timer' });
|
||||
let timerUUID;
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.describe.skip('pluginFixtures tests', () => {
|
||||
// test.use({ domainObjectName: 'Timer' });
|
||||
// let timerUUID;
|
||||
|
||||
test('Creates a timer object @framework @unstable', ({ domainObject }) => {
|
||||
const { uuid } = domainObject;
|
||||
const uuidRegexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/;
|
||||
expect(uuid).toMatch(uuidRegexp);
|
||||
timerUUID = uuid;
|
||||
});
|
||||
// test('Creates a timer object @framework @unstable', ({ domainObject }) => {
|
||||
// const { uuid } = domainObject;
|
||||
// const uuidRegexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/;
|
||||
// expect(uuid).toMatch(uuidRegexp);
|
||||
// timerUUID = uuid;
|
||||
// });
|
||||
|
||||
test('Provides same uuid for subsequent uses of the same object @framework', ({ domainObject }) => {
|
||||
const { uuid } = domainObject;
|
||||
expect(uuid).toEqual(timerUUID);
|
||||
});
|
||||
// test('Provides same uuid for subsequent uses of the same object @framework', ({ domainObject }) => {
|
||||
// const { uuid } = domainObject;
|
||||
// expect(uuid).toEqual(timerUUID);
|
||||
// });
|
||||
});
|
||||
|
||||
@@ -38,14 +38,14 @@ test.describe('Branding tests', () => {
|
||||
await expect(page.locator('.c-about__image')).toBeVisible();
|
||||
|
||||
// Modify the Build information in 'about' Modal
|
||||
const versionInformationLocator = page.locator('ul.t-info.l-info.s-info');
|
||||
const versionInformationLocator = page.locator('ul.t-info.l-info.s-info').first();
|
||||
await expect(versionInformationLocator).toBeEnabled();
|
||||
await expect.soft(versionInformationLocator).toContainText(/Version: \d/);
|
||||
await expect.soft(versionInformationLocator).toContainText(/Build Date: ((?:Mon|Tue|Wed|Thu|Fri|Sat|Sun))/);
|
||||
await expect.soft(versionInformationLocator).toContainText(/Revision: \b[0-9a-f]{5,40}\b/);
|
||||
await expect.soft(versionInformationLocator).toContainText(/Branch: ./);
|
||||
});
|
||||
test('Verify Links in About Modal', async ({ page }) => {
|
||||
test('Verify Links in About Modal @2p', async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ but only assume that example imagery is present.
|
||||
|
||||
const { waitForAnimations } = require('../../../../baseFixtures');
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const backgroundImageSelector = '.c-imagery__main-image__background-image';
|
||||
const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
|
||||
const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
|
||||
@@ -39,26 +40,17 @@ test.describe('Example Imagery Object', () => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
// Create a default 'Example Imagery' object
|
||||
createDomainObjectWithDefaults(page, 'Example Imagery');
|
||||
|
||||
// Click text=Example Imagery
|
||||
await page.click('text=Example Imagery');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK'),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
page.waitForNavigation(),
|
||||
page.locator(backgroundImageSelector).hover({trial: true}),
|
||||
// eslint-disable-next-line playwright/missing-playwright-await
|
||||
expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery')
|
||||
]);
|
||||
// Close Banner
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
|
||||
//Wait until Save Banner is gone
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
// Verify that the created object is focused
|
||||
});
|
||||
|
||||
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
|
||||
@@ -208,7 +200,7 @@ test.describe('Example Imagery Object', () => {
|
||||
const pausePlayButton = page.locator('.c-button.pause-play');
|
||||
|
||||
// open the time conductor drop down
|
||||
await page.locator('button:has-text("Fixed Timespan")').click();
|
||||
await page.locator('.c-mode-button').click();
|
||||
|
||||
// Click local clock
|
||||
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
|
||||
@@ -532,7 +524,7 @@ test.describe('Example Imagery in Flexible layout', () => {
|
||||
await page.locator('.c-mode-button').click();
|
||||
|
||||
// Select local clock mode
|
||||
await page.locator('[data-testid=conductor-modeOption-realtime]').click();
|
||||
await page.locator('[data-testid=conductor-modeOption-realtime]').nth(0).click();
|
||||
|
||||
// Zoom in on next image
|
||||
await mouseZoomIn(page);
|
||||
|
||||
@@ -25,25 +25,18 @@ This test suite is dedicated to tests which verify form functionality.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
|
||||
/**
|
||||
* Creates a notebook object and adds an entry.
|
||||
* @param {import('@playwright/test').Page} - page to load
|
||||
* @param {number} [iterations = 1] - the number of entries to create
|
||||
*/
|
||||
async function createNotebookAndEntry(page, myItemsFolderName, iterations = 1) {
|
||||
async function createNotebookAndEntry(page, iterations = 1) {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('[title="Create and save timestamped notes with embedded object snapshots."]').click();
|
||||
// Click button:has-text("OK")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator(`[name="mctForm"] >> text=${myItemsFolderName}`).click(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
createDomainObjectWithDefaults(page, 'Notebook');
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
// Click text=To start a new entry, click here or drag and drop any object
|
||||
@@ -52,7 +45,6 @@ async function createNotebookAndEntry(page, myItemsFolderName, iterations = 1) {
|
||||
await page.locator(entryLocator).click();
|
||||
await page.locator(entryLocator).fill(`Entry ${iteration}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,8 +52,8 @@ async function createNotebookAndEntry(page, myItemsFolderName, iterations = 1) {
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} [iterations = 1] - the number of entries (and tags) to create
|
||||
*/
|
||||
async function createNotebookEntryAndTags(page, myItemsFolderName, iterations = 1) {
|
||||
await createNotebookAndEntry(page, myItemsFolderName, iterations);
|
||||
async function createNotebookEntryAndTags(page, iterations = 1) {
|
||||
await createNotebookAndEntry(page, iterations);
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
// Click text=To start a new entry, click here or drag and drop any object
|
||||
@@ -81,11 +73,10 @@ async function createNotebookEntryAndTags(page, myItemsFolderName, iterations =
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('Tagging in Notebooks', () => {
|
||||
test('Can load tags', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
test.describe('Tagging in Notebooks @addInit', () => {
|
||||
test('Can load tags', async ({ page }) => {
|
||||
|
||||
await createNotebookAndEntry(page, myItemsFolderName);
|
||||
await createNotebookAndEntry(page);
|
||||
// Click text=To start a new entry, click here or drag and drop any object
|
||||
await page.locator('button:has-text("Add Tag")').click();
|
||||
|
||||
@@ -96,10 +87,8 @@ test.describe('Tagging in Notebooks', () => {
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Driving");
|
||||
});
|
||||
test('Can add tags', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
await createNotebookEntryAndTags(page, myItemsFolderName);
|
||||
test('Can add tags', async ({ page }) => {
|
||||
await createNotebookEntryAndTags(page);
|
||||
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving");
|
||||
@@ -113,10 +102,8 @@ test.describe('Tagging in Notebooks', () => {
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Driving");
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
|
||||
});
|
||||
test('Can search for tags', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
await createNotebookEntryAndTags(page, myItemsFolderName);
|
||||
test('Can search for tags', async ({ page }) => {
|
||||
await createNotebookEntryAndTags(page);
|
||||
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
@@ -139,10 +126,8 @@ test.describe('Tagging in Notebooks', () => {
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('Can delete tags', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
await createNotebookEntryAndTags(page, myItemsFolderName);
|
||||
test('Can delete tags', async ({ page }) => {
|
||||
await createNotebookEntryAndTags(page);
|
||||
await page.locator('[aria-label="Notebook Entries"]').click();
|
||||
// Delete Driving
|
||||
await page.locator('text=Science Driving Add Tag >> button').nth(1).click();
|
||||
@@ -154,28 +139,14 @@ test.describe('Tagging in Notebooks', () => {
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
|
||||
});
|
||||
test('Tags persist across reload', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
test('Tags persist across reload', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create a clock object we can navigate to
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click Clock
|
||||
await page.click('text=Clock');
|
||||
// Click button:has-text("OK")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator(`[name="mctForm"] >> text=${myItemsFolderName}`).click(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
await page.click('.c-disclosure-triangle');
|
||||
await createDomainObjectWithDefaults(page, 'Clock');
|
||||
|
||||
const ITERATIONS = 4;
|
||||
await createNotebookEntryAndTags(page, myItemsFolderName, ITERATIONS);
|
||||
await createNotebookEntryAndTags(page, ITERATIONS);
|
||||
|
||||
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
|
||||
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
|
||||
@@ -183,6 +154,11 @@ test.describe('Tagging in Notebooks', () => {
|
||||
await expect(page.locator(entryLocator)).toContainText("Driving");
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.goto('./#/browse/mine?hideTree=false'),
|
||||
page.click('.c-disclosure-triangle')
|
||||
]);
|
||||
// Click Unnamed Clock
|
||||
await page.click('text="Unnamed Clock"');
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ test.describe('Time conductor input fields real-time mode', () => {
|
||||
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01');
|
||||
|
||||
// Verify url parameters persist after mode switch
|
||||
await page.waitForNavigation();
|
||||
await page.waitForNavigation({ waitUntil: 'networkidle' });
|
||||
expect(page.url()).toContain(`startDelta=${startDelta}`);
|
||||
expect(page.url()).toContain(`endDelta=${endDelta}`);
|
||||
});
|
||||
|
||||
@@ -21,14 +21,14 @@
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { openObjectTreeContextMenu } = require('../../../../appActions');
|
||||
|
||||
const options = {
|
||||
type: 'Timer'
|
||||
};
|
||||
const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
|
||||
test.describe('Timer', () => {
|
||||
test.use({ objectCreateOptions: options });
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await createDomainObjectWithDefaults(page, 'timer');
|
||||
});
|
||||
|
||||
test('Can perform actions on the Timer', async ({ page, openmctConfig }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
|
||||
@@ -32,39 +32,31 @@ Note: Larger testsuite sizes are OK due to the setup time associated with these
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { test, expect } = require('../../baseFixtures.js');
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const path = require('path');
|
||||
|
||||
const VISUAL_GRACE_PERIOD = 5 * 1000; //Lets the application "simmer" before the snapshot is taken
|
||||
|
||||
const CUSTOM_NAME = 'CUSTOM_NAME';
|
||||
|
||||
test.describe('Visual - addInit', () => {
|
||||
test.use({
|
||||
clockOptions: {
|
||||
shouldAdvanceTime: true
|
||||
now: 0, //Set browser clock to UNIX Epoch
|
||||
shouldAdvanceTime: false //Don't advance the clock
|
||||
}
|
||||
});
|
||||
|
||||
test('Restricted Notebook is visually correct @addInit', async ({ page }) => {
|
||||
test('Restricted Notebook is visually correct @addInit @unstable', async ({ page, theme }) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
await page.addInitScript({ path: path.join(__dirname, '../../helper', './addInitRestrictedNotebook.js') });
|
||||
//Go to baseURL
|
||||
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
// Click text=CUSTOM_NAME
|
||||
await page.click(`text=${CUSTOM_NAME}`);
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK')
|
||||
]);
|
||||
|
||||
await createDomainObjectWithDefaults(page, CUSTOM_NAME);
|
||||
|
||||
// Take a snapshot of the newly created CUSTOM_NAME notebook
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'Restricted Notebook with CUSTOM_NAME');
|
||||
await percySnapshot(page, `Restricted Notebook with CUSTOM_NAME (theme: '${theme}')`);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,27 +25,21 @@ Collection of Visual Tests set to run in a default context. The tests within thi
|
||||
are only meant to run against openmct's app.js started by `npm run start` within the
|
||||
`./e2e/playwright-visual.config.js` file.
|
||||
|
||||
These should only use functional expect statements to verify assumptions about the state
|
||||
in a test and not for functional verification of correctness. Visual tests are not supposed
|
||||
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
|
||||
|
||||
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../baseFixtures.js');
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
|
||||
test.describe('Visual - Controlled Clock', () => {
|
||||
test.describe('Visual - Controlled Clock @localStorage', () => {
|
||||
test.use({
|
||||
storageState: './e2e/test-data/VisualTestData_storage.json',
|
||||
clockOptions: {
|
||||
now: 0, //Set browser clock to UNIX Epoch
|
||||
shouldAdvanceTime: false, //Don't advance the clock
|
||||
toFake: ["setTimeout", "nextTick"]
|
||||
shouldAdvanceTime: false //Don't advance the clock
|
||||
}
|
||||
});
|
||||
|
||||
test('Overlay Plot Loading Indicator @localstorage', async ({ page }) => {
|
||||
test('Overlay Plot Loading Indicator @localStorage', async ({ page, theme }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
||||
|
||||
@@ -57,6 +51,6 @@ test.describe('Visual - Controlled Clock', () => {
|
||||
await page.locator('canvas >> nth=1').hover({trial: true});
|
||||
|
||||
//Take snapshot of Sine Wave Generator within Overlay Plot
|
||||
await percySnapshot(page, 'SineWaveInOverlayPlot');
|
||||
await percySnapshot(page, `SineWaveInOverlayPlot (theme: '${theme}')`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,85 +32,62 @@ 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, expect } = require('../../baseFixtures.js');
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const VISUAL_GRACE_PERIOD = 5 * 1000; //Lets the application "simmer" before the snapshot is taken
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
|
||||
test.describe('Visual - Default', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Go to baseURL and Hide Tree
|
||||
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
||||
});
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: 0,
|
||||
shouldAdvanceTime: true
|
||||
now: 0, //Set browser clock to UNIX Epoch
|
||||
shouldAdvanceTime: false //Don't advance the clock
|
||||
}
|
||||
});
|
||||
|
||||
test('Visual - Root and About', async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
||||
|
||||
test('Visual - Root and About', async ({ page, theme }) => {
|
||||
// Verify that Create button is actionable
|
||||
await expect(page.locator('button:has-text("Create")')).toBeEnabled();
|
||||
|
||||
// Take a snapshot of the Dashboard
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'Root');
|
||||
await percySnapshot(page, `Root (theme: '${theme}')`);
|
||||
|
||||
// Click About button
|
||||
await page.click('.l-shell__app-logo');
|
||||
|
||||
// Modify the Build information in 'about' to be consistent run-over-run
|
||||
const versionInformationLocator = page.locator('ul.t-info.l-info.s-info');
|
||||
const versionInformationLocator = page.locator('ul.t-info.l-info.s-info').first();
|
||||
await expect(versionInformationLocator).toBeEnabled();
|
||||
await versionInformationLocator.evaluate(node => node.innerHTML = '<li>Version: visual-snapshot</li> <li>Build Date: Mon Nov 15 2021 08:07:51 GMT-0800 (Pacific Standard Time)</li> <li>Revision: 93049cdbc6c047697ca204893db9603b864b8c9f</li> <li>Branch: master</li>');
|
||||
|
||||
// Take a snapshot of the About modal
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'About');
|
||||
await percySnapshot(page, `About (theme: '${theme}')`);
|
||||
});
|
||||
|
||||
test('Visual - Default Condition Set', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
test('Visual - Default Condition Set', async ({ page, theme }) => {
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Condition Set
|
||||
await page.click('text=Condition Set');
|
||||
|
||||
// Click text=OK
|
||||
await page.click('text=OK');
|
||||
await createDomainObjectWithDefaults(page, 'Condition Set');
|
||||
|
||||
// Take a snapshot of the newly created Condition Set object
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'Default Condition Set');
|
||||
await percySnapshot(page, `Default Condition Set (theme: '${theme}')`);
|
||||
});
|
||||
|
||||
test.fixme('Visual - Default Condition Widget', async ({ page }) => {
|
||||
test.fixme('Visual - Default Condition Widget', async ({ page, theme }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5349'
|
||||
});
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Condition Widget
|
||||
await page.click('text=Condition Widget');
|
||||
|
||||
// Click text=OK
|
||||
await page.click('text=OK');
|
||||
await createDomainObjectWithDefaults(page, 'Condition Widget');
|
||||
|
||||
// Take a snapshot of the newly created Condition Widget object
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'Default Condition Widget');
|
||||
await percySnapshot(page, `Default Condition Widget (theme: '${theme}')`);
|
||||
});
|
||||
|
||||
test('Visual - Time Conductor start time is less than end time', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
test('Visual - Time Conductor start time is less than end time', async ({ page, theme }) => {
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
let startDate = 'xxxx-01-01 01:00:00.000Z';
|
||||
@@ -123,16 +100,14 @@ test.describe('Visual - Default', () => {
|
||||
await page.locator('input[type="text"]').first().fill(startDate.toString());
|
||||
|
||||
// verify no error msg
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'Default Time conductor');
|
||||
await percySnapshot(page, `Default Time conductor (theme: '${theme}')`);
|
||||
|
||||
startDate = (year + 1) + startDate.substring(4);
|
||||
await page.locator('input[type="text"]').first().fill(startDate.toString());
|
||||
await page.locator('input[type="text"]').nth(1).click();
|
||||
|
||||
// verify error msg for start time (unable to capture snapshot of popup)
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'Start time error');
|
||||
await percySnapshot(page, `Start time error (theme: '${theme}')`);
|
||||
|
||||
startDate = (year - 1) + startDate.substring(4);
|
||||
await page.locator('input[type="text"]').first().fill(startDate.toString());
|
||||
@@ -143,79 +118,51 @@ test.describe('Visual - Default', () => {
|
||||
await page.locator('input[type="text"]').first().click();
|
||||
|
||||
// verify error msg for end time (unable to capture snapshot of popup)
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'End time error');
|
||||
await percySnapshot(page, `End time error (theme: '${theme}')`);
|
||||
});
|
||||
|
||||
test('Visual - Sine Wave Generator Form', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
test('Visual - Sine Wave Generator Form', async ({ page, theme }) => {
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Sine Wave Generator
|
||||
await page.click('text=Sine Wave Generator');
|
||||
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'Default Sine Wave Generator Form');
|
||||
await percySnapshot(page, `Default Sine Wave Generator Form (theme: '${theme}')`);
|
||||
|
||||
await page.locator('.field.control.l-input-sm input').first().click();
|
||||
await page.locator('.field.control.l-input-sm input').first().fill('');
|
||||
|
||||
// Validate red x mark
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'removed amplitude property value');
|
||||
await percySnapshot(page, `removed amplitude property value (theme: '${theme}')`);
|
||||
});
|
||||
|
||||
test('Visual - Save Successful Banner', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
test('Visual - Save Successful Banner', async ({ page, theme }) => {
|
||||
await createDomainObjectWithDefaults(page, 'Timer');
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
//NOTE Something other than example imagery
|
||||
await page.click('text=Timer');
|
||||
|
||||
// Click text=OK
|
||||
await page.click('text=OK');
|
||||
await page.locator('.c-message-banner__message').hover({ trial: true });
|
||||
await percySnapshot(page, 'Banner message shown');
|
||||
await percySnapshot(page, `Banner message shown (theme: '${theme}')`);
|
||||
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
await percySnapshot(page, 'Banner message gone');
|
||||
await percySnapshot(page, `Banner message gone (theme: '${theme}')`);
|
||||
});
|
||||
|
||||
test('Visual - Display Layout Icon is correct', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
test('Visual - Display Layout Icon is correct', async ({ page, theme }) => {
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
//Hover on Display Layout option.
|
||||
await page.locator('text=Display Layout').hover();
|
||||
await percySnapshot(page, 'Display Layout Create Menu');
|
||||
await percySnapshot(page, `Display Layout Create Menu (theme: '${theme}')`);
|
||||
|
||||
});
|
||||
|
||||
test('Visual - Default Gauge is correct', async ({ page }) => {
|
||||
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
await page.click('text=Gauge');
|
||||
|
||||
await page.click('text=OK');
|
||||
test('Visual - Default Gauge is correct', async ({ page, theme }) => {
|
||||
await createDomainObjectWithDefaults(page, 'Gauge');
|
||||
|
||||
// Take a snapshot of the newly created Gauge object
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'Default Gauge');
|
||||
|
||||
await percySnapshot(page, `Default Gauge (theme: '${theme}')`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,81 +24,58 @@
|
||||
This test suite is dedicated to tests which verify search functionality.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../baseFixtures.js');
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
|
||||
/**
|
||||
* Creates a notebook object and adds an entry.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function createClockAndDisplayLayout(page) {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
// Click li:has-text("Notebook")
|
||||
await page.locator('li:has-text("Clock")').click();
|
||||
// Click button:has-text("OK")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
// Click a:has-text("My Items")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('a:has-text("My Items") >> nth=0').click()
|
||||
]);
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
// Click li:has-text("Notebook")
|
||||
await page.locator('li:has-text("Display Layout")').click();
|
||||
// Click button:has-text("OK")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
}
|
||||
|
||||
test.describe('Grand Search', () => {
|
||||
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page }) => {
|
||||
await createClockAndDisplayLayout(page);
|
||||
test.beforeEach(async ({ page, theme }) => {
|
||||
//Go to baseURL and Hide Tree
|
||||
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
|
||||
});
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: 0, //Set browser clock to UNIX Epoch
|
||||
shouldAdvanceTime: false //Don't advance the clock
|
||||
}
|
||||
});
|
||||
//This needs to be rewritten to use a non clock or non display layout object
|
||||
test('Can search for objects, and subsequent search dropdown behaves properly @unstable', async ({ page, theme }) => {
|
||||
// await createDomainObjectWithDefaults(page, 'Display Layout');
|
||||
// await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
// await page.locator('text=Save and Finish Editing').click();
|
||||
const folder1 = 'Folder1';
|
||||
await createDomainObjectWithDefaults(page, 'Folder', folder1);
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText('Clock');
|
||||
await percySnapshot(page, 'Searching for Clocks');
|
||||
// Click text=Elements >> nth=0
|
||||
await page.locator('text=Elements').first().click();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill(folder1);
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText(folder1);
|
||||
await percySnapshot(page, 'Searching for Folder Object');
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
|
||||
// Click [aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock
|
||||
await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click();
|
||||
await percySnapshot(page, 'Preview for clock should display when editing enabled and search item clicked');
|
||||
|
||||
// Click [aria-label="Close"]
|
||||
await page.locator('[aria-label="Close"]').click();
|
||||
await percySnapshot(page, 'Search should still be showing after preview closed');
|
||||
|
||||
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
// Click text=Save and Finish Editing
|
||||
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"]
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
|
||||
// Click text=Unnamed Clock
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Clock').click()
|
||||
]);
|
||||
await percySnapshot(page, 'Clicking on search results should navigate to them if not editing');
|
||||
await percySnapshot(page, `Clicking on search results should navigate to them if not editing (theme: '${theme}')`);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -161,8 +161,12 @@
|
||||
}
|
||||
|
||||
function sin(timestamp, period, amplitude, offset, phase, randomness) {
|
||||
return amplitude
|
||||
* Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
|
||||
if (Math.round(Math.random())) {
|
||||
return 1 / 0;
|
||||
} else {
|
||||
return amplitude
|
||||
* Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
|
||||
}
|
||||
}
|
||||
|
||||
function wavelengths() {
|
||||
|
||||
10
package.json
10
package.json
@@ -3,9 +3,9 @@
|
||||
"version": "2.1.0-SNAPSHOT",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.18.2",
|
||||
"@babel/eslint-parser": "7.18.9",
|
||||
"@braintree/sanitize-url": "6.0.0",
|
||||
"@percy/cli": "1.2.1",
|
||||
"@percy/cli": "1.7.2",
|
||||
"@percy/playwright": "1.0.4",
|
||||
"@playwright/test": "1.23.0",
|
||||
"@types/eventemitter3": "^1.0.0",
|
||||
@@ -26,7 +26,7 @@
|
||||
"eslint": "8.18.0",
|
||||
"eslint-plugin-compat": "4.0.2",
|
||||
"eslint-plugin-playwright": "0.10.0",
|
||||
"eslint-plugin-vue": "9.1.1",
|
||||
"eslint-plugin-vue": "9.3.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.3.0",
|
||||
"jsdoc": "3.6.11",
|
||||
"karma": "6.3.20",
|
||||
"karma-chrome-launcher": "3.1.1",
|
||||
@@ -94,7 +94,7 @@
|
||||
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",
|
||||
"test:e2e: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",
|
||||
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js --grep-invert @unstable",
|
||||
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js",
|
||||
"test:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
|
||||
"test:watch": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
|
||||
|
||||
@@ -88,7 +88,7 @@ export default class ObjectAPI {
|
||||
this.cache = {};
|
||||
this.interceptorRegistry = new InterceptorRegistry();
|
||||
|
||||
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan'];
|
||||
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan', 'annotation'];
|
||||
|
||||
this.errors = {
|
||||
Conflict: ConflictError
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,7 @@ describe("The Object API", () => {
|
||||
let openmct = {};
|
||||
let mockDomainObject;
|
||||
const TEST_NAMESPACE = "test-namespace";
|
||||
const TEST_KEY = "test-key";
|
||||
const FIFTEEN_MINUTES = 15 * 60 * 1000;
|
||||
|
||||
beforeEach((done) => {
|
||||
@@ -22,7 +23,7 @@ describe("The Object API", () => {
|
||||
mockDomainObject = {
|
||||
identifier: {
|
||||
namespace: TEST_NAMESPACE,
|
||||
key: "test-key"
|
||||
key: TEST_KEY
|
||||
},
|
||||
name: "test object",
|
||||
type: "test-type"
|
||||
@@ -84,6 +85,31 @@ describe("The Object API", () => {
|
||||
expect(mockProvider.create).not.toHaveBeenCalled();
|
||||
expect(mockProvider.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("Shows a notification on persistence conflict", () => {
|
||||
beforeEach(() => {
|
||||
openmct.notifications.error = jasmine.createSpy('error');
|
||||
});
|
||||
|
||||
it("on create", () => {
|
||||
mockProvider.create.and.returnValue(Promise.reject(new openmct.objects.errors.Conflict("Test Conflict error")));
|
||||
|
||||
return objectAPI.save(mockDomainObject).catch(() => {
|
||||
expect(openmct.notifications.error).toHaveBeenCalledWith(`Conflict detected while saving ${TEST_NAMESPACE}:${TEST_KEY}`);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it("on update", () => {
|
||||
mockProvider.update.and.returnValue(Promise.reject(new openmct.objects.errors.Conflict("Test Conflict error")));
|
||||
mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;
|
||||
mockDomainObject.modified = Date.now();
|
||||
|
||||
return objectAPI.save(mockDomainObject).catch(() => {
|
||||
expect(openmct.notifications.error).toHaveBeenCalledWith(`Conflict detected while saving ${TEST_NAMESPACE}:${TEST_KEY}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -138,21 +164,33 @@ describe("The Object API", () => {
|
||||
});
|
||||
|
||||
it("Caches multiple requests for the same object", () => {
|
||||
const promises = [];
|
||||
expect(mockProvider.get.calls.count()).toBe(0);
|
||||
objectAPI.get(mockDomainObject.identifier);
|
||||
promises.push(objectAPI.get(mockDomainObject.identifier));
|
||||
expect(mockProvider.get.calls.count()).toBe(1);
|
||||
objectAPI.get(mockDomainObject.identifier);
|
||||
promises.push(objectAPI.get(mockDomainObject.identifier));
|
||||
expect(mockProvider.get.calls.count()).toBe(1);
|
||||
|
||||
return Promise.all(promises);
|
||||
});
|
||||
|
||||
it("applies any applicable interceptors", () => {
|
||||
expect(mockDomainObject.changed).toBeUndefined();
|
||||
objectAPI.get(mockDomainObject.identifier).then((object) => {
|
||||
|
||||
return objectAPI.get(mockDomainObject.identifier).then((object) => {
|
||||
expect(object.changed).toBeTrue();
|
||||
expect(object.alsoChanged).toBeTrue();
|
||||
expect(object.shouldNotBeChanged).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("displays a notification in the event of an error", () => {
|
||||
mockProvider.get.and.returnValue(Promise.reject());
|
||||
|
||||
return objectAPI.get(mockDomainObject.identifier).catch(() => {
|
||||
expect(openmct.notifications.error).toHaveBeenCalledWith(`Failed to retrieve object ${TEST_NAMESPACE}:${TEST_KEY}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -168,7 +206,7 @@ describe("The Object API", () => {
|
||||
testObject = {
|
||||
identifier: {
|
||||
namespace: TEST_NAMESPACE,
|
||||
key: 'test-key'
|
||||
key: TEST_KEY
|
||||
},
|
||||
name: 'test object',
|
||||
type: 'notebook',
|
||||
@@ -195,6 +233,8 @@ describe("The Object API", () => {
|
||||
"observeObjectChanges"
|
||||
]);
|
||||
mockProvider.get.and.returnValue(Promise.resolve(testObject));
|
||||
mockProvider.create.and.returnValue(Promise.resolve(true));
|
||||
mockProvider.update.and.returnValue(Promise.resolve(true));
|
||||
mockProvider.observeObjectChanges.and.callFake(() => {
|
||||
callbacks[0](updatedTestObject);
|
||||
callbacks.splice(0, 1);
|
||||
|
||||
@@ -83,9 +83,11 @@ class UserAPI extends EventEmitter {
|
||||
* @throws Will throw an error if no user provider is set
|
||||
*/
|
||||
getCurrentUser() {
|
||||
this.noProviderCheck();
|
||||
|
||||
return this._provider.getCurrentUser();
|
||||
if (!this.hasProvider()) {
|
||||
return Promise.resolve(undefined);
|
||||
} else {
|
||||
return this._provider.getCurrentUser();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
68
src/plugins/imagery/components/ImageThumbnail.vue
Normal file
68
src/plugins/imagery/components/ImageThumbnail.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="c-imagery__thumb c-thumb"
|
||||
:class="{
|
||||
'active': active,
|
||||
'selected': selected,
|
||||
'real-time': realTime
|
||||
}"
|
||||
:title="image.formattedTime"
|
||||
>
|
||||
<a
|
||||
href=""
|
||||
:download="image.imageDownloadName"
|
||||
@click.prevent
|
||||
>
|
||||
<img
|
||||
class="c-thumb__image"
|
||||
:src="image.url"
|
||||
>
|
||||
</a>
|
||||
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
image: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
realTime: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -166,26 +166,15 @@
|
||||
class="c-imagery__thumbs-scroll-area"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div
|
||||
<ImageThumbnail
|
||||
v-for="(image, index) in imageHistory"
|
||||
:key="image.url + image.time"
|
||||
class="c-imagery__thumb c-thumb"
|
||||
:class="{ selected: focusedImageIndex === index && isPaused }"
|
||||
:title="image.formattedTime"
|
||||
@click="thumbnailClicked(index)"
|
||||
>
|
||||
<a
|
||||
href=""
|
||||
:download="image.imageDownloadName"
|
||||
@click.prevent
|
||||
>
|
||||
<img
|
||||
class="c-thumb__image"
|
||||
:src="image.url"
|
||||
>
|
||||
</a>
|
||||
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
|
||||
</div>
|
||||
:image="image"
|
||||
:active="focusedImageIndex === index"
|
||||
:selected="focusedImageIndex === index && isPaused"
|
||||
:real-time="!isFixed"
|
||||
@click.native="thumbnailClicked(index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -205,6 +194,7 @@ import moment from 'moment';
|
||||
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
|
||||
import Compass from './Compass/Compass.vue';
|
||||
import ImageControls from './ImageControls.vue';
|
||||
import ImageThumbnail from './ImageThumbnail.vue';
|
||||
import imageryData from "../../imagery/mixins/imageryData";
|
||||
|
||||
const REFRESH_CSS_MS = 500;
|
||||
@@ -229,9 +219,11 @@ const SHOW_THUMBS_THRESHOLD_HEIGHT = 200;
|
||||
const SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600;
|
||||
|
||||
export default {
|
||||
name: 'ImageryView',
|
||||
components: {
|
||||
Compass,
|
||||
ImageControls
|
||||
ImageControls,
|
||||
ImageThumbnail
|
||||
},
|
||||
mixins: [imageryData],
|
||||
inject: ['openmct', 'domainObject', 'objectPath', 'currentView', 'imageFreshnessOptions'],
|
||||
@@ -254,6 +246,7 @@ export default {
|
||||
visibleLayers: [],
|
||||
durationFormatter: undefined,
|
||||
imageHistory: [],
|
||||
bounds: {},
|
||||
timeSystem: timeSystem,
|
||||
keyString: undefined,
|
||||
autoScroll: true,
|
||||
@@ -569,6 +562,16 @@ export default {
|
||||
this.resetAgeCSS();
|
||||
this.updateRelatedTelemetryForFocusedImage();
|
||||
this.getImageNaturalDimensions();
|
||||
},
|
||||
bounds() {
|
||||
this.scrollToFocused();
|
||||
},
|
||||
isFixed(newValue) {
|
||||
const isRealTime = !newValue;
|
||||
// if realtime unpause which will focus on latest image
|
||||
if (isRealTime) {
|
||||
this.paused(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
@@ -610,6 +613,7 @@ export default {
|
||||
this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY);
|
||||
this.handleThumbWindowResizeEnded = _.debounce(this.handleThumbWindowResizeEnded, SCROLL_LATENCY);
|
||||
this.handleThumbWindowResizeStart = _.debounce(this.handleThumbWindowResizeStart, SCROLL_LATENCY);
|
||||
this.scrollToFocused = _.debounce(this.scrollToFocused, 400);
|
||||
|
||||
if (this.$refs.thumbsWrapper) {
|
||||
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
|
||||
@@ -845,7 +849,8 @@ export default {
|
||||
if (domThumb) {
|
||||
domThumb.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
block: 'center',
|
||||
inline: 'center'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -258,13 +258,22 @@
|
||||
min-width: $w;
|
||||
width: $w;
|
||||
|
||||
&.active {
|
||||
background: $colorSelectedBg;
|
||||
color: $colorSelectedFg;
|
||||
}
|
||||
&:hover {
|
||||
background: $colorThumbHoverBg;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: $colorPausedBg !important;
|
||||
color: $colorPausedFg !important;
|
||||
// fixed time - selected bg will match active bg color
|
||||
background: $colorSelectedBg;
|
||||
color: $colorSelectedFg;
|
||||
&.real-time {
|
||||
// real time - bg orange when selected
|
||||
background: $colorPausedBg !important;
|
||||
color: $colorPausedFg !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__image {
|
||||
|
||||
@@ -139,6 +139,7 @@ export default {
|
||||
// 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() {
|
||||
this.timeSystem = this.timeContext.timeSystem();
|
||||
|
||||
@@ -296,12 +296,17 @@ export default {
|
||||
window.addEventListener('orientationchange', this.formatSidebar);
|
||||
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||
this.filterAndSortEntries();
|
||||
this.unobserveEntries = this.openmct.objects.observe(this.domainObject, '*', this.filterAndSortEntries);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
|
||||
if (this.unobserveEntries) {
|
||||
this.unobserveEntries();
|
||||
}
|
||||
|
||||
window.removeEventListener('orientationchange', this.formatSidebar);
|
||||
window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||
},
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
:annotation-type="openmct.annotation.ANNOTATION_TYPES.NOTEBOOK"
|
||||
:annotation-search-type="openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS"
|
||||
:target-specific-details="{entryId: entry.id}"
|
||||
@tags-updated="timestampAndUpdate"
|
||||
/>
|
||||
|
||||
<div class="c-snapshots c-ne__embeds">
|
||||
@@ -146,6 +147,8 @@ import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '..
|
||||
|
||||
import Moment from 'moment';
|
||||
|
||||
const UNKNOWN_USER = 'Unknown';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NotebookEmbed,
|
||||
@@ -206,7 +209,8 @@ export default {
|
||||
|
||||
return {
|
||||
targetKeyString,
|
||||
entryId: this.entry.id
|
||||
entryId: this.entry.id,
|
||||
modified: this.entry.modified
|
||||
};
|
||||
},
|
||||
createdOnTime() {
|
||||
@@ -283,7 +287,7 @@ export default {
|
||||
await this.addNewEmbed(objectPath);
|
||||
}
|
||||
|
||||
this.$emit('updateEntry', this.entry);
|
||||
this.timestampAndUpdate();
|
||||
},
|
||||
findPositionInArray(array, id) {
|
||||
let position = -1;
|
||||
@@ -321,7 +325,7 @@ export default {
|
||||
// TODO: remove notebook snapshot object using object remove API
|
||||
this.entry.embeds.splice(embedPosition, 1);
|
||||
|
||||
this.$emit('updateEntry', this.entry);
|
||||
this.timestampAndUpdate();
|
||||
},
|
||||
updateEmbed(newEmbed) {
|
||||
this.entry.embeds.some(e => {
|
||||
@@ -333,6 +337,17 @@ export default {
|
||||
return found;
|
||||
});
|
||||
|
||||
this.timestampAndUpdate();
|
||||
},
|
||||
async timestampAndUpdate() {
|
||||
const user = await this.openmct.user.getCurrentUser();
|
||||
|
||||
if (user === undefined) {
|
||||
this.entry.modifiedBy = UNKNOWN_USER;
|
||||
}
|
||||
|
||||
this.entry.modified = Date.now();
|
||||
|
||||
this.$emit('updateEntry', this.entry);
|
||||
},
|
||||
editingEntry() {
|
||||
@@ -342,7 +357,7 @@ export default {
|
||||
const value = $event.target.innerText;
|
||||
if (value !== this.entry.text && value.match(/\S/)) {
|
||||
this.entry.text = value;
|
||||
this.$emit('updateEntry', this.entry);
|
||||
this.timestampAndUpdate();
|
||||
} else {
|
||||
this.$emit('cancelEdit');
|
||||
}
|
||||
|
||||
@@ -211,10 +211,17 @@ describe("Notebook plugin:", () => {
|
||||
|
||||
describe("synchronization", () => {
|
||||
|
||||
let objectCloneToSyncFrom;
|
||||
|
||||
beforeEach(() => {
|
||||
objectCloneToSyncFrom = structuredClone(notebookViewObject);
|
||||
objectCloneToSyncFrom.persisted = notebookViewObject.modified + 1;
|
||||
});
|
||||
|
||||
it("updates an entry when another user modifies it", () => {
|
||||
expect(getEntryText(0).innerText).toBe("First Test Entry");
|
||||
notebookViewObject.configuration.entries["test-section-1"]["test-page-1"][0].text = "Modified entry text";
|
||||
objectProviderObserver(notebookViewObject);
|
||||
objectCloneToSyncFrom.configuration.entries["test-section-1"]["test-page-1"][0].text = "Modified entry text";
|
||||
objectProviderObserver(objectCloneToSyncFrom);
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(getEntryText(0).innerText).toBe("Modified entry text");
|
||||
@@ -223,13 +230,13 @@ describe("Notebook plugin:", () => {
|
||||
|
||||
it("shows new entry when another user adds one", () => {
|
||||
expect(allNotebookEntryElements().length).toBe(2);
|
||||
notebookViewObject.configuration.entries["test-section-1"]["test-page-1"].push({
|
||||
objectCloneToSyncFrom.configuration.entries["test-section-1"]["test-page-1"].push({
|
||||
"id": "entry-3",
|
||||
"createdOn": 0,
|
||||
"text": "Third Test Entry",
|
||||
"embeds": []
|
||||
});
|
||||
objectProviderObserver(notebookViewObject);
|
||||
objectProviderObserver(objectCloneToSyncFrom);
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(allNotebookEntryElements().length).toBe(3);
|
||||
@@ -237,9 +244,9 @@ describe("Notebook plugin:", () => {
|
||||
});
|
||||
it("removes an entry when another user removes one", () => {
|
||||
expect(allNotebookEntryElements().length).toBe(2);
|
||||
let entries = notebookViewObject.configuration.entries["test-section-1"]["test-page-1"];
|
||||
notebookViewObject.configuration.entries["test-section-1"]["test-page-1"] = entries.splice(0, 1);
|
||||
objectProviderObserver(notebookViewObject);
|
||||
let entries = objectCloneToSyncFrom.configuration.entries["test-section-1"]["test-page-1"];
|
||||
objectCloneToSyncFrom.configuration.entries["test-section-1"]["test-page-1"] = entries.splice(0, 1);
|
||||
objectProviderObserver(objectCloneToSyncFrom);
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(allNotebookEntryElements().length).toBe(1);
|
||||
@@ -256,8 +263,8 @@ describe("Notebook plugin:", () => {
|
||||
};
|
||||
|
||||
expect(allNotebookPageElements().length).toBe(2);
|
||||
notebookViewObject.configuration.sections[0].pages.push(newPage);
|
||||
objectProviderObserver(notebookViewObject);
|
||||
objectCloneToSyncFrom.configuration.sections[0].pages.push(newPage);
|
||||
objectProviderObserver(objectCloneToSyncFrom);
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(allNotebookPageElements().length).toBe(3);
|
||||
@@ -267,8 +274,8 @@ describe("Notebook plugin:", () => {
|
||||
|
||||
it("updates the notebook when a user removes a page", () => {
|
||||
expect(allNotebookPageElements().length).toBe(2);
|
||||
notebookViewObject.configuration.sections[0].pages.splice(0, 1);
|
||||
objectProviderObserver(notebookViewObject);
|
||||
objectCloneToSyncFrom.configuration.sections[0].pages.splice(0, 1);
|
||||
objectProviderObserver(objectCloneToSyncFrom);
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(allNotebookPageElements().length).toBe(1);
|
||||
@@ -291,8 +298,8 @@ describe("Notebook plugin:", () => {
|
||||
};
|
||||
|
||||
expect(allNotebookSectionElements().length).toBe(2);
|
||||
notebookViewObject.configuration.sections.push(newSection);
|
||||
objectProviderObserver(notebookViewObject);
|
||||
objectCloneToSyncFrom.configuration.sections.push(newSection);
|
||||
objectProviderObserver(objectCloneToSyncFrom);
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(allNotebookSectionElements().length).toBe(3);
|
||||
@@ -301,8 +308,8 @@ describe("Notebook plugin:", () => {
|
||||
|
||||
it("updates the notebook when a user removes a section", () => {
|
||||
expect(allNotebookSectionElements().length).toBe(2);
|
||||
notebookViewObject.configuration.sections.splice(0, 1);
|
||||
objectProviderObserver(notebookViewObject);
|
||||
objectCloneToSyncFrom.configuration.sections.splice(0, 1);
|
||||
objectProviderObserver(objectCloneToSyncFrom);
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(allNotebookSectionElements().length).toBe(1);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +289,7 @@ class CouchObjectProvider {
|
||||
this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]);
|
||||
}
|
||||
|
||||
if (isNotebookType(object)) {
|
||||
if (isNotebookType(object) || object.type === 'annotation') {
|
||||
//Temporary measure until object sync is supported for all object types
|
||||
//Always update notebook revision number because we have realtime sync, so always assume it's the latest.
|
||||
this.objectQueue[key].updateRevision(response[REV]);
|
||||
@@ -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);
|
||||
|
||||
@@ -58,7 +58,6 @@ define([
|
||||
'./condition/plugin',
|
||||
'./conditionWidget/plugin',
|
||||
'./themes/espresso',
|
||||
'./themes/maelstrom',
|
||||
'./themes/snow',
|
||||
'./URLTimeSettingsSynchronizer/plugin',
|
||||
'./notificationIndicator/plugin',
|
||||
@@ -122,7 +121,6 @@ define([
|
||||
ConditionPlugin,
|
||||
ConditionWidgetPlugin,
|
||||
Espresso,
|
||||
Maelstrom,
|
||||
Snow,
|
||||
URLTimeSettingsSynchronizer,
|
||||
NotificationIndicator,
|
||||
@@ -207,7 +205,6 @@ define([
|
||||
plugins.ClearData = ClearData;
|
||||
plugins.WebPage = WebPagePlugin.default;
|
||||
plugins.Espresso = Espresso.default;
|
||||
plugins.Maelstrom = Maelstrom.default;
|
||||
plugins.Snow = Snow.default;
|
||||
plugins.Condition = ConditionPlugin.default;
|
||||
plugins.ConditionWidget = ConditionWidgetPlugin.default;
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
@import "../../styles/vendor/normalize-min";
|
||||
@import "../../styles/constants";
|
||||
@import "../../styles/constants-mobile.scss";
|
||||
|
||||
@import "../../styles/constants-maelstrom";
|
||||
|
||||
@import "../../styles/mixins";
|
||||
@import "../../styles/animations";
|
||||
@import "../../styles/about";
|
||||
@import "../../styles/glyphs";
|
||||
@import "../../styles/global";
|
||||
@import "../../styles/status";
|
||||
@import "../../styles/limits";
|
||||
@import "../../styles/controls";
|
||||
@import "../../styles/forms";
|
||||
@import "../../styles/table";
|
||||
@import "../../styles/legacy";
|
||||
@import "../../styles/legacy-plots";
|
||||
@import "../../styles/plotly";
|
||||
@import "../../styles/legacy-messages";
|
||||
|
||||
@import "../../styles/vue-styles.scss";
|
||||
@@ -1,7 +0,0 @@
|
||||
import { installTheme } from './installTheme';
|
||||
|
||||
export default function plugin() {
|
||||
return function install(openmct) {
|
||||
installTheme(openmct, 'maelstrom');
|
||||
};
|
||||
}
|
||||
@@ -97,14 +97,17 @@ export default {
|
||||
this.tagsChanged(this.annotation.tags);
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
annotationQuery: {
|
||||
handler() {
|
||||
this.unloadAnnotation();
|
||||
this.loadAnnotation();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.annotation = await this.openmct.annotation.getAnnotation(this.annotationQuery, this.annotationSearchType);
|
||||
this.addAnnotationListener(this.annotation);
|
||||
if (this.annotation && this.annotation.tags) {
|
||||
this.tagsChanged(this.annotation.tags);
|
||||
}
|
||||
mounted() {
|
||||
this.loadAnnotation();
|
||||
},
|
||||
destroyed() {
|
||||
if (this.removeTagsListener) {
|
||||
@@ -114,7 +117,23 @@ export default {
|
||||
methods: {
|
||||
addAnnotationListener(annotation) {
|
||||
if (annotation && !this.removeTagsListener) {
|
||||
this.removeTagsListener = this.openmct.objects.observe(annotation, 'tags', this.tagsChanged);
|
||||
this.removeTagsListener = this.openmct.objects.observe(annotation, '*', (newAnnotation) => {
|
||||
this.tagsChanged(newAnnotation.tags);
|
||||
this.annotation = newAnnotation;
|
||||
});
|
||||
}
|
||||
},
|
||||
async loadAnnotation() {
|
||||
this.annotation = await this.openmct.annotation.getAnnotation(this.annotationQuery, this.annotationSearchType);
|
||||
this.addAnnotationListener(this.annotation);
|
||||
if (this.annotation && this.annotation.tags) {
|
||||
this.tagsChanged(this.annotation.tags);
|
||||
}
|
||||
},
|
||||
unloadAnnotation() {
|
||||
if (this.removeTagsListener) {
|
||||
this.removeTagsListener();
|
||||
this.removeTagsListener = undefined;
|
||||
}
|
||||
},
|
||||
tagsChanged(newTags) {
|
||||
@@ -133,8 +152,11 @@ export default {
|
||||
this.addedTags.push(newTagValue);
|
||||
this.userAddingTag = true;
|
||||
},
|
||||
tagRemoved(tagToRemove) {
|
||||
return this.openmct.annotation.removeAnnotationTag(this.annotation, tagToRemove);
|
||||
async tagRemoved(tagToRemove) {
|
||||
const result = await this.openmct.annotation.removeAnnotationTag(this.annotation, tagToRemove);
|
||||
this.$emit('tags-updated');
|
||||
|
||||
return result;
|
||||
},
|
||||
async tagAdded(newTag) {
|
||||
const annotationWasCreated = this.annotation === null || this.annotation === undefined;
|
||||
@@ -146,6 +168,8 @@ export default {
|
||||
|
||||
this.tagsChanged(this.annotation.tags);
|
||||
this.userAddingTag = false;
|
||||
|
||||
this.$emit('tags-updated');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -30,7 +30,6 @@ const config = {
|
||||
inMemorySearchWorker: './src/api/objects/InMemorySearchWorker.js',
|
||||
espressoTheme: './src/plugins/themes/espresso-theme.scss',
|
||||
snowTheme: './src/plugins/themes/snow-theme.scss',
|
||||
maelstromTheme: './src/plugins/themes/maelstrom-theme.scss'
|
||||
},
|
||||
output: {
|
||||
globalObject: 'this',
|
||||
|
||||
Reference in New Issue
Block a user