Skip to content

Commit

Permalink
[ENH] Implement Notification Icon with Badge for Warnings and Its Test (
Browse files Browse the repository at this point in the history
#432)

* Implement notification icon with badge for warnings and its test

* Improved notification functionality and added e2e & component tests

* revert changes

* add tests for deleting and clearing all functionality

---------

Co-authored-by: Arman Jahanpour <[email protected]>
  • Loading branch information
Tusharjamdade and rmanaem authored Jan 23, 2025
1 parent 13a76dc commit ffe0612
Show file tree
Hide file tree
Showing 5 changed files with 327 additions and 66 deletions.
71 changes: 65 additions & 6 deletions cypress/component/Navbar.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
import { useState } from 'react';
import Navbar from '../../src/components/Navbar';

const props = {
isLoggedIn: true,
onLogin: () => {},
};
import { Notification } from '../../src/utils/types';

describe('Navbar', () => {
const mockNotifications: Notification[] = [
{ id: '1', type: 'info', message: 'This is an info notification.' },
{ id: '2', type: 'warning', message: 'This is a warning notification.' },
];

function NavbarWrapper() {
const [notifications, setNotifications] = useState(mockNotifications);

return (
<Navbar
isLoggedIn
onLogin={() => {}}
notifications={notifications}
setNotifications={setNotifications}
/>
);
}

it('Displays a MUI Toolbar with logo, title, subtitle, documentation link, and GitHub link', () => {
cy.mount(<Navbar isLoggedIn={props.isLoggedIn} onLogin={props.onLogin} />);
cy.mount(<NavbarWrapper />);
cy.get("[data-cy='navbar']").should('be.visible');
cy.get("[data-cy='navbar'] img").should('exist');
cy.get("[data-cy='navbar'] h5").should('contain', 'Neurobagel Query');
Expand All @@ -25,4 +40,48 @@ describe('Navbar', () => {
.eq(1)
.should('have.attr', 'href', 'https://github.com/neurobagel/query-tool/');
});
// Verify that the visibility and texts match for each notification.
it('Verifies the visibility and functionality of the Navbar notifications feature', () => {
cy.mount(<NavbarWrapper />);
cy.get("[data-cy='notification-button']").should('exist');
cy.get("[data-cy='navbar']")
.find('.MuiBadge-badge')
.should('contain', mockNotifications.length);
cy.get("[data-cy='notification-button']").click();
cy.get('.MuiPopover-paper').should('be.visible');

cy.get('.MuiList-root').within(() => {
cy.get('.MuiListItem-root').should('have.length', mockNotifications.length);
mockNotifications.forEach((notification, index) => {
cy.get('.MuiListItem-root')
.eq(index)
.within(() => {
cy.get('.MuiListItemText-primary').should('contain', notification.type.toUpperCase());
cy.get('.MuiListItemText-secondary').should('contain', notification.message);
});
});
});

cy.get('.MuiPopover-paper').trigger('keydown', { key: 'Escape' });
cy.get('.MuiPopover-paper').should('not.exist');
});
// Verify delete functionality
it('Deletes a notification', () => {
cy.mount(<NavbarWrapper />);
cy.get("[data-cy='notification-button']").click();
cy.get('.MuiPopover-paper').should('be.visible');
cy.get("[data-cy='delete-notification']").first().click();
cy.get("[data-cy='notification-item']").should('have.length', mockNotifications.length - 1);
});
// Verify clear all functionality
it('Clears all notifications', () => {
cy.mount(<NavbarWrapper />);
cy.get("[data-cy='notification-button']").click();
cy.get('.MuiPopover-paper').should('be.visible');
cy.get("[data-cy='clear-all-notifications']").click();
cy.get("[data-cy='notification-item']").should('have.length', 0);
cy.contains('No notifications').should('be.visible');
cy.get('.MuiPopover-paper').trigger('keydown', { key: 'Escape' });
cy.get('.MuiPopover-paper').should('not.exist');
});
});
75 changes: 47 additions & 28 deletions cypress/e2e/APIRequests.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,14 @@ describe('Successful API attribute responses', () => {
).as('getDiagnosisOptions');
cy.visit('/');
cy.wait('@getDiagnosisOptions');
cy.get('.notistack-SnackbarContainer')
.find('.notistack-MuiContent-info')
.should('contain', 'No diagnoses options were available');
cy.get("[data-cy='notification-button']").should('exist');
cy.get("[data-cy='notification-button']").click({ force: true });
cy.get('.MuiListItem-root')
.eq(0)
.within(() => {
cy.get('.MuiListItemText-primary').should('contain', 'INFO');
cy.get('p').should('contain.text', 'No diagnoses options were available');
});
});
it('Empty assessment response makes info toast appear', () => {
cy.intercept(
Expand All @@ -100,10 +105,14 @@ describe('Successful API attribute responses', () => {
).as('getAssessmentToolOptions');
cy.visit('/');
cy.wait('@getAssessmentToolOptions');

cy.get('.notistack-SnackbarContainer')
.find('.notistack-MuiContent-info')
.should('contain', 'No assessments options were available');
cy.get("[data-cy='notification-button']").should('exist');
cy.get("[data-cy='notification-button']").click({ force: true });
cy.get('.MuiListItem-root')
.eq(0)
.within(() => {
cy.get('.MuiListItemText-primary').should('contain', 'INFO');
cy.get('p').should('contain.text', 'No assessments options were available');
});
});
it('Empty pipeline response makes info toast appear', () => {
cy.intercept(
Expand All @@ -115,9 +124,14 @@ describe('Successful API attribute responses', () => {
).as('getPipelineOptions');
cy.visit('/');
cy.wait('@getPipelineOptions');
cy.get('.notistack-SnackbarContainer')
.find('.notistack-MuiContent-info')
.should('contain', 'No pipelines options were available');
cy.get("[data-cy='notification-button']").should('exist');
cy.get("[data-cy='notification-button']").click({ force: true });
cy.get('.MuiListItem-root')
.eq(0)
.within(() => {
cy.get('.MuiListItemText-primary').should('contain', 'INFO');
cy.get('p').should('contain.text', 'No pipelines options were available');
});
});
it('Empty pipeline version response makes info toast appear', () => {
cy.intercept(
Expand All @@ -131,9 +145,14 @@ describe('Successful API attribute responses', () => {
cy.get('[data-cy="close-auth-dialog-button"]').click();
cy.get('[data-cy="Pipeline name-categorical-field"]').type('fmri{downarrow}{enter}');
cy.wait('@getPipelineVersionsOptions');
cy.get('.notistack-SnackbarContainer')
.find('.notistack-MuiContent-info')
.should('contain', 'No fmriprep versions were available');
cy.get("[data-cy='notification-button']").should('exist');
cy.get("[data-cy='notification-button']").click({ force: true });
cy.get('.MuiListItem-root')
.eq(0)
.within(() => {
cy.get('.MuiListItemText-primary').should('contain', 'INFO');
cy.get('p').should('contain.text', 'No fmriprep versions were available');
});
});
});

Expand Down Expand Up @@ -183,27 +202,27 @@ describe('Partially successful API attribute responses', () => {
]);
});
it('Shows warning for node that failed Assessment tool option request', () => {
cy.get('.notistack-SnackbarContainer')
.find('.notistack-MuiContent-warning')
.should('contain', 'NoAssessmentNode');
cy.get("[data-cy='notification-button']").should('exist');
cy.get("[data-cy='notification-button']").click({ force: true });
cy.get('.MuiList-root').should('contain', 'NoAssessmentNode');
});
it('Shows warning for node that failed Diagnosis option request', () => {
cy.get('.notistack-SnackbarContainer')
.find('.notistack-MuiContent-warning')
.should('contain', 'NoDiagnosisNode');
cy.get("[data-cy='notification-button']").should('exist');
cy.get("[data-cy='notification-button']").click({ force: true });
cy.get('.MuiList-root').should('contain', 'NoDiagnosisNode');
});
it('Shows warning for node that failed Pipeline option request', () => {
cy.get('.notistack-SnackbarContainer')
.find('.notistack-MuiContent-warning')
.should('contain', 'NoPipelineNode');
cy.get("[data-cy='notification-button']").should('exist');
cy.get("[data-cy='notification-button']").click({ force: true });
cy.get('.MuiList-root').should('contain', 'NoPipelineNode');
});
it('Shows warning for node that failed Pipeline version option request', () => {
cy.get('[data-cy="close-auth-dialog-button"]').click();
cy.get('[data-cy="Pipeline name-categorical-field"]').type('fmri{downarrow}{enter}');
cy.wait('@getPipelineVersionsOptions');
cy.get('.notistack-SnackbarContainer')
.find('.notistack-MuiContent-warning')
.should('contain', 'NoPipelineVersionNode');
cy.get("[data-cy='notification-button']").should('exist');
cy.get("[data-cy='notification-button']").click({ force: true });
cy.get('.MuiList-root').should('contain', 'NoPipelineVersionNode');
});
});

Expand Down Expand Up @@ -505,9 +524,9 @@ describe('Partially successful API query requests', () => {
it('Shows a warning for nodes that failed to return any results', () => {
cy.get('[data-cy="submit-query-button"]').click();
cy.wait('@call');
cy.get('.notistack-SnackbarContainer')
.find('.notistack-MuiContent-warning')
.should('contain', 'DidNotWorkNode');
cy.get("[data-cy='notification-button']").should('exist');
cy.get("[data-cy='notification-button']").click({ force: true });
cy.get('.MuiList-root').should('contain', 'DidNotWorkNode');
});
});

Expand Down
93 changes: 65 additions & 28 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import useMediaQuery from '@mui/material/useMediaQuery';
import CloseIcon from '@mui/icons-material/Close';
import { SnackbarKey, SnackbarProvider, closeSnackbar, enqueueSnackbar } from 'notistack';
import { useAuth0 } from '@auth0/auth0-react';
import { v4 as uuidv4 } from 'uuid';
import { queryURL, baseAPIURL, nodesURL, enableAuth, enableChatbot } from './utils/constants';
import {
RetrievedAttributeOption,
Expand All @@ -17,6 +18,7 @@ import {
Pipelines,
NodeError,
QueryResponse,
Notification,
} from './utils/types';
import QueryForm from './components/QueryForm';
import ResultContainer from './components/ResultContainer';
Expand Down Expand Up @@ -62,6 +64,8 @@ function App() {
const [IDToken, setIDToken] = useState<string | undefined>('');
const { isAuthenticated, isLoading, getIdTokenClaims } = useAuth0();

const [notifications, setNotifications] = useState<Notification[]>([]);

// Extract the raw OIDC ID token from the Auth0 SDK
useEffect(() => {
if (enableAuth && !isLoading) {
Expand Down Expand Up @@ -116,26 +120,38 @@ function App() {
} else {
// If any errors occurred, report them
response.data.errors.forEach((error) => {
enqueueSnackbar(`Failed to retrieve ${NBResource} options from ${error.node_name}`, {
variant: 'warning',
action,
});
setNotifications((prev) => [
...prev,
{
id: uuidv4(),
type: 'warning',
message: `Failed to retrieve ${NBResource} options from ${error.node_name}`,
},
]);
});
// If the results are empty, report that
if (Object.keys(response.data.responses[dataElementURI]).length === 0) {
enqueueSnackbar(`No ${NBResource} options were available`, {
variant: 'info',
action,
});
setNotifications((prev) => [
...prev,
{
id: uuidv4(),
type: 'info',
message: `No ${NBResource} options were available`,
},
]);
// TODO: remove the second condition once pipeline labels are added
} else if (
response.data.responses[dataElementURI].some((item) => item.Label === null) &&
NBResource !== 'pipelines'
) {
enqueueSnackbar(`Warning: Missing labels were removed for ${NBResource} `, {
variant: 'warning',
action,
});
setNotifications((prev) => [
...prev,
{
id: uuidv4(),
type: 'warning',
message: `Warning: Missing labels were removed for ${NBResource}`,
},
]);
response.data.responses[dataElementURI] = response.data.responses[
dataElementURI
].filter((item) => item.Label !== null);
Expand Down Expand Up @@ -180,7 +196,14 @@ function App() {
if (nodeResponse === null) {
enqueueSnackbar('Failed to retrieve Node options', { variant: 'error', action });
} else if (nodeResponse.length === 0) {
enqueueSnackbar('No options found for Node', { variant: 'info', action });
setNotifications((prev) => [
...prev,
{
id: uuidv4(),
type: 'info',
message: 'No options found for Node',
},
]);
} else {
setAvailableNodes([...nodeResponse, { NodeName: 'All', ApiURL: 'allNodes' }]);
}
Expand All @@ -201,20 +224,25 @@ function App() {
} else {
// If any errors occurred, report them
response.data.errors.forEach((error) => {
enqueueSnackbar(
`Failed to retrieve ${pipelineURI.label} versions from ${error.node_name}`,
setNotifications((prev) => [
...prev,
{
variant: 'warning',
action,
}
);
id: uuidv4(),
type: 'warning',
message: `Failed to retrieve ${pipelineURI.label} versions from ${error.node_name}`,
},
]);
});
// If the results are empty, report that
if (Object.keys(response.data.responses[pipelineURI.id]).length === 0) {
enqueueSnackbar(`No ${pipelineURI.label} versions were available`, {
variant: 'info',
action,
});
setNotifications((prev) => [
...prev,
{
id: uuidv4(),
type: 'info',
message: `No ${pipelineURI.label} versions were available`,
},
]);
}
}
return response.data.responses[pipelineURI.id];
Expand Down Expand Up @@ -409,10 +437,14 @@ function App() {
switch (response.data.nodes_response_status) {
case 'partial success': {
response.data.errors.forEach((error: NodeError) => {
enqueueSnackbar(`${error.node_name} failed to respond`, {
variant: 'warning',
action,
});
setNotifications((prev) => [
...prev,
{
id: uuidv4(),
type: 'warning',
message: `${error.node_name} failed to respond`,
},
]);
});
break;
}
Expand All @@ -439,7 +471,12 @@ function App() {
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
maxSnack={7}
/>
<Navbar isLoggedIn={isAuthenticated} onLogin={() => setOpenAuthDialog(true)} />
<Navbar
isLoggedIn={isAuthenticated}
onLogin={() => setOpenAuthDialog(true)}
notifications={notifications}
setNotifications={setNotifications}
/>
{showAlert() && (
<>
<Grow in={!alertDismissed}>
Expand Down
Loading

0 comments on commit ffe0612

Please sign in to comment.