Set location hash if query parameters or path have changed (#7306)

* refactor to es6 class

* change URL on path or params change

* add test for url

* put into edit mode for test

* es6 module export

* a11y: add `status` role to clock component

* a11y: add label to overlay

* a11y: update roles for search results

* a11y: add `dialog` role and label for PreviewContainer

* refactor(e2e): get rid of a bunch of `page.locator()`s

* refactor(e2e): spruce up locators

* test: fix unit tests

* fix tests with new aria labels

* fix tests with new aria labels

* fix tests with new aria labels

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
This commit is contained in:
Scott Bell 2024-01-08 20:29:01 +01:00 committed by GitHub
parent dfba4e23c5
commit 64d4ddd80e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 341 additions and 241 deletions

View File

@ -91,27 +91,30 @@ test.describe('Notification Overlay', () => {
// Create a new Display Layout object
await createDomainObjectWithDefaults(page, { type: 'Display Layout' });
// Dismiss notification banner
await page.getByRole('button', { name: 'Dismiss' }).click();
// Click on the button "Review 1 Notification"
await page.click('button[aria-label="Review 1 Notification"]');
await page.getByRole('button', { name: 'Review 1 Notification' }).click();
// Verify that Notification List is open
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(true);
await expect(page.getByRole('dialog', { name: 'Overlay' })).toBeVisible();
// Wait until there is no Notification Banner
await page.waitForSelector('div[role="alert"]', { state: 'detached' });
await expect(page.getByRole('alert')).not.toBeAttached();
// Click on the "Close" button of the Notification List
await page.click('button[aria-label="Close"]');
await page.getByRole('button', { name: 'Close' }).click();
// On the Display Layout object, click on the "Edit" button
await page.click('button[title="Edit"]');
await page.getByRole('button', { name: 'Edit' }).click();
// Click on the "Save" button
await page.click('button[title="Save"]');
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Verify that Notification List is NOT open
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(false);
await expect(page.getByRole('dialog', { name: 'Overlay' })).toBeHidden();
});
});

View File

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

View File

@ -185,16 +185,20 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
//Partial match for "Science" should only return Science
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc');
await expect(page.locator('[aria-label="Search Result"]').first()).toContainText('Science');
await expect(page.locator('[aria-label="Search Result"]').first()).not.toContainText('Driving');
await expect(page.locator('[aria-label="Search Result"]').first()).not.toContainText(
await expect(page.locator('[aria-label="Annotation Search Result"]').first()).toContainText(
'Science'
);
await expect(page.locator('[aria-label="Annotation Search Result"]').first()).not.toContainText(
'Driving'
);
await expect(page.locator('[aria-label="Annotation Search Result"]').first()).not.toContainText(
'Drilling'
);
//Searching for a tag which does not exist should return an empty result
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq');
await expect(page.locator('text=No results found')).toBeVisible();
await expect(page.getByText('No results found')).toBeVisible();
});
});

View File

