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 // Create a new Display Layout object
await createDomainObjectWithDefaults(page, { type: 'Display Layout' }); await createDomainObjectWithDefaults(page, { type: 'Display Layout' });
// Dismiss notification banner
await page.getByRole('button', { name: 'Dismiss' }).click();
// Click on the button "Review 1 Notification" // 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 // 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 // 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 // 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 // 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 // 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(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Verify that Notification List is NOT open // 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(); 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')).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?' 'This action will permanently delete this container from this Flexible Layout. Do you want to continue?'
); );
await page.getByRole('button', { name: 'OK' }).click(); 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); 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')).toHaveText( await expect(page.getByRole('dialog', { name: 'Overlay' })).toHaveText(
'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' }).click(); 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(); await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
//Partial match for "Science" should only return Science //Partial match for "Science" should only return Science
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc'); 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="Annotation Search Result"]').first()).toContainText(
await expect(page.locator('[aria-label="Search Result"]').first()).not.toContainText('Driving'); 'Science'
await expect(page.locator('[aria-label="Search Result"]').first()).not.toContainText( );
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' 'Drilling'
); );
//Searching for a tag which does not exist should return an empty result //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"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq'); 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('[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(); await expect(page.locator('button:has-text("Add Tag")')).toBeVisible();
// Test canceling adding a tag after we just click "Add Tag" // Test canceling adding a tag after we just click "Add Tag"
await page.locator('button:has-text("Add Tag")').click(); 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(); await expect(page.locator('button:has-text("Add Tag")')).toBeVisible();
}); });
test('Can search for tags and preview works properly', async ({ page }) => { test('Can search for tags and preview works properly', async ({ page }) => {
await createNotebookEntryAndTags(page); await createNotebookEntryAndTags(page);
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); await page.getByRole('search').getByLabel('Search Input').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); await page.getByRole('search').getByLabel('Search Input').fill('sc');
await expect(page.locator('[aria-label="Search Result"]')).toContainText('Science'); await expect(page.getByRole('listitem', { name: 'Annotation Search Result' })).toContainText(
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText('Driving'); '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.getByRole('search').getByLabel('Search Input').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc'); await page.getByRole('search').getByLabel('Search Input').fill('Sc');
await expect(page.locator('[aria-label="Search Result"]')).toContainText('Science'); await expect(page.getByRole('listitem', { name: 'Annotation Search Result' })).toContainText(
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText('Driving'); '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.getByRole('search').getByLabel('Search Input').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq'); await page.getByRole('search').getByLabel('Search Input').fill('Xq');
await expect(page.locator('text=No results found')).toBeVisible(); await expect(page.getByText('No results found')).toBeVisible();
await createDomainObjectWithDefaults(page, { await createDomainObjectWithDefaults(page, {
type: 'Display Layout' type: 'Display Layout'
}); });
// Go back into edit mode for the 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.getByRole('search').getByLabel('Search Input').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc'); await page.getByRole('search').getByLabel('Search Input').fill('Sc');
await expect(page.locator('[aria-label="Search Result"]')).toContainText('Science'); await expect(page.getByRole('listitem', { name: 'Annotation Search Result' })).toContainText(
'Science'
);
await page.getByText('Entry 0').click(); await page.getByText('Entry 0').click();
await expect(page.locator('.js-preview-window')).toBeVisible(); 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"]')).toContainText('Science');
await expect(page.locator('[aria-label="Tags Inspector"]')).not.toContainText('Driving'); await expect(page.locator('[aria-label="Tags Inspector"]')).not.toContainText('Driving');
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); await page.getByRole('search').getByLabel('Search Input').fill('sc');
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText('Driving'); await expect(
page.getByRole('listitem', { name: 'Annotation Search Result' })
).not.toContainText('Driving');
}); });
test('Can delete entries without tags', async ({ page }) => { 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.locator('button:has-text("OK")').click();
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed'); await page.getByRole('search').getByLabel('Search Input').fill('Unnamed');
await expect(page.locator('text=No results found')).toBeVisible(); await expect(page.getByText('No results found')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sci'); await page.getByRole('search').getByLabel('Search Input').fill('sci');
await expect(page.locator('text=No results found')).toBeVisible(); await expect(page.getByText('No results found')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('dri'); await page.getByRole('search').getByLabel('Search Input').fill('dri');
await expect(page.locator('text=No results found')).toBeVisible(); await expect(page.getByText('No results found')).toBeVisible();
}); });
test('Tags persist across reload', async ({ page }) => { test('Tags persist across reload', async ({ page }) => {
//Go to baseURL //Go to baseURL

View File

@ -29,12 +29,15 @@ import { createDomainObjectWithDefaults } from '../../appActions.js';
import { expect, test } from '../../pluginFixtures.js'; import { expect, test } from '../../pluginFixtures.js';
test.describe('Grand Search', () => { test.describe('Grand Search', () => {
const searchResultSelector = '.c-gsearch-result__title'; let grandSearchInput;
const searchResultDropDownSelector = '.c-gsearch__results';
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
grandSearchInput = page
.getByLabel('OpenMCT Search')
.getByRole('searchbox', { name: 'Search Input' });
// Go to baseURL // 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 ({ 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); const createdObjects = await createObjectsForSearch(page);
// Click [aria-label="OpenMCT Search"] input[type="search"] // Go back into edit mode for the display layout
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); await page.getByRole('button', { name: 'Edit' }).click();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl'); await grandSearchInput.click();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText( await grandSearchInput.fill('Cl');
await expect(page.getByLabel('Object Search Result').first()).toContainText(
`Clock A ${myItemsFolderName} Red Folder Blue Folder` `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` `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` `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` `Clock D ${myItemsFolderName} Red Folder Blue Folder`
); );
// Click the Elements pool to dismiss the search menu // Click the Elements pool to dismiss the search menu
await page.getByRole('tab', { name: 'Elements' }).click(); 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 grandSearchInput.click();
await page.locator('[aria-label="Clock A clock result"] >> text=Clock A').click(); await page.getByLabel('OpenMCT Search').getByText('Clock A').click();
await expect(page.locator('.js-preview-window')).toBeVisible(); await expect(page.getByRole('dialog', { name: 'Preview Container' })).toBeVisible();
// Click [aria-label="Close"] // Close the Preview window
await page.locator('[aria-label="Close"]').click(); await page.getByRole('button', { name: 'Close' }).click();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeVisible(); await expect(page.getByLabel('Object Search Result').first()).toBeVisible();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText( await expect(page.getByLabel('Object Search Result').first()).toContainText(
`Clock A ${myItemsFolderName} Red Folder Blue Folder` `Clock A ${myItemsFolderName} Red Folder Blue Folder`
); );
// Click [aria-label="OpenMCT Search"] a >> nth=0 await page.getByLabel('Object Search Result').first().click();
await page.locator('[aria-label="Search Result"] >> nth=0').click(); await expect(page.getByLabel('Object Search Result').first()).toBeHidden();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
// Fill [aria-label="OpenMCT Search"] input[type="search"] await grandSearchInput.fill('foo');
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo'); await expect(page.getByLabel('Object Search Result').first()).toBeHidden();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1 // Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
await page await page.getByRole('button', { name: 'Save' }).click();
.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button')
.nth(1)
.click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"] // 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"] // 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([ await Promise.all([
page.waitForNavigation(), 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 grandSearchInput.fill('Disp');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText( await expect(page.getByLabel('Object Search Result').first()).toContainText(
createdObjects.displayLayout.name 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 grandSearchInput.fill('Clock C');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText( await expect(page.getByLabel('Object Search Result').first()).toContainText(
`Clock C ${myItemsFolderName} Red Folder Blue Folder` `Clock C ${myItemsFolderName} Red Folder Blue Folder`
); );
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cloc'); await grandSearchInput.fill('Cloc');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText( await expect(page.getByLabel('Object Search Result').first()).toContainText(
`Clock A ${myItemsFolderName} Red Folder Blue Folder` `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` `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` `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` `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 }) => { test('Validate empty search result', async ({ page }) => {
// Invalid search for objects // Invalid search for objects
await page.type('input[type=search]', 'not found'); await grandSearchInput.fill('not found');
// Wait for search to complete // Wait for search to complete
await waitForSearchCompletion(page); await waitForSearchCompletion(page);
// Get the search results // Get the search results
const searchResults = page.locator(searchResultSelector); const searchResults = page.getByRole('listitem', { name: 'Object Search Result' });
// Verify that no results are found // Verify that no results are found
expect(await searchResults.count()).toBe(0); expect(await searchResults.count()).toBe(0);
// Verify proper message appears // 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 }) => { test('Validate single object in search result @couchdb', async ({ page }) => {
@ -153,18 +177,18 @@ test.describe('Grand Search', () => {
}); });
// Full search for object // Full search for object
await page.type('input[type=search]', folderName); await grandSearchInput.fill(folderName);
// Wait for search to complete // Wait for search to complete
await waitForSearchCompletion(page); await waitForSearchCompletion(page);
// Get the search results // Get the search results
const searchResults = page.locator(searchResultSelector); const searchResults = page.getByLabel('Object Search Result');
// Verify that one result is found // Verify that one result is found
await expect(searchResults).toBeVisible(); await expect(searchResults).toBeVisible();
expect(await searchResults.count()).toBe(1); expect(await searchResults.count()).toBe(1);
await expect(searchResults).toHaveText(folderName); await expect(searchResults).toContainText(folderName);
}); });
test('Search results are debounced @couchdb', async ({ page }) => { test('Search results are debounced @couchdb', async ({ page }) => {
@ -185,7 +209,7 @@ test.describe('Grand Search', () => {
}); });
// Full search for object // Full search for object
await page.type('input[type=search]', 'Clock', { delay: 100 }); await grandSearchInput.pressSequentially('Clock', { delay: 100 });
// Wait for search to finish // Wait for search to finish
await waitForSearchCompletion(page); await waitForSearchCompletion(page);
@ -194,9 +218,7 @@ test.describe('Grand Search', () => {
// 1. batched request for latest telemetry using the bulk API // 1. batched request for latest telemetry using the bulk API
expect(networkRequests.length).toBe(1); expect(networkRequests.length).toBe(1);
const searchResultDropDown = await page.locator(searchResultDropDownSelector); await expect(page.getByRole('list', { name: 'Object Results' })).toContainText('Clock A');
await expect(searchResultDropDown).toContainText('Clock A');
}); });
test('Slowly typing after search debounce will abort requests @couchdb', async ({ page }) => { test('Slowly typing after search debounce will abort requests @couchdb', async ({ page }) => {
@ -246,27 +268,40 @@ test.describe('Grand Search', () => {
}); });
// Partial search for objects // Partial search for objects
await page.type('input[type=search]', 'e928a26e'); await grandSearchInput.fill('e928a26e');
// Wait for search to finish // Wait for search to finish
await waitForSearchCompletion(page); 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 // Verify that the search result/s correctly match the search query
await expect(searchResultDropDown).toContainText(folderName1); await expect(searchResultDropDown).toContainText(folderName1);
await expect(searchResultDropDown).toContainText(folderName2); await expect(searchResultDropDown).toContainText(folderName2);
// Get the search results // Get the search results
const searchResults = page.locator(searchResultSelector); const objectSearchResults = page.getByLabel('Object Search Result');
// Verify that two results are found // 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) { async function waitForSearchCompletion(page) {
// Wait loading spinner to disappear // 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 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, { const displayLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout' type: 'Display Layout'
}); });
// Go back into edit mode for the display layout
await page.locator('button[title="Edit"]').click();
return { return {
redFolder, redFolder,
blueFolder, blueFolder,
@ -320,6 +362,8 @@ async function createObjectsForSearch(page) {
clockB, clockB,
clockC, clockC,
clockD, 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' name: 'Default Clock'
}); });
// Click on the div with role="alert" that has "Save successful" text // 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" // 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" // 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}')`); await percySnapshot(page, `Notification banner shows Save successful (theme: '${theme}')`);
// Verify there is a button with text "Dismiss" // 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}')`); await percySnapshot(page, `Notification banner shows Dismiss (theme: '${theme}')`);
// Click on button with text "Dismiss" // 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" // 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}')`); await percySnapshot(page, `Notification banner dismissed (theme: '${theme}')`);
}); });
test.afterEach(async ({ page }, testInfo) => { test.afterEach(async ({ page }, testInfo) => {

View File

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

View File

@ -34,6 +34,7 @@
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-modal="true"
aria-label="Overlay"
role="dialog" role="dialog"
></div> ></div>
<div v-if="buttons" class="c-overlay__button-bar"> <div v-if="buttons" class="c-overlay__button-bar">

View File

@ -22,7 +22,13 @@
<template> <template>
<div class="u-contents"> <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"> <div class="c-clock__timezone">
{{ timeZoneAbbr }} {{ timeZoneAbbr }}
</div> </div>

View File

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

View File

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

View File

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

View File

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

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="l-preview-window js-preview-window"> <div role="dialog" aria-label="Preview Container" class="l-preview-window js-preview-window">
<PreviewHeader <PreviewHeader
ref="previewHeader" ref="previewHeader"
:current-view="currentViewProvider" :current-view="currentViewProvider"

View File

@ -306,10 +306,11 @@ class ApplicationRouter extends EventEmitter {
* *
* @param {string} newPath new path of url * @param {string} newPath new path of url
* @param {string} oldPath old path of url * @param {string} oldPath old path of url
* @returns {boolean} true if path changed, false otherwise
*/ */
doPathChange(newPath, oldPath) { doPathChange(newPath, oldPath) {
if (newPath === oldPath) { if (newPath === oldPath) {
return; return false;
} }
let route = this.routes.filter((r) => r.matcher.test(newPath))[0]; let route = this.routes.filter((r) => r.matcher.test(newPath))[0];
@ -320,6 +321,8 @@ class ApplicationRouter extends EventEmitter {
this.openmct.telemetry.abortAllRequests(); this.openmct.telemetry.abortAllRequests();
this.emit('change:path', newPath, oldPath); 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} newParams new params of url
* @param {object} oldParams old params of url * @param {object} oldParams old params of url
* @returns {boolean} true if params changed, false otherwise
*/ */
doParamsChange(newParams, oldParams) { doParamsChange(newParams, oldParams) {
if (_.isEqual(newParams, oldParams)) { if (_.isEqual(newParams, oldParams)) {
return; return false;
} }
let changedParams = {}; let changedParams = {};
@ -347,6 +351,7 @@ class ApplicationRouter extends EventEmitter {
}); });
this.emit('change:params', newParams, oldParams, changedParams); this.emit('change:params', newParams, oldParams, changedParams);
return true;
} }
/** /**
@ -368,9 +373,13 @@ class ApplicationRouter extends EventEmitter {
return; 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; * Open MCT, Copyright (c) 2014-2023, United States Government
let browseObject; * as represented by the Administrator of the National Aeronautics and Space
let unobserve = undefined; * Administration. All rights reserved.
let currentObjectPath; *
let isRoutingInProgress = false; * 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); export default class Browse {
openmct.router.route(/^\/browse\/(.*)$/, (path, results, params) => { #navigateCall = 0;
isRoutingInProgress = true; #browseObject = null;
let navigatePath = results[1]; #unobserve = undefined;
clearMutationListeners(); #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); #onParamsChanged(newParams, oldParams, changed) {
if (this.#isRoutingInProgress) {
function onParamsChanged(newParams, oldParams, changed) {
if (isRoutingInProgress) {
return; return;
} }
if (changed.view && browseObject) { if (changed.view && this.#browseObject) {
let provider = openmct.objectViews.getByProviderKey(changed.view); const provider = this.#openmct.objectViews.getByProviderKey(changed.view);
viewObject(browseObject, provider); this.#viewObject(this.#browseObject, provider);
} }
} }
function viewObject(object, viewProvider) { #viewObject(object, viewProvider) {
currentObjectPath = openmct.router.path; this.#currentObjectPath = this.#openmct.router.path;
openmct.layout.$refs.browseObject.show(object, viewProvider.key, true, currentObjectPath); this.#openmct.layout.$refs.browseObject.show(
openmct.layout.$refs.browseBar.domainObject = object; object,
viewProvider.key,
openmct.layout.$refs.browseBar.viewKey = 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) { if (typeof newName === 'string' && newName !== document.title) {
document.title = newName; document.title = newName;
openmct.layout.$refs.browseBar.domainObject = { this.#openmct.layout.$refs.browseBar.domainObject = {
...openmct.layout.$refs.browseBar.domainObject, ...this.#openmct.layout.$refs.browseBar.domainObject,
name: newName name: newName
}; };
} }
} }
function navigateToPath(path, currentViewKey) { async #navigateToPath(path, currentViewKey) {
navigateCall++; this.#navigateCall++;
let currentNavigation = navigateCall; const currentNavigation = this.#navigateCall;
if (unobserve) { if (this.#unobserve) {
unobserve(); this.#unobserve();
unobserve = undefined; this.#unobserve = undefined;
} }
//Split path into object identifiers
if (!Array.isArray(path)) { if (!Array.isArray(path)) {
path = path.split('/'); path = path.split('/');
} }
return pathToObjects(path).then((objects) => { let objects = await this.#pathToObjects(path);
isRoutingInProgress = false; if (currentNavigation !== this.#navigateCall) {
return; // Prevent race.
if (currentNavigation !== navigateCall) { }
return; // Prevent race. this.#isRoutingInProgress = false;
} objects = objects.reverse();
this.#openmct.router.path = objects;
objects = objects.reverse(); this.#browseObject = objects[0];
this.#openmct.router.emit('afterNavigation');
openmct.router.path = objects; this.#openmct.layout.$refs.browseBar.domainObject = this.#browseObject;
openmct.router.emit('afterNavigation'); if (!this.#browseObject) {
browseObject = objects[0]; this.#openmct.layout.$refs.browseObject.clear();
return;
openmct.layout.$refs.browseBar.domainObject = browseObject; }
if (!browseObject) { document.title = this.#browseObject.name; //change document title to current object in main view
openmct.layout.$refs.browseObject.clear(); this.#unobserve = this.#openmct.objects.observe(
this.#browseObject,
return; 'name',
} this.#updateDocumentTitleOnNameMutation.bind(this)
);
let currentProvider = openmct.objectViews.getByProviderKey(currentViewKey); const currentProvider = this.#openmct.objectViews.getByProviderKey(currentViewKey);
document.title = browseObject.name; //change document title to current object in main view if (currentProvider && currentProvider.canView(this.#browseObject, this.#openmct.router.path)) {
// assign listener to global for later clearing this.#viewObject(this.#browseObject, currentProvider);
unobserve = openmct.objects.observe(browseObject, 'name', updateDocumentTitleOnNameMutation); return;
}
if (currentProvider && currentProvider.canView(browseObject, openmct.router.path)) { const routerPath = this.#openmct.router.path;
viewObject(browseObject, currentProvider); const retrievedObjectViews = this.#openmct.objectViews.get(this.#browseObject, routerPath);
const defaultProvider = retrievedObjectViews?.[0];
return; if (defaultProvider) {
} this.#openmct.router.updateParams({ view: defaultProvider.key });
} else {
let defaultProvider = openmct.objectViews.get(browseObject, openmct.router.path)[0]; this.#openmct.router.updateParams({ view: undefined });
if (defaultProvider) { this.#openmct.layout.$refs.browseObject.clear();
openmct.router.updateParams({ }
view: defaultProvider.key
});
} else {
openmct.router.updateParams({
view: undefined
});
openmct.layout.$refs.browseObject.clear();
}
});
} }
function pathToObjects(path) { #pathToObjects(path) {
return Promise.all( return Promise.all(
path.map((keyString) => { path.map((keyString) => {
let identifier = openmct.objects.parseKeyString(keyString); const identifier = this.#openmct.objects.parseKeyString(keyString);
if (openmct.objects.supportsMutation(identifier)) { return this.#openmct.objects.supportsMutation(identifier)
return openmct.objects.getMutable(identifier); ? this.#openmct.objects.getMutable(identifier)
} else { : this.#openmct.objects.get(identifier);
return openmct.objects.get(identifier);
}
}) })
); );
} }
function navigateToFirstChildOfRoot() { async #navigateToFirstChildOfRoot() {
openmct.objects try {
.get('ROOT') const rootObject = await this.#openmct.objects.get('ROOT');
.then((rootObject) => { const composition = this.#openmct.composition.get(rootObject);
const composition = openmct.composition.get(rootObject); if (!composition) {
if (!composition) { return;
return; }
}
composition const children = await composition.load();
.load() const lastChild = children[children.length - 1];
.then((children) => { if (lastChild) {
let lastChild = children[children.length - 1]; const lastChildId = this.#openmct.objects.makeKeyString(lastChild.identifier);
if (lastChild) { this.#openmct.router.setPath(`#/browse/${lastChildId}`);
let lastChildId = openmct.objects.makeKeyString(lastChild.identifier); }
openmct.router.setPath(`#/browse/${lastChildId}`); } catch (e) {
} console.error(e);
}) }
.catch((e) => console.error(e));
})
.catch((e) => console.error(e));
} }
function clearMutationListeners() { #clearMutationListeners() {
if (openmct.router.path !== undefined) { if (this.#openmct.router.path) {
openmct.router.path.forEach((pathObject) => { this.#openmct.router.path.forEach((pathObject) => {
if (pathObject.isMutable) { 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);
}
} }