Skip to content

Commit

Permalink
feat: add input and schema validation tckt-365
Browse files Browse the repository at this point in the history
  • Loading branch information
kalasgarov committed Nov 29, 2024
1 parent 4fafc9d commit 8be510e
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 10 deletions.
2 changes: 1 addition & 1 deletion packages/common/src/locales/en/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export const en = {
hint: 'For example, man, woman, non-binary',
errorTextMustContainChar: 'String must contain at least 1 character(s)',
preferNotToAnswerTextLabel:
'Prefer not to share my gender identity label',
'Prefer not to share my gender identity checkbox label',
},
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,17 @@ export const Default: StoryObj<typeof GenderIdPattern> = {
genderId: 'gender-identity',
label: 'Gender identity',
hint: 'For example, man, woman, non-binary',
required: false,
required: true,
preferNotToAnswerText: 'Prefer not to share my gender identity',
},
};

export const WithRequired: StoryObj<typeof GenderIdPattern> = {
export const Optional: StoryObj<typeof GenderIdPattern> = {
args: {
genderId: 'gender-identity',
label: 'Gender identity',
hint: 'For example, man, woman, non-binary',
required: true,
required: false,
preferNotToAnswerText: 'Prefer not to share my gender identity',
},
};
Expand Down Expand Up @@ -73,18 +73,18 @@ export const WithCheckboxChecked: StoryObj<typeof GenderIdPattern> = {
genderId: 'gender-identity',
label: 'Gender identity',
hint: 'For example, man, woman, non-binary',
required: false,
required: true,
preferNotToAnswerText: 'Prefer not to share my gender identity',
preferNotToAnswerChecked: true,
},
};