@ -88,43 +88,53 @@ test.describe('Tagging in Notebooks @addInit', () => {
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.getByRole('search').getByLabel('Search Input').click();
await expect(page.locator('button:has-text("Add Tag")')).toBeVisible();
// Test canceling adding a tag after we just click "Add Tag"
await page.locator('button:has-text("Add Tag")').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.getByRole('search').getByLabel('Search Input').click();
await expect(page.locator('button:has-text("Add Tag")')).toBeVisible();
});
test('Can search for tags and preview works properly', async ({ page }) => {
await createNotebookEntryAndTags(page);
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
await expect(page.locator('[aria-label="Search Result"]')).toContainText('Science');
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText('Driving');
await page.getByRole('search').getByLabel('Search Input').click();
await page.getByRole('search').getByLabel('Search Input').fill('sc');
await expect(page.getByRole('listitem', { name: 'Annotation Search Result' })).toContainText(
'Science'
);
await expect(
page.getByRole('listitem', { name: 'Annotation Search Result' })
).not.toContainText('Driving');
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc');
await expect(page.locator('[aria-label="Search Result"]')).toContainText('Science');
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText('Driving');
await page.getByRole('search').getByLabel('Search Input').click();
await page.getByRole('search').getByLabel('Search Input').fill('Sc');
await expect(page.getByRole('listitem', { name: 'Annotation Search Result' })).toContainText(
'Science'
);
await expect(
page.getByRole('listitem', { name: 'Annotation Search Result' })
).not.toContainText('Driving');
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq');
await expect(page.locator('text=No results found')).toBeVisible();
await page.getByRole('search').getByLabel('Search Input').click();
await page.getByRole('search').getByLabel('Search Input').fill('Xq');
await expect(page.getByText('No results found')).toBeVisible();
await createDomainObjectWithDefaults(page, {
type: 'Display Layout'
});
// Go back into edit mode for the display layout
await page.locator('button[title="Edit"]').click();
await page.getByRole('button', { name: 'Edit' }).click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc');
await expect(page.locator('[aria-label="Search Result"]')).toContainText('Science');
await page.getByRole('search').getByLabel('Search Input').click();
await page.getByRole('search').getByLabel('Search Input').fill('Sc');
await expect(page.getByRole('listitem', { name: 'Annotation Search Result' })).toContainText(
'Science'
);
await page.getByText('Entry 0').click();
await expect(page.locator('.js-preview-window')).toBeVisible();
});
@ -138,8 +148,10 @@ test.describe('Tagging in Notebooks @addInit', () => {
await expect(page.locator('[aria-label="Tags Inspector"]')).toContainText('Science');
await expect(page.locator('[aria-label="Tags Inspector"]')).not.toContainText('Driving');
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText('Driving');
await page.getByRole('search').getByLabel('Search Input').fill('sc');
await expect(
page.getByRole('listitem', { name: 'Annotation Search Result' })
).not.toContainText('Driving');
});
test('Can delete entries without tags', async ({ page }) => {
@ -172,12 +184,12 @@ test.describe('Tagging in Notebooks @addInit', () => {
await page.locator('button:has-text("OK")').click();
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed');
await expect(page.locator('text=No results found')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sci');
await expect(page.locator('text=No results found')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('dri');
await expect(page.locator('text=No results found')).toBeVisible();
await page.getByRole('search').getByLabel('Search Input').fill('Unnamed');
await expect(page.getByText('No results found')).toBeVisible();
await page.getByRole('search').getByLabel('Search Input').fill('sci');
await expect(page.getByText('No results found')).toBeVisible();
await page.getByRole('search').getByLabel('Search Input').fill('dri');
await expect(page.getByText('No results found')).toBeVisible();
});
test('Tags persist across reload', async ({ page }) => {
//Go to baseURL

View File

@ -29,12 +29,15 @@ import { createDomainObjectWithDefaults } from '../../appActions.js';
import { expect, test } from '../../pluginFixtures.js';
test.describe('Grand Search', () => {
const searchResultSelector = '.c-gsearch-result__title';
const searchResultDropDownSelector = '.c-gsearch__results';
let grandSearchInput;
test.beforeEach(async ({ page }) => {
grandSearchInput = page
.getByLabel('OpenMCT Search')
.getByRole('searchbox', { name: 'Search Input' });
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Can search for objects, and subsequent search dropdown behaves properly', async ({
@ -45,103 +48,124 @@ test.describe('Grand Search', () => {
const createdObjects = await createObjectsForSearch(page);
// Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(
// Go back into edit mode for the display layout
await page.getByRole('button', { name: 'Edit' }).click();
await grandSearchInput.click();
await grandSearchInput.fill('Cl');
await expect(page.getByLabel('Object Search Result').first()).toContainText(
`Clock A ${myItemsFolderName} Red Folder Blue Folder`
);
await expect(page.locator('[aria-label="Search Result"] >> nth=1')).toContainText(
await expect(page.getByLabel('Object Search Result').nth(1)).toContainText(
`Clock B ${myItemsFolderName} Red Folder Blue Folder`
);
await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText(
await expect(page.getByLabel('Object Search Result').nth(2)).toContainText(
`Clock C ${myItemsFolderName} Red Folder Blue Folder`
);
await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText(
await expect(page.getByLabel('Object Search Result').nth(3)).toContainText(
`Clock D ${myItemsFolderName} Red Folder Blue Folder`
);
// Click the Elements pool to dismiss the search menu
await page.getByRole('tab', { name: 'Elements' }).click();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
await expect(page.getByLabel('Object Search Result').first()).toBeHidden();
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
await page.locator('[aria-label="Clock A clock result"] >> text=Clock A').click();
await expect(page.locator('.js-preview-window')).toBeVisible();
await grandSearchInput.click();
await page.getByLabel('OpenMCT Search').getByText('Clock A').click();
await expect(page.getByRole('dialog', { name: 'Preview Container' })).toBeVisible();
// Click [aria-label="Close"]
await page.locator('[aria-label="Close"]').click();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeVisible();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(
// Close the Preview window
await page.getByRole('button', { name: 'Close' }).click();
await expect(page.getByLabel('Object Search Result').first()).toBeVisible();
await expect(page.getByLabel('Object Search Result').first()).toContainText(
`Clock A ${myItemsFolderName} Red Folder Blue Folder`
);
// Click [aria-label="OpenMCT Search"] a >> nth=0
await page.locator('[aria-label="Search Result"] >> nth=0').click();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
await page.getByLabel('Object Search Result').first().click();
await expect(page.getByLabel('Object Search Result').first()).toBeHidden();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
await grandSearchInput.fill('foo');
await expect(page.getByLabel('Object Search Result').first()).toBeHidden();
// 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();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
await grandSearchInput.click();
// Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"]
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
await grandSearchInput.fill('Cl');
await Promise.all([
page.waitForNavigation(),
page.locator('[aria-label="Clock A clock result"] >> text=Clock A').click()
page.getByLabel('OpenMCT Search').getByText('Clock A').click()
]);
await expect(page.locator('.is-object-type-clock')).toBeVisible();
await expect(page.getByRole('status', { name: 'Clock' })).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Disp');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(
await grandSearchInput.fill('Disp');
await expect(page.getByLabel('Object Search Result').first()).toContainText(
createdObjects.displayLayout.name
);
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toContainText('Folder');
await expect(page.getByLabel('Object Search Result').first()).not.toContainText('Folder');
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Clock C');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(
await grandSearchInput.fill('Clock C');
await expect(page.getByLabel('Object Search Result').first()).toContainText(
`Clock C ${myItemsFolderName} Red Folder Blue Folder`
);
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cloc');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(
await grandSearchInput.fill('Cloc');
await expect(page.getByLabel('Object Search Result').first()).toContainText(
`Clock A ${myItemsFolderName} Red Folder Blue Folder`
);
await expect(page.locator('[aria-label="Search Result"] >> nth=1')).toContainText(
await expect(page.getByLabel('Object Search Result').nth(1)).toContainText(
`Clock B ${myItemsFolderName} Red Folder Blue Folder`
);
await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText(
await expect(page.getByLabel('Object Search Result').nth(2)).toContainText(
`Clock C ${myItemsFolderName} Red Folder Blue Folder`
);
await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText(
await expect(page.getByLabel('Object Search Result').nth(3)).toContainText(
`Clock D ${myItemsFolderName} Red Folder Blue Folder`
);
await grandSearchInput.click();
await grandSearchInput.fill('Sine');
});
test('Clicking on a search result changes the URL even if the same type is already selected', async ({
page
}) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7303'
});
const { sineWaveGeneratorAlpha, sineWaveGeneratorBeta } = await createObjectsForSearch(page);
await grandSearchInput.click();
await grandSearchInput.fill('Sine');
await waitForSearchCompletion(page);
await page.getByLabel('OpenMCT Search').getByText('Sine Wave Generator Alpha').click();
const alphaPattern = new RegExp(sineWaveGeneratorAlpha.url.substring(1));
await expect(page).toHaveURL(alphaPattern);
await grandSearchInput.click();
await page.getByLabel('OpenMCT Search').getByText('Sine Wave Generator Beta').click();
const betaPattern = new RegExp(sineWaveGeneratorBeta.url.substring(1));
await expect(page).toHaveURL(betaPattern);
});
test('Validate empty search result', async ({ page }) => {
// Invalid search for objects
await page.type('input[type=search]', 'not found');
await grandSearchInput.fill('not found');
// Wait for search to complete
await waitForSearchCompletion(page);
// Get the search results
const searchResults = page.locator(searchResultSelector);
const searchResults = page.getByRole('listitem', { name: 'Object Search Result' });
// Verify that no results are found
expect(await searchResults.count()).toBe(0);
// Verify proper message appears
await expect(page.locator('text=No results found')).toBeVisible();
await expect(page.getByText('No results found')).toBeVisible();
});
test('Validate single object in search result @couchdb', async ({ page }) => {
@ -153,18 +177,18 @@ test.describe('Grand Search', () => {
});
// Full search for object
await page.type('input[type=search]', folderName);
await grandSearchInput.fill(folderName);
// Wait for search to complete
await waitForSearchCompletion(page);
// Get the search results
const searchResults = page.locator(searchResultSelector);
const searchResults = page.getByLabel('Object Search Result');
// Verify that one result is found
await expect(searchResults).toBeVisible();
expect(await searchResults.count()).toBe(1);
await expect(searchResults).toHaveText(folderName);
await expect(searchResults).toContainText(folderName);
});
test('Search results are debounced @couchdb', async ({ page }) => {
@ -185,7 +209,7 @@ test.describe('Grand Search', () => {
});
// Full search for object
await page.type('input[type=search]', 'Clock', { delay: 100 });
await grandSearchInput.pressSequentially('Clock', { delay: 100 });
// Wait for search to finish
await waitForSearchCompletion(page);
@ -194,9 +218,7 @@ test.describe('Grand Search', () => {
// 1. batched request for latest telemetry using the bulk API
expect(networkRequests.length).toBe(1);
const searchResultDropDown = await page.locator(searchResultDropDownSelector);
await expect(searchResultDropDown).toContainText('Clock A');
await expect(page.getByRole('list', { name: 'Object Results' })).toContainText('Clock A');
});
test('Slowly typing after search debounce will abort requests @couchdb', async ({ page }) => {
@ -246,27 +268,40 @@ test.describe('Grand Search', () => {
});
// Partial search for objects
await page.type('input[type=search]', 'e928a26e');
await grandSearchInput.fill('e928a26e');
// Wait for search to finish
await waitForSearchCompletion(page);
const searchResultDropDown = page.locator(searchResultDropDownSelector);
const searchResultDropDown = page.getByRole('dialog', { name: 'Search Results' });
// Verify that the search result/s correctly match the search query
await expect(searchResultDropDown).toContainText(folderName1);
await expect(searchResultDropDown).toContainText(folderName2);
// Get the search results
const searchResults = page.locator(searchResultSelector);
const objectSearchResults = page.getByLabel('Object Search Result');
// Verify that two results are found
expect(await searchResults.count()).toBe(2);
expect(await objectSearchResults.count()).toBe(2);
});
});
/**
* Wait for search to complete
*
* @param {import('@playwright/test').Page} page
*/
async function waitForSearchCompletion(page) {
// Wait loading spinner to disappear
await page.waitForSelector('.search-finished');
await expect(
page
.getByRole('list', { name: 'Object Results' })
.or(
page
.getByRole('list', { name: 'Annotation Results' })
.or(page.getByText('No results found'))
)
).toBeVisible();
}
/**
@ -306,13 +341,20 @@ async function createObjectsForSearch(page) {
parent: blueFolder.uuid
});
const sineWaveGeneratorAlpha = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Sine Wave Generator Alpha'
});
const sineWaveGeneratorBeta = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Sine Wave Generator Beta'
});
const displayLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout'
});
// Go back into edit mode for the display layout
await page.locator('button[title="Edit"]').click();
return {
redFolder,
blueFolder,
@ -320,6 +362,8 @@ async function createObjectsForSearch(page) {
clockB,
clockC,
clockD,
displayLayout
displayLayout,
sineWaveGeneratorAlpha,
sineWaveGeneratorBeta
};
}

View File

@ -45,19 +45,21 @@ test.describe("Visual - Check Notification Info Banner of 'Save successful' @a11
name: 'Default Clock'
});
// Click on the div with role="alert" that has "Save successful" text
await page.locator('div[role="alert"]:has-text("Save successful")').click();
await page.getByRole('alert').filter({ hasText: 'Save successful' }).click();
// Verify there is a div with role="dialog"
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(true);
await expect(page.getByRole('dialog', { name: 'Overlay' })).toBeVisible();
// Verify the div with role="dialog" contains text "Save successful"
expect(await page.locator('div[role="dialog"]').innerText()).toContain('Save successful');
expect(await page.getByRole('dialog', { name: 'Overlay' }).innerText()).toContain(
'Save successful'
);
await percySnapshot(page, `Notification banner shows Save successful (theme: '${theme}')`);
// Verify there is a button with text "Dismiss"
expect(await page.locator('button:has-text("Dismiss")').isVisible()).toBe(true);
await expect(page.getByText('Dismiss', { exact: true })).toBeVisible();
await percySnapshot(page, `Notification banner shows Dismiss (theme: '${theme}')`);
// Click on button with text "Dismiss"
await page.locator('button:has-text("Dismiss")').click();
await page.getByText('Dismiss', { exact: true }).click();
// Verify there is no div with role="dialog"
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(false);
await expect(page.getByRole('dialog', { name: 'Overlay' })).toBeHidden();
await percySnapshot(page, `Notification banner dismissed (theme: '${theme}')`);
});
test.afterEach(async ({ page }, testInfo) => {

View File

@ -338,7 +338,7 @@ export class MCT extends EventEmitter {
component.$nextTick(() => {
this.layout = component;
this.app = appLayout;
Browse(this);
this.browseRoutes = new Browse(this);
window.addEventListener('beforeunload', this.destroy);
this.router.start();
this.emit('start');

View File

@ -34,6 +34,7 @@
class="c-overlay__contents js-notebook-snapshot-item-wrapper"
tabindex="0"
aria-modal="true"
aria-label="Overlay"
role="dialog"
></div>
<div v-if="buttons" class="c-overlay__button-bar">

View File

@ -22,7 +22,13 @@
<template>
<div class="u-contents">
<div class="c-clock l-time-display u-style-receiver js-style-receiver">
<div
role="status"
aria-live="polite"
aria-atomic="true"
aria-label="Clock"
class="c-clock l-time-display u-style-receiver js-style-receiver"
>
<div class="c-clock__timezone">
{{ timeZoneAbbr }}
</div>

View File

@ -23,11 +23,11 @@
<template>
<div
class="c-gsearch-result c-gsearch-result--annotation"
aria-label="Search Result"
role="presentation"
aria-label="Annotation Search Result"
role="listitem"
>
<div class="c-gsearch-result__type-icon" :class="resultTypeIcon"></div>
<div class="c-gsearch-result__body" aria-label="Annotation Search Result">
<div class="c-gsearch-result__body">
<div class="c-gsearch-result__title" @click="clickedResult">
{{ getResultName }}
</div>

View File

@ -272,15 +272,15 @@ describe('GrandSearch', () => {
it('should render an annotation search result', async () => {
await grandSearchComponent.$refs.root.searchEverything('S');
await nextTick();
const annotationResults = document.querySelectorAll('[aria-label="Search Result"]');
expect(annotationResults.length).toBe(2);
expect(annotationResults[1].innerText).toContain('Driving');
const annotationResults = document.querySelectorAll('[aria-label="Annotation Search Result"]');
expect(annotationResults.length).toBe(1);
expect(annotationResults[0].innerText).toContain('Driving');
});
it('should render no annotation search results if no match', async () => {
await grandSearchComponent.$refs.root.searchEverything('Qbert');
await nextTick();
const annotationResults = document.querySelectorAll('[aria-label="Search Result"]');
const annotationResults = document.querySelectorAll('[aria-label="Annotation Search Result"]');
expect(annotationResults.length).toBe(0);
});
@ -288,10 +288,9 @@ describe('GrandSearch', () => {
await grandSearchComponent.$refs.root.searchEverything('Folder');
grandSearchComponent.$refs.root.openmct.router.path = [mockDisplayLayout];
await nextTick();
const searchResults = document.querySelectorAll('[name="Test Folder"]');
expect(searchResults.length).toBe(1);
expect(searchResults[0].innerText).toContain('Folder');
searchResults[0].click();
const folderResult = document.querySelector('[name="Test Folder"]');
expect(folderResult).not.toBeNull();
folderResult.click();
const previewWindow = document.querySelector('.js-preview-window');
expect(previewWindow.innerText).toContain('Snapshot');
});
@ -300,7 +299,7 @@ describe('GrandSearch', () => {
await grandSearchComponent.$refs.root.searchEverything('Dri');
grandSearchComponent.$refs.root.openmct.router.path = [mockDisplayLayout];
await nextTick();
const annotationResults = document.querySelectorAll('[aria-label="Search Result"]');
const annotationResults = document.querySelectorAll('[aria-label="Annotation Search Result"]');
expect(annotationResults.length).toBe(1);
expect(annotationResults[0].innerText).toContain('Driving');
annotationResults[0].click();

View File

@ -23,8 +23,8 @@
<template>
<div
class="c-gsearch-result c-gsearch-result--object"
aria-label="Search Result"
role="presentation"
aria-label="Object Search Result"
role="listitem"
>
<div class="c-gsearch-result__type-icon" :class="resultTypeIcon"></div>
<div

View File

@ -21,14 +21,15 @@
-->
<template>
<div class="c-gsearch__dropdown">
<div role="dialog" aria-label="Search Results Dropdown" class="c-gsearch__dropdown">
<div v-show="resultsShown" class="c-gsearch__results-wrapper">
<div class="c-gsearch__results" :class="{ 'search-finished': !searchLoading }">
<div
v-if="objectResults && objectResults.length"
v-if="objectResults?.length"
ref="objectResults"
class="c-gsearch__results-section"
role="listbox"
role="list"
aria-label="Object Results"
>
<div class="c-gsearch__results-section-title">Object Results</div>
<object-search-result
@ -39,7 +40,12 @@
@click="selectedResult"
/>
</div>
<div v-if="annotationResults && annotationResults.length" ref="annotationResults">
<div
v-if="annotationResults?.length"
ref="annotationResults"
role="list"
aria-label="Annotation Results"
>
<div class="c-gsearch__results-section-title">Annotation Results</div>
<annotation-search-result
v-for="annotationResult in annotationResults"

View File

@ -20,7 +20,7 @@
at runtime from the About dialog for additional information.
-->
<template>
<div class="l-preview-window js-preview-window">
<div role="dialog" aria-label="Preview Container" class="l-preview-window js-preview-window">
<PreviewHeader
ref="previewHeader"
:current-view="currentViewProvider"

View File

@ -306,10 +306,11 @@ class ApplicationRouter extends EventEmitter {
*
* @param {string} newPath new path of url
* @param {string} oldPath old path of url
* @returns {boolean} true if path changed, false otherwise
*/
doPathChange(newPath, oldPath) {
if (newPath === oldPath) {
return;
return false;
}
let route = this.routes.filter((r) => r.matcher.test(newPath))[0];
@ -320,6 +321,8 @@ class ApplicationRouter extends EventEmitter {
this.openmct.telemetry.abortAllRequests();
this.emit('change:path', newPath, oldPath);
return true;
}
/**
@ -328,10 +331,11 @@ class ApplicationRouter extends EventEmitter {
*
* @param {object} newParams new params of url
* @param {object} oldParams old params of url
* @returns {boolean} true if params changed, false otherwise
*/
doParamsChange(newParams, oldParams) {
if (_.isEqual(newParams, oldParams)) {
return;
return false;
}
let changedParams = {};
@ -347,6 +351,7 @@ class ApplicationRouter extends EventEmitter {
});
this.emit('change:params', newParams, oldParams, changedParams);
return true;
}
/**
@ -368,9 +373,13 @@ class ApplicationRouter extends EventEmitter {
return;
}
this.doPathChange(newLocation.path, oldLocation.path);
const pathChanged = this.doPathChange(newLocation.path, oldLocation.path);
this.doParamsChange(newLocation.params, oldLocation.params);
const paramsChanged = this.doParamsChange(newLocation.params, oldLocation.params, pathChanged);
if (pathChanged || paramsChanged) {
// If either path or parameters have changed, we update the URL in the address bar.
this.set(newLocation.path, newLocation.getQueryString());
}
}
/**

View File

@ -1,153 +1,167 @@
export default function install(openmct) {
let navigateCall = 0;
let browseObject;
let unobserve = undefined;
let currentObjectPath;
let isRoutingInProgress = false;
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, 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.
*****************************************************************************/
openmct.router.route(/^\/browse\/?$/, navigateToFirstChildOfRoot);
openmct.router.route(/^\/browse\/(.*)$/, (path, results, params) => {
isRoutingInProgress = true;
let navigatePath = results[1];
clearMutationListeners();
export default class Browse {
#navigateCall = 0;
#browseObject = null;
#unobserve = undefined;
#currentObjectPath = undefined;
#isRoutingInProgress = false;
#openmct;
navigateToPath(navigatePath, params.view);
});
constructor(openmct) {
this.#openmct = openmct;
this.#openmct.router.route(/^\/browse\/?$/, this.#navigateToFirstChildOfRoot.bind(this));
this.#openmct.router.route(/^\/browse\/(.*)$/, this.#handleBrowseRoute.bind(this));
this.#openmct.router.on('change:params', this.#onParamsChanged.bind(this));
}
openmct.router.on('change:params', onParamsChanged);
function onParamsChanged(newParams, oldParams, changed) {
if (isRoutingInProgress) {
#onParamsChanged(newParams, oldParams, changed) {
if (this.#isRoutingInProgress) {
return;
}
if (changed.view && browseObject) {
let provider = openmct.objectViews.getByProviderKey(changed.view);
viewObject(browseObject, provider);
if (changed.view && this.#browseObject) {
const provider = this.#openmct.objectViews.getByProviderKey(changed.view);
this.#viewObject(this.#browseObject, provider);
}
}
function viewObject(object, viewProvider) {
currentObjectPath = openmct.router.path;
#viewObject(object, viewProvider) {
this.#currentObjectPath = this.#openmct.router.path;
openmct.layout.$refs.browseObject.show(object, viewProvider.key, true, currentObjectPath);
openmct.layout.$refs.browseBar.domainObject = object;
openmct.layout.$refs.browseBar.viewKey = viewProvider.key;
this.#openmct.layout.$refs.browseObject.show(
object,
viewProvider.key,
true,
this.#currentObjectPath
);
this.#openmct.layout.$refs.browseBar.domainObject = object;
this.#openmct.layout.$refs.browseBar.viewKey = viewProvider.key;
}
function updateDocumentTitleOnNameMutation(newName) {
#updateDocumentTitleOnNameMutation(newName) {
if (typeof newName === 'string' && newName !== document.title) {
document.title = newName;
openmct.layout.$refs.browseBar.domainObject = {
...openmct.layout.$refs.browseBar.domainObject,
this.#openmct.layout.$refs.browseBar.domainObject = {
...this.#openmct.layout.$refs.browseBar.domainObject,
name: newName
};
}
}
function navigateToPath(path, currentViewKey) {
navigateCall++;
let currentNavigation = navigateCall;
async #navigateToPath(path, currentViewKey) {
this.#navigateCall++;
const currentNavigation = this.#navigateCall;
if (unobserve) {
unobserve();
unobserve = undefined;
if (this.#unobserve) {
this.#unobserve();
this.#unobserve = undefined;
}
//Split path into object identifiers
if (!Array.isArray(path)) {
path = path.split('/');
}
return pathToObjects(path).then((objects) => {
isRoutingInProgress = false;
if (currentNavigation !== navigateCall) {
return; // Prevent race.
}
objects = objects.reverse();
openmct.router.path = objects;
openmct.router.emit('afterNavigation');
browseObject = objects[0];
openmct.layout.$refs.browseBar.domainObject = browseObject;
if (!browseObject) {
openmct.layout.$refs.browseObject.clear();
return;
}
let currentProvider = openmct.objectViews.getByProviderKey(currentViewKey);
document.title = browseObject.name; //change document title to current object in main view
// assign listener to global for later clearing
unobserve = openmct.objects.observe(browseObject, 'name', updateDocumentTitleOnNameMutation);
if (currentProvider && currentProvider.canView(browseObject, openmct.router.path)) {
viewObject(browseObject, currentProvider);
return;
}
let defaultProvider = openmct.objectViews.get(browseObject, openmct.router.path)[0];
if (defaultProvider) {
openmct.router.updateParams({
view: defaultProvider.key
});
} else {
openmct.router.updateParams({
view: undefined
});
openmct.layout.$refs.browseObject.clear();
}
});
let objects = await this.#pathToObjects(path);
if (currentNavigation !== this.#navigateCall) {
return; // Prevent race.
}
this.#isRoutingInProgress = false;
objects = objects.reverse();
this.#openmct.router.path = objects;
this.#browseObject = objects[0];
this.#openmct.router.emit('afterNavigation');
this.#openmct.layout.$refs.browseBar.domainObject = this.#browseObject;
if (!this.#browseObject) {
this.#openmct.layout.$refs.browseObject.clear();
return;
}
document.title = this.#browseObject.name; //change document title to current object in main view
this.#unobserve = this.#openmct.objects.observe(
this.#browseObject,
'name',
this.#updateDocumentTitleOnNameMutation.bind(this)
);
const currentProvider = this.#openmct.objectViews.getByProviderKey(currentViewKey);
if (currentProvider && currentProvider.canView(this.#browseObject, this.#openmct.router.path)) {
this.#viewObject(this.#browseObject, currentProvider);
return;
}
const routerPath = this.#openmct.router.path;
const retrievedObjectViews = this.#openmct.objectViews.get(this.#browseObject, routerPath);
const defaultProvider = retrievedObjectViews?.[0];
if (defaultProvider) {
this.#openmct.router.updateParams({ view: defaultProvider.key });
} else {
this.#openmct.router.updateParams({ view: undefined });
this.#openmct.layout.$refs.browseObject.clear();
}
}
function pathToObjects(path) {
#pathToObjects(path) {
return Promise.all(
path.map((keyString) => {
let identifier = openmct.objects.parseKeyString(keyString);
if (openmct.objects.supportsMutation(identifier)) {
return openmct.objects.getMutable(identifier);
} else {
return openmct.objects.get(identifier);
}
const identifier = this.#openmct.objects.parseKeyString(keyString);
return this.#openmct.objects.supportsMutation(identifier)
? this.#openmct.objects.getMutable(identifier)
: this.#openmct.objects.get(identifier);
})
);
}
function navigateToFirstChildOfRoot() {
openmct.objects
.get('ROOT')
.then((rootObject) => {
const composition = openmct.composition.get(rootObject);
if (!composition) {
return;
}
async #navigateToFirstChildOfRoot() {
try {
const rootObject = await this.#openmct.objects.get('ROOT');
const composition = this.#openmct.composition.get(rootObject);
if (!composition) {
return;
}
composition
.load()
.then((children) => {
let lastChild = children[children.length - 1];
if (lastChild) {
let lastChildId = openmct.objects.makeKeyString(lastChild.identifier);
openmct.router.setPath(`#/browse/${lastChildId}`);
}
})
.catch((e) => console.error(e));
})
.catch((e) => console.error(e));
const children = await composition.load();
const lastChild = children[children.length - 1];
if (lastChild) {
const lastChildId = this.#openmct.objects.makeKeyString(lastChild.identifier);
this.#openmct.router.setPath(`#/browse/${lastChildId}`);
}
} catch (e) {
console.error(e);
}
}
function clearMutationListeners() {
if (openmct.router.path !== undefined) {
openmct.router.path.forEach((pathObject) => {
#clearMutationListeners() {
if (this.#openmct.router.path) {
this.#openmct.router.path.forEach((pathObject) => {
if (pathObject.isMutable) {
openmct.objects.destroyMutable(pathObject);
this.#openmct.objects.destroyMutable(pathObject);
}
});
}
}
#handleBrowseRoute(path, results, params) {
this.#isRoutingInProgress = true;
const navigatePath = results[1];
this.#clearMutationListeners();
this.#navigateToPath(navigatePath, params.view);
}
}