diff --git a/console/src/features/GrowthExperiments/constants.ts b/console/src/features/GrowthExperiments/constants.ts index 87c57f7cbb3..026d71ae589 100644 --- a/console/src/features/GrowthExperiments/constants.ts +++ b/console/src/features/GrowthExperiments/constants.ts @@ -2,7 +2,7 @@ * GraphQl query to fetch all growth experiments data related to the current user. */ export const query = ` -query { +query fetchAllExperimentsData { experiments_config { experiment metadata diff --git a/console/src/features/GrowthExperiments/types.ts b/console/src/features/GrowthExperiments/types.ts index 4ef7356e216..af74a01c426 100644 --- a/console/src/features/GrowthExperiments/types.ts +++ b/console/src/features/GrowthExperiments/types.ts @@ -5,20 +5,21 @@ export type ExperimentConfig = { userActivity: ExperimentsResponseData['data']['experiments_cohort'][0]['activity']; }; +// TODO: These types should be replaced by autogenerated types, when we use typescript graphql codegen +type ExperimentsConfig = { + experiment: string; + metadata: Record; + status: string; +}; + +type ExperimentsCohort = { + experiment: string; + activity: Record; +}; + export type ExperimentsResponseData = { data: { - experiments_config: [ - { - experiment: string; - metadata: Record; - status: string; - } - ]; - experiments_cohort: [ - { - experiment: string; - activity: Record; - } - ]; + experiments_config: ExperimentsConfig[]; + experiments_cohort: ExperimentsCohort[]; }; }; diff --git a/console/src/features/OnboardingWizard/Root.stories.tsx b/console/src/features/OnboardingWizard/Root.stories.tsx index a2f1fd08878..01c8ac706e4 100644 --- a/console/src/features/OnboardingWizard/Root.stories.tsx +++ b/console/src/features/OnboardingWizard/Root.stories.tsx @@ -1,30 +1,50 @@ import React from 'react'; import { ComponentMeta, Story } from '@storybook/react'; import { ReactQueryDecorator } from '@/storybook/decorators/react-query'; +import { useQueryClient } from 'react-query'; import { Root } from './Root'; -import { handlers } from './mocks/handlers.mock'; +import { + baseHandlers, + fetchAnsweredSurveysHandler, + fetchUnansweredSurveysHandler, +} from './mocks/handlers.mock'; +import { mockGrowthClient } from './mocks/constants'; +import { surveysQueryKey } from '../Surveys/constants'; export default { title: 'features/Onboarding Wizard/Root', component: Root, decorators: [ReactQueryDecorator()], - parameters: { - msw: handlers(), - }, } as ComponentMeta; -const mockGrowthClient = { - getAllExperimentConfig: () => [ - { - experiment: 'console_onboarding_wizard_v1', - status: 'enabled', - metadata: {}, - userActivity: {}, - }, - ], - setAllExperimentConfig: () => Promise.resolve(), +export const WithSurvey: Story = () => { + const queryClient = useQueryClient(); + // need to invalidate as useSurveysData hook is using a stale time + queryClient.invalidateQueries(surveysQueryKey, { + refetchActive: false, + }); + + return ( + + ); }; -export const Base: Story = () => ( - -); +WithSurvey.parameters = { + msw: [...baseHandlers(), fetchUnansweredSurveysHandler], +}; + +export const WithoutSurvey: Story = () => { + const queryClient = useQueryClient(); + // need to invalidate as `useSurveysData` hook is using a stale time + queryClient.invalidateQueries(surveysQueryKey, { + refetchActive: false, + }); + + return ( + + ); +}; + +WithoutSurvey.parameters = { + msw: [...baseHandlers(), fetchAnsweredSurveysHandler], +}; diff --git a/console/src/features/OnboardingWizard/Root.test.tsx b/console/src/features/OnboardingWizard/Root.test.tsx new file mode 100644 index 00000000000..fdd40b721a0 --- /dev/null +++ b/console/src/features/OnboardingWizard/Root.test.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { setupServer } from 'msw/node'; +import { Provider as ReduxProvider } from 'react-redux'; +import { configureStore } from '@reduxjs/toolkit'; +import { GrowthExperimentsClient } from '../GrowthExperiments'; +import { baseHandlers } from './mocks/handlers.mock'; +import { mockGrowthClient } from './mocks/constants'; +import { OnboardingWizard } from '.'; + +const server = setupServer(...baseHandlers()); + +beforeAll(() => server.listen()); +afterAll(() => server.close()); + +const OnboardingWizardRender = async ( + mockedGrowthClient: GrowthExperimentsClient +) => { + // redux provider is needed as ConnectDBScreen is using dispatch to push routes + const store = configureStore({ + reducer: { + tables: () => ({ currentDataSource: 'postgres' }), + }, + }); + + // react query provider is needed as surveys component is using it + const reactQueryClient = new QueryClient(); + + render( + + + + + + ); +}; + +describe('Check different configurations of experiments client', () => { + it('should hide wizard as error in experiment name', async () => { + await OnboardingWizardRender(mockGrowthClient.nameError); + + expect( + screen.queryByText('Welcome to your new Hasura project!') + ).not.toBeInTheDocument(); + }); + + it('should show wizard as experiment is enabled and user activity is empty', async () => { + await OnboardingWizardRender(mockGrowthClient.enabledWithoutActivity); + + expect( + screen.queryByText('Welcome to your new Hasura project!') + ).toBeInTheDocument(); + }); + + it('should hide wizard as experiment is disabled', async () => { + await OnboardingWizardRender(mockGrowthClient.disabledWithoutActivity); + + expect( + screen.queryByText('Welcome to your new Hasura project!') + ).not.toBeInTheDocument(); + }); + + it('should hide wizard as user has completed the onboarding', async () => { + await OnboardingWizardRender(mockGrowthClient.enabledWithCorrectActivity); + + expect( + screen.queryByText('Welcome to your new Hasura project!') + ).not.toBeInTheDocument(); + }); + + it('should hide wizard as experiment is disabled, user activity does not matter in this case', async () => { + await OnboardingWizardRender(mockGrowthClient.disabledWithCorrectActivity); + + expect( + screen.queryByText('Welcome to your new Hasura project!') + ).not.toBeInTheDocument(); + }); + + it('should show wizard as experiment is enabled, and user activity is incorrect. Could mean a error on backend.', async () => { + await OnboardingWizardRender(mockGrowthClient.enabledWithWrongActivity); + + expect( + screen.queryByText('Welcome to your new Hasura project!') + ).toBeInTheDocument(); + }); +}); diff --git a/console/src/features/OnboardingWizard/hooks/constants.ts b/console/src/features/OnboardingWizard/hooks/constants.ts index 60ec829158d..1714f435201 100644 --- a/console/src/features/OnboardingWizard/hooks/constants.ts +++ b/console/src/features/OnboardingWizard/hooks/constants.ts @@ -4,7 +4,7 @@ import globals from '@/Globals'; export const experimentId = growthExperimentsIds.onboardingWizardV1; export const graphQlMutation = ` -mutation ($projectId: uuid!, $experimentId: String!, $kind: String!) { +mutation trackExperimentsCohortActivity ($projectId: uuid!, $experimentId: String!, $kind: String!) { trackExperimentsCohortActivity(experiment: $experimentId, payload: {kind: $kind, project_id: $projectId}) { status } diff --git a/console/src/features/OnboardingWizard/mocks/constants.ts b/console/src/features/OnboardingWizard/mocks/constants.ts new file mode 100644 index 00000000000..b46656cb957 --- /dev/null +++ b/console/src/features/OnboardingWizard/mocks/constants.ts @@ -0,0 +1,177 @@ +import { GrowthExperimentsClient } from '@/features/GrowthExperiments'; +import { SurveysResponseData } from '@/features/Surveys'; + +export const mockGrowthClient: Record = { + /** + * config to hide wizard as experiment name error + */ + nameError: { + getAllExperimentConfig: () => [ + { + experiment: 'console_onboarding_wizardddd_v1', + status: 'enabled', + metadata: {}, + userActivity: {}, + }, + ], + setAllExperimentConfig: () => Promise.resolve(), + }, + /** + * config to show wizard as experiment enabled and + * user activity is empty + */ + enabledWithoutActivity: { + getAllExperimentConfig: () => [ + { + experiment: 'console_onboarding_wizard_v1', + status: 'enabled', + metadata: {}, + userActivity: {}, + }, + ], + setAllExperimentConfig: () => Promise.resolve(), + }, + /** + * config to hide wizard as experiment is disabled + */ + disabledWithoutActivity: { + getAllExperimentConfig: () => [ + { + experiment: 'console_onboarding_wizard_v1', + status: 'disabled', + metadata: {}, + userActivity: {}, + }, + ], + setAllExperimentConfig: () => Promise.resolve(), + }, + /** + * config to hide wizard as user has completed + * the onboarding + */ + enabledWithCorrectActivity: { + getAllExperimentConfig: () => [ + { + experiment: 'console_onboarding_wizard_v1', + status: 'enabled', + metadata: {}, + userActivity: { + onboarding_complete: true, + }, + }, + ], + setAllExperimentConfig: () => Promise.resolve(), + }, + /** + * config to hide wizard as experiment is disabled, + * user has completed the onboarding or not should not matter + * in this case. + */ + disabledWithCorrectActivity: { + getAllExperimentConfig: () => [ + { + experiment: 'console_onboarding_wizard_v1', + status: 'disabled', + metadata: {}, + userActivity: { + onboarding_complete: true, + }, + }, + ], + setAllExperimentConfig: () => Promise.resolve(), + }, + /** + * config to show wizard as experiment is enabled, + * and user activity is incorrect. Could mean a error on backend + * while saving user activity. + */ + enabledWithWrongActivity: { + getAllExperimentConfig: () => [ + { + experiment: 'console_onboarding_wizard_v1', + status: 'enabled', + metadata: {}, + userActivity: { + onboarding_completessss: true, + }, + }, + ], + setAllExperimentConfig: () => Promise.resolve(), + }, +}; + +export const fetchSurveysDataResponse: Record< + string, + SurveysResponseData['data'] +> = { + unanswered: { + survey: [ + { + survey_name: 'Hasura familiarity survey', + survey_questions: [ + { + kind: 'radio', + question: 'How familiar are you with Hasura?', + id: '4595916a-1c5b-4d55-b7ca-11616131d1d3', + survey_question_options: [ + { + option: 'new user', + id: 'dcd2480b-cc1b-4e1a-b111-27aed8b89b8b', + }, + { + option: 'active user', + id: '53ed19af-7ae7-4225-9969-13b06d9b8f66', + }, + { + option: 'recurring user', + id: '8f81e3d0-f45b-40ba-9456-8865a5a1cb93', + }, + { + option: 'past user', + id: 'b25edd78-0af3-4448-8302-14e2b818c4c6', + }, + ], + }, + ], + }, + ], + survey_question_answers: [], + }, + answered: { + survey: [ + { + survey_name: 'Hasura familiarity survey', + survey_questions: [ + { + kind: 'radio', + question: 'How familiar are you with Hasura?', + id: '4595916a-1c5b-4d55-b7ca-11616131d1d3', + survey_question_options: [ + { + option: 'new user', + id: 'dcd2480b-cc1b-4e1a-b111-27aed8b89b8b', + }, + { + option: 'active user', + id: '53ed19af-7ae7-4225-9969-13b06d9b8f66', + }, + { + option: 'recurring user', + id: '8f81e3d0-f45b-40ba-9456-8865a5a1cb93', + }, + { + option: 'past user', + id: 'b25edd78-0af3-4448-8302-14e2b818c4c6', + }, + ], + }, + ], + }, + ], + survey_question_answers: [ + { + survey_question_id: '4595916a-1c5b-4d55-b7ca-11616131d1d3', + }, + ], + }, +}; diff --git a/console/src/features/OnboardingWizard/mocks/handlers.mock.ts b/console/src/features/OnboardingWizard/mocks/handlers.mock.ts index f0482f4310f..ae1b1d51ea4 100644 --- a/console/src/features/OnboardingWizard/mocks/handlers.mock.ts +++ b/console/src/features/OnboardingWizard/mocks/handlers.mock.ts @@ -1,5 +1,7 @@ import { graphql } from 'msw'; import Endpoints from '@/Endpoints'; +import { SurveysResponseData } from '@/features/Surveys'; +import { fetchSurveysDataResponse } from './constants'; type ResponseBodyOnSuccess = { status: 'success'; @@ -7,13 +9,50 @@ type ResponseBodyOnSuccess = { const controlPlaneApi = graphql.link(Endpoints.luxDataGraphql); -export const handlers = () => [ - controlPlaneApi.operation((req, res, ctx) => { - return res( - ctx.status(200), - ctx.data({ - status: 'success', - }) - ); - }), +export const baseHandlers = () => [ + controlPlaneApi.query( + 'fetchAllExperimentsData', + (req, res, ctx) => { + return res( + ctx.status(200), + ctx.data({ + status: 'success', + }) + ); + } + ), + controlPlaneApi.mutation( + 'addSurveyAnswer', + (req, res, ctx) => { + return res( + ctx.status(200), + ctx.data({ + status: 'success', + }) + ); + } + ), + controlPlaneApi.mutation( + 'trackExperimentsCohortActivity', + (req, res, ctx) => { + return res( + ctx.status(200), + ctx.data({ + status: 'success', + }) + ); + } + ), ]; + +export const fetchUnansweredSurveysHandler = controlPlaneApi.query< + SurveysResponseData['data'] +>('fetchAllSurveysData', (req, res, ctx) => { + return res(ctx.status(200), ctx.data(fetchSurveysDataResponse.unanswered)); +}); + +export const fetchAnsweredSurveysHandler = controlPlaneApi.query< + SurveysResponseData['data'] +>('fetchAllSurveysData', (req, res, ctx) => { + return res(ctx.status(200), ctx.data(fetchSurveysDataResponse.answered)); +}); diff --git a/console/src/features/Surveys/constants.ts b/console/src/features/Surveys/constants.ts index 8a219328248..8c27fffd0f5 100644 --- a/console/src/features/Surveys/constants.ts +++ b/console/src/features/Surveys/constants.ts @@ -7,7 +7,7 @@ export const surveyName = 'Hasura familiarity survey'; * GraphQl query to fetch all surveys related data */ export const fetchAllSurveysDataQuery = ` - query ($currentTime: timestamptz!) { + query fetchAllSurveysData($currentTime: timestamptz!) { survey(where: { _or: [ {ended_at: {_gte: $currentTime}} @@ -39,7 +39,7 @@ export const fetchAllSurveysDataQueryVariables = { * GraphQl mutation to save the survey answer */ export const addSurveyAnswerMutation = ` - mutation ($responses: [QuestionAnswers]!, $surveyName: String!) { + mutation addSurveyAnswer ($responses: [QuestionAnswers]!, $surveyName: String!) { saveSurveyAnswer(payload: {responses: $responses, surveyName: $surveyName}) { status } diff --git a/console/src/features/Surveys/index.ts b/console/src/features/Surveys/index.ts index b7c518c639e..9e4a2bcc3c2 100644 --- a/console/src/features/Surveys/index.ts +++ b/console/src/features/Surveys/index.ts @@ -3,3 +3,4 @@ export { useFamiliaritySurveyData, } from './HasuraFamiliaritySurvey'; export { prefetchSurveysData } from './utils'; +export { SurveysResponseData } from './types'; diff --git a/console/src/features/Surveys/types.ts b/console/src/features/Surveys/types.ts index 7b29d33e3c4..b3e0b320f7b 100644 --- a/console/src/features/Surveys/types.ts +++ b/console/src/features/Surveys/types.ts @@ -1,40 +1,40 @@ +// TODO: These types should be replaced by autogenerated types, when we use typescript graphql codegen + +type SurveyQuestionOptions = { + // Note: `option` comes from cloud backend, and is not strongly typed in backend. Hence it is typed as `string` + // As the form is dynamic in nature, there could be additions/deletions to survey options in future + option: string; + id: string; +}; + +type SurveyQuestion = { + kind: string; + question: string; + id: string; + survey_question_options: SurveyQuestionOptions[]; +}; + +type SurveyDetail = { + survey_name: string; + survey_questions: SurveyQuestion[]; +}; + export type SurveysResponseData = { data: { - survey: [ - { - survey_name: string; - survey_questions: [ - { - kind: string; - question: string; - id: string; - survey_question_options: [ - { - // Note: this value comes from the cloud backend, and is not strongly typed in backend. - // As the form is dynamic in nature, there could be additions/deletions to survey options in future - option: string; - id: string; - } - ]; - } - ]; - } - ]; - survey_question_answers: [ - { - survey_question_id: string; - } - ]; + survey: SurveyDetail[]; + survey_question_answers: { + survey_question_id: string; + }[]; }; }; +type QuestionAnswersResponse = { + answer?: string; + optionsSelected?: string; + question_id: string; +}; + export type QuestionAnswers = { surveyName: string; - responses: [ - { - answer?: string; - optionsSelected?: string; - question_id: string; - } - ]; + responses: QuestionAnswersResponse[]; }; diff --git a/console/src/storybook/decorators/react-query.tsx b/console/src/storybook/decorators/react-query.tsx index 9f640cc9891..a4b68085805 100644 --- a/console/src/storybook/decorators/react-query.tsx +++ b/console/src/storybook/decorators/react-query.tsx @@ -6,9 +6,9 @@ import { ReactQueryDevtools } from 'react-query/devtools'; export const ReactQueryDecorator = (): DecoratorFn => { const reactQueryClient = new QueryClient(); - return story => ( + return Story => ( - {story()} + ); diff --git a/console/src/storybook/decorators/redux-decorator.tsx b/console/src/storybook/decorators/redux-decorator.tsx index 95e635f851d..e06ef1169f6 100644 --- a/console/src/storybook/decorators/redux-decorator.tsx +++ b/console/src/storybook/decorators/redux-decorator.tsx @@ -27,5 +27,9 @@ export const ReduxDecorator = ( devTools: true, }); - return story => {story()}; + return Story => ( + + + + ); };