From d6d80113c903e9ad5092866e20b5e99e8ce90f34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= Date: Tue, 27 Feb 2024 13:13:24 +0100 Subject: [PATCH] chore(web): improvements for the onboarding cypress e2e tests --- apps/web/cypress/plugins/index.ts | 92 ++++++++++- .../cypress/tests/get-started-page.spec.ts | 152 +++++++++++------- .../web/cypress/tests/templates-store.spec.ts | 1 - .../components/OpenWorkflowButton.tsx | 4 +- .../get-started/consts/DelayUseCase.const.tsx | 24 ++- .../consts/DigestUseCase.const.tsx | 23 ++- .../get-started/consts/InAppUseCase.const.tsx | 17 +- .../consts/MultiChannelUseCase.const.tsx | 17 +- .../consts/TranslationUseCase.const.tsx | 12 +- .../src/pages/get-started/consts/shared.tsx | 13 ++ .../components/layout/GetStartedLayout.tsx | 4 +- .../shared/src/consts/template-store/index.ts | 23 ++- .../src/notification-template.service.ts | 7 +- 13 files changed, 291 insertions(+), 98 deletions(-) diff --git a/apps/web/cypress/plugins/index.ts b/apps/web/cypress/plugins/index.ts index 16cbbed4ac4e..3859475d8f4d 100644 --- a/apps/web/cypress/plugins/index.ts +++ b/apps/web/cypress/plugins/index.ts @@ -12,7 +12,14 @@ import { EnvironmentService, } from '@novu/testing'; import { JobsService } from '@novu/testing'; -import { ChannelTypeEnum, getPopularTemplateIds, ProvidersIdEnum } from '@novu/shared'; +import { + ChannelTypeEnum, + getPopularTemplateIds, + getGetStartedTemplateIds, + ProvidersIdEnum, + TriggerTypeEnum, + StepTypeEnum, +} from '@novu/shared'; const jobsService = new JobsService(); @@ -198,6 +205,11 @@ module.exports = (on, config) => { _environmentId: productionEnvironmentId, _organizationId: organizationId, }); + const productionGetStartedlGroup = await notificationGroupRepository.findOne({ + name: 'Get started', + _environmentId: productionEnvironmentId, + _organizationId: organizationId, + }); if (!productionGeneralGroup) { await notificationGroupRepository.create({ @@ -206,6 +218,13 @@ module.exports = (on, config) => { _organizationId: organizationId, }); } + if (!productionGetStartedlGroup) { + await notificationGroupRepository.create({ + name: 'Get started', + _environmentId: productionEnvironmentId, + _organizationId: organizationId, + }); + } const productionNotificationTemplateService = new NotificationTemplateService( user._id, @@ -214,6 +233,7 @@ module.exports = (on, config) => { ); const popularTemplateIds = getPopularTemplateIds({ production: false }); + const getStartedTemplateIds = getGetStartedTemplateIds({ production: false }); const blueprintTemplates = await productionNotificationTemplateService.getBlueprintTemplates( organizationId, @@ -236,6 +256,76 @@ module.exports = (on, config) => { name: ':fa-solid fa-lock: Password reset', isBlueprint: true, }), + productionNotificationTemplateService.createTemplate({ + _id: getStartedTemplateIds[0], + noFeedId: true, + noLayoutId: true, + name: ':fa-solid fa-clock: Delay', + isBlueprint: true, + steps: [ + { + type: StepTypeEnum.DELAY, + name: 'Delay', + content: '', + }, + ], + triggers: [ + { + identifier: 'get-started-delay', + type: TriggerTypeEnum.EVENT, + variables: [], + }, + ], + }), + productionNotificationTemplateService.createTemplate({ + _id: getStartedTemplateIds[1], + noFeedId: true, + noLayoutId: true, + name: ':fa-solid fa-layer-group: Digest', + isBlueprint: true, + steps: [ + { + type: StepTypeEnum.DIGEST, + name: 'Digest', + content: '', + }, + ], + triggers: [ + { + identifier: 'get-started-digest', + type: TriggerTypeEnum.EVENT, + variables: [], + }, + ], + }), + productionNotificationTemplateService.createTemplate({ + _id: getStartedTemplateIds[2], + noFeedId: true, + noLayoutId: true, + name: ':fa-solid fa-bell: In-App', + isBlueprint: true, + triggers: [ + { + identifier: 'get-started-in-app', + type: TriggerTypeEnum.EVENT, + variables: [], + }, + ], + }), + productionNotificationTemplateService.createTemplate({ + _id: getStartedTemplateIds[3], + noFeedId: true, + noLayoutId: true, + name: ':fa-solid fa-earth-americas: Multi-channel', + isBlueprint: true, + triggers: [ + { + identifier: 'get-started-multi-channel', + type: TriggerTypeEnum.EVENT, + variables: [], + }, + ], + }), ]); } diff --git a/apps/web/cypress/tests/get-started-page.spec.ts b/apps/web/cypress/tests/get-started-page.spec.ts index 4e225e8b3ad8..a4effd4d90dd 100644 --- a/apps/web/cypress/tests/get-started-page.spec.ts +++ b/apps/web/cypress/tests/get-started-page.spec.ts @@ -2,6 +2,7 @@ interface ITabLinkInfo { label: string; type: 'button' | 'a'; urlRegex: RegExp; + windowOpenCallIndex?: number; } interface ITabTest { @@ -15,8 +16,11 @@ const BASE_ROUTE = '/get-started'; const visitTabAndVerifyContent = ({ tabName, tabTitle, numTimelineSteps, linkSteps }: ITabTest) => { cy.visit(BASE_ROUTE); + cy.window().then((win) => { + cy.stub(win, 'open').as('windowOpen'); + }); - cy.contains(tabName).click(); + cy.contains(tabName).as('tab').click(); // Check that the clicked tab is now selected cy.get('button[role="tab"][aria-selected="true"]').contains(tabName).should('exist'); @@ -38,23 +42,38 @@ const visitTabAndVerifyContent = ({ tabName, tabTitle, numTimelineSteps, linkSte }); // validate various links, open them, and go back to the tab - linkSteps.forEach(({ urlRegex, label, type }) => { - cy.contains(type, label).should('be.visible').click(); - - cy.url().should('match', urlRegex); - - cy.go('back'); + linkSteps.forEach(({ urlRegex, label, type, windowOpenCallIndex }) => { + cy.contains(type, label).should('be.visible'); + if (type === 'a') { + cy.contains(type, label).should('have.attr', 'target', '_blank'); + cy.contains(type, label).should('have.attr', 'rel', 'noopener noreferrer'); + cy.contains(type, label).should('have.attr', 'href').should('match', urlRegex); + } else { + const index = windowOpenCallIndex ?? 0; + cy.contains(type, label).click(); + cy.get('@windowOpen') + .its('callCount') + .should('equal', index + 1); - // FIXME: required until feature flag check is removed since we always end up back on `in-app` tab - cy.contains(tabName).click(); + cy.get('@windowOpen') + .its('args') + .then((args) => { + const newTabUrl = args[index][0]; + const target = args[index][1]; + const rel = args[index][2]; + expect(newTabUrl).to.match(urlRegex); + expect(target).to.equal('_blank'); + expect(rel).to.equal('noreferrer noopener'); + }); + } }); }; describe('GetStartedPage', () => { beforeEach(function () { cy.mockFeatureFlags({ IS_IMPROVED_ONBOARDING_ENABLED: true }); - cy.initializeSession().as('session'); + cy.makeBlueprints(); }); it('should have all tabs and default to in-app', () => { @@ -71,8 +90,7 @@ describe('GetStartedPage', () => { cy.get('button[role="tab"][aria-selected="true"]').contains('In-app').should('exist'); }); - // FIXME: this will fail until we remove the conditional load based on feature-flag - it.skip('should load the page with a specific tab selected when the appropriate URL search param is passed', () => { + it('should load the page with a specific tab selected when the appropriate URL search param is passed', () => { cy.visit(`${BASE_ROUTE}?tab=multi-channel`); // Check that In-app is defaulted and selected @@ -90,16 +108,18 @@ describe('GetStartedPage', () => { type: 'a', urlRegex: new RegExp('/integrations/[A-Z0-9]+', 'i'), }, - /* // { - // label: 'Customize', - // type: 'button', - // urlRegex: new RegExp('/workflows'), - // }, - // { - // label: 'Test the trigger', - // type: 'button', - // urlRegex: new RegExp('/workflows'), - // }, */ + { + label: 'Customize', + type: 'button', + urlRegex: /\/workflows\/edit\/\w{1,}/, + windowOpenCallIndex: 0, + }, + { + label: 'Test the trigger', + type: 'button', + urlRegex: /\/workflows\/edit\/\w{1,}\/test-workflow/, + windowOpenCallIndex: 1, + }, { label: 'activity feed', type: 'a', @@ -120,16 +140,18 @@ describe('GetStartedPage', () => { type: 'a', urlRegex: new RegExp('/integrations/create'), }, - /* // { - // label: 'Customize', - // type: 'button', - // urlRegex: new RegExp('/workflows'), - // }, - // { - // label: 'Test the trigger', - // type: 'button', - // urlRegex: new RegExp('/workflows'), - // }, */ + { + label: 'Customize', + type: 'button', + urlRegex: /\/workflows\/edit\/\w{1,}/, + windowOpenCallIndex: 0, + }, + { + label: 'Test the trigger', + type: 'button', + urlRegex: /\/workflows\/edit\/\w{1,}\/test-workflow/, + windowOpenCallIndex: 1, + }, { label: 'activity feed', type: 'a', @@ -150,21 +172,24 @@ describe('GetStartedPage', () => { type: 'a', urlRegex: new RegExp('/integrations/create'), }, - /* // { - // label: 'Customize', - // type: 'button', - // urlRegex: new RegExp('/workflows'), - // }, - // { - // label: 'Customize digest node', - // type: 'button', - // urlRegex: new RegExp('/workflows'), - // }, - // { - // label: 'Test the trigger', - // type: 'button', - // urlRegex: new RegExp('/workflows'), - // }, */ + { + label: 'Customize', + type: 'button', + urlRegex: /\/workflows\/edit\/\w{1,}/, + windowOpenCallIndex: 0, + }, + { + label: 'Customize digest node', + type: 'button', + urlRegex: /\/workflows\/edit\/\w{1,}\/digest\/\w{1,}/, + windowOpenCallIndex: 1, + }, + { + label: 'Test the trigger', + type: 'button', + urlRegex: /\/workflows\/edit\/\w{1,}\/test-workflow/, + windowOpenCallIndex: 2, + }, { label: 'activity feed', type: 'a', @@ -185,21 +210,24 @@ describe('GetStartedPage', () => { type: 'a', urlRegex: new RegExp('/integrations/create'), }, - /* // { - // label: 'Customize', - // type: 'button', - // urlRegex: new RegExp('/workflows'), - // }, - // { - // label: 'Customize delay', - // type: 'button', - // urlRegex: new RegExp('/workflows'), - // }, - // { - // label: 'Test the trigger', - // type: 'button', - // urlRegex: new RegExp('/workflows'), - // }, */ + { + label: 'Customize', + type: 'button', + urlRegex: /\/workflows\/edit\/\w{1,}/, + windowOpenCallIndex: 0, + }, + { + label: 'Customize delay', + type: 'button', + urlRegex: /\/workflows\/edit\/\w{1,}\/delay\/\w{1,}/, + windowOpenCallIndex: 1, + }, + { + label: 'Test the trigger', + type: 'button', + urlRegex: /\/workflows\/edit\/\w{1,}\/test-workflow/, + windowOpenCallIndex: 2, + }, { label: 'activity feed', type: 'a', diff --git a/apps/web/cypress/tests/templates-store.spec.ts b/apps/web/cypress/tests/templates-store.spec.ts index 1d4de1a483dd..5c03d09100b6 100644 --- a/apps/web/cypress/tests/templates-store.spec.ts +++ b/apps/web/cypress/tests/templates-store.spec.ts @@ -234,7 +234,6 @@ describe('Templates Store', function () { cy.getByTestId('all-workflow-tile').should('exist').should('not.be.disabled').click(); cy.getByTestId('templates-store-modal').should('be.visible'); cy.getByTestId('templates-store-modal-use-template').should('be.enabled').click(); - cy.getByTestId('templates-store-modal-use-template').should('be.disabled'); cy.wait('@createTemplate'); diff --git a/apps/web/src/pages/get-started/components/OpenWorkflowButton.tsx b/apps/web/src/pages/get-started/components/OpenWorkflowButton.tsx index 069229958cb6..978218c5321c 100644 --- a/apps/web/src/pages/get-started/components/OpenWorkflowButton.tsx +++ b/apps/web/src/pages/get-started/components/OpenWorkflowButton.tsx @@ -3,7 +3,7 @@ import { errorMessage } from '@novu/design-system'; import { TemplateCreationSourceEnum } from '../../templates/shared'; import { useSegment } from '../../../components/providers/SegmentProvider'; import { OnboardingWorkflowRouteEnum } from '../consts/types'; -import { StyledLink } from '../consts/shared'; +import { LinkButton } from '../consts/shared'; import { useCreateWorkflowFromBlueprint } from '../../../hooks'; import { openInNewTab } from '../../../utils'; import { buildWorkflowEditorUrl } from '../utils/workflowEditorUrl'; @@ -35,5 +35,5 @@ export function OpenWorkflowButton({ createWorkflowFromBlueprint({ blueprintIdentifier }); }; - return {children}; + return {children}; } diff --git a/apps/web/src/pages/get-started/consts/DelayUseCase.const.tsx b/apps/web/src/pages/get-started/consts/DelayUseCase.const.tsx index 6740e2b5325f..4bff312c3562 100644 --- a/apps/web/src/pages/get-started/consts/DelayUseCase.const.tsx +++ b/apps/web/src/pages/get-started/consts/DelayUseCase.const.tsx @@ -22,7 +22,12 @@ export const DelayUseCaseConst: OnboardingUseCase = { Novu has set up trial email and SMS providers for you. To expand your options, add more providers in the - + . ); @@ -33,9 +38,9 @@ export const DelayUseCaseConst: OnboardingUseCase = { Description: function () { return ( - Novu pre-built workflow with a delay node. - {' Customize '} - the workflow or create a new one on the Workflows page. + Novu pre-built workflow with a delay node. + Customize + the workflow or create a new one on the Workflows page. ); }, @@ -45,12 +50,12 @@ export const DelayUseCaseConst: OnboardingUseCase = { Description: function () { return ( - Novu has predefined a time interval of 5 minutes. + Novu has predefined a time interval of 5 minutes. - {' Customize delay'} + Customize delay . @@ -81,7 +86,12 @@ export const DelayUseCaseConst: OnboardingUseCase = { return ( Discover - + to monitor notifications activity and see potential issues with a specific provider or channel. diff --git a/apps/web/src/pages/get-started/consts/DigestUseCase.const.tsx b/apps/web/src/pages/get-started/consts/DigestUseCase.const.tsx index 426101f2b91a..c2aed0be20d5 100644 --- a/apps/web/src/pages/get-started/consts/DigestUseCase.const.tsx +++ b/apps/web/src/pages/get-started/consts/DigestUseCase.const.tsx @@ -25,7 +25,12 @@ export const DigestUseCaseConst: OnboardingUseCase = { Novu has set up trial email and SMS providers for you. To expand your options, add more providers in the - + . ); @@ -37,15 +42,15 @@ export const DigestUseCaseConst: OnboardingUseCase = { Description: function () { return ( - Novu pre-built workflow with a digest node. - {' Customize '} + Novu pre-built workflow with a digest node. + Customize the workflow or create a new one on the Workflows page. ); }, }, { - title: 'Set-up a delay preferences', + title: 'Set-up a digest preferences', Description: function () { return ( @@ -70,9 +75,10 @@ export const DigestUseCaseConst: OnboardingUseCase = { blueprintIdentifier={USECASE_BLUEPRINT_IDENTIFIER} node={OnboardingWorkflowRouteEnum.TEST_WORKFLOW} > - Test the trigger{' '} + Test the trigger + {' '} as if you sent it from your API. Add a subscriber by sending data to the trigger method. Click multiple times to see how the digest node butch messages. @@ -86,7 +92,12 @@ export const DigestUseCaseConst: OnboardingUseCase = { return ( Discover - + to monitor notifications activity and see potential issues with a specific provider or channel. diff --git a/apps/web/src/pages/get-started/consts/InAppUseCase.const.tsx b/apps/web/src/pages/get-started/consts/InAppUseCase.const.tsx index f4c3497d7875..dcb0fcba3d76 100644 --- a/apps/web/src/pages/get-started/consts/InAppUseCase.const.tsx +++ b/apps/web/src/pages/get-started/consts/InAppUseCase.const.tsx @@ -34,7 +34,9 @@ export const InAppUseCaseConst: OnboardingUseCase = { return ( - Create In-app provider + + Create In-app provider + {' instance, and select a framework to set up credentials in the Novu’s Integration store.'} @@ -47,9 +49,9 @@ export const InAppUseCaseConst: OnboardingUseCase = { Description: function () { return ( - Novu pre-built a workflow for testing. - {' Customize '} - it or create a new one on the Workflows page. + Novu pre-built a workflow for testing. + Customize + it or create a new one on the Workflows page. ); }, @@ -78,7 +80,12 @@ export const InAppUseCaseConst: OnboardingUseCase = { return ( Discover - + to monitor notifications activity and see potential issues with a specific provider or channel. diff --git a/apps/web/src/pages/get-started/consts/MultiChannelUseCase.const.tsx b/apps/web/src/pages/get-started/consts/MultiChannelUseCase.const.tsx index 2e643b9fc4a7..1db41a28ed76 100644 --- a/apps/web/src/pages/get-started/consts/MultiChannelUseCase.const.tsx +++ b/apps/web/src/pages/get-started/consts/MultiChannelUseCase.const.tsx @@ -25,7 +25,9 @@ export const MultiChannelUseCaseConst: OnboardingUseCase = { Novu has set up trial email and SMS providers for you. To expand your options, add more providers in the - {' Integration store'} + + {' Integration store'} + . ); @@ -36,9 +38,9 @@ export const MultiChannelUseCaseConst: OnboardingUseCase = { Description: function () { return ( - Novu has prepared workflow templates. - {' Customize '} - a Multi-Channel template or start with a blank workflow. + Novu has prepared workflow templates. + Customize + a Multi-Channel template or start with a blank workflow. ); }, @@ -67,7 +69,12 @@ export const MultiChannelUseCaseConst: OnboardingUseCase = { return ( Discover - + to monitor notifications activity and see potential issues with a specific provider or channel. diff --git a/apps/web/src/pages/get-started/consts/TranslationUseCase.const.tsx b/apps/web/src/pages/get-started/consts/TranslationUseCase.const.tsx index b840ba09e45d..53749daa2a17 100644 --- a/apps/web/src/pages/get-started/consts/TranslationUseCase.const.tsx +++ b/apps/web/src/pages/get-started/consts/TranslationUseCase.const.tsx @@ -21,7 +21,10 @@ export const TranslationUseCaseConst: OnboardingUseCase = { Novu has set up trial email and SMS providers for you. To expand your options, add more providers in the - Integration store + + {' '} + Integration store + . ); @@ -33,7 +36,12 @@ export const TranslationUseCaseConst: OnboardingUseCase = { return ( Add a translation group and specify the languages in the - + . ); diff --git a/apps/web/src/pages/get-started/consts/shared.tsx b/apps/web/src/pages/get-started/consts/shared.tsx index 2deebe102575..a31b2837cb22 100644 --- a/apps/web/src/pages/get-started/consts/shared.tsx +++ b/apps/web/src/pages/get-started/consts/shared.tsx @@ -25,6 +25,19 @@ export const StyledLink = styled.a` cursor: pointer; `; +export const LinkButton = styled.button` + margin: 0; + padding: 0; + border: none; + background: none; + outline: none; + color: ${colors.gradientEnd}; + font-family: inherit; + font-size: 0.875rem; + line-height: 1.25rem; + cursor: pointer; +`; + export function Link({ children, ...linkProps }: React.AnchorHTMLAttributes) { const segment = useSegment(); diff --git a/apps/web/src/pages/quick-start/components/layout/GetStartedLayout.tsx b/apps/web/src/pages/quick-start/components/layout/GetStartedLayout.tsx index d2830082bec8..698527416f4a 100644 --- a/apps/web/src/pages/quick-start/components/layout/GetStartedLayout.tsx +++ b/apps/web/src/pages/quick-start/components/layout/GetStartedLayout.tsx @@ -23,8 +23,8 @@ export function GetStartedLayout({ children, footer }: IGetStartedLayoutProps) { const navigate = useNavigate(); useEffect(() => { - currentOnboardingStep().set(location.pathname); - }, [location.pathname]); + currentOnboardingStep().set(`${location.pathname}${location.search}`); + }, [location.pathname, location.search]); useEffect(() => { const route = currentOnboardingStep().get(); diff --git a/libs/shared/src/consts/template-store/index.ts b/libs/shared/src/consts/template-store/index.ts index 961b56333f5b..08cd43ebcb7d 100644 --- a/libs/shared/src/consts/template-store/index.ts +++ b/libs/shared/src/consts/template-store/index.ts @@ -3,14 +3,31 @@ * 646f123c720b54f89ed2130a : Mention in a comment * 646c7aee958d8bed2e00b8e9 : Account Activation */ -const productionIds = ['646c77cf693b8e668a900a73', '646f123c720b54f89ed2130a', '646c7aee958d8bed2e00b8e9']; +const popularProductionIds = ['646c77cf693b8e668a900a73', '646f123c720b54f89ed2130a', '646c7aee958d8bed2e00b8e9']; /* * 64731d4e1084f5a48293ce9f : Password Reset * 64731d4e1084f5a48293ceab : Mention in a comment */ -const developmentIds = ['64731d4e1084f5a48293ce9f', '64731d4e1084f5a48293ceab']; +const popularDevelopmentIds = ['64731d4e1084f5a48293ce9f', '64731d4e1084f5a48293ceab']; + +/* + * 65c25bd6f4de5ad335bb8e48 : Delay + * 65c25bd5f4de5ad335bb8dc0 : Digest + * 65c25bd1f4de5ad335bb8c91 : In-App + * 65c25bd3f4de5ad335bb8d2a : Multi-channel + */ +const getStartedDevelopmentIds = [ + '65c25bd6f4de5ad335bb8e48', + '65c25bd5f4de5ad335bb8dc0', + '65c25bd1f4de5ad335bb8c91', + '65c25bd3f4de5ad335bb8d2a', +]; export function getPopularTemplateIds({ production }: { production: boolean }) { - return production ? productionIds : developmentIds; + return production ? popularProductionIds : popularDevelopmentIds; +} + +export function getGetStartedTemplateIds({ production }: { production: boolean }) { + return production ? [] : getStartedDevelopmentIds; } diff --git a/libs/testing/src/notification-template.service.ts b/libs/testing/src/notification-template.service.ts index 752ff0d121b2..c036ab4f953b 100644 --- a/libs/testing/src/notification-template.service.ts +++ b/libs/testing/src/notification-template.service.ts @@ -9,6 +9,8 @@ import { FeedRepository, LayoutRepository, } from '@novu/dal'; +import { v4 as uuid } from 'uuid'; + import { CreateTemplatePayload } from './create-notification-template.interface'; export class NotificationTemplateService { @@ -136,7 +138,8 @@ export class NotificationTemplateService { active: message.active, metadata: message.metadata as any, replyCallback: message.replyCallback, - uuid: message.uuid, + uuid: message.uuid ?? uuid(), + name: message.name, }); } } @@ -152,7 +155,7 @@ export class NotificationTemplateService { draft: false, tags: ['test-tag'], description: faker.commerce.productDescription().slice(0, 90), - triggers: [ + triggers: override.triggers ?? [ { identifier: `test-event-${faker.datatype.uuid()}`, type: 'event',