Skip to content

Commit

Permalink
Feat/form management config (#1409)
Browse files Browse the repository at this point in the history
* [ADM-887][frontend]: chore: roughly build up the form context.

* [ADM-887][frontend]: feat: reconstruct the config step forms.

* [ADM-887][frontend]: feat: make <ProjectName /> controlled.

* [ADM-887][frontend]: feat: useDefaultValues to make projectName accept imports.

* [ADM-887][frontend]: chore: refine code.

* [ADM-887][frontend]: feat: integrate <CalendarType /> & <RequiredMetrics />.

* [ADM-887][frontend]: feat: integrate <BoardConfig /> form with pre-submit validations.

* [ADM-887][frontend]: feat: apply `verify` disabled state with form centralized `isValid`.

* [ADM-887][frontend]: feat: refactor the useVerifyBaordEffect

* [ADM-887][frontend]: chore: refine code.

* [ADM-887][frontend]: feat: integrate <PipelineTool /> with RHF.

* [ADM-887][frontend]: feat: integrate <SourceControl /> with RHF.

* [ADM-887][frontend]: feat: reset form's validation state when submittion succeeds.

* [ADM-887][frontend]: feat: roughly integrate the `next` button state with RHF.

* [ADM-887][frontend]: feat: make form re-submit anytime when user change fields.

* [ADM-887][frontend]: feat: correct Labels && fix some test.

* [ADM-887][frontend]: feat: add <RHFFormProvider /> to facilitate testing.

* [ADM-887][frontend]: test: fix <PipelineTool /> test because of aria-label change.

* i[ADM-887][frontend]: chore: refine code.

* [ADM-887][frontend]: chore: refine code.

* [ADM-887][frontend]: feat: refine naming.

* [ADM-887][frontend]: test: fix broken test.

* [ADM-887][frontend]: test: fix the test of useVerifyBoardEffect.

* [ADM-887][frontend]: test: fix the test of useVerifyPipelineToolEffect.

* [ADM-887][frontend]: test: refine test.

* [ADM-887][frontend]: test: add FormProvider for <BasicInfo />

* [ADM-887][frontend]: feat: remove sourceControl.isVerified since no longer use it to compute the `next` state.

* [ADM-887][frontend]: feat: remove board.isVerified since no longer use it to compute the `next` state.

* [ADM-887][frontend]: feat: remove `isVerified`, use `isValid` & `isSubmitSuccessful` instead.

* [ADM-887][frontend]: feat: complete the schemas of basicInfo except for `dateRange`

* [ADM-887][frontend]: feat: manually control the error state when date-range error detected.

* [ADM-887][frontend]: feat: integrate the date range with RHF.

* [ADM-887][frontend]: feat: persist token in redux when onchange.

* [ADM-887][frontend]: feat: persist sourceControl token in redux when onchange.

* [ADM-887][frontend]: feat: persist board config in redux when onchange.

* [ADM-887][frontend]: feat: customize validation rule for date ranges to synchronise form & UI.

* [ADM-887][frontend]: test: fix test.

* [ADM-887][frontend]: chore: restore fixture consts.

* [ADM-887][frontend]: test: improve testing coverage.

* [ADM-887][frontend]: test: improve coverage.

* [ADM-887][frontend]: test: cover every scenario to match 100% coverage for date range.

* [ADM-887][frontend]: feat: fix lint errors.

* [ADM-887][frontend]: feat: no need to give default sort type in useState.

* [ADM-887][frontend]: chore: refine code.

* [ADM-887][frontend]: feat: move form initialization to MetricsStepper to persist the form submition state while jumping pages.

* [ADM-887][frontend]: feat: trigger form validation while adding & removing range.

* [ADM-887][frontend]: test: fix ConfigStep test and reduce duplicates of test runner.

* [ADM-887][frontend]: chore: abstract CONST for reused labels.
  • Loading branch information
mrcuriosity-tw authored and PengxiWPix committed Apr 28, 2024
1 parent f37b455 commit 00adc1f
Show file tree
Hide file tree
Showing 55 changed files with 2,141 additions and 1,435 deletions.
2 changes: 1 addition & 1 deletion frontend/__tests__/constants/fileConfig/fileConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
CHINA_CALENDAR,
DEFAULT_REWORK_SETTINGS,
} from '../../fixtures';
import { SortType } from '@src/containers/ConfigStep/DateRangePicker/DateRangePickerGroup';
import { SortType } from '@src/containers/ConfigStep/DateRangePicker/types';
import { convertToNewFileConfig } from '@src/constants/fileConfig';

