diff --git a/package-lock.json b/package-lock.json index b680c01f4..6777abdf4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@defra/forms-model": "^3.0.569", + "@defra/forms-model": "^3.0.574", "@defra/hapi-tracing": "^1.26.0", "@elastic/ecs-pino-format": "^1.5.0", "@hapi/boom": "^10.0.1", @@ -2298,9 +2298,9 @@ } }, "node_modules/@defra/forms-model": { - "version": "3.0.569", - "resolved": "https://registry.npmjs.org/@defra/forms-model/-/forms-model-3.0.569.tgz", - "integrity": "sha512-icmi4k0hTIUVN0V/quVPT7/SfhWGyTN+akFR0lmGmrcIVakXzJvxLwgeEZBFMg8ezI3kLr2u6ppP5rbXjtV55w==", + "version": "3.0.574", + "resolved": "https://registry.npmjs.org/@defra/forms-model/-/forms-model-3.0.574.tgz", + "integrity": "sha512-H9efjCaR/y5MYfyUD3HgZv0shBHNzPNycuixB71ynd8OSTs0Z6qA4j88qCHmYtBfIfX/yywiOeGa8J/OpCG0JQ==", "license": "OGL-UK-3.0", "dependencies": { "@joi/date": "^2.1.1", diff --git a/package.json b/package.json index 85a092a93..41ffefabf 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ }, "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@defra/forms-model": "^3.0.569", + "@defra/forms-model": "^3.0.574", "@defra/hapi-tracing": "^1.26.0", "@elastic/ecs-pino-format": "^1.5.0", "@hapi/boom": "^10.0.1", diff --git a/src/server/forms/components.json b/src/server/forms/components.json index 8613cdbf0..743e4081b 100644 --- a/src/server/forms/components.json +++ b/src/server/forms/components.json @@ -120,6 +120,13 @@ "content": "### This is a H3 in markdown\n\n[An internal link](http://localhost:3009/fictional-page)\n\n[An external link](https://defra.gov.uk/fictional-page)", "options": {}, "schema": {} + }, + { + "type": "DeclarationField", + "name": "declaration", + "title": "Declaration", + "content": "By submitting this form, I agree to:\n\n- Provide accurate and complete information\n- Comply with all applicable regulations\n- Accept responsibility for any false statements", + "hint": "Please read and confirm the following terms" } ] }, diff --git a/src/server/forms/register-as-a-unicorn-breeder.yaml b/src/server/forms/register-as-a-unicorn-breeder.yaml index 82239c8e4..d8de940d6 100644 --- a/src/server/forms/register-as-a-unicorn-breeder.yaml +++ b/src/server/forms/register-as-a-unicorn-breeder.yaml @@ -219,7 +219,7 @@ pages: controller: FileUploadPageController section: Regnsa next: - - path: '/how-many-unicorns-do-you-expect-to-breed-each-year' + - path: '/declaration' components: - name: dLzALM title: Documents @@ -230,6 +230,17 @@ pages: schema: min: 1 max: 3 + - title: Declaration + path: '/declaration' + section: section + components: + - name: diLmal + title: Declaration + type: DeclarationField + content: 'Fill in this field' + options: {} + next: + - path: '/summary' conditions: - displayName: Address is different name: IrVmYz diff --git a/src/server/index.test.ts b/src/server/index.test.ts index f3f741b52..ebd747845 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -585,6 +585,7 @@ describe('prepareEnvironment', () => { 'href', 'field', 'page', + 'merge', 'markdown' ] diff --git a/src/server/plugins/engine/components/ComponentBase.ts b/src/server/plugins/engine/components/ComponentBase.ts index e341fc671..77b723c9b 100644 --- a/src/server/plugins/engine/components/ComponentBase.ts +++ b/src/server/plugins/engine/components/ComponentBase.ts @@ -90,6 +90,7 @@ export type ComponentSchema = | ArraySchema | ArraySchema | BooleanSchema + | BooleanSchema | DateSchema | NumberSchema | NumberSchema diff --git a/src/server/plugins/engine/components/ComponentCollection.ts b/src/server/plugins/engine/components/ComponentCollection.ts index faf8271ca..df0ae3f48 100644 --- a/src/server/plugins/engine/components/ComponentCollection.ts +++ b/src/server/plugins/engine/components/ComponentCollection.ts @@ -256,6 +256,7 @@ export class ComponentCollection { */ validate(value: FormPayload = {}): FormValidationResult { const result = this.formSchema.validate(value, opts) + const details = result.error?.details return { diff --git a/src/server/plugins/engine/components/DeclarationField.test.ts b/src/server/plugins/engine/components/DeclarationField.test.ts new file mode 100644 index 000000000..bfb8a41a4 --- /dev/null +++ b/src/server/plugins/engine/components/DeclarationField.test.ts @@ -0,0 +1,426 @@ +import { + ComponentType, + type DeclarationFieldComponent +} from '@defra/forms-model' + +import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js' +import { DeclarationField } from '~/src/server/plugins/engine/components/DeclarationField.js' +import { + getAnswer, + type Field +} from '~/src/server/plugins/engine/components/helpers/components.js' +import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js' +import definition from '~/test/form/definitions/blank.js' +import { getFormData, getFormState } from '~/test/helpers/component-helpers.js' + +describe('DeclarationField', () => { + let model: FormModel + + beforeEach(() => { + model = new FormModel(definition, { + basePath: 'test' + }) + }) + + describe('Defaults', () => { + let def: DeclarationFieldComponent + let collection: ComponentCollection + let field: Field + + beforeEach(() => { + def = { + title: 'Example Declaration Component', + name: 'myComponent', + content: 'Lorem ipsum dolar sit amet', + shortDescription: 'Terms and conditions', + type: ComponentType.DeclarationField, + options: {} + } satisfies DeclarationFieldComponent + + collection = new ComponentCollection([def], { model }) + field = collection.fields[0] + }) + + describe('Schema', () => { + it('uses component title as label as default', () => { + const { formSchema } = collection + const { keys } = formSchema.describe() + + expect(keys).toHaveProperty( + 'myComponent', + expect.objectContaining({ + flags: expect.objectContaining({ + label: 'Terms and conditions' + }) + }) + ) + }) + + it('uses component name as keys', () => { + const { formSchema } = collection + const { keys } = formSchema.describe() + + expect(field.keys).toEqual(['myComponent']) + expect(field.collection).toBeUndefined() + + for (const key of field.keys) { + expect(keys).toHaveProperty(key) + } + }) + + it('is required by default', () => { + const { formSchema } = collection + const { keys } = formSchema.describe() + + expect(keys).toHaveProperty( + 'myComponent', + expect.objectContaining({ + items: expect.arrayContaining([ + expect.objectContaining({ + allow: ['true'], + flags: expect.objectContaining({ + presence: 'required' + }) + }) + ]) + }) + ) + }) + + it('may have unchecked value in addition to true', () => { + const { formSchema } = collection + const { keys } = formSchema.describe() + + expect(keys).toHaveProperty( + 'myComponent', + expect.objectContaining({ + items: expect.arrayContaining([ + expect.objectContaining({ + allow: ['unchecked'], + flags: expect.objectContaining({ + result: 'strip' + }) + }) + ]) + }) + ) + + expect(keys).toHaveProperty( + 'myComponent', + expect.objectContaining({ + items: expect.arrayContaining([ + expect.objectContaining({ + allow: ['unchecked'] + }) + ]) + }) + ) + }) + + it('is optional when configured', () => { + const collectionOptional = new ComponentCollection( + [{ ...def, options: { required: false } }], + { model } + ) + + const { formSchema } = collectionOptional + const { keys } = formSchema.describe() + + expect(keys).toHaveProperty( + 'myComponent', + expect.objectContaining({ + items: expect.arrayContaining([ + expect.objectContaining({ + allow: ['true'], + flags: expect.not.objectContaining({ + presence: 'required' + }) + }) + ]) + }) + ) + + const result = collectionOptional.validate(getFormData(['unchecked'])) + expect(result.errors).toBeUndefined() + }) + + it('accepts valid values', () => { + const result1 = collection.validate(getFormData(['unchecked', 'true'])) + + expect(result1.errors).toBeUndefined() + }) + + it('adds errors for empty value', () => { + const result = collection.validate(getFormData(['unchecked'])) + + expect(result.errors).toEqual([ + expect.objectContaining({ + text: 'You must confirm you understand and agree with the terms and conditions to continue' + }) + ]) + }) + + it('adds errors for invalid values', () => { + const result1 = collection.validate(getFormData(['invalid'])) + const result2 = collection.validate( + // @ts-expect-error - Allow invalid param for test + getFormData({ unknown: 'invalid' }) + ) + // @ts-expect-error - Allow invalid param for test + const result3 = collection.validate('false') + + expect(result1.errors).toBeTruthy() + expect(result2.errors).toBeTruthy() + expect(result3.errors).toBeTruthy() + }) + }) + + describe('State', () => { + it('returns text from state', () => { + const state1 = getFormState(true) + const state2 = getFormState() + // context - boolean + // state - boolean + // string - I confirm that I understand and accept this declaration + const answer1 = getAnswer(field, state1) + const answer2 = getAnswer(field, state2) + + expect(answer1).toBe('I understand and agree') + expect(answer2).toBe('') + }) + + it('returns payload from state', () => { + const state1 = getFormState(true) + const state2 = getFormState(null) + + const payload1 = field.getFormDataFromState(state1) + const payload2 = field.getFormDataFromState(state2) + + expect(payload1).toEqual(getFormData('true')) + expect(payload2).toEqual(getFormData()) + }) + + it('returns value from state', () => { + const state1 = getFormState(true) + const state2 = getFormState(null) + + const value1 = field.getFormValueFromState(state1) + const value2 = field.getFormValueFromState(state2) + + expect(value1).toBe('true') + expect(value2).toBeUndefined() + }) + + it('returns context for conditions and form submission', () => { + const state1 = getFormState(true) + const state2 = getFormState(null) + + const value1 = field.getContextValueFromState(state1) + const value2 = field.getContextValueFromState(state2) + + expect(value1).toBe(true) + expect(value2).toBe(false) + }) + + it('returns state from payload', () => { + const payload1 = getFormData(['true']) + const payload2 = getFormData([]) + const payload3 = getFormData(['unchecked']) + + const value1 = field.getStateFromValidForm(payload1) + const value2 = field.getStateFromValidForm(payload2) + const value3 = field.getStateFromValidForm(payload3) + + expect(value1).toEqual(getFormState(true)) + expect(value2).toEqual(getFormState(false)) + expect(value3).toEqual(getFormState(false)) + }) + }) + + describe('View model', () => { + it('sets Nunjucks component defaults', () => { + const viewModel = field.getViewModel(getFormData([])) + + expect(viewModel).toEqual( + expect.objectContaining({ + label: { text: def.title }, + name: 'myComponent', + attributes: {}, + values: [], + content: 'Lorem ipsum dolar sit amet', + id: 'myComponent', + fieldset: { + legend: { + text: 'Example Declaration Component' + } + }, + items: [ + { + value: 'true', + text: 'I understand and agree' + } + ] + }) + ) + }) + + it('sets Nunjucks component value when posted', () => { + def = { + ...def, + hint: 'Please read and confirm the following' + } satisfies DeclarationFieldComponent + + collection = new ComponentCollection([def], { model }) + field = collection.fields[0] + const viewModel = field.getViewModel(getFormData(['true'])) + + expect(viewModel).toEqual( + expect.objectContaining({ + values: ['true'], + hint: { + text: 'Please read and confirm the following' + } + }) + ) + }) + + it('sets custom message when in def', () => { + def = { + ...def, + title: 'Declaration', + content: + 'Declaration:\n' + + 'By submitting this form, I consent to the collection and processing of my personal data for the purposes described.\n' + + 'I understand that my data may be shared with authorised third parties where required by law', + options: { + declarationConfirmationLabel: + 'I consent to the processing of my personal data' + } + } satisfies DeclarationFieldComponent + + collection = new ComponentCollection([def], { model }) + field = collection.fields[0] + + const viewModel = field.getViewModel(getFormData(['unchecked', 'true'])) + + expect(viewModel).toEqual( + expect.objectContaining({ + items: [ + { + value: 'true', + text: 'I consent to the processing of my personal data' + } + ] + }) + ) + }) + }) + + describe('AllPossibleErrors', () => { + it('should return errors', () => { + const errors = field.getAllPossibleErrors() + expect(errors.baseErrors).not.toBeEmpty() + expect(errors.advancedSettingsErrors).toBeEmpty() + }) + }) + + describe('getFormValue', () => { + test('should return correct value', () => { + expect(field.getFormValue(undefined)).toBeUndefined() + expect(field.getFormValue([true])).toEqual([true]) + expect(field.getFormValue([])).toEqual([]) + expect(field.getFormValue({})).toBeUndefined() + }) + }) + }) + + describe('Validation', () => { + describe.each([ + { + description: 'Default', + component: { + title: 'Terms and conditions', + shortDescription: 'The terms and conditions', + content: 'Lorem ipsum dolar sit amet', + name: 'myComponent', + type: ComponentType.DeclarationField, + options: {} + } satisfies DeclarationFieldComponent, + assertions: [ + { + input: getFormData(['unchecked', 'true']), + output: { + value: { + myComponent: ['true'] + }, + errors: undefined + } + } + ] + }, + { + description: 'Use short description if it exists', + component: { + title: 'Terms and conditions', + shortDescription: 'Terms and conditions', + content: 'Lorem ipsum dolar sit amet', + name: 'myComponent', + type: ComponentType.DeclarationField, + options: {} + } satisfies DeclarationFieldComponent, + assertions: [ + { + input: getFormData(['unchecked']), + output: { + value: { myComponent: [] }, + errors: [ + expect.objectContaining({ + text: 'You must confirm you understand and agree with the terms and conditions to continue' + }) + ] + } + } + ] + }, + { + description: 'Optional field', + component: { + title: 'Example text field', + name: 'myComponent', + content: 'Lorem ipsum dolar sit amet', + type: ComponentType.DeclarationField, + options: { + required: false + } + } satisfies DeclarationFieldComponent, + assertions: [ + { + input: getFormData(['unchecked']), + output: { value: { myComponent: [] } } + } + ] + } + ])('$description', ({ component: def, assertions }) => { + let collection: ComponentCollection + + beforeEach(() => { + collection = new ComponentCollection([def], { model }) + }) + + it.each([...assertions])( + 'validates custom example', + ({ input, output }) => { + const result = collection.validate(input) + expect(result).toEqual(output) + } + ) + }) + }) + + describe('isBool', () => { + test('should return correct boolean', () => { + expect(DeclarationField.isBool('string')).toBe(false) + expect(DeclarationField.isBool(true)).toBe(true) + }) + }) +}) diff --git a/src/server/plugins/engine/components/DeclarationField.ts b/src/server/plugins/engine/components/DeclarationField.ts new file mode 100644 index 000000000..f328abc19 --- /dev/null +++ b/src/server/plugins/engine/components/DeclarationField.ts @@ -0,0 +1,167 @@ +import { type DeclarationFieldComponent, type Item } from '@defra/forms-model' +import joi, { + type ArraySchema, + type BooleanSchema, + type StringSchema +} from 'joi' + +import { + FormComponent, + isFormValue +} from '~/src/server/plugins/engine/components/FormComponent.js' +import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js' +import { + type ErrorMessageTemplateList, + type FormPayload, + type FormState, + type FormStateValue, + type FormSubmissionError, + type FormSubmissionState, + type FormValue +} from '~/src/server/plugins/engine/types.js' + +export class DeclarationField extends FormComponent { + private readonly DEFAULT_DECLARATION_LABEL = 'I understand and agree' + + declare options: DeclarationFieldComponent['options'] + + declare declarationConfirmationLabel: string + + declare formSchema: ArraySchema + declare stateSchema: BooleanSchema + declare content: string + + constructor( + def: DeclarationFieldComponent, + props: ConstructorParameters[1] + ) { + super(def, props) + + const { options, content } = def + + let checkboxSchema = joi.string().valid('true') + + if (options.required !== false) { + checkboxSchema = checkboxSchema.required() + } + + const formSchema = joi + .array() + .items(checkboxSchema, joi.string().valid('unchecked').strip()) + .label(this.label) + .single() + .messages({ + 'any.required': messageTemplate.declarationRequired as string, + 'any.unknown': messageTemplate.declarationRequired as string, + 'array.includesRequiredUnknowns': + messageTemplate.declarationRequired as string + }) as ArraySchema + + this.formSchema = formSchema + this.stateSchema = joi.boolean().cast('string').label(this.label).required() + + this.options = options + this.content = content + this.declarationConfirmationLabel = + options.declarationConfirmationLabel ?? this.DEFAULT_DECLARATION_LABEL + } + + getFormValueFromState(state: FormSubmissionState) { + const { name } = this + return state[name] === true ? 'true' : undefined + } + + getFormDataFromState(state: FormSubmissionState): FormPayload { + const { name } = this + return { [name]: state[name] === true ? 'true' : undefined } + } + + getStateFromValidForm(payload: FormPayload): FormState { + const { name } = this + const payloadValue = payload[name] + const value = + this.isValue(payloadValue) && + payloadValue.length > 0 && + payloadValue.every((v) => { + return v === 'true' + }) + + return { [name]: value } + } + + getContextValueFromFormValue(value: FormValue | FormPayload): boolean { + return value === 'true' + } + + getFormValue(value?: FormStateValue | FormState) { + return this.isValue(value) ? value : undefined + } + + getDisplayStringFromFormValue(value: FormValue | FormPayload): string { + return value ? this.declarationConfirmationLabel : '' + } + + getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) { + const defaultDeclarationConfirmationLabel = + 'I confirm that I understand and accept this declaration' + const { + title, + hint, + content, + declarationConfirmationLabel = defaultDeclarationConfirmationLabel + } = this + return { + ...super.getViewModel(payload, errors), + hint: hint ? { text: hint } : undefined, + fieldset: { + legend: { + text: title + } + }, + content, + values: payload[this.name], + items: [ + { + text: declarationConfirmationLabel, + value: 'true' + } + ] + } + } + + isValue(value?: FormStateValue | FormState): value is Item['value'][] { + if (!Array.isArray(value)) { + return false + } + + // Skip checks when empty + if (!value.length) { + return true + } + + return value.every(isFormValue) + } + + /** + * For error preview page that shows all possible errors on a component + */ + getAllPossibleErrors(): ErrorMessageTemplateList { + return DeclarationField.getAllPossibleErrors() + } + + /** + * Static version of getAllPossibleErrors that doesn't require a component instance. + */ + static getAllPossibleErrors(): ErrorMessageTemplateList { + return { + baseErrors: [ + { type: 'required', template: messageTemplate.declarationRequired } + ], + advancedSettingsErrors: [] + } + } + + static isBool(value?: FormStateValue | FormState): value is boolean { + return isFormValue(value) && typeof value === 'boolean' + } +} diff --git a/src/server/plugins/engine/components/helpers/components.ts b/src/server/plugins/engine/components/helpers/components.ts index 052d2a63d..880df9812 100644 --- a/src/server/plugins/engine/components/helpers/components.ts +++ b/src/server/plugins/engine/components/helpers/components.ts @@ -20,6 +20,7 @@ export type Field = InstanceType< | typeof Components.YesNoField | typeof Components.CheckboxesField | typeof Components.DatePartsField + | typeof Components.DeclarationField | typeof Components.EastingNorthingField | typeof Components.EmailAddressField | typeof Components.LatLongField @@ -102,6 +103,10 @@ export function createComponent( component = new Components.DatePartsField(def, options) break + case ComponentType.DeclarationField: + component = new Components.DeclarationField(def, options) + break + case ComponentType.Details: component = new Components.Details(def, options) break diff --git a/src/server/plugins/engine/components/index.ts b/src/server/plugins/engine/components/index.ts index b17a786ea..43c342e26 100644 --- a/src/server/plugins/engine/components/index.ts +++ b/src/server/plugins/engine/components/index.ts @@ -7,6 +7,7 @@ export { AutocompleteField } from '~/src/server/plugins/engine/components/AutocompleteField.js' export { CheckboxesField } from '~/src/server/plugins/engine/components/CheckboxesField.js' export { DatePartsField } from '~/src/server/plugins/engine/components/DatePartsField.js' +export { DeclarationField } from '~/src/server/plugins/engine/components/DeclarationField.js' export { Details } from '~/src/server/plugins/engine/components/Details.js' export { EmailAddressField } from '~/src/server/plugins/engine/components/EmailAddressField.js' export { FileUploadField } from '~/src/server/plugins/engine/components/FileUploadField.js' diff --git a/src/server/plugins/engine/pageControllers/validationOptions.ts b/src/server/plugins/engine/pageControllers/validationOptions.ts index 4c4575c58..2c20a6b3e 100644 --- a/src/server/plugins/engine/pageControllers/validationOptions.ts +++ b/src/server/plugins/engine/pageControllers/validationOptions.ts @@ -20,6 +20,10 @@ const opts = { * see @link https://joi.dev/api/?v=17.4.2#template-syntax for template syntax */ export const messageTemplate: Record = { + declarationRequired: joi.expression( + 'You must confirm you understand and agree with the {{lowerFirst(#label)}} to continue', + opts + ) as JoiExpression, required: joi.expression( 'Enter {{lowerFirst(#label)}}', opts diff --git a/src/server/plugins/engine/views/components/declarationfield.html b/src/server/plugins/engine/views/components/declarationfield.html new file mode 100644 index 000000000..7f18e2383 --- /dev/null +++ b/src/server/plugins/engine/views/components/declarationfield.html @@ -0,0 +1,14 @@ +{% from "govuk/components/fieldset/macro.njk" import govukFieldset %} +{% from "govuk/components/checkboxes/macro.njk" import govukCheckboxes %} +{% from "govuk/components/hint/macro.njk" import govukHint %} + +{% macro DeclarationField(component) %} + {% set content %} +
+ {{ component.model.content | markdown | safe }} +
+ {% endset %} + {% set checkboxes = component.model | merge({ formGroup: { beforeInputs: { html: content } } }) %} + {{ govukCheckboxes(checkboxes) }} + +{% endmacro %} diff --git a/src/server/plugins/nunjucks/filters/index.js b/src/server/plugins/nunjucks/filters/index.js index 0e636b5c0..b622c5ab8 100644 --- a/src/server/plugins/nunjucks/filters/index.js +++ b/src/server/plugins/nunjucks/filters/index.js @@ -5,3 +5,4 @@ export { answer } from '~/src/server/plugins/nunjucks/filters/answer.js' export { href } from '~/src/server/plugins/nunjucks/filters/href.js' export { field } from '~/src/server/plugins/nunjucks/filters/field.js' export { page } from '~/src/server/plugins/nunjucks/filters/page.js' +export { merge } from '~/src/server/plugins/nunjucks/filters/merge.js' diff --git a/src/server/plugins/nunjucks/filters/merge.js b/src/server/plugins/nunjucks/filters/merge.js new file mode 100644 index 000000000..fccdc62dc --- /dev/null +++ b/src/server/plugins/nunjucks/filters/merge.js @@ -0,0 +1,16 @@ +/** + * Nunjucks filter to get the page for a given path + * @param {Record} targetDictionary - Object to extend + * @param {Record | string} sourceDictionary - Object to merge into target + * @returns {Record} + */ +export function merge(targetDictionary, sourceDictionary) { + if (typeof sourceDictionary !== 'object') { + return targetDictionary + } + + return { + ...targetDictionary, + ...sourceDictionary + } +} diff --git a/src/server/plugins/nunjucks/filters/merge.test.js b/src/server/plugins/nunjucks/filters/merge.test.js new file mode 100644 index 000000000..73f3f5ecf --- /dev/null +++ b/src/server/plugins/nunjucks/filters/merge.test.js @@ -0,0 +1,15 @@ +import { merge } from '~/src/server/plugins/nunjucks/filters/merge.js' + +describe('merge', () => { + const propertyToMerge = { lorem: 'ipsum' } + it('should return the target if source is not an object', () => { + expect(merge(propertyToMerge, 'dolar')).toBe(propertyToMerge) + }) + + it('should merge the properties if they are valid', () => { + expect(merge({ lorem: 'dolar', dolar: 'sit' }, propertyToMerge)).toEqual({ + lorem: 'ipsum', + dolar: 'sit' + }) + }) +})