mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
console: add onboarding wizard tests
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6041 GitOrigin-RevId: 72e5f6e0fc7c51c9a4db70665059771194be2730
This commit is contained in:
parent
c89d283dd2
commit
90543c99ae
@ -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
|
||||
|
@ -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[];
|
||||
};
|
||||
};
|
||||
|
@ -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],
|
||||
};
|
||||
|
87
console/src/features/OnboardingWizard/Root.test.tsx
Normal file
87
console/src/features/OnboardingWizard/Root.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
@ -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
|
||||
}
|
||||
|
177
console/src/features/OnboardingWizard/mocks/constants.ts
Normal file
177
console/src/features/OnboardingWizard/mocks/constants.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
@ -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));
|
||||
});
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -3,3 +3,4 @@ export {
|
||||
useFamiliaritySurveyData,
|
||||
} from './HasuraFamiliaritySurvey';
|
||||
export { prefetchSurveysData } from './utils';
|
||||
export { SurveysResponseData } from './types';
|
||||
|
@ -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[];
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -27,5 +27,9 @@ export const ReduxDecorator = (
|
||||
devTools: true,
|
||||
});
|
||||
|
||||
return story => <Provider store={store}>{story()}</Provider>;
|
||||
return Story => (
|
||||
<Provider store={store}>
|
||||
<Story />
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user