describe('#fileConfig', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
ALL,
DEV_CHANGE_FAILURE_RATE,
CLASSIFICATION,
CONFIG_TITLE,
CYCLE_TIME,
DEPLOYMENT_FREQUENCY,
LEAD_TIME_FOR_CHANGES,
Expand All @@ -12,11 +11,13 @@ import {
REWORK_TIMES,
VELOCITY,
} from '../../fixtures';
import { MetricsTypeCheckbox } from '@src/containers/ConfigStep/MetricsTypeCheckbox';
import { basicInfoDefaultValues } from '@src/containers/ConfigStep/Form/useDefaultValues';
import { basicInfoSchema } from '@src/containers/ConfigStep/Form/schema';
import { render, waitFor, within, screen } from '@testing-library/react';
import { SELECTED_VALUE_SEPARATOR } from '@src/constants/commons';
import BasicInfo from '@src/containers/ConfigStep/BasicInfo';
import { setupStore } from '../../utils/setupStoreUtil';
import { FormProvider } from '@test/utils/FormProvider';
import userEvent from '@testing-library/user-event';
import { Provider } from 'react-redux';

Expand All @@ -27,8 +28,9 @@ describe('MetricsTypeCheckbox', () => {
store = setupStore();
return render(
<Provider store={store}>
<BasicInfo />
<MetricsTypeCheckbox />
<FormProvider schema={basicInfoSchema} defaultValues={basicInfoDefaultValues}>
<BasicInfo />
</FormProvider>
</Provider>,
);
};
Expand Down Expand Up @@ -146,23 +148,4 @@ describe('MetricsTypeCheckbox', () => {

expect(getByText(/Metrics is required/i)).toBeInTheDocument();
});

it('should show board component when click MetricsTypeCheckbox selection velocity ', async () => {
setup();
await userEvent.click(screen.getByRole('combobox', { name: REQUIRED_DATA }));
const listBox = within(screen.getByRole('listbox'));
await userEvent.click(listBox.getByRole('option', { name: VELOCITY }));
expect(screen.getAllByText(CONFIG_TITLE.BOARD)[0]).toBeInTheDocument();
});

it('should hidden board component when MetricsTypeCheckbox select is null given MetricsTypeCheckbox select is velocity ', async () => {
setup();

await userEvent.click(screen.getByRole('combobox', { name: REQUIRED_DATA }));
const requireDateSelection = within(screen.getByRole('listbox'));
await userEvent.click(requireDateSelection.getByRole('option', { name: VELOCITY }));
await userEvent.click(requireDateSelection.getByRole('option', { name: VELOCITY }));

expect(screen.queryByText(CONFIG_TITLE.BOARD)).not.toBeInTheDocument();
});
});
42 changes: 41 additions & 1 deletion frontend/__tests__/containers/ConfigStep/Board.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ import {
FAKE_TOKEN,
REVERIFY,
} from '../../fixtures';
import { boardConfigDefaultValues } from '@src/containers/ConfigStep/Form/useDefaultValues';
import { boardConfigSchema } from '@src/containers/ConfigStep/Form/schema';
import { render, screen, waitFor, within } from '@testing-library/react';
import { AXIOS_REQUEST_ERROR_CODE } from '@src/constants/resources';
import { boardClient } from '@src/clients/board/BoardClient';
import { Board } from '@src/containers/ConfigStep/Board';
import { setupStore } from '../../utils/setupStoreUtil';
import { FormProvider } from '@test/utils/FormProvider';
import { TimeoutError } from '@src/errors/TimeoutError';
import userEvent from '@testing-library/user-event';
import { Provider } from 'react-redux';
Expand Down Expand Up @@ -59,7 +62,9 @@ describe('Board', () => {
store = setupStore();
return render(
<Provider store={store}>
<Board />
<FormProvider schema={boardConfigSchema} defaultValues={boardConfigDefaultValues}>
<Board />
</FormProvider>
</Provider>,
);
};
Expand Down Expand Up @@ -252,4 +257,39 @@ describe('Board', () => {
).toBeInTheDocument();
});
});

it('should close alert modal when user manually close the alert', async () => {
setup();
await fillBoardFieldsInformation();
const timeoutError = new TimeoutError('', AXIOS_REQUEST_ERROR_CODE.TIMEOUT);
boardClient.getVerifyBoard = jest.fn().mockImplementation(() => Promise.reject(timeoutError));

await userEvent.click(screen.getByText(VERIFY));

expect(screen.getByTestId('timeoutAlert')).toBeInTheDocument();

await userEvent.click(screen.getByLabelText('Close'));

expect(screen.queryByLabelText('timeoutAlert')).not.toBeInTheDocument();
});

