Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions modules/ui/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const routes: Routes = [
{
path: 'risk-assessment',
component: RiskAssessmentComponent,
canDeactivate: [CanDeactivateGuard],
title: 'Testrun - Risk Assessment',
},
{
Expand Down
11 changes: 7 additions & 4 deletions modules/ui/src/app/mocks/profile.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const PROFILE_MOCK_3: Profile = {

export const PROFILE_FORM: ProfileFormat[] = [
{
question: 'Email',
question: 'What is the email of the device owner(s)?',
type: FormControlType.EMAIL_MULTIPLE,
validation: {
required: true,
Expand Down Expand Up @@ -108,7 +108,10 @@ export const NEW_PROFILE_MOCK = {
status: ProfileStatus.VALID,
name: 'New profile',
questions: [
{ question: 'Email', answer: 'a@test.te;b@test.te, c@test.te' },
{
question: 'What is the email of the device owner(s)?',
answer: 'a@test.te;b@test.te, c@test.te',
},
{
question: 'What type of device do you need reviewed?',
answer: 'test',
Expand All @@ -129,7 +132,7 @@ export const NEW_PROFILE_MOCK_DRAFT = {
status: ProfileStatus.DRAFT,
name: 'New profile',
questions: [
{ question: 'Email', answer: '' },
{ question: 'What is the email of the device owner(s)?', answer: '' },
{
question: 'What type of device do you need reviewed?',
answer: '',
Expand Down Expand Up @@ -217,7 +220,7 @@ export const OUTDATED_DRAFT_PROFILE_MOCK: Profile = {
},
{
question: 'What is the email of the device owner(s)?',
answer: 'boddey@google.com, cmeredith@google.com',
answer: '',
},
{
question: 'What type of device do you need reviewed?',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';

import { ProfileFormComponent } from './profile-form.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
Expand All @@ -30,15 +30,31 @@ import {
RENAME_PROFILE_MOCK,
} from '../../../mocks/profile.mock';
import { ProfileStatus } from '../../../model/profile';
import { RiskAssessmentStore } from '../risk-assessment.store';
import { TestRunService } from '../../../services/test-run.service';
import { provideMockStore } from '@ngrx/store/testing';
import { of } from 'rxjs';
import { MatDialogRef } from '@angular/material/dialog';
import { SimpleDialogComponent } from '../../../components/simple-dialog/simple-dialog.component';

describe('ProfileFormComponent', () => {
let component: ProfileFormComponent;
let fixture: ComponentFixture<ProfileFormComponent>;
let compiled: HTMLElement;
const testrunServiceMock: jasmine.SpyObj<TestRunService> =
jasmine.createSpyObj('testrunServiceMock', [
'fetchQuestionnaireFormat',
'saveDevice',
]);

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProfileFormComponent, BrowserAnimationsModule],
providers: [
RiskAssessmentStore,
{ provide: TestRunService, useValue: testrunServiceMock },
provideMockStore({}),
],
}).compileComponents();

fixture = TestBed.createComponent(ProfileFormComponent);
Expand Down Expand Up @@ -312,14 +328,15 @@ describe('ProfileFormComponent', () => {
).toBeTrue();
});

it('should have an error when uses the name of copy profile', () => {
it('should have an error when uses the name of copy profile', fakeAsync(() => {
component.selectedProfile = DRAFT_COPY_PROFILE_MOCK;
component.profiles = [PROFILE_MOCK, PROFILE_MOCK_2, COPY_PROFILE_MOCK];
fixture.detectChanges();

expect(
component.nameControl.hasError('has_same_profile_name')
).toBeTrue();
});
}));
});

describe('with no profile', () => {
Expand All @@ -336,6 +353,31 @@ describe('ProfileFormComponent', () => {
expect(emitSpy).toHaveBeenCalledWith(NEW_PROFILE_MOCK);
});
});

describe('openCloseDialog', () => {
it('should open discard modal', fakeAsync(() => {
const openSpy = spyOn(component.dialog, 'open').and.returnValue({
afterClosed: () => of(true),
} as MatDialogRef<typeof SimpleDialogComponent>);

component.openCloseDialog();

expect(openSpy).toHaveBeenCalledWith(SimpleDialogComponent, {
ariaLabel: 'Discard the Risk Assessment changes',
data: {
title: 'Discard changes?',
content: `You have unsaved changes that would be permanently lost.`,
confirmName: 'Discard',
},
autoFocus: true,
hasBackdrop: true,
disableClose: true,
panelClass: ['simple-dialog', 'discard-dialog'],
});

openSpy.calls.reset();
}));
});
});

function fillForm(component: ProfileFormComponent) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ import { FormControlType } from '../../../model/question';
import { ProfileValidators } from './profile.validators';
import { DynamicFormComponent } from '../../../components/dynamic-form/dynamic-form.component';
import { CdkTrapFocus } from '@angular/cdk/a11y';
import { Observable } from 'rxjs/internal/Observable';
import { of } from 'rxjs/internal/observable/of';
import { map } from 'rxjs/internal/operators/map';
import { SimpleDialogComponent } from '../../../components/simple-dialog/simple-dialog.component';
import { MatDialog } from '@angular/material/dialog';
import { RiskAssessmentStore } from '../risk-assessment.store';

@Component({
selector: 'app-profile-form',
Expand All @@ -75,21 +81,23 @@ import { CdkTrapFocus } from '@angular/cdk/a11y';
export class ProfileFormComponent implements OnInit, AfterViewInit {
private profileValidators = inject(ProfileValidators);
private fb = inject(FormBuilder);

private store = inject(RiskAssessmentStore);
private profile: Profile | null = null;
private profileList!: Profile[];
private injector = inject(Injector);
private nameValidator!: ValidatorFn;
private changeProfile = true;
public readonly ProfileStatus = ProfileStatus;
profileForm: FormGroup = this.fb.group({});
dialog = inject(MatDialog);
readonly autosize = viewChildren(CdkTextareaAutosize);
@Input() profileFormat!: ProfileFormat[];
@Input() isCopyProfile!: boolean;
@Input()
set profiles(profiles: Profile[]) {
this.profileList = profiles;
if (this.nameControl) {
this.updateNameValidator();
if (this.nameControl && this.profile) {
this.updateNameValidator(this.profile);
}
}
get profiles() {
Expand All @@ -100,12 +108,20 @@ export class ProfileFormComponent implements OnInit, AfterViewInit {
if (this.isCopyProfile && this.profile) {
this.deleteCopy.emit(this.profile);
}
this.profile = profile;
if (profile && this.nameControl) {
this.updateNameValidator();
this.fillProfileForm(this.profileFormat, profile);
if (this.changeProfile || this.profileHasNoChanges()) {
this.changeProfile = false;
this.profile = profile;
if (profile && this.nameControl) {
this.updateNameValidator(profile);
this.fillProfileForm(this.profileFormat, profile);
}
} else if (this.profile != profile) {
// prevent select profile before user confirmation
this.store.updateSelectedProfile(this.profile);
this.openCloseDialogToChangeProfile(profile);
}
}

get selectedProfile() {
return this.profile;
}
Expand Down Expand Up @@ -277,6 +293,7 @@ export class ProfileFormComponent implements OnInit, AfterViewInit {
onSaveClick(status: ProfileStatus) {
const response = this.buildResponseFromForm(status, this.selectedProfile);
this.saveProfile.emit(response);
this.changeProfile = true;
}

onDiscardClick() {
Expand All @@ -291,6 +308,39 @@ export class ProfileFormComponent implements OnInit, AfterViewInit {
this.copyProfile.emit(this.selectedProfile!);
}

close(): Observable<boolean> {
if (this.profileHasNoChanges()) {
return of(true);
}
return this.openCloseDialog().pipe(map(res => !!res));
}

openCloseDialog() {
const dialogRef = this.dialog.open(SimpleDialogComponent, {
ariaLabel: 'Discard the Risk Assessment changes',
data: {
title: 'Discard changes?',
content: `You have unsaved changes that would be permanently lost.`,
confirmName: 'Discard',
},
autoFocus: true,
hasBackdrop: true,
disableClose: true,
panelClass: ['simple-dialog', 'discard-dialog'],
});

return dialogRef?.afterClosed();
}

private openCloseDialogToChangeProfile(profile: Profile | null) {
this.openCloseDialog().subscribe(close => {
if (close) {
this.changeProfile = true;
this.store.updateSelectedProfile(profile);
}
});
}

private buildResponseFromForm(
status: ProfileStatus | '',
profile: Profile | null
Expand All @@ -301,13 +351,13 @@ export class ProfileFormComponent implements OnInit, AfterViewInit {
};
if (profile && !this.isCopyProfile) {
request.name = profile.name;
request.rename = this.nameControl.value?.trim();
request.rename = this.nameControl?.value?.trim();
} else {
request.name = this.nameControl.value?.trim();
request.name = this.nameControl?.value?.trim();
}
const questions: Question[] = [];

this.profileFormat.forEach((initialQuestion, index) => {
this.profileFormat?.forEach((initialQuestion, index) => {
const question: Question = {};
question.question = initialQuestion.question;

Expand Down Expand Up @@ -342,11 +392,11 @@ export class ProfileFormComponent implements OnInit, AfterViewInit {
);
}

private updateNameValidator() {
private updateNameValidator(profile: Profile) {
this.nameControl.removeValidators([this.nameValidator]);
this.nameValidator = this.profileValidators.differentProfileName(
this.profileList,
this.profile
profile
);
this.nameControl.addValidators(this.nameValidator);
this.nameControl.updateValueAndValidity();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
message="Select a profile from the list on the left to view or edit the profile.">
</app-no-entity-selected>
<app-profile-form
#profileFormComponent
*ngIf="isOpenProfileForm"
[selectedProfile]="vm.selectedProfile"
[isCopyProfile]="isCopyProfile"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,16 @@ import {
PROFILE_MOCK,
} from '../../mocks/profile.mock';
import { of, Subscription } from 'rxjs';
import { Component, Input } from '@angular/core';
import { Component, Input, ViewEncapsulation } from '@angular/core';
import { Profile, ProfileAction, 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';
import { LiveAnnouncer } from '@angular/cdk/a11y';
import { Observable } from 'rxjs/internal/Observable';
import { ProfileFormComponent } from './profile-form/profile-form.component';
import { MatIcon } from '@angular/material/icon';
import { MatIconTestingModule } from '@angular/material/icon/testing';

describe('RiskAssessmentComponent', () => {
let component: RiskAssessmentComponent;
Expand Down Expand Up @@ -76,13 +79,19 @@ describe('RiskAssessmentComponent', () => {
MatToolbarModule,
MatSidenavModule,
BrowserAnimationsModule,
MatIconTestingModule,
MatIcon,
],
providers: [
{ provide: TestRunService, useValue: mockService },
{ provide: RiskAssessmentStore, useValue: mockRiskAssessmentStore },
{ provide: LiveAnnouncer, useValue: mockLiveAnnouncer },
],
}).compileComponents();
})
.overrideComponent(RiskAssessmentComponent, {
set: { encapsulation: ViewEncapsulation.None },
})
.compileComponents();

TestBed.overrideProvider(RiskAssessmentStore, {
useValue: mockRiskAssessmentStore,
Expand Down Expand Up @@ -388,35 +397,29 @@ describe('RiskAssessmentComponent', () => {
});

describe('#discard', () => {
it('should open discard modal', fakeAsync(() => {
const openSpy = spyOn(component.dialog, 'open').and.returnValue({
afterClosed: () => of(true),
} as MatDialogRef<typeof SimpleDialogComponent>);
beforeEach(async () => {
await component.openForm();
});

it('should call openCloseDialog', () => {
const openCloseDialogSpy = spyOn(
<ProfileFormComponent>component.form(),
'openCloseDialog'
).and.returnValue(of(true));

component.discard(null);

expect(openSpy).toHaveBeenCalledWith(SimpleDialogComponent, {
ariaLabel: 'Discard the Risk Assessment changes',
data: {
title: 'Discard changes?',
content: `You have unsaved changes that would be permanently lost.`,
confirmName: 'Discard',
},
autoFocus: true,
hasBackdrop: true,
disableClose: true,
panelClass: ['simple-dialog', 'discard-dialog'],
});
expect(openCloseDialogSpy).toHaveBeenCalled();

openSpy.calls.reset();
}));
openCloseDialogSpy.calls.reset();
});

describe('with no selected profile', () => {
beforeEach(() => {
spyOn(component.dialog, 'open').and.returnValue({
afterClosed: () => of(true),
} as MatDialogRef<typeof SimpleDialogComponent>);

spyOn(
<ProfileFormComponent>component.form(),
'openCloseDialog'
).and.returnValue(of(true));
component.discard(null);
});

Expand All @@ -433,10 +436,10 @@ describe('RiskAssessmentComponent', () => {

describe('with selected profile', () => {
beforeEach(fakeAsync(() => {
spyOn(component.dialog, 'open').and.returnValue({
afterClosed: () => of(true),
} as MatDialogRef<typeof SimpleDialogComponent>);

spyOn(
<ProfileFormComponent>component.form(),
'openCloseDialog'
).and.returnValue(of(true));
component.discard(PROFILE_MOCK);
tick(100);
}));
Expand Down
Loading
Loading