From 72c64859d0e009c2203f4d097bdd0c51c6f7cec1 Mon Sep 17 00:00:00 2001 From: kurilova Date: Wed, 31 Jul 2024 08:14:46 +0000 Subject: [PATCH] Revert "Expired profile (#619)" Prevent opening of Expired risk profile --- modules/ui/src/app/mocks/profile.mock.ts | 66 +- modules/ui/src/app/model/profile.ts | 8 +- .../profile-form/profile-form.component.html | 2 +- .../profile-form.component.spec.ts | 598 ++++++++---------- .../profile-form/profile-form.component.ts | 45 +- .../risk-assessment.component.html | 2 +- .../risk-assessment.component.spec.ts | 4 +- .../risk-assessment.component.ts | 6 + .../risk-assessment/risk-assessment.store.ts | 16 +- .../ui/src/app/services/test-run.service.ts | 6 +- 10 files changed, 318 insertions(+), 435 deletions(-) diff --git a/modules/ui/src/app/mocks/profile.mock.ts b/modules/ui/src/app/mocks/profile.mock.ts index 614b6e752..f2dbe82c8 100644 --- a/modules/ui/src/app/mocks/profile.mock.ts +++ b/modules/ui/src/app/mocks/profile.mock.ts @@ -17,7 +17,7 @@ import { FormControlType, Profile, - Question, + ProfileFormat, ProfileStatus, } from '../model/profile'; @@ -29,51 +29,22 @@ export const PROFILE_MOCK: Profile = { { question: 'What is the email of the device owner(s)?', answer: 'boddey@google.com, cmeredith@google.com', - type: FormControlType.EMAIL_MULTIPLE, - validation: { - required: true, - max: '30', - }, }, { question: 'What type of device do you need reviewed?', - answer: 'Type', - type: FormControlType.TEXTAREA, - validation: { - required: true, - max: '28', - }, - description: 'This tells us about the device', + answer: 'IoT Sensor', }, { question: 'Are any of the following statements true about your device?', answer: 'First', - type: FormControlType.SELECT, - options: ['First', 'Second'], - validation: { - required: true, - }, }, { question: 'What features does the device have?', - description: - 'This tells us about the data your device will collectThis tells us about the data your device will collect', - type: FormControlType.SELECT_MULTIPLE, answer: [0, 1, 2], - options: ['Wi-fi', 'Bluetooth', 'ZigBee / Z-Wave / Thread / Matter'], - validation: { - required: true, - }, }, { question: 'Comments', answer: 'Yes', - type: FormControlType.TEXT, - description: 'Please enter any comments here', - validation: { - max: '28', - required: true, - }, }, ], }; @@ -90,7 +61,7 @@ export const PROFILE_MOCK_3: Profile = { questions: [], }; -export const PROFILE_FORM: Question[] = [ +export const PROFILE_FORM: ProfileFormat[] = [ { question: 'Email', type: FormControlType.EMAIL_MULTIPLE, @@ -192,51 +163,22 @@ export const COPY_PROFILE_MOCK: Profile = { { question: 'What is the email of the device owner(s)?', answer: 'boddey@google.com, cmeredith@google.com', - type: FormControlType.EMAIL_MULTIPLE, - validation: { - required: true, - max: '30', - }, }, { question: 'What type of device do you need reviewed?', - answer: 'Type', - type: FormControlType.TEXTAREA, - validation: { - required: true, - max: '28', - }, - description: 'This tells us about the device', + answer: 'IoT Sensor', }, { question: 'Are any of the following statements true about your device?', answer: 'First', - type: FormControlType.SELECT, - options: ['First', 'Second'], - validation: { - required: true, - }, }, { question: 'What features does the device have?', - description: - 'This tells us about the data your device will collectThis tells us about the data your device will collect', - type: FormControlType.SELECT_MULTIPLE, answer: [0, 1, 2], - options: ['Wi-fi', 'Bluetooth', 'ZigBee / Z-Wave / Thread / Matter'], - validation: { - required: true, - }, }, { question: 'Comments', answer: 'Yes', - type: FormControlType.TEXT, - description: 'Please enter any comments here', - validation: { - max: '28', - required: true, - }, }, ], }; diff --git a/modules/ui/src/app/model/profile.ts b/modules/ui/src/app/model/profile.ts index fef15f101..059b3cafe 100644 --- a/modules/ui/src/app/model/profile.ts +++ b/modules/ui/src/app/model/profile.ts @@ -35,13 +35,17 @@ export interface Validation { max?: string; } -export interface Question { +export interface ProfileFormat { question: string; - type?: FormControlType; + type: FormControlType; description?: string; options?: string[]; default?: string; validation?: Validation; +} + +export interface Question { + question?: string; answer?: string | number[]; } diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html index af31e4d6d..50f0bf3c7 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html @@ -43,7 +43,7 @@ - + { - [ - 'very long value very long value very long value very long value very long value very long value very long value', - 'as&@3$', - ].forEach(value => { - const name: HTMLInputElement = compiled.querySelector( - '.form-name' - ) as HTMLInputElement; - name.value = value; - name.dispatchEvent(new Event('input')); - component.nameControl.markAsTouched(); - fixture.detectChanges(); + beforeEach(() => { + component.selectedProfile = null; + fixture.detectChanges(); + }); - const nameError = compiled.querySelector('mat-error')?.innerHTML; - const error = component.nameControl.hasError('invalid_format'); + describe('Profile name input', () => { + it('should be present', () => { + const name: HTMLInputElement = compiled.querySelector( + '.form-name' + ) as HTMLInputElement; - expect(error).toBeTruthy(); - expect(nameError).toContain( - 'Please, check. The Profile name must be a maximum of 28 characters. Only letters, numbers, and accented letters are permitted.' - ); - }); - }); + expect(name).toBeTruthy(); + }); - it('should have "required" error when field is not filled', () => { - const name: HTMLInputElement = compiled.querySelector( - '.form-name' - ) as HTMLInputElement; - ['', ' '].forEach(value => { - name.value = value; - name.dispatchEvent(new Event('input')); - component.nameControl.markAsTouched(); - fixture.detectChanges(); + it('should not contain errors when input is correct', () => { + const name: HTMLInputElement = compiled.querySelector( + '.form-name' + ) as HTMLInputElement; + ['name', 'Gebäude', 'jardín'].forEach(value => { + name.value = value; + name.dispatchEvent(new Event('input')); - const nameError = compiled.querySelector('mat-error')?.innerHTML; - const error = component.nameControl.hasError('required'); + const errors = component.nameControl.errors; + const uiValue = name.value; + const formValue = component.nameControl.value; - expect(error).toBeTruthy(); - expect(nameError).toContain('The Profile name is required'); - }); + expect(uiValue).toEqual(formValue); + expect(errors).toBeNull(); }); + }); - it('should have different profile name error when profile with name is exist', () => { + it('should have "invalid_format" error when field does not satisfy validation rules', () => { + [ + 'very long value very long value very long value very long value very long value very long value very long value', + 'as&@3$', + ].forEach(value => { const name: HTMLInputElement = compiled.querySelector( '.form-name' ) as HTMLInputElement; - name.value = 'Primary profile'; + name.value = value; name.dispatchEvent(new Event('input')); component.nameControl.markAsTouched(); - fixture.detectChanges(); const nameError = compiled.querySelector('mat-error')?.innerHTML; - const error = component.nameControl.hasError('has_same_profile_name'); + const error = component.nameControl.hasError('invalid_format'); expect(error).toBeTruthy(); expect(nameError).toContain( - 'This Profile name is already used for another Risk Assessment profile' + 'Please, check. The Profile name must be a maximum of 28 characters. Only letters, numbers, and accented letters are permitted.' ); }); }); - PROFILE_FORM.forEach((item, index) => { - const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i + it('should have "required" error when field is not filled', () => { + const name: HTMLInputElement = compiled.querySelector( + '.form-name' + ) as HTMLInputElement; + ['', ' '].forEach(value => { + name.value = value; + name.dispatchEvent(new Event('input')); + component.nameControl.markAsTouched(); + fixture.detectChanges(); - it(`should have form field with specific type"`, () => { - const fields = compiled.querySelectorAll('.profile-form-field'); + const nameError = compiled.querySelector('mat-error')?.innerHTML; + const error = component.nameControl.hasError('required'); - if (item.type === FormControlType.SELECT) { - const select = fields[uiIndex].querySelector('mat-select'); - expect(select).toBeTruthy(); - } else if (item.type === FormControlType.SELECT_MULTIPLE) { - const select = fields[uiIndex].querySelector('mat-checkbox'); - expect(select).toBeTruthy(); - } else if (item.type === FormControlType.TEXTAREA) { - const input = fields[uiIndex]?.querySelector('textarea'); - expect(input).toBeTruthy(); - } else { - const input = fields[uiIndex]?.querySelector('input'); - expect(input).toBeTruthy(); - } + expect(error).toBeTruthy(); + expect(nameError).toContain('The Profile name is required'); }); + }); - it('should have label', () => { - const labels = compiled.querySelectorAll('.field-label'); - const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i + it('should have different profile name error when profile with name is exist', () => { + const name: HTMLInputElement = compiled.querySelector( + '.form-name' + ) as HTMLInputElement; + name.value = 'Primary profile'; + name.dispatchEvent(new Event('input')); + component.nameControl.markAsTouched(); - const label = item?.validation?.required - ? item.question + ' *' - : item.question; - expect(labels[uiIndex].textContent?.trim()).toEqual(label); - }); + fixture.detectChanges(); - it('should have hint', () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i - const hint = fields[uiIndex].querySelector('mat-hint'); + const nameError = compiled.querySelector('mat-error')?.innerHTML; + const error = component.nameControl.hasError('has_same_profile_name'); - if (item.description) { - expect(hint?.textContent?.trim()).toEqual(item.description); - } else { - expect(hint).toBeNull(); - } - }); + expect(error).toBeTruthy(); + expect(nameError).toContain( + 'This Profile name is already used for another Risk Assessment profile' + ); + }); + }); + + PROFILE_FORM.forEach((item, index) => { + const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i + + it(`should have form field with specific type"`, () => { + const fields = compiled.querySelectorAll('.profile-form-field'); if (item.type === FormControlType.SELECT) { - describe('select', () => { - it(`should have default value if provided`, () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const select = fields[uiIndex].querySelector('mat-select'); - expect(select?.textContent?.trim()).toEqual(item.default || ''); - }); + const select = fields[uiIndex].querySelector('mat-select'); + expect(select).toBeTruthy(); + } else if (item.type === FormControlType.SELECT_MULTIPLE) { + const select = fields[uiIndex].querySelector('mat-checkbox'); + expect(select).toBeTruthy(); + } else if (item.type === FormControlType.TEXTAREA) { + const input = fields[uiIndex]?.querySelector('textarea'); + expect(input).toBeTruthy(); + } else { + const input = fields[uiIndex]?.querySelector('input'); + expect(input).toBeTruthy(); + } + }); - it('should have "required" error when field is not filled', () => { - const fields = compiled.querySelectorAll('.profile-form-field'); + it('should have label', () => { + const labels = compiled.querySelectorAll('.field-label'); + const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i - component.getControl(index).setValue(''); - component.getControl(index).markAsTouched(); + const label = item?.validation?.required + ? item.question + ' *' + : item.question; + expect(labels[uiIndex].textContent?.trim()).toEqual(label); + }); - fixture.detectChanges(); + it('should have hint', () => { + const fields = compiled.querySelectorAll('.profile-form-field'); + const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i + const hint = fields[uiIndex].querySelector('mat-hint'); - const error = - fields[uiIndex].querySelector('mat-error')?.innerHTML; + if (item.description) { + expect(hint?.textContent?.trim()).toEqual(item.description); + } else { + expect(hint).toBeNull(); + } + }); - expect(error).toContain('The field is required'); - }); + if (item.type === FormControlType.SELECT) { + describe('select', () => { + it(`should have default value if provided`, () => { + const fields = compiled.querySelectorAll('.profile-form-field'); + const select = fields[uiIndex].querySelector('mat-select'); + expect(select?.textContent?.trim()).toEqual(item.default || ''); }); - } - if (item.type === FormControlType.SELECT_MULTIPLE) { - describe('select multiple', () => { - it(`should mark form group as dirty while tab navigation`, () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const checkbox = fields[uiIndex].querySelector( - '.field-select-checkbox:last-of-type mat-checkbox' - ); - checkbox?.dispatchEvent( - new KeyboardEvent('keydown', { key: 'Tab' }) - ); - fixture.detectChanges(); + it('should have "required" error when field is not filled', () => { + const fields = compiled.querySelectorAll('.profile-form-field'); - expect(component.getControl(index).dirty).toBeTrue(); - }); + component.getControl(index).setValue(''); + component.getControl(index).markAsTouched(); + + fixture.detectChanges(); + + const error = fields[uiIndex].querySelector('mat-error')?.innerHTML; + + expect(error).toContain('The field is required'); }); - } + }); + } + + if (item.type === FormControlType.SELECT_MULTIPLE) { + describe('select multiple', () => { + it(`should mark form group as dirty while tab navigation`, () => { + const fields = compiled.querySelectorAll('.profile-form-field'); + const checkbox = fields[uiIndex].querySelector( + '.field-select-checkbox:last-of-type mat-checkbox' + ); + checkbox?.dispatchEvent( + new KeyboardEvent('keydown', { key: 'Tab' }) + ); + fixture.detectChanges(); - if ( - item.type === FormControlType.TEXT || - item.type === FormControlType.TEXTAREA || - item.type === FormControlType.EMAIL_MULTIPLE - ) { - describe('text or text-long or email-multiple', () => { - if (item.validation?.required) { - it('should have "required" error when field is not filled', () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i - const input = fields[uiIndex].querySelector( - '.mat-mdc-input-element' - ) as HTMLInputElement; - ['', ' '].forEach(value => { - input.value = value; - input.dispatchEvent(new Event('input')); - component.getControl(index).markAsTouched(); - fixture.detectChanges(); - const errors = fields[uiIndex].querySelectorAll('mat-error'); - let hasError = false; - errors.forEach(error => { - if (error.textContent === 'The field is required') { - hasError = true; - } - }); - - expect(hasError).toBeTrue(); + expect(component.getControl(index).dirty).toBeTrue(); + }); + }); + } + + if ( + item.type === FormControlType.TEXT || + item.type === FormControlType.TEXTAREA || + item.type === FormControlType.EMAIL_MULTIPLE + ) { + describe('text or text-long or email-multiple', () => { + if (item.validation?.required) { + it('should have "required" error when field is not filled', () => { + const fields = compiled.querySelectorAll('.profile-form-field'); + const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i + const input = fields[uiIndex].querySelector( + '.mat-mdc-input-element' + ) as HTMLInputElement; + ['', ' '].forEach(value => { + input.value = value; + input.dispatchEvent(new Event('input')); + component.getControl(index).markAsTouched(); + fixture.detectChanges(); + const errors = fields[uiIndex].querySelectorAll('mat-error'); + let hasError = false; + errors.forEach(error => { + if (error.textContent === 'The field is required') { + hasError = true; + } }); + + expect(hasError).toBeTrue(); }); - } + }); + } + + it('should have "invalid_format" error when field does not satisfy validation rules', () => { + const fields = compiled.querySelectorAll('.profile-form-field'); + const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i + const input: HTMLInputElement = fields[uiIndex].querySelector( + '.mat-mdc-input-element' + ) as HTMLInputElement; + input.value = 'as\\\\\\\\\\""""""""'; + input.dispatchEvent(new Event('input')); + component.getControl(index).markAsTouched(); + fixture.detectChanges(); + const result = + item.type === FormControlType.EMAIL_MULTIPLE + ? 'Please, check the email address. Valid e-mail can contain only latin letters, numbers, @ and . (dot).' + : 'Please, check. “ and \\ are not allowed.'; + const errors = fields[uiIndex].querySelectorAll('mat-error'); + let hasError = false; + errors.forEach(error => { + if (error.textContent === result) { + hasError = true; + } + }); - it('should have "invalid_format" error when field does not satisfy validation rules', () => { + expect(hasError).toBeTrue(); + }); + + if (item.validation?.max) { + it('should have "maxlength" error when field is exceeding max length', () => { const fields = compiled.querySelectorAll('.profile-form-field'); const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i const input: HTMLInputElement = fields[uiIndex].querySelector( '.mat-mdc-input-element' ) as HTMLInputElement; - input.value = 'as\\\\\\\\\\""""""""'; + input.value = + 'very long value very long value very long value very long value very long value very long value very long value very long value very long value very long value'; input.dispatchEvent(new Event('input')); component.getControl(index).markAsTouched(); fixture.detectChanges(); - const result = - item.type === FormControlType.EMAIL_MULTIPLE - ? 'Please, check the email address. Valid e-mail can contain only latin letters, numbers, @ and . (dot).' - : 'Please, check. “ and \\ are not allowed.'; + const errors = fields[uiIndex].querySelectorAll('mat-error'); let hasError = false; errors.forEach(error => { - if (error.textContent === result) { + if ( + error.textContent === + `The field must be a maximum of ${item.validation?.max} characters.` + ) { hasError = true; } }); - expect(hasError).toBeTrue(); }); - - if (item.validation?.max) { - it('should have "maxlength" error when field is exceeding max length', () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i - const input: HTMLInputElement = fields[uiIndex].querySelector( - '.mat-mdc-input-element' - ) as HTMLInputElement; - input.value = - 'very long value very long value very long value very long value very long value very long value very long value very long value very long value very long value'; - input.dispatchEvent(new Event('input')); - component.getControl(index).markAsTouched(); - fixture.detectChanges(); - - const errors = fields[uiIndex].querySelectorAll('mat-error'); - let hasError = false; - errors.forEach(error => { - if ( - error.textContent === - `The field must be a maximum of ${item.validation?.max} characters.` - ) { - hasError = true; - } - }); - expect(hasError).toBeTrue(); - }); - } - }); - } - }); - - describe('Draft button', () => { - it('should be disabled when profile name is empty', () => { - component.nameControl.setValue(''); - fixture.detectChanges(); - const draftButton = compiled.querySelector( - '.save-draft-button' - ) as HTMLButtonElement; - - expect(draftButton.disabled).toBeTrue(); + } }); + } + }); - it('should be disabled when profile name is not empty but other fields in wrong format', () => { - component.nameControl.setValue('New profile'); - component.getControl('0').setValue('test'); - fixture.detectChanges(); - const draftButton = compiled.querySelector( - '.save-draft-button' - ) as HTMLButtonElement; - - expect(draftButton.disabled).toBeTrue(); - }); + describe('Draft button', () => { + it('should be disabled when profile name is empty', () => { + component.nameControl.setValue(''); + fixture.detectChanges(); + const draftButton = compiled.querySelector( + '.save-draft-button' + ) as HTMLButtonElement; - it('should be enabled when profile name is not empty; other fields are empty or in correct format', () => { - component.nameControl.setValue('New profile'); - component.getControl('0').setValue('a@test.te;b@test.te, c@test.te'); - fixture.detectChanges(); - const draftButton = compiled.querySelector( - '.save-draft-button' - ) as HTMLButtonElement; + expect(draftButton.disabled).toBeTrue(); + }); - expect(draftButton.disabled).toBeFalse(); - }); + it('should be disabled when profile name is not empty but other fields in wrong format', () => { + component.nameControl.setValue('New profile'); + component.getControl('0').setValue('test'); + fixture.detectChanges(); + const draftButton = compiled.querySelector( + '.save-draft-button' + ) as HTMLButtonElement; - it('should emit new profile in draft status', () => { - component.nameControl.setValue('New profile'); - fixture.detectChanges(); - const emitSpy = spyOn(component.saveProfile, 'emit'); - const draftButton = compiled.querySelector( - '.save-draft-button' - ) as HTMLButtonElement; - draftButton.click(); - - expect(emitSpy).toHaveBeenCalledWith({ - ...NEW_PROFILE_MOCK_DRAFT, - }); - }); + expect(draftButton.disabled).toBeTrue(); }); - describe('Save button', () => { - beforeEach(() => { - fillForm(component); - fixture.detectChanges(); - }); - - it('should be enabled when required fields are present', () => { - const saveButton = compiled.querySelector( - '.save-profile-button' - ) as HTMLButtonElement; + it('should be enabled when profile name is not empty; other fields are empty or in correct format', () => { + component.nameControl.setValue('New profile'); + component.getControl('0').setValue('a@test.te;b@test.te, c@test.te'); + fixture.detectChanges(); + const draftButton = compiled.querySelector( + '.save-draft-button' + ) as HTMLButtonElement; - expect(saveButton.disabled).toBeFalse(); - }); + expect(draftButton.disabled).toBeFalse(); + }); - it('should emit new profile', () => { - const emitSpy = spyOn(component.saveProfile, 'emit'); - const saveButton = compiled.querySelector( - '.save-profile-button' - ) as HTMLButtonElement; - saveButton.click(); + it('should emit new profile in draft status', () => { + component.nameControl.setValue('New profile'); + fixture.detectChanges(); + const emitSpy = spyOn(component.saveProfile, 'emit'); + const draftButton = compiled.querySelector( + '.save-draft-button' + ) as HTMLButtonElement; + draftButton.click(); - expect(emitSpy).toHaveBeenCalledWith({ - ...NEW_PROFILE_MOCK, - }); + expect(emitSpy).toHaveBeenCalledWith({ + ...NEW_PROFILE_MOCK_DRAFT, }); }); + }); - describe('Discard button', () => { - beforeEach(() => { - fillForm(component); - fixture.detectChanges(); - }); - - it('should has Discard text', () => { - const discardButton = compiled.querySelector( - '.discard-button' - ) as HTMLButtonElement; - - expect(discardButton.textContent?.trim()).toEqual('Discard'); - }); + describe('Save button', () => { + beforeEach(() => { + fillForm(component); + fixture.detectChanges(); + }); - it('should be enabled when form is filled', () => { - const discardButton = compiled.querySelector( - '.discard-button' - ) as HTMLButtonElement; + it('should be enabled when required fields are present', () => { + const saveButton = compiled.querySelector( + '.save-profile-button' + ) as HTMLButtonElement; - expect(discardButton.disabled).toBeFalse(); - }); + expect(saveButton.disabled).toBeFalse(); + }); - it('should emit discard', () => { - const emitSpy = spyOn(component.discard, 'emit'); - const discardButton = compiled.querySelector( - '.discard-button' - ) as HTMLButtonElement; - discardButton.click(); + it('should emit new profile', () => { + const emitSpy = spyOn(component.saveProfile, 'emit'); + const saveButton = compiled.querySelector( + '.save-profile-button' + ) as HTMLButtonElement; + saveButton.click(); - expect(emitSpy).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalledWith({ + ...NEW_PROFILE_MOCK, }); }); }); - describe('with expire profile', () => { + describe('Discard button', () => { beforeEach(() => { - component.selectedProfile = Object.assign({}, PROFILE_MOCK, { - status: ProfileStatus.EXPIRED, - }); + fillForm(component); fixture.detectChanges(); }); - it('should have disabled fields', () => { - const fields = compiled.querySelectorAll('mat-form-field'); - fields.forEach(field => { - expect( - field.classList.contains('mat-form-field-disabled') - ).toBeTrue(); - }); - }); - - describe('Close button', () => { - it('should has Discard text', () => { - const discardButton = compiled.querySelector( - '.discard-button' - ) as HTMLButtonElement; - - expect(discardButton.textContent?.trim()).toEqual('Close'); - }); + it('should be enabled when form is filled', () => { + const discardButton = compiled.querySelector( + '.discard-button' + ) as HTMLButtonElement; - it('should be enabled', () => { - const discardButton = compiled.querySelector( - '.discard-button' - ) as HTMLButtonElement; - - expect(discardButton.disabled).toBeFalse(); - }); + expect(discardButton.disabled).toBeFalse(); + }); - it('should emit discard', () => { - const emitSpy = spyOn(component.discard, 'emit'); - const discardButton = compiled.querySelector( - '.discard-button' - ) as HTMLButtonElement; - discardButton.click(); + it('should emit discard', () => { + const emitSpy = spyOn(component.discard, 'emit'); + const discardButton = compiled.querySelector( + '.discard-button' + ) as HTMLButtonElement; + discardButton.click(); - expect(emitSpy).toHaveBeenCalled(); - }); + expect(emitSpy).toHaveBeenCalled(); }); }); }); @@ -482,7 +425,6 @@ describe('ProfileFormComponent', () => { it('save profile should have rename field', () => { const emitSpy = spyOn(component.saveProfile, 'emit'); fillForm(component); - fixture.detectChanges(); component.onSaveClick(ProfileStatus.VALID); expect(emitSpy).toHaveBeenCalledWith(RENAME_PROFILE_MOCK); diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts index 7ac96fd18..684bc67da 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts @@ -46,6 +46,7 @@ import { DeviceValidators } from '../../devices/components/device-form/device.va import { FormControlType, Profile, + ProfileFormat, ProfileStatus, Question, Validation, @@ -80,8 +81,7 @@ export class ProfileFormComponent implements OnInit { profileForm: FormGroup = this.fb.group({}); @ViewChildren(CdkTextareaAutosize) autosize!: QueryList; - questionnaire!: Question[]; - @Input() profileFormat!: Question[]; + @Input() profileFormat!: ProfileFormat[]; @Input() set profiles(profiles: Profile[]) { this.profileList = profiles; @@ -95,8 +95,9 @@ export class ProfileFormComponent implements OnInit { @Input() set selectedProfile(profile: Profile | null) { this.profile = profile; - if (profile && this.questionnaire) { - this.updateForm(profile); + if (profile && this.nameControl) { + this.updateNameValidator(); + this.fillProfileForm(this.profileFormat, profile); } } get selectedProfile() { @@ -111,22 +112,9 @@ export class ProfileFormComponent implements OnInit { private fb: FormBuilder ) {} ngOnInit() { + this.profileForm = this.createProfileForm(this.profileFormat); if (this.selectedProfile) { - this.updateForm(this.selectedProfile); - } else { - this.questionnaire = this.profileFormat; - this.profileForm = this.createProfileForm(this.questionnaire); - } - } - - updateForm(profile: Profile) { - this.questionnaire = profile.questions; - this.profileForm = this.createProfileForm(this.questionnaire); - this.fillProfileForm(profile); - if (profile.status === ProfileStatus.EXPIRED) { - this.profileForm.disable(); - } else { - this.profileForm.enable(); + this.fillProfileForm(this.profileFormat, this.selectedProfile); } } @@ -135,7 +123,7 @@ export class ProfileFormComponent implements OnInit { } private get fieldsHasError(): boolean { - return this.questionnaire.some((field, index) => { + return this.profileFormat.some((field, index) => { return ( this.getControl(index).hasError('invalid_format') || this.getControl(index).hasError('maxlength') @@ -151,7 +139,7 @@ export class ProfileFormComponent implements OnInit { return this.profileForm.get(name.toString()) as AbstractControl; } - createProfileForm(questions: Question[]): FormGroup { + createProfileForm(questions: ProfileFormat[]): FormGroup { // eslint-disable-next-line @typescript-eslint/no-explicit-any const group: any = {}; @@ -171,7 +159,7 @@ export class ProfileFormComponent implements OnInit { group[index] = this.getMultiSelectGroup(question); } else { const validators = this.getValidators( - question.type!, + question.type, question.validation ); group[index] = new FormControl(question.default || '', validators); @@ -199,7 +187,7 @@ export class ProfileFormComponent implements OnInit { return validators; } - getMultiSelectGroup(question: Question): FormGroup { + getMultiSelectGroup(question: ProfileFormat): FormGroup { // eslint-disable-next-line @typescript-eslint/no-explicit-any const group: any = {}; question.options?.forEach((option, index) => { @@ -216,9 +204,9 @@ export class ProfileFormComponent implements OnInit { return this.profileForm?.controls[name] as FormGroup; } - fillProfileForm(profile: Profile): void { + fillProfileForm(profileFormat: ProfileFormat[], profile: Profile): void { this.nameControl.setValue(profile.name); - profile.questions.forEach((question, index) => { + profileFormat.forEach((question, index) => { if (question.type === FormControlType.SELECT_MULTIPLE) { question.options?.forEach((item, idx) => { if ((profile.questions[index].answer as number[])?.includes(idx)) { @@ -260,7 +248,7 @@ export class ProfileFormComponent implements OnInit { } private buildResponseFromForm( - initialQuestions: Question[], + initialQuestions: ProfileFormat[], profileForm: FormGroup, status: ProfileStatus, profile: Profile | null @@ -278,9 +266,8 @@ export class ProfileFormComponent implements OnInit { const questions: Question[] = []; initialQuestions.forEach((initialQuestion, index) => { - const question: Question = { - question: initialQuestion.question, - }; + const question: Question = {}; + question.question = initialQuestion.question; if (initialQuestion.type === FormControlType.SELECT_MULTIPLE) { const answer: number[] = []; diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html index 0a72e6f70..c5e38e360 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html @@ -47,7 +47,7 @@

Saved profiles

class="profile-item-{{ i }}" [ngClass]="{ selected: profile.name === vm.selectedProfile?.name }" (deleteButtonClicked)="deleteProfile($event, i, vm.selectedProfile)" - (profileClicked)="openForm($event)" + (profileClicked)="profileClicked($event)" (copyProfileClicked)="copyProfileAndOpenForm($event)"> diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.spec.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.spec.ts index dbb359121..8e792ff83 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.spec.ts +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.spec.ts @@ -34,7 +34,7 @@ import { } from '../../mocks/profile.mock'; import { of } from 'rxjs'; import { Component, Input } from '@angular/core'; -import { Profile, Question } from '../../model/profile'; +import { Profile, ProfileFormat } from '../../model/profile'; import { MatDialogRef } from '@angular/material/dialog'; import { SimpleDialogComponent } from '../../components/simple-dialog/simple-dialog.component'; import { RiskAssessmentStore } from './risk-assessment.store'; @@ -372,5 +372,5 @@ class FakeProfileItemComponent { class FakeProfileFormComponent { @Input() profiles!: Profile[]; @Input() selectedProfile!: Profile; - @Input() profileFormat!: Question[]; + @Input() profileFormat!: ProfileFormat[]; } diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts index 0b895f693..dd3d33d9d 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts @@ -54,6 +54,12 @@ export class RiskAssessmentComponent implements OnInit, OnDestroy { this.destroy$.unsubscribe(); } + async profileClicked(profile: Profile | null = null) { + if (profile === null || profile.status !== ProfileStatus.EXPIRED) { + await this.openForm(profile); + } + } + async openForm(profile: Profile | null = null) { this.isOpenProfileForm = true; this.store.updateSelectedProfile(profile); diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.ts index 8dc9cce04..93e89b434 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.ts +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.ts @@ -19,7 +19,7 @@ import { ComponentStore } from '@ngrx/component-store'; import { tap, withLatestFrom } from 'rxjs/operators'; import { delay, exhaustMap } from 'rxjs'; import { TestRunService } from '../../services/test-run.service'; -import { Profile, Question } from '../../model/profile'; +import { Profile, ProfileFormat } from '../../model/profile'; import { FocusManagerService } from '../../services/focus-manager.service'; import { Store } from '@ngrx/store'; import { AppState } from '../../store/state'; @@ -29,7 +29,7 @@ import { fetchRiskProfiles, setRiskProfiles } from '../../store/actions'; export interface AppComponentState { selectedProfile: Profile | null; profiles: Profile[]; - profileFormat: Question[]; + profileFormat: ProfileFormat[]; } @Injectable() export class RiskAssessmentStore extends ComponentStore { @@ -43,10 +43,12 @@ export class RiskAssessmentStore extends ComponentStore { selectedProfile: this.selectedProfile$, }); - updateProfileFormat = this.updater((state, profileFormat: Question[]) => ({ - ...state, - profileFormat, - })); + updateProfileFormat = this.updater( + (state, profileFormat: ProfileFormat[]) => ({ + ...state, + profileFormat, + }) + ); updateSelectedProfile = this.updater( (state, selectedProfile: Profile | null) => ({ ...state, @@ -121,7 +123,7 @@ export class RiskAssessmentStore extends ComponentStore { return trigger$.pipe( exhaustMap(() => { return this.testRunService.fetchProfilesFormat().pipe( - tap((profileFormat: Question[]) => { + tap((profileFormat: ProfileFormat[]) => { this.updateProfileFormat(profileFormat); }) ); diff --git a/modules/ui/src/app/services/test-run.service.ts b/modules/ui/src/app/services/test-run.service.ts index 9eaf7444b..0ac2b1510 100644 --- a/modules/ui/src/app/services/test-run.service.ts +++ b/modules/ui/src/app/services/test-run.service.ts @@ -30,7 +30,7 @@ import { Version } from '../model/version'; import { Certificate } from '../model/certificate'; import { Profile, - Question, + ProfileFormat, ProfileRisk, RiskResultClassName, } from '../model/profile'; @@ -303,8 +303,8 @@ export class TestRunService { }); } - fetchProfilesFormat(): Observable { - return this.http.get(`${API_URL}/profiles/format`); + fetchProfilesFormat(): Observable { + return this.http.get(`${API_URL}/profiles/format`); } saveProfile(profile: Profile): Observable {