Skip to content

Commit

Permalink
fix(conditional styling): conditional visibility for images and alpha…
Browse files Browse the repository at this point in the history
…-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!
  • Loading branch information
ozyx authored Sep 9, 2024
1 parent 21a4335 commit 440474b
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 60 deletions.
22 changes: 22 additions & 0 deletions e2e/test-data/condition_set_storage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"954af939-eaf8-4977-8cee-57f36b58aae3\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1725480977300,\"created\":1725480975674,\"persisted\":1725480977301},\"954af939-eaf8-4977-8cee-57f36b58aae3\":{\"identifier\":{\"key\":\"954af939-eaf8-4977-8cee-57f36b58aae3\",\"namespace\":\"\"},\"name\":\"Test Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"id\":\"1f4b8d87-297b-4a2a-a2d2-46c42eb41b39\",\"configuration\":{\"name\":\"Test Condition\",\"output\":\"Test Condition Met\",\"trigger\":\"all\",\"criteria\":[{\"id\":\"034b4dfe-b17e-43f0-9787-93e4666d2690\",\"telemetry\":{\"key\":\"1eafa8cc-092f-4a5f-9206-9e5d8a070ea1\",\"namespace\":\"\"},\"operation\":\"greaterThan\",\"input\":[0],\"metadata\":\"sin\"}]},\"summary\":\"Match if all criteria are met: VIPER Rover Heading Sine > 0 \"},{\"isDefault\":true,\"id\":\"c56ff651-547e-4704-a8b7-4f01247e2fa7\",\"configuration\":{\"name\":\"Default\",\"output\":\"Test Condition Unmet\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"\"}]},\"composition\":[{\"key\":\"1eafa8cc-092f-4a5f-9206-9e5d8a070ea1\",\"namespace\":\"\"}],\"telemetry\":{},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Conditional Styling Data @localStorage @generatedata\\nGenerate basic condition set\\nchrome\",\"modified\":1725480978924,\"location\":\"mine\",\"created\":1725480977299,\"persisted\":1725480978924},\"1eafa8cc-092f-4a5f-9206-9e5d8a070ea1\":{\"identifier\":{\"key\":\"1eafa8cc-092f-4a5f-9206-9e5d8a070ea1\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":5,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"modified\":1725480978545,\"location\":\"954af939-eaf8-4977-8cee-57f36b58aae3\",\"created\":1725480977993,\"persisted\":1725480978545}}"
},
{
"name": "mct-tree-expanded",
"value": "[]"
},
{
"name": "mct-recent-objects",
"value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"954af939-eaf8-4977-8cee-57f36b58aae3\",\"namespace\":\"\"},\"name\":\"Test Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"c56ff651-547e-4704-a8b7-4f01247e2fa7\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[{\"key\":\"1eafa8cc-092f-4a5f-9206-9e5d8a070ea1\",\"namespace\":\"\"}],\"telemetry\":{},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Conditional Styling Data @localStorage @generatedata\\nGenerate basic condition set\\nchrome\",\"modified\":1725480977994,\"location\":\"mine\",\"created\":1725480977299,\"persisted\":1725480977994},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"954af939-eaf8-4977-8cee-57f36b58aae3\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1725480977300,\"created\":1725480975674,\"persisted\":1725480977301},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/954af939-eaf8-4977-8cee-57f36b58aae3\",\"domainObject\":{\"identifier\":{\"key\":\"954af939-eaf8-4977-8cee-57f36b58aae3\",\"namespace\":\"\"},\"name\":\"Test Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"c56ff651-547e-4704-a8b7-4f01247e2fa7\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[{\"key\":\"1eafa8cc-092f-4a5f-9206-9e5d8a070ea1\",\"namespace\":\"\"}],\"telemetry\":{},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Conditional Styling Data @localStorage @generatedata\\nGenerate basic condition set\\nchrome\",\"modified\":1725480977994,\"location\":\"mine\",\"created\":1725480977299,\"persisted\":1725480977994}},{\"objectPath\":[{\"identifier\":{\"key\":\"1eafa8cc-092f-4a5f-9206-9e5d8a070ea1\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":5,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"modified\":1725480978545,\"location\":\"954af939-eaf8-4977-8cee-57f36b58aae3\",\"created\":1725480977993,\"persisted\":1725480978545},{\"identifier\":{\"key\":\"954af939-eaf8-4977-8cee-57f36b58aae3\",\"namespace\":\"\"},\"name\":\"Test Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"c56ff651-547e-4704-a8b7-4f01247e2fa7\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[{\"key\":\"1eafa8cc-092f-4a5f-9206-9e5d8a070ea1\",\"namespace\":\"\"}],\"telemetry\":{},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Conditional Styling Data @localStorage @generatedata\\nGenerate basic condition set\\nchrome\",\"modified\":1725480977994,\"location\":\"mine\",\"created\":1725480977299,\"persisted\":1725480977994},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"954af939-eaf8-4977-8cee-57f36b58aae3\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1725480977300,\"created\":1725480975674,\"persisted\":1725480977301},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/954af939-eaf8-4977-8cee-57f36b58aae3/1eafa8cc-092f-4a5f-9206-9e5d8a070ea1\",\"domainObject\":{\"identifier\":{\"key\":\"1eafa8cc-092f-4a5f-9206-9e5d8a070ea1\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"modified\":1725480978542,\"location\":\"954af939-eaf8-4977-8cee-57f36b58aae3\",\"created\":1725480977993,\"persisted\":1725480977993}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"954af939-eaf8-4977-8cee-57f36b58aae3\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1725480977300,\"created\":1725480975674,\"persisted\":1725480977301},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine\",\"domainObject\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"954af939-eaf8-4977-8cee-57f36b58aae3\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1725480977300,\"created\":1725480975674,\"persisted\":1725480977301}}]"
}
]
}
]
}
49 changes: 49 additions & 0 deletions e2e/tests/framework/generateLocalStorageData.e2e.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
});
13 changes: 8 additions & 5 deletions src/plugins/condition/components/ConditionCollection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 }">
Expand Down
5 changes: 4 additions & 1 deletion src/plugins/displayLayout/components/ImageView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 6 additions & 2 deletions src/plugins/displayLayout/components/TelemetryView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -151,7 +152,10 @@ export default {
};
},
computed: {
itemClasses() {
showTelemetry() {
return this.isEditing || !this.itemStyle?.isStyleInvisible;
},
classNames() {
let classes = [];
if (this.status) {
Expand Down
Loading

0 comments on commit 440474b

Please sign in to comment.