it('should allow user to re-submit when user interact again with form given form is already submit successfully', async () => {
setup();
mockVerifySuccess();
await fillBoardFieldsInformation();

expect(screen.getByRole('button', { name: /verify/i })).toBeEnabled();

await userEvent.click(screen.getByText(/verify/i));

expect(await screen.findByRole('button', { name: /reset/i })).toBeInTheDocument();
expect(await screen.findByRole('button', { name: /verified/i })).toBeDisabled();

const emailInput = (await screen.findByRole('textbox', { name: 'Email' })) as HTMLInputElement;
await userEvent.clear(emailInput);
await userEvent.type(emailInput, 'other@qq.com');
const verifyButton = await screen.findByRole('button', { name: /verify/i });

expect(verifyButton).toBeEnabled();
});
});
83 changes: 80 additions & 3 deletions frontend/__tests__/containers/ConfigStep/ConfigStep.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,33 @@ import {
VELOCITY,
VERIFIED,
VERIFY,
ALL,
FAKE_TOKEN,
PIPELINE_TOOL_TOKEN_INPUT_LABEL,
} from '../../fixtures';
import { fillBoardFieldsInformation } from '@test/containers/ConfigStep/Board.test';
import {
basicInfoSchema,
boardConfigSchema,
pipelineToolSchema,
sourceControlSchema,
IBasicInfoData,
IBoardConfigData,
IPipelineToolData,
ISourceControlData,
} from '@src/containers/ConfigStep/Form/schema';
import {
basicInfoDefaultValues,
boardConfigDefaultValues,
pipelineToolDefaultValues,
sourceControlDefaultValues,
} from '@src/containers/ConfigStep/Form/useDefaultValues';
import { act, render, screen, waitFor, within } from '@testing-library/react';
import { setupStore } from '../../utils/setupStoreUtil';
import { yupResolver } from '@hookform/resolvers/yup';
import userEvent from '@testing-library/user-event';
import ConfigStep from '@src/containers/ConfigStep';
import { closeMuiModal } from '@test/testUtils';
import { useForm } from 'react-hook-form';
import { Provider } from 'react-redux';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
Expand All @@ -39,18 +59,59 @@ const server = setupServer(
),
);

export const fillBoardFieldsInformation = async () => {
await userEvent.type(screen.getByLabelText(/board id/i), '1');
await userEvent.type(screen.getByLabelText(/email/i), 'fake@qq.com');
await userEvent.type(screen.getByLabelText(/site/i), 'fake');
await userEvent.type(screen.getByLabelText(/token/i), FAKE_TOKEN);
};

let store = null;
jest.mock('@src/context/config/configSlice', () => ({
...jest.requireActual('@src/context/config/configSlice'),
selectWarningMessage: jest.fn().mockReturnValue('Test warning Message'),
}));

const ConfigStepWithFormInstances = () => {
const basicInfoMethods = useForm<IBasicInfoData>({
defaultValues: basicInfoDefaultValues,
resolver: yupResolver(basicInfoSchema),
mode: 'onChange',
});

const boardConfigMethods = useForm<IBoardConfigData>({
defaultValues: boardConfigDefaultValues,
resolver: yupResolver(boardConfigSchema),
mode: 'onChange',
});

const pipelineToolMethods = useForm<IPipelineToolData>({
defaultValues: pipelineToolDefaultValues,
resolver: yupResolver(pipelineToolSchema),
mode: 'onChange',
});

const sourceControlMethods = useForm<ISourceControlData>({
defaultValues: sourceControlDefaultValues,
resolver: yupResolver(sourceControlSchema),
mode: 'onChange',
});
return (
<ConfigStep
basicInfoMethods={basicInfoMethods}
boardConfigMethods={boardConfigMethods}
pipelineToolMethods={pipelineToolMethods}
sourceControlMethods={sourceControlMethods}
/>
);
};

