Compare commits

...

19 Commits

Author SHA1 Message Date
01ae48375c Merge branch 'master' into enable-disabled-tests 2022-08-01 14:02:55 -07:00
f2ed518300 Bump @babel/eslint-parser from 7.18.2 to 7.18.9 (#5530)
Bumps [@babel/eslint-parser](https://github.com/babel/babel/tree/HEAD/eslint/babel-eslint-parser) from 7.18.2 to 7.18.9.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.18.9/eslint/babel-eslint-parser)

---
updated-dependencies:
- dependency-name: "@babel/eslint-parser"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-31 14:36:52 -07:00
bf8d1561ae Bump @percy/cli from 1.6.4 to 1.7.2 (#5583)
Bumps [@percy/cli](https://github.com/percy/cli/tree/HEAD/packages/cli) from 1.6.4 to 1.7.2.
- [Release notes](https://github.com/percy/cli/releases)
- [Commits](https://github.com/percy/cli/commits/v1.7.2/packages/cli)

---
updated-dependencies:
- dependency-name: "@percy/cli"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-31 13:41:15 -07:00
2e1ede1427 Add Visual Test for Snow Theme and add visual tests to PR execution (#5570) 2022-07-29 17:35:43 -07:00
fc3614dfbd [e2e] More VIPER compatibility fixes (#5582) 2022-07-29 16:50:40 -07:00
22924f18fc Better handling of persistence errors (#5576)
* conflict errors in particular
2022-07-29 14:29:34 -07:00
60f20c64d5 Fix multi user notebook (#5563)
* Detect remote changes to notebook object and re-render entries. Detect changes to tags as well

* Do not throw an error when getCurrentUser is called, just return undefined. Code needs a way of testing whether there is a valid user

* Support remote sync of annotations for notebook entries

* Fix bug in notebook spec that prevented multi-user notebook regression being detected

* Fixes edge case where an annotation does not initially exist

* Use structuredClone instead of JSON functions. Fix logical error in entry modification attribution. Address magical value

Co-authored-by: Scott Bell <scott@traclabs.com>
2022-07-29 11:14:09 +02:00
8dc8a1c0a9 Bump jasmine-core from 4.2.0 to 4.3.0 (#5553)
Bumps [jasmine-core](https://github.com/jasmine/jasmine) from 4.2.0 to 4.3.0.
- [Release notes](https://github.com/jasmine/jasmine/releases)
- [Changelog](https://github.com/jasmine/jasmine/blob/main/RELEASE.md)
- [Commits](https://github.com/jasmine/jasmine/compare/v4.2.0...v4.3.0)

---
updated-dependencies:
- dependency-name: jasmine-core
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-07-28 20:19:13 -07:00
710259b5f0 Bump eslint-plugin-vue from 9.1.1 to 9.3.0 (#5554)
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 9.1.1 to 9.3.0.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v9.1.1...v9.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-07-28 13:56:04 -07:00
e774eb01f3 5211 tests arent using source maps master (#5566)
* removed unused require
* simplify config

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-07-28 11:23:41 -07:00
4c4bcfafb7 jQuery to vanilla js fix for test 2022-06-13 16:23:00 -05:00
cd0c01244c Merge branch 'master' into enable-disabled-tests 2022-06-09 05:50:42 -07:00
ab81b07b14 Merge branch 'master' into enable-disabled-tests 2022-06-08 20:08:44 -07:00
c135bdfb20 fixed tests for autoflow plugin 2022-06-08 20:04:58 -07:00
312f90ac59 remove checked in orig file by mistake 2022-06-08 18:34:48 -07:00
508d9c6334 fix merge conflict 2022-06-08 18:33:36 -07:00
53a326c7c3 WIP fixing tests 2022-06-08 18:29:50 -07:00
12610ce054 Merge branch 'master' into enable-disabled-tests 2022-06-08 14:31:27 -07:00
231f4796c0 Re-enable tests, disable plot tests, and add browser types 2022-06-08 14:27:24 -07:00
41 changed files with 834 additions and 693 deletions

View File

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

View File

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

View File

@ -1,9 +0,0 @@
// This is a Babel config that webpack.coverage.js uses in order to instrument
// code with coverage instrumentation.
const babelConfig = {
plugins: [['babel-plugin-istanbul', {
extension: ['.js', '.vue']
}]]
};
module.exports = babelConfig;

View File

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

View File

@ -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}`;
}
/**

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

View File

@ -32,6 +32,7 @@ const config = {
projects: [
{
name: 'chrome',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
use: {
browserName: 'chromium'
}

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,7 +19,6 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import AutoflowTabularPlugin from './AutoflowTabularPlugin';
import AutoflowTabularConstants from './AutoflowTabularConstants';
import DOMObserver from './dom-observer';
import {
@ -29,344 +28,376 @@ import {
} from 'utils/testing';
import Vue from 'vue';
// TODO lots of its without expects
xdescribe("AutoflowTabularPlugin", () => {
let testType;
let testObject;
let mockmct;
describe("AutoflowTabularPlugin", () => {
let testTypeObject;
let autoflowObject;
let otherObject;
let openmct;
let viewProviders;
let autoflowViewProvider;
beforeEach(() => {
testType = "some-type";
testObject = { type: testType };
mockmct = createOpenMct();
spyOn(mockmct.composition, 'get');
spyOn(mockmct.objectViews, 'addProvider');
spyOn(mockmct.telemetry, 'getMetadata');
spyOn(mockmct.telemetry, 'getValueFormatter');
spyOn(mockmct.telemetry, 'limitEvaluator');
spyOn(mockmct.telemetry, 'request');
spyOn(mockmct.telemetry, 'subscribe');
beforeEach(done => {
testTypeObject = {
type: 'some-type'
};
autoflowObject = {
identifier: {
namespace: '',
key: 'some-type-key'
},
type: 'some-type'
};
otherObject = {
identifier: {
namespace: '',
key: 'other-type-key'
},
type: 'other-type'
};
const plugin = new AutoflowTabularPlugin({ type: testType });
plugin(mockmct);
openmct = createOpenMct();
openmct.install(openmct.plugins.AutoflowView(testTypeObject));
spyOn(openmct.composition, 'get');
spyOn(openmct.telemetry, 'getMetadata');
spyOn(openmct.telemetry, 'getValueFormatter');
spyOn(openmct.telemetry, 'limitEvaluator');
spyOn(openmct.telemetry, 'request');
spyOn(openmct.telemetry, 'subscribe');
openmct.on('start', done);
openmct.startHeadless();
viewProviders = openmct.objectViews.get(autoflowObject, []);
autoflowViewProvider = viewProviders.filter(provider => provider?.key === 'autoflow')?.[0];
});
afterEach(() => {
return resetApplicationState(mockmct);
return resetApplicationState(openmct);
});
it("installs a view provider", () => {
expect(mockmct.objectViews.addProvider).toHaveBeenCalled();
expect(autoflowViewProvider).toBeDefined();
});
describe("installs a view provider which", () => {
let provider;
it("applies its view to the type from options", () => {
expect(autoflowViewProvider.canView(autoflowObject, [])).toBeTrue();
});
it("does not apply to other types", () => {
expect(autoflowViewProvider.canView(otherObject, [])).toBeFalse();
});
describe("provides a view which", () => {
let testKeys;
let testChildren;
let testContainer;
let testHistories;
let mockComposition;
let mockMetadata;
let mockEvaluator;
let mockUnsubscribes;
let callbacks;
let view;
let domObserver;
function waitsForChange() {
return new Promise(function (resolve) {
window.requestAnimationFrame(resolve);
});
}
function emitEvent(mockEmitter, type, event) {
mockEmitter.on.calls.all().forEach((call) => {
if (call.args[0] === type) {
call.args[1](event);
}
});
}
beforeEach(() => {
provider =
mockmct.objectViews.addProvider.calls.mostRecent().args[0];
});
callbacks = {};
it("applies its view to the type from options", () => {
expect(provider.canView(testObject, [])).toBe(true);
});
spyOnBuiltins(['requestAnimationFrame']);
window.requestAnimationFrame.and.callFake((callBack) => {
callBack();
});
it("does not apply to other types", () => {
expect(provider.canView({ type: 'foo' }, [])).toBe(false);
});
testKeys = ['abc', 'def', 'xyz'];
testChildren = testKeys.map((key) => {
return {
identifier: {
namespace: "test",
key: key
},
name: "Object " + key
};
});
testContainer = document.createElement('div');
domObserver = new DOMObserver(testContainer);
describe("provides a view which", () => {
let testKeys;
let testChildren;
let testContainer;
let testHistories;
let mockComposition;
let mockMetadata;
let mockEvaluator;
let mockUnsubscribes;
let callbacks;
let view;
let domObserver;
testHistories = testKeys.reduce((histories, key, index) => {
histories[key] = {
key: key,
range: index + 10,
domain: key + index
};
function waitsForChange() {
return new Promise(function (resolve) {
window.requestAnimationFrame(resolve);
return histories;
}, {});
mockComposition =
jasmine.createSpyObj('composition', ['load', 'on', 'off']);
mockMetadata =
jasmine.createSpyObj('metadata', ['valuesForHints']);
mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']);
mockUnsubscribes = testKeys.reduce((map, key) => {
map[key] = jasmine.createSpy('unsubscribe-' + key);
return map;
}, {});
openmct.composition.get.and.returnValue(mockComposition);
mockComposition.load.and.callFake(() => {
testChildren.forEach(emitEvent.bind(null, mockComposition, 'add'));
return Promise.resolve(testChildren);
});
openmct.telemetry.getMetadata.and.returnValue(mockMetadata);
openmct.telemetry.getValueFormatter.and.callFake((metadatum) => {
const mockFormatter = jasmine.createSpyObj('formatter', ['format']);
mockFormatter.format.and.callFake((datum) => {
return datum[metadatum.hint];
});
return mockFormatter;
});
openmct.telemetry.limitEvaluator.and.returnValue(mockEvaluator);
openmct.telemetry.subscribe.and.callFake((obj, callback) => {
const key = obj.identifier.key;
callbacks[key] = callback;
return mockUnsubscribes[key];
});
openmct.telemetry.request.and.callFake((obj, request) => {
const key = obj.identifier.key;
return Promise.resolve([testHistories[key]]);
});
mockMetadata.valuesForHints.and.callFake((hints) => {
return [{ hint: hints[0] }];
});
view = autoflowViewProvider.view(autoflowObject);
view.show(testContainer);
return Vue.nextTick();
});
afterEach(() => {
domObserver.destroy();
});
it("populates its container", () => {
expect(testContainer.children.length > 0).toBe(true);
});
describe("when rows have been populated", () => {
function rowsMatch() {
const rows = testContainer.querySelectorAll(".l-autoflow-row").length;
return rows === testChildren.length;
}
function emitEvent(mockEmitter, type, event) {
mockEmitter.on.calls.all().forEach((call) => {
if (call.args[0] === type) {
call.args[1](event);
}
});
it("shows one row per child object", async () => {
const success = await domObserver.when(rowsMatch);
expect(success).toBeTrue();
});
it("adds rows on composition change", async () => {
const child = {
identifier: {
namespace: "test",
key: "123"
},
name: "Object 123"
};
testChildren.push(child);
emitEvent(mockComposition, 'add', child);
const success = await domObserver.when(rowsMatch);
expect(success).toBeTrue();
});
it("removes rows on composition change", async () => {
const child = testChildren.pop();
emitEvent(mockComposition, 'remove', child.identifier);
const success = await domObserver.when(rowsMatch);
expect(success).toBeTrue();
});
});
it("removes subscriptions when destroyed", () => {
testKeys.forEach((key) => {
expect(mockUnsubscribes[key]).not.toHaveBeenCalled();
});
view.destroy();
testKeys.forEach((key) => {
expect(mockUnsubscribes[key]).toHaveBeenCalled();
});
});
it("provides a button to change column width", async () => {
let buttonClicked;
const initialWidth = testContainer.querySelector('.l-autoflow-col').style.width;
expect(initialWidth.length).toBeGreaterThan(0);
function widthHasChanged() {
if (!buttonClicked) {
buttonClicked = true;
testContainer.querySelector('.change-column-width').click();
}
const changedWidth = testContainer.querySelector('.l-autoflow-col').style.width;
return changedWidth !== initialWidth;
}
beforeEach(() => {
callbacks = {};
const success = await domObserver.when(widthHasChanged);
spyOnBuiltins(['requestAnimationFrame']);
window.requestAnimationFrame.and.callFake((callBack) => {
callBack();
expect(success).toBeTrue();
});
it("subscribes to all child objects", () => {
testKeys.forEach((key) => {
expect(callbacks[key]).toEqual(jasmine.any(Function));
});
});
it("displays historical telemetry", () => {
function rowTextDefined() {
return testContainer.querySelector(".l-autoflow-item.r").textContent !== "";
}
return domObserver.when(rowTextDefined).then(() => {
const rows = testContainer.querySelectorAll(".l-autoflow-row");
testKeys.forEach((key, index) => {
const datum = testHistories[key];
const $cell = rows[index].querySelector(".l-autoflow-item.r");
expect($cell.textContent).toEqual(String(datum.range));
});
});
});
testObject = { type: 'some-type' };
testKeys = ['abc', 'def', 'xyz'];
testChildren = testKeys.map((key) => {
return {
identifier: {
namespace: "test",
key: key
},
name: "Object " + key
};
});
testContainer = document.createElement('div');
domObserver = new DOMObserver(testContainer);
testHistories = testKeys.reduce((histories, key, index) => {
histories[key] = {
key: key,
range: index + 10,
domain: key + index
};
return histories;
}, {});
mockComposition =
jasmine.createSpyObj('composition', ['load', 'on', 'off']);
mockMetadata =
jasmine.createSpyObj('metadata', ['valuesForHints']);
mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']);
mockUnsubscribes = testKeys.reduce((map, key) => {
map[key] = jasmine.createSpy('unsubscribe-' + key);
return map;
}, {});
mockmct.composition.get.and.returnValue(mockComposition);
mockComposition.load.and.callFake(() => {
testChildren.forEach(emitEvent.bind(null, mockComposition, 'add'));
return Promise.resolve(testChildren);
});
mockmct.telemetry.getMetadata.and.returnValue(mockMetadata);
mockmct.telemetry.getValueFormatter.and.callFake((metadatum) => {
const mockFormatter = jasmine.createSpyObj('formatter', ['format']);
mockFormatter.format.and.callFake((datum) => {
return datum[metadatum.hint];
});
return mockFormatter;
});
mockmct.telemetry.limitEvaluator.and.returnValue(mockEvaluator);
mockmct.telemetry.subscribe.and.callFake((obj, callback) => {
const key = obj.identifier.key;
callbacks[key] = callback;
return mockUnsubscribes[key];
});
mockmct.telemetry.request.and.callFake((obj, request) => {
const key = obj.identifier.key;
return Promise.resolve([testHistories[key]]);
});
mockMetadata.valuesForHints.and.callFake((hints) => {
return [{ hint: hints[0] }];
});
view = provider.view(testObject);
view.show(testContainer);
return Vue.nextTick();
it("displays incoming telemetry", () => {
const testData = testKeys.map((key, index) => {
return {
key: key,
range: index * 100,
domain: key + index
};
});
afterEach(() => {
domObserver.destroy();
testData.forEach((datum) => {
callbacks[datum.key](datum);
});
it("populates its container", () => {
expect(testContainer.children.length > 0).toBe(true);
});
return waitsForChange().then(() => {
const rows = testContainer.querySelectorAll(".l-autoflow-row");
describe("when rows have been populated", () => {
function rowsMatch() {
const rows = testContainer.querySelectorAll(".l-autoflow-row").length;
testData.forEach((datum, index) => {
const $cell = rows[index].querySelector(".l-autoflow-item.r");
return rows === testChildren.length;
}
it("shows one row per child object", () => {
return domObserver.when(rowsMatch);
expect($cell.textContent).toEqual(String(datum.range));
});
});
});
// it("adds rows on composition change", () => {
// const child = {
// identifier: {
// namespace: "test",
// key: "123"
// },
// name: "Object 123"
// };
// testChildren.push(child);
// emitEvent(mockComposition, 'add', child);
it("updates classes for limit violations", () => {
const testClass = "some-limit-violation";
// return domObserver.when(rowsMatch);
// });
mockEvaluator.evaluate.and.returnValue({ cssClass: testClass });
it("removes rows on composition change", () => {
const child = testChildren.pop();
emitEvent(mockComposition, 'remove', child.identifier);
return domObserver.when(rowsMatch);
testKeys.forEach((key) => {
callbacks[key]({
range: 'foo',
domain: 'bar'
});
});
it("removes subscriptions when destroyed", () => {
testKeys.forEach((key) => {
expect(mockUnsubscribes[key]).not.toHaveBeenCalled();
});
view.destroy();
testKeys.forEach((key) => {
expect(mockUnsubscribes[key]).toHaveBeenCalled();
return waitsForChange().then(() => {
const rows = testContainer.querySelectorAll(".l-autoflow-row");
testKeys.forEach((datum, index) => {
const $cell = rows[index].querySelector(".l-autoflow-item.r");
expect($cell.classList.contains(testClass)).toBe(true);
});
});
});
it("provides a button to change column width", () => {
const initialWidth = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH;
const nextWidth =
initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP;
it("automatically flows to new columns", () => {
const rowHeight = AutoflowTabularConstants.ROW_HEIGHT;
const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT;
const count = testKeys.length;
const $container = testContainer;
let promiseChain = Promise.resolve();
expect(testContainer.querySelector('.l-autoflow-col').css('width'))
.toEqual(initialWidth + 'px');
function columnsHaveAutoflowed() {
const itemsHeight = $container.querySelector('.l-autoflow-items').style.height;
const availableHeight = itemsHeight - sliderHeight;
const availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1);
const columns = Math.ceil(count / availableRows);
testContainer.querySelector('.change-column-width').click();
return $container.querySelectorAll('.l-autoflow-col').length === columns;
}
function widthHasChanged() {
const width = testContainer.querySelector('.l-autoflow-col').css('width');
const absElement = $container.querySelector('.abs');
absElement.style.position = 'absolute';
absElement.style.left = 0;
absElement.style.right = 0;
absElement.style.top = 0;
absElement.style.bottom = 0;
return width !== initialWidth + 'px';
}
$container.style.position = 'absolute';
return domObserver.when(widthHasChanged)
.then(() => {
expect(testContainer.querySelector('.l-autoflow-col').css('width'))
.toEqual(nextWidth + 'px');
});
document.body.append($container);
function setHeight(height) {
$container.style.height = `${height}px`;
return domObserver.when(columnsHaveAutoflowed);
}
for (let height = 0; height < rowHeight * count * 2; height += rowHeight / 2) {
// eslint-disable-next-line no-invalid-this
promiseChain = promiseChain.then(setHeight.bind(this, height));
}
return promiseChain.then(success => {
expect(success).toBeTrue();
$container.remove();
});
});
it("subscribes to all child objects", () => {
testKeys.forEach((key) => {
expect(callbacks[key]).toEqual(jasmine.any(Function));
});
});
it("displays historical telemetry", () => {
function rowTextDefined() {
return testContainer.querySelector(".l-autoflow-item").filter(".r").text() !== "";
}
return domObserver.when(rowTextDefined).then(() => {
testKeys.forEach((key, index) => {
const datum = testHistories[key];
const $cell = testContainer.querySelector(".l-autoflow-row").eq(index).find(".r");
expect($cell.text()).toEqual(String(datum.range));
});
});
});
it("displays incoming telemetry", () => {
const testData = testKeys.map((key, index) => {
return {
key: key,
range: index * 100,
domain: key + index
};
});
testData.forEach((datum) => {
callbacks[datum.key](datum);
});
return waitsForChange().then(() => {
testData.forEach((datum, index) => {
const $cell = testContainer.querySelector(".l-autoflow-row").eq(index).find(".r");
expect($cell.text()).toEqual(String(datum.range));
});
});
});
it("updates classes for limit violations", () => {
const testClass = "some-limit-violation";
mockEvaluator.evaluate.and.returnValue({ cssClass: testClass });
testKeys.forEach((key) => {
callbacks[key]({
range: 'foo',
domain: 'bar'
});
});
return waitsForChange().then(() => {
testKeys.forEach((datum, index) => {
const $cell = testContainer.querySelector(".l-autoflow-row").eq(index).find(".r");
expect($cell.hasClass(testClass)).toBe(true);
});
});
});
it("automatically flows to new columns", () => {
const rowHeight = AutoflowTabularConstants.ROW_HEIGHT;
const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT;
const count = testKeys.length;
const $container = testContainer;
let promiseChain = Promise.resolve();
function columnsHaveAutoflowed() {
const itemsHeight = $container.querySelector('.l-autoflow-items').height();
const availableHeight = itemsHeight - sliderHeight;
const availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1);
const columns = Math.ceil(count / availableRows);
return $container.querySelector('.l-autoflow-col').length === columns;
}
$container.find('.abs').css({
position: 'absolute',
left: '0px',
right: '0px',
top: '0px',
bottom: '0px'
});
$container.css({ position: 'absolute' });
$container.appendTo(document.body);
function setHeight(height) {
$container.css('height', height + 'px');
return domObserver.when(columnsHaveAutoflowed);
}
for (let height = 0; height < rowHeight * count * 2; height += rowHeight / 2) {
// eslint-disable-next-line no-invalid-this
promiseChain = promiseChain.then(setHeight.bind(this, height));
}
return promiseChain.then(() => {
$container.remove();
});
});
it("loads composition exactly once", () => {
const testObj = testChildren.pop();
emitEvent(mockComposition, 'remove', testObj.identifier);
testChildren.push(testObj);
emitEvent(mockComposition, 'add', testObj);
expect(mockComposition.load.calls.count()).toEqual(1);
});
it("loads composition exactly once", () => {
const testObj = testChildren.pop();
emitEvent(mockComposition, 'remove', testObj.identifier);
testChildren.push(testObj);
emitEvent(mockComposition, 'add', testObj);
expect(mockComposition.load.calls.count()).toEqual(1);
});
});
});

View File

@ -30,7 +30,7 @@ define([], function () {
return new Promise(function (resolve, reject) {
//Test latch function at least once
if (latchFunction()) {
resolve();
resolve(true);
} else {
//Latch condition not true yet, create observer on DOM and test again on change.
const config = {
@ -40,7 +40,7 @@ define([], function () {
};
const observer = new MutationObserver(function () {
if (latchFunction()) {
resolve();
resolve(true);
}
});
observer.observe(this.element, config);

View File

@ -493,6 +493,7 @@ describe("The Imagery View Layouts", () => {
});
});
});
//Note this is a WIP test. Coverage was added in e2e suite
xit('should change the image zoom factor when using the zoom buttons', async (done) => {
await Vue.nextTick();
let imageSizeBefore;
@ -514,6 +515,7 @@ describe("The Imagery View Layouts", () => {
expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width);
done();
});
//Note this is a WIP test. Coverage was added in e2e suite
xit('should reset the zoom factor on the image when clicking the zoom button', async (done) => {
await Vue.nextTick();
// test clicking the zoom reset button

View File

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

View File

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

View File

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

View File

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

View File

@ -28,7 +28,7 @@ import EventEmitter from "EventEmitter";
import PlotOptions from "./inspector/PlotOptions.vue";
import PlotConfigurationModel from "./configuration/PlotConfigurationModel";
describe("the plugin", function () {
xdescribe("the plugin", function () {
let element;
let child;
let openmct;

View File

@ -26,7 +26,7 @@ define([
SummaryWidgetTelemetryProvider
) {
xdescribe('SummaryWidgetTelemetryProvider', function () {
describe('SummaryWidgetTelemetryProvider', function () {
let telemObjectA;
let telemObjectB;
let summaryWidgetObject;

View File

@ -21,7 +21,7 @@
*****************************************************************************/
define(['../src/ConditionManager'], function (ConditionManager) {
xdescribe('A Summary Widget Condition Manager', function () {
describe('A Summary Widget Condition Manager', function () {
let conditionManager;
let mockDomainObject;
let mockCompObject1;
@ -360,7 +360,7 @@ define(['../src/ConditionManager'], function (ConditionManager) {
});
});
it('populates its LAD cache with historial data on load, if available', function (done) {
xit('populates its LAD cache with historial data on load, if available', function (done) {
expect(telemetryRequests.length).toBe(2);
expect(telemetryRequests[0].object).toBe(mockCompObject1);
expect(telemetryRequests[1].object).toBe(mockCompObject2);
@ -379,7 +379,7 @@ define(['../src/ConditionManager'], function (ConditionManager) {
telemetryRequests[1].resolve([mockTelemetryValues.mockCompObject2]);
});
it('updates its LAD cache upon receiving telemetry and invokes the appropriate handlers', function () {
xit('updates its LAD cache upon receiving telemetry and invokes the appropriate handlers', function () {
mockTelemetryAPI.triggerTelemetryCallback('mockCompObject1');
expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual('Its a different string');
mockTelemetryAPI.triggerTelemetryCallback('mockCompObject2');

View File

@ -21,7 +21,7 @@
*****************************************************************************/
define(['../src/Condition'], function (Condition) {
xdescribe('A summary widget condition', function () {
describe('A summary widget condition', function () {
let testCondition;
let mockConfig;
let mockConditionManager;

View File

@ -21,7 +21,7 @@
*****************************************************************************/
define(['../src/SummaryWidget'], function (SummaryWidget) {
xdescribe('The Summary Widget', function () {
describe('The Summary Widget', function () {
let summaryWidget;
let mockDomainObject;
let mockOldDomainObject;
@ -97,7 +97,7 @@ define(['../src/SummaryWidget'], function (SummaryWidget) {
summaryWidget.show(mockContainer);
});
it('queries with legacyId', function () {
xit('queries with legacyId', function () {
expect(mockObjectService.getObjects).toHaveBeenCalledWith(['testNamespace:testKey']);
});
@ -154,14 +154,14 @@ define(['../src/SummaryWidget'], function (SummaryWidget) {
setTimeout(function () {
summaryWidget.onEdit([]);
expect(summaryWidget.editing).toEqual(false);
expect(summaryWidget.ruleArea.css('display')).toEqual('none');
expect(summaryWidget.testDataArea.css('display')).toEqual('none');
expect(summaryWidget.addRuleButton.css('display')).toEqual('none');
expect(summaryWidget.ruleArea.style.display).toEqual('none');
expect(summaryWidget.testDataArea.style.display).toEqual('none');
expect(summaryWidget.addRuleButton.style.display).toEqual('none');
summaryWidget.onEdit(['editing']);
expect(summaryWidget.editing).toEqual(true);
expect(summaryWidget.ruleArea.css('display')).not.toEqual('none');
expect(summaryWidget.testDataArea.css('display')).not.toEqual('none');
expect(summaryWidget.addRuleButton.css('display')).not.toEqual('none');
expect(summaryWidget.ruleArea.style.display).not.toEqual('none');
expect(summaryWidget.testDataArea.style.display).not.toEqual('none');
expect(summaryWidget.addRuleButton.style.display).not.toEqual('none');
}, 100);
});

View File

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

View File

@ -83,7 +83,7 @@ describe("the inspector", () => {
});
});
xit("should allow a saved style to be applied", () => {
it("should allow a saved style to be applied", () => {
spyOn(openmct.editor, 'isEditing').and.returnValue(true);
selection = mockTelemetryTableSelection;

View File

@ -2,7 +2,6 @@
// instrumentation using babel-plugin-istanbul (see babel.coverage.js)
const config = require('./webpack.dev');
const path = require('path');
const vueLoaderRule = config.module.rules.find(r => r.use === 'vue-loader');
// eslint-disable-next-line no-undef
const CI = process.env.CI === 'true';
@ -34,8 +33,11 @@ config.module.rules.push({
use: {
loader: 'babel-loader',
options: {
retainLines: true,
// eslint-disable-next-line no-undef
configFile: path.resolve(process.cwd(), 'babel.coverage.js')
plugins: [['babel-plugin-istanbul', {
extension: ['.js', '.vue']
}]]
}
}
});