Skip to content

Commit

Permalink
SLEIGHT-32: new component-local validation -- validation and transien…
Browse files Browse the repository at this point in the history
…t state should not be part of the redux store
  • Loading branch information
synkarius committed Jul 17, 2022
1 parent c3b9e83 commit 7510425
Show file tree
Hide file tree
Showing 26 changed files with 445 additions and 242 deletions.
3 changes: 1 addition & 2 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React from 'react';
import { render } from '@testing-library/react';
import { Provider } from 'react-redux';
import { store } from './app/store';
import App from './App';

test('renders learn react link', () => {
test('basic render test', () => {
const { getByText } = render(
<Provider store={store}>
<App />
Expand Down
1 change: 0 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import { Navigation } from './features/menu/Navigation';
import { SidebarComponent } from './features/sidebar/SidebarComponent';
Expand Down
4 changes: 2 additions & 2 deletions src/app/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { actionReducer } from '../features/model/action/action-reducers';
import { commandReducer } from '../features/model/command/command-reducers';
import { contextReducer } from '../features/model/context/context-reducers';
import { variableReducer } from '../features/model/variable/variable-reducers';
import { roleKeyReducer } from '../features/model/role-key/role-key-reducers';
import { roleKeyReduxReducer } from '../features/model/role-key/role-key-reducers';
import { selectorReducer } from '../features/model/selector/selector-reducers';
import { specReducer } from '../features/model/spec/spec-reducers';

Expand All @@ -16,7 +16,7 @@ export const store = configureStore({
context: contextReducer,
counter: counterReducer,
focus: focusReducer,
roleKey: roleKeyReducer,
roleKey: roleKeyReduxReducer,
selector: selectorReducer,
spec: specReducer,
variable: variableReducer,
Expand Down
11 changes: 11 additions & 0 deletions src/error/ImproperContextUsageError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ErrorCode } from './error-codes';
import { SleightError } from './SleightError';

export class ImproperContextUsageError extends SleightError {
constructor() {
super(
ErrorCode.IMPROPER_CONTEXT_USAGE,
ErrorCode.IMPROPER_CONTEXT_USAGE.toString()
);
}
}
8 changes: 8 additions & 0 deletions src/error/NotImplementedError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ErrorCode } from './error-codes';
import { SleightError } from './SleightError';

export class NotImplementedError extends SleightError {
constructor() {
super(ErrorCode.NOT_IMPLEMENTED, ErrorCode.NOT_IMPLEMENTED.toString());
}
}
12 changes: 12 additions & 0 deletions src/error/UnhandledRoleKeyEditingEventTypeError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { RoleKeyActionType } from '../features/model/role-key/role-key-editing-context';
import { ErrorCode } from './error-codes';
import { SleightError } from './SleightError';

export class UnhandledRoleKeyEditingEventTypeError extends SleightError {
constructor(type: RoleKeyActionType) {
super(
ErrorCode.UNHANDLED_ROLE_KEY_EDITING_EVENT_TYPE,
RoleKeyActionType[type]
);
}
}
26 changes: 15 additions & 11 deletions src/error/error-codes.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
export enum ErrorCode {
SELECTOR_ID_NOT_FOUND,
SELECTOR_ID_ALREADY_IN_USE,
IMPROPER_CONTEXT_USAGE = 'do not use default context impl - provide values to context provider in JSX instead',

NOT_EDITING_AN_ACTION,
NOT_EDITING_AN_ACTION = 'not editing an action',
NOT_IMPLEMENTED = 'method not implemented',

UNHANDLED_ACTION_TYPE,
UNHANDLED_ACTION_VALUE_TYPE,
UNHANDLED_ACTION_VALUE_OPERATION,
UNHANDLED_SEND_KEY_FIELD,
UNHANDLED_SEND_KEY_MODE,
UNHANDLED_SEND_KEY_MODIFIER_TYPE,
UNHANDLED_SPEC_ITEM_TYPE,
UNHANDLED_VARIABLE_TYPE,
SELECTOR_ID_ALREADY_IN_USE = 'selector id already in use',
SELECTOR_ID_NOT_FOUND = 'selector id not found',

UNHANDLED_ACTION_TYPE = 'unhandled action type',
UNHANDLED_ACTION_VALUE_OPERATION = 'unhandled action value operation',
UNHANDLED_ACTION_VALUE_TYPE = 'unhandled action value type',
UNHANDLED_ROLE_KEY_EDITING_EVENT_TYPE = 'unhandled role key editing event type',
UNHANDLED_SPEC_ITEM_TYPE = 'unhandled spec item type',
UNHANDLED_SEND_KEY_FIELD = 'unhandled send key field',
UNHANDLED_SEND_KEY_MODE = 'unhandled send key mode',
UNHANDLED_SEND_KEY_MODIFIER_TYPE = 'unhandled send key modifier type',
UNHANDLED_VARIABLE_TYPE = 'unhandled variable type',
}
8 changes: 4 additions & 4 deletions src/features/menu/focus/FocusComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import { CommandComponent } from '../../model/command/CommandComponent';
import { ElementType } from '../../model/common/element-types';
import { ContextComponent } from '../../model/context/ContextComponent';
import { VariableComponent } from '../../model/variable/VariableComponent';
import { RoleKeyComponent } from '../../model/role-key/RoleKeyComponent';
import { SpecComponent } from '../../model/spec/SpecComponent';
import { RoleKeyParentComponent } from '../../model/role-key/RoleKeyParentComponent';

export const FocusComponent: React.FC<{}> = () => {
const elementType = useAppSelector((state) => state.focus.elementType);
const action = useAppSelector((state) => state.action.editing);
const command = useAppSelector((state) => state.command.editing);
const context = useAppSelector((state) => state.context.editing);
const roleKey = useAppSelector((state) => state.roleKey.editing);
const roleKeyId = useAppSelector((state) => state.roleKey.editingId);
const spec = useAppSelector((state) => state.spec.editing);
const variable = useAppSelector((state) => state.variable.editing);

Expand All @@ -28,8 +28,8 @@ export const FocusComponent: React.FC<{}> = () => {
{elementType === ElementType.CONTEXT && context && (
<ContextComponent context={context} />
)}
{elementType === ElementType.ROLE_KEY && roleKey && (
<RoleKeyComponent roleKey={roleKey} />
{elementType === ElementType.ROLE_KEY && (
<RoleKeyParentComponent roleKeyId={roleKeyId} key={roleKeyId} />
)}
{elementType === ElementType.SPEC && spec && (
<SpecComponent spec={spec} />
Expand Down
11 changes: 4 additions & 7 deletions src/features/menu/focus/focus-reducers.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

type Focus = {
elementType: string | null;
elementType: string | undefined;
};

const initialState: Focus = {
elementType: null,
elementType: undefined,
};

const focusSlice = createSlice({
name: 'focus',
initialState,
reducers: {
setFocus: (state, action: PayloadAction<string>) => {
setFocus: (state, action: PayloadAction<string | undefined>) => {
state.elementType = action.payload;
},
clearFocus: (state) => {
state.elementType = null;
},
},
});

export const { setFocus, clearFocus } = focusSlice.actions;
export const { setFocus } = focusSlice.actions;
export const focusReducer = focusSlice.reducer;
6 changes: 3 additions & 3 deletions src/features/model/action/action-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type IdValued = TextValued | RangeValue;

const getNotEmptyValidator = (fieldName: string): Validator<TextValued> => {
return {
test: (actionValue: TextValued) => {
isValid: (actionValue: TextValued) => {
return (
ActionValueType.ENTER_VALUE !== actionValue.actionValueType ||
(actionValue.value != null && actionValue.value.trim().length > 0)
Expand All @@ -28,7 +28,7 @@ const getVariableNotSelectedValidator = (
fieldName: string
): Validator<IdValued> => {
return {
test: (actionValue: IdValued) => {
isValid: (actionValue: IdValued) => {
return (
ActionValueType.USE_VARIABLE !== actionValue.actionValueType ||
(actionValue.variableId != null &&
Expand All @@ -43,7 +43,7 @@ const getRoleKeyNotSelectedValidator = (
fieldName: string
): Validator<IdValued> => {
return {
test: (actionValue: IdValued) => {
isValid: (actionValue: IdValued) => {
return (
ActionValueType.USE_ROLE_KEY !== actionValue.actionValueType ||
(actionValue.roleKeyId != null &&
Expand Down
17 changes: 17 additions & 0 deletions src/features/model/common/editing-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import { ImproperContextUsageError } from '../../../error/ImproperContextUsageError';

interface EditingData<P> {
// P is a payload
localDispatchFn: React.Dispatch<P>;
}
const createDefaultEditingDataState = <P>(): EditingData<P> => {
return {
localDispatchFn: (p: P) => {
throw new ImproperContextUsageError();
},
};
};
export const createEditingContext = <T>(): React.Context<EditingData<T>> => {
return React.createContext(createDefaultEditingDataState<T>());
};
4 changes: 2 additions & 2 deletions src/features/model/context/context-validation.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { notEmpty } from '../../../validation/common-validations';
import { notEmptyPredicate } from '../../../validation/common-validations';
import {
createValidationError,
Validator,
} from '../../../validation/validator';

export const contextMatcherValidator: Validator<string> = {
test: notEmpty,
isValid: notEmptyPredicate,
error: createValidationError("matcher can't be empty"),
};
45 changes: 27 additions & 18 deletions src/features/model/role-key/RoleKeyComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
import React from 'react';
import { Button, Form, FormControl, FormText } from 'react-bootstrap';
import { useAppDispatch, useAppSelector } from '../../../app/hooks';
import React, { useContext } from 'react';
import { Button, Form, FormControl } from 'react-bootstrap';
import { useAppDispatch } from '../../../app/hooks';
import { ValidationContext } from '../../../validation/validation-context';
import { Field } from '../../../validation/validation-field';
import { setFocus } from '../../menu/focus/focus-reducers';
import { FormGroupRowComponent } from '../../ui/FormGroupRowComponent';
import { PanelComponent } from '../../ui/PanelComponent';
import { RoleKey } from './role-key';
import {
changeEditingRoleKeyValue,
clearEditingRoleKey,
saveAndClearEditingRoleKey,
validateRoleKeyText,
} from './role-key-reducers';
RoleKeyActionType,
RoleKeyEditingContext,
} from './role-key-editing-context';
import { saveRoleKey } from './role-key-reducers';
import { roleKeyTextValidator } from './role-key-validation';

export const RoleKeyComponent: React.FC<{ roleKey: RoleKey }> = (props) => {
const dispatch = useAppDispatch();
const validationErrors = useAppSelector(
(state) => state.roleKey.validationErrors
);
const reduxDispatch = useAppDispatch();
const validationContext = useContext(ValidationContext);
const editingContext = useContext(RoleKeyEditingContext);

const valueChangedHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
dispatch(changeEditingRoleKeyValue(event.target.value));
dispatch(validateRoleKeyText());
editingContext.localDispatchFn({
type: RoleKeyActionType.CHANGE_VALUE,
payload: event.target.value,
});
validationContext.touch(Field.RK_ROLE_KEY);
};
const submitHandler = (_event: React.MouseEvent<HTMLButtonElement>) => {
dispatch(validateRoleKeyText());
dispatch(saveAndClearEditingRoleKey());
const formIsValid = validationContext.validateForm();
if (formIsValid) {
reduxDispatch(saveRoleKey(props.roleKey));
reduxDispatch(setFocus(undefined));
}
};

const validationErrors = validationContext.getErrors();
return (
<PanelComponent header="Create/Edit Role Key">
<FormGroupRowComponent
Expand All @@ -37,9 +45,10 @@ export const RoleKeyComponent: React.FC<{ roleKey: RoleKey }> = (props) => {
<FormControl
type="text"
onChange={valueChangedHandler}
onBlur={(_e) => dispatch(validateRoleKeyText())}
onBlur={(_e) => validationContext.touch(Field.RK_ROLE_KEY)}
value={props.roleKey.value}
isInvalid={validationErrors.includes(roleKeyTextValidator.error)}
name={Field[Field.RK_ROLE_KEY]}
/>
<Form.Control.Feedback type="invalid">
{roleKeyTextValidator.error.message}
Expand All @@ -54,7 +63,7 @@ export const RoleKeyComponent: React.FC<{ roleKey: RoleKey }> = (props) => {
Save
</Button>
<Button
onClick={(_e) => dispatch(clearEditingRoleKey())}
onClick={(_e) => reduxDispatch(setFocus(undefined))}
className="mx-3"
variant="warning"
size="lg"
Expand Down
71 changes: 71 additions & 0 deletions src/features/model/role-key/RoleKeyParentComponent.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { Provider } from 'react-redux';
import App from '../../../App';
import { store } from '../../../app/store';
import { Field } from '../../../validation/validation-field';

beforeEach(() => {
render(
<Provider store={store}>
<App />
</Provider>
);
});

const clickRoleKeysSidebarSection = () => {
const roleKeysSidebarSection =
screen.getByText<HTMLButtonElement>('Role Keys');
fireEvent.click(roleKeysSidebarSection);
};

const clickCreateNewRoleKey = () => {
const createNewRoleKeyButton = screen.getByText('Create New Role Key');
fireEvent.click(createNewRoleKeyButton);
};

const getRoleKeyInputField = (): HTMLInputElement => {
const qs = `input[name="${Field[Field.RK_ROLE_KEY]}"]`;
return document.querySelector<HTMLInputElement>(qs) as HTMLInputElement;
};

describe('role key component tests', () => {
it('should handle create new', () => {
clickRoleKeysSidebarSection();
clickCreateNewRoleKey();

const input = getRoleKeyInputField();

expect(input?.value).toBe('');
});

it('should not save if validation errors', () => {
clickRoleKeysSidebarSection();
clickCreateNewRoleKey();
// there will be a validation error for the role key being empty

const saveButton = screen.getByText<HTMLButtonElement>('Save');
fireEvent.click(saveButton);

expect(saveButton).toBeDisabled();
});

it('should invalidate empty role key', () => {
clickRoleKeysSidebarSection();
clickCreateNewRoleKey();

const input = getRoleKeyInputField();
fireEvent.blur(input);

expect(input).toHaveClass('is-invalid');
});

it('should validate non-empty role key', () => {
clickRoleKeysSidebarSection();
clickCreateNewRoleKey();

const input = getRoleKeyInputField();
fireEvent.change(input, { target: { value: 'a' } });

expect(input).not.toHaveClass('is-invalid');
});
});
Loading

0 comments on commit 7510425

Please sign in to comment.