describe('ConfigStep', () => {
const setup = () => {
store = setupStore();
return render(
<Provider store={store}>
<ConfigStep />
<ConfigStepWithFormInstances />
</Provider>,
);
};
Expand Down Expand Up @@ -235,7 +296,9 @@ describe('ConfigStep', () => {
const requireDateSelection = within(screen.getByRole('listbox'));
await userEvent.click(requireDateSelection.getByRole('option', { name: DEPLOYMENT_FREQUENCY }));
await closeMuiModal(userEvent);
const tokenNode = within(screen.getByTestId('pipelineToolTextField')).getByLabelText('input Token');
const tokenNode = within(screen.getByTestId('pipelineToolTextField')).getByLabelText(
PIPELINE_TOOL_TOKEN_INPUT_LABEL,
);
await userEvent.type(tokenNode, FAKE_PIPELINE_TOKEN);
const submitButton = screen.getByText(VERIFY);
await userEvent.click(submitButton);
Expand All @@ -248,4 +311,18 @@ describe('ConfigStep', () => {
expect(screen.queryByText(VERIFIED)).toBeVisible();
expect(screen.queryByText(RESET)).toBeVisible();
});

it('should show all forms given all metrics selected', async () => {
setup();

const requiredMetricsField = screen.getByRole('combobox', { name: REQUIRED_DATA });
await userEvent.click(requiredMetricsField);
const requireDateSelection = within(screen.getByRole('listbox'));
await userEvent.click(requireDateSelection.getByRole('option', { name: ALL }));
await closeMuiModal(userEvent);

expect(screen.getByLabelText('Board Config')).toBeInTheDocument();
expect(screen.getByLabelText('Pipeline Tool Config')).toBeInTheDocument();
expect(screen.getByLabelText('Source Control Config')).toBeInTheDocument();
});
});
53 changes: 50 additions & 3 deletions frontend/__tests__/containers/ConfigStep/DateRangePicker.test.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { updateShouldGetBoardConfig, updateShouldGetPipelineConfig } from '@src/context/Metrics/metricsSlice';
import { basicInfoDefaultValues } from '@src/containers/ConfigStep/Form/useDefaultValues';
import { DateRangePickerSection } from '@src/containers/ConfigStep/DateRangePicker';
import { basicInfoSchema } from '@src/containers/ConfigStep/Form/schema';
import { ERROR_DATE, TIME_RANGE_ERROR_MESSAGE } from '../../fixtures';
import { render, screen, within } from '@testing-library/react';
import { setupStore } from '../../utils/setupStoreUtil';
import { FormProvider } from '@test/utils/FormProvider';
import userEvent from '@testing-library/user-event';
import { Provider } from 'react-redux';
import React from 'react';
import dayjs from 'dayjs';

const START_DATE_LABEL = 'From *';
const END_DATE_LABEL = 'To *';
const START_DATE_LABEL = 'From';
const END_DATE_LABEL = 'To';
const TODAY = dayjs('2024-03-20');
const INPUT_DATE_VALUE = TODAY.format('MM/DD/YYYY');
let store = setupStore();
Expand All @@ -26,7 +29,9 @@ const setup = () => {
store = setupStore();
return render(
<Provider store={store}>
<DateRangePickerSection />
<FormProvider schema={basicInfoSchema} defaultValues={basicInfoDefaultValues}>
<DateRangePickerSection />
</FormProvider>
</Provider>,
);
};
Expand Down Expand Up @@ -300,4 +305,46 @@ describe('DateRangePickerSection', () => {
await userEvent.click(sortButton);
expect(screen.getByRole('button', { name: 'Ascending' })).toBeInTheDocument();
});

it('should provide unified error message when given all invalid time input', async () => {
const correctRange = ['03/15/2024', '03/25/2024'];
const rangeOfTooEarly = ['03/15/1600', '03/25/1600'];
const rangeOfInvalidFormat = ['XXxYY/2024', 'ZZ/11/2024'];
const startDateRequiredErrorMessage = 'Start date is required';
const endDateRequiredErrorMessage = 'End date is required';
const unifiedStartDateErrorMessage = 'Start date is invalid';
const unifiedEndDateErrorMessage = 'End date is invalid';

const ranges = screen.getAllByLabelText('Range picker row');
const startDateInput = within(ranges[0]).getByRole('textbox', { name: START_DATE_LABEL }) as HTMLInputElement;
const endDateInput = within(ranges[0]).getByRole('textbox', { name: END_DATE_LABEL }) as HTMLInputElement;
await userEvent.type(startDateInput, rangeOfTooEarly[0]);
await userEvent.type(endDateInput, rangeOfTooEarly[1]);

expect(await screen.findByText(unifiedStartDateErrorMessage)).toBeVisible();
expect(await screen.findByText(unifiedEndDateErrorMessage)).toBeVisible();

await userEvent.clear(startDateInput);
await userEvent.clear(endDateInput);
await userEvent.keyboard('{Tab}');

expect(await screen.findByText(startDateRequiredErrorMessage)).toBeVisible();
expect(await screen.findByText(endDateRequiredErrorMessage)).toBeVisible();

await userEvent.type(startDateInput, correctRange[0]);
await userEvent.type(endDateInput, correctRange[1]);

expect(screen.queryByText(startDateRequiredErrorMessage)).toBeNull();
expect(screen.queryByText(endDateRequiredErrorMessage)).toBeNull();
expect(screen.queryByText(unifiedStartDateErrorMessage)).toBeNull();
expect(screen.queryByText(unifiedEndDateErrorMessage)).toBeNull();

await userEvent.type(startDateInput, rangeOfInvalidFormat[0]);
await userEvent.type(endDateInput, rangeOfInvalidFormat[1]);

expect(screen.queryByText(startDateRequiredErrorMessage)).toBeNull();
expect(screen.queryByText(endDateRequiredErrorMessage)).toBeNull();
expect(screen.queryByText(unifiedStartDateErrorMessage)).toBeVisible();
expect(screen.queryByText(unifiedEndDateErrorMessage)).toBeVisible();
});
});
Loading

0 comments on commit 00adc1f

Please sign in to comment.