export const WithoutPreferNotToAnswerText: StoryObj<typeof GenderIdPattern> = {
export const WithoutCheckbox: StoryObj<typeof GenderIdPattern> = {
args: {
genderId: 'gender-identity',
label: 'Gender identity',
hint: 'For example, man, woman, non-binary',
required: false,
required: true,
preferNotToAnswerText: undefined,
},
};
};
10 changes: 9 additions & 1 deletion packages/design/src/Form/components/GenderId/GenderId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ export const GenderIdPattern: PatternComponent<GenderIdProps> = ({
className={classNames('usa-input', {
'usa-input--error': error,
})}
style={
preferNotToAnswerChecked
? {
backgroundColor: '#e9ecef',
pointerEvents: 'none',
opacity: 0.65,
}
: {}
}
id={genderId}
type="text"
defaultValue={value}
Expand All @@ -77,7 +86,6 @@ export const GenderIdPattern: PatternComponent<GenderIdProps> = ({
`${hint ? `${hintId}` : ''}${error ? ` ${errorId}` : ''}`.trim() ||
undefined
}
disabled={preferNotToAnswerChecked}
/>
{preferNotToAnswerText && (
<div className="usa-checkbox">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const pattern: GenderIdPattern = {
type: 'gender-id',
data: {
label: message.patterns.genderId.displayName,
required: false,
required: true,
hint: undefined,
preferNotToAnswerText: message.patterns.genderId.preferNotToAnswerTextLabel,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
.draggableListItemWrapper .dateOfBirthPattern legend,
.draggableListItemWrapper .phoneNumberPattern legend,
.draggableListItemWrapper .ssnPattern legend,
.draggableListItemWrapper .genderIdPattern legend,
.draggableListItemWrapper .emailInputPattern legend {
padding-left: 0;
}
Expand Down
125 changes: 125 additions & 0 deletions packages/forms/src/patterns/gender-id/gender-id.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { describe, expect, it } from 'vitest';
import {
createGenderIdSchema,
genderIdConfig,
type GenderIdPattern,
} from './gender-id';

describe('GenderIdPattern tests', () => {
describe('createGenderIdSchema', () => {
it('should create schema for required gender identity input', () => {
const data: GenderIdPattern['data'] = {
label: 'Test Gender Identity Label',
required: true,
};

const schema = createGenderIdSchema(data);
const validInput = 'Test Gender';
const invalidInput = '';

expect(schema.safeParse(validInput).success).toBe(true);
expect(schema.safeParse(invalidInput).success).toBe(false);
});

it('should create schema for optional gender identity input', () => {
const data: GenderIdPattern['data'] = {
label: 'Test Gender Identity Label',
required: false,
};

const schema = createGenderIdSchema(data);
const validInput = 'Test Gender';
const emptyInput = '';

expect(schema.safeParse(validInput).success).toBe(true);
expect(schema.safeParse(emptyInput).success).toBe(true);
});
});

describe('genderIdConfig', () => {
it('should parse user input correctly', () => {
const pattern: GenderIdPattern = {
id: 'gender-identity-1',
type: 'gender-id',
data: {
label: 'Test Gender Identity Label',
required: true,
},
};

const inputValue = 'Test Gender';
if (!genderIdConfig.parseUserInput) {
expect.fail('genderIdConfig.parseUserInput is undefined');
}
const result = genderIdConfig.parseUserInput(pattern, inputValue);
if (result.success) {
expect(result.data).toEqual(inputValue);
} else {
expect.fail('Unexpected validation failure');
}
});

it('should handle validation error for user input', () => {
const pattern: GenderIdPattern = {
id: 'gender-identity-1',
type: 'gender-id',
data: {
label: 'Test Gender Identity Label',
required: true,
},
};

const inputValue = '';
if (!genderIdConfig.parseUserInput) {
expect.fail('genderIdConfig.parseUserInput is undefined');
}
const result = genderIdConfig.parseUserInput(pattern, inputValue);
if (!result.success) {
expect(result.error).toBeDefined();
} else {
expect.fail('Unexpected validation success');
}
});

it('should parse config data correctly', () => {
const obj = {
label: 'Test Gender Identity Label',
required: true,
hint: 'For example, man, woman, non-binary',
preferNotToAnswerText: 'Prefer not to share my gender identity',
};

if (!genderIdConfig.parseConfigData) {
expect.fail('genderIdConfig.parseConfigData is undefined');
}
const result = genderIdConfig.parseConfigData(obj);
if (result.success) {
expect(result.data.label).toBe('Test Gender Identity Label');
expect(result.data.required).toBe(true);
expect(result.data.hint).toBe('For example, man, woman, non-binary');
expect(result.data.preferNotToAnswerText).toBe(
'Prefer not to share my gender identity'
);
} else {
expect.fail('Unexpected validation failure');
}
});

it('should handle invalid config data', () => {
const obj = {
label: '',
required: true,
};

if (!genderIdConfig.parseConfigData) {
expect.fail('genderIdConfig.parseConfigData is undefined');
}
const result = genderIdConfig.parseConfigData(obj);
if (!result.success) {
expect(result.error).toBeDefined();
} else {
expect.fail('Unexpected validation success');
}
});
});
});
86 changes: 86 additions & 0 deletions packages/forms/src/patterns/gender-id/gender-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import * as z from 'zod';
import { type GenderIdProps } from '../../components.js';
import { type Pattern, type PatternConfig } from '../../pattern.js';
import { getFormSessionValue } from '../../session.js';
import {
safeZodParseFormErrors,
safeZodParseToFormError,
} from '../../util/zod.js';

const configSchema = z.object({
label: z.string().min(1),
required: z.boolean(),
hint: z.string().optional(),
preferNotToAnswerText: z.string().optional(),
});

export type GenderIdPattern = Pattern<z.infer<typeof configSchema>>;

export type GenderIdPatternOutput = z.infer<
ReturnType<typeof createGenderIdSchema>
>;

export const createGenderIdSchema = (data: GenderIdPattern['data']) => {
return z.string().superRefine((value, ctx) => {
if (value === data.preferNotToAnswerText) {
return;
}
if (data.required && value.trim() === '') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'This field is required',
});
}
});
};

export const genderIdConfig: PatternConfig<
GenderIdPattern,
GenderIdPatternOutput
> = {
displayName: 'Gender ID',
iconPath: 'gender-id-icon.svg',
initial: {
label: 'Gender identity',
required: true,
hint: 'For example, man, woman, non-binary',
preferNotToAnswerText: 'Prefer not to share my gender identity',
},

parseUserInput: (pattern, inputValue) => {
const result = safeZodParseToFormError(
createGenderIdSchema(pattern.data),
inputValue
);
return result;
},

parseConfigData: obj => {
return safeZodParseFormErrors(configSchema, obj);
},
getChildren() {
return [];
},

createPrompt(_, session, pattern, options) {
const extraAttributes: Record<string, any> = {};
const sessionValue = getFormSessionValue(session, pattern.id);
const error = session.data.errors[pattern.id];

return {
props: {
_patternId: pattern.id,
type: 'gender-id',
label: pattern.data.label,
genderId: pattern.id,
required: pattern.data.required,
hint: pattern.data.hint,
preferNotToAnswerText: pattern.data.preferNotToAnswerText,
value: sessionValue,
error,
...extraAttributes,
} as GenderIdProps,
children: [],
};
},
};
3 changes: 3 additions & 0 deletions packages/forms/src/patterns/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { dateOfBirthConfig } from './date-of-birth/date-of-birth.js';
import { emailInputConfig } from './email-input/email-input.js';
import { fieldsetConfig } from './fieldset/index.js';
import { formSummaryConfig } from './form-summary.js';
import { genderIdConfig } from './gender-id/gender-id.js';
import { inputConfig } from './input/index.js';
import { packageDownloadConfig } from './package-download/index.js';
import { pageConfig } from './page/index.js';
Expand All @@ -31,6 +32,7 @@ export const defaultFormConfig: FormConfig = {
'email-input': emailInputConfig,
fieldset: fieldsetConfig,
'form-summary': formSummaryConfig,
'gender-id': genderIdConfig,
input: inputConfig,
'package-download': packageDownloadConfig,
page: pageConfig,
Expand All @@ -55,6 +57,7 @@ export * from './email-input/email-input.js';
export * from './fieldset/index.js';
export { type FieldsetPattern } from './fieldset/config.js';
export * from './form-summary.js';
export * from './gender-id/gender-id.js';
export * from './input/index.js';
export { type InputPattern } from './input/config.js';
export * from './package-download/index.js';
Expand Down

0 comments on commit 8be510e

Please sign in to comment.