Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GEN-2177]: add awaitToast function for improved notification handling in Cypress tests #2165

Merged
merged 5 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion frontend/webapp/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { ApolloWrapper } from '@/lib';
import { ThemeProviderWrapper } from '@/styles';

const LAYOUT_STYLE: React.CSSProperties = {
position: 'fixed',
width: '100vw',
height: '100vh',
margin: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export const SourcesList: React.FC<Props> = ({
const hasFilteredSources = !!filteredSources.length;

return (
<Group data-id={`namespace-${namespace}`} key={`namespace-${namespace}`} $selected={isNamespaceAllSourcesSelected} $isOpen={isNamespaceSelected && hasFilteredSources}>
<Group key={`namespace-${namespace}`} data-id={`namespace-${namespace}`} $selected={isNamespaceAllSourcesSelected} $isOpen={isNamespaceSelected && hasFilteredSources}>
<NamespaceItem $selected={isNamespaceAllSourcesSelected} onClick={() => onSelectNamespace(namespace)}>
<FlexRow>
<Checkbox value={isNamespaceAllSourcesSelected} onChange={(bool) => onSelectAll(bool, namespace)} />
Expand Down Expand Up @@ -141,7 +141,7 @@ export const SourcesList: React.FC<Props> = ({
const isSourceSelected = !!onlySelectedSources.find(({ name }) => name === source.name);

return (
<SourceItem key={`source-${source.name}`} $selected={isSourceSelected} onClick={() => onSelectSource(source)}>
<SourceItem key={`source-${source.name}`} data-id={`source-${source.name}`} $selected={isSourceSelected} onClick={() => onSelectSource(source)}>
<FlexRow>
<Checkbox value={isSourceSelected} onChange={() => onSelectSource(source, namespace)} />
<Text>{source.name}</Text>
Expand Down
24 changes: 22 additions & 2 deletions frontend/webapp/cypress/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const NAMESPACES = {

export const SELECTED_ENTITIES = {
NAMESPACE: NAMESPACES.DEFAULT,
NAMESPACE_SOURCES: ['coupon', 'frontend', 'inventory', 'membership', 'pricing'],
SOURCE: 'frontend',
DESTINATION_TYPE: 'jaeger',
DESTINATION_DISPLAY_NAME: 'Jaeger',
Expand All @@ -37,6 +38,7 @@ export const SELECTED_ENTITIES = {

export const DATA_IDS = {
SELECT_NAMESPACE: `[data-id=namespace-${SELECTED_ENTITIES.NAMESPACE}]`,
SELECT_SOURCE: (sourceName: string) => `[data-id=source-${sourceName}]`,
SELECT_DESTINATION: `[data-id=select-potential-destination-${SELECTED_ENTITIES.DESTINATION_TYPE}]`,
SELECT_DESTINATION_AUTOFILL_FIELD: `[data-id=${SELECTED_ENTITIES.DESTINATION_AUTOFILL_FIELD}]`,

Expand All @@ -60,6 +62,10 @@ export const DATA_IDS = {
APPROVE: '[data-id=approve]',
DENY: '[data-id=deny]',

TOAST: '[data-id=toast]',
TOAST_CLOSE: '[data-id=toast-close]',
TOAST_ACTION: '[data-id=toast-action]',

SOURCE_NODE_HEADER: '[data-id=source-header]',
SOURCE_NODE: '[data-id=source-1]',
DESTINATION_NODE: '[data-id=destination-0]',
Expand Down Expand Up @@ -102,6 +108,20 @@ export const TEXTS = {
ACTION_WARN_MODAL_TITLE: `Delete action (${CYPRESS_TEST})`,
INSTRUMENTATION_RULE_WARN_MODAL_TITLE: `Delete rule (${CYPRESS_TEST})`,

NOTIF_SOURCES_CREATED: 'Successfully created 5 sources',
NOTIF_SOURCES_DELETED: 'Successfully deleted 5 sources',
NOTIF_SOURCES_CREATED: (amount: number) => `Successfully created ${amount} sources`,
NOTIF_SOURCES_UPDATED: (amount: number) => `Successfully updated ${amount} source`,
NOTIF_SOURCES_DELETED: (amount: number) => `Successfully deleted ${amount} sources`,

NOTIF_DESTINATIONS_CREATED: (amount: number) => `Successfully created ${amount} destinations`,
// TODO: this message isn't right, fix in backend
NOTIF_DESTINATIONS_UPDATED: (amount: number) => `Successfully transformed ${amount + 1} destinations to otelcol configuration`,
NOTIF_DESTINATIONS_DELETED: (amount: number) => `Successfully deleted ${amount} destinations`,

NOTIF_ACTION_CREATED: (crdId: string) => `Action "${crdId}" created`,
NOTIF_ACTION_UPDATED: (crdId: string) => `Action "${crdId}" updated`,
NOTIF_ACTION_DELETED: (crdId: string) => `Action "${crdId}" delete`,

NOTIF_INSTRUMENTATION_RULE_CREATED: (crdId: string) => `Rule "${crdId}" created`,
NOTIF_INSTRUMENTATION_RULE_UPDATED: (crdId: string) => `Rule "${crdId}" updated`,
NOTIF_INSTRUMENTATION_RULE_DELETED: (crdId: string) => `Rule "${crdId}" delete`,
};
7 changes: 5 additions & 2 deletions frontend/webapp/cypress/e2e/02-onboarding.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import { BUTTONS, DATA_IDS, ROUTES, SELECTED_ENTITIES } from '../constants';
describe('Onboarding', () => {
beforeEach(() => cy.intercept('/graphql').as('gql'));

it('Should contain a "default" namespace', () => {
it('Should contain a "default" namespace, and it should have 5 sources', () => {
cy.visit(ROUTES.CHOOSE_SOURCES);
cy.wait('@gql').then(() => {
cy.get(DATA_IDS.SELECT_NAMESPACE).contains(SELECTED_ENTITIES.NAMESPACE).should('exist');
cy.get(DATA_IDS.SELECT_NAMESPACE).contains(SELECTED_ENTITIES.NAMESPACE).should('exist').click();
SELECTED_ENTITIES.NAMESPACE_SOURCES.forEach((sourceName) => {
cy.get(DATA_IDS.SELECT_NAMESPACE).get(DATA_IDS.SELECT_SOURCE(sourceName)).contains(sourceName).should('exist');
});
});
});

Expand Down
33 changes: 14 additions & 19 deletions frontend/webapp/cypress/e2e/03-sources.cy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getCrdById, getCrdIds, updateEntity } from '../functions';
import { awaitToast, getCrdById, getCrdIds, updateEntity } from '../functions';
import { BUTTONS, CRD_IDS, CRD_NAMES, DATA_IDS, NAMESPACES, ROUTES, SELECTED_ENTITIES, TEXTS } from '../constants';

// The number of CRDs that exist in the cluster before running any tests should be 0.
Expand All @@ -20,18 +20,15 @@ describe('Sources CRUD', () => {
cy.get(DATA_IDS.MODAL_ADD_SOURCE).should('exist');
cy.get(DATA_IDS.SELECT_NAMESPACE).find(DATA_IDS.CHECKBOX).click();

// Wait for 3 seconds to allow the namespace & it's resources to be loaded into the UI
cy.wait(3000).then(() => {
cy.contains('button', BUTTONS.DONE).click();
SELECTED_ENTITIES.NAMESPACE_SOURCES.forEach((sourceName) => {
cy.get(DATA_IDS.SELECT_NAMESPACE).get(DATA_IDS.SELECT_SOURCE(sourceName)).contains(sourceName).should('exist');
});

cy.wait('@gql').then(() => {
getCrdIds({ namespace, crdName, expectedError: '', expectedLength: 5 }, () => {
// Wait for 5 seconds to allow the backend to batch an SSE notification
cy.wait(5000).then(() => {
cy.get(DATA_IDS.NOTIF_MANAGER_BUTTON).click();
cy.get(DATA_IDS.NOTIF_MANAGER_CONTENR).contains(TEXTS.NOTIF_SOURCES_CREATED).should('exist');
});
});
cy.contains('button', BUTTONS.DONE).click();

cy.wait('@gql').then(() => {
awaitToast({ withSSE: true, message: TEXTS.NOTIF_SOURCES_CREATED(5) }, () => {
getCrdIds({ namespace, crdName, expectedError: '', expectedLength: 5 });
});
});
});
Expand All @@ -53,7 +50,9 @@ describe('Sources CRUD', () => {
getCrdIds({ namespace, crdName, expectedError: '', expectedLength: 5 }, (crdIds) => {
const crdId = CRD_IDS.SOURCE;
expect(crdIds).includes(crdId);
getCrdById({ namespace, crdName, crdId, expectedError: '', expectedKey: 'serviceName', expectedValue: TEXTS.UPDATED_NAME });
awaitToast({ withSSE: false, message: TEXTS.NOTIF_SOURCES_UPDATED(1) }, () => {
getCrdById({ namespace, crdName, crdId, expectedError: '', expectedKey: 'serviceName', expectedValue: TEXTS.UPDATED_NAME });
});
});
});
},
Expand All @@ -72,12 +71,8 @@ describe('Sources CRUD', () => {
cy.get(DATA_IDS.APPROVE).click();

cy.wait('@gql').then(() => {
getCrdIds({ namespace, crdName, expectedError: TEXTS.NO_RESOURCES(namespace), expectedLength: 0 }, () => {
// Wait for 5 seconds to allow the backend to batch an SSE notification
cy.wait(5000).then(() => {
cy.get(DATA_IDS.NOTIF_MANAGER_BUTTON).click();
cy.get(DATA_IDS.NOTIF_MANAGER_CONTENR).contains(TEXTS.NOTIF_SOURCES_DELETED).should('exist');
});
awaitToast({ withSSE: true, message: TEXTS.NOTIF_SOURCES_DELETED(5) }, () => {
getCrdIds({ namespace, crdName, expectedError: TEXTS.NO_RESOURCES(namespace), expectedLength: 0 });
});
});
});
Expand Down
21 changes: 14 additions & 7 deletions frontend/webapp/cypress/e2e/04-destinations.cy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { deleteEntity, getCrdById, getCrdIds, updateEntity } from '../functions';
import { awaitToast, deleteEntity, getCrdById, getCrdIds, updateEntity } from '../functions';
import { BUTTONS, CRD_NAMES, DATA_IDS, NAMESPACES, ROUTES, SELECTED_ENTITIES, TEXTS } from '../constants';

// The number of CRDs that exist in the cluster before running any tests should be 0.
Expand All @@ -11,7 +11,7 @@ const crdName = CRD_NAMES.DESTINATION;
describe('Destinations CRUD', () => {
beforeEach(() => cy.intercept('/graphql').as('gql'));

it('Should create a CRD in the cluster', () => {
it('Should create a CRD in the cluster, and notify with SSE', () => {
cy.visit(ROUTES.OVERVIEW);

getCrdIds({ namespace, crdName, expectedError: TEXTS.NO_RESOURCES(namespace), expectedLength: 0 }, () => {
Expand All @@ -23,12 +23,14 @@ describe('Destinations CRUD', () => {
cy.get('button').contains(BUTTONS.DONE).click();

cy.wait('@gql').then(() => {
getCrdIds({ namespace, crdName, expectedError: '', expectedLength: 1 });
awaitToast({ withSSE: true, message: TEXTS.NOTIF_DESTINATIONS_CREATED(1) }, () => {
getCrdIds({ namespace, crdName, expectedError: '', expectedLength: 1 });
});
});
});
});

it('Should update the CRD in the cluster', () => {
it('Should update the CRD in the cluster, and notify with SSE', () => {
cy.visit(ROUTES.OVERVIEW);

getCrdIds({ namespace, crdName, expectedError: '', expectedLength: 1 }, () => {
Expand All @@ -43,15 +45,18 @@ describe('Destinations CRUD', () => {
cy.wait('@gql').then(() => {
getCrdIds({ namespace, crdName, expectedError: '', expectedLength: 1 }, (crdIds) => {
const crdId = crdIds[0];
getCrdById({ namespace, crdName, crdId, expectedError: '', expectedKey: 'destinationName', expectedValue: TEXTS.UPDATED_NAME });

awaitToast({ withSSE: true, message: TEXTS.NOTIF_DESTINATIONS_UPDATED(1) }, () => {
getCrdById({ namespace, crdName, crdId, expectedError: '', expectedKey: 'destinationName', expectedValue: TEXTS.UPDATED_NAME });
});
});
});
},
);
});
});

it('Should delete the CRD from the cluster', () => {
it('Should delete the CRD from the cluster, and notify with SSE', () => {
cy.visit(ROUTES.OVERVIEW);

getCrdIds({ namespace, crdName, expectedError: '', expectedLength: 1 }, () => {
Expand All @@ -64,7 +69,9 @@ describe('Destinations CRUD', () => {
},
() => {
cy.wait('@gql').then(() => {
getCrdIds({ namespace, crdName, expectedError: TEXTS.NO_RESOURCES(namespace), expectedLength: 0 });
awaitToast({ withSSE: true, message: TEXTS.NOTIF_DESTINATIONS_DELETED(1) }, () => {
getCrdIds({ namespace, crdName, expectedError: TEXTS.NO_RESOURCES(namespace), expectedLength: 0 });
});
});
},
);
Expand Down
18 changes: 13 additions & 5 deletions frontend/webapp/cypress/e2e/05-actions.cy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { deleteEntity, getCrdById, getCrdIds, updateEntity } from '../functions';
import { awaitToast, deleteEntity, getCrdById, getCrdIds, updateEntity } from '../functions';
import { BUTTONS, CRD_NAMES, DATA_IDS, INPUTS, NAMESPACES, ROUTES, SELECTED_ENTITIES, TEXTS } from '../constants';

// The number of CRDs that exist in the cluster before running any tests should be 0.
Expand All @@ -23,7 +23,10 @@ describe('Actions CRUD', () => {
cy.get('button').contains(BUTTONS.DONE).click();

cy.wait('@gql').then(() => {
getCrdIds({ namespace, crdName, expectedError: '', expectedLength: 1 });
getCrdIds({ namespace, crdName, expectedError: '', expectedLength: 1 }, (crdIds) => {
const crdId = crdIds[0];
awaitToast({ withSSE: false, message: TEXTS.NOTIF_ACTION_CREATED(crdId) });
});
});
});
});
Expand All @@ -43,7 +46,9 @@ describe('Actions CRUD', () => {
cy.wait('@gql').then(() => {
getCrdIds({ namespace, crdName, expectedError: '', expectedLength: 1 }, (crdIds) => {
const crdId = crdIds[0];
getCrdById({ namespace, crdName, crdId, expectedError: '', expectedKey: 'actionName', expectedValue: TEXTS.UPDATED_NAME });
awaitToast({ withSSE: false, message: TEXTS.NOTIF_ACTION_UPDATED(crdId) }, () => {
getCrdById({ namespace, crdName, crdId, expectedError: '', expectedKey: 'actionName', expectedValue: TEXTS.UPDATED_NAME });
});
});
});
},
Expand All @@ -54,7 +59,8 @@ describe('Actions CRUD', () => {
it('Should delete the CRD from the cluster', () => {
cy.visit(ROUTES.OVERVIEW);

getCrdIds({ namespace, crdName, expectedError: '', expectedLength: 1 }, () => {
getCrdIds({ namespace, crdName, expectedError: '', expectedLength: 1 }, (crdIds) => {
const crdId = crdIds[0];
deleteEntity(
{
nodeId: DATA_IDS.ACTION_NODE,
Expand All @@ -63,7 +69,9 @@ describe('Actions CRUD', () => {
},
() => {
cy.wait('@gql').then(() => {
getCrdIds({ namespace, crdName, expectedError: TEXTS.NO_RESOURCES(namespace), expectedLength: 0 });
awaitToast({ withSSE: false, message: TEXTS.NOTIF_ACTION_DELETED(crdId) }, () => {
getCrdIds({ namespace, crdName, expectedError: TEXTS.NO_RESOURCES(namespace), expectedLength: 0 });
});
});
},
);
Expand Down
18 changes: 13 additions & 5 deletions frontend/webapp/cypress/e2e/06-rules.cy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { deleteEntity, getCrdById, getCrdIds, updateEntity } from '../functions';
import { awaitToast, deleteEntity, getCrdById, getCrdIds, updateEntity } from '../functions';
import { BUTTONS, CRD_NAMES, DATA_IDS, NAMESPACES, ROUTES, SELECTED_ENTITIES, TEXTS } from '../constants';

// The number of CRDs that exist in the cluster before running any tests should be 0.
Expand All @@ -21,7 +21,10 @@ describe('Instrumentation Rules CRUD', () => {
cy.get('button').contains(BUTTONS.DONE).click();

cy.wait('@gql').then(() => {
getCrdIds({ namespace, crdName, expectedError: '', expectedLength: 1 });
getCrdIds({ namespace, crdName, expectedError: '', expectedLength: 1 }, (crdIds) => {
const crdId = crdIds[0];
awaitToast({ withSSE: false, message: TEXTS.NOTIF_INSTRUMENTATION_RULE_CREATED(crdId) });
});
});
});
});
Expand All @@ -41,7 +44,9 @@ describe('Instrumentation Rules CRUD', () => {
cy.wait('@gql').then(() => {
getCrdIds({ namespace, crdName, expectedError: '', expectedLength: 1 }, (crdIds) => {
const crdId = crdIds[0];
getCrdById({ namespace, crdName, crdId, expectedError: '', expectedKey: 'ruleName', expectedValue: TEXTS.UPDATED_NAME });
awaitToast({ withSSE: false, message: TEXTS.NOTIF_INSTRUMENTATION_RULE_UPDATED(crdId) }, () => {
getCrdById({ namespace, crdName, crdId, expectedError: '', expectedKey: 'ruleName', expectedValue: TEXTS.UPDATED_NAME });
});
});
});
},
Expand All @@ -52,7 +57,8 @@ describe('Instrumentation Rules CRUD', () => {
it('Should delete the CRD from the cluster', () => {
cy.visit(ROUTES.OVERVIEW);

getCrdIds({ namespace, crdName, expectedError: '', expectedLength: 1 }, () => {
getCrdIds({ namespace, crdName, expectedError: '', expectedLength: 1 }, (crdIds) => {
const crdId = crdIds[0];
deleteEntity(
{
nodeId: DATA_IDS.INSTRUMENTATION_RULE_NODE,
Expand All @@ -61,7 +67,9 @@ describe('Instrumentation Rules CRUD', () => {
},
() => {
cy.wait('@gql').then(() => {
getCrdIds({ namespace, crdName, expectedError: TEXTS.NO_RESOURCES(namespace), expectedLength: 0 });
awaitToast({ withSSE: false, message: TEXTS.NOTIF_INSTRUMENTATION_RULE_DELETED(crdId) }, () => {
getCrdIds({ namespace, crdName, expectedError: TEXTS.NO_RESOURCES(namespace), expectedLength: 0 });
});
});
},
);
Expand Down
19 changes: 19 additions & 0 deletions frontend/webapp/cypress/functions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,22 @@ export const deleteEntity = ({ nodeId, nodeContains, warnModalTitle, warnModalNo

if (!!callback) callback();
};

interface AwaitToastOptions {
withSSE: boolean;
message: string;
}

export const awaitToast = ({ withSSE, message }: AwaitToastOptions, callback?: () => void) => {
// In case of SSE, we need around 5 seconds to allow the backend to batch a notification.
// We will force 2 seconds, and Cypress will add 4 more seconds, giving us 6 seconds total.
// We don't want to force too much time or we might miss a notification that was sent earlier than expected!

cy.wait(withSSE ? 2000 : 0).then(() => {
cy.get(DATA_IDS.TOAST).contains(message).as('toast-msg');
cy.get('@toast-msg').should('exist');
cy.get('@toast-msg').parent().parent().find(DATA_IDS.TOAST_CLOSE).click();

if (!!callback) callback();
});
};
10 changes: 7 additions & 3 deletions frontend/webapp/reuseable-components/notification-note/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export const NotificationNote: React.FC<Props> = ({ type, title, message, action

return (
<Container className={onClose ? 'animated' : ''} $isLeaving={isLeaving} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<Content $type={type} style={style}>
<Content data-id='toast' $type={type} style={style}>
<StatusIcon />

<TextWrapper $withAction={!!action}>
Expand All @@ -157,9 +157,13 @@ export const NotificationNote: React.FC<Props> = ({ type, title, message, action

{(!!action || !!onClose) && (
<ButtonsWrapper>
{action && <ActionButton onClick={action.onClick}>{action.label}</ActionButton>}
{action && (
<ActionButton data-id='toast-action' onClick={action.onClick}>
{action.label}
</ActionButton>
)}
{onClose && (
<IconButton onClick={() => closeToast({ asSeen: true })}>
<IconButton data-id='toast-close' onClick={() => closeToast({ asSeen: true })}>
<XIcon size={12} />
</IconButton>
)}
Expand Down
Loading