fix(conditional styling): conditional visibility for images and alpha-numerics in display layouts (#7824)

* fix: apply `is-style-invisible` className to image and alphanumeric items

* test: generate storagestate file with basic condition set

* refactor: small a11y additions for Toolbars

* test: add suite for display layout conditional styling

* fix: make condition true half of the time

* fix: use a period of 5 so tests are more stable

* test: mark as slow

* test: use inline base64 image text instead of a url

* fix: use vue reactivity system to conditionally show these objects

* test: use tiny base64 image

* fix: condition for v-show

* fix: use both v-if and v-show to toggle visibility

* refactor: convert to ES6 class

* fix: remove focused test

* fix: switch back to a div due to visual artifacts. settle for an aria role instead

- IT'S CALLED COMPROMISE!
This commit is contained in:
Jesse Mazzella 2024-09-09 15:41:29 -07:00 committed by GitHub
parent 21a4335c4e
commit 440474b2e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 268 additions and 69 deletions

File diff suppressed because one or more lines are too long

View File

@ -286,6 +286,55 @@ test.describe('Generate Visual Test Data @localStorage @generatedata @clock', ()
});
});
test.describe('Generate Conditional Styling Data @localStorage @generatedata', () => {
test('Generate basic condition set', async ({ page, context }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create a Condition Set
const conditionSet = await createDomainObjectWithDefaults(page, {
type: 'Condition Set',
name: 'Test Condition Set'
});
// Create a Telemetry Object (Sine Wave Generator)
const swg = await createExampleTelemetryObject(page, conditionSet.uuid);
// Edit the Telemetry Object to have a 10hz data rate (Gotta go fast!)
await page.goto(swg.url);
await page.getByLabel('More actions').click();
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
await page.getByLabel('Period', { exact: true }).fill('5');
await page.getByLabel('Save').click();
// Edit the Condition Set
await page.goto(conditionSet.url);
await page.getByLabel('Edit Object').click();
// Add a Condition to the Condition Set
await page.getByLabel('Add Condition').click();
await page.getByLabel('Condition Name Input').first().fill('Test Condition');
await page.getByLabel('Condition Output Type').first().selectOption('String');
await page.getByLabel('Condition Output String').first().fill('Test Condition Met');
// Condition: True if sine value > 0 (half the time)
await page.getByLabel('Criterion Telemetry Selection').selectOption(swg.name);
await page.getByLabel('Criterion Metadata Selection').selectOption('Sine');
await page.getByLabel('Criterion Comparison Selection').selectOption('is greater than');
await page.getByLabel('Criterion Input').first().fill('0');
// Rename default condition
await page.getByLabel('Condition Output String').nth(1).fill('Test Condition Unmet');
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Save localStorage for future test execution
await context.storageState({
path: fileURLToPath(
new URL('../../../e2e/test-data/condition_set_storage.json', import.meta.url)
)
});
});
});
test.describe('Validate Overlay Plot with Telemetry Object @localStorage @generatedata', () => {
test.use({
storageState: fileURLToPath(

View File

@ -0,0 +1,114 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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 { fileURLToPath } from 'url';
import {
createDomainObjectWithDefaults,
navigateToObjectWithRealTime
} from '../../../../../appActions.js';
import { expect, test } from '../../../../../pluginFixtures.js';
const TINY_IMAGE_BASE64 =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII';
test.describe('Display Layout Conditional Styling', () => {
test.use({
storageState: fileURLToPath(
new URL('../../../../../test-data/condition_set_storage.json', import.meta.url)
)
});
let displayLayout;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
displayLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Test Display Layout'
});
});
test('Image Drawing Object can have visibility toggled conditionally', async ({ page }) => {
await page.getByLabel('Edit Object').click();
// Add Image Drawing Object to the layout
await page.getByLabel('Add Drawing Object').click();
await page.getByLabel('Image').click();
await page.getByLabel('Image URL').fill(TINY_IMAGE_BASE64);
await page.getByText('Ok').click();
// Use the "Test Condition Set" for conditional styling on the image
await page.getByRole('tab', { name: 'Styles' }).click();
await page.getByRole('button', { name: 'Use Conditional Styling...' }).click();
await page.getByLabel('Modal Overlay').getByLabel('Expand My Items folder').click();
await page.getByLabel('Modal Overlay').getByLabel('Preview Test Condition Set').click();
await page.getByText('Ok').click();
// Set the image to be hidden when the condition is met
await page.getByTitle('Visible').first().click();
await page.getByLabel('Save Style').first().click();
await page.getByLabel('Save', { exact: true }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Switch to real-time mode and verify that the image toggles visibility
await navigateToObjectWithRealTime(page, displayLayout.url);
await expect(page.getByLabel('Image View')).toBeVisible();
await expect(page.getByLabel('Image View')).toBeHidden();
// Reload the page and verify that the image toggles visibility
await page.reload({ waitUntil: 'domcontentloaded' });
await expect(page.getByLabel('Image View')).toBeVisible();
await expect(page.getByLabel('Image View')).toBeHidden();
});
test('Alphanumeric object can have visibility toggled conditionally', async ({ page }) => {
await page.getByLabel('Edit Object').click();
// Add Alphanumeric Object to the layout
await page.getByLabel('Expand My Items folder').click();
await page.getByLabel('Expand Test Condition Set').click();
await page.getByLabel('Preview VIPER Rover Heading').dragTo(page.getByLabel('Layout Grid'));
// Use the "Test Condition Set" for conditional styling on the alphanumeric
await page.getByRole('tab', { name: 'Styles' }).click();
await page.getByRole('button', { name: 'Use Conditional Styling...' }).click();
await page.getByLabel('Modal Overlay').getByLabel('Expand My Items folder').click();
await page.getByLabel('Modal Overlay').getByLabel('Preview Test Condition Set').click();
await page.getByText('Ok').click();
// Set the alphanumeric to be hidden when the condition is met
await page.getByTitle('Visible').first().click();
await page.getByLabel('Save Style').first().click();
await page.getByLabel('Save', { exact: true }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Switch to real-time mode and verify that the image toggles visibility
await navigateToObjectWithRealTime(page, displayLayout.url);
await expect(page.getByLabel('Alpha-numeric telemetry', { exact: true })).toBeVisible();
await expect(page.getByLabel('Alpha-numeric telemetry', { exact: true })).toBeHidden();
// Reload the page and verify that the alphanumeric toggles visibility
await page.reload({ waitUntil: 'domcontentloaded' });
await expect(page.getByLabel('Alpha-numeric telemetry', { exact: true })).toBeVisible();
await expect(page.getByLabel('Alpha-numeric telemetry', { exact: true })).toBeHidden();
});
});

View File

@ -27,14 +27,16 @@
aria-label="Condition Set Condition Collection"
>
<div class="c-cs__header c-section__header">
<span
<button
class="c-disclosure-triangle c-tree__item__view-control is-enabled"
:class="{ 'c-disclosure-triangle--expanded': expanded }"
@click="expanded = !expanded"
></span>
:aria-expanded="expanded"
aria-controls="conditionContent"
@click="toggleExpanded"
></button>
<div class="c-cs__header-label c-section__label">Conditions</div>
</div>
<div v-if="expanded" class="c-cs__content">
<div v-if="expanded" id="conditionContent" class="c-cs__content">
<div
v-show="isEditing"
class="hint"
@ -54,9 +56,10 @@
v-show="isEditing"
id="addCondition"
class="c-button c-button--major icon-plus labeled"
aria-labelledby="addConditionButtonLabel"
@click="addCondition"
>
<span class="c-cs-button__label">Add Condition</span>
<span id="addConditionButtonLabel" class="c-cs-button__label">Add Condition</span>
</button>
<div class="c-cs__conditions-h" :class="{ 'is-active-dragging': isDragging }">

View File

@ -29,7 +29,7 @@
@end-move="endMove"
>
<template #content>
<div class="c-image-view" :style="style"></div>
<div v-show="showImage" aria-label="Image View" class="c-image-view" :style="style"></div>
</template>
</LayoutFrame>
</template>
@ -76,6 +76,9 @@ export default {
},
emits: ['move', 'end-move'],
computed: {
showImage() {
return this.isEditing || !this.itemStyle?.isStyleInvisible;
},
style() {
let backgroundImage = 'url(' + this.item.url + ')';
let border = '1px solid ' + this.item.stroke;

View File

@ -31,9 +31,10 @@
<template #content>
<div
v-if="domainObject"
v-show="showTelemetry"
ref="telemetryViewWrapper"
class="c-telemetry-view u-style-receiver"
:class="[itemClasses]"
:class="classNames"
:style="styleObject"
:data-font-size="item.fontSize"
:data-font="item.font"
@ -151,7 +152,10 @@ export default {
};
},
computed: {
itemClasses() {
showTelemetry() {
return this.isEditing || !this.itemStyle?.isStyleInvisible;
},
classNames() {
let classes = [];
if (this.status) {

View File

@ -25,65 +25,67 @@
*
* @interface ToolbarRegistry
*/
export default function ToolbarRegistry() {
this.providers = {};
export default class ToolbarRegistry {
constructor() {
this.providers = {};
}
/**
* Gets toolbar controls from providers which can provide a toolbar for this selection.
*
* @param {Object} selection the selection object
* @returns {Object[]} an array of objects defining controls for the toolbar
* @private for platform-internal use
*/
get(selection) {
const providers = this.getAllProviders().filter(function (provider) {
return provider.forSelection(selection);
});
const structure = [];
providers.forEach((provider) => {
provider.toolbar(selection).forEach((item) => structure.push(item));
});
return structure;
}
/**
* @private
*/
getAllProviders() {
return Object.values(this.providers);
}
/**
* @private
*/
getByProviderKey(key) {
return this.providers[key];
}
/**
* Registers a new type of toolbar.
*
* @param {module:openmct.ToolbarRegistry} provider the provider for this toolbar
* @method addProvider
*/
addProvider(provider) {
const key = provider.key;
if (key === undefined) {
throw "Toolbar providers must have a unique 'key' property defined.";
}
if (this.providers[key] !== undefined) {
console.warn("Provider already defined for key '%s'. Provider keys must be unique.", key);
}
this.providers[key] = provider;
}
}
/**
* Gets toolbar controls from providers which can provide a toolbar for this selection.
*
* @param {Object} selection the selection object
* @returns {Object[]} an array of objects defining controls for the toolbar
* @private for platform-internal use
*/
ToolbarRegistry.prototype.get = function (selection) {
const providers = this.getAllProviders().filter(function (provider) {
return provider.forSelection(selection);
});
const structure = [];
providers.forEach((provider) => {
provider.toolbar(selection).forEach((item) => structure.push(item));
});
return structure;
};
/**
* @private
*/
ToolbarRegistry.prototype.getAllProviders = function () {
return Object.values(this.providers);
};
/**
* @private
*/
ToolbarRegistry.prototype.getByProviderKey = function (key) {
return this.providers[key];
};
/**
* Registers a new type of toolbar.
*
* @param {module:openmct.ToolbarRegistry} provider the provider for this toolbar
* @method addProvider
*/
ToolbarRegistry.prototype.addProvider = function (provider) {
const key = provider.key;
if (key === undefined) {
throw "Toolbar providers must have a unique 'key' property defined.";
}
if (this.providers[key] !== undefined) {
console.warn("Provider already defined for key '%s'. Provider keys must be unique.", key);
}
this.providers[key] = provider;
};
/**
* Exposes types of toolbars in Open MCT.
*

View File

@ -34,9 +34,9 @@
}"
@click="onClick"
>
<div v-if="options.label" class="c-icon-button__label">
<span v-if="options.label" class="c-icon-button__label">
{{ options.label }}
</div>
</span>
</div>
</div>
</template>

View File

@ -25,11 +25,13 @@
class="c-icon-button c-icon-button--menu"
:class="options.icon"
:title="options.title"
:aria-label="options.label"
role="button"
@click="toggle"
>
<div v-if="options.label" class="c-icon-button__label">
<span v-if="options.label" class="c-icon-button__label">
{{ options.label }}
</div>
</span>
</div>
<div v-if="open" class="c-menu" role="menu">
<ul>