This commit is contained in:
Joshi 2022-05-24 15:05:16 -07:00
commit 4e79725897
30 changed files with 578 additions and 180 deletions

View File

@ -27,7 +27,7 @@ assignees: ''
#### Environment
<!--- If encountered on local machine, execute the following:
<!--- npx envinfo --system --browsers --npmPackages --binaries --languages --markdown -->
<!--- npx envinfo --system --browsers --npmPackages --binaries --markdown -->
* Open MCT Version: <!--- date of build, version, or SHA -->
* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yamcs? -->
* OS:

View File

@ -4,10 +4,10 @@
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 0,
testDir: 'tests',
retries: 0, // visual tests should never retry due to snapshot comparison errors
testDir: 'tests/visual',
timeout: 90 * 1000,
workers: 1,
workers: 1, // visual tests should never run in parallel due to test pollution
webServer: {
command: 'npm run start',
port: 8080,
@ -17,7 +17,7 @@ const config = {
use: {
browserName: "chromium",
baseURL: 'http://localhost:8080/',
headless: true,
headless: true, // this needs to remain headless to avoid visual changes due to GPU
ignoreHTTPSErrors: true,
screenshot: 'on',
trace: 'off',
@ -25,8 +25,7 @@ const config = {
},
reporter: [
['list'],
['junit', { outputFile: 'test-results/results.xml' }],
['allure-playwright']
['junit', { outputFile: 'test-results/results.xml' }]
]
};

View File

@ -344,6 +344,95 @@ test('Example Imagery in Display layout', async ({ page }) => {
expect(backgroundImageUrl2 >= backgroundImageUrl1);
});
test.describe('Example imagery thumbnails resize in display layouts', () => {
test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' });
const thumbsWrapperLocator = await page.locator('.c-imagery__thumbs-wrapper');
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Display Layout")
await page.locator('li:has-text("Display Layout")').click();
const displayLayoutTitleField = page.locator('text=Properties Title Notes Horizontal grid (px) Vertical grid (px) Horizontal size ( >> input[type="text"]');
await displayLayoutTitleField.click();
await displayLayoutTitleField.fill('Thumbnail Display Layout');
// Click text=OK
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click()
]);
// 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 button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Example Imagery")
await page.locator('li:has-text("Example Imagery")').click();
const imageryTitleField = page.locator('text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]');
// Click text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]
await imageryTitleField.click();
// Fill text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]
await imageryTitleField.fill('Thumbnail Example Imagery');
// Click text=OK
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click()
]);
// Click text=Thumbnail Example Imagery Imagery Layout Snapshot >> button >> nth=0
await Promise.all([
page.waitForNavigation(),
page.locator('text=Thumbnail Example Imagery Imagery Layout Snapshot >> button').first().click()
]);
// Edit mode
await page.locator('text=Thumbnail Display Layout Snapshot >> button').nth(3).click();
// Click on example imagery to expose toolbar
await page.locator('text=Thumbnail Example Imagery Snapshot Large View').click();
// expect thumbnails not be visible when first added
await expect.soft(thumbsWrapperLocator.isHidden()).toBeTruthy();
// Resize the example imagery vertically to change the thumbnail visibility
/*
The following arbitrary values are added to observe the separate visual
conditions of the thumbnails (hidden, small thumbnails, regular thumbnails).
Specifically, height is set to 50px for small thumbs and 100px for regular
*/
// Click #mct-input-id-103
await page.locator('#mct-input-id-103').click();
// Fill #mct-input-id-103
await page.locator('#mct-input-id-103').fill('50');
expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
await expect(thumbsWrapperLocator).toHaveClass(/is-small-thumbs/);
// Resize the example imagery vertically to change the thumbnail visibility
// Click #mct-input-id-103
await page.locator('#mct-input-id-103').click();
// Fill #mct-input-id-103
await page.locator('#mct-input-id-103').fill('100');
expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
await expect(thumbsWrapperLocator).not.toHaveClass(/is-small-thumbs/);
});
});
test.describe('Example Imagery in Flexible layout', () => {
test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
test.fixme('Can use alt+drag to move around image once zoomed in');

View File

@ -23,9 +23,9 @@
const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test');
test.describe('Time counductor operations', () => {
test.describe('Time conductor operations', () => {
test('validate start time does not exceeds end time', async ({ page }) => {
//Go to baseURL
// Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
const year = new Date().getFullYear();
@ -73,37 +73,163 @@ test.describe('Time counductor operations', () => {
// Try to change the realtime offsets when in realtime (local clock) mode.
test.describe('Time conductor input fields real-time mode', () => {
test('validate input fields in real-time mode', async ({ page }) => {
//Go to baseURL
const startOffset = {
secs: '23'
};
const endOffset = {
secs: '31'
};
// Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Click fixed timespan button
await page.locator('.c-button__label >> text=Fixed Timespan').click();
// Switch to real-time mode
await setRealTimeMode(page);
// Click local clock
await page.locator('.icon-clock >> text=Local Clock').click();
// Click time offset button
await page.locator('.c-conductor__delta-button >> text=00:30:00').click();
// Input start time offset
await page.fill('.pr-time-controls__secs', '23');
// Click the check button
await page.locator('.icon-check').click();
// Set start time offset
await setStartOffset(page, startOffset);
// Verify time was updated on time offset button
await expect(page.locator('.c-conductor__delta-button').first()).toContainText('00:30:23');
await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23');
// Click time offset set preceding now button
await page.locator('.c-conductor__delta-button >> text=00:00:30').click();
// Input preceding time offset
await page.fill('.pr-time-controls__secs', '31');
// Click the check buttons
await page.locator('.icon-check').click();
// Set end time offset
await setEndOffset(page, endOffset);
// Verify time was updated on preceding time offset button
await expect(page.locator('.c-conductor__delta-button').nth(1)).toContainText('00:00:31');
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:31');
});
/**
* Verify that offsets and url params are preserved when switching
* between fixed timespan and real-time mode.
*/
test('preserve offsets and url params when switching between fixed and real-time mode', async ({ page }) => {
const startOffset = {
mins: '30',
secs: '23'
};
const endOffset = {
secs: '01'
};
// Convert offsets to milliseconds
const startDelta = (30 * 60 * 1000) + (23 * 1000);
const endDelta = (1 * 1000);
// Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Switch to real-time mode
await setRealTimeMode(page);
// Set start time offset
await setStartOffset(page, startOffset);
// Set end time offset
await setEndOffset(page, endOffset);
// Switch to fixed timespan mode
await setFixedTimeMode(page);
// Switch back to real-time mode
await setRealTimeMode(page);
// Verify updated start time offset persists after mode switch
await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23');
// Verify updated end time offset persists after mode switch
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01');
// Verify url parameters persist after mode switch
await page.waitForNavigation();
expect(page.url()).toContain(`startDelta=${startDelta}`);
expect(page.url()).toContain(`endDelta=${endDelta}`);
});
});
/**
* @typedef {Object} OffsetValues
* @property {string | undefined} hours
* @property {string | undefined} mins
* @property {string | undefined} secs
*/
/**
* Set the values (hours, mins, secs) for the start time offset when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
*/
async function setStartOffset(page, offset) {
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
await setTimeConductorOffset(page, offset, startOffsetButton);
}
/**
* Set the values (hours, mins, secs) for the end time offset when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
*/
async function setEndOffset(page, offset) {
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
await setTimeConductorOffset(page, offset, endOffsetButton);
}
/**
* Set the time conductor to fixed timespan mode
* @param {import('@playwright/test').Page} page
*/
async function setFixedTimeMode(page) {
await setTimeConductorMode(page, true);
}
/**
* Set the time conductor to realtime mode
* @param {import('@playwright/test').Page} page
*/
async function setRealTimeMode(page) {
await setTimeConductorMode(page, false);
}
/**
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
* @param {import('@playwright/test').Locator} offsetButton
*/
async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) {
await offsetButton.click();
if (hours) {
await page.fill('.pr-time-controls__hrs', hours);
}
if (mins) {
await page.fill('.pr-time-controls__mins', mins);
}
if (secs) {
await page.fill('.pr-time-controls__secs', secs);
}
// Click the check button
await page.locator('.icon-check').click();
}
/**
* Set the time conductor mode to either fixed timespan or realtime mode.
* @param {import('@playwright/test').Page} page
* @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
*/
async function setTimeConductorMode(page, isFixedTimespan = true) {
// Click 'mode' button
await page.locator('.c-mode-button').click();
// Switch time conductor mode
if (isFixedTimespan) {
await page.locator('data-testid=conductor-modeOption-fixed').click();
} else {
await page.locator('data-testid=conductor-modeOption-realtime').click();
}
}

View File

@ -47,7 +47,10 @@ test.beforeEach(async ({ context }) => {
path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js')
});
await context.addInitScript(() => {
window.__clock = sinon.useFakeTimers(); //Set browser clock to UNIX Epoch
window.__clock = sinon.useFakeTimers({
now: 0,
shouldAdvanceTime: true
}); //Set browser clock to UNIX Epoch
});
});
@ -171,3 +174,24 @@ test('Visual - Sine Wave Generator Form', async ({ page }) => {
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'removed amplitude property value');
});
test('Visual - Save Successful Banner', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//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');
//Wait until Save Banner is gone
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await percySnapshot(page, 'Banner message gone');
});

View File

@ -196,6 +196,8 @@
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
openmct.install(openmct.plugins.Timer());
openmct.install(openmct.plugins.Timelist());
openmct.install(openmct.plugins.BarChart());
openmct.install(openmct.plugins.ScatterPlot());
openmct.start();
</script>
</html>

View File

@ -5,7 +5,7 @@
"devDependencies": {
"@babel/eslint-parser": "7.16.3",
"@braintree/sanitize-url": "6.0.0",
"@percy/cli": "1.0.4",
"@percy/cli": "1.2.1",
"@percy/playwright": "1.0.3",
"@playwright/test": "1.21.1",
"@types/eventemitter3": "^1.0.0",
@ -37,7 +37,7 @@
"imports-loader": "0.8.0",
"jasmine-core": "4.1.1",
"jsdoc": "3.5.5",
"karma": "6.3.18",
"karma": "6.3.20",
"karma-chrome-launcher": "3.1.1",
"karma-cli": "2.0.0",
"karma-coverage": "2.2.0",
@ -48,7 +48,7 @@
"karma-sourcemap-loader": "0.3.8",
"karma-spec-reporter": "0.0.34",
"karma-webpack": "5.0.0",
"lighthouse": "9.5.0",
"lighthouse": "9.6.1",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"mini-css-extract-plugin": "2.6.0",
@ -64,7 +64,7 @@
"resolve-url-loader": "5.0.0",
"sass": "1.49.9",
"sass-loader": "12.6.0",
"sinon": "13.0.1",
"sinon": "14.0.0",
"style-loader": "^1.0.1",
"uuid": "3.3.3",
"vue": "2.6.14",
@ -73,7 +73,7 @@
"vue-template-compiler": "2.6.14",
"webpack": "5.68.0",
"webpack-cli": "4.9.2",
"webpack-dev-middleware": "5.3.1",
"webpack-dev-middleware": "5.3.3",
"webpack-hot-middleware": "2.25.1",
"webpack-merge": "5.8.0",
"zepto": "1.2.0"
@ -95,7 +95,7 @@
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke default condition timeConductor branding clock exampleImagery",
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-local.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 default",
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js",
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js",
"test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",

View File

@ -242,8 +242,6 @@ define([
// Plugins that are installed by default
this.install(this.plugins.Plot());
this.install(this.plugins.ScatterPlot());
this.install(this.plugins.BarChart());
this.install(this.plugins.TelemetryTable.default());
this.install(PreviewPlugin.default());
this.install(LicensesPlugin.default());

View File

@ -12,6 +12,7 @@
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
>
{{ action.name }}
@ -37,6 +38,7 @@
:key="action.name"
:class="action.cssClass"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
>
{{ action.name }}

View File

@ -15,6 +15,7 @@
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
@ -45,6 +46,7 @@
:key="action.name"
:class="action.cssClass"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"

View File

@ -51,7 +51,7 @@ class ImageExporter {
const overlays = this.openmct.overlays;
const dialog = overlays.dialog({
iconClass: 'info',
message: 'Caputuring an image',
message: 'Capturing image, please wait...',
buttons: [
{
label: 'Cancel',

View File

@ -52,7 +52,6 @@ export default (agent, document) => {
if (agent.isMobile()) {
const mediaQuery = window.matchMedia("(orientation: landscape)");
function eventHandler(event) {
console.log("changed");
if (event.matches) {
body.classList.remove("portrait");
body.classList.add("landscape");

View File

@ -85,14 +85,13 @@
class="c-dial__bg"
viewBox="0 0 10 10"
>
<g
v-if="limitLow !== null && dialLowLimitDeg < getLimitDegree('low', 'max')"
v-if="isDialLowLimit"
class="c-dial__limit-low"
:style="`transform: rotate(${dialLowLimitDeg}deg)`"
>
<rect
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q1')"
v-if="isDialLowLimitLow"
class="c-dial__low-limit__low"
x="5"
y="5"
@ -100,7 +99,7 @@
height="5"
/>
<rect
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q2')"
v-if="isDialLowLimitMid"
class="c-dial__low-limit__mid"
x="5"
y="0"
@ -108,7 +107,7 @@
height="5"
/>
<rect
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q3')"
v-if="isDialLowLimitHigh"
class="c-dial__low-limit__high"
x="0"
y="0"
@ -118,12 +117,12 @@
</g>
<g
v-if="limitHigh !== null && dialHighLimitDeg < getLimitDegree('high', 'max')"
v-if="isDialHighLimit"
class="c-dial__limit-high"
:style="`transform: rotate(${dialHighLimitDeg}deg)`"
>
<rect
v-if="dialHighLimitDeg <= getLimitDegree('high', 'max')"
v-if="isDialHighLimitLow"
class="c-dial__high-limit__low"
x="0"
y="5"
@ -131,7 +130,7 @@
height="5"
/>
<rect
v-if="dialHighLimitDeg <= getLimitDegree('high', 'q2')"
v-if="isDialHighLimitMid"
class="c-dial__high-limit__mid"
x="0"
y="0"
@ -139,7 +138,7 @@
height="5"
/>
<rect
v-if="dialHighLimitDeg <= getLimitDegree('high', 'q3')"
v-if="isDialHighLimitHigh"
class="c-dial__high-limit__high"
x="5"
y="0"
@ -159,7 +158,7 @@
:style="`transform: rotate(${degValueFilledDial}deg)`"
>
<rect
v-if="degValue >= getLimitDegree('low', 'q1')"
v-if="isDialFilledValueLow"
class="c-dial__filled-value__low"
x="5"
y="5"
@ -167,7 +166,7 @@
height="5"
/>
<rect
v-if="degValue >= getLimitDegree('low', 'q2')"
v-if="isDialFilledValueMid"
class="c-dial__filled-value__mid"
x="5"
y="0"
@ -175,7 +174,7 @@
height="5"
/>
<rect
v-if="degValue >= getLimitDegree('low', 'q3')"
v-if="isDialFilledValueHigh"
class="c-dial__filled-value__high"
x="0"
y="0"
@ -216,13 +215,13 @@
></div>
<div
v-if="limitHigh !== null && meterHighLimitPerc > 0"
v-if="isMeterLimitHigh"
class="c-meter__limit-high"
:style="`height: ${meterHighLimitPerc}%`"
></div>
<div
v-if="limitLow !== null && meterLowLimitPerc > 0"
v-if="isMeterLimitLow"
class="c-meter__limit-low"
:style="`height: ${meterLowLimitPerc}%`"
></div>
@ -235,13 +234,13 @@
></div>
<div
v-if="limitHigh !== null && meterHighLimitPerc > 0"
v-if="isMeterLimitHigh"
class="c-meter__limit-high"
:style="`width: ${meterHighLimitPerc}%`"
></div>
<div
v-if="limitLow !== null && meterLowLimitPerc > 0"
v-if="isMeterLimitLow"
class="c-meter__limit-low"
:style="`width: ${meterLowLimitPerc}%`"
></div>
@ -275,6 +274,7 @@
import { DIAL_VALUE_DEG_OFFSET, getLimitDegree } from '../gauge-limit-util';
const LIMIT_PADDING_IN_PERCENT = 10;
const DEFAULT_CURRENT_VALUE = '--';
export default {
name: 'Gauge',
@ -283,7 +283,7 @@ export default {
let gaugeController = this.domainObject.configuration.gaugeController;
return {
curVal: 0,
curVal: DEFAULT_CURRENT_VALUE,
digits: 3,
precision: gaugeController.precision,
displayMinMax: gaugeController.isDisplayMinMax,
@ -319,6 +319,45 @@ export default {
return VIEWBOX_STR.replace('X', this.digits * DIGITS_RATIO);
},
isDialLowLimit() {
return this.limitLow.length > 0 && this.dialLowLimitDeg < getLimitDegree('low', 'max');
},
isDialLowLimitLow() {
return this.dialLowLimitDeg >= getLimitDegree('low', 'q1');
},
isDialLowLimitMid() {
return this.dialLowLimitDeg >= getLimitDegree('low', 'q2');
},
isDialLowLimitHigh() {
return this.dialLowLimitDeg >= getLimitDegree('low', 'q3');
},
isDialHighLimit() {
return this.limitHigh.length > 0 && this.dialHighLimitDeg < getLimitDegree('high', 'max');
},
isDialHighLimitLow() {
return this.dialHighLimitDeg <= getLimitDegree('high', 'max');
},
isDialHighLimitMid() {
return this.dialHighLimitDeg <= getLimitDegree('high', 'q2');
},
isDialHighLimitHigh() {
return this.dialHighLimitDeg <= getLimitDegree('high', 'q3');
},
isDialFilledValueLow() {
return this.degValue >= getLimitDegree('low', 'q1');
},
isDialFilledValueMid() {
return this.degValue >= getLimitDegree('low', 'q2');
},
isDialFilledValueHigh() {
return this.degValue >= getLimitDegree('low', 'q3');
},
isMeterLimitHigh() {
return this.limitHigh.length > 0 && this.meterHighLimitPerc > 0;
},
isMeterLimitLow() {
return this.limitLow.length > 0 && this.meterLowLimitPerc > 0;
},
typeDial() {
return this.matchGaugeType('dial');
},
@ -459,13 +498,14 @@ export default {
this.unsubscribe = null;
}
this.metadata = null;
this.curVal = DEFAULT_CURRENT_VALUE;
this.formats = null;
this.valueKey = null;
this.limitHigh = null;
this.limitLow = null;
this.limitHigh = '';
this.limitLow = '';
this.metadata = null;
this.rangeHigh = null;
this.rangeLow = null;
this.valueKey = null;
},
request(domainObject = this.telemetryObject) {
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
@ -518,13 +558,20 @@ export default {
} else if (telemetryLimit.WATCH) {
limits = telemetryLimit.WATCH;
} else {
this.openmct.notifications.error('No limits definition for given telemetry');
this.openmct.notifications.error('No limits definition for given telemetry, hiding low and high limits');
this.displayMinMax = false;
this.limitHigh = '';
this.limitLow = '';
return;
}
this.limitHigh = this.round(limits.high[this.valueKey]);
this.limitLow = this.round(limits.low[this.valueKey]);
this.rangeHigh = this.round(this.limitHigh + this.limitHigh * LIMIT_PADDING_IN_PERCENT / 100);
this.rangeLow = this.round(this.limitLow - Math.abs(this.limitLow * LIMIT_PADDING_IN_PERCENT / 100));
this.displayMinMax = this.domainObject.configuration.gaugeController.isDisplayMinMax;
},
updateValue(datum) {
this.datum = datum;

View File

@ -177,7 +177,7 @@ export default {
SearchResults,
Sidebar
},
inject: ['openmct', 'snapshotContainer'],
inject: ['agent', 'openmct', 'snapshotContainer'],
props: {
domainObject: {
type: Object,
@ -455,12 +455,9 @@ export default {
- tablet portrait
- in a layout frame (within .c-so-view)
*/
const classList = document.querySelector('body').classList;
const isPhone = Array.from(classList).includes('phone');
const isTablet = Array.from(classList).includes('tablet');
// address in https://github.com/nasa/openmct/issues/4875
// eslint-disable-next-line compat/compat
const isPortrait = window.screen.orientation.type.includes('portrait');
const isPhone = this.agent.isPhone();
const isTablet = this.agent.isTablet();
const isPortrait = this.agent.isPortrait();
const isInLayout = Boolean(this.$el.closest('.c-so-view'));
const sidebarCoversEntries = (isPhone || (isTablet && isPortrait) || isInLayout);
this.sidebarCoversEntries = sidebarCoversEntries;

View File

@ -8,6 +8,7 @@ import { notebookImageMigration, IMAGE_MIGRATION_VER } from '../notebook/utils/n
import { NOTEBOOK_TYPE } from './notebook-constants';
import Vue from 'vue';
import Agent from '@/utils/agent/Agent';
export default function NotebookPlugin() {
return function install(openmct) {
@ -18,7 +19,7 @@ export default function NotebookPlugin() {
}
openmct.actions.register(new CopyToNotebookAction(openmct));
const agent = new Agent(window);
const notebookType = {
name: 'Notebook',
description: 'Create and save timestamped notes with embedded object snapshots.',
@ -142,6 +143,7 @@ export default function NotebookPlugin() {
Notebook
},
provide: {
agent,
openmct,
snapshotContainer
},

View File

@ -154,6 +154,22 @@
>
</button>
</div>
<div class="c-button-set c-button-set--strip-h">
<button
class="c-button icon-crosshair"
:class="{ 'is-active': cursorGuide }"
title="Toggle cursor guides"
@click="toggleCursorGuide"
>
</button>
<button
class="c-button"
:class="{ 'icon-grid-on': gridLines, 'icon-grid-off': !gridLines }"
title="Toggle grid lines"
@click="toggleGridLines"
>
</button>
</div>
</div>
<!--Cursor guides-->
@ -213,16 +229,16 @@ export default {
};
}
},
gridLines: {
initGridLines: {
type: Boolean,
default() {
return true;
}
},
cursorGuide: {
initCursorGuide: {
type: Boolean,
default() {
return true;
return false;
}
},
plotTickWidth: {
@ -252,7 +268,9 @@ export default {
isTimeOutOfSync: false,
showLimitLineLabels: undefined,
isFrozenOnMouseDown: false,
hasSameRangeValue: true
hasSameRangeValue: true,
cursorGuide: this.initCursorGuide,
gridLines: this.initGridLines
};
},
computed: {
@ -273,6 +291,14 @@ export default {
return this.plotTickWidth || this.tickWidth;
}
},
watch: {
initGridLines(newGridLines) {
this.gridLines = newGridLines;
},
initCursorGuide(newCursorGuide) {
this.cursorGuide = newCursorGuide;
}
},
mounted() {
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp);
@ -1130,6 +1156,14 @@ export default {
},
legendHoverChanged(data) {
this.showLimitLineLabels = data;
},
toggleCursorGuide() {
this.cursorGuide = !this.cursorGuide;
this.$emit('cursorGuide', this.cursorGuide);
},
toggleGridLines() {
this.gridLines = !this.gridLines;
this.$emit('gridLines', this.gridLines);
}
}
};

View File

@ -24,41 +24,6 @@
ref="plotWrapper"
class="c-plot holder holder-plot has-control-bar"
>
<div
v-if="!options.compact"
class="c-control-bar"
>
<span class="c-button-set c-button-set--strip-h">
<button
class="c-button icon-download"
title="Export This View's Data as PNG"
@click="exportPNG()"
>
<span class="c-button__label">PNG</span>
</button>
<button
class="c-button"
title="Export This View's Data as JPG"
@click="exportJPG()"
>
<span class="c-button__label">JPG</span>
</button>
</span>
<button
class="c-button icon-crosshair"
:class="{ 'is-active': cursorGuide }"
title="Toggle cursor guides"
@click="toggleCursorGuide"
>
</button>
<button
class="c-button"
:class="{ 'icon-grid-on': gridLines, 'icon-grid-off': !gridLines }"
title="Toggle grid lines"
@click="toggleGridLines"
>
</button>
</div>
<div
ref="plotContainer"
@ -70,8 +35,8 @@
class="c-loading--overlay loading"
></div>
<mct-plot
:grid-lines="gridLines"
:cursor-guide="cursorGuide"
:init-grid-lines="gridLines"
:init-cursor-guide="cursorGuide"
:options="options"
@loadingUpdated="loadingUpdated"
@statusUpdated="setStatus"
@ -135,15 +100,14 @@ export default {
this.imageExporter.exportPNG(plotElement, 'plot.png', 'export-plot');
},
toggleCursorGuide() {
this.cursorGuide = !this.cursorGuide;
},
toggleGridLines() {
this.gridLines = !this.gridLines;
},
setStatus(status) {
this.status = status;
},
getViewContext() {
return {
exportPNG: this.exportPNG,
exportJPG: this.exportJPG
};
}
}
};

View File

@ -80,9 +80,16 @@ export default function PlotViewProvider(openmct) {
}
};
},
template: '<plot :options="options"></plot>'
template: '<plot ref="plotComponent" :options="options"></plot>'
});
},
getViewContext() {
if (!component) {
return {};
}
return component.$refs.plotComponent.getViewContext();
},
destroy: function () {
component.$destroy();
component = undefined;

View File

@ -0,0 +1,57 @@
/*****************************************************************************
* 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.
*****************************************************************************/
import {isPlotView} from "@/plugins/plot/actions/utils";
const exportPNG = {
name: 'Export as PNG',
key: 'export-as-png',
description: 'Export This View\'s Data as PNG',
cssClass: 'c-icon-button icon-download',
group: 'view',
invoke(objectPath, view) {
view.getViewContext().exportPNG();
}
};
const exportJPG = {
name: 'Export as JPG',
key: 'export-as-jpg',
description: 'Export This View\'s Data as JPG',
cssClass: 'c-icon-button icon-download',
group: 'view',
invoke(objectPath, view) {
view.getViewContext().exportJPG();
}
};
const viewActions = [
exportPNG,
exportJPG
];
viewActions.forEach(action => {
action.appliesTo = (objectPath, view = {}) => {
return isPlotView(view);
};
});
export default viewActions;

View File

@ -0,0 +1,3 @@
export function isPlotView(view) {
return view.key === 'plot-single' || view.key === 'plot-overlay' || view.key === 'plot-stacked';
}

View File

@ -65,9 +65,16 @@ export default function OverlayPlotViewProvider(openmct) {
}
};
},
template: '<plot :options="options"></plot>'
template: '<plot ref="plotComponent" :options="options"></plot>'
});
},
getViewContext() {
if (!component) {
return {};
}
return component.$refs.plotComponent.getViewContext();
},
destroy: function () {
component.$destroy();
component = undefined;

View File

@ -25,6 +25,7 @@ import StackedPlotViewProvider from './stackedPlot/StackedPlotViewProvider';
import PlotsInspectorViewProvider from './inspector/PlotsInspectorViewProvider';
import OverlayPlotCompositionPolicy from './overlayPlot/OverlayPlotCompositionPolicy';
import StackedPlotCompositionPolicy from './stackedPlot/StackedPlotCompositionPolicy';
import PlotViewActions from "./actions/ViewActions";
export default function () {
return function install(openmct) {
@ -67,6 +68,9 @@ export default function () {
openmct.composition.addPolicy(new OverlayPlotCompositionPolicy(openmct).allow);
openmct.composition.addPolicy(new StackedPlotCompositionPolicy(openmct).allow);
PlotViewActions.forEach(action => {
openmct.actions.register(action);
});
};
}

View File

@ -724,16 +724,16 @@ describe("the plugin", function () {
});
it("turns on cursor Guides all telemetry objects", (done) => {
expect(plotViewComponentObject.cursorGuide).toBeFalse();
plotViewComponentObject.toggleCursorGuide();
expect(plotViewComponentObject.$children[0].cursorGuide).toBeFalse();
plotViewComponentObject.$children[0].cursorGuide = true;
Vue.nextTick(() => {
expect(plotViewComponentObject.$children[0].component.$children[0].cursorGuide).toBeTrue();
expect(plotViewComponentObject.$children[0].cursorGuide).toBeTrue();
done();
});
});
it("shows grid lines for all telemetry objects", () => {
expect(plotViewComponentObject.gridLines).toBeTrue();
expect(plotViewComponentObject.$children[0].gridLines).toBeTrue();
let gridLinesContainer = element.querySelectorAll(".gl-plot-display-area .js-ticks");
let visible = 0;
gridLinesContainer.forEach(el => {
@ -745,10 +745,10 @@ describe("the plugin", function () {
});
it("hides grid lines for all telemetry objects", (done) => {
expect(plotViewComponentObject.gridLines).toBeTrue();
plotViewComponentObject.toggleGridLines();
expect(plotViewComponentObject.$children[0].gridLines).toBeTrue();
plotViewComponentObject.$children[0].gridLines = false;
Vue.nextTick(() => {
expect(plotViewComponentObject.gridLines).toBeFalse();
expect(plotViewComponentObject.$children[0].gridLines).toBeFalse();
let gridLinesContainer = element.querySelectorAll(".gl-plot-display-area .js-ticks");
let visible = 0;
gridLinesContainer.forEach(el => {

View File

@ -22,41 +22,6 @@
<template>
<div class="c-plot c-plot--stacked holder holder-plot has-control-bar">
<div
v-show="!hideExportButtons && !options.compact"
class="c-control-bar"
>
<span class="c-button-set c-button-set--strip-h">
<button
class="c-button icon-download"
title="Export This View's Data as PNG"
@click="exportPNG()"
>
<span class="c-button__label">PNG</span>
</button>
<button
class="c-button"
title="Export This View's Data as JPG"
@click="exportJPG()"
>
<span class="c-button__label">JPG</span>
</button>
</span>
<button
class="c-button icon-crosshair"
:class="{ 'is-active': cursorGuide }"
title="Toggle cursor guides"
@click="toggleCursorGuide"
>
</button>
<button
class="c-button"
:class="{ 'icon-grid-on': gridLines, 'icon-grid-off': !gridLines }"
title="Toggle grid lines"
@click="toggleGridLines"
>
</button>
</div>
<div class="l-view-section">
<stacked-plot-item
v-for="object in compositionObjects"
@ -69,6 +34,8 @@
:plot-tick-width="maxTickWidth"
@plotTickWidth="onTickWidthChange"
@loadingUpdated="loadingUpdated"
@cursorGuide="onCursorGuideChange"
@gridLines="onGridLinesChange"
/>
</div>
</div>
@ -184,20 +151,24 @@ export default {
this.hideExportButtons = false;
}.bind(this));
},
toggleCursorGuide() {
this.cursorGuide = !this.cursorGuide;
},
toggleGridLines() {
this.gridLines = !this.gridLines;
},
onTickWidthChange(width, plotId) {
if (!Object.prototype.hasOwnProperty.call(this.tickWidthMap, plotId)) {
return;
}
this.$set(this.tickWidthMap, plotId, width);
},
onCursorGuideChange(cursorGuide) {
this.cursorGuide = cursorGuide === true;
},
onGridLinesChange(gridLines) {
this.gridLines = gridLines === true;
},
getViewContext() {
return {
exportPNG: this.exportPNG,
exportJPG: this.exportJPG
};
}
}
};

View File

@ -96,6 +96,8 @@ export default {
}
const onTickWidthChange = this.onTickWidthChange;
const onCursorGuideChange = this.onCursorGuideChange;
const onGridLinesChange = this.onGridLinesChange;
const loadingUpdated = this.loadingUpdated;
const setStatus = this.setStatus;
@ -121,16 +123,24 @@ export default {
return {
...getProps(),
onTickWidthChange,
onCursorGuideChange,
onGridLinesChange,
loadingUpdated,
setStatus
};
},
template: '<div ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver" :class="{\'s-status-timeconductor-unsynced\': status && status === \'timeconductor-unsynced\'}"><div v-show="!!loading" class="c-loading--overlay loading"></div><mct-plot :grid-lines="gridLines" :cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" :options="options" @plotTickWidth="onTickWidthChange" @statusUpdated="setStatus" @loadingUpdated="loadingUpdated"/></div>'
template: '<div ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver" :class="{\'s-status-timeconductor-unsynced\': status && status === \'timeconductor-unsynced\'}"><div v-show="!!loading" class="c-loading--overlay loading"></div><mct-plot :init-grid-lines="gridLines" :init-cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" :options="options" @plotTickWidth="onTickWidthChange" @cursorGuide="onCursorGuideChange" @gridLines="onGridLinesChange" @statusUpdated="setStatus" @loadingUpdated="loadingUpdated"/></div>'
});
},
onTickWidthChange() {
this.$emit('plotTickWidth', ...arguments);
},
onCursorGuideChange() {
this.$emit('cursorGuide', ...arguments);
},
onGridLinesChange() {
this.$emit('gridLines', ...arguments);
},
setStatus(status) {
this.status = status;
this.updateComponentProp('status', status);

View File

@ -67,9 +67,16 @@ export default function StackedPlotViewProvider(openmct) {
}
};
},
template: '<stacked-plot :options="options"></stacked-plot>'
template: '<stacked-plot ref="plotComponent" :options="options"></stacked-plot>'
});
},
getViewContext() {
if (!component) {
return {};
}
return component.$refs.plotComponent.getViewContext();
},
destroy: function () {
component.$destroy();
component = undefined;

View File

@ -22,6 +22,7 @@
ref="startOffset"
class="c-button c-conductor__delta-button"
title="Set the time offset after now"
data-testid="conductor-start-offset-button"
@click.prevent.stop="showTimePopupStart"
>
{{ offsets.start }}
@ -61,6 +62,7 @@
ref="endOffset"
class="c-button c-conductor__delta-button"
title="Set the time offset preceding now"
data-testid="conductor-end-offset-button"
@click.prevent.stop="showTimePopupEnd"
>
{{ offsets.end }}

View File

@ -105,6 +105,7 @@ export default {
name: 'Fixed Timespan',
description: 'Query and explore data that falls between two fixed datetimes.',
cssClass: 'icon-tabular',
testId: 'conductor-modeOption-fixed',
onItemClicked: () => this.setOption(key)
};
} else {
@ -116,6 +117,7 @@ export default {
description: "Monitor streaming data in real-time. The Time "
+ "Conductor and displays will automatically advance themselves based on this clock. " + clock.description,
cssClass: clock.cssClass || 'icon-clock',
testId: 'conductor-modeOption-realtime',
onItemClicked: () => this.setOption(key)
};
}
@ -148,7 +150,8 @@ export default {
if (clockKey === undefined) {
this.openmct.time.stopClock();
} else {
this.openmct.time.clock(clockKey, configuration.clockOffsets);
const offsets = this.openmct.time.clockOffsets() || configuration.clockOffsets;
this.openmct.time.clock(clockKey, offsets);
}
},

View File

@ -89,7 +89,21 @@ export default class Agent {
* @returns {boolean} true in portrait mode
*/
isPortrait() {
return this.window.innerWidth < this.window.innerHeight;
const { screen } = this.window;
const hasScreenOrientation = screen && Object.prototype.hasOwnProperty.call(screen, 'orientation');
const hasWindowOrientation = Object.prototype.hasOwnProperty.call(this.window, 'orientation');
if (hasScreenOrientation) {
return screen.orientation.type.includes('portrait');
} else if (hasWindowOrientation) {
// Use window.orientation API if available (e.g. Safari mobile)
// which returns [-90, 0, 90, 180] based on device orientation.
const { orientation } = this.window;
return Math.abs(orientation / 90) % 2 === 0;
} else {
return this.window.innerWidth < this.window.innerHeight;
}
}
/**
* Check if the user's device is in a landscape-style

View File

@ -68,7 +68,7 @@ describe("The Agent", function () {
expect(agent.isTablet()).toBeTruthy();
});
it("detects display orientation", function () {
it("detects display orientation by innerHeight and innerWidth", function () {
agent = new Agent(testWindow);
testWindow.innerWidth = 1024;
testWindow.innerHeight = 400;
@ -80,6 +80,34 @@ describe("The Agent", function () {
expect(agent.isLandscape()).toBeFalsy();
});
it("detects display orientation by screen.orientation", function () {
agent = new Agent(testWindow);
testWindow.screen = {
orientation: {
type: "landscape-primary"
}
};
expect(agent.isPortrait()).toBeFalsy();
expect(agent.isLandscape()).toBeTruthy();
testWindow.screen = {
orientation: {
type: "portrait-primary"
}
};
expect(agent.isPortrait()).toBeTruthy();
expect(agent.isLandscape()).toBeFalsy();
});
it("detects display orientation by window.orientation", function () {
agent = new Agent(testWindow);
testWindow.orientation = 90;
expect(agent.isPortrait()).toBeFalsy();
expect(agent.isLandscape()).toBeTruthy();
testWindow.orientation = 0;
expect(agent.isPortrait()).toBeTruthy();
expect(agent.isLandscape()).toBeFalsy();
});
it("detects touch support", function () {
testWindow.ontouchstart = null;
expect(new Agent(testWindow).isTouch()).toBe(true);