console: add onboarding wizard tests

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6041
GitOrigin-RevId: 72e5f6e0fc7c51c9a4db70665059771194be2730
This commit is contained in:
Abhijeet Khangarot 2022-09-26 16:03:04 +05:30 committed by hasura-bot
parent c89d283dd2
commit 90543c99ae
12 changed files with 407 additions and 78 deletions

View File

@ -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

View File

@ -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<string, unknown>;
status: string;
};
type ExperimentsCohort = {
experiment: string;
activity: Record<string, unknown>;
};
export type ExperimentsResponseData = {
data: {
experiments_config: [
{
experiment: string;
metadata: Record<string, unknown>;
status: string;
}
];
experiments_cohort: [
{
experiment: string;
activity: Record<string, unknown>;
}
];
experiments_config: ExperimentsConfig[];
experiments_cohort: ExperimentsCohort[];
};
};

View File

@ -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<typeof Root>;
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 (
<Root growthExperimentsClient={mockGrowthClient.enabledWithoutActivity} />
);
};
export const Base: Story = () => (
<Root growthExperimentsClient={mockGrowthClient} />
);
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 (
<Root growthExperimentsClient={mockGrowthClient.enabledWithoutActivity} />
);
};
WithoutSurvey.parameters = {
msw: [...baseHandlers(), fetchAnsweredSurveysHandler],
};

View File

@ -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(
<ReduxProvider store={store} key="provider">
<QueryClientProvider client={reactQueryClient}>
<OnboardingWizard growthExperimentsClient={mockedGrowthClient} />
</QueryClientProvider>
</ReduxProvider>
);
};
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();
});
});

View File

@ -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
}

View File

@ -0,0 +1,177 @@
import { GrowthExperimentsClient } from '@/features/GrowthExperiments';
import { SurveysResponseData } from '@/features/Surveys';
export const mockGrowthClient: Record<string, GrowthExperimentsClient> = {
/**
* 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',
},
],
},
};

View File

@ -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<any, ResponseBodyOnSuccess>((req, res, ctx) => {
return res(
ctx.status(200),
ctx.data({
status: 'success',
})
);
}),
export const baseHandlers = () => [
controlPlaneApi.query<ResponseBodyOnSuccess>(
'fetchAllExperimentsData',
(req, res, ctx) => {
return res(
ctx.status(200),
ctx.data({
status: 'success',
})
);
}
),
controlPlaneApi.mutation<ResponseBodyOnSuccess>(
'addSurveyAnswer',
(req, res, ctx) => {
return res(
ctx.status(200),
ctx.data({
status: 'success',
})
);
}
),
controlPlaneApi.mutation<ResponseBodyOnSuccess>(
'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));
});

View File

@ -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
}

View File

@ -3,3 +3,4 @@ export {
useFamiliaritySurveyData,
} from './HasuraFamiliaritySurvey';
export { prefetchSurveysData } from './utils';
export { SurveysResponseData } from './types';

View File

@ -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[];
};

View File

@ -6,9 +6,9 @@ import { ReactQueryDevtools } from 'react-query/devtools';
export const ReactQueryDecorator = (): DecoratorFn => {
const reactQueryClient = new QueryClient();
return story => (
return Story => (
<QueryClientProvider client={reactQueryClient}>
{story()}
<Story />
<ReactQueryDevtools />
</QueryClientProvider>
);

View File

@ -27,5 +27,9 @@ export const ReduxDecorator = (
devTools: true,
});
return story => <Provider store={store}>{story()}</Provider>;
return Story => (
<Provider store={store}>
<Story />
</Provider>
);
};