fix(#7552): Fix notebook snapshot image annotations (#7555)

* fix: painterro import

* test(snapshotAnnotation): add minimal e2e test

* chore: add e2e test annotation

* fix: notebook snapshot test

* refactor: put `v-else` on template

* small changes to the test and a visual one

* additional a11y

* fix: html structure

* test(e2e): fix notebook snapshot tests

* Update documentation for file download and JSON testing

* Update stubs and add jpg/png export

* refactor(TimelistComponent): tidy up

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
This commit is contained in:
Jesse Mazzella 2024-03-13 13:27:49 -07:00 committed by GitHub
parent 18e976ad12
commit faed27c143
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 206 additions and 117 deletions

View File

@ -516,6 +516,30 @@ test.describe('foo test suite', () => {
- Working with multiple pages - Working with multiple pages
There are instances where multiple browser pages will needed to verify multi-page or multi-tab application behavior. Make sure to use the `@2p` annotation as well as name each page appropriately: i.e. `page1` and `page2` or `tab1` and `tab2` depending on the intended use case. Generally pages should be used unless testing `sharedWorker` code, specifically. There are instances where multiple browser pages will needed to verify multi-page or multi-tab application behavior. Make sure to use the `@2p` annotation as well as name each page appropriately: i.e. `page1` and `page2` or `tab1` and `tab2` depending on the intended use case. Generally pages should be used unless testing `sharedWorker` code, specifically.
- Working with file downloads and JSON data
Open MCT has the capability of exporting certain objects in the form of a JSON file handled by the chrome browser. The best example of this type of test can be found in the exportAsJson test.
```js
const [download] = await Promise.all([
page.waitForEvent('download'), // Waits for the download event
page.getByLabel('Export as JSON').click() // Triggers the download
]);
// Wait for the download process to complete
const path = await download.path();
// Read the contents of the downloaded file using readFile from fs/promises
const fileContents = await fs.readFile(path, 'utf8');
const jsonData = JSON.parse(fileContents);
// Use the function to retrieve the key
const key = getFirstKeyFromOpenMctJson(jsonData);
// Verify the contents of the JSON file
expect(jsonData.openmct[key]).toHaveProperty('name', 'e2e folder');
```
### Reporting ### Reporting
Test Reporting is done through official Playwright reporters and the CI Systems which execute them. Test Reporting is done through official Playwright reporters and the CI Systems which execute them.

View File

@ -289,7 +289,7 @@ test.describe('Flexible Layout Toolbar Actions @localStorage', () => {
await page.getByTitle('Add Container').click(); await page.getByTitle('Add Container').click();
expect(await containerHandles.count()).toEqual(3); expect(await containerHandles.count()).toEqual(3);
await page.getByTitle('Remove Container').click(); await page.getByTitle('Remove Container').click();
await expect(page.getByRole('dialog', { name: 'Overlay' })).toHaveText( await expect(page.getByRole('dialog', { name: 'Overlay' })).toContainText(
'This action will permanently delete this container from this Flexible Layout. Do you want to continue?' 'This action will permanently delete this container from this Flexible Layout. Do you want to continue?'
); );
await page.getByRole('button', { name: 'OK', exact: true }).click(); await page.getByRole('button', { name: 'OK', exact: true }).click();
@ -299,7 +299,7 @@ test.describe('Flexible Layout Toolbar Actions @localStorage', () => {
expect(await page.getByRole('group', { name: 'Frame' }).count()).toEqual(2); expect(await page.getByRole('group', { name: 'Frame' }).count()).toEqual(2);
await page.getByRole('group', { name: 'Child Layout 1' }).click(); await page.getByRole('group', { name: 'Child Layout 1' }).click();
await page.getByTitle('Remove Frame').click(); await page.getByTitle('Remove Frame').click();
await expect(page.getByRole('dialog', { name: 'Overlay' })).toHaveText( await expect(page.getByRole('dialog', { name: 'Overlay' })).toContainText(
'This action will remove this frame from this Flexible Layout. Do you want to continue?' 'This action will remove this frame from this Flexible Layout. Do you want to continue?'
); );
await page.getByRole('button', { name: 'OK', exact: true }).click(); await page.getByRole('button', { name: 'OK', exact: true }).click();

View File

@ -71,42 +71,89 @@ test.describe('Snapshot Container tests', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
//Navigate to baseURL //Navigate to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create Notebook
// const notebook = await createDomainObjectWithDefaults(page, {
// type: 'Notebook',
// name: "Test Notebook"
// });
// // Create Overlay Plot
// const snapShotObject = await createDomainObjectWithDefaults(page, {
// type: 'Overlay Plot',
// name: "Dropped Overlay Plot"
// });
await page.getByLabel('Open the Notebook Snapshot Menu').click(); await page.getByLabel('Open the Notebook Snapshot Menu').click();
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click(); await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
await page.getByLabel('Show Snapshots').click(); await page.getByLabel('Show Snapshots').click();
}); });
test('A snapshot can be Quick Viewed from Container with 3 dot action menu', async ({ page }) => { test('A snapshot can be Quick Viewed from Container with 3 dot action menu', async ({ page }) => {
await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More actions').click(); await page.getByLabel('My Items Notebook Embed').getByLabel('More actions').click();
await page.getByRole('menuitem', { name: 'Quick View' }).click(); await page.getByRole('menuitem', { name: 'Quick View' }).click();
await expect(page.locator('.c-overlay__outer')).toBeVisible(); await expect(page.getByLabel('Modal Overlay')).toBeVisible();
await expect(page.getByLabel('Preview Container')).toBeVisible();
});
test('A snapshot can be Viewed, Annotated, display deleted, and saved from Container with 3 dot action menu', async ({
page
}) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7552'
});
//Open Snapshot Object View
await page.getByLabel('My Items Notebook Embed').getByLabel('More actions').click();
await page.getByRole('menuitem', { name: 'View Snapshot' }).click();
await expect(page.getByRole('dialog', { name: 'Modal Overlay' })).toBeVisible();
await expect(page.locator('#snapshotDescriptor')).toHaveText(
/SNAPSHOT \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/
);
// Open Annotation Editor with Painterro
await page.getByLabel('Annotate this snapshot').click();
await expect(page.locator('#snap-annotation-canvas')).toBeVisible();
// Clear the canvas
await page.getByRole('button', { name: 'Put text [T]' }).click();
// Click in the Painterro canvas to add a text annotation
await page.locator('.ptro-crp-el').click();
await page.locator('.ptro-text-tool-input').fill('...is there life on mars?');
// When working with Painterro, we need to check that the Apply button is hidden after clicking
await page.getByTitle('Apply').click();
await expect(page.getByTitle('Apply')).toBeHidden();
// Save and exit annotation window
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Done' }).click();
// Open up annotation again
await page.getByRole('img', { name: 'My Items thumbnail' }).click();
await expect(page.getByLabel('Modal Overlay').getByRole('img')).toBeVisible();
});
test('A snapshot can be Annotated and saved as a JPG and PNG', async ({ page }) => {
//Open Snapshot Object View
await page.getByLabel('My Items Notebook Embed').getByLabel('More actions').click();
await page.getByRole('menuitem', { name: 'View Snapshot' }).click();
await expect(page.getByRole('dialog', { name: 'Modal Overlay' })).toBeVisible();
// Open Annotation Editor with Painterro
await page.getByLabel('Annotate this snapshot').click();
await expect(page.locator('#snap-annotation-canvas')).toBeVisible();
// Clear the canvas
await page.getByRole('button', { name: 'Put text [T]' }).click();
// Click in the Painterro canvas to add a text annotation
await page.locator('.ptro-crp-el').click();
await page.locator('.ptro-text-tool-input').fill('...is there life on mars?');
// When working with Painterro, we need to check that the Apply button is hidden after clicking
await page.getByTitle('Apply').click();
await expect(page.getByTitle('Apply')).toBeHidden();
// Save and exit annotation window
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Done' }).click();
// Open up annotation again
await page.getByRole('img', { name: 'My Items thumbnail' }).click();
await expect(page.getByLabel('Modal Overlay').getByRole('img')).toBeVisible();
// Save as JPG
await Promise.all([
page.waitForEvent('download'), // Waits for the download event
page.getByLabel('Export as JPG').click() // Triggers the download
]);
// Save as PNG
await expect(page.getByLabel('Modal Overlay').getByRole('img')).toBeVisible();
await Promise.all([
page.waitForEvent('download'), // Waits for the download event
page.getByLabel('Export as PNG').click() // Triggers the download
]);
}); });
test.fixme(
'A snapshot can be Viewed, Annotated, display deleted, and saved from Container with 3 dot action menu',
async ({ page }) => {
await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More actions').click();
await page.getByRole('menuitem', { name: ' View Snapshot' }).click();
await expect(page.locator('.c-overlay__outer')).toBeVisible();
await page.getByTitle('Annotate').click();
await expect(page.locator('#snap-annotation-canvas')).toBeVisible();
await page.getByRole('button', { name: '' }).click();
// await expect(page.locator('#snap-annotation-canvas')).not.toBeVisible();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Done' }).click();
//await expect(await page.locator)
}
);
test.fixme('5 Snapshots can be added to a container', async ({ page }) => {}); test.fixme('5 Snapshots can be added to a container', async ({ page }) => {});
test.fixme( test.fixme(
'5 Snapshots can be added to a container and Deleted with Delete All action', '5 Snapshots can be added to a container and Deleted with Delete All action',
@ -116,10 +163,6 @@ test.describe('Snapshot Container tests', () => {
'A snapshot can be Deleted from Container with 3 dot action menu', 'A snapshot can be Deleted from Container with 3 dot action menu',
async ({ page }) => {} async ({ page }) => {}
); );
test.fixme(
'A snapshot can be Navigated To from Container with 3 dot action menu',
async ({ page }) => {}
);
test.fixme( test.fixme(
'A snapshot can be Navigated To Item in Time from Container with 3 dot action menu', 'A snapshot can be Navigated To Item in Time from Container with 3 dot action menu',
async ({ page }) => {} async ({ page }) => {}
@ -151,11 +194,4 @@ test.describe('Snapshot Container tests', () => {
//Snapshot removed from container? //Snapshot removed from container?
} }
); );
test.fixme(
'Verify Embedded options for PNG, JPG, and Annotate work correctly',
async ({ page }) => {
//Add snapshot to container
//Verify PNG, JPG, and Annotate buttons work correctly
}
);
}); });

View File

@ -23,7 +23,7 @@
import percySnapshot from '@percy/playwright'; import percySnapshot from '@percy/playwright';
import { createDomainObjectWithDefaults, expandTreePaneItemByName } from '../../appActions.js'; import { createDomainObjectWithDefaults, expandTreePaneItemByName } from '../../appActions.js';
import { test } from '../../avpFixtures.js'; import { expect, test } from '../../avpFixtures.js';
import { VISUAL_URL } from '../../constants.js'; import { VISUAL_URL } from '../../constants.js';
import { enterTextEntry, startAndAddRestrictedNotebookObject } from '../../helper/notebookUtils.js'; import { enterTextEntry, startAndAddRestrictedNotebookObject } from '../../helper/notebookUtils.js';
@ -39,6 +39,44 @@ test.describe('Visual - Restricted Notebook @a11y', () => {
}); });
}); });
test.describe('Visual - Notebook Snapshot @a11y', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./?hideTree=true&hideInspector=true', { waitUntil: 'domcontentloaded' });
});
test('Visual check for Snapshot Annotation', async ({ page, theme }) => {
await page.getByLabel('Open the Notebook Snapshot Menu').click();
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
await page.getByLabel('Show Snapshots').click();
await page.getByLabel('My Items Notebook Embed').getByLabel('More actions').click();
await page.getByRole('menuitem', { name: 'View Snapshot' }).click();
await page.getByLabel('Annotate this snapshot').click();
await expect(page.locator('#snap-annotation-canvas')).toBeVisible();
// Clear the canvas
await page.getByRole('button', { name: 'Put text [T]' }).click();
// Click in the Painterro canvas to add a text annotation
await page.locator('.ptro-crp-el').click();
await page.locator('.ptro-text-tool-input').fill('...is there life on mars?');
await percySnapshot(page, `Notebook Snapshot with text entry open (theme: '${theme}')`);
// When working with Painterro, we need to check that the Apply button is hidden after clicking
await page.getByTitle('Apply').click();
await expect(page.getByTitle('Apply')).toBeHidden();
// Save and exit annotation window
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Done' }).click();
// Open up annotation again
await page.getByRole('img', { name: 'My Items thumbnail' }).click();
await expect(page.getByLabel('Modal Overlay').getByRole('img')).toBeVisible();
// Take a snapshot
await percySnapshot(page, `Notebook Snapshot with annotation (theme: '${theme}')`);
});
});
test.describe('Visual - Notebook @a11y', () => { test.describe('Visual - Notebook @a11y', () => {
let notebook; let notebook;
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {

View File

@ -20,7 +20,7 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<template> <template>
<div class="c-overlay js-overlay"> <div class="c-overlay js-overlay" role="dialog" aria-modal="true" aria-label="Modal Overlay">
<div class="c-overlay__blocker" @click="destroy"></div> <div class="c-overlay__blocker" @click="destroy"></div>
<div class="c-overlay__outer"> <div class="c-overlay__outer">
<button <button
@ -34,9 +34,6 @@
ref="element" ref="element"
class="c-overlay__contents js-notebook-snapshot-item-wrapper" class="c-overlay__contents js-notebook-snapshot-item-wrapper"
tabindex="0" tabindex="0"
aria-modal="true"
aria-label="Overlay"
role="dialog"
></div> ></div>
<div v-if="buttons" class="c-overlay__button-bar"> <div v-if="buttons" class="c-overlay__button-bar">
<button <button
@ -61,7 +58,7 @@
export default { export default {
inject: ['dismiss', 'element', 'buttons', 'dismissable'], inject: ['dismiss', 'element', 'buttons', 'dismissable'],
emits: ['destroy'], emits: ['destroy'],
data: function () { data() {
return { return {
focusIndex: -1 focusIndex: -1
}; };

View File

@ -23,6 +23,7 @@
<div <div
ref="notebookEmbed" ref="notebookEmbed"
class="c-snapshot c-ne__embed" class="c-snapshot c-ne__embed"
:aria-label="`${embed.name} Notebook Embed`"
@mouseover.ctrl="showToolTip" @mouseover.ctrl="showToolTip"
@mouseleave="hideToolTip" @mouseleave="hideToolTip"
> >

View File

@ -1,36 +1,32 @@
<div class="c-notebook-snapshot"> <div class="c-notebook-snapshot">
<!-- parent container sets up this for flex column layout -->
<div class="c-notebook-snapshot__header l-browse-bar"> <div class="c-notebook-snapshot__header l-browse-bar">
<div class="l-browse-bar__start"> <div class="l-browse-bar__start">
<div class="l-browse-bar__object-name--w"> <div class="l-browse-bar__object-name--w">
<span class="c-object-label l-browse-bar__object-name"> <span class="c-object-label l-browse-bar__object-name">
<span class="c-object-label__type-icon" v-bind:class="cssClass"></span> <span class="c-object-label__type-icon" :class="cssClass"></span>
<span class="c-object-label__name">{{ name }}</span> <span class="c-object-label__name">{{ name }}</span>
</span> </span>
</div> </div>
</div> </div>
<div id="snapshotDescriptor" class="l-browse-bar__snapshot-datetime">
SNAPSHOT {{ createdOn }}
</div>
<div class="c-button-set c-button-set--strip-h" role="toolbar">
<button class="c-button icon-download" aria-label="Export as PNG" @click="exportImage('png')">
<span class="c-button__label">PNG</span>
</button>
<button class="c-button icon-download" aria-label="Export as JPG" @click="exportImage('jpg')">
<span class="c-button__label">JPG</span>
</button>
</div>
<div class="l-browse-bar__end"> <div class="l-browse-bar__end">
<div class="l-browse-bar__snapshot-datetime">SNAPSHOT {{ createdOn }}</div> <button
<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="exportImage('png')"
>
<span class="c-button__label">PNG</span>
</button>
<button class="c-button" title="Export This View's Data as JPG" @click="exportImage('jpg')">
<span class="c-button__label">JPG</span>
</button>
</span>
<a
class="l-browse-bar__annotate-button c-button icon-pencil" class="l-browse-bar__annotate-button c-button icon-pencil"
title="Annotate" aria-label="Annotate this snapshot"
@click="annotateSnapshot" @click="annotateSnapshot"
> >
<span class="title-label">Annotate</span> <span class="title-label">Annotate</span>
</a> </button>
</div> </div>
</div> </div>
@ -38,5 +34,7 @@
ref="snapshot-image" ref="snapshot-image"
class="c-notebook-snapshot__image" class="c-notebook-snapshot__image"
:style="{ backgroundImage: 'url(' + src + ')' }" :style="{ backgroundImage: 'url(' + src + ')' }"
role="img"
alt="Annotatable Snapshot"
></div> ></div>
</div> </div>

View File

@ -46,7 +46,7 @@ export default class PainterroInstance {
this.config.id = this.elementId; this.config.id = this.elementId;
this.config.saveHandler = this.saveHandler.bind(this); this.config.saveHandler = this.saveHandler.bind(this);
this.painterro = Painterro(this.config); this.painterro = Painterro.default(this.config);
} }
save(callback) { save(callback) {

View File

@ -35,42 +35,39 @@
:item-properties="itemProperties" :item-properties="itemProperties"
:execution-state="persistedActivityStates[item.id]" :execution-state="persistedActivityStates[item.id]"
@click.stop="setSelectionForActivity(item, $event.currentTarget)" @click.stop="setSelectionForActivity(item, $event.currentTarget)"
> />
</expanded-view-item>
</template> </template>
<div v-else class="c-table c-table--sortable c-list-view c-list-view--sticky-header sticky"> <template v-else>
<table class="c-table__body js-table__body"> <div class="c-table c-table--sortable c-list-view c-list-view--sticky-header sticky">
<thead class="c-table__header"> <table class="c-table__body js-table__body">
<tr> <thead class="c-table__header">
<list-header <tr>
v-for="headerItem in headerItems" <list-header
:key="headerItem.property" v-for="headerItem in headerItems"
:direction=" :key="headerItem.property"
defaultSort.property === headerItem.property :direction="getSortDirection(headerItem)"
? defaultSort.defaultDirection :is-sortable="headerItem.isSortable"
: headerItem.defaultDirection :aria-label="headerItem.name"
" :title="headerItem.name"
:is-sortable="headerItem.isSortable" :property="headerItem.property"
:aria-label="headerItem.name" :current-sort="defaultSort.property"
:title="headerItem.name" @sort="sort"
:property="headerItem.property" />
:current-sort="defaultSort.property" </tr>
@sort="sort" </thead>
<tbody>
<list-item
v-for="item in sortedItems"
:key="item.key"
:class="{ '--is-in-progress': persistedActivityStates[item.id] === 'in-progress' }"
:item="item"
:item-properties="itemProperties"
@click.stop="setSelectionForActivity(item, $event.currentTarget)"
/> />
</tr> </tbody>
</thead> </table>
<tbody> </div>
<list-item </template>
v-for="item in sortedItems"
:key="item.key"
:class="{ '--is-in-progress': persistedActivityStates[item.id] === 'in-progress' }"
:item="item"
:item-properties="itemProperties"
@click.stop="setSelectionForActivity(item, $event.currentTarget)"
/>
</tbody>
</table>
</div>
</div> </div>
</template> </template>
@ -526,20 +523,16 @@ export default {
return activities.map(this.styleActivity); return activities.map(this.styleActivity);
}, },
setSort() { setSort() {
const sortOrder = SORT_ORDER_OPTIONS[this.domainObject.configuration.sortOrderIndex]; const { property, direction } =
const property = sortOrder.property; SORT_ORDER_OPTIONS[this.domainObject.configuration.sortOrderIndex];
const direction = sortOrder.direction.toLowerCase() === 'asc';
this.defaultSort = { this.defaultSort = {
property, property,
defaultDirection: direction defaultDirection: direction.toLowerCase() === 'asc'
}; };
}, },
sortItems(activities) { sortItems(activities) {
let sortedItems = _.sortBy(activities, this.defaultSort.property); const sortedItems = _.sortBy(activities, this.defaultSort.property);
if (!this.defaultSort.defaultDirection) { return this.defaultSort.defaultDirection ? sortedItems : sortedItems.reverse();
sortedItems = sortedItems.reverse();
}
return sortedItems;
}, },
setStatus(status) { setStatus(status) {
this.status = status; this.status = status;
@ -548,10 +541,7 @@ export default {
this.isEditing = isEditing; this.isEditing = isEditing;
this.setViewFromConfig(this.domainObject.configuration); this.setViewFromConfig(this.domainObject.configuration);
}, },
sort(data) { sort({ property, direction }) {
const property = data.property;
const direction = data.direction;
if (this.defaultSort.property === property) { if (this.defaultSort.property === property) {
this.defaultSort.defaultDirection = !this.defaultSort.defaultDirection; this.defaultSort.defaultDirection = !this.defaultSort.defaultDirection;
} else { } else {
@ -565,10 +555,10 @@ export default {
this.openmct.selection.select( this.openmct.selection.select(
[ [
{ {
element: element, element,
context: { context: {
type: 'activity', type: 'activity',
activity: activity activity
} }
}, },
{ {
@ -581,6 +571,11 @@ export default {
], ],
multiSelect multiSelect
); );
},
getSortDirection(headerItem) {
return this.defaultSort.property === headerItem.property
? this.defaultSort.defaultDirection
: headerItem.defaultDirection;
} }
} }